88from dataclasses import dataclass
99import math
1010from numbers import Real
11+ from operator import itemgetter
1112from typing import Any
1213from urllib .parse import urlparse
1314
15+ import numpy as np
16+
1417from ..core import UnsupportedOperationError
1518
1619
@@ -125,8 +128,12 @@ def preflight(
125128 * ,
126129 rows : Iterable [Mapping [str , Any ]] | None = None ,
127130 require_canonical_mapping : bool = True ,
128- ) -> None :
129- """Validate strict target capabilities before any source dataset is created."""
131+ ) -> bool :
132+ """Validate strict target capabilities before any source dataset is created.
133+
134+ Returns True only when every cell was proven to be float/None (numeric) or
135+ str, so callers may bypass per-value bind processing.
136+ """
130137 variables = None if isinstance (variables_or_count , int ) else list (variables_or_count )
131138 variable_count = variables_or_count if isinstance (variables_or_count , int ) else len (variables )
132139 if variable_count > profile .max_source_variables :
@@ -140,7 +147,7 @@ def preflight(
140147 max_physical = profile .max_source_variables + 1 ,
141148 )
142149 if variables is None :
143- return
150+ return False
144151
145152 used = {"__case_ordinal" }
146153 source_names : set [str ] = set ()
@@ -211,7 +218,11 @@ def preflight(
211218 f"{ profile .name } permits { profile .max_row_bytes } ." ,
212219 declared_row_bytes = declared_row_bytes , maximum = profile .max_row_bytes ,
213220 )
214- for row_ordinal , row in enumerate (rows or (), start = 1 ):
221+ rows = list (rows or ())
222+ if not rows or _rows_within_limits (profile , variables , rows ):
223+ return True
224+ # Slow path only reports the first failing row in row-major order.
225+ for row_ordinal , row in enumerate (rows , start = 1 ):
215226 row_bytes = 8
216227 for variable in variables :
217228 if variable .get ("storage_kind" ) == "numeric" :
@@ -310,6 +321,54 @@ def preflight(
310321 row_ordinal = row_ordinal , statement_bytes = statement_bytes ,
311322 maximum = profile .max_statement_bytes ,
312323 )
324+ return False
325+
326+
327+ def _rows_within_limits (
328+ profile : SqlProfile , variables : list [Mapping [str , Any ]],
329+ rows : list [Mapping [str , Any ]],
330+ ) -> bool :
331+ """Column-wise proof that the row-by-row checks in preflight cannot fail.
332+
333+ Per-column maxima bound every row's row and statement size from above, so
334+ True means no row exceeds a limit; False defers to the exact slow path.
335+ """
336+ row_bytes , statement_bytes = 8 , 32
337+ for variable in variables :
338+ name = str (variable ["physical_name" ])
339+ try :
340+ column = list (map (itemgetter (name ), rows ))
341+ except KeyError :
342+ return False
343+ if variable .get ("storage_kind" ) == "numeric" :
344+ if not set (map (type , column )) <= _PLAIN_NUMERIC_TYPES :
345+ return False
346+ values = np .array (column , dtype = float ) # None becomes NaN
347+ if np .isinf (values ).any () or np .isnan (values ).sum () != column .count (None ):
348+ return False
349+ row_bytes += 8
350+ statement_bytes += 24
351+ continue
352+ if set (map (type , column )) != {str }:
353+ return False
354+ try :
355+ longest = max (map (len , map (str .encode , column )))
356+ except UnicodeEncodeError :
357+ return False
358+ if longest > profile .max_text_value_bytes :
359+ return False
360+ row_bytes += 20 if profile .name in MYSQL_WIRE_PROFILES else longest
361+ statement_bytes += longest + 8
362+ return (
363+ (profile .max_row_bytes is None or row_bytes <= profile .max_row_bytes )
364+ and (
365+ profile .max_statement_bytes is None
366+ or statement_bytes <= profile .max_statement_bytes
367+ )
368+ )
369+
370+
371+ _PLAIN_NUMERIC_TYPES = {float , type (None )}
313372
314373
315374def _row_storage_bytes (profile : SqlProfile , variable : Mapping [str , Any ]) -> int :
0 commit comments