@@ -206,6 +206,86 @@ def test_prepare_file_data_file_error(self, mock_file_open, mock_logger):
206206 mock_file_open .assert_called_once ()
207207
208208
209+ class TestGenerateInsertQuery :
210+ mock_field_timestamp = MagicMock ()
211+ mock_field_timestamp .name = "field_timestamp"
212+ mock_field_timestamp .db_column = "_timestamp"
213+ mock_field_timestamp .generated = False
214+ mock_field_timestamp .get_internal_type .return_value = "DateTimeField"
215+
216+ mock_field_comment = MagicMock ()
217+ mock_field_comment .name = "comment"
218+ mock_field_comment .db_column = None
219+ mock_field_comment .generated = False
220+ mock_field_comment .get_internal_type .return_value = "CharField"
221+
222+ @patch ("kernelCI_app.management.commands.helpers.kcidbng_ingester.PRIO_DB" , True )
223+ def test_generate_model_insert_query_prio_db_true (self ):
224+ """Test _generate_model_insert_query with PRIO_DB=True (prefers existing DB values)."""
225+ from kernelCI_app .management .commands .helpers .kcidbng_ingester import (
226+ _generate_model_insert_query ,
227+ )
228+
229+ mock_model = MagicMock ()
230+
231+ mock_field3 = MagicMock ()
232+ mock_field3 .name = "checkout"
233+ mock_field3 .db_column = None
234+ mock_field3 .generated = False
235+ mock_field3 .get_internal_type .return_value = "ForeignKey"
236+
237+ mock_field4 = MagicMock ()
238+ mock_field4 .name = "series"
239+ mock_field4 .generated = True
240+
241+ mock_model ._meta .fields = [
242+ self .mock_field_timestamp ,
243+ self .mock_field_comment ,
244+ mock_field3 ,
245+ mock_field4 ,
246+ ]
247+
248+ with patch (
249+ "kernelCI_app.management.commands.helpers.kcidbng_ingester.MODEL_MAP" ,
250+ {"issues" : mock_model },
251+ ):
252+ updateable_fields , query = _generate_model_insert_query (
253+ "issues" , mock_model
254+ )
255+
256+ assert updateable_fields == ["field_timestamp" , "comment" , "checkout_id" ]
257+ assert "INSERT INTO issues" in query
258+ assert "_timestamp, comment, checkout_id" in query
259+ # PRIO_DB=True means database values come first in COALESCE/GREATEST
260+ assert "GREATEST(issues._timestamp, EXCLUDED._timestamp)" in query
261+ assert "COALESCE(issues.comment, EXCLUDED.comment)" in query
262+ assert "COALESCE(issues.checkout_id, EXCLUDED.checkout_id)" in query
263+ assert "series" not in query
264+
265+ @patch ("kernelCI_app.management.commands.helpers.kcidbng_ingester.PRIO_DB" , False )
266+ def test_generate_model_insert_query_prio_db_false (self ):
267+ """Test _generate_model_insert_query with PRIO_DB=False (prefers new values)."""
268+ from kernelCI_app .management .commands .helpers .kcidbng_ingester import (
269+ _generate_model_insert_query ,
270+ )
271+
272+ mock_model = MagicMock ()
273+ mock_model ._meta .fields = [self .mock_field_timestamp , self .mock_field_comment ]
274+
275+ with patch (
276+ "kernelCI_app.management.commands.helpers.kcidbng_ingester.MODEL_MAP" ,
277+ {"tests" : mock_model },
278+ ):
279+ updateable_fields , query = _generate_model_insert_query ("tests" , mock_model )
280+
281+ assert updateable_fields == ["field_timestamp" , "comment" ]
282+ assert "INSERT INTO tests" in query
283+ assert "_timestamp, comment" in query
284+ # PRIO_DB=False means new values (EXCLUDED) come first in COALESCE/GREATEST
285+ assert "GREATEST(EXCLUDED._timestamp, tests._timestamp)" in query
286+ assert "COALESCE(EXCLUDED.comment, tests.comment)" in query
287+
288+
209289class TestConsumeBuffer :
210290 """Test cases for consume_buffer function."""
211291
@@ -218,24 +298,45 @@ class TestConsumeBuffer:
218298 INGEST_BATCH_SIZE_MOCK ,
219299 )
220300 @patch ("kernelCI_app.management.commands.helpers.kcidbng_ingester.out" )
301+ @patch (
302+ "kernelCI_app.management.commands.helpers.kcidbng_ingester._generate_model_insert_query"
303+ )
304+ @patch ("kernelCI_app.management.commands.helpers.kcidbng_ingester.connections" )
221305 @patch ("time.time" , side_effect = TIME_MOCK )
222- def test_consume_buffer_with_items (self , mock_time , mock_out ):
306+ def test_consume_buffer_with_items (
307+ self , mock_time , mock_connections , mock_generate_query , mock_out
308+ ):
223309 """Test consume_buffer with items in buffer."""
310+ table_name = "issues"
224311 mock_model = MagicMock ()
225312 mock_buffer = [MagicMock (), MagicMock ()]
313+ mock_generate_query .return_value = (
314+ ["_timestamp" , "other_field" ],
315+ """
316+ INSERT INTO issues (
317+ _timestamp, other_field
318+ )
319+ VALUES (
320+ %s, %s
321+ )
322+ ON CONFLICT (id)
323+ DO UPDATE SET
324+ GREATEST(issues._timestamp, EXCLUDED._timestamp),
325+ COALESCE(issues.other_field, EXCLUDED.other_field);""" ,
326+ )
327+ mock_cursor = MagicMock ()
328+ mock_connections ["default" ].cursor .return_value .__enter__ .return_value = (
329+ mock_cursor
330+ )
226331
227332 with patch (
228333 "kernelCI_app.management.commands.helpers.kcidbng_ingester.MODEL_MAP" ,
229334 {"issues" : mock_model },
230335 ):
231- consume_buffer (mock_buffer , "issues" )
336+ consume_buffer (mock_buffer , table_name )
232337
233338 assert mock_time .call_count == 2
234- mock_model .objects .bulk_create .assert_called_once_with (
235- mock_buffer ,
236- batch_size = INGEST_BATCH_SIZE_MOCK ,
237- ignore_conflicts = True ,
238- )
339+ mock_cursor .executemany .assert_called_once ()
239340 mock_out .assert_called_once ()
240341
241342 @patch ("kernelCI_app.management.commands.helpers.kcidbng_ingester.out" )
@@ -254,6 +355,12 @@ def test_consume_buffer_empty_buffer(self, mock_time, mock_out):
254355 mock_time .assert_not_called ()
255356 mock_out .assert_not_called ()
256357
358+ def test_consume_buffer_wrong_table (self ):
359+ """Test consume_buffer with invalid table name raises KeyError."""
360+ with pytest .raises (KeyError ):
361+ mock_model = MagicMock ()
362+ consume_buffer ([mock_model ], "another" )
363+
257364
258365class TestFlushBuffers :
259366 """Test cases for flush_buffers function."""
0 commit comments