1"use strict";(self.webpackChunkerpl_docusaurus=self.webpackChunkerpl_docusaurus||[]).push([[1774],{7017:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>d,contentTitle:()=>o,default:()=>c,frontMatter:()=>r,metadata:()=>s,toc:()=>l});var s=n(54870),a=n(74848),i=n(28453);const r={slug:"sap-cds-delta-extraction-abap-pipeline-engine",title:"SAP Blocked ODP-RFC. Here Is CDS Delta, Deletes Included.",authors:["joachim-rosskopf"],tags:["erpl","sap","abap","cds","delta","duckdb","sap-integration","real-time"],date:new Date("2026-09-23T00:00:00.000Z"),image:"./img/hero.jpg",description:"SAP Note 3255746 prohibits third-party use of the ODP-RFC API, a security patch has enforced it since June, and the opt-out expires at the end of 2026. The note points at CDS view extraction instead. We built that path on the ABAP Pipeline Engine \u2014 the same runtime SAP Datasphere uses \u2014 and it reports deletes, which ODP's CDS delta cannot."},o=void 0,d={image:n(11767).A,authorsImageUrls:[void 0]},l=[{value:"Where the data went",id:"where-the-data-went",level:2},{value:"The engine that was already there",id:"the-engine-that-was-already-there",level:2},{value:"The protocol, in one section",id:"the-protocol-in-one-section",level:2},{value:"Delta, and what it costs",id:"delta-and-what-it-costs",level:2},{value:"How we know it is right",id:"how-we-know-it-is-right",level:2},{value:"Using it from erpl",id:"using-it-from-erpl",level:2},{value:"Using it from erpl-rev",id:"using-it-from-erpl-rev",level:2},{value:"One protocol, two directions",id:"one-protocol-two-directions",level:2},{value:"Proving the scope to whoever has to approve it",id:"proving-the-scope-to-whoever-has-to-approve-it",level:2}
1,{value:"Provenance and legality",id:"provenance-and-legality",level:2},{value:"What to do with this",id:"what-to-do-with-this",level:2}];function h(e){const t={a:"a",code:"code",em:"em",h2:"h2",img:"img",mermaid:"mermaid",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,i.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsxs)(t.p,{children:["Every SAP extraction pipeline built in the last decade rests on one of a handful of\ninterfaces. For a great many of them, that interface is ODP-RFC \u2014 the RFC modules of\nthe Operational Data Provisioning framework, ",(0,a.jsx)(t.code,{children:"RODPS_REPL_*"}),"."]}),"\n",(0,a.jsxs)(t.p,{children:[(0,a.jsx)(t.strong,{children:"SAP Note 3255746 prohibits third-party use of those modules."})," Not deprecates:\nprohibits. They are defined as being exclusively for data transfer between SAP\napplications, and the prohibition covers customer and third-party applications reading\nS/4HANA, BW or ECC, on-premise or in private cloud. Since 9 June 2026 a security patch\nin the support packages ",(0,a.jsx)(t.strong,{children:"technically blocks"})," the calls. A temporary opt-out carries\nnon-compliant integrations to the end of 2026, and then it does not."]}),"\n",(0,a.jsxs)(t.p,{children:["The note does not leave you without a path. It names what remains permitted, and one\nitem on that list is where SAP developers have been putting their semantics for years:\n",(0,a.jsx)(t.strong,{children:"CDS view extraction"}),". So we built that path \u2014 and the engine we built it on turns out\nto be the one SAP's own recommended alternative already runs on."]}),"\n",(0,a.jsx)(t.p,{children:(0,a.jsx)(t.img,{alt:"A duck engineer in a hard hat and safety vest stands between two doorways in an industrial wall. On the left a riveted steel door stencilled ODP-RFC is shut, a red warning lamp lit above it. On the right an open doorway stencilled CDS carries a conveyor belt of labelled crates out toward a large crate bearing a duck logo; one crate on the belt is stamped with a red letter D for delete. The duck holds a clipboard and gestures toward the open door.",src:n(31726).A+"",width:"1408",height:"768"})}),"\n",(0,a.jsx)(t.h2,{id:"where-the-data-went",children:"Where the data went"}),"\n",(0,a.jsx)(t.p,{children:"Ask an ABAP developer where the business logic lives today and you will hear CDS.\nReleased entities with C1 contracts, annotations carrying meaning, a decade of modelling\neffort. The semantic layer moved."}),"\n",(0,a.jsxs)(t.p,{children:["Extraction did not follow it cleanly. ODP treats ",(0,a.jsx)(t.code,{children:"ABAP_CDS"})," as one context among five,\nand its delta on a CDS view requires the view to carry\n",(0,a.jsx)(t.code,{children:"@Analytics.dataExtraction.delta.byElement"})," \u2014 an annotation pointing at a change\ntimestamp, set at design time by whoever built the view. If it is not there, you get no\ndelta. If you do not own the view, you may not be able to have it added."]}),"\n",(0,a.jsxs)(t.p,{children:["And there is a limit that no annotation fixes. A change-timestamp delta finds rows whose\ntimestamp moved. ",(0,a.jsx)(t.strong,{children:"A deleted row has no timestamp left to move."})," It is simply absent,\nand absence is not something a ",(0,a.jsx)(t.code,{children:"WHERE changed_at > watermark"})," can return. So a warehouse\nfed by that mechanism accumulates rows that no longer exist in SAP, quietly, until\nsomeone reconciles counts and finds the drift."]}),"\n",(0,a.jsx)(t.p,{children:"That is the gap worth closing, and closing it needs change capture at the database level\nrather than a timestamp in a column."}),"\n",(0,a.jsx)(t.h2,{id:"the-engine-that-was-already-there",children:"The engine that was already there"}),"\n",(0,a.jsxs)(t.p,{children:["The ABAP Pipeline Engine \u2014 function modules under ",(0,a.jsx)(t.code,{children:"DHAPE_*"}),", with the ABAP Metadata\nBrowser under ",(0,a.jsx)(t.code,{children:"DHAMB_*"})," for discovery \u2014 is an ABAP-based data-flow runtime that ships\ninside the stack. It runs a small graph of operators: a reader, a channel, an outport.\nThere is a GUI workbench for it on transaction ",(0,a.jsx)(t.code,{children:"DHAPE"}),"."]}),"\n",(0,a.jsxs)(t.p,{children:["It is not obscure because it is unimportant. It is obscure because it is normally\ninvisible: it is ",(0,a.jsx)(t.a,{href:"https://community.sap.com/t5/technology-blog-posts-by-sap/under-the-hood-of-abap-cdc-in-sap-datasphere-replication-flows/ba-p/14411878",children:"the runtime SAP Datasphere replication flows use to extract from ABAP\nsources"}),",\nand ",(0,a.jsx)(t.a,{href:"https://help.sap.com/docs/SAP_DATA_INTELLIGENCE",children:"the mechanism behind ABAP integration in SAP Data\nIntelligence"}),". When SAP tells you to\nmove your CDS extraction to Datasphere, this is what Datasphere then does on your ABAP\nsystem."]}),"\n",(0,a.jsxs)(t.p,{children:["The operator we care about is publicly n
1amed: the ABAP CDS Reader,\n",(0,a.jsx)(t.code,{children:"com.sap.abap.cds.reader.v2"}),". On S/4HANA Cloud it is reached through communication\nscenario ",(0,a.jsx)(t.code,{children:"SAP_COM_0532"}),". Nothing is installed to use it \u2014 no transport, no Z objects, no\nABAP. The engine is already in your system."]}),"\n",(0,a.jsx)(t.mermaid,{value:'graph LR\n subgraph SAP["SAP system"]\n R["CDS reader<br/>com.sap.abap.cds.reader.v2"]\n O["outport"]\n T["DHCDC triggers<br/>+ logging tables"]\n E["CDS entity"]\n R --\x3e O\n E --\x3e R\n T --\x3e R\n end\n subgraph C["Client"]\n M["DHAPE_GRAPH_MANAGER<br/>create \xb7 start \xb7 stop"]\n P["DHAPE_GRAPH_ROUNDTRIP<br/>the only data path"]\n end\n M --\x3e R\n O --\x3e P\n\n style R fill:#fff100,stroke:#000,stroke-width:2px\n style P fill:#e1f5fe,stroke:#01579b,stroke-width:2px'}),"\n",(0,a.jsx)(t.h2,{id:"the-protocol-in-one-section",children:"The protocol, in one section"}),"\n",(0,a.jsxs)(t.p,{children:["You create a graph by handing ",(0,a.jsx)(t.code,{children:"DHAPE_GRAPH_MANAGER"})," a JSON definition: named processes,\neach with a ",(0,a.jsx)(t.code,{children:"Component"})," naming its operator, and channels between their ports. Protocol\nv6 starts the graph during create, and the graph is bound to your RFC session."]}),"\n",(0,a.jsxs)(t.p,{children:["The reader takes seven configuration keys and no others: ",(0,a.jsx)(t.code,{children:"subscriptionType"}),",\n",(0,a.jsx)(t.code,{children:"subscriptionID"}),", ",(0,a.jsx)(t.code,{children:"subscriptionName"}),", ",(0,a.jsx)(t.code,{children:"cdsname"}),", ",(0,a.jsx)(t.code,{children:"action"}),", ",(0,a.jsx)(t.code,{children:"chunkSize"}),", ",(0,a.jsx)(t.code,{children:"wireformat"}),".\n",(0,a.jsx)(t.code,{children:"action"})," is what selects the mode \u2014 ",(0,a.jsx)(t.code,{children:"Initial Load"})," for a snapshot, ",(0,a.jsx)(t.code,{children:"Replication"})," for\ndelta. There is no filter key and no schema key, which matters later."]}),"\n",(0,a.jsxs)(t.p,{children:[(0,a.jsx)(t.code,{children:"DHAPE_GRAPH_ROUNDTRIP"})," is the only call that moves data. Each roundtrip returns a\npackage: a JSON envelope carrying ",(0,a.jsx)(t.code,{children:'Encoding: "csv"'}),", a self-describing\n",(0,a.jsx)(t.code,{children:"Attributes.ABAP.Fields[]"})," block so every package declares its own schema, a\n",(0,a.jsx)(t.code,{children:"batchIndex"}),", and a ",(0,a.jsx)(t.code,{children:"lastBatch"})," marker. The body is RFC 4180 quoted \u2014 a comma inside a\nvalue arrives as ",(0,a.jsx)(t.code,{children:'"comma,inside"'}),", a quote as ",(0,a.jsx)(t.code,{children:'"quote""inside"'})," \u2014 so a decoder that\nsplits on commas will shift every later column on the first free-text field it meets."]}),"\n",(0,a.jsxs)(t.p,{children:["Delta rows carry one extra column, ",(0,a.jsx)(t.code,{children:"/1DH/OPERATION"}),":"]}),"\n",(0,a.jsxs)(t.table,{children:[(0,a.jsx)(t.thead,{children:(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.th,{children:"Value"}),(0,a.jsx)(t.th,{children:"Meaning"})]})}),(0,a.jsxs)(t.tbody,{children:[(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:(0,a.jsx)(t.code,{children:"U"})}),(0,a.jsxs)(t.td,{children:["the row's current state \u2014 an insert ",(0,a.jsx)(t.strong,{children:"or"})," an update, both as after-images"]})]}),(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:(0,a.jsx)(t.code,{children:"D"})}),(0,a.jsxs)(t.td,{children:["the row was deleted; ",(0,a.jsx)(t.strong,{children:"only the key fields are populated"})]})]})]})]}),"\n",(0,a.jsxs)(t.p,{children:["Inserts and updates are not distinguished, because the engine sends after-images for\nboth. The delete shape is not our interpretation either: SAP's own description of ABAP\nCDC is that for deletions ",(0,a.jsx)(t.em,{children:"the key columns are populated and all other columns are\nblank"}),". That single row type is the whole reason for this work."]}),"\n",(0,a.jsxs)(t.p,{children:["The normative spec we wrote and work from is\n",(0,a.jsx)(t.a,{href:"https://github.com/DataZooDE/erpl",children:(0,a.jsx)(t.code,{children:"ape/docs/protocol.md"})})," \u2014 fourteen sections, with\neach claim marked as verified against a live system or still open."]}),"\n",(0,a.jsx)(t.h2,{id:"delta-and-what-it-costs",children:"Delta, and what it costs"}),"\n",(0,a.jsx)(t.p,{children:"Delta is not free, and the costs are worth knowing before you enable it."}),"\n",(0,a.jsxs)(t.p,{children:["The first delta read on an entity makes SAP generate DHCDC logging tables and ",(0,a.jsx)(t.strong,{children:"database\ntriggers"})," on the tables underlying the view, in a background job. Changes then flow\nbase-table trigger \u2192 master logging table \u2192 subscriber logging table \u2192 buffer, and the\nreader drains that. Those triggers persist until the subscription is erased, so a\nsubscription you no longer read is overhead on a production table. The entity also needs\n",(0,a.jsx)(t.code,{children:"@Analytics.dataExtraction.delta.changeDataCapture.automatic"})," for this path, in addition\nto extraction being enabled."]}),"\n",(0,a.jsxs)(t.p,{children:["While the preparation job is pending the engine returns ",(0,a.jsx)(t.strong,{children:"neither data nor an error"})," \u2014\nthe read waits. If no batch work process is free, it waits indefinitely. That is the\nsingle most common way a first delta looks broken when it is merely queued."]}),"\n",(0,a.jsxs)(t.p,{children:["Then the part that shapes the client design: ",(0,a.jsx)(t.strong,{children:"the engine commits each package as it\nhands
1it over."})," There is no client acknowledgement in the protocol. Once a roundtrip\nreturns, SAP considers those changes delivered and will not re-send them. If your\nprocess dies holding a package, that package is gone from SAP's point of view. So\nat-least-once has to be built on the client side, which both our implementations do by\nwriting each package to a local spill table before its rows are consumed."]}),"\n",(0,a.jsx)(t.p,{children:"One measured limitation belongs here rather than in a footnote. On our trial system, one\ncycle window carries a commit's first two DMLs, so a change from a three-statement commit\ncan land a cycle later rather than immediately. Counts and keys stay exact and a delete\narrives within one or two cycles, because the snapshot seed covers the tail \u2014 but a\nconsumer built directly on the protocol without a seed would need to account for it."}),"\n",(0,a.jsx)(t.h2,{id:"how-we-know-it-is-right",children:"How we know it is right"}),"\n",(0,a.jsx)(t.p,{children:"Two claims need evidence: that the decoder reads packages correctly, and that the delta\nsemantics are what we say."}),"\n",(0,a.jsxs)(t.p,{children:["On the erpl side the SQL suite runs 12 of 12 against a live ABAP trial on both RFC\nbackends, and the offline C++ tests run 239 assertions across 49 cases. A separate\nharness mutates real data \u2014 it inserts, updates and deletes rows through a CDS view and\nasserts each operation surfaces with the right ",(0,a.jsx)(t.code,{children:"/1DH/OPERATION"}),", that a resumed\nsubscription is found rather than re-registered, and that packages replay in order when\nforced to one row per package. Correctness against the source is a symmetric ",(0,a.jsx)(t.code,{children:"EXCEPT ALL"}),"\nin both directions against ",(0,a.jsx)(t.code,{children:"sap_read_table"})," \u2014 order-insensitive but duplicate-sensitive,\nso a dropped or doubled row fails where a ",(0,a.jsx)(t.code,{children:"count(*)"})," would pass. Recovery is tested by\npointing the connection at a dead host and confirming the replay still works, with a\nnegative control that proves an ordinary read against that host does fail."]}),"\n",(0,a.jsxs)(t.p,{children:["The strongest evidence is structural, though. ",(0,a.jsx)(t.code,{children:"erpl-rev"})," implements this protocol\n",(0,a.jsx)(t.strong,{children:"independently"})," \u2014 no shared code with ",(0,a.jsx)(t.code,{children:"erpl"}),", no vendored library, the spec document\ncited as specification only, and a golden fixture pinning the exact graph JSON. Two\nimplementations written from one written spec, agreeing against the same system, is\nevidence about the spec. One library agreeing with itself is not."]}),"\n",(0,a.jsx)(t.p,{children:"We are also explicit about where evidence is thinner. The unit suites and the first\nregistration suite run in CI; the delta, recovery and hundred-thousand-row volume suites\nare ABAP test classes run against the trial, not yet wired into the end-to-end script."}),"\n",(0,a.jsx)(t.h2,{id:"using-it-from-erpl",children:"Using it from erpl"}),"\n",(0,a.jsxs)(t.p,{children:[(0,a.jsx)(t.code,{children:"erpl"})," is the DuckDB extension. ",(0,a.jsx)(t.code,{children:"LOAD erpl"})," brings it in beside RFC, BICS and ODP."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-sql",children:"-- Find the entity, then look at it without creating anything server-side\nSELECT cds_name, object_path, is_released FROM sap_ape_show(search => 'SALESORDER');\nSELECT * FROM sap_ape_preview('ZERPL_SALESORDER', max_rows => 20);\n"})}),"\n",(0,a.jsx)(t.p,{children:"A snapshot is one function. It creates a subscription for the scan and erases it\nafterwards, including when you abandon the scan early:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-sql",children:"SELECT * FROM sap_ape_read_full('ZERPL_SALESORDER',\n columns => ['SalesOrder', 'SoldToParty', 'TotalNetAmount']);\n"})}),"\n",(0,a.jsx)(t.p,{children:"Delta takes a second argument naming the consumer. The same name resumes:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-sql",children:"-- First call: initial load, and the triggers get generated\nSELECT count(*) FROM sap_ape_read_delta('ZERPL_SALESORDER', 'NIGHTLY_ETL');\n"})}
1),"\n",(0,a.jsx)(t.p,{children:"Then someone inserts one order, updates another and deletes a third, and the next\ncall reports all three:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-sql",children:"SELECT \"/1DH/OPERATION\" AS op, SalesOrder, SoldToParty, TotalNetAmount\nFROM sap_ape_read_delta('ZERPL_SALESORDER', 'NIGHTLY_ETL') ORDER BY SalesOrder;\n"})}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-text",children:"\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 op \u2502 SalesOrder \u2502 SoldToParty \u2502 TotalNetAmount \u2502\n\u2502 varchar \u2502 varchar \u2502 varchar \u2502 decimal(15,2) \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 U \u2502 R0002 \u2502 updated \u2502 20.00 \u2502\n\u2502 D \u2502 R0003 \u2502 \u2502 0.00 \u2502\n\u2502 U \u2502 R0004 \u2502 inserted \u2502 40.00 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,a.jsxs)(t.p,{children:["That is the whole argument in one result set. ",(0,a.jsx)(t.code,{children:"R0004"})," was inserted and ",(0,a.jsx)(t.code,{children:"R0002"})," updated \u2014\nboth arrive as ",(0,a.jsx)(t.code,{children:"U"}),", both carrying their current state. ",(0,a.jsx)(t.code,{children:"R0003"})," was deleted, and it arrives\nas ",(0,a.jsx)(t.code,{children:"D"})," with its key and nothing else. A timestamp-based delta returns the first two rows\nand has no way to tell you about the third."]}),"\n",(0,a.jsxs)(t.p,{children:["The run above is reproducible: the entity is a fixture view over a small table on the free\nABAP Platform Trial, and the tape that drives it is committed next to this post in\n",(0,a.jsx)(t.code,{children:"demo/ape-lifecycle.tape"}),"."]}),"\n",(0,a.jsx)(t.p,{children:"And after a crash, the replay \u2014 which reads the local spill and contacts SAP not at all,\nso it works when SAP is unreachable:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-sql",children:"SELECT * FROM sap_ape_read_delta('ZERPL_SALESORDER', 'NIGHTLY_ETL', recover => true);\n"})}),"\n",(0,a.jsxs)(t.p,{children:["Note what is ",(0,a.jsx)(t.em,{children:"not"})," here. ",(0,a.jsx)(t.code,{children:"filters"})," exists in the signature and is ",(0,a.jsx)(t.strong,{children:"refused"}),", because\nthe v6 reader reads no filter key: emitting one would return every row while your query\nlooked filtered. Filter with a SQL ",(0,a.jsx)(t.code,{children:"WHERE"})," instead \u2014 the scan streams, so DuckDB applies\npredicates as rows arrive."]}),"\n",(0,a.jsx)(t.h2,{id:"using-it-from-erpl-rev",children:"Using it from erpl-rev"}),"\n",(0,a.jsxs)(t.p,{children:[(0,a.jsx)(t.a,{href:"/blog/erpl-rev-10m-bseg-rows-a-minute",children:(0,a.jsx)(t.code,{children:"erpl-rev"})})," is the inverse product: instead of\nDuckDB calling into SAP, SAP calls out into DuckDB. APE arrives there as two more values\nin an existing delta-method enum, registered like any other target:"]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-bash",children:"erpl-rev sync create sales \\\n --method APE_DELTA --source ZERPL_SALESORDER --keys SalesOrder \\\n --subscriber-process NIGHTLY_ETL --chunk-size 20000 \\\n --cadence hourly\n"})}),"\n",(0,a.jsxs)(t.p,{children:["From then on the existing machinery owns it: the scheduler, the per-target lease, the run\nstatistics, ",(0,a.jsx)(t.code,{children:"erpl-rev top"}),". Each package is spilled and then merged into the target \u2014\n",(0,a.jsx)(t.code,{children:"U"})," rows upserted by key, ",(0,a.jsx)(t.code,{children:"D"})," rows deleted by key. ",(0,a.jsx)(t.code,{children:"erpl-rev sync drop sales"})," erases the\nSAP-side subscription and its spill, and keeps the DuckDB table."]}),"\n",(0,a.jsxs)(t.p,{children:["One rule differs from the Open-SQL methods: ",(0,a.jsx)(t.code,{children:"micro:*"})," cadences are refused for APE.\nA two-second cycle makes no sense against an engine whose preparation alone takes tens of\nseconds."]}),"\n",(0,a.jsx)(t.h2,{id:"one-protocol-two-directions",children:"One protocol, two directions"}),"\n",(0,a.jsx)(t.p,{children:"Here is the part we find most interesting, and it is not a feature \u2014 it is a\nconsequence."}),"\n",(0,a.jsxs)(t.p,{children:["In ",(0,a.jsx)(t.code,{children:"erpl"}),", ",(0,a.jsx)(t.strong,{children:"DuckDB is the RFC client."})," Your session opens a connection into SAP, drives\n",(0,a.jsx)(t.code,{children:"DHAPE_GRAPH_MANAGER"}),", polls ",(0,a.jsx)(t.code,{children:"DHAPE_GRAPH_ROUNDTRIP"}),", and rows arrive as a table\nfunction you compose in SQL."]}),"\n",(0,a.jsxs)(t.p,{children:["In ",(0,a.jsx)(t.code,{children:"erpl-rev"}),", ",(0,a.jsx)(t.strong,{children:"ABAP is the client."})," The server registers at the SAP gateway as an RFC\ndestination; an ABAP job creates the graph, polls it, and pushes each package out to the\nserver, which is a sink. Graph creation, polling and stop all happen inside one SAP\nsession, which gives the session affinity the protocol requires by construction rather\nthan by care."]}),"\n",(0,a.jsx)(t.p,{children:"Same protocol, same operator, opposite drive direction. Which you want is mostly a\nquestion about your network and your operating model, not about SAP:"}),"\n",(0,a.jsxs)(t.table,{children:[(0,a.jsx)(t.thead,{children:(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.th,{}),(0,a.jsx)(t.th,{children:(0,a.jsx)(t.code,{children:"erpl"})}),(0,a.jsx)(t.th,{children:(0,a.jsx)(t.code,{children:"erpl-rev"})})]})}),(0,a.jsxs)(t.tbody,{children:[(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:"Who initiates"}),(0,a.jsx)(t.td,{children:"your SQL session"}),(0,a.jsx)(t.td,{children:"an ABAP job inside SAP"})]}),(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:"Connection opens"}),(0,a.jsx)(t.td,{children:"DuckDB host \u2192 SAP"}),(0,a.jsx)(t.td,{children:"SAP \u2192 the registered destination"})]}),(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:"The APE client is"}),(0,a.jsx)(t.td,{children:"the DuckDB extension"}),(0,a.jsx)(t.td,{children:"ABAP"})]}),(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:"Resume token"}),(0,a.jsx)(t.td,{children:"the named SAP subscription"}),(0,a.jsx)(t.td,{children:"the same"})]}),(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:"Scheduling"}),(0,a.jsx)(t.td,{children:"you run the query"}),(0,a.jsx)(t.td,{children:"cadence, lease, run stats"})]}),(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:"Output"}),(0,a.jsx)(t.td,{children:"a table function to compose"}),(0,a.jsx)(t.td,{children:"a merged table, publishable to Parquet, DuckLake, Iceberg, Postgres"})]}),(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:"Reach for it when"}),(0,a.jsx)(t.td,{children:"exploring, ad-hoc work, composing in DuckDB"}),(0,a.jsxs)(t.td,{children:["unattended replication, or when nothing may connect ",(0,a.jsx)(t.em,{children:"into"})," SAP"]})]})]})]}),"\n",(0,a.jsx)(t.p,{children:"The second row is the one that decides most architectures. If your security model has no\noutbound path from the analytics host into SAP, the pull model is not available to you at\nany price, and the push model is."}),"\n",(0,a.jsx)(t.h2,{id:"proving-the-scope-to-whoever-has-to-approve-it",children:"Proving the scope to whoever has to approve it"}),"\n",(0,a.jsx)(t.p,{children:"An extraction tool claiming it only reads CDS is worth exactly as much as the claim is\ncheckable. This one is checkable, and not by reading our source."}),"\n",(0,a.jsxs)(t.p,{children:["The engine checks ",(0,a.jsx)(t.code,{children:"S_DHAPEOPR"})," ",(0,a.jsx)(t.strong,{children:"per operator name"}),", in\n",(0,a.jsx)(t.code,{children:"CL_DHAPE_OPERATOR_REGISTRY"}),', as the graph resolves its processes. That means a role can\nexpress "this user may drive the CDS reader and n
1othing else" \u2014 and a role granting only\nour four operators refuses an ODP or SLT operator regardless of what the client sends.']}),"\n",(0,a.jsxs)(t.p,{children:["There is one trap, and it is worth knowing before an audit rather than after. When the\n",(0,a.jsx)(t.code,{children:"S_DHAPEOPR"})," check fails, the same method falls back to the older object ",(0,a.jsx)(t.code,{children:"S_DHAPEOP"}),",\nkeyed on the operator's implementation class. A user holding that \u2014 from this role or any\nother \u2014 is authorised for operators ",(0,a.jsx)(t.code,{children:"S_DHAPEOPR"})," denies. It must not be granted."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-sql",children:"SELECT auth_object, checked_values, verdict, note\nFROM sap_ape_check_authorizations('ZERPL_SALESORDER')\nWHERE verdict <> 'ok';\n"})}),"\n",(0,a.jsxs)(t.p,{children:["Each requirement is probed live with ",(0,a.jsx)(t.code,{children:"AUTHORITY_CHECK"})," and reported as a verdict. The\n",(0,a.jsx)(t.a,{href:"/docs/security/ape-role",children:"role page"})," is written to be handed to a Basis team."]}
1),"\n",(0,a.jsx)(t.h2,{id:"provenance-and-legality",children:"Provenance and legality"}),"\n",(0,a.jsx)(t.p,{children:"We should be precise about what we are and are not claiming."}),"\n",(0,a.jsxs)(t.p,{children:["SAP Note 3255746 restricts the ",(0,a.jsx)(t.strong,{children:"ODP Data Replication API's RFC modules"}),". It names\nneither ",(0,a.jsx)(t.code,{children:"DHAPE_*"})," nor ",(0,a.jsx)(t.code,{children:"DHAMB_*"}),". It names table and CDS view extraction, BAPIs, function\nmodules and DeltaQ as remaining permitted, and points customers at SAP Datasphere for\nSAP-to-third-party replication. The engine described here is the runtime that Datasphere\nitself uses for ABAP sources, and the CDS reader operator is publicly documented, as is\nthe workbench transaction and the S/4HANA Cloud communication scenario for it."]}),"\n",(0,a.jsxs)(t.p,{children:["What we do ",(0,a.jsx)(t.strong,{children:"not"})," have is a statement from SAP releasing ",(0,a.jsx)(t.code,{children:"DHAPE_*"})," and ",(0,a.jsx)(t.code,{children:"DHAMB_*"})," for\nthird-party consumption under a C1 contract. Those modules are not published as released\nAPIs, and our reading of an adjacent permission is a reading, not a clearance. We have\nasked. If the answer is that this is not permitted, the path ships disabled rather than\nquietly."]}),"\n",(0,a.jsxs)(t.p,{children:["The protocol description above was derived by reading ABAP source through ADT on a system\nwe are licensed to use, and verified by observing the documented interfaces behave as\ndescribed. No SAP code is copied, linked or redistributed. If you are evaluating this for\nproduction, run ",(0,a.jsx)(t.a,{href:"https://userapps.support.sap.com/sap/support/knowledge/en/3439624",children:"SAP Note 3439624"}),"'s\nself-assessment on your own ODP-RFC exposure first \u2014 it will tell you how much time you\nactually have, which is the number that should drive the decision."]}),"\n",(0,a.jsx)(t.h2,{id:"what-to-do-with-this",children:"What to do with this"}),"\n",(0,a.jsx)(t.p,{children:"If you have an ODP-RFC pipeline, the deadline is real and already partly enforced, and\nthe note points at CDS extraction. If the entity you need is modelled in CDS \u2014 and\nincreasingly it is \u2014 this path reads it, and reports the deletes the timestamp-based\npath structurally cannot."}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-sql",children:"INSTALL erpl FROM 'http://get.erpl.io';\nLOAD erpl;\n"})}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-bash",children:"uvx erpl-rev doctor\n"})}),"\n",(0,a.jsxs)(t.p,{children:["The protocol spec, the role handout and the deliberate non-goals are in the\n",(0,a.jsx)(t.a,{href:"https://github.com/DataZooDE/erpl",children:"erpl repository"}),"; the\n",(0,a.jsx)(t.a,{href:"/docs/erpl/ape",children:"APE guide"})," and the\n",(0,a.jsx)(t.a,{href:"/docs/guides/advanced/ape-delta-extraction",children:"delta and recovery guide"})," cover the\nday-to-day. Two implementations agreeing is good evidence but it is not your system \u2014 if\nthis behaves differently on yours, that is the interesting case and I would like to hear\nabout it. Come argue with me\n",(0,a.jsx)(t.a,{href:"https://www.linkedin.com/in/joachim-rosskopf/",children:"on LinkedIn"}),"."]})]})}function c(e={}){const{wrapper:t}={...(0,i.R)(),...e.components};return t?(0,a.jsx)(t,{...e,children:(0,a.jsx)(h,{...e})}):h(e)}},11767:(e,t,n)=>{n.d(t,{A:()=>s});const s=n.p+"assets/images/hero-f7629edebec2c92157f2b5fbd4d413e8.jpg"},28453:(e,t,n)=>{n.d(t,{R:()=>r,x:()=>o});var s=n(96540);const a={},i=s.createContext(a);function r(e){const t=s.useContext(i);return s.useMemo(function(){return"function"==typeof e?e(t):{...t,...e}},[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),s.createElement(i.Provider,{value:t},e.children)}},31726:(e,t,n)=>{n.d(t,{A:()=>s});const s=n.p+"assets/images/hero-f7629edebec2c92157f2b5fbd4d413e8.jpg"},54870:e=>{e.exports=JSON.parse('{"permalink":"/blog/sap-cds-delta-extraction-abap-pipeline-engine","editUrl":"https://github.com/datazoode/erpl-landingpage/tree/main/blog/2026-09-23-sap-cds-delta-abap-pipeline-engine/index.md","source":"@site/blog/2026-09-23-sap-cds-delta-abap-pipeline-engine/index.md","title":"SAP Blocked ODP-RFC. Here Is CDS Delta, Deletes Included.","description":"SAP Note 3255746 prohibits third-party use of the ODP-RFC API, a security patch has enforced it since June, and the opt-out expires at the end of 2026. The note points at CDS view extraction instead. We built that path on the ABAP Pipeline Engine \u2014 the same runtime SAP Datasphere uses \u2014 and it reports deletes, which ODP\'s CDS delta cannot.","date":"2026-09-23T00:00:00.000Z","tags":[{"inline":false,"label":"ERPL","permalink":"/blog/tags/erpl","description":"ERPL extension tutorials"}
1,{"inline":false,"label":"SAP","permalink":"/blog/tags/sap","description":"SAP system integration"},{"inline":false,"label":"ABAP","permalink":"/blog/tags/abap","description":"ABAP development and tooling"},{"inline":false,"label":"ABAP CDS","permalink":"/blog/tags/cds","description":"ABAP Core Data Services views and entities"},{"inline":false,"label":"Delta & CDC","permalink":"/blog/tags/delta","description":"Delta extraction and change data capture"},{"inline":false,"label":"DuckDB","permalink":"/blog/tags/duckdb","description":"DuckDB database tutorials"},{"inline":false,"label":"SAP Integration","permalink":"/blog/tags/sap-integration","description":"SAP system integration"},{"inline":false,"label":"Real-time","permalink":"/blog/tags/real-time","description":"Real-time data processing"}],"readingTime":14.56,"hasTruncateMarker":true,"authors":[{"name":"Joachim Rosskopf","title":"Co-Founder & CEO","url":"https://data-zoo.de","socials":{"linkedin":"https://www.linkedin.com/in/joachim-rosskopf/","github":"https://github.com/jrosskopf"},"imageURL":"/images/profiles/profile_jr.png","key":"joachim-rosskopf","page":null}],"frontMatter":{"slug":"sap-cds-delta-extraction-abap-pipeline-engine","title":"SAP Blocked ODP-RFC. Here Is CDS Delta, Deletes Included.","authors":["joachim-rosskopf"],"tags":["erpl","sap","abap","cds","delta","duckdb","sap-integration","real-time"],"date":"2026-09-23T00:00:00.000Z","image":"./img/hero.jpg","description":"SAP Note 3255746 prohibits third-party use of the ODP-RFC API, a security patch has enforced it since June, and the opt-out expires at the end of 2026. The note points at CDS view extraction instead. We built that path on the ABAP Pipeline Engine \u2014 the same runtime SAP Datasphere uses \u2014 and it reports deletes, which ODP\'s CDS delta cannot."},"unlisted":false,"nextItem":{"title":"From Nothing to 2.8 Million SAP Rows in Ten Minutes","permalink":"/blog/sap-to-duckdb-in-ten-minutes"}}')}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.