@@ -188,13 +188,25 @@ def _convert_named_params_with_casting(
188188 # Build the conversion mapping and new parameter list
189189 param_order = []
190190 seen_params = set ()
191+ missing_params = []
191192
192193 # Process matches to determine parameter order (first occurrence wins)
193194 for match in matches :
194195 param_name = match .group (1 )
195- if param_name not in seen_params and param_name in parameters :
196- param_order .append (param_name )
197- seen_params .add (param_name )
196+ if param_name not in seen_params :
197+ if param_name in parameters :
198+ param_order .append (param_name )
199+ seen_params .add (param_name )
200+ else :
201+ missing_params .append (param_name )
202+
203+ # Defensive check: ensure all parameters found in query are available
204+ if missing_params :
205+ raise ValueError (
206+ f"Missing parameters in query: { missing_params } . "
207+ f"Query contains parameters { [m .group (1 ) for m in matches ]} "
208+ f"but parameters dict only has { list (parameters .keys ())} "
209+ )
198210
199211 # Convert the query string by replacing each parameter with its positional equivalent
200212 converted_query = querystring
@@ -205,15 +217,54 @@ def _convert_named_params_with_casting(
205217 f":({ re .escape (param_name )} )" + r"(::[\w\[\]]+)?"
206218 )
207219 replacement = f"${ i } \\ 2" # $N + casting part (group 2)
208- converted_query = re .sub (
220+
221+ # Perform replacement and verify it worked
222+ new_query = re .sub (
209223 param_pattern_specific , replacement , converted_query
210224 )
211225
226+ # Defensive check: ensure replacement actually occurred
227+ if (
228+ new_query == converted_query
229+ and f":{ param_name } " in converted_query
230+ ):
231+ raise RuntimeError (
232+ f"Failed to replace parameter '{ param_name } ' in query. "
233+ f"Pattern: { param_pattern_specific } , Query: { converted_query } "
234+ )
235+
236+ converted_query = new_query
237+
212238 # Convert parameters dict to list in the correct order
213239 converted_params = [
214240 parameters [param_name ] for param_name in param_order
215241 ]
216242
243+ # Final defensive check: ensure no named parameters remain in the converted query
244+ # Look for the original parameter pattern, but exclude matches that are part of casting syntax
245+ remaining_matches = []
246+ for match in re .finditer (param_pattern , converted_query ):
247+ full_match = match .group (0 )
248+ param_name = match .group (1 )
249+ # Check if this looks like a real parameter (not casting syntax)
250+ # Real parameters should not be preceded by a positional parameter like $1, $2, etc.
251+ start_pos = match .start ()
252+ if start_pos > 0 :
253+ # Look at the characters before the match
254+ preceding_text = converted_query [
255+ max (0 , start_pos - 3 ) : start_pos
256+ ]
257+ # If preceded by $N, this is likely casting syntax, not a parameter
258+ if re .search (r"\$\d+$" , preceding_text ):
259+ continue
260+ remaining_matches .append (full_match )
261+
262+ if remaining_matches :
263+ raise RuntimeError (
264+ f"Conversion incomplete: named parameters still present in query: { remaining_matches } . "
265+ f"Converted query: { converted_query } , Original query: { querystring } "
266+ )
267+
217268 return converted_query , converted_params
218269
219270 @property
0 commit comments