1"use strict";(self.webpackChunkerpl_docusaurus=self.webpackChunkerpl_docusaurus||[]).push([[861],{2477:e=>{e.exports=JSON.parse('{"permalink":"/blog/sap-duckdb-replication-faster-ingest-no-sap-sdk","editUrl":"https://github.com/datazoode/erpl-landingpage/tree/main/blog/2026-08-29-erpl-rev-faster-ingest-no-sdk/index.md","source":"@site/blog/2026-08-29-erpl-rev-faster-ingest-no-sdk/index.md","title":"erpl-rev: 2.5\xd7 Faster Ingest, and the SAP SDK Is Gone","description":"In June we shipped erpl-rev and explained why it was fast. Profiling it since turned up two more improvements: the DuckDB staging table can go entirely, and so can the SAP NW RFC SDK \u2014 replaced by erpl-proto in pure Rust. Ingest is now 2.5x faster in process. Here is where that shows up end to end, and where it does not.","date":"2026-08-29T00:00:00.000Z","tags":[{"inline":false,"label":"ERPL","permalink":"/blog/tags/erpl","description":"ERPL extension tutorials"},{"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":"DuckDB","permalink":"/blog/tags/duckdb","description":"DuckDB database tutorials"},{"inline":false,"label":"Performance","permalink":"/blog/tags/performance","description":"Performance optimization"},{"inline":false,"label":"SAP Integration","permalink":"/blog/tags/sap-integration","description":"SAP system integration"}],"readingTime":9.7,"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-duckdb-replication-faster-ingest-no-sap-sdk","title":"erpl-rev: 2.5\xd7 Faster Ingest, and the SAP SDK Is Gone","authors":["joachim-rosskopf"],"tags":["erpl","sap","abap","duckdb","performance","sap-integration"],"date":"2026-08-29T00:00:00.000Z","image":"./img/hero.jpg","description":"In June we shipped erpl-rev and explained why it was fast. Profiling it since turned up two more improvements: the DuckDB staging table can go entirely, and so can the SAP NW RFC SDK \u2014 replaced by erpl-proto in pure Rust. Ingest is now 2.5x faster in process. Here is where that shows up end to end, and where it does not."},"unlisted":false,"prevItem":{"title":"PyRFC Is Archived. Here Is a Drop-In Replacement.","permalink":"/blog/drop-in-replacement-for-archived-pyrfc"},"nextItem":{"title":"Reach On-Prem Postgres and SAP from a Cloud Container \u2014 Without Opening a Port","permalink":"/blog/reach-on-prem-postgres-sap-from-cloud-duckdb"}}')},3933:(e,t,n)=>{n.d(t,{A:()=>s});const s=n.p+"assets/images/hero-3f3bc1b16d6c0dd71ce549b4dfa312b9.jpg"},28453:(e,t,n)=>{n.d(t,{R:()=>o,x:()=>a});var s=n(96540);const i={},r=s.createContext(i);function o(e){const t=s.useContext(r);return s.useMemo(function(){return"function"==typeof e?e(t):{...t,...e}},[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),s.createElement(r.Provider,{value:t},e.children)}},69155:(e,t,n)=>{n.d(t,{A:()=>s});const s=n.p+"assets/images/perf-gradient-469caeb63f3a73f95725343aa0579a59.png"},78792:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>c,frontMatter:()=>o,metadata:()=>s,toc:()=>d});var s=n(2477),i=n(74848),r=n(28453);const o={slug:"sap-duckdb-replication-faster-ingest-no-sap-sdk",title:"erpl-rev: 2.5\xd7 Faster Ingest, and the SAP SDK Is Gone",authors:["joachim-rosskopf"],tags:["erpl","sap","abap","duckdb","performance","sap-integration"],date:new Date("2026-08-29T00:00:00.000Z"),image:"./img/hero.jpg",description:"In June we shipped erpl-rev and explained why it was fast. Profiling it since turned up two more improvements: the DuckDB staging table can go entirely, and so can the SAP NW RFC SDK \u2014 replaced by erpl-proto in pure Rust. Ingest is now 2.5x faster in process. Here is where that shows up end to end, and where it does not."},a=void 0,l={image:n(89194).A,authorsImageUrls:[void 0]},d=[{value:"What the flamegraph showed",id:"what-the-flamegraph-showed",level:2},{value:"Twenty-one million allocations we can skip",id:"twenty-one-million-allocations-we-can-skip",level:2},{value:"The numbers, and where they stop",id:"the-numbers-and-where-they-stop",level:2},{value:"And the SDK is gone",id:"and-the-sdk-is-gone",level:2},{value:"Where this goes",id:"where-this-goes",level:2}];function h(e){const t={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(t.p,{children:["In June I wrote up ",(0,i.jsx)(t.a,{href:"/blog/erpl-rev-10m-bseg-rows-a-minute",children:"erpl-rev"}
1)," \u2014 the little\nregistered RFC server that lets ABAP call ",(0,i.jsx)(t.em,{children:"out"})," into DuckDB \u2014 and it replicated\n10 million rows of a 400-column BSEG-shaped table in about a minute. That post\ndid not just claim a number, it explained ",(0,i.jsx)(t.em,{children:"why"})," the number was what it was."]}),"\n",(0,i.jsx)(t.p,{children:"Since then I put it under a profiler to find out what was still on the table.\nTwo things came back, pulling in opposite directions. A step that genuinely\nearned its place in June still had about a third of the ingest path left in it.\nAnd a constraint I had treated as fixed \u2014 SAP's proprietary RFC library \u2014\nturned out to be removable altogether."}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{alt:"A duck engineer pointing at a flamegraph with one wide bar struck out and the layers beneath it shortened, the SDK toolbox closed and pushed aside, a Rust gear wired to the machine instead",src:n(3933).A+"",width:"1408",height:"768"})}),"\n",(0,i.jsx)(t.h2,{id:"what-the-flamegraph-showed",children:"What the flamegraph showed"}),"\n",(0,i.jsx)(t.p,{children:"Here is the sentence from the June post, verbatim:"}),"\n",(0,i.jsxs)(t.blockquote,{children:["\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Vectorized ingest."})," Each package lands via a DuckDB ",(0,i.jsx)(t.code,{children:"Appender"})," into a\nstaging clone, then one ",(0,i.jsx)(t.code,{children:"INSERT \u2026 SELECT"})," with casts \u2014 about ",(0,i.jsx)(t.strong,{children:"230\xd7 faster"}),"\nthan the naive row-by-row path."]}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"That 230\xd7 was real, and the staging clone is what delivered it. What a profiler\nshowed later is that there was still about a third of the path to reclaim\nunderneath it."}),"\n",(0,i.jsxs)(t.p,{children:["A flamegraph of a 50,000-row \xd7 420-column package put ~16% of ingest inside\n",(0,i.jsx)(t.code,{children:"StringStats::Update"}),", ",(0,i.jsx)(t.code,{children:"UncompressedStringStorage::StringAppend"}),",\n",(0,i.jsx)(t.code,{children:"Utf8Proc::Analyze"})," and ",(0,i.jsx)(t.code,{children:"HyperLogLog::Update"}),". Those are DuckDB doing its job\nproperly: computing string statistics, running UTF-8 analysis, building\nHyperLogLog distinct-count sketches, dictionary-compressing segments. All of it\ncorrect, all of it useful \u2014 for a table you are going to ",(0,i.jsx)(t.em,{children:"keep"}),"."]}),"\n",(0,i.jsxs)(t.p,{children:["Our staging clone was written once, scanned exactly once by the following\n",(0,i.jsx)(t.code,{children:"INSERT \u2026 SELECT"}),", and then thrown away. We were paying full storage price for\na table with a lifetime of about two seconds."]}),"\n",(0,i.jsxs)(t.p,{children:["DuckDB has exactly the right tool for skipping that:\n",(0,i.jsx)(t.code,{children:"QueryAppender"}),". You hand it a SQL statement and a set of column types, append\nyour rows into it, and on flush it injects the accumulated ",(0,i.jsx)(t.code,{children:"ColumnDataCollection"}),"\ninto that statement as a never-materialized CTE. No table, no storage layer, no\nstatistics \u2014 the rows go straight into the ",(0,i.jsx)(t.code,{children:"INSERT"})," or ",(0,i.jsx)(t.code,{children:"MERGE INTO"})," that\nconsumes them."]}),"\n",(0,i.jsxs)(t.p,{children:["The awkward part of the change is that the statement now has to exist ",(0,i.jsx)(t.em,{children:"before"}),"\nthe first row is appended, so building the projection and the ",(0,i.jsx)(t.code,{children:"MERGE"}),"/",(0,i.jsx)(t.code,{children:"INSERT"}),"\nSQL moved from after the decode loop into the callback that fires when the\ncolumn list arrives. The whole append then runs inside one explicit transaction,\nso an oversized package still cannot half-apply."]}),"\n",(0,i.jsx)(t.h2,{id:"twenty-one-million-allocations-we-can-skip",children:"Twenty-one million allocations we can skip"}),"\n",(0,i.jsx)(t.p,{children:"The other half of the profile was our own BXML decoder, and it had even more\nto give."}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.code,{children:"Decode"})," materialised the entire package as a ",(0,i.jsx)(t.code,{children:"vector<vector<string>>"})," before\nhanding it to the ingest layer \u2014 for a 50,000-row, 420-column package that is\n21 million ",(0,i.jsx)(t.code,{children:"std::string"})," allocations and roughly half a gigabyte resident,\nbuilt solely so the next loop could copy every cell straight back out into\nDuckDB's vectors."]}),"\n",(0,i.jsxs)(t.p,{children:["The streaming form hands each row to a callback as ",(0,i.jsx)(t.code,{children:"string_view"}),"s into the\npayload buffer, so the copy into DuckDB is the only copy that happens. Two\nsmaller things fell out of the same profile:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["Every element carried an ",(0,i.jsx)(t.strong,{children:"owned"})," name string \u2014 one allocation to copy it out\nof the interned map, one to move it into the element stack. That is 42 million\nallocations for a name that is read exactly once, on the first row, to build\nthe column list. It is a ",(0,i.jsx)(t.code,{children:"string_view"})," into the interned name now."]}),"\n",(0,i.jsxs)(t.li,{children:["Interned names lived in a hash map keyed by element id. ",(0,i.jsx)(t.code,{children:"readId"})," is a one- or\ntwo-byte form capped at 4095, so the id space is small, dense and known \u2014 a\nflat table indexed by id replaced an int hash on every element of every row."]}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"the-numbers-and-where-they-stop",children:"The numbers, and where they stop"}),"\n",(0,i.jsxs)(t.p,{children:["The in-process ingest benchmark (",(0,i.jsx)(t.code,{children:"test/bench_ingest.cpp"}
1),", no SAP involved,\nmedian of 5 runs) is unambiguous:"]}),"\n",(0,i.jsxs)(t.table,{children:[(0,i.jsx)(t.thead,{children:(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.th,{}),(0,i.jsx)(t.th,{style:{textAlign:"right"},children:"before"}),(0,i.jsx)(t.th,{style:{textAlign:"right"},children:"after"}),(0,i.jsx)(t.th,{style:{textAlign:"right"}})]})}),(0,i.jsxs)(t.tbody,{children:[(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:"decode"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"1,604 ms"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"622 ms"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"2.58\xd7"})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:"decode + append + apply"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"3,994 ms"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"1,605 ms"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:(0,i.jsx)(t.strong,{children:"2.49\xd7"})})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:"throughput"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"12,519 rows/s"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"31,153 rows/s"}),(0,i.jsx)(t.td,{style:{textAlign:"right"}})]})]})]}),"\n",(0,i.jsxs)(t.p,{children:["Now the honest part. That 2.5\xd7 is what the ",(0,i.jsx)(t.em,{children:"client"})," does. It is not what you\nget end to end, because the SAP side of the pipe does not get faster. Same\nmachine, same A4H trial, arms run alternately so that a busy period on the host\nhits both equally, a fresh DuckDB file for every single sample, medians rather\nthan best-of:"]}),"\n",(0,i.jsxs)(t.table,{children:[(0,i.jsx)(t.thead,{children:(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.th,{children:"Shape"}),(0,i.jsx)(t.th,{style:{textAlign:"right"},children:"before"}),(0,i.jsx)(t.th,{style:{textAlign:"right"},children:"after"}),(0,i.jsx)(t.th,{style:{textAlign:"right"}})]})}),(0,i.jsxs)(t.tbody,{children:[(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:"In-process ingest only (50k \xd7 420 cols)"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"12,519 rows/s"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"31,153 rows/s"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:(0,i.jsx)(t.strong,{children:"2.49\xd7"})})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsxs)(t.td,{children:["100k rows, 420 cols, ",(0,i.jsx)(t.strong,{children:"1 worker"})]}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"19 s"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"13 s"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:(0,i.jsx)(t.strong,{children:"1.46\xd7"})})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsxs)(t.td,{children:["3M rows, 420 cols, ",(0,i.jsx)(t.strong,{children:"5 workers"})]}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"190 s"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"158 s"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:(0,i.jsx)(t.strong,{children:"1.20\xd7"})})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsxs)(t.td,{children:["10M rows, ",(0,i.jsx)(t.strong,{children:"50-col slice"}),", 5 workers"]}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"82 s"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"69 s"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:(0,i.jsx)(t.strong,{children:"1.19\xd7"})})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsxs)(t.td,{children:["10M rows, ",(0,i.jsx)(t.strong,{children:"50-col slice"}),", 4 workers"]}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"91 s"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"86 s"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"1.06\xd7"})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsxs)(t.td,{children:["10M rows, ",(0,i.jsx)(t.strong,{children:"50-col slice"}),", 2 workers"]}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"122 s"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:"122 s"}),(0,i.jsx)(t.td,{style:{textAlign:"right"},children:(0,i.jsx)(t.strong,{children:"1.00\xd7"})})]})]})]}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{alt:"Speed-up by workload shape: 2.49x with no SAP in the loop, 1.46x for a single-worker 420-column load, 1.20x for a 5-worker 420-column load, 1.19x and 1.06x for 5 and 4 workers on a 50-column slice, and exactly 1.00x at 2 workers on a 50-column slice",src:n(69155).A+"",width:"966",height:"490"})}),"\n",(0,i.jsxs)(t.p,{children:["That last row is the one worth sitting with. At two workers on a narrow slice,\nthe two builds are not merely close \u2014 their distributions are ",(0,i.jsx)(t.em,{children:"identical"}),", 121\nto 123 seconds on both arms across seven rounds each. Every second of that run\nis SAP reading and serialising; the client could be twice as fast again and the\nwall clock would not move."]}),"\n",(0,i.jsx)(t.p,{children:"This is just Amdahl's law arriving on schedule, and the June post already\npointed at it when it noted that per-worker throughput tapers as workers contend\nfor the SAP read side. Optimising the client pays in proportion to how much of\nthe wall clock the client owns: everything at 2.5\xd7 when there is no SAP in the\nloop, 1.46\xd7 for a serial wide load, 1.20\xd7 once five workers share it, and\nnothing at all when two workers already saturate the read side with a narrow\nprojection."}),"\n",(0,i.jsxs)(t.p,{children:["Two things improved regardless. The first is ",(0,i.jsx)(t.strong,{children:"consistency"}),": across the wide\n5-worker runs the old code ranged 167\u2013193 s while the new code sat at 157\u2013164 s,\nand the same tightening shows wherever there was spread to begin with \u2014 at four\nworkers the old path wandered between 75 and 105 s, the new one between 84 and\n86 s. The second is ",(0,i.jsx)(t.strong,{children:"memory"})," \u2014\npeak server RSS on the 3M-row wide load dropped from 12.1 GB to 9.0 GB, because\nthose staging tables were real, resident data."]}),"\n",(0,i.jsx)(t.h2,{id:"and-the-sdk-is-gone",children:"And the SDK is gone"}),"\n",(0,i.jsxs)(t.p,{children:["The June post had a section called ",(0,i.jsx)(t.em,{children:'"One file to ship"'}),", and it opened like this:"]}),"\n",(0,i.jsxs)(t.blockquote,{children:["\n",(0,i.jsxs)(t.p,{children:["A SAP RFC server has an awkward dependency footprint: it links\n",(0,i.jsx)(t.code,{children:"libsapnwrfc"}),", which in turn ",(0,i.jsx)(t.code,{children:"dlopen"}),"s a set of ICU libraries ",(0,i.jsx)(t.em,{children:"by name"}),", plus\nwe link ",(0,i.jsx)(t.code,{children:"libduckdb"}),"."]}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"We solved that by bundling \u2014 a self-extracting binary with the SAP SDK and ICU\nriding along inside. It works, and it was the right call at the time. But the\nbetter answer is not to need them."}),"\n",(0,i.jsxs)(t.p,{children:["erpl-rev now builds against ",(0,i.jsx)(t.a,{href:"/blog/sap-rfc-protocol-byte-by-byte",children:(0,i.jsx)(t.code,{children:"erpl-proto"})}),",\nour pure-Rust implementation of the RFC protocol. It is clean-room work:\nevery rule in the specification is derived from packet captures of SAP's own\n",(0,i.jsx)(t.code,{children:"libsapnwrfc"})," talking to a live ABAP system, validated byte-for-byte by\nre-encoding captured records and by requiring a real SAP server to answer our\nbytes exactly as it answered SAP's library. No disassembly, no reading anyone\nelse's source. The ",(0,i.jsx)(t.a,{href:"/blog/sap-rfc-protocol-byte-by-byte",children:"protocol post"})," walks\nthrough the wire format and the provenance argument in detail."]}),"\n",(0,i.jsxs)(t.p,{children:["The compatibility trick is that ",(0,i.jsx)(t.code,{children:"erpl-proto"})," exposes SAP's own C ABI. erpl-rev\nis not ported to a new API \u2014 it is recompiled against a different implementation\nof the same 19 ",(0,i.jsx)(t.code,{children:"Rfc*"})," C entry points it already called, and links it statically. Built that way, the\nserver's entire non-system dependency list is:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-text",children:"$ ldd build/erpl_rev_server\n libduckdb.so => vendor/duckdb-1.5.4/libduckdb.so\n libm.so.6, libc.so.6, libstdc++.so.6, libgcc_s.so.1, libdl.so.2, libpthread.so.0\n"})}),"\n",(0,i.jsxs)(t.p,{children:["No ",(0,i.jsx)(t.code,{children:"libsapnwrfc"}),", no ",(0,i.jsx)(t.code,{children:"libsapucum"}),", no ICU \u2014 not even erpl-proto's own shared\nobject, because it is inside the binary. DuckDB is the only library left in the\npayload."]}),"\n",(0,i.jsxs)(t.p,{children:['"Compatible" is a claim that deserves evidence, so: the same 13-stage live\nend-to-end suite runs against a real A4H system on both backends and passes on\nboth, including the stage that compares every replicated cell against its SAP\nsource, and the delta/',(0,i.jsx)(t.code,{children:"MERGE"})," paths. Plus 86 test cases and 16,475 assertions\nin the unit suite."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsxs)(t.strong,{children:["All three release builds now link ",(0,i.jsx)(t.code,{children:"erpl-proto"})," statically"]})," \u2014 Linux, macOS and\nWindows. None of them fetches the SAP SDK, and each one fails its build if\nanything RFC- or ICU-shaped ends up linked. DuckDB is the only library left in\nthe payload."]}),"\n",(0,i.jsx)(t.p,{children:"The evidence behind that is not equal across the three, and it is worth saying\nso rather than letting one number stand for all of them. Linux is what the\n13-stage suite runs against a live ABAP system, every cell of a replicated table\ncompared against its source. macOS and Windows are verified to compile, link,\nstart and answer the SDK's version call \u2014 no more than that. Neither has yet\ntalked to a real SAP system."}
1),"\n",(0,i.jsxs)(t.p,{children:["That gap was wider than it looked: until this week nothing in ",(0,i.jsx)(t.code,{children:"erpl-proto"})," had\never been ",(0,i.jsx)(t.em,{children:"compiled"})," off Linux, only type-checked, and the very first Windows\nbuild failed on a POSIX call that had sat there unnoticed. Which is the argument\nfor building on a platform before claiming to support it."]}),"\n",(0,i.jsxs)(t.p,{children:["That is all shipped, as of ",(0,i.jsx)(t.code,{children:"v2026.08.30"})," \u2014 the first release with no SAP\nlibraries in any bundle. It is also the first on PyPI, which makes trying it a\none-liner:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-bash",children:'uvx erpl-rev --smoke\n# erpl-rev smoke ok: RFC backend erpl-proto 0.0.1 (0.0.1); DuckDB {"v":"v1.5.4"}\n\nuvx erpl-rev --db erpl-rev.duckdb\n'})}),"\n",(0,i.jsxs)(t.p,{children:["No download, no unpacking, no ",(0,i.jsx)(t.code,{children:"LD_LIBRARY_PATH"}),". The second line starts the\nserver, which registers its ",(0,i.jsx)(t.code,{children:"PROGRAM_ID"})," at the gateway and waits for ABAP to\ncall out to it; ",(0,i.jsx)(t.code,{children:"pip install erpl-rev"})," works too if you would rather have it on\n",(0,i.jsx)(t.code,{children:"PATH"}),", and the single-file binaries and the Docker image are still there for\nhosts without Python."]}),"\n",(0,i.jsx)(t.h2,{id:"where-this-goes",children:"Where this goes"}),"\n",(0,i.jsx)(t.p,{children:"The measurements point somewhere specific. The client is no longer the\nbottleneck for partitioned loads \u2014 SAP's read side is, and it has been all\nalong. The remaining lever on our side is server-side columnar ingest, so that\na package arrives in a form DuckDB can consume without a per-cell conversion at\nall. That is a bigger change than anything in this post."}),"\n",(0,i.jsxs)(t.p,{children:["The code is ",(0,i.jsx)(t.a,{href:"https://github.com/DataZooDE/erpl-rev",children:"DataZooDE/erpl-rev"}),", and the\ningest work is ",(0,i.jsx)(t.a,{href:"https://github.com/DataZooDE/erpl-rev/pull/67",children:"PR #67"}),"."]}),"\n",(0,i.jsxs)(t.p,{children:["If the clean-room side of ",(0,i.jsx)(t.code,{children:"erpl-proto"})," is the part that interests you \u2014 how you\nbuild a protocol implementation from captures and prove it correct, and what\nthat means for getting your own data out of SAP \u2014 come argue with me about it\n",(0,i.jsx)(t.a,{href:"https://www.linkedin.com/in/joachim-rosskopf/",children:"on LinkedIn"}),". It is the part I\nmost enjoy talking about."]})]})}function c(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},89194:(e,t,n)=>{n.d(t,{A:()=>s});const s=n.p+"assets/images/hero-3f3bc1b16d6c0dd71ce549b4dfa312b9.jpg"}}]);
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.