@@ -38,13 +38,28 @@ def initialize
3838 @errors = { }
3939 @session_counter = 0
4040
41- put_statement_result "SELECT 1" , StatementResult . create_select1_result
42- put_statement_result "INSERT INTO test_table (id, name) VALUES ('1', 'Alice')" , StatementResult . create_update_count_result ( 1 )
41+ @mock_file_path = ENV . fetch ( "MOCK_MSG_FILE" , nil )
42+ @req_file_path = ENV . fetch ( "MOCK_REQ_FILE" , nil )
4343
44- dialect_sql = "select option_value from information_schema.database_options where option_name='database_dialect'"
45- dialect_result = StatementResult . create_dialect_result
44+ add_default_results
45+ end
4646
47- put_statement_result dialect_sql , dialect_result
47+ def log_request ( request )
48+ @requests << request # Keep memory copy just in case
49+ return unless @req_file_path
50+
51+ # We must manually serialize the Protobuf before Marshaling
52+ data = {
53+ class : request . class . name ,
54+ payload : request . class . encode ( request )
55+ }
56+
57+ # Append to file using atomic open
58+ File . open ( @req_file_path , "ab" ) do |f |
59+ Marshal . dump ( data , f )
60+ end
61+ rescue StandardError => e
62+ warn "Failed to log request: #{ e . message } "
4863 end
4964
5065 def put_statement_result ( sql , result )
@@ -57,12 +72,12 @@ def push_error(sql_or_method, error)
5772 end
5873
5974 def create_session ( request , _unused_call )
60- @requests << request
75+ log_request ( request )
6176 do_create_session ( request . database , request . session )
6277 end
6378
6479 def batch_create_sessions ( request , _unused_call )
65- @requests << request
80+ log_request ( request )
6681 num_created = 0
6782 response = Google ::Cloud ::Spanner ::V1 ::BatchCreateSessionsResponse . new
6883 while num_created < request . session_count
@@ -73,12 +88,12 @@ def batch_create_sessions(request, _unused_call)
7388 end
7489
7590 def get_session ( request , _unused_call )
76- @requests << request
91+ log_request ( request )
7792 @sessions [ request . name ]
7893 end
7994
8095 def list_sessions ( request , _unused_call )
81- @requests << request
96+ log_request ( request )
8297 response = Google ::Cloud ::Spanner ::V1 ::ListSessionsResponse . new
8398 @sessions . each_value do |s |
8499 response . sessions << s
@@ -87,16 +102,18 @@ def list_sessions(request, _unused_call)
87102 end
88103
89104 def delete_session ( request , _unused_call )
90- @requests << request
105+ log_request ( request )
91106 @sessions . delete request . name
92107 Google ::Protobuf ::Empty . new
93108 end
94109
95110 def execute_sql ( request , _unused_call )
111+ log_request ( request )
96112 do_execute_sql request , false
97113 end
98114
99115 def execute_streaming_sql ( request , _unused_call )
116+ log_request ( request )
100117 do_execute_sql request , true
101118 end
102119
@@ -111,7 +128,13 @@ def do_execute_sql(request, streaming)
111128 raise @errors [ request . sql ] . pop if @errors [ request . sql ] && !@errors [ request . sql ] . empty?
112129
113130 result = get_statement_result ( request . sql ) . clone
114- raise result . result if result . result_type == StatementResult ::EXCEPTION
131+
132+ if result . result_type == StatementResult ::EXCEPTION
133+ raise GRPC ::BadStatus . new ( result . result . code , result . result . message ) if result . result . is_a? ( Google ::Rpc ::Status )
134+
135+ raise result . result
136+
137+ end
115138
116139 if streaming
117140 result . each created_transaction
@@ -121,6 +144,7 @@ def do_execute_sql(request, streaming)
121144 end
122145
123146 def execute_batch_dml ( request , _unused_call )
147+ log_request ( request )
124148 @requests << request
125149 validate_session request . session
126150 created_transaction = do_create_transaction request . session if request . transaction &.begin
@@ -133,8 +157,14 @@ def execute_batch_dml(request, _unused_call)
133157 request . statements . each do |stmt |
134158 result = get_statement_result ( stmt . sql ) . clone
135159 if result . result_type == StatementResult ::EXCEPTION
136- status . code = result . result . code
137- status . message = result . result . message
160+ err_proto = result . result
161+ if err_proto . is_a? ( Google ::Rpc ::Status )
162+ status . code = err_proto . code
163+ status . message = err_proto . message
164+ else
165+ status . code = GRPC ::Core ::StatusCodes ::UNKNOWN
166+ status . message = err_proto . to_s
167+ end
138168 break
139169 end
140170 if first
@@ -149,16 +179,19 @@ def execute_batch_dml(request, _unused_call)
149179 end
150180
151181 def read ( request , _unused_call )
182+ log_request ( request )
152183 @requests << request
153184 raise GRPC ::BadStatus . new GRPC ::Core ::StatusCodes ::UNIMPLEMENTED , "Not yet implemented"
154185 end
155186
156187 def streaming_read ( request , _unused_call )
188+ log_request ( request )
157189 @requests << request
158190 raise GRPC ::BadStatus . new GRPC ::Core ::StatusCodes ::UNIMPLEMENTED , "Not yet implemented"
159191 end
160192
161193 def begin_transaction ( request , _unused_call )
194+ log_request ( request )
162195 @requests << request
163196 raise @errors [ __method__ . to_s ] . pop if @errors [ __method__ . to_s ] && !@errors [ __method__ . to_s ] . empty?
164197
@@ -167,13 +200,15 @@ def begin_transaction(request, _unused_call)
167200 end
168201
169202 def commit ( request , _unused_call )
203+ log_request ( request )
170204 @requests << request
171205 validate_session request . session
172206 validate_transaction request . session , request . transaction_id
173207 Google ::Cloud ::Spanner ::V1 ::CommitResponse . new commit_timestamp : Google ::Protobuf ::Timestamp . new ( seconds : Time . now . to_i )
174208 end
175209
176210 def rollback ( request , _unused_call )
211+ log_request ( request )
177212 @requests << request
178213 validate_session request . session
179214 name = "#{ request . session } /transactions/#{ request . transaction_id } "
@@ -182,16 +217,19 @@ def rollback(request, _unused_call)
182217 end
183218
184219 def partition_query ( request , _unused_call )
220+ log_request ( request )
185221 @requests << request
186222 raise GRPC ::BadStatus . new GRPC ::Core ::StatusCodes ::UNIMPLEMENTED , "Not yet implemented"
187223 end
188224
189225 def partition_read ( request , _unused_call )
226+ log_request ( request )
190227 @requests << request
191228 raise GRPC ::BadStatus . new GRPC ::Core ::StatusCodes ::UNIMPLEMENTED , "Not yet implemented"
192229 end
193230
194231 def get_database ( request , _unused_call )
232+ log_request ( request )
195233 @requests << request
196234 raise GRPC ::BadStatus . new GRPC ::Core ::StatusCodes ::UNIMPLEMENTED , "Not yet implemented"
197235 end
@@ -208,18 +246,50 @@ def abort_next_transaction
208246 end
209247
210248 def get_statement_result ( sql )
249+ load_dynamic_mocks!
250+
211251 unless @statement_results . key? sql
212252 @statement_results . each do |key , value |
213253 return value if key . end_with? ( "%" ) && sql . start_with? ( key . chop )
214254 end
255+ available_keys = @statement_results . keys . join ( ", " )
215256 raise GRPC ::BadStatus . new (
216257 GRPC ::Core ::StatusCodes ::INVALID_ARGUMENT ,
217- "There's no result registered for #{ sql } "
258+ "No result registered for ' #{ sql } '. Available: [ #{ available_keys } ] "
218259 )
219260 end
220261 @statement_results [ sql ]
221262 end
222263
264+ def load_dynamic_mocks!
265+ return unless @mock_file_path && File . exist? ( @mock_file_path )
266+
267+ begin
268+ # Read binary data from file
269+ content = File . binread ( @mock_file_path )
270+ return if content . empty?
271+
272+ # Deserialize the Ruby Hash containing new mocks
273+ # rubocop:disable Security/MarshalLoad
274+ new_mocks = Marshal . load ( content )
275+ # rubocop:enable Security/MarshalLoad
276+
277+ # Merge into existing results
278+ @statement_results . merge! ( new_mocks )
279+ rescue StandardError => e
280+ # Ignore read errors (race conditions during writing)
281+ warn "Failed to load mocks: #{ e . message } "
282+ end
283+ end
284+
285+ def add_default_results
286+ put_statement_result "SELECT 1" , StatementResult . create_select1_result
287+ put_statement_result "INSERT INTO test_table (id, name) VALUES ('1', 'Alice')" , StatementResult . create_update_count_result ( 1 )
288+
289+ dialect_sql = "select option_value from information_schema.database_options where option_name='database_dialect'"
290+ put_statement_result dialect_sql , StatementResult . create_dialect_result
291+ end
292+
223293 def delete_all_sessions
224294 @sessions . clear
225295 end
0 commit comments