@@ -149,7 +149,10 @@ def _lit(value):
149149 (e.g. 0x.. hex) form for string keys. Both forms are self-contained (no surrounding quotes).
150150 """
151151
152- if value is not None and re .match (r"\A-?[0-9]+\Z" , value ):
152+ if value is None :
153+ return NULL # unescaper.escape() passes None through, and a bare
154+ # None formatted into a predicate is not even SQL
155+ if re .match (r"\A-?[0-9]+\Z" , value ):
153156 return value
154157 return unescaper .escape (value , False )
155158
@@ -161,18 +164,24 @@ def _embed(template, value, *fixed):
161164 template = template .replace ("'%s'" , "%s" )
162165 return template % (fixed + (_lit (value ),))
163166
164- def _dumpSingle (tbl , colList , count , cursor , tableRef , entries , lengths ):
165- blind = queries [Backend .getIdentifiedDbms ()].dump_table .blind
166- field = agent .preprocessField (tbl , cursor )
167+ def _target (count ):
168+ """Rows the walk is expected to produce, honouring --start/--stop."""
167169
168170 if conf .limitStart and conf .limitStop :
169- target = max (0 , conf .limitStop - conf .limitStart + 1 )
171+ return max (0 , conf .limitStop - conf .limitStart + 1 )
170172 elif conf .limitStop :
171- target = conf .limitStop
173+ return conf .limitStop
172174 elif conf .limitStart :
173- target = max (0 , count - conf .limitStart + 1 )
174- else :
175- target = count
175+ return max (0 , count - conf .limitStart + 1 )
176+
177+ return count
178+
179+ def _dumpSingle (tbl , colList , count , cursor , tableRef , entries , lengths ):
180+ """False when the walk gave up mid-table (the caller then discards the partial result)."""
181+
182+ blind = queries [Backend .getIdentifiedDbms ()].dump_table .blind
183+ field = agent .preprocessField (tbl , cursor )
184+ target = _target (count )
176185
177186 pivotValue = None
178187
@@ -182,7 +191,7 @@ def _dumpSingle(tbl, colList, count, cursor, tableRef, entries, lengths):
182191 seed = unArrayizeValue (inject .getValue (query ))
183192
184193 if isNoneValue (seed ) or seed == NULL :
185- return
194+ return False # no seed, no walk - and an empty table is not that
186195
187196 pivotValue = safechardecode (seed )
188197
@@ -205,7 +214,7 @@ def _dumpSingle(tbl, colList, count, cursor, tableRef, entries, lengths):
205214 # safety latch against a non-advancing cursor (e.g. encoding edge cases)
206215 if value == pivotValue :
207216 singleTimeWarnMessage ("keyset cursor stopped advancing prematurely" )
208- break
217+ return False
209218
210219 pivotValue = value
211220
@@ -223,20 +232,17 @@ def _dumpSingle(tbl, colList, count, cursor, tableRef, entries, lengths):
223232
224233 produced += 1
225234
235+ return True
236+
226237def _dumpComposite (tbl , colList , count , cursorCols , tableRef , entries , lengths ):
238+ """False when the walk gave up mid-table (the caller then discards the partial result)."""
239+
227240 blind = queries [Backend .getIdentifiedDbms ()].dump_table .blind
228241 fields = [agent .preprocessField (tbl , _ ) for _ in cursorCols ]
229242 orderExpr = ',' .join (fields )
230243
231244 startSkip = (conf .limitStart - 1 ) if conf .limitStart else 0
232- if conf .limitStart and conf .limitStop :
233- target = max (0 , conf .limitStop - conf .limitStart + 1 )
234- elif conf .limitStop :
235- target = conf .limitStop
236- elif conf .limitStart :
237- target = max (0 , count - conf .limitStart + 1 )
238- else :
239- target = count
245+ target = _target (count )
240246
241247 prev = None
242248 produced = 0
@@ -263,11 +269,18 @@ def _dumpComposite(tbl, colList, count, cursorCols, tableRef, entries, lengths):
263269 tup .append (None if isNoneValue (value ) else safechardecode (value ))
264270
265271 if all (isNoneValue (_ ) for _ in tup ):
266- break
272+ break # nothing past the cursor: the table is walked
273+
274+ # A key column that did not come back (an error-channel miss, a blocked payload) cannot be
275+ # seeked on, and its equality would pin the rest of the row to a NULL - so the walk stops
276+ # here rather than emitting a row of empty cells and carrying the hole into the next seek
277+ if any (isNoneValue (_ ) for _ in tup ):
278+ singleTimeWarnMessage ("keyset cursor could not be retrieved for one of the key column(s)" )
279+ return False
267280
268281 if prev is not None and tup == prev :
269282 singleTimeWarnMessage ("keyset cursor stopped advancing prematurely" )
270- break
283+ return False
271284
272285 prev = tup
273286 seen += 1
@@ -290,6 +303,8 @@ def _dumpComposite(tbl, colList, count, cursorCols, tableRef, entries, lengths):
290303
291304 produced += 1
292305
306+ return True
307+
293308def keysetDumpTable (tbl , colList , count , cursor ):
294309 """
295310 Dumps a table one row at a time using keyset (seek) pagination on 'cursor' (a list of
@@ -298,6 +313,10 @@ def keysetDumpTable(tbl, colList, count, cursor):
298313 exact equality on the cursor (index point seek), so no row is skipped via OFFSET and no
299314 per-row ORDER BY filesort is needed. A deep --start uses a single OFFSET "seed" jump
300315 (single-column cursors), after which the walk is pure keyset.
316+
317+ Returns None when the walk gave up mid-table (a key value that did not come back, a cursor
318+ that stopped advancing): a short table is worse than a slow one, so the caller redoes it
319+ with the standard OFFSET dump instead of showing whatever was reached.
301320 """
302321
303322 tableRef = _tableRef (tbl )
@@ -309,9 +328,16 @@ def keysetDumpTable(tbl, colList, count, cursor):
309328 entries [column ] = BigArray ()
310329
311330 if len (cursor ) == 1 :
312- _dumpSingle (tbl , colList , count , cursor [0 ], tableRef , entries , lengths )
331+ complete = _dumpSingle (tbl , colList , count , cursor [0 ], tableRef , entries , lengths )
313332 else :
314- _dumpComposite (tbl , colList , count , cursor , tableRef , entries , lengths )
333+ complete = _dumpComposite (tbl , colList , count , cursor , tableRef , entries , lengths )
334+
335+ if not complete :
336+ warnMsg = "keyset pagination did not complete for table '%s', " % unsafeSQLIdentificatorNaming (tbl )
337+ warnMsg += "falling back to the standard dump"
338+ logger .warning (warnMsg )
339+
340+ return None
315341
316342 debugMsg = "keyset pagination retrieved %d row(s) for table '%s'" % (len (entries [colList [0 ]]) if colList and colList [0 ] in entries else 0 , unsafeSQLIdentificatorNaming (tbl ))
317343 logger .debug (debugMsg )
0 commit comments