@@ -155,6 +155,110 @@ public static String tblCreateSQL(String table) {
155155 }
156156 }
157157
158+ // A non-nullable Array column cannot represent a null. Writing a Java null into one through the POJO
159+ // insert path (POJOSerDe -> RowBinaryFormatSerializer.writeValuePreamble) must fail loudly like every
160+ // other non-nullable type instead of emitting a stray marker byte, which the server read as a phantom
161+ // extra row or a column shift. The fixed-width 'tail' column after the array would be corrupted by any
162+ // stray byte, so it doubles as a misframing sentinel.
163+ @ Test (groups = {"integration" }, dataProvider = "nonNullableArrayNullColumns" )
164+ public void testInsertNullIntoNonNullableArrayThrows (String columns ) throws Exception {
165+ final String table = "test_non_nullable_array_null" ;
166+ client .execute ("DROP TABLE IF EXISTS " + table ).get ();
167+ client .execute (tableDefinition (table , columns )).get ();
168+
169+ client .register (DTOForNonNullableArrayTests .class , client .getTableSchema (table ));
170+
171+ Exception thrown = null ;
172+ try {
173+ client .insert (table , Collections .singletonList (new DTOForNonNullableArrayTests (1 , null , 7 )))
174+ .get (EXECUTE_CMD_TIMEOUT , TimeUnit .SECONDS );
175+ } catch (Exception e ) {
176+ thrown = e ;
177+ }
178+
179+ Assert .assertNotNull (thrown , "Expected the insert to fail for a null in a non-nullable Array column using " + columns );
180+ boolean clearMessage = false ;
181+ for (Throwable t = thrown ; t != null ; t = t .getCause ()) {
182+ if (t .getMessage () != null && t .getMessage ().contains ("An attempt to write null into not nullable column" )) {
183+ clearMessage = true ;
184+ break ;
185+ }
186+ }
187+ Assert .assertTrue (clearMessage , "Expected a clear non-nullable column error using " + columns + ", but got: " + thrown );
188+ }
189+
190+ @ DataProvider (name = "nonNullableArrayNullColumns" )
191+ public static Object [][] nonNullableArrayNullColumns () {
192+ return new Object [][] {
193+ // No defaults anywhere: the POJO insert uses plain RowBinary (non-defaults preamble branch).
194+ {"rowId Int32, arr Array(Int32), tail Int32" },
195+ // A sibling column has a default, so the POJO insert uses RowBinaryWithDefaults; the
196+ // non-nullable arr itself has no default and so must still reject a null (defaults branch).
197+ {"rowId Int32, arr Array(Int32), tail Int32 DEFAULT 99" },
198+ };
199+ }
200+
201+ // Contrast: valid non-nullable arrays (empty and populated) still round-trip through the POJO insert
202+ // path, and the trailing 'tail' column keeps its value - proving the array length is still written as a
203+ // single leading byte and that rejecting null did not perturb valid array writes.
204+ @ Test (groups = {"integration" }, dataProvider = "nonNullableArrayRoundTrip" )
205+ public void testInsertNonNullableArrayRoundTrips (List <Integer > arr , int expectedLength , String expectedConcat ) throws Exception {
206+ final String table = "test_non_nullable_array_round_trip" ;
207+ client .execute ("DROP TABLE IF EXISTS " + table ).get ();
208+ client .execute (tableDefinition (table , "rowId Int32" , "arr Array(Int32)" , "tail Int32" )).get ();
209+
210+ client .register (DTOForNonNullableArrayTests .class , client .getTableSchema (table ));
211+ client .insert (table , Collections .singletonList (new DTOForNonNullableArrayTests (1 , arr , 7 )))
212+ .get (EXECUTE_CMD_TIMEOUT , TimeUnit .SECONDS );
213+
214+ List <GenericRecord > records = client .queryAll (
215+ "SELECT toInt32(length(arr)) AS alen, arrayStringConcat(arr, ',') AS acat, tail FROM " + table + " ORDER BY rowId" );
216+ Assert .assertEquals (records .size (), 1 );
217+ GenericRecord row = records .get (0 );
218+ Assert .assertEquals (row .getInteger ("alen" ), expectedLength );
219+ Assert .assertEquals (row .getString ("acat" ), expectedConcat );
220+ Assert .assertEquals (row .getInteger ("tail" ), 7 );
221+ }
222+
223+ @ DataProvider (name = "nonNullableArrayRoundTrip" )
224+ public static Object [][] nonNullableArrayRoundTrip () {
225+ return new Object [][] {
226+ {new ArrayList <Integer >(), 0 , "" },
227+ {Arrays .asList (1 , 2 , 3 ), 3 , "1,2,3" },
228+ };
229+ }
230+
231+ // Contrast: with a DDL DEFAULT the write uses RowBinaryWithDefaults, where a null into the non-nullable
232+ // Array column falls back to the default instead of throwing - proving the fix only rejects null for a
233+ // non-nullable array that has no default.
234+ @ Test (groups = {"integration" })
235+ public void testInsertNullIntoDefaultedNonNullableArrayUsesDefault () throws Exception {
236+ final String table = "test_non_nullable_array_null_default" ;
237+ client .execute ("DROP TABLE IF EXISTS " + table ).get ();
238+ client .execute (tableDefinition (table , "rowId Int32" , "arr Array(Int32) DEFAULT [1, 2]" , "tail Int32" )).get ();
239+
240+ client .register (DTOForNonNullableArrayTests .class , client .getTableSchema (table ));
241+ client .insert (table , Collections .singletonList (new DTOForNonNullableArrayTests (1 , null , 7 )))
242+ .get (EXECUTE_CMD_TIMEOUT , TimeUnit .SECONDS );
243+
244+ List <GenericRecord > records = client .queryAll (
245+ "SELECT toInt32(length(arr)) AS alen, arrayStringConcat(arr, ',') AS acat, tail FROM " + table + " ORDER BY rowId" );
246+ Assert .assertEquals (records .size (), 1 );
247+ GenericRecord row = records .get (0 );
248+ Assert .assertEquals (row .getInteger ("alen" ), 2 );
249+ Assert .assertEquals (row .getString ("acat" ), "1,2" );
250+ Assert .assertEquals (row .getInteger ("tail" ), 7 );
251+ }
252+
253+ @ Data
254+ @ AllArgsConstructor
255+ @ NoArgsConstructor
256+ public static class DTOForNonNullableArrayTests {
257+ private int rowId ;
258+ private List <Integer > arr ;
259+ private int tail ;
260+ }
261+
158262 @ Data
159263 @ AllArgsConstructor
160264 @ NoArgsConstructor
0 commit comments