@@ -255,15 +255,37 @@ def _chunkedJsonAggUse(expression, expressionFields, expressionFieldsList, count
255255 caps. K is halved adaptively if a chunk response still gets truncated. Returns a BigArray of
256256 rows, or None to let the caller fall back to the regular per-row UNION path.
257257
258- NOTE: MySQL only for now (windowed 'LIMIT offset,K' + JSON_ARRAYAGG ); other DBMSes return None.
258+ Same DBMS coverage as the single-shot JSON-agg (per-DBMS aggregate + windowing ); others -> None.
259259 """
260- if not Backend .isDbms (DBMS .MYSQL ) or not expressionFields or not expressionFieldsList :
260+ dbms = Backend .getIdentifiedDbms ()
261+
262+ if dbms not in (DBMS .MYSQL , DBMS .PGSQL , DBMS .SQLITE , DBMS .H2 , DBMS .HSQLDB , DBMS .FIREBIRD ) or not expressionFields or not expressionFieldsList :
261263 return None
262264
265+ start , stop , delimiter = kb .chars .start , kb .chars .stop , kb .chars .delimiter
266+
263267 # a stable total ordering (all output columns) so the LIMIT/OFFSET windows never overlap or drop rows
264268 base = re .sub (r"(?i)\s+ORDER BY\s+.+\Z" , "" , expression )
265269 orderBy = "ORDER BY %s" % ',' .join (str (_ + 1 ) for _ in range (len (expressionFieldsList )))
266- aggFields = "CONCAT_WS('%s',%s)" % (kb .chars .delimiter , ',' .join (agent .nullAndCastField (_ ) for _ in expressionFieldsList ))
270+ nulled = [agent .nullAndCastField (_ ) for _ in expressionFieldsList ]
271+
272+ # per-DBMS: aggregate-over-windowed-columns expression (mirrors the single-shot branches) plus
273+ # the "K rows at offset" window clause appended to the inner derived table
274+ if dbms == DBMS .MYSQL :
275+ aggExpr = "CONCAT('%s',JSON_ARRAYAGG(CONCAT_WS('%s',%s)),'%s')" % (start , delimiter , ',' .join (nulled ), stop )
276+ window = lambda o , k : "%s LIMIT %d,%d" % (orderBy , o , k )
277+ elif dbms == DBMS .PGSQL :
278+ aggExpr = "STRING_AGG('%s'||%s||'%s','')" % (start , ("||'%s'||" % delimiter ).join ("COALESCE(%s::text,' ')" % _ for _ in expressionFieldsList ), stop )
279+ window = lambda o , k : "%s LIMIT %d OFFSET %d" % (orderBy , k , o )
280+ elif dbms == DBMS .SQLITE :
281+ aggExpr = "'%s'||JSON_GROUP_ARRAY(%s)||'%s'" % (start , ("||'%s'||" % delimiter ).join ("COALESCE(%s,' ')" % _ for _ in expressionFieldsList ), stop )
282+ window = lambda o , k : "%s LIMIT %d OFFSET %d" % (orderBy , k , o )
283+ elif dbms in (DBMS .H2 , DBMS .HSQLDB ):
284+ aggExpr = "GROUP_CONCAT('%s'||%s||'%s' SEPARATOR '')" % (start , ("||'%s'||" % delimiter ).join (nulled ), stop )
285+ window = lambda o , k : "%s LIMIT %d OFFSET %d" % (orderBy , k , o )
286+ elif dbms == DBMS .FIREBIRD :
287+ aggExpr = "LIST('%s'||%s||'%s','')" % (start , ("||'%s'||" % delimiter ).join (nulled ), stop )
288+ window = lambda o , k : "%s ROWS %d TO %d" % (orderBy , o + 1 , o + k )
267289
268290 debugMsg = "single-shot UNION dump output was too large; switching to "
269291 debugMsg += "chunked (windowed) JSON aggregation of %d entries" % count
@@ -274,8 +296,7 @@ def _chunkedJsonAggUse(expression, expressionFields, expressionFieldsList, count
274296 offset = 0
275297
276298 while offset < count :
277- inner = "%s %s LIMIT %d,%d" % (base , orderBy , offset , chunk )
278- query = "SELECT CONCAT('%s',JSON_ARRAYAGG(%s),'%s') FROM (%s) AS sqmapx" % (kb .chars .start , aggFields , kb .chars .stop , inner )
299+ query = "SELECT %s FROM (%s %s) sqmapx" % (aggExpr , base , window (offset , chunk ))
279300
280301 kb .jsonAggMode = True
281302 output = _oneShotUnionUse (query , False )
@@ -348,6 +369,18 @@ def unionUse(expression, unpack=True, dump=False):
348369 value = parseUnionPage (output )
349370 kb .jsonAggMode = False
350371
372+ # If the single-shot aggregate failed (typically too large for the DBMS packet limit /
373+ # response cap) and the table is large, retrieve the rows in bounded windows (chunked
374+ # JSON aggregation) before the slow per-row fallback. Done here (independent of the
375+ # detected UNION where-clause) so it engages for any dumpable FROM-table query.
376+ if value is None and " FROM " in expression .upper () and not re .search (SQL_SCALAR_REGEX , expression , re .I ) and not any ((kb .forcePartialUnion , conf .forcePartial , conf .disableJson , conf .binaryFields , conf .limitStart , conf .limitStop )):
377+ chunkCountExpr = expression .replace (expressionFields , queries [Backend .getIdentifiedDbms ()].count .query % '*' , 1 )
378+ if " ORDER BY " in chunkCountExpr .upper ():
379+ chunkCountExpr = chunkCountExpr [:chunkCountExpr .upper ().rindex (" ORDER BY " )]
380+ chunkCount = unArrayizeValue (parseUnionPage (_oneShotUnionUse (chunkCountExpr , unpack )))
381+ if isNumPosStrValue (chunkCount ) and (int (chunkCount ) >= JSON_AGG_CHUNK_ROWS or kb .respTruncated ):
382+ value = _chunkedJsonAggUse (expression , expressionFields , expressionFieldsList , int (chunkCount ))
383+
351384 # We have to check if the SQL query might return multiple entries
352385 # if the technique is partial UNION query and in such case forge the
353386 # SQL limiting the query output one entry at a time
@@ -398,14 +431,6 @@ def unionUse(expression, unpack=True, dump=False):
398431 return value
399432
400433 if isNumPosStrValue (count ) and int (count ) > 1 :
401- # The single-shot full UNION dump failed and the table is large (or its oversized
402- # response was detected as truncated): retrieve the rows in bounded windows via
403- # chunked JSON aggregation (K rows/request) instead of the slow per-row path below.
404- if Backend .isDbms (DBMS .MYSQL ) and not any ((kb .forcePartialUnion , conf .forcePartial , conf .disableJson , conf .binaryFields , conf .limitStart , conf .limitStop )) and (int (count ) >= JSON_AGG_CHUNK_ROWS or kb .respTruncated ):
405- chunked = _chunkedJsonAggUse (expression , expressionFields , expressionFieldsList , int (count ))
406- if chunked is not None :
407- return chunked
408-
409434 threadData = getCurrentThreadData ()
410435
411436 try :
0 commit comments