@@ -129,49 +129,73 @@ def getCursor(conn, driver=None, preamb=None, notNamed=False):
129129 return cur
130130
131131
132- def __fromrecords (recList , dtype = None , intNullVal = None ):
132+ def __fromrecords (recList , dtype = None , intNullVal = None , strNullVal = 'None' ):
133133 """
134134 This function was taken from np.core.records and updated to
135- support conversion null integers to intNullVal
135+ support conversion null integers to intNullVal and strings to strNullVal
136136 """
137137
138- shape = None
139138 descr = np .dtype ((np .record , dtype ))
139+
140+ # Check if we must avoid fast path for strings to prevent None -> 'None' conversion
141+ # Note: numpy's default conversion for None to string is 'None', so if
142+ # strNullVal is 'None', we can still use the fast path.
143+ force_object_strings = False
144+ if strNullVal is not None and strNullVal != 'None' :
145+ if any (dtype .fields [n ][0 ].kind in ('S' , 'U' ) for n in dtype .names ):
146+ force_object_strings = True
147+
148+ if not force_object_strings :
149+ try :
150+ retval = np .array (recList , dtype = descr )
151+ return retval .view (numpy .recarray )
152+ except (TypeError , ValueError ):
153+ # Failed (likely due to None in int col). Fall through to object path.
154+ pass
155+
156+ # Vectorized fallback path using object arrays
157+ names = dtype .names
158+ new_formats = []
159+ converters = {}
160+
161+ for name in names :
162+ dt = dtype .fields [name ][0 ]
163+ if dt .kind in ('i' , 'u' ):
164+ new_formats .append (object )
165+ converters [name ] = intNullVal
166+ elif dt .kind in ('S' , 'U' ) and strNullVal is not None :
167+ new_formats .append (object )
168+ converters [name ] = strNullVal
169+ else :
170+ new_formats .append (dt )
171+
172+ temp_dtype = np .dtype ({'names' : names , 'formats' : new_formats })
173+
174+ if len (recList ) == 0 :
175+ return np .recarray ((0 ,), dtype = descr )
176+
140177 try :
141- retval = np .array (recList , dtype = descr )
142- except TypeError : # list of lists instead of list of tuples
178+ arr = np .array (recList , dtype = temp_dtype )
179+ except (TypeError , ValueError ):
180+ # Handle cases where np.array fails (e.g. ragged lists)
181+ # by manually filling the object array
143182 shape = (len (recList ), )
144- _array = np .recarray (shape , descr )
145- try :
146- for k in range (_array .size ):
147- _array [k ] = tuple (recList [k ])
148- except TypeError :
149- convs = []
150- ncols = len (dtype .fields )
151- for _k in dtype .names :
152- _v = dtype .fields [_k ]
153- if _v [0 ] in [np .int16 , np .int32 , np .int64 ]:
154- convs .append (lambda x : intNullVal if x is None else x )
155- else :
156- convs .append (lambda x : x )
157- convs = tuple (convs )
158-
159- def convF (x ):
160- return [convs [_ ](x [_ ]) for _ in range (ncols )]
161-
162- for k in range (k , _array .size ):
163- try :
164- _array [k ] = tuple (recList [k ])
165- except TypeError :
166- _array [k ] = tuple (convF (recList [k ]))
167- return _array
168- else :
169- if shape is not None and retval .shape != shape :
170- retval .shape = shape
183+ arr = np .recarray (shape , temp_dtype )
184+ for k in range (len (recList )):
185+ arr [k ] = tuple (recList [k ])
171186
172- res = retval .view (numpy .recarray )
187+ # Apply conversions
188+ for name , val in converters .items ():
189+ col = arr [name ]
190+ # Check for None efficiently
191+ mask = (col == None )
192+ if np .any (mask ):
193+ if val is not None :
194+ col [mask ] = val
173195
174- return res
196+ # Cast to final dtype
197+ retval = arr .astype (descr )
198+ return retval .view (numpy .recarray )
175199
176200
177201def __getDType (row , typeCodes , strLength ):
@@ -269,6 +293,7 @@ def get(query,
269293 batched = True ,
270294 asDict = False ,
271295 intNullVal = - 9999 ,
296+ strNullVal = 'None' ,
272297 nthreads = 1 ):
273298 '''
274299 Executes the sql query and returns the tuple or dictionary
@@ -292,6 +317,9 @@ def get(query,
292317 intNullVal : integer, optional
293318 All the integer columns with nulls will have null replaced by
294319 this value
320+ strNullVal : string, optional
321+ All the string columns with nulls will have null replaced by
322+ this value
295323 db : string
296324 The name of the database
297325 driver : string, optional
@@ -401,7 +429,8 @@ def process_batch(batch):
401429 dtype = __getDType (first_row , type_codes , strLength )
402430 return __fromrecords (batch ,
403431 dtype = dtype ,
404- intNullVal = intNullVal )
432+ intNullVal = intNullVal ,
433+ strNullVal = strNullVal )
405434
406435 def batch_iter (first ):
407436 yield first
@@ -786,6 +815,7 @@ def local_join(query,
786815 timeout = None ,
787816 strLength = STRLEN_DEFAULT ,
788817 intNullVal = - 9999 ,
818+ strNullVal = 'None' ,
789819 asDict = False ):
790820 """
791821 Join your local data in python with the data in the database
@@ -838,7 +868,8 @@ def local_join(query,
838868 preamb = preamb ,
839869 strLength = strLength ,
840870 asDict = asDict ,
841- intNullVal = intNullVal )
871+ intNullVal = intNullVal ,
872+ strNullVal = strNullVal )
842873 except BaseException :
843874 failure_cleanup (conn , connSupplied )
844875 raise
0 commit comments