Homelab, Linux, JS & ABAP (~˘▾˘)~
 

[CAP] Handling Mass Selection >1000 Rows in Fiori Elements

Recently I was working on a Fiori Elements application, where users should be able to select large amount of rows. With the default ResponsiveTable this cannot be achieved, but Fiori Elements with Grid Table allows multi-selection of thousands of rows via Multi-selection plug-in. This is what I’ve added to the manifest.json.

Honestly, not 100% sure if every property was necessary, but I couldn’t find a clear documentation what property is valid for a GridTable…

The result looked good and selecting large amounts of rows was possible in the UI! However, actions like “Go” or table refresh after a custom action generated a massive OData $filter:, because each selected row got appended to the filter like this:​

$filter=ID eq 'uuid1' or ID eq 'uuid2' or ... (4000+ ORs)

When sending such a large request, it exceeds that standard Header Limit: “Request Header Fields Too Large

However, this can easily be solved by increasing the request limit.

Now the request went through and reached the backend. But CAP parses the request to CQN, which becomes a deep SQL WHERE tree. SQLite rejects this with “Expression tree is too large (maximum depth 1000)“.

Haven’t tested on HANA, but I wanted to make sure it also works in my local development environment. So I needed to find a way to reduce the Expression tree size.

CAP Solution: Rewrite OR Chain to IN

Intercept READ requests, parse the CQN SELECT.where array, extract IDs, rebuild as ID IN (...) using cds.parse.xpr.

srv/service.js:

const cds = require('@sap/cds');

this.before('READ', 'YourEntity', req => {
  const whereArray = req.query.SELECT?.where;
  if (!Array.isArray(whereArray) || whereArray.length < 50) return;

  const ids = [];
  let i = 0;
  
  while (i < whereArray.length) {
    // Skip 'or' connectors
    if (typeof whereArray[i] === 'string' && whereArray[i] === 'or') {
      i++; 
      continue;
    }
    
    const refObj = whereArray[i];     // {ref: ['ID']}
    const op = whereArray[i + 1];     // '='
    const valObj = whereArray[i + 2]; // {val: 'uuid'}
    
    if (refObj?.ref && op === '=' && valObj?.val) {
      ids.push(`'${valObj.val}'`);
      i += 3;
    } else {
      console.log('Pattern mismatch at', i);
      return;
    }
  }

  if (ids.length < 20) return; // Skip small filters

  // Build IN clause
  const condition = `ID in (${ids.join(',')})`;
  req.query.SELECT.where = cds.parse.xpr(condition);[web:180][web:187]
  
  console.log(`Rewrote ${whereArray.length/3} OR triplets → IN (${ids.length} IDs)`);
});

CQN Structure

Fiori sends: [{ref:['ID']}, '=', {val:'uuid1'}, 'or', {ref:['ID']}, '=', {val:'uuid2'}, ...]

The handler rebuilds this to ID in ('uuid1','uuid2',...) → compact SQL without deep tree.

This worked, and now the SQLite could process the query! So to summarize what needed to be done:

  1. Grid Table (not Responsive Table) for large datasets.
  2. Multi-selection plug-in (limit: 0 or high value).​
  3. Increase max_batch_header_size in your reverse proxy/Gateway – otherwise $filter gets truncated before reaching CAP.
  4. Custom Handler to transform the where condition

Quite complex – at least with the classic ALV, that wouldn’t have been an issue at all…

Update 27.03.2026: Here is a blog tackling a very similar situation, but with RAP instead of CAP: Fiori List Report: The “Process All” Dilemma — Parsing OData $filter in ABAP
But with the provided solution it is not possible to select/unselect certain lines, since only the filter criteria are respected and no manual selection/deselection of specific rows is possible.