Skip to main content

SAP BW / BW/4HANA Connection

Extension Credit

This guide uses the erpl extension by the DuckDB community. Thanks to the contributors who made SAP BW/4HANA integration possible through RFC connections!

Connect flAPI to SAP BW (Business Warehouse) and BW/4HANA systems using the ERPL extension. Access InfoCubes, DSOs, and BEx queries for analytics APIs.

Prerequisites

  • SAP NetWeaver RFC SDK installed
  • SAP BW user with query authorizations
  • Network access to BW system

Quick Start

The ERPL extension is installed from its custom repository and authenticates with a DuckDB sap_rfc secret:

# flapi.yaml
connections:
sap-bw:
init: |
INSTALL 'erpl' FROM 'http://get.erpl.io';
LOAD 'erpl';

CREATE OR REPLACE PERSISTENT SECRET sap_bw (
TYPE sap_rfc,
ASHOST '${SAP_BW_HOST}',
SYSNR '${SAP_BW_SYSNR}',
CLIENT '${SAP_BW_CLIENT}',
USER '${SAP_BW_USER}',
PASSWD '${SAP_BW_PASSWORD}',
LANG 'EN'
);

Connection Parameters

Same as SAP ERP:

ParameterDescriptionRequiredExample
ASHOSTSAP BW server hostnameYesbw.company.com
SYSNRSystem numberYes'00'
CLIENTClient numberYes'100'
USERSAP usernameYes${SAP_BW_USER}
PASSWDSAP passwordYes${SAP_BW_PASSWORD}
LANGLanguageNoEN

Reading BW Data

InfoCube Queries

# sqls/sales-analysis.yaml
url-path: /sales/analysis/
method: GET

request:
- field-name: year
field-in: query
required: true
validators:
- type: string
regex: '^\d{4}$'
- field-name: region
field-in: query
required: false
validators:
- type: enum
allowedValues: [EMEA, AMER, APAC]

