test_data_rule_polars_execution.py 84 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234
  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. customer_path = tmp_path / "customers.parquet"
  661. segment_path = tmp_path / "segments.parquet"
  662. pl.DataFrame(customer_rows).write_parquet(customer_path)
  663. pl.DataFrame(segment_rows).write_parquet(segment_path)
  664. resolver = PostgresArtifactResolver(platform, store)
  665. customer_artifact = resolver.publish_path(
  666. str(customer_path),
  667. binding_id=input_binding["id"],
  668. binding_hash=compiled["plan"]["input_binding_hash"],
  669. correlation_id=correlation_id,
  670. kind="input",
  671. ttl_seconds=900,
  672. schema_fields=input_schema["fields"],
  673. limits=compiled["plan"]["resource_limits"],
  674. )
  675. segment_artifact = resolver.publish_path(
  676. str(segment_path),
  677. binding_id=lookup_binding["id"],
  678. binding_hash=lookup_operation["lookup_binding_hash"],
  679. correlation_id=correlation_id,
  680. kind="lookup",
  681. ttl_seconds=900,
  682. schema_fields=lookup_schema["fields"],
  683. limits=compiled["plan"]["resource_limits"],
  684. )
  685. node = {
  686. "id": "task5_real_polars",
  687. "type": "rule.apply",
  688. "purpose": "write",
  689. "idempotency": {
  690. "strategy": "deduplication_key",
  691. "key": "customer_id",
  692. },
  693. "config": {
  694. "component_binding_id": component_binding_id,
  695. "rule_version_id": rule["id"],
  696. "execution_plan_hash": compiled["plan_hash"],
  697. },
  698. }
  699. from app.core.mcp.gateway import SchedulingGateway
  700. from app.core.mcp.identity import AgentIdentity
  701. from app.core.mcp.persistence import PostgresSchedulingPlanStore
  702. from app.runner.api import create_runner_app
  703. from app.runner.auth import TaskTokenIssuer, TaskTokenVerifier
  704. from app.runner.ledger import PostgresTaskLedger
  705. from app.runner.nodes import NodeRegistry
  706. from app.runner.rule_evidence import PostgresRuleEvidenceWriter
  707. class Audit:
  708. def __init__(self):
  709. self.events = []
  710. def record(self, event):
  711. self.events.append(event)
  712. class CanaryEngine:
  713. def __init__(self):
  714. self.calls = []
  715. def deploy_disabled(self, _definition):
  716. return {"revision": "task6-real-gateway"}
  717. def activate(self, namespace, flow_id):
  718. self.calls.append(("activate", namespace, flow_id))
  719. def execute(self, namespace, flow_id, inputs=None, **_options):
  720. self.calls.append(
  721. ("execute", namespace, flow_id, dict(inputs or {}))
  722. )
  723. return {"id": f"task6-canary-{correlation_id}"}
  724. def deactivate(self, namespace, flow_id):
  725. self.calls.append(("deactivate", namespace, flow_id))
  726. task_secret = "task5-real-http-secret-value-32-bytes"
  727. verifier = TaskTokenVerifier(task_secret)
  728. gateway_engine = CanaryEngine()
  729. gateway_store = PostgresSchedulingPlanStore(platform)
  730. gateway = SchedulingGateway(
  731. plans=gateway_store,
  732. audit=Audit(),
  733. engine=gateway_engine,
  734. token_issuer=TaskTokenIssuer(task_secret),
  735. )
  736. gateway_identity = AgentIdentity(
  737. subject="task6-real-scheduler",
  738. roles=frozenset({"scheduler"}),
  739. business_domains=frozenset({"sales"}),
  740. environments=frozenset({"test"}),
  741. correlation_id=correlation_id,
  742. )
  743. candidate = gateway.create_candidate_plan(
  744. gateway_identity,
  745. business_domain="sales",
  746. environment="test",
  747. workflow_spec={
  748. "schema_version": "1.0",
  749. "dataflow_uid": dataflow_uid,
  750. "name": "Task 6 governed rule canary",
  751. "nodes": [node],
  752. "edges": [],
  753. "parameters": {},
  754. },
  755. schedule_plan={
  756. "schema_version": "1.0",
  757. "timezone": "Asia/Shanghai",
  758. "triggers": [{"type": "manual"}],
  759. "max_concurrency": 1,
  760. "conflict_policy": "skip",
  761. "timeout_seconds": 600,
  762. "retry": {
  763. "max_attempts": 1,
  764. "delay_seconds": 1,
  765. },
  766. "backfill": {"max_days": 1, "max_runs": 1},
  767. },
  768. )
  769. gateway_candidate_id = candidate["candidate_id"]
  770. with platform.begin() as connection:
  771. connection.execute(
  772. text(
  773. """
  774. UPDATE public.dataflow_workflow_versions
  775. SET write_authorized = TRUE
  776. WHERE id = CAST(:id AS uuid)
  777. """
  778. ),
  779. {"id": gateway_candidate_id},
  780. )
  781. deployed = gateway.deploy_disabled_version(
  782. gateway_identity,
  783. candidate_id=gateway_candidate_id,
  784. business_domain="sales",
  785. environment="test",
  786. )
  787. assert deployed["deployment_id"] == deployment_id
  788. assert deployed["candidate_id"] != deployed["deployment_id"]
  789. gateway.run_canary(
  790. gateway_identity,
  791. candidate_id=gateway_candidate_id,
  792. business_domain="sales",
  793. environment="test",
  794. inputs={},
  795. )
  796. execution_call = next(
  797. call for call in gateway_engine.calls if call[0] == "execute"
  798. )
  799. task_token = execution_call[3]["dataops_task_tokens"][node["id"]]
  800. issued_claims = verifier.verify(task_token, node=node)
  801. assert issued_claims.deployment_id == deployment_id
  802. assert issued_claims.workflow_version == 1
  803. assert issued_claims.environment == "test"
  804. executor = RulePlanExecutor(
  805. PostgresRulePlanRepository(platform),
  806. adapters={
  807. "polars_batch": PolarsRulePlanAdapter(
  808. artifact_store=store,
  809. artifact_resolver=resolver,
  810. artifact_ttl_seconds=900,
  811. )
  812. },
  813. )
  814. result = executor.execute(
  815. node,
  816. {},
  817. write_authorized=True,
  818. correlation_id=correlation_id,
  819. )
  820. repeated = executor.execute(
  821. node,
  822. {},
  823. write_authorized=True,
  824. correlation_id=correlation_id,
  825. )
  826. assert result["rows_in"] == 4
  827. assert result["rows_out"] == 2
  828. assert result["rows_rejected"] == 1
  829. assert result["rows_deduplicated"] == 1
  830. assert result["rows_filtered"] == 0
  831. assert result["rows_join_dropped"] == 0
  832. assert result["rows_aggregated"] == 0
  833. assert result["violation_count"] == 1
  834. assert result["violations"] == [
  835. {"step_id": "valid_mobile", "count": 1}
  836. ]
  837. output = store.read(
  838. result["artifact_ref"],
  839. result["digest"],
  840. expected_schema_fields=output_schema["fields"],
  841. limits=compiled["plan"]["resource_limits"],
  842. ).collect()
  843. assert output.sort("customer_id").to_dicts() == [
  844. {
  845. "customer_id": 1,
  846. "mobile": "13800138000",
  847. "name": "Alice Updated",
  848. "segment_code": "A",
  849. "segment_name": "Gold",
  850. "version_no": 2,
  851. },
  852. {
  853. "customer_id": 3,
  854. "mobile": "13900139000",
  855. "name": "Carol",
  856. "segment_code": "C",
  857. "segment_name": "Silver",
  858. "version_no": 1,
  859. },
  860. ]
  861. assert all(
  862. item.object_name.startswith(prefix)
  863. for item in minio.list_objects(
  864. bucket, prefix=prefix, recursive=True
  865. )
  866. )
  867. assert repeated["rows_out"] == 2
  868. assert repeated["artifact_ref"] == result["artifact_ref"]
  869. assert "schema_fields" not in result
  870. ledger_jti = verifier.verify(task_token, node=node).jti
  871. retry_token = TaskTokenIssuer(task_secret).issue(
  872. task_uid=new_governance_uid(),
  873. dataflow_uid=dataflow_uid,
  874. deployment_id=deployment_id,
  875. environment="test",
  876. workflow_version=1,
  877. correlation_id=correlation_id,
  878. node=node,
  879. write_authorized=True,
  880. )
  881. retry_ledger_jti = verifier.verify(
  882. retry_token, node=node
  883. ).jti
  884. real_evidence_writer = PostgresRuleEvidenceWriter(
  885. platform,
  886. store,
  887. sample_ttl_seconds=900,
  888. )
  889. evidenced_executor = RulePlanExecutor(
  890. PostgresRulePlanRepository(platform),
  891. adapters={
  892. "polars_batch": PolarsRulePlanAdapter(
  893. artifact_store=store,
  894. artifact_resolver=resolver,
  895. artifact_ttl_seconds=900,
  896. )
  897. },
  898. evidence_writer=real_evidence_writer,
  899. )
  900. real_ledger = PostgresTaskLedger(platform, lease_seconds=30)
  901. runner_app = create_runner_app(
  902. verifier=verifier,
  903. ledger=real_ledger,
  904. registry=NodeRegistry({"rule.apply": evidenced_executor}),
  905. )
  906. no_run_crash_token = TaskTokenIssuer(task_secret).issue(
  907. task_uid=new_governance_uid(),
  908. dataflow_uid=dataflow_uid,
  909. deployment_id=deployment_id,
  910. environment="test",
  911. workflow_version=1,
  912. correlation_id=no_run_crash_correlation_id,
  913. node=node,
  914. write_authorized=True,
  915. )
  916. no_run_claims = verifier.verify(no_run_crash_token, node=node)
  917. no_run_crash_jti = no_run_claims.jti
  918. evidence_crash_token = TaskTokenIssuer(task_secret).issue(
  919. task_uid=new_governance_uid(),
  920. dataflow_uid=dataflow_uid,
  921. deployment_id=deployment_id,
  922. environment="test",
  923. workflow_version=1,
  924. correlation_id=evidence_crash_correlation_id,
  925. node=node,
  926. write_authorized=True,
  927. )
  928. evidence_crash_claims = verifier.verify(
  929. evidence_crash_token,
  930. node=node,
  931. )
  932. evidence_crash_jti = evidence_crash_claims.jti
  933. active_retry_token = TaskTokenIssuer(task_secret).issue(
  934. task_uid=new_governance_uid(),
  935. dataflow_uid=dataflow_uid,
  936. deployment_id=deployment_id,
  937. environment="test",
  938. workflow_version=1,
  939. correlation_id=active_retry_correlation_id,
  940. node=node,
  941. write_authorized=True,
  942. )
  943. active_retry_claims = verifier.verify(
  944. active_retry_token,
  945. node=node,
  946. )
  947. active_retry_jti = active_retry_claims.jti
  948. def task_binding(claims):
  949. return {
  950. "task_uid": claims.task_uid,
  951. "dataflow_uid": claims.dataflow_uid,
  952. "deployment_id": claims.deployment_id,
  953. "environment": claims.environment,
  954. "workflow_version": claims.workflow_version,
  955. "correlation_id": claims.correlation_id,
  956. "node_id": claims.node_id,
  957. "node_type": claims.node_type,
  958. "data_source_uid": None,
  959. "idempotency_key": "customer_id",
  960. }
  961. assert real_ledger.claim(
  962. no_run_crash_jti,
  963. task_binding(no_run_claims),
  964. expires_at=no_run_claims.expires_at,
  965. )
  966. assert real_ledger.claim(
  967. evidence_crash_jti,
  968. task_binding(evidence_crash_claims),
  969. expires_at=evidence_crash_claims.expires_at,
  970. )
  971. assert real_ledger.claim(
  972. active_retry_jti,
  973. task_binding(active_retry_claims),
  974. expires_at=active_retry_claims.expires_at,
  975. )
  976. real_evidence_writer.start(
  977. component_binding_id=component_binding_id,
  978. rule_version_id=rule_id,
  979. plan_hash=compiled["plan_hash"],
  980. correlation_id=evidence_crash_correlation_id,
  981. dataflow_uid=dataflow_uid,
  982. deployment_id=deployment_id,
  983. environment="test",
  984. workflow_version=1,
  985. node_id=node["id"],
  986. lease_owner=evidence_crash_jti,
  987. )
  988. real_evidence_writer.start(
  989. component_binding_id=component_binding_id,
  990. rule_version_id=rule_id,
  991. plan_hash=compiled["plan_hash"],
  992. correlation_id=active_retry_correlation_id,
  993. dataflow_uid=dataflow_uid,
  994. deployment_id=deployment_id,
  995. environment="test",
  996. workflow_version=1,
  997. node_id=node["id"],
  998. lease_owner=active_retry_jti,
  999. )
  1000. with platform.begin() as connection:
  1001. connection.execute(
  1002. text(
  1003. """
  1004. UPDATE public.runner_task_executions
  1005. SET lease_expires_at =
  1006. CURRENT_TIMESTAMP - INTERVAL '1 second'
  1007. WHERE token_jti IN (
  1008. CAST(:no_run_jti AS uuid),
  1009. CAST(:evidence_jti AS uuid)
  1010. )
  1011. """
  1012. ),
  1013. {
  1014. "no_run_jti": no_run_crash_jti,
  1015. "evidence_jti": evidence_crash_jti,
  1016. },
  1017. )
  1018. connection.execute(
  1019. text(
  1020. """
  1021. UPDATE public.rule_runs
  1022. SET lease_expires_at =
  1023. CURRENT_TIMESTAMP - INTERVAL '1 second'
  1024. WHERE lease_owner = CAST(:jti AS uuid)
  1025. """
  1026. ),
  1027. {"jti": evidence_crash_jti},
  1028. )
  1029. with runner_app.test_client() as client:
  1030. http_result = client.post(
  1031. "/v1/tasks/execute",
  1032. json={
  1033. "task_token": task_token,
  1034. "node": node,
  1035. "parameters": {},
  1036. },
  1037. )
  1038. replay = client.post(
  1039. "/v1/tasks/execute",
  1040. json={
  1041. "task_token": task_token,
  1042. "node": node,
  1043. "parameters": {},
  1044. },
  1045. )
  1046. retried = client.post(
  1047. "/v1/tasks/execute",
  1048. json={
  1049. "task_token": retry_token,
  1050. "node": node,
  1051. "parameters": {},
  1052. },
  1053. )
  1054. no_run_crash = client.post(
  1055. "/v1/tasks/execute",
  1056. json={
  1057. "task_token": no_run_crash_token,
  1058. "node": node,
  1059. "parameters": {},
  1060. },
  1061. )
  1062. evidence_crash = client.post(
  1063. "/v1/tasks/execute",
  1064. json={
  1065. "task_token": evidence_crash_token,
  1066. "node": node,
  1067. "parameters": {},
  1068. },
  1069. )
  1070. active_retry = client.post(
  1071. "/v1/tasks/execute",
  1072. json={
  1073. "task_token": active_retry_token,
  1074. "node": node,
  1075. "parameters": {},
  1076. },
  1077. )
  1078. assert http_result.status_code == 200, http_result.get_json()
  1079. assert http_result.get_json()["output_artifact"] == result[
  1080. "artifact_ref"
  1081. ]
  1082. assert http_result.get_json()["result"]["artifact_ref"] == result[
  1083. "artifact_ref"
  1084. ]
  1085. assert replay.status_code == 200
  1086. assert replay.headers["X-Idempotent-Replay"] == "true"
  1087. assert replay.get_json() == http_result.get_json()
  1088. assert retried.status_code == 200
  1089. assert retried.get_json()["output_artifact"] == result[
  1090. "artifact_ref"
  1091. ]
  1092. assert no_run_crash.status_code == 409
  1093. assert no_run_crash.get_json() == {
  1094. "error": "task execution outcome is unknown"
  1095. }
  1096. assert evidence_crash.status_code == 409
  1097. assert evidence_crash.get_json() == {
  1098. "error": "task execution outcome is unknown"
  1099. }
  1100. assert active_retry.status_code == 202
  1101. assert active_retry.headers["Retry-After"] == "2"
  1102. assert real_ledger.get(active_retry_jti).status == "running"
  1103. assert real_ledger.get(active_retry_jti).replay_body is None
  1104. assert real_ledger.get(no_run_crash_jti).status == "unknown"
  1105. assert real_ledger.get(evidence_crash_jti).status == "unknown"
  1106. with platform.connect() as connection:
  1107. assert connection.execute(
  1108. text(
  1109. """
  1110. SELECT COUNT(*)
  1111. FROM public.rule_runs
  1112. WHERE correlation_id =
  1113. CAST(:correlation_id AS uuid)
  1114. """
  1115. ),
  1116. {"correlation_id": no_run_crash_correlation_id},
  1117. ).scalar_one() == 0
  1118. crash_evidence = connection.execute(
  1119. text(
  1120. """
  1121. SELECT status, commit_outcome
  1122. FROM public.rule_runs
  1123. WHERE correlation_id =
  1124. CAST(:correlation_id AS uuid)
  1125. """
  1126. ),
  1127. {"correlation_id": evidence_crash_correlation_id},
  1128. ).mappings().one()
  1129. assert dict(crash_evidence) == {
  1130. "status": "unknown",
  1131. "commit_outcome": "unknown",
  1132. }
  1133. terminal_replay = real_evidence_writer.reconcile_expired_lease(
  1134. lease_owner=ledger_jti,
  1135. deployment_id=deployment_id,
  1136. correlation_id=correlation_id,
  1137. component_binding_id=component_binding_id,
  1138. rule_version_id=rule_id,
  1139. plan_hash=compiled["plan_hash"],
  1140. )
  1141. assert terminal_replay["state"] == "terminal"
  1142. assert terminal_replay["status"] == "success"
  1143. assert terminal_replay["result_digest"] == hashlib.sha256(
  1144. json.dumps(
  1145. terminal_replay["result"],
  1146. sort_keys=True,
  1147. separators=(",", ":"),
  1148. ensure_ascii=False,
  1149. ).encode("utf-8")
  1150. ).hexdigest()
  1151. assert re.fullmatch(
  1152. r"[0-9a-f]{64}",
  1153. terminal_replay["evidence_digest"],
  1154. )
  1155. ledger_record = PostgresTaskLedger(platform).get(ledger_jti)
  1156. assert ledger_record is not None
  1157. assert ledger_record.status == "success"
  1158. assert ledger_record.commit_outcome == "committed"
  1159. with platform.connect() as connection:
  1160. run_evidence = connection.execute(
  1161. text(
  1162. """
  1163. SELECT status, commit_outcome, rows_in, rows_out,
  1164. rows_rejected, rows_quarantined, public_result
  1165. FROM public.rule_runs
  1166. WHERE correlation_id = CAST(:correlation_id AS uuid)
  1167. AND component_binding_id =
  1168. CAST(:component_binding_id AS uuid)
  1169. """
  1170. ),
  1171. {
  1172. "correlation_id": correlation_id,
  1173. "component_binding_id": component_binding_id,
  1174. },
  1175. ).mappings().one()
  1176. sample_evidence = connection.execute(
  1177. text(
  1178. """
  1179. SELECT s.artifact_ref, s.artifact_digest,
  1180. s.sample_count, s.redaction_policy,
  1181. s.expires_at, s.handoff_status
  1182. FROM public.rule_violation_samples s
  1183. JOIN public.rule_runs r ON r.id = s.rule_run_id
  1184. WHERE r.correlation_id =
  1185. CAST(:correlation_id AS uuid)
  1186. """
  1187. ),
  1188. {"correlation_id": correlation_id},
  1189. ).mappings().one()
  1190. assert run_evidence["status"] == "success"
  1191. assert run_evidence["commit_outcome"] == "committed"
  1192. assert run_evidence["rows_in"] == 4
  1193. assert run_evidence["rows_out"] == 2
  1194. assert run_evidence["rows_rejected"] == 1
  1195. assert run_evidence["rows_quarantined"] == 0
  1196. assert run_evidence["public_result"]["output_artifact"] == result[
  1197. "artifact_ref"
  1198. ]
  1199. assert sample_evidence["artifact_digest"]
  1200. assert sample_evidence["sample_count"] == 1
  1201. assert (
  1202. sample_evidence["redaction_policy"]
  1203. == "rule-violation-default-v1"
  1204. )
  1205. assert sample_evidence["handoff_status"] == "ready"
  1206. assert store.read(
  1207. sample_evidence["artifact_ref"],
  1208. sample_evidence["artifact_digest"],
  1209. expected_schema_fields=[
  1210. {
  1211. "name": name,
  1212. "type": "string",
  1213. "nullable": True,
  1214. }
  1215. for name in (
  1216. "customer_id",
  1217. "mobile",
  1218. "name",
  1219. "segment_code",
  1220. "segment_name",
  1221. "version_no",
  1222. )
  1223. ],
  1224. ).collect().to_dicts() == [
  1225. {
  1226. "customer_id": "[REDACTED]",
  1227. "mobile": "[REDACTED]",
  1228. "name": "[REDACTED]",
  1229. "segment_code": "[REDACTED]",
  1230. "segment_name": "[REDACTED]",
  1231. "version_no": "[REDACTED]",
  1232. }
  1233. ]
  1234. orphan = store.write(
  1235. pl.DataFrame({"value": ["orphan"]}),
  1236. correlation_id,
  1237. 900,
  1238. schema_fields=[
  1239. {
  1240. "name": "value",
  1241. "type": "string",
  1242. "nullable": True,
  1243. }
  1244. ],
  1245. )
  1246. original_clock = store.clock
  1247. store.clock = lambda: datetime.now(UTC) + timedelta(seconds=60)
  1248. try:
  1249. reconciliation = resolver.reconcile(
  1250. limit=20,
  1251. grace_seconds=30,
  1252. )
  1253. finally:
  1254. store.clock = original_clock
  1255. assert reconciliation["orphans_deleted"] >= 1
  1256. assert store.describe_optional(orphan["artifact_ref"]) is None
  1257. assert store.describe(sample_evidence["artifact_ref"])[
  1258. "digest"
  1259. ] == sample_evidence["artifact_digest"]
  1260. downstream_node = {
  1261. "id": "task6_real_handoff",
  1262. "type": "rule.apply",
  1263. "purpose": "write",
  1264. "idempotency": {
  1265. "strategy": "deduplication_key",
  1266. "key": "customer_id",
  1267. },
  1268. "config": {
  1269. "component_binding_id": (
  1270. downstream_component_binding_id
  1271. ),
  1272. "rule_version_id": downstream_rule_id,
  1273. "execution_plan_hash": downstream_compiled[
  1274. "plan_hash"
  1275. ],
  1276. },
  1277. }
  1278. downstream_result = evidenced_executor.execute(
  1279. downstream_node,
  1280. {"input_artifact": result["artifact_ref"]},
  1281. write_authorized=True,
  1282. correlation_id=correlation_id,
  1283. dataflow_uid=dataflow_uid,
  1284. deployment_id=deployment_id,
  1285. environment="test",
  1286. workflow_version=1,
  1287. node_id="task6_real_handoff",
  1288. task_jti=new_governance_uid(),
  1289. )
  1290. assert downstream_result["rows_in"] == 2
  1291. assert downstream_result["rows_out"] == 2
  1292. assert downstream_result["output_artifact"] != result[
  1293. "artifact_ref"
  1294. ]
  1295. assert store.read(
  1296. downstream_result["artifact_ref"],
  1297. downstream_result["digest"],
  1298. expected_schema_fields=output_schema["fields"],
  1299. limits=downstream_compiled["plan"]["resource_limits"],
  1300. ).collect().sort("customer_id").to_dicts() == (
  1301. output.sort("customer_id").to_dicts()
  1302. )
  1303. replayed_downstream = evidenced_executor.execute(
  1304. downstream_node,
  1305. {"input_artifact": result["artifact_ref"]},
  1306. write_authorized=True,
  1307. correlation_id=correlation_id,
  1308. dataflow_uid=dataflow_uid,
  1309. deployment_id=deployment_id,
  1310. environment="test",
  1311. workflow_version=1,
  1312. node_id="task6_real_handoff",
  1313. task_jti=new_governance_uid(),
  1314. )
  1315. assert replayed_downstream["artifact_ref"] == downstream_result[
  1316. "artifact_ref"
  1317. ]
  1318. with platform.connect() as connection:
  1319. assert connection.execute(
  1320. text(
  1321. """
  1322. SELECT COUNT(*)
  1323. FROM public.rule_runs
  1324. WHERE correlation_id =
  1325. CAST(:correlation_id AS uuid)
  1326. """
  1327. ),
  1328. {"correlation_id": correlation_id},
  1329. ).scalar_one() == 2
  1330. from app.runner.nodes import NodeExecutionError
  1331. class FailingAdapter:
  1332. def execute(self, **_kwargs):
  1333. raise NodeExecutionError(
  1334. "safe downstream failure",
  1335. commit_outcome="not_committed",
  1336. )
  1337. failed_executor = RulePlanExecutor(
  1338. PostgresRulePlanRepository(platform),
  1339. adapters={"polars_batch": FailingAdapter()},
  1340. evidence_writer=PostgresRuleEvidenceWriter(
  1341. platform,
  1342. store,
  1343. sample_ttl_seconds=900,
  1344. ),
  1345. )
  1346. with pytest.raises(NodeExecutionError, match="safe downstream"):
  1347. failed_executor.execute(
  1348. node,
  1349. {},
  1350. write_authorized=True,
  1351. correlation_id=failure_correlation_id,
  1352. dataflow_uid=dataflow_uid,
  1353. deployment_id=deployment_id,
  1354. environment="test",
  1355. workflow_version=1,
  1356. node_id="task5_real_polars",
  1357. task_jti=new_governance_uid(),
  1358. )
  1359. with platform.connect() as connection:
  1360. failed_evidence = connection.execute(
  1361. text(
  1362. """
  1363. SELECT status, commit_outcome
  1364. FROM public.rule_runs
  1365. WHERE correlation_id =
  1366. CAST(:correlation_id AS uuid)
  1367. """
  1368. ),
  1369. {"correlation_id": failure_correlation_id},
  1370. ).mappings().one()
  1371. assert dict(failed_evidence) == {
  1372. "status": "failed",
  1373. "commit_outcome": "not_committed",
  1374. }
  1375. class UnknownAdapter:
  1376. def execute(self, **_kwargs):
  1377. raise NodeExecutionError(
  1378. "safe uncertain commit",
  1379. commit_outcome="unknown",
  1380. )
  1381. unknown_executor = RulePlanExecutor(
  1382. PostgresRulePlanRepository(platform),
  1383. adapters={"polars_batch": UnknownAdapter()},
  1384. evidence_writer=PostgresRuleEvidenceWriter(
  1385. platform,
  1386. store,
  1387. sample_ttl_seconds=900,
  1388. ),
  1389. )
  1390. with pytest.raises(NodeExecutionError, match="uncertain commit"):
  1391. unknown_executor.execute(
  1392. node,
  1393. {},
  1394. write_authorized=True,
  1395. correlation_id=unknown_correlation_id,
  1396. dataflow_uid=dataflow_uid,
  1397. deployment_id=deployment_id,
  1398. environment="test",
  1399. workflow_version=1,
  1400. node_id="task5_real_polars",
  1401. task_jti=new_governance_uid(),
  1402. )
  1403. with platform.connect() as connection:
  1404. unknown_evidence = connection.execute(
  1405. text(
  1406. """
  1407. SELECT status, commit_outcome
  1408. FROM public.rule_runs
  1409. WHERE correlation_id =
  1410. CAST(:correlation_id AS uuid)
  1411. """
  1412. ),
  1413. {"correlation_id": unknown_correlation_id},
  1414. ).mappings().one()
  1415. assert dict(unknown_evidence) == {
  1416. "status": "unknown",
  1417. "commit_outcome": "unknown",
  1418. }
  1419. evidence_writer = PostgresRuleEvidenceWriter(
  1420. platform,
  1421. store,
  1422. sample_ttl_seconds=900,
  1423. lease_seconds=30,
  1424. )
  1425. lease_owner = new_governance_uid()
  1426. lease_run_id = evidence_writer.start(
  1427. component_binding_id=component_binding_id,
  1428. rule_version_id=rule_id,
  1429. plan_hash=compiled["plan_hash"],
  1430. correlation_id=lease_correlation_id,
  1431. dataflow_uid=dataflow_uid,
  1432. deployment_id=deployment_id,
  1433. environment="test",
  1434. workflow_version=1,
  1435. node_id="task5_real_polars",
  1436. lease_owner=lease_owner,
  1437. )
  1438. with pytest.raises(ValueError, match="not owned"):
  1439. evidence_writer.heartbeat(
  1440. lease_run_id,
  1441. new_governance_uid(),
  1442. )
  1443. evidence_writer.heartbeat(lease_run_id, lease_owner)
  1444. with platform.begin() as connection:
  1445. connection.execute(
  1446. text(
  1447. """
  1448. UPDATE public.rule_runs
  1449. SET lease_expires_at =
  1450. CURRENT_TIMESTAMP - INTERVAL '1 second'
  1451. WHERE id = CAST(:id AS uuid)
  1452. """
  1453. ),
  1454. {"id": lease_run_id},
  1455. )
  1456. assert evidence_writer.start(
  1457. component_binding_id=component_binding_id,
  1458. rule_version_id=rule_id,
  1459. plan_hash=compiled["plan_hash"],
  1460. correlation_id=lease_correlation_id,
  1461. dataflow_uid=dataflow_uid,
  1462. deployment_id=deployment_id,
  1463. environment="test",
  1464. workflow_version=1,
  1465. node_id="task5_real_polars",
  1466. lease_owner=new_governance_uid(),
  1467. ) == lease_run_id
  1468. assert evidence_writer.replay(lease_run_id)["status"] == "unknown"
  1469. class FailOnceConnection:
  1470. def __init__(self, connection, state):
  1471. self.connection = connection
  1472. self.state = state
  1473. def execute(self, statement, parameters=None):
  1474. sql = str(statement)
  1475. if (
  1476. self.state["armed"]
  1477. and "UPDATE public.rule_runs" in sql
  1478. and "rows_in = :rows_in" in sql
  1479. ):
  1480. self.state["armed"] = False
  1481. raise RuntimeError("simulated response loss")
  1482. return self.connection.execute(statement, parameters)
  1483. class FailOnceBegin:
  1484. def __init__(self, context, state):
  1485. self.context = context
  1486. self.state = state
  1487. def __enter__(self):
  1488. return FailOnceConnection(
  1489. self.context.__enter__(),
  1490. self.state,
  1491. )
  1492. def __exit__(self, *args):
  1493. return self.context.__exit__(*args)
  1494. class FailOnceEngine:
  1495. def __init__(self, engine):
  1496. self.engine = engine
  1497. self.state = {"armed": True}
  1498. def connect(self):
  1499. return self.engine.connect()
  1500. def begin(self):
  1501. return FailOnceBegin(
  1502. self.engine.begin(),
  1503. self.state,
  1504. )
  1505. sample_crash_writer = PostgresRuleEvidenceWriter(
  1506. FailOnceEngine(platform),
  1507. store,
  1508. sample_ttl_seconds=900,
  1509. )
  1510. sample_crash_run_id = sample_crash_writer.start(
  1511. component_binding_id=component_binding_id,
  1512. rule_version_id=rule_id,
  1513. plan_hash=compiled["plan_hash"],
  1514. correlation_id=sample_crash_correlation_id,
  1515. dataflow_uid=dataflow_uid,
  1516. deployment_id=deployment_id,
  1517. environment="test",
  1518. workflow_version=1,
  1519. node_id="task5_real_polars",
  1520. lease_owner=new_governance_uid(),
  1521. )
  1522. sample_crash_evidence = {
  1523. "status": "success",
  1524. "rows_in": 1,
  1525. "rows_out": 0,
  1526. "rows_rejected": 1,
  1527. "rows_quarantined": 0,
  1528. "commit_outcome": "committed",
  1529. "timings": {"duration_ms": 1},
  1530. "violation_sample": [{"mobile": "[REDACTED]"}],
  1531. "sample_count": 1,
  1532. "redaction_policy": "rule-violation-default-v1",
  1533. }
  1534. with pytest.raises(RuntimeError, match="outcome is unknown"):
  1535. sample_crash_writer.finish(
  1536. sample_crash_run_id,
  1537. sample_crash_evidence,
  1538. )
  1539. with platform.connect() as connection:
  1540. assert connection.execute(
  1541. text(
  1542. """
  1543. SELECT handoff_status
  1544. FROM public.rule_violation_samples
  1545. WHERE rule_run_id = CAST(:id AS uuid)
  1546. """
  1547. ),
  1548. {"id": sample_crash_run_id},
  1549. ).scalar_one() == "ready"
  1550. with platform.begin() as connection:
  1551. connection.execute(
  1552. text(
  1553. """
  1554. UPDATE public.rule_violation_samples
  1555. SET handoff_status = 'unknown',
  1556. cleanup_claim = CAST(:cleanup_claim AS uuid),
  1557. cleanup_claim_expires_at =
  1558. CURRENT_TIMESTAMP + INTERVAL '5 minutes'
  1559. WHERE rule_run_id = CAST(:id AS uuid)
  1560. """
  1561. ),
  1562. {
  1563. "id": sample_crash_run_id,
  1564. "cleanup_claim": new_governance_uid(),
  1565. },
  1566. )
  1567. before_claim_expiry = evidence_writer.reconcile_samples(
  1568. limit=10
  1569. )
  1570. assert before_claim_expiry["claimed"] == 0
  1571. with platform.begin() as connection:
  1572. connection.execute(
  1573. text(
  1574. """
  1575. UPDATE public.rule_violation_samples
  1576. SET cleanup_claim_expires_at =
  1577. CURRENT_TIMESTAMP - INTERVAL '1 second'
  1578. WHERE rule_run_id = CAST(:id AS uuid)
  1579. """
  1580. ),
  1581. {"id": sample_crash_run_id},
  1582. )
  1583. sample_reconciliation = evidence_writer.reconcile_samples(
  1584. limit=10
  1585. )
  1586. assert sample_reconciliation["claimed"] == 1
  1587. assert sample_reconciliation["ready"] >= 1
  1588. evidence_writer.finish(
  1589. sample_crash_run_id,
  1590. sample_crash_evidence,
  1591. )
  1592. assert evidence_writer.replay(sample_crash_run_id)[
  1593. "status"
  1594. ] == "success"
  1595. with platform.connect() as connection:
  1596. missing_sample_ref = connection.execute(
  1597. text(
  1598. """
  1599. SELECT artifact_ref
  1600. FROM public.rule_violation_samples
  1601. WHERE rule_run_id = CAST(:id AS uuid)
  1602. """
  1603. ),
  1604. {"id": sample_crash_run_id},
  1605. ).scalar_one()
  1606. store.delete(missing_sample_ref)
  1607. with platform.begin() as connection:
  1608. connection.execute(
  1609. text(
  1610. """
  1611. UPDATE public.rule_violation_samples
  1612. SET handoff_status = 'unknown',
  1613. cleanup_claim = NULL,
  1614. cleanup_claim_expires_at = NULL
  1615. WHERE rule_run_id = CAST(:id AS uuid)
  1616. """
  1617. ),
  1618. {"id": sample_crash_run_id},
  1619. )
  1620. missing_reconciliation = evidence_writer.reconcile_samples(
  1621. limit=10
  1622. )
  1623. assert missing_reconciliation["failed"] == 1
  1624. with platform.connect() as connection:
  1625. assert connection.execute(
  1626. text(
  1627. """
  1628. SELECT handoff_status
  1629. FROM public.rule_violation_samples
  1630. WHERE rule_run_id = CAST(:id AS uuid)
  1631. """
  1632. ),
  1633. {"id": sample_crash_run_id},
  1634. ).scalar_one() == "failed"
  1635. class TransientArtifactStore:
  1636. def __init__(self, wrapped):
  1637. self.wrapped = wrapped
  1638. def __getattr__(self, name):
  1639. return getattr(self.wrapped, name)
  1640. def describe_optional(self, _ref):
  1641. raise TimeoutError("temporary MinIO timeout")
  1642. with platform.begin() as connection:
  1643. transient_sample_id = connection.execute(
  1644. text(
  1645. """
  1646. UPDATE public.rule_violation_samples s
  1647. SET handoff_status = 'unknown',
  1648. cleanup_claim = NULL,
  1649. cleanup_claim_expires_at = NULL
  1650. FROM public.rule_runs r
  1651. WHERE s.rule_run_id = r.id
  1652. AND r.correlation_id =
  1653. CAST(:correlation_id AS uuid)
  1654. RETURNING s.id::text
  1655. """
  1656. ),
  1657. {"correlation_id": correlation_id},
  1658. ).scalar_one()
  1659. transient_writer = PostgresRuleEvidenceWriter(
  1660. platform,
  1661. TransientArtifactStore(store),
  1662. sample_ttl_seconds=900,
  1663. )
  1664. transient_result = transient_writer.reconcile_samples(
  1665. limit=10
  1666. )
  1667. assert transient_result["claimed"] == 1
  1668. assert transient_result["ready"] == 0
  1669. assert transient_result["failed"] == 0
  1670. with platform.connect() as connection:
  1671. transient_state = connection.execute(
  1672. text(
  1673. """
  1674. SELECT handoff_status,
  1675. cleanup_claim,
  1676. cleanup_claim_expires_at
  1677. FROM public.rule_violation_samples
  1678. WHERE id = CAST(:id AS uuid)
  1679. """
  1680. ),
  1681. {"id": transient_sample_id},
  1682. ).mappings().one()
  1683. assert transient_state["handoff_status"] == "unknown"
  1684. assert transient_state["cleanup_claim"] is None
  1685. assert transient_state["cleanup_claim_expires_at"] is None
  1686. assert evidence_writer.reconcile_samples(limit=10)["ready"] == 1
  1687. with platform.begin() as connection:
  1688. connection.execute(
  1689. text(
  1690. """
  1691. INSERT INTO public.dataflow_dataset_bindings
  1692. (id, dataflow_deployment_id, logical_ref,
  1693. data_source_uid, object_kind, object_ref,
  1694. schema_snapshot_id, dialect, access_mode, write_mode,
  1695. binding_hash)
  1696. VALUES (
  1697. CAST(:id AS uuid), CAST(:deployment_id AS uuid),
  1698. 'sql_receipt', CAST(:source_uid AS uuid), 'table',
  1699. 'public.task6_sql_receipt',
  1700. CAST(:schema_snapshot_id AS uuid), 'postgresql',
  1701. 'read_write', 'upsert', :binding_hash
  1702. )
  1703. """
  1704. ),
  1705. {
  1706. "id": sql_binding_id,
  1707. "deployment_id": deployment_id,
  1708. "source_uid": output_binding["data_source_uid"],
  1709. "schema_snapshot_id": output_schema["id"],
  1710. "binding_hash": "b" * 64,
  1711. },
  1712. )
  1713. receipt_run_id = evidence_writer.start(
  1714. component_binding_id=component_binding_id,
  1715. rule_version_id=rule_id,
  1716. plan_hash=compiled["plan_hash"],
  1717. correlation_id=receipt_correlation_id,
  1718. dataflow_uid=dataflow_uid,
  1719. deployment_id=deployment_id,
  1720. environment="test",
  1721. workflow_version=1,
  1722. node_id="task5_real_polars",
  1723. lease_owner=new_governance_uid(),
  1724. )
  1725. receipt = evidence_writer.stage_sql_output(
  1726. receipt_run_id,
  1727. output_binding_id=sql_binding_id,
  1728. )
  1729. assert re.fullmatch(
  1730. r"dataops-staging://[0-9a-f-]{36}",
  1731. receipt,
  1732. )
  1733. with pytest.raises(ValueError, match="not executable"):
  1734. evidence_writer.resolve_sql_staging(
  1735. receipt,
  1736. deployment_id=deployment_id,
  1737. correlation_id=receipt_correlation_id,
  1738. input_binding_id=sql_binding_id,
  1739. )
  1740. receipt_evidence = {
  1741. "status": "success",
  1742. "rows_in": 2,
  1743. "rows_out": 2,
  1744. "rows_rejected": 0,
  1745. "rows_quarantined": 0,
  1746. "commit_outcome": "committed",
  1747. "timings": {"duration_ms": 1},
  1748. "public_result": {
  1749. "rows_in": 2,
  1750. "rows_out": 2,
  1751. "rows_rejected": 0,
  1752. "rows_quarantined": 0,
  1753. "commit_outcome": "committed",
  1754. },
  1755. }
  1756. evidence_writer.finish(receipt_run_id, receipt_evidence)
  1757. evidence_writer.finish(receipt_run_id, receipt_evidence)
  1758. with pytest.raises(ValueError, match="immutable"):
  1759. evidence_writer.finish(
  1760. receipt_run_id,
  1761. {
  1762. **receipt_evidence,
  1763. "rows_out": 1,
  1764. },
  1765. )
  1766. resolved_receipt = evidence_writer.resolve_sql_staging(
  1767. receipt,
  1768. deployment_id=deployment_id,
  1769. correlation_id=receipt_correlation_id,
  1770. input_binding_id=sql_binding_id,
  1771. )
  1772. assert resolved_receipt["relation_ref"] == (
  1773. "public.task6_sql_receipt"
  1774. )
  1775. assert len(resolved_receipt["relation_digest"]) == 64
  1776. with pytest.raises(ValueError, match="not executable"):
  1777. evidence_writer.resolve_sql_staging(
  1778. receipt,
  1779. deployment_id=deployment_id,
  1780. correlation_id=new_governance_uid(),
  1781. input_binding_id=sql_binding_id,
  1782. )
  1783. with pytest.raises(ValueError, match="invalid"):
  1784. evidence_writer.resolve_sql_staging(
  1785. "dataops-staging://public.task6_sql_receipt",
  1786. deployment_id=deployment_id,
  1787. correlation_id=receipt_correlation_id,
  1788. input_binding_id=sql_binding_id,
  1789. )
  1790. failed_receipt_run_id = evidence_writer.start(
  1791. component_binding_id=component_binding_id,
  1792. rule_version_id=rule_id,
  1793. plan_hash=compiled["plan_hash"],
  1794. correlation_id=failed_receipt_correlation_id,
  1795. dataflow_uid=dataflow_uid,
  1796. deployment_id=deployment_id,
  1797. environment="test",
  1798. workflow_version=1,
  1799. node_id="task5_real_polars",
  1800. lease_owner=new_governance_uid(),
  1801. )
  1802. failed_receipt = evidence_writer.stage_sql_output(
  1803. failed_receipt_run_id,
  1804. output_binding_id=sql_binding_id,
  1805. )
  1806. evidence_writer.finish(
  1807. failed_receipt_run_id,
  1808. {
  1809. "status": "failed",
  1810. "commit_outcome": "not_committed",
  1811. "timings": {"duration_ms": 1},
  1812. },
  1813. )
  1814. with pytest.raises(ValueError, match="not executable"):
  1815. evidence_writer.resolve_sql_staging(
  1816. failed_receipt,
  1817. deployment_id=deployment_id,
  1818. correlation_id=failed_receipt_correlation_id,
  1819. input_binding_id=sql_binding_id,
  1820. )
  1821. with platform.begin() as connection:
  1822. connection.execute(
  1823. text(
  1824. """
  1825. UPDATE public.rule_sql_staging_receipts
  1826. SET expires_at =
  1827. CURRENT_TIMESTAMP - INTERVAL '1 second',
  1828. cleanup_claim = CASE
  1829. WHEN correlation_id =
  1830. CAST(:ready AS uuid)
  1831. THEN CAST(:cleanup_claim AS uuid)
  1832. ELSE NULL
  1833. END,
  1834. cleanup_claim_expires_at = CASE
  1835. WHEN correlation_id =
  1836. CAST(:ready AS uuid)
  1837. THEN CURRENT_TIMESTAMP
  1838. + INTERVAL '5 minutes'
  1839. ELSE NULL
  1840. END
  1841. WHERE correlation_id IN (
  1842. CAST(:ready AS uuid),
  1843. CAST(:failed AS uuid)
  1844. )
  1845. """
  1846. ),
  1847. {
  1848. "ready": receipt_correlation_id,
  1849. "failed": failed_receipt_correlation_id,
  1850. "cleanup_claim": new_governance_uid(),
  1851. },
  1852. )
  1853. assert evidence_writer.cleanup_sql_staging(limit=10) == 1
  1854. with platform.connect() as connection:
  1855. assert connection.execute(
  1856. text(
  1857. """
  1858. SELECT status
  1859. FROM public.rule_sql_staging_receipts
  1860. WHERE correlation_id = CAST(:ready AS uuid)
  1861. """
  1862. ),
  1863. {"ready": receipt_correlation_id},
  1864. ).scalar_one() == "ready"
  1865. with platform.begin() as connection:
  1866. connection.execute(
  1867. text(
  1868. """
  1869. UPDATE public.rule_sql_staging_receipts
  1870. SET cleanup_claim_expires_at =
  1871. CURRENT_TIMESTAMP - INTERVAL '1 second'
  1872. WHERE correlation_id = CAST(:ready AS uuid)
  1873. """
  1874. ),
  1875. {"ready": receipt_correlation_id},
  1876. )
  1877. assert evidence_writer.cleanup_sql_staging(limit=10) == 1
  1878. with platform.connect() as connection:
  1879. assert connection.execute(
  1880. text(
  1881. """
  1882. SELECT COUNT(*)
  1883. FROM public.rule_sql_staging_receipts
  1884. WHERE correlation_id IN (
  1885. CAST(:ready AS uuid),
  1886. CAST(:failed AS uuid)
  1887. )
  1888. AND status = 'expired'
  1889. """
  1890. ),
  1891. {
  1892. "ready": receipt_correlation_id,
  1893. "failed": failed_receipt_correlation_id,
  1894. },
  1895. ).scalar_one() == 2
  1896. conflict_path = tmp_path / "conflict.parquet"
  1897. pl.DataFrame(
  1898. {
  1899. "customer_id": [99],
  1900. "mobile": ["13800138000"],
  1901. "name": ["Conflict"],
  1902. "segment_code": ["A"],
  1903. "segment_name": ["Gold"],
  1904. "version_no": [1],
  1905. }
  1906. ).write_parquet(conflict_path)
  1907. object_count_before_conflict = len(
  1908. list(minio.list_objects(bucket, prefix=prefix, recursive=True))
  1909. )
  1910. with pytest.raises(ValueError, match="immutable|digest"):
  1911. resolver.publish_path(
  1912. str(conflict_path),
  1913. binding_id=output_binding["id"],
  1914. binding_hash=compiled["plan"]["output_binding_hash"],
  1915. correlation_id=correlation_id,
  1916. kind="output",
  1917. ttl_seconds=900,
  1918. schema_fields=output_schema["fields"],
  1919. limits=compiled["plan"]["resource_limits"],
  1920. )
  1921. assert len(
  1922. list(minio.list_objects(bucket, prefix=prefix, recursive=True))
  1923. ) == object_count_before_conflict
  1924. assert customer_artifact["digest"]
  1925. assert segment_artifact["digest"]
  1926. with platform.connect() as connection:
  1927. catalog_rows = connection.execute(
  1928. text(
  1929. """
  1930. SELECT artifact_kind, artifact_ref, handoff_status,
  1931. binding_hash
  1932. FROM public.rule_run_artifacts
  1933. WHERE correlation_id = CAST(:correlation_id AS uuid)
  1934. ORDER BY artifact_kind
  1935. """
  1936. ),
  1937. {"correlation_id": correlation_id},
  1938. ).mappings().all()
  1939. # The repeated deterministic output has the same digest and is
  1940. # idempotently retained as one stable catalog handoff.
  1941. assert len(catalog_rows) == 4
  1942. assert all(row["handoff_status"] == "ready" for row in catalog_rows)
  1943. assert all(len(row["binding_hash"]) == 64 for row in catalog_rows)
  1944. assert {
  1945. row["artifact_ref"]
  1946. for row in catalog_rows
  1947. if row["artifact_kind"] == "output"
  1948. } == {
  1949. result["artifact_ref"],
  1950. downstream_result["artifact_ref"],
  1951. }
  1952. assert len(
  1953. list(minio.list_objects(bucket, prefix=prefix, recursive=True))
  1954. ) == 5
  1955. finally:
  1956. for item in list(
  1957. minio.list_objects(bucket, prefix=prefix, recursive=True)
  1958. ):
  1959. minio.remove_object(bucket, item.object_name)
  1960. assert list(
  1961. minio.list_objects(bucket, prefix=prefix, recursive=True)
  1962. ) == []
  1963. with postgres.begin() as connection:
  1964. connection.execute(text(f"DROP TABLE IF EXISTS {customer_table}"))
  1965. with mysql.begin() as connection:
  1966. connection.execute(text(f"DROP TABLE IF EXISTS {segment_table}"))
  1967. with platform.begin() as connection:
  1968. if ledger_jti is not None:
  1969. connection.execute(
  1970. text(
  1971. "DELETE FROM public.runner_task_executions "
  1972. "WHERE token_jti = CAST(:jti AS uuid)"
  1973. ),
  1974. {"jti": ledger_jti},
  1975. )
  1976. if retry_ledger_jti is not None:
  1977. connection.execute(
  1978. text(
  1979. "DELETE FROM public.runner_task_executions "
  1980. "WHERE token_jti = CAST(:jti AS uuid)"
  1981. ),
  1982. {"jti": retry_ledger_jti},
  1983. )
  1984. for crash_jti in (
  1985. no_run_crash_jti,
  1986. evidence_crash_jti,
  1987. active_retry_jti,
  1988. ):
  1989. if crash_jti is not None:
  1990. connection.execute(
  1991. text(
  1992. "DELETE FROM public.runner_task_executions "
  1993. "WHERE token_jti = CAST(:jti AS uuid)"
  1994. ),
  1995. {"jti": crash_jti},
  1996. )
  1997. connection.execute(
  1998. text(
  1999. """
  2000. DELETE FROM public.rule_sql_staging_receipts
  2001. WHERE correlation_id IN (
  2002. CAST(:receipt_correlation_id AS uuid),
  2003. CAST(:failed_receipt_correlation_id AS uuid)
  2004. )
  2005. """
  2006. ),
  2007. {
  2008. "receipt_correlation_id": receipt_correlation_id,
  2009. "failed_receipt_correlation_id": (
  2010. failed_receipt_correlation_id
  2011. ),
  2012. },
  2013. )
  2014. connection.execute(
  2015. text(
  2016. """
  2017. DELETE FROM public.rule_violation_samples s
  2018. USING public.rule_runs r
  2019. WHERE s.rule_run_id = r.id
  2020. AND r.correlation_id IN (
  2021. CAST(:correlation_id AS uuid),
  2022. CAST(:sample_crash_correlation_id AS uuid)
  2023. )
  2024. """
  2025. ),
  2026. {
  2027. "correlation_id": correlation_id,
  2028. "sample_crash_correlation_id": (
  2029. sample_crash_correlation_id
  2030. ),
  2031. },
  2032. )
  2033. connection.execute(
  2034. text(
  2035. "DELETE FROM public.rule_runs "
  2036. "WHERE correlation_id IN ("
  2037. "CAST(:correlation_id AS uuid), "
  2038. "CAST(:failure_correlation_id AS uuid), "
  2039. "CAST(:unknown_correlation_id AS uuid), "
  2040. "CAST(:lease_correlation_id AS uuid), "
  2041. "CAST(:sample_crash_correlation_id AS uuid), "
  2042. "CAST(:receipt_correlation_id AS uuid), "
  2043. "CAST(:failed_receipt_correlation_id AS uuid), "
  2044. "CAST(:no_run_crash_correlation_id AS uuid), "
  2045. "CAST(:evidence_crash_correlation_id AS uuid), "
  2046. "CAST(:active_retry_correlation_id AS uuid))"
  2047. ),
  2048. {
  2049. "correlation_id": correlation_id,
  2050. "failure_correlation_id": failure_correlation_id,
  2051. "unknown_correlation_id": unknown_correlation_id,
  2052. "lease_correlation_id": lease_correlation_id,
  2053. "sample_crash_correlation_id": (
  2054. sample_crash_correlation_id
  2055. ),
  2056. "receipt_correlation_id": receipt_correlation_id,
  2057. "failed_receipt_correlation_id": (
  2058. failed_receipt_correlation_id
  2059. ),
  2060. "no_run_crash_correlation_id": (
  2061. no_run_crash_correlation_id
  2062. ),
  2063. "evidence_crash_correlation_id": (
  2064. evidence_crash_correlation_id
  2065. ),
  2066. "active_retry_correlation_id": (
  2067. active_retry_correlation_id
  2068. ),
  2069. },
  2070. )
  2071. connection.execute(
  2072. text(
  2073. "DELETE FROM public.dataflow_dataset_bindings "
  2074. "WHERE id = CAST(:id AS uuid)"
  2075. ),
  2076. {"id": sql_binding_id},
  2077. )
  2078. connection.execute(
  2079. text(
  2080. "DELETE FROM public.rule_run_artifacts "
  2081. "WHERE correlation_id = CAST(:correlation_id AS uuid)"
  2082. ),
  2083. {"correlation_id": correlation_id},
  2084. )
  2085. connection.execute(
  2086. text(
  2087. "DELETE FROM public.rule_execution_plans "
  2088. "WHERE id IN (CAST(:id AS uuid), "
  2089. "CAST(:downstream_id AS uuid))"
  2090. ),
  2091. {
  2092. "id": plan_id,
  2093. "downstream_id": downstream_plan_id,
  2094. },
  2095. )
  2096. connection.execute(
  2097. text(
  2098. "DELETE FROM public.dataflow_dataset_bindings "
  2099. "WHERE dataflow_deployment_id = CAST(:id AS uuid)"
  2100. ),
  2101. {"id": deployment_id},
  2102. )
  2103. connection.execute(
  2104. text(
  2105. "DELETE FROM public.dataflow_component_bindings "
  2106. "WHERE id IN (CAST(:id AS uuid), "
  2107. "CAST(:downstream_id AS uuid))"
  2108. ),
  2109. {
  2110. "id": component_binding_id,
  2111. "downstream_id": downstream_component_binding_id,
  2112. },
  2113. )
  2114. connection.execute(
  2115. text(
  2116. "DELETE FROM public.dataflow_deployments "
  2117. "WHERE id = CAST(:id AS uuid)"
  2118. ),
  2119. {"id": deployment_id},
  2120. )
  2121. if gateway_candidate_id is not None:
  2122. connection.execute(
  2123. text(
  2124. "DELETE FROM public.workflow_canary_evidence "
  2125. "WHERE workflow_version_id = CAST(:id AS uuid)"
  2126. ),
  2127. {"id": gateway_candidate_id},
  2128. )
  2129. connection.execute(
  2130. text(
  2131. "DELETE FROM public.workflow_schedules "
  2132. "WHERE workflow_version_id = CAST(:id AS uuid)"
  2133. ),
  2134. {"id": gateway_candidate_id},
  2135. )
  2136. connection.execute(
  2137. text(
  2138. "DELETE FROM public.workflow_engine_bindings "
  2139. "WHERE workflow_version_id = CAST(:id AS uuid)"
  2140. ),
  2141. {"id": gateway_candidate_id},
  2142. )
  2143. connection.execute(
  2144. text(
  2145. "DELETE FROM public.dataflow_workflow_versions "
  2146. "WHERE id = CAST(:id AS uuid)"
  2147. ),
  2148. {"id": gateway_candidate_id},
  2149. )
  2150. connection.execute(
  2151. text(
  2152. "DELETE FROM public.dataflow_versions "
  2153. "WHERE id = CAST(:id AS uuid)"
  2154. ),
  2155. {"id": dataflow_version_id},
  2156. )
  2157. connection.execute(
  2158. text(
  2159. "DELETE FROM public.data_rule_versions "
  2160. "WHERE id IN (CAST(:id AS uuid), "
  2161. "CAST(:downstream_id AS uuid))"
  2162. ),
  2163. {
  2164. "id": rule_id,
  2165. "downstream_id": downstream_rule_id,
  2166. },
  2167. )
  2168. connection.execute(
  2169. text(
  2170. "DELETE FROM public.data_rules "
  2171. "WHERE rule_uid IN (CAST(:rule_uid AS uuid), "
  2172. "CAST(:downstream_rule_uid AS uuid))"
  2173. ),
  2174. {
  2175. "rule_uid": rule_uid,
  2176. "downstream_rule_uid": downstream_rule_uid,
  2177. },
  2178. )
  2179. for schema in (input_schema, lookup_schema, output_schema):
  2180. connection.execute(
  2181. text(
  2182. "DELETE FROM public.data_schema_snapshots "
  2183. "WHERE id = CAST(:id AS uuid)"
  2184. ),
  2185. {"id": schema["id"]},
  2186. )
  2187. postgres.dispose()
  2188. mysql.dispose()
  2189. platform.dispose()