FireDAC and DBMS Interoperability: Server-Side Pagination Consistency
0 reputation · 17 May 2020, 12:14 UTC
0 reputation · 17 May 2020, 12:14 UTC
In Delphi 10.4 and later, TFDQuery provides FetchOptions.RecordCount and FetchOptions.RecordsSkip to implement server-side bounding. When integrated with databases like PostgreSQL or MySQL, these properties typically map to LIMIT and OFFSET clauses to reduce network payload and memory consumption.
A challenge arises when the application must remain database-agnostic. While modern DBMS versions support native bounding, older versions (such as Oracle pre-12c) do not. FireDAC does not automatically rewrite the SQL to use ROW_NUMBER() or similar window functions for these legacy systems, requiring manual query construction.
This creates a discrepancy in how pagination is handled across different database drivers within the same application architecture.
FetchOptions are consistently applied when switching between databases that support native bounding and those that require sub-queries?No. FireDAC has no built-in layer that translates one pagination statement into dialect-specific LIMIT/OFFSET, ROWNUM, or ROW_NUMBER() forms. The SQL you place in TFDQuery.SQL is sent to the driver essentially as written. The nearest supported abstraction is macro substitution plus runtime branching on TFDConnection.RDBMSKind — and you still author each dialect fragment yourself.
Well-established behavior:
FetchOptions bounds the fetch, not the statement. RecsMax is the page size and RecsSkip is the number of rows to skip; both act after the server has produced the result set.FetchOptions.RecordCount is not a page-size setting — it governs whether an exact row count is produced. Treat it as a separate concern from paging.TFDQuery.Macros, !Name placeholders) expand before the statement reaches the driver, so one SQL text can carry dialect fragments.RDBMSKind identifies the driver family, not the server version.Version-sensitive and worth confirming locally: exact property names, defaults, and enum spellings differ between Delphi releases and driver builds. Verify them in the IDE property inspector rather than trusting any snippet, including the ones below.
RecsMax only as a client-side safety cap. Never combine a SQL OFFSET with a non-zero RecsSkip, or rows are skipped twice.ORDER BY over a unique key. Without it, page contents are undefined even on a single DBMS.SELECT t.Id, t.Name
FROM MyTable t
ORDER BY t.Id
!LIMIT
!OFFSET
case FDConnection.RDBMSKind of
mkPostgreSQL, mkMySQL, mkSQLite:
begin
FDQuery.Macros.MacroByName('LIMIT').Value :=
'LIMIT ' + IntToStr(PageSize);
FDQuery.Macros.MacroByName('OFFSET').Value :=
'OFFSET ' + IntToStr(PageOffset);
end;
mkMSSQL, mkOracle, mkFirebird:
begin
FDQuery.Macros.MacroByName('LIMIT').Value :=
'OFFSET ' + IntToStr(PageOffset) +
' ROWS FETCH NEXT ' + IntToStr(PageSize) + ' ROWS ONLY';
FDQuery.Macros.MacroByName('OFFSET').Value := '';
end;
end;
Confirm the enum names in your build. The second branch assumes OFFSET/FETCH support (SQL Server 2012+, Oracle 12c+, Firebird 3+). For Oracle pre-12c, replace the whole SQL text with a ROW_NUMBER() subquery rather than trying to express it as a macro fragment.
Even with correct dialect SQL, OFFSET pagination re-evaluates the result set on every request. Concurrent inserts or deletes move rows between pages, producing duplicates or gaps. If your pages must be stable under concurrent writes, the recommendation changes to keyset (seek) pagination on the ordered unique key; OFFSET remains acceptable for read-mostly or snapshot-consistent workloads.
Attach a FireDAC monitor link to the connection and enable monitoring for SQL, then capture the exact statement sent to each DBMS. Confirm that no rewriting occurs and that the expected fragment was substituted. Run the same logical page against each target DBMS with a fixed ORDER BY and compare returned key sets and ordering. Then insert and delete rows between two page fetches and re-read both pages to measure duplicate and omission behavior.
Do the pages need to stay stable while other sessions write to the table? If yes, plan for keyset pagination and treat the dialect macro layer as a fallback; if no, the macro approach above is sufficient.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 17 May 2020, 18:41 UTC
When you set TFDQuery.FetchOptions.RecsSkip and RecsMax, FireDAC only injects the bound after the driver has executed the statement. The actual LIMIT/OFFSET (or ROW_NUMBER() wrapper) is produced by the driver, and you can see it by enabling the connection’s Tracing flag. Open the Log Window, run the query, and look for the SELECT … LIMIT … OFFSET … line. If you are on an Oracle pre‑12c server, the log will show a ROW_NUMBER() sub‑query; on PostgreSQL it will show a plain LIMIT. This quick check guarantees that your FetchOptions are actually being applied and not silently ignored.
Remember that TFDConnection.RDBMSKind only tells you the driver family, not the server version, so you may still need to query SELECT VERSION() to decide whether a native LIMIT is available.