template-source: sales-analysis.sql
connection:
- sap-bw
-- sqls/sales-analysis.sql
-- Query InfoCube for sales data via the ERPL sap_read_table function
SELECT
"/BIC/OCALYEAR" AS year,
"/BIC/OCREGION" AS region,
"/BIC/OCPRODUCT" AS product,
"/BIC/OCUSTOMER" AS customer,
"/BIC/OREVENUE" AS revenue,
"/BIC/OQUANTITY" AS quantity,
"/BIC/OMARGIN" AS margin
FROM sap_read_table('/BIC/ASALES01')
WHERE "/BIC/OCALYEAR" = '{{{ params.year }}}'
{{#params.region}}
AND "/BIC/OCREGION" = '{{{ params.region }}}'
{{/params.region}}

DSO (DataStore Object) Queries

-- sqls/customer-360.sql
SELECT
CUSTOMER_ID,
CUSTOMER_NAME,
SEGMENT,
TOTAL_PURCHASES,
LAST_ORDER_DATE,
LIFETIME_VALUE,
CHURN_RISK_SCORE
FROM sap_read_table('/BIC/ACUST360')
WHERE ACTIVE_FLAG = 'X'
ORDER BY LIFETIME_VALUE DESC
LIMIT 1000

ADSO (Advanced DSO in BW/4HANA)

-- sqls/realtime-inventory.sql
SELECT
PLANT,
MATERIAL,
STORAGE_LOCATION,
STOCK_QUANTITY,
LAST_UPDATE_TIMESTAMP
FROM sap_read_table('/BIC/AINVENT01')
WHERE LAST_UPDATE_TIMESTAMP >= CURRENT_TIMESTAMP - INTERVAL 1 HOUR

Common BW Objects

InfoCubes

Pre-aggregated analytical data:

  • 0SD_C03 - Sales analysis
  • 0PUR_C01 - Purchase analysis
  • 0FI_GL_4 - General ledger
  • /BIC/A* - Custom InfoCubes

DSOs

Detailed transactional data:

  • 0MATERIAL - Material master
  • 0CUSTOMER - Customer master
  • /BIC/A* - Custom DSOs

BEx Queries

Access published BEx queries via ERPL (requires RFC function modules).

Caching BW Data

Critical for BW: queries can take 30+ seconds. Always cache:

# sqls/sales-dashboard.yaml
url-path: /sales/dashboard/
method: GET

cache:
enabled: true
table: bw_sales_cache
schedule: 12h
template-file: sales_dashboard_cache.sql

template-source: sales_dashboard.sql
connection:
- sap-bw
-- sqls/sales_dashboard_cache.sql
-- Materialize complex BW query results into DuckLake
INSERT INTO {{cache.catalog}}.{{cache.schema}}.{{cache.table}}
SELECT
c.CALYEAR AS year,
c.CALMONTH AS month,
r.REGION_TEXT AS region,
p.PRODUCT_TEXT AS product_category,
SUM(f.REVENUE) AS total_revenue,
SUM(f.QUANTITY) AS total_quantity,
AVG(f.MARGIN_PCT) AS avg_margin_pct,
COUNT(DISTINCT f.CUSTOMER) AS unique_customers
FROM sap_read_table('/BIC/FSALES') f
LEFT JOIN sap_read_table('/BIC/PCALENDAR') c
ON f.CALYEAR = c.CALYEAR AND f.CALMONTH = c.CALMONTH
LEFT JOIN sap_read_table('/BIC/PREGION') r
ON f.REGION = r.REGION
LEFT JOIN sap_read_table('/BIC/PPRODUCT') p
ON f.PRODUCT = p.PRODUCT
WHERE c.CALYEAR >= YEAR(CURRENT_DATE) - 2
GROUP BY c.CALYEAR, c.CALMONTH, r.REGION_TEXT, p.PRODUCT_TEXT

Performance:

MethodQuery TimeAPI Response
Direct BW30-120 secondsTimeout
flAPI cacheN/A1-3ms

BW-Specific Considerations

1. Technical Names

BW uses cryptic technical names:

  • InfoCubes: /BIC/ASALES01, 0SD_C03
  • Characteristics: /BIC/OCUSTOMER, 0CALMONTH
  • Key figures: /BIC/OREVENUE, 0AMOUNT

Find names in:

  • Transaction RSA1 (Data Warehousing Workbench)
  • Transaction LISTCUBE (view InfoCube data)

2. Naming Conventions

/BIC/A*  - Custom InfoCubes/DSOs
/BIC/O* - InfoObject characteristics
/BIC/P* - Master data tables
0* - SAP standard objects

3. Date/Time Fields

BW uses specific formats:

  • CALMONTH: YYYYMM (e.g., 202401)
  • CALDAY: YYYYMMDD (e.g., 20240115)
  • Convert in SQL: strptime(CALDAY, '%Y%m%d')

4. Master Data

Join with master data for descriptions:

SELECT
f.CUSTOMER,
m.CUSTOMER_NAME,
m.CITY,
m.COUNTRY,
SUM(f.REVENUE) AS revenue
FROM sap_read_table('/BIC/FSALES') f
LEFT JOIN sap_read_table('/BIC/PCUSTOMER') m
ON f.CUSTOMER = m.CUSTOMER
GROUP BY f.CUSTOMER, m.CUSTOMER_NAME, m.CITY, m.COUNTRY

Complete Example

# ═══════════════════════════════════════════════════════════════
# flapi.yaml - BW connection
# ═══════════════════════════════════════════════════════════════
project-name: sap-bw-reporting-api

template:
path: './sqls'
environment-whitelist:
- '^SAP_BW_.*'
- '^REPORTING_.*'

connections:
sap-bw-prod:
init: |
INSTALL 'erpl' FROM 'http://get.erpl.io';
LOAD 'erpl';

CREATE OR REPLACE PERSISTENT SECRET sap_bw_prod (
TYPE sap_rfc,
ASHOST '${SAP_BW_HOST}',
SYSNR '${SAP_BW_SYSNR}',
CLIENT '${SAP_BW_CLIENT}',
USER '${SAP_BW_USER}',
PASSWD '${SAP_BW_PASSWORD}',
LANG 'EN'
);

# ═══════════════════════════════════════════════════════════════
# sqls/revenue-report.yaml
# ═══════════════════════════════════════════════════════════════
url-path: /reports/revenue/
method: GET

auth:
enabled: true
type: bearer
jwt-secret: '${REPORTING_JWT_SECRET}'

cache:
enabled: true
table: bw_revenue_report
schedule: 24h
template-file: revenue_cache.sql

request:
- field-name: year
field-in: query
description: Fiscal year (YYYY)
required: false
default: "2024"
validators:
- type: string
regex: '^\d{4}$'

- field-name: region
field-in: query
description: Region code
required: false
validators:
- type: enum
allowedValues: [EMEA, AMER, APAC]

template-source: revenue_report.sql
connection:
- sap-bw-prod
-- sqls/revenue_cache.sql
-- Daily materialization from BW InfoCube
INSERT INTO {{cache.catalog}}.{{cache.schema}}.{{cache.table}}
SELECT
strptime(fc.CALMONTH || '01', '%Y%m%d') AS month_date,
SUBSTRING(fc.CALMONTH, 1, 4) AS year,
SUBSTRING(fc.CALMONTH, 5, 2) AS month,
reg.REGION_TEXT AS region,
pc.PRODUCT_CAT_TEXT AS product_category,
cc.COUNTRY_TEXT AS country,
SUM(fc.NET_REVENUE) AS net_revenue,
SUM(fc.GROSS_REVENUE) AS gross_revenue,
SUM(fc.QUANTITY) AS quantity_sold,
COUNT(DISTINCT fc.ORDER_NUMBER) AS order_count,
AVG(fc.MARGIN_PCT) AS avg_margin_pct
FROM sap_read_table('/BIC/AREVENUE01') fc
LEFT JOIN sap_read_table('/BIC/PREGION') reg
ON fc.REGION = reg.REGION
LEFT JOIN sap_read_table('/BIC/PPRODCAT') pc
ON fc.PRODUCT_CAT = pc.PRODUCT_CAT
LEFT JOIN sap_read_table('/BIC/PCOUNTRY') cc
ON fc.COUNTRY = cc.COUNTRY
WHERE SUBSTRING(fc.CALMONTH, 1, 4) >= '2020'
AND fc.RECORD_MODE = 'A'
GROUP BY fc.CALMONTH, reg.REGION_TEXT, pc.PRODUCT_CAT_TEXT, cc.COUNTRY_TEXT
-- sqls/revenue_report.sql
-- Fast API queries against the materialized cache
SELECT
year,
month,
month_date,
region,
product_category,
country,
net_revenue,
gross_revenue,
quantity_sold,
order_count,
avg_margin_pct
FROM {{cache.catalog}}.{{cache.schema}}.{{cache.table}}
WHERE 1=1
{{#params.year}}
AND year = '{{{ params.year }}}'
{{/params.year}}
{{#params.region}}
AND region = '{{{ params.region }}}'
{{/params.region}}
ORDER BY month_date DESC, net_revenue DESC

Best Practices

DO

  • Cache aggressively (BW queries are slow)
  • Refresh overnight when BW updates
  • Use master data joins for readable output
  • Filter by date ranges to limit data volume
  • Test queries in SE16N/LISTCUBE first
  • Request BW indexes on frequently filtered fields

DON'T

  • Query BW directly without caching
  • Use SELECT * on large InfoCubes
  • Expose raw technical names in APIs
  • Query historical data without date filters
  • Run real-time queries against InfoCubes

Troubleshooting

No Data Returned

-- Check if data exists
SELECT COUNT(*) FROM sap_read_table('/BIC/ASALES01')

Common causes:

  • Wrong technical name
  • No data for filter criteria
  • Need to activate DSO/InfoCube
  • Authorization missing

Slow Queries

BW query taking > 60 seconds:

Solutions:

  1. Enable caching (required)
  2. Add date range filters
  3. Request BW team add indexes
  4. Use aggregated InfoCubes instead of DSOs
  5. Pre-aggregate in cache template

Authorization Issues

Error: RFC_ERROR_AUTHORITY

Required authorizations:

  • S_RFC - RFC authorization
  • S_RS_COMP - BW component authorization
  • Query-specific authorizations

Contact SAP Basis/BW team.

Environment Setup

# .env
export SAP_BW_HOST="bw-prod.company.com"
export SAP_BW_SYSNR="00"
export SAP_BW_CLIENT="100"
export SAP_BW_USER="BW_API_USER"
export SAP_BW_PASSWORD="secure-password"

Next Steps

🍪 Cookie Settings