@@ -184,6 +184,9 @@ type rangeProber interface {
184184 fetchNextRealKey (
185185 ctx context.Context , tableName string , quotedCol string , midpoint string , start string , end string ,
186186 ) (string , bool , error )
187+ fetchPrevRealKey (
188+ ctx context.Context , tableName string , quotedCol string , midpoint string , start string , end string ,
189+ ) (string , bool , error )
187190}
188191
189192// buildAdaptiveStringPartitions splits an arbitrary string watermark column
@@ -225,6 +228,15 @@ func buildAdaptiveStringPartitions(
225228 if err != nil {
226229 return nil , fmt .Errorf ("failed to fetch next real key for [%s, %s]: %w" , p .start , p .end , err )
227230 }
231+ if ! found {
232+ // The interpolated midpoint can overshoot every key in the range when
233+ // keys occupy a narrow slice of the character space. So also probe
234+ // backwards before declaring the partition unsplittable.
235+ k , found , err = prober .fetchPrevRealKey (ctx , tableName , quotedCol , mid , p .start , p .end )
236+ if err != nil {
237+ return nil , fmt .Errorf ("failed to fetch prev real key for [%s, %s]: %w" , p .start , p .end , err )
238+ }
239+ }
228240 if ! found {
229241 outputs = append (outputs , p )
230242 continue
@@ -267,6 +279,23 @@ func (c *MySqlConnector) fetchNextRealKey(
267279 query := fmt .Sprintf (
268280 "SELECT %[1]s FROM %[2]s WHERE %[1]s >= '%[3]s' AND %[1]s > '%[4]s' AND %[1]s < '%[5]s' ORDER BY %[1]s LIMIT 1" ,
269281 quotedCol , tableName , mysql .Escape (midpoint ), mysql .Escape (start ), mysql .Escape (end ))
282+ return c .fetchRealKey (ctx , query )
283+ }
284+
285+ // fetchPrevRealKey returns the largest real value of the watermark column that
286+ // is below the interpolated midpoint and strictly inside (start, end). The
287+ // bounds are enforced by MySQL using its column's collation. Return false when
288+ // no such key exists.
289+ func (c * MySqlConnector ) fetchPrevRealKey (
290+ ctx context.Context , tableName string , quotedCol string , midpoint string , start string , end string ,
291+ ) (string , bool , error ) {
292+ query := fmt .Sprintf (
293+ "SELECT %[1]s FROM %[2]s WHERE %[1]s < '%[3]s' AND %[1]s > '%[4]s' AND %[1]s < '%[5]s' ORDER BY %[1]s DESC LIMIT 1" ,
294+ quotedCol , tableName , mysql .Escape (midpoint ), mysql .Escape (start ), mysql .Escape (end ))
295+ return c .fetchRealKey (ctx , query )
296+ }
297+
298+ func (c * MySqlConnector ) fetchRealKey (ctx context.Context , query string ) (string , bool , error ) {
270299 rs , err := c .Execute (ctx , query )
271300 if err != nil {
272301 return "" , false , err
@@ -277,7 +306,7 @@ func (c *MySqlConnector) fetchNextRealKey(
277306 }
278307 key , err := rs .GetString (0 , 0 )
279308 if err != nil {
280- return "" , false , fmt .Errorf ("failed to read next real key: %w" , err )
309+ return "" , false , fmt .Errorf ("failed to read real key: %w" , err )
281310 }
282311 // GetString is zero-copy over the resultset's row buffer, which rs.Close()
283312 // recycles into a pool where the next query's response overwrites it; the
0 commit comments