1313# limitations under the License.
1414
1515import base64
16+ import copy
1617import datetime
17- import decimal
1818import gzip
1919import json
20- import uuid
2120from dataclasses import dataclass
22- from google .cloud .spanner_v1 .data_types import Interval , JsonObject
2321from typing import Any
2422
2523from google .protobuf .json_format import MessageToDict , ParseDict
2624from google .protobuf .message import Message
25+ from google .protobuf .struct_pb2 import Struct
2726
2827from google .cloud .spanner_v1 import BatchTransactionId
29- from google .cloud .spanner_v1 .types import ExecuteSqlRequest , DirectedReadOptions
28+ from google .cloud .spanner_v1 .types import ExecuteSqlRequest , DirectedReadOptions , Type
29+ from google .cloud .spanner_v1 ._helpers import _make_value_pb
3030
3131_PROTO_CLASS_MAP = {
3232 "QueryOptions" : ExecuteSqlRequest .QueryOptions ,
3333 "DirectedReadOptions" : DirectedReadOptions ,
34+ "Struct" : Struct ,
35+ "Type" : Type ,
3436}
3537
3638
@@ -39,14 +41,6 @@ def _serialize_value(val: Any) -> Any:
3941 return {"__type__" : "bytes" , "value" : base64 .b64encode (val ).decode ("utf-8" )}
4042 elif isinstance (val , datetime .datetime ):
4143 return {"__type__" : "datetime" , "value" : val .isoformat ()}
42- elif isinstance (val , datetime .date ):
43- return {"__type__" : "date" , "value" : val .isoformat ()}
44- elif isinstance (val , decimal .Decimal ):
45- return {"__type__" : "decimal" , "value" : str (val )}
46- elif isinstance (val , uuid .UUID ):
47- return {"__type__" : "uuid" , "value" : str (val )}
48- elif isinstance (val , Interval ):
49- return {"__type__" : "interval" , "value" : str (val )}
5044 elif hasattr (val , "_pb" ):
5145 return {
5246 "__type__" : "protobuf" ,
@@ -59,8 +53,6 @@ def _serialize_value(val: Any) -> Any:
5953 "class" : val .__class__ .__name__ ,
6054 "value" : MessageToDict (val , preserving_proto_field_name = True ),
6155 }
62- elif isinstance (val , JsonObject ):
63- return {"__type__" : "json_object" , "value" : val .serialize ()}
6456 elif isinstance (val , dict ):
6557 return {k : _serialize_value (v ) for k , v in val .items ()}
6658 elif isinstance (val , list ):
@@ -81,33 +73,40 @@ def _deserialize_value(val: Any) -> Any:
8173 if dt_str .endswith ("Z" ):
8274 dt_str = dt_str [:- 1 ] + "+00:00"
8375 return datetime .datetime .fromisoformat (dt_str )
84- elif t == "date" :
85- return datetime .date .fromisoformat (val ["value" ])
86- elif t == "decimal" :
87- return decimal .Decimal (val ["value" ])
88- elif t == "uuid" :
89- return uuid .UUID (val ["value" ])
90- elif t == "interval" :
91- return Interval .from_str (val ["value" ])
92- elif t == "json_object" :
93- return JsonObject .from_str (val ["value" ])
9476 elif t == "tuple" :
9577 return tuple (_deserialize_value (x ) for x in val ["value" ])
9678 elif t == "protobuf" :
9779 cls_name = val .get ("class" )
9880 dict_val = val ["value" ]
9981 if cls_name in _PROTO_CLASS_MAP :
10082 cls = _PROTO_CLASS_MAP [cls_name ]
101- msg = cls ()._pb
83+ msg = cls ()._pb if hasattr ( cls (), "_pb" ) else cls ()
10284 ParseDict (dict_val , msg )
103- return cls (msg )
85+ return cls (msg ) if hasattr ( cls (), "_pb" ) else msg
10486 return _deserialize_value (dict_val )
10587 return {k : _deserialize_value (v ) for k , v in val .items ()}
10688 elif isinstance (val , list ):
10789 return [_deserialize_value (v ) for v in val ]
10890 return val
10991
11092
93+ def _unpack_value_pb (value ):
94+ which = value .WhichOneof ("kind" )
95+ if which == "null_value" :
96+ return None
97+ elif which == "number_value" :
98+ return value .number_value
99+ elif which == "string_value" :
100+ return value .string_value
101+ elif which == "bool_value" :
102+ return value .bool_value
103+ elif which == "struct_value" :
104+ return {k : _unpack_value_pb (v ) for k , v in value .struct_value .fields .items ()}
105+ elif which == "list_value" :
106+ return [_unpack_value_pb (v ) for v in value .list_value .values ]
107+ return None
108+
109+
111110def decode_from_string (encoded_partition_id ):
112111 gzip_bytes = base64 .b64decode (bytes (encoded_partition_id , "utf-8" ))
113112 partition_id_bytes = gzip .decompress (gzip_bytes )
@@ -120,10 +119,29 @@ def decode_from_string(encoded_partition_id):
120119 read_timestamp = _deserialize_value (btid_data ["read_timestamp" ]),
121120 )
122121 partition_result = _deserialize_value (data ["partition_result" ])
122+
123+ # Post-process query params back from Protobuf Struct to Python primitives
124+ if "query" in partition_result and "params" in partition_result ["query" ]:
125+ params_pb = partition_result ["query" ]["params" ]
126+ if params_pb :
127+ partition_result ["query" ]["params" ] = {
128+ k : _unpack_value_pb (v ) for k , v in params_pb .fields .items ()
129+ }
130+
123131 return PartitionId (btid , partition_result )
124132
125133
126134def encode_to_string (batch_transaction_id , partition_result ):
135+ # Copy to avoid modifying the caller's dictionary in connection.py
136+ partition_result = copy .deepcopy (partition_result )
137+
138+ # Pre-process query params into a Protobuf Struct
139+ if "query" in partition_result and "params" in partition_result ["query" ]:
140+ params = partition_result ["query" ]["params" ]
141+ if params :
142+ params_pb = Struct (fields = {k : _make_value_pb (v ) for k , v in params .items ()})
143+ partition_result ["query" ]["params" ] = params_pb
144+
127145 data = {
128146 "batch_transaction_id" : {
129147 "transaction_id" : _serialize_value (batch_transaction_id .transaction_id ),
0 commit comments