1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414import base64
15+ import datetime
1516from enum import Enum
1617import logging
1718from typing import TYPE_CHECKING , Any
19+ import uuid
1820
1921from google .cloud .spanner_v1 import (
2022 ExecuteBatchDmlRequest ,
@@ -132,6 +134,84 @@ def rowcount(self) -> int:
132134 """
133135 return self ._rowcount
134136
137+ def _prepare_params (
138+ self , parameters : dict [str , Any ] | list [Any ] | tuple [Any ] | None = None
139+ ) -> (dict [str , Any ] | None , dict [str , Type ] | None ):
140+ """
141+ Prepares parameters for Spanner execution
142+
143+ Args:
144+ parameters: A dictionary (for named parameters/GoogleSQL)
145+ or a list/tuple
146+ (for positional parameters/PostgreSQL).
147+
148+ Returns:
149+ A tuple containing:
150+ - converted_params: Dictionary of parameters with values
151+ converted for Spanner (e.g. ints to strings).
152+ - param_types: Dictionary mapping parameter names to
153+ their Spanner Type.
154+ """
155+ if not parameters :
156+ return {}, {}
157+
158+ converted_params = {}
159+ param_types = {}
160+
161+ # Normalize input to an iterable of (key, value)
162+ if isinstance (parameters , (list , tuple )):
163+ # PostgreSQL Dialect: Positional parameters $1, $2... are
164+ # mapped to P1, P2...
165+ iterator = ((f"P{ i } " , val ) for i , val in enumerate (parameters , 1 ))
166+ elif isinstance (parameters , dict ):
167+ # GoogleSQL Dialect: Named parameters @name are mapped directly.
168+ iterator = parameters .items ()
169+ else :
170+ # If strictly required, raise an error for unsupported types
171+ return {}, {}
172+
173+ for key , value in iterator :
174+ if value is None :
175+ converted_params [key ] = None
176+ continue
177+ # Note: check bool before int, as bool is a subclass of int
178+ if isinstance (value , bool ):
179+ converted_params [key ] = value
180+ param_types [key ] = Type (code = TypeCode .BOOL )
181+ elif isinstance (value , int ):
182+ # Spanner expects INT64 as strings to preserve precision
183+ # in JSON
184+ converted_params [key ] = str (value )
185+ param_types [key ] = Type (code = TypeCode .INT64 )
186+ elif isinstance (value , float ):
187+ converted_params [key ] = value
188+ param_types [key ] = Type (code = TypeCode .FLOAT64 )
189+ elif isinstance (value , bytes ):
190+ converted_params [key ] = value
191+ param_types [key ] = Type (code = TypeCode .BYTES )
192+ elif isinstance (value , uuid .UUID ):
193+ # Convert UUID to string as requested
194+ converted_params [key ] = str (value )
195+ # Use STRING type for UUIDs (unless specific UUID type is
196+ # required/supported by your backend version)
197+ param_types [key ] = Type (code = TypeCode .STRING )
198+ elif isinstance (value , datetime .datetime ):
199+ # Convert Datetime to string (RFC 3339 format is standard
200+ # for str(datetime))
201+ converted_params [key ] = str (value )
202+ param_types [key ] = Type (code = TypeCode .TIMESTAMP )
203+ elif isinstance (value , datetime .date ):
204+ converted_params [key ] = str (value )
205+ param_types [key ] = Type (code = TypeCode .DATE )
206+ else :
207+ # Fallback for strings and other types
208+ converted_params [key ] = value
209+ # For strings, we can explicitly set the type or let it default.
210+ if isinstance (value , str ):
211+ param_types [key ] = Type (code = TypeCode .STRING )
212+
213+ return converted_params , param_types
214+
135215 @check_not_closed
136216 def execute (
137217 self ,
@@ -152,18 +232,8 @@ def execute(
152232 logger .debug (f"Executing operation: { operation } " )
153233
154234 request = ExecuteSqlRequest (sql = operation )
155- if parameters :
156- converted_params = {}
157- param_types = {}
158- for key , value in parameters .items ():
159- if isinstance (value , int ) and not isinstance (value , bool ):
160- converted_params [key ] = str (value )
161- param_types [key ] = Type (code = TypeCode .INT64 )
162- else :
163- converted_params [key ] = value
164-
165- request .params = converted_params
166- request .param_types = param_types
235+ params , _ = self ._prepare_params (parameters )
236+ request .params = params
167237
168238 try :
169239 self ._rows = self ._connection ._internal_conn .execute (request )
@@ -202,18 +272,8 @@ def executemany(
202272
203273 for parameters in seq_of_parameters :
204274 statement = ExecuteBatchDmlRequest .Statement (sql = operation )
205- if parameters :
206- converted_params = {}
207- param_types = {}
208- for key , value in parameters .items ():
209- if isinstance (value , int ) and not isinstance (value , bool ):
210- converted_params [key ] = str (value )
211- param_types [key ] = Type (code = TypeCode .INT64 )
212- else :
213- converted_params [key ] = value
214-
215- statement .params = converted_params
216- statement .param_types = param_types
275+ params , _ = self ._prepare_params (parameters )
276+ statement .params = params
217277
218278 request .statements .append (statement )
219279
0 commit comments