Skip to main content

SAP Blocked ODP-RFC. Here Is CDS Delta, Deletes Included.

· 15 min read
Joachim Rosskopf
Co-Founder & CEO

Every SAP extraction pipeline built in the last decade rests on one of a handful of interfaces. For a great many of them, that interface is ODP-RFC — the RFC modules of the Operational Data Provisioning framework, RODPS_REPL_*.

SAP Note 3255746 prohibits third-party use of those modules. Not deprecates: prohibits. They are defined as being exclusively for data transfer between SAP applications, and the prohibition covers customer and third-party applications reading S/4HANA, BW or ECC, on-premise or in private cloud. Since 9 June 2026 a security patch in the support packages technically blocks the calls. A temporary opt-out carries non-compliant integrations to the end of 2026, and then it does not.

The note does not leave you without a path. It names what remains permitted, and one item on that list is where SAP developers have been putting their semantics for years: CDS view extraction. So we built that path — and the engine we built it on turns out to be the one SAP's own recommended alternative already runs on.

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.

Where the data went

Ask an ABAP developer where the business logic lives today and you will hear CDS. Released entities with C1 contracts, annotations carrying meaning, a decade of modelling effort. The semantic layer moved.

Extraction did not follow it cleanly. ODP treats ABAP_CDS as one context among five, and its delta on a CDS view requires the view to carry @Analytics.dataExtraction.delta.byElement — an annotation pointing at a change timestamp, set at design time by whoever built the view. If it is not there, you get no delta. If you do not own the view, you may not be able to have it added.

And there is a limit that no annotation fixes. A change-timestamp delta finds rows whose timestamp moved. A deleted row has no timestamp left to move. It is simply absent, and absence is not something a WHERE changed_at > watermark can return. So a warehouse fed by that mechanism accumulates rows that no longer exist in SAP, quietly, until someone reconciles counts and finds the drift.

That is the gap worth closing, and closing it needs change capture at the database level rather than a timestamp in a column.

The engine that was already there

The ABAP Pipeline Engine — function modules under DHAPE_*, with the ABAP Metadata Browser under DHAMB_* for discovery — is an ABAP-based data-flow runtime that ships inside the stack. It runs a small graph of operators: a reader, a channel, an outport. There is a GUI workbench for it on transaction DHAPE.

It is not obscure because it is unimportant. It is obscure because it is normally invisible: it is the runtime SAP Datasphere replication flows use to extract from ABAP sources, and the mechanism behind ABAP integration in SAP Data Intelligence. When SAP tells you to move your CDS extraction to Datasphere, this is what Datasphere then does on your ABAP system.

The operator we care about is publicly named: the ABAP CDS Reader, com.sap.abap.cds.reader.v2. On S/4HANA Cloud it is reached through communication scenario SAP_COM_0532. Nothing is installed to use it — no transport, no Z objects, no ABAP. The engine is already in your system.

The protocol, in one section

You create a graph by handing DHAPE_GRAPH_MANAGER a JSON definition: named processes, each with a Component naming its operator, and channels between their ports. Protocol v6 starts the graph during create, and the graph is bound to your RFC session.

The reader takes seven configuration keys and no others: subscriptionType, subscriptionID, subscriptionName, cdsname, action, chunkSize, wireformat. action is what selects the mode — Initial Load for a snapshot, Replication for delta. There is no filter key and no schema key, which matters later.

DHAPE_GRAPH_ROUNDTRIP is the only call that moves data. Each roundtrip returns a package: a JSON envelope carrying Encoding: "csv", a self-describing Attributes.ABAP.Fields[] block so every package declares its own schema, a batchIndex, and a lastBatch marker. The body is RFC 4180 quoted — a comma inside a value arrives as "comma,inside", a quote as "quote""inside" — so a decoder that splits on commas will shift every later column on the first free-text field it meets.

Delta rows carry one extra column, /1DH/OPERATION:

ValueMeaning
Uthe row's current state — an insert or an update, both as after-images
Dthe row was deleted; only the key fields are populated

Inserts and updates are not distinguished, because the engine sends after-images for both. The delete shape is not our interpretation either: SAP's own description of ABAP CDC is that for deletions the key columns are populated and all other columns are blank. That single row type is the whole reason for this work.

The normative spec we wrote and work from is ape/docs/protocol.md — fourteen sections, with each claim marked as verified against a live system or still open.

Delta, and what it costs

Delta is not free, and the costs are worth knowing before you enable it.

The first delta read on an entity makes SAP generate DHCDC logging tables and database triggers on the tables underlying the view, in a background job. Changes then flow base-table trigger → master logging table → subscriber logging table → buffer, and the reader drains that. Those triggers persist until the subscription is erased, so a subscription you no longer read is overhead on a production table. The entity also needs @Analytics.dataExtraction.delta.changeDataCapture.automatic for this path, in addition to extraction being enabled.

