@@ -1944,6 +1944,147 @@ def store_callback(table, **kwargs):
19441944 assert_that (failed_values , equal_to (failed_rows ))
19451945
19461946
1947+ def test_with_batched_input_exceeds_size_limit (self ):
1948+ from apache_beam .utils .windowed_value import WindowedValue
1949+ from apache_beam .transforms import window
1950+
1951+ client = mock .Mock ()
1952+ client .tables .Get .return_value = bigquery .Table (
1953+ tableReference = bigquery .TableReference (
1954+ projectId = 'project-id' , datasetId = 'dataset_id' , tableId = 'table_id' ))
1955+ client .insert_rows_json .return_value = []
1956+ fn = beam .io .gcp .bigquery .BigQueryWriteFn (
1957+ batch_size = 10 ,
1958+ create_disposition = beam .io .BigQueryDisposition .CREATE_IF_NEEDED ,
1959+ write_disposition = beam .io .BigQueryDisposition .WRITE_APPEND ,
1960+ kms_key = None ,
1961+ with_batched_input = True ,
1962+ max_insert_payload_size = 500 , # Small size to trigger the check
1963+ test_client = client )
1964+
1965+ fn .start_bundle ()
1966+
1967+ # Create rows where the third one exceeds the size limit
1968+ rows = [
1969+ ({
1970+ 'month' : 1
1971+ }, 'insertid1' ),
1972+ ({
1973+ 'month' : 2
1974+ }, 'insertid2' ),
1975+ ({
1976+ 'columnA' : 'large_string' * 100
1977+ }, 'insertid3' ) # This exceeds 500 bytes
1978+ ]
1979+
1980+ # Create a windowed value for testing
1981+ test_window = window .GlobalWindow ()
1982+ test_timestamp = window .MIN_TIMESTAMP
1983+ windowed_value = WindowedValue (None , test_timestamp , [test_window ])
1984+
1985+ # Process the batched input
1986+ result = fn .process (('project-id:dataset_id.table_id' , rows ), windowed_value )
1987+
1988+ # Convert generator to list to check results
1989+ failed_rows = list (result )
1990+
1991+ # Should have 2 failed outputs (FAILED_ROWS_WITH_ERRORS and FAILED_ROWS)
1992+ self .assertEqual (len (failed_rows ), 2 )
1993+
1994+ # Check that the large row was sent to DLQ
1995+ failed_with_errors = [
1996+ r for r in failed_rows if r .tag == fn .FAILED_ROWS_WITH_ERRORS
1997+ ]
1998+ failed_without_errors = [r for r in failed_rows if r .tag == fn .FAILED_ROWS ]
1999+
2000+ self .assertEqual (len (failed_with_errors ), 1 )
2001+ self .assertEqual (len (failed_without_errors ), 1 )
2002+
2003+ # Verify the error message
2004+ destination , row , error = failed_with_errors [0 ].value .value
2005+ self .assertEqual (destination , 'project-id:dataset_id.table_id' )
2006+ self .assertEqual (row , {'columnA' : 'large_string' * 100 })
2007+ self .assertIn ('exceeds the maximum insert payload size' , error )
2008+
2009+ # Verify that only the valid rows were sent to BigQuery
2010+ self .assertTrue (client .insert_rows_json .called )
2011+ # Check that the rows were inserted (might be in multiple batches)
2012+ total_rows = []
2013+ for call in client .insert_rows_json .call_args_list :
2014+ total_rows .extend (call [1 ]['json_rows' ])
2015+
2016+ self .assertEqual (len (total_rows ), 2 )
2017+ self .assertEqual (total_rows [0 ], {'month' : 1 })
2018+ self .assertEqual (total_rows [1 ], {'month' : 2 })
2019+
2020+
2021+ def test_with_batched_input_splits_large_batch (self ):
2022+ from apache_beam .utils .windowed_value import WindowedValue
2023+ from apache_beam .transforms import window
2024+
2025+ client = mock .Mock ()
2026+ client .tables .Get .return_value = bigquery .Table (
2027+ tableReference = bigquery .TableReference (
2028+ projectId = 'project-id' , datasetId = 'dataset_id' , tableId = 'table_id' ))
2029+ client .insert_rows_json .return_value = []
2030+ create_disposition = beam .io .BigQueryDisposition .CREATE_IF_NEEDED
2031+ write_disposition = beam .io .BigQueryDisposition .WRITE_APPEND
2032+
2033+ fn = beam .io .gcp .bigquery .BigQueryWriteFn (
2034+ batch_size = 10 ,
2035+ create_disposition = create_disposition ,
2036+ write_disposition = write_disposition ,
2037+ kms_key = None ,
2038+ with_batched_input = True ,
2039+ max_insert_payload_size = 800 , # Small size to force batch splitting
2040+ test_client = client )
2041+
2042+ fn .start_bundle ()
2043+
2044+ # Create rows that together exceed the batch size limit.
2045+ # Each row with 'data' * 10 is about 200 bytes
2046+ # So 2 rows should fit, 3rd should cause flush.
2047+ rows = [
2048+ ({
2049+ 'data' : 'x' * 10
2050+ }, 'insertid1' ),
2051+ ({
2052+ 'data' : 'y' * 10
2053+ }, 'insertid2' ),
2054+ ({
2055+ 'data' : 'z' * 10
2056+ }, 'insertid3' ), # This should go in second batch
2057+ ]
2058+
2059+ # Create a windowed value for testing
2060+ test_window = window .GlobalWindow ()
2061+ test_timestamp = window .MIN_TIMESTAMP
2062+ windowed_value = WindowedValue (None , test_timestamp , [test_window ])
2063+
2064+ # Process the batched input
2065+ result = fn .process (('project-id:dataset_id.table_id' , rows ), windowed_value )
2066+
2067+ # Convert generator to list (should be empty as no failures)
2068+ failed_rows = list (result )
2069+ self .assertEqual (len (failed_rows ), 0 )
2070+
2071+ # With 800 byte limit and ~341 byte rows
2072+ # we should be able to fit 2 rows per batch.
2073+ # So we expect 2 calls: [row1, row2] and [row3]
2074+ self .assertEqual (client .insert_rows_json .call_count , 2 )
2075+
2076+ # Check first batch (2 rows)
2077+ first_call = client .insert_rows_json .call_args_list [0 ][1 ]
2078+ self .assertEqual (len (first_call ['json_rows' ]), 2 )
2079+ self .assertEqual (first_call ['json_rows' ][0 ], {'data' : 'x' * 10 })
2080+ self .assertEqual (first_call ['json_rows' ][1 ], {'data' : 'y' * 10 })
2081+
2082+ # Check second batch (1 row)
2083+ second_call = client .insert_rows_json .call_args_list [1 ][1 ]
2084+ self .assertEqual (len (second_call ['json_rows' ]), 1 )
2085+ self .assertEqual (second_call ['json_rows' ][0 ], {'data' : 'z' * 10 })
2086+
2087+
19472088@unittest .skipIf (HttpError is None , 'GCP dependencies are not installed' )
19482089class BigQueryStreamingInsertTransformTests (unittest .TestCase ):
19492090 def test_dofn_client_process_performs_batching (self ):
0 commit comments