@@ -155,12 +155,84 @@ def _normalize_parameters_for_capi(
155155 normalized_params [key ] = "" .join (f"\\ x{ byte :02x} " for byte in binary )
156156 pattern = rf"(?i)(?<!BLOB\()\${ re .escape (key )} \b"
157157 normalized_query = re .sub (pattern , f"BLOB(${ key } )" , normalized_query )
158- elif isinstance (value , str ):
159- pattern = rf"(?i)\bto_json\(\s*\${ re .escape (key )} \s*\)"
160- if re .search (pattern , normalized_query ) is not None :
158+ else :
159+ pattern = self ._to_json_parameter_pattern (key )
160+ has_to_json_param = re .search (pattern , normalized_query ) is not None
161+ if isinstance (value , str ) and has_to_json_param :
161162 json .loads (value )
162163 normalized_params [key ] = get_capi_module ().CAPIJsonParameter (value )
163164 normalized_query = re .sub (pattern , f"${ key } " , normalized_query )
165+ elif (
166+ has_to_json_param
167+ and self ._is_json_serializable_parameter (value )
168+ and self ._contains_unresolved_json_type (value )
169+ ):
170+ normalized_params [key ] = get_capi_module ().CAPIJsonParameter (
171+ json .dumps (value , allow_nan = False )
172+ )
173+ normalized_query = re .sub (pattern , f"${ key } " , normalized_query )
174+
175+ return normalized_query , normalized_params
176+
177+ @staticmethod
178+ def _to_json_parameter_pattern (key : str ) -> str :
179+ return rf"(?i)\bto_json\(\s*\${ re .escape (key )} \s*\)"
180+
181+ @staticmethod
182+ def _is_json_serializable_parameter (value : Any ) -> bool :
183+ return value is None or isinstance (value , (bool , int , float , list , tuple , dict ))
184+
185+ @classmethod
186+ def _contains_unresolved_json_type (cls , value : Any ) -> bool :
187+ if value is None :
188+ return True
189+ if isinstance (value , (list , tuple )):
190+ return len (value ) == 0 or any (
191+ cls ._contains_unresolved_json_type (item ) for item in value
192+ )
193+ if isinstance (value , dict ):
194+ return len (value ) == 0 or any (
195+ cls ._contains_unresolved_json_type (item ) for item in value .values ()
196+ )
197+ return False
198+
199+ @staticmethod
200+ def _json_string_literal (value : str ) -> str :
201+ return "'" + value .replace ("\\ " , "\\ \\ " ).replace ("'" , "\\ u0027" ) + "'"
202+
203+ def _normalize_parameters_for_pybind (
204+ self ,
205+ query : str ,
206+ parameters : dict [str , Any ],
207+ ) -> tuple [str , dict [str , Any ]]:
208+ normalized_query = query
209+ normalized_params = dict (parameters )
210+
211+ for key , value in list (normalized_params .items ()):
212+ if not isinstance (key , str ):
213+ msg = f"Parameter name must be of type string but got { type (key )} "
214+ raise RuntimeError (msg ) # noqa: TRY004
215+
216+ pattern = self ._to_json_parameter_pattern (key )
217+ if re .search (pattern , normalized_query ) is None :
218+ continue
219+ if isinstance (value , str ):
220+ json .loads (value )
221+ json_value = value
222+ elif self ._is_json_serializable_parameter (
223+ value
224+ ) and self ._contains_unresolved_json_type (value ):
225+ json_value = json .dumps (value , allow_nan = False )
226+ else :
227+ continue
228+ json_expr = f"CAST({ self ._json_string_literal (json_value )} AS JSON)"
229+ normalized_query = re .sub (
230+ pattern ,
231+ lambda _ , json_expr = json_expr : json_expr ,
232+ normalized_query ,
233+ )
234+ if re .search (rf"\${ re .escape (key )} \b" , normalized_query ) is None :
235+ normalized_params .pop (key , None )
164236
165237 return normalized_query , normalized_params
166238
@@ -378,6 +450,7 @@ def _execute_with_pybind(
378450 if len (parameters ) == 0 :
379451 return py_connection .query (query )
380452
453+ query , parameters = self ._normalize_parameters_for_pybind (query , parameters )
381454 prepared = py_connection .prepare (query , parameters )
382455 return py_connection .execute (prepared , parameters )
383456
0 commit comments