While the preparation job is pending the engine returns neither data nor an error — the read waits. If no batch work process is free, it waits indefinitely. That is the single most common way a first delta looks broken when it is merely queued.

Then the part that shapes the client design: the engine commits each package as it hands it over. There is no client acknowledgement in the protocol. Once a roundtrip returns, SAP considers those changes delivered and will not re-send them. If your process dies holding a package, that package is gone from SAP's point of view. So at-least-once has to be built on the client side, which both our implementations do by writing each package to a local spill table before its rows are consumed.

One measured limitation belongs here rather than in a footnote. On our trial system, one cycle window carries a commit's first two DMLs, so a change from a three-statement commit can land a cycle later rather than immediately. Counts and keys stay exact and a delete arrives within one or two cycles, because the snapshot seed covers the tail — but a consumer built directly on the protocol without a seed would need to account for it.

How we know it is right

Two claims need evidence: that the decoder reads packages correctly, and that the delta semantics are what we say.

On the erpl side the SQL suite runs 12 of 12 against a live ABAP trial on both RFC backends, and the offline C++ tests run 239 assertions across 49 cases. A separate harness mutates real data — it inserts, updates and deletes rows through a CDS view and asserts each operation surfaces with the right /1DH/OPERATION, that a resumed subscription is found rather than re-registered, and that packages replay in order when forced to one row per package. Correctness against the source is a symmetric EXCEPT ALL in both directions against sap_read_table — order-insensitive but duplicate-sensitive, so a dropped or doubled row fails where a count(*) would pass. Recovery is tested by pointing the connection at a dead host and confirming the replay still works, with a negative control that proves an ordinary read against that host does fail.

The strongest evidence is structural, though. erpl-rev implements this protocol independently — no shared code with erpl, no vendored library, the spec document cited as specification only, and a golden fixture pinning the exact graph JSON. Two implementations written from one written spec, agreeing against the same system, is evidence about the spec. One library agreeing with itself is not.

We are also explicit about where evidence is thinner. The unit suites and the first registration suite run in CI; the delta, recovery and hundred-thousand-row volume suites are ABAP test classes run against the trial, not yet wired into the end-to-end script.

Using it from erpl

erpl is the DuckDB extension. LOAD erpl brings it in beside RFC, BICS and ODP.

-- Find the entity, then look at it without creating anything server-side
SELECT cds_name, object_path, is_released FROM sap_ape_show(search => 'SALESORDER');
SELECT * FROM sap_ape_preview('ZERPL_SALESORDER', max_rows => 20);

A snapshot is one function. It creates a subscription for the scan and erases it afterwards, including when you abandon the scan early:

SELECT * FROM sap_ape_read_full('ZERPL_SALESORDER',
columns => ['SalesOrder', 'SoldToParty', 'TotalNetAmount']);

Delta takes a second argument naming the consumer. The same name resumes:

-- First call: initial load, and the triggers get generated
SELECT count(*) FROM sap_ape_read_delta('ZERPL_SALESORDER', 'NIGHTLY_ETL');

Then someone inserts one order, updates another and deletes a third, and the next call reports all three:

SELECT "/1DH/OPERATION" AS op, SalesOrder, SoldToParty, TotalNetAmount
FROM sap_ape_read_delta('ZERPL_SALESORDER', 'NIGHTLY_ETL') ORDER BY SalesOrder;
┌─────────┬────────────┬─────────────┬────────────────┐
│ op │ SalesOrder │ SoldToParty │ TotalNetAmount │
│ varchar │ varchar │ varchar │ decimal(15,2) │
├─────────┼────────────┼─────────────┼────────────────┤
│ U │ R0002 │ updated │ 20.00 │
│ D │ R0003 │ │ 0.00 │
│ U │ R0004 │ inserted │ 40.00 │
└─────────┴────────────┴─────────────┴────────────────┘

That is the whole argument in one result set. R0004 was inserted and R0002 updated — both arrive as U, both carrying their current state. R0003 was deleted, and it arrives as D with its key and nothing else. A timestamp-based delta returns the first two rows and has no way to tell you about the third.

The run above is reproducible: the entity is a fixture view over a small table on the free ABAP Platform Trial, and the tape that drives it is committed next to this post in demo/ape-lifecycle.tape.

And after a crash, the replay — which reads the local spill and contacts SAP not at all, so it works when SAP is unreachable:

SELECT * FROM sap_ape_read_delta('ZERPL_SALESORDER', 'NIGHTLY_ETL', recover => true);

Note what is not here. filters exists in the signature and is refused, because the v6 reader reads no filter key: emitting one would return every row while your query looked filtered. Filter with a SQL WHERE instead — the scan streams, so DuckDB applies predicates as rows arrive.

Using it from erpl-rev

erpl-rev is the inverse product: instead of DuckDB calling into SAP, SAP calls out into DuckDB. APE arrives there as two more values in an existing delta-method enum, registered like any other target:

erpl-rev sync create sales \
--method APE_DELTA --source ZERPL_SALESORDER --keys SalesOrder \
--subscriber-process NIGHTLY_ETL --chunk-size 20000 \
--cadence hourly

From then on the existing machinery owns it: the scheduler, the per-target lease, the run statistics, erpl-rev top. Each package is spilled and then merged into the target — U rows upserted by key, D rows deleted by key. erpl-rev sync drop sales erases the SAP-side subscription and its spill, and keeps the DuckDB table.

One rule differs from the Open-SQL methods: micro:* cadences are refused for APE. A two-second cycle makes no sense against an engine whose preparation alone takes tens of seconds.

One protocol, two directions

Here is the part we find most interesting, and it is not a feature — it is a consequence.

In erpl, DuckDB is the RFC client. Your session opens a connection into SAP, drives DHAPE_GRAPH_MANAGER, polls DHAPE_GRAPH_ROUNDTRIP, and rows arrive as a table function you compose in SQL.

In erpl-rev, ABAP is the client. The server registers at the SAP gateway as an RFC destination; an ABAP job creates the graph, polls it, and pushes each package out to the server, which is a sink. Graph creation, polling and stop all happen inside one SAP session, which gives the session affinity the protocol requires by construction rather than by care.

Same protocol, same operator, opposite drive direction. Which you want is mostly a question about your network and your operating model, not about SAP:

erplerpl-rev
Who initiatesyour SQL sessionan ABAP job inside SAP
Connection opensDuckDB host → SAPSAP → the registered destination
The APE client isthe DuckDB extensionABAP
Resume tokenthe named SAP subscriptionthe same
Schedulingyou run the querycadence, lease, run stats
Outputa table function to composea merged table, publishable to Parquet, DuckLake, Iceberg, Postgres
Reach for it whenexploring, ad-hoc work, composing in DuckDBunattended replication, or when nothing may connect into SAP

The second row is the one that decides most architectures. If your security model has no outbound path from the analytics host into SAP, the pull model is not available to you at any price, and the push model is.

Proving the scope to whoever has to approve it

An extraction tool claiming it only reads CDS is worth exactly as much as the claim is checkable. This one is checkable, and not by reading our source.

The engine checks S_DHAPEOPR per operator name, in CL_DHAPE_OPERATOR_REGISTRY, as the graph resolves its processes. That means a role can express "this user may drive the CDS reader and nothing else" — and a role granting only our four operators refuses an ODP or SLT operator regardless of what the client sends.

There is one trap, and it is worth knowing before an audit rather than after. When the S_DHAPEOPR check fails, the same method falls back to the older object S_DHAPEOP, keyed on the operator's implementation class. A user holding that — from this role or any other — is authorised for operators S_DHAPEOPR denies. It must not be granted.

SELECT auth_object, checked_values, verdict, note
FROM sap_ape_check_authorizations('ZERPL_SALESORDER')
WHERE verdict <> 'ok';

Each requirement is probed live with AUTHORITY_CHECK and reported as a verdict. The role page is written to be handed to a Basis team.

Provenance and legality

We should be precise about what we are and are not claiming.

SAP Note 3255746 restricts the ODP Data Replication API's RFC modules. It names neither DHAPE_* nor DHAMB_*. It names table and CDS view extraction, BAPIs, function modules and DeltaQ as remaining permitted, and points customers at SAP Datasphere for SAP-to-third-party replication. The engine described here is the runtime that Datasphere itself uses for ABAP sources, and the CDS reader operator is publicly documented, as is the workbench transaction and the S/4HANA Cloud communication scenario for it.

What we do not have is a statement from SAP releasing DHAPE_* and DHAMB_* for third-party consumption under a C1 contract. Those modules are not published as released APIs, and our reading of an adjacent permission is a reading, not a clearance. We have asked. If the answer is that this is not permitted, the path ships disabled rather than quietly.

The protocol description above was derived by reading ABAP source through ADT on a system we are licensed to use, and verified by observing the documented interfaces behave as described. No SAP code is copied, linked or redistributed. If you are evaluating this for production, run SAP Note 3439624's self-assessment on your own ODP-RFC exposure first — it will tell you how much time you actually have, which is the number that should drive the decision.

What to do with this

If you have an ODP-RFC pipeline, the deadline is real and already partly enforced, and the note points at CDS extraction. If the entity you need is modelled in CDS — and increasingly it is — this path reads it, and reports the deletes the timestamp-based path structurally cannot.

INSTALL erpl FROM 'http://get.erpl.io';
LOAD erpl;
uvx erpl-rev doctor

The protocol spec, the role handout and the deliberate non-goals are in the erpl repository; the APE guide and the delta and recovery guide cover the day-to-day. Two implementations agreeing is good evidence but it is not your system — if this behaves differently on yours, that is the interesting case and I would like to hear about it. Come argue with me on LinkedIn.