Prepare ABAP Open SQL for SAP's ADT data-preview console. Fixes the dialect, enforces the limit.
These three statements fail on a live system:
| Sent | Response |
|---|---|
SELECT SUM(netwr) FROM vbap |
400 Unknown column name "SUM(NETWR)" |
SELECT COUNT(DISTINCT kunnr) FROM vbak |
400 ")" is invalid here (due to grammar) |
SELECT a FROM t WHERE (x = '1' OR y = '2') |
400 ") " was expected here. |
Three messages, three apparent rules, one actual cause: the parser wants a space on the inside of every parenthesis. It even says so in the third message. All three pass once spaced.
This package knows that rule, and the handful of others that cost people afternoons.
npm install abap-sqlDriving an agent rather than writing code? The same rules ship as an MCP server:
sap-abap-sql-mcp.
The data-preview console (/sap/bc/adt/datapreview/freestyle) is the widest read surface an on-premise ABAP system exposes over HTTP. It reaches any table, needs no database user, and is where every ADT client and agent bridge ends up. It is also strict in ways its error messages do not explain.
Reaching one from outside the network is a separate problem, and a worse one: SICF nodes, service registration, system versus dialog users, and a tunnel that opens no inbound port. There is a free write-up of what actually works: connecting an on-premise S/4HANA over HTTP.
import { prepare, AbapSqlError } from "abap-sql";
prepare("SELECT COUNT(DISTINCT kunnr) FROM vbak WHERE vtweg = '10'");
// → "SELECT COUNT( DISTINCT kunnr ) FROM vbak WHERE vtweg = '10'"
prepare("SELECT vbeln FROM vbak ORDER BY vbeln DESC");
// throws AbapSqlError: Sorting is ORDER BY x DESCENDING, not DESC.prepare runs the checks, applies what is mechanical, and refuses what is not. Lower-level pieces are exported too: normalize, lint, isReadOnly, tokenize.
Spacing inside parentheses, everywhere it is needed at once: aggregates (SUM( netwr )), DISTINCT (COUNT( DISTINCT kunnr )), subqueries (IN ( SELECT … )), and boolean groups (( a OR b )). COUNT(*) is left compact, because the parser takes it either way and every character counts against the ceiling.
| Written | Told |
|---|---|
LIMIT 10 |
ABAP Open SQL has no LIMIT. Cap the result with the reader's row limit instead. |
ORDER BY a DESC |
Sorting is ORDER BY x DESCENDING, not DESC. |
p.vbeln |
Qualify join fields with ~, not a dot: write p~vbeln, not p.vbeln. |
DELETE FROM … |
Only SELECT is allowed. |
Each returns a stable rule id, so you can suppress or test one of them.
The console rejects 256 characters and accepts 255, measured across the whole statement with newlines collapsed. Wrapping buys nothing: the server's message names "line 1", but a 300-character statement across five short lines fails identically.
prepare measures after normalizing, because spacing lengthens the statement. Checking the input instead lets a statement pass locally and fail on the system.
Because a parenthesis inside 'PUMP (SPARE)' is data, and rewriting it changes the answer rather than the shape. ABAP has two string forms, an escape, and a comment form, and all four have to be understood before a single character is moved:
normalize("SELECT a FROM t WHERE x = 'PUMP (SPARE)'"); // unchanged
normalize("SELECT a FROM t WHERE x = `PUMP (SPARE)`"); // unchanged, backtick form
normalize("SELECT a FROM t WHERE x = 'it''s'"); // unchanged, doubled quote is an escape
normalize("SELECT a FROM t WHERE x = 'a b'"); // inner spaces survive the collapseThe same reasoning applies to the read-only check. A scan of the raw text for write verbs refuses this, which is a read:
isReadOnly("SELECT belnr FROM bkpf WHERE bktxt = 'Update pending'"); // trueTokenizing is done by abaplint's lexer, which is the one correct ABAP tokenizer in TypeScript. It is reached at a private path because the package does not export it from the root; test/contract.test.ts asserts every assumption we make about it against the installed copy, so an upstream move fails a test here rather than corrupting a query quietly.
This prepares statements for the ADT data-preview console. It does not parse responses, hold a connection, or know anything about your system. Pair it with whatever ADT client you already have.
Open SQL written inside an ABAP program is a related but different dialect: it has INTO TABLE, UP TO n ROWS, @host variables, and no character ceiling. The paren rule and the sort keyword still apply there; the ceiling does not, so pass prepare(sql, { maxChars: Infinity }) if you are working on program source.
git tag v0.1.1 && git push --tags. CI checks the tag against package.json, runs the tests and the Node import check, then publishes to npm.
Publishing uses npm trusted publishing, so there is no token in this repo and nothing to rotate. npm trusts this repository and this workflow through OIDC, and provenance is attested automatically.
Tokenization by abaplint (MIT). The rules come from statements that actually failed against a live on-premise system, with the system's own error text recorded next to each fix.
Built by Daslab. We connect agents to on-premise ERP systems.
MIT