@@ -159,52 +159,61 @@ private Object[][] nestedNullableData() throws Exception {
159159 UUID uuid = UUID .fromString ("61f0c404-5cb3-11e7-907b-a6006ad3dba0" );
160160 InetAddress ipv4 = InetAddress .getByName ("1.2.3.4" );
161161 return new Object [][] {
162- // A present Nullable element of each datatype nested in a Tuple, with a trailing
163- // non-nullable sibling whose bytes misalign if the present-marker is dropped.
164- {"Tuple(Nullable(String), String)" , Arrays .asList ("opt" , "tail" )},
165- {"Tuple(Nullable(FixedString(3)), String)" , Arrays .asList ("abc" , "tail" )},
166- {"Tuple(Nullable(Int8), String)" , Arrays .asList ((byte ) -5 , "tail" )},
167- {"Tuple(Nullable(UInt8), String)" , Arrays .asList ((short ) 200 , "tail" )},
168- {"Tuple(Nullable(Int16), String)" , Arrays .asList ((short ) -1600 , "tail" )},
169- {"Tuple(Nullable(UInt16), String)" , Arrays .asList (40000 , "tail" )},
170- {"Tuple(Nullable(Int32), String)" , Arrays .asList (42 , "tail" )},
171- {"Tuple(Nullable(UInt32), String)" , Arrays .asList (4_000_000_000L , "tail" )},
172- {"Tuple(Nullable(Int64), String)" , Arrays .asList (-64L , "tail" )},
173- {"Tuple(Nullable(UInt64), String)" , Arrays .asList (BigInteger .valueOf (64 ), "tail" )},
174- {"Tuple(Nullable(Float32), String)" , Arrays .asList (1.5f , "tail" )},
175- {"Tuple(Nullable(Float64), String)" , Arrays .asList (2.5d , "tail" )},
176- {"Tuple(Nullable(Bool), String)" , Arrays .asList (true , "tail" )},
177- {"Tuple(Nullable(UUID), String)" , Arrays .asList (uuid , "tail" )},
178- {"Tuple(Nullable(Date), String)" , Arrays .asList (LocalDate .of (2021 , 2 , 3 ), "tail" )},
179- {"Tuple(Nullable(Decimal64(4)), String)" , Arrays .asList (new BigDecimal ("1.2345" ), "tail" )},
180- {"Tuple(Nullable(IPv4), String)" , Arrays .asList (ipv4 , "tail" )},
162+ // Each present Nullable element sits in the MIDDLE of the schema: a non-nullable
163+ // leading column, the Nullable, then a trailing non-nullable Float64. If the
164+ // present-marker byte is dropped, every following byte shifts and the trailing
165+ // Float64 reads a wrong value, so a faulty serialization is detected positionally
166+ // rather than only by running out of bytes. The assertion compares the whole row.
167+ {"Tuple(Int32, Nullable(String), Float64)" , Arrays .asList (7 , "opt" , 9.5d )},
168+ {"Tuple(Int32, Nullable(FixedString(3)), Float64)" , Arrays .asList (7 , "abc" , 9.5d )},
169+ {"Tuple(Int32, Nullable(Int8), Float64)" , Arrays .asList (7 , (byte ) -5 , 9.5d )},
170+ {"Tuple(Int32, Nullable(UInt8), Float64)" , Arrays .asList (7 , (short ) 200 , 9.5d )},
171+ {"Tuple(Int32, Nullable(Int16), Float64)" , Arrays .asList (7 , (short ) -1600 , 9.5d )},
172+ {"Tuple(Int32, Nullable(UInt16), Float64)" , Arrays .asList (7 , 40000 , 9.5d )},
173+ {"Tuple(Int32, Nullable(Int32), Float64)" , Arrays .asList (7 , 42 , 9.5d )},
174+ {"Tuple(Int32, Nullable(UInt32), Float64)" , Arrays .asList (7 , 4_000_000_000L , 9.5d )},
175+ {"Tuple(Int32, Nullable(Int64), Float64)" , Arrays .asList (7 , -64L , 9.5d )},
176+ {"Tuple(Int32, Nullable(UInt64), Float64)" , Arrays .asList (7 , BigInteger .valueOf (64 ), 9.5d )},
177+ {"Tuple(Int32, Nullable(Float32), Float64)" , Arrays .asList (7 , 1.5f , 9.5d )},
178+ {"Tuple(Int32, Nullable(Float64), Float64)" , Arrays .asList (7 , 2.5d , 9.5d )},
179+ {"Tuple(Int32, Nullable(Bool), Float64)" , Arrays .asList (7 , true , 9.5d )},
180+ {"Tuple(Int32, Nullable(UUID), Float64)" , Arrays .asList (7 , uuid , 9.5d )},
181+ {"Tuple(Int32, Nullable(Date), Float64)" , Arrays .asList (7 , LocalDate .of (2021 , 2 , 3 ), 9.5d )},
182+ {"Tuple(Int32, Nullable(Decimal64(4)), Float64)" , Arrays .asList (7 , new BigDecimal ("1.2345" ), 9.5d )},
183+ {"Tuple(Int32, Nullable(IPv4), Float64)" , Arrays .asList (7 , ipv4 , 9.5d )},
181184
182185 // A Tuple value given as a Java array (not a List) takes the other branch of
183- // serializeTupleData, which is routed through the same nested-marker path.
184- {"Tuple(Nullable(String), String)" , new Object [] {"opt" , "tail" }},
185-
186- // The same marker handling on the Map value path, across a range of widths.
187- {"Map(String, Nullable(String))" , newMap ("k" , "v" )},
188- {"Map(String, Nullable(Int32))" , newMap ("k" , 32 )},
189- {"Map(String, Nullable(Float64))" , newMap ("k" , 2.5d )},
190- {"Map(String, Nullable(UUID))" , newMap ("k" , uuid )},
191-
192- // Null elements/values still serialize a single null-marker byte.
193- {"Tuple(Nullable(String), String)" , Arrays .asList (null , "tail" )},
194- {"Tuple(Nullable(Int32), String)" , Arrays .asList (null , "tail" )},
195- {"Tuple(Nullable(Int32), Nullable(String))" , Arrays .asList (null , null )},
196- {"Map(String, Nullable(String))" , newMap ("k" , null )},
186+ // serializeTupleData, which is routed through the same nested-marker path, for
187+ // both a present value and a null.
188+ {"Tuple(Int32, Nullable(String), Float64)" , new Object [] {7 , "opt" , 9.5d }},
189+ {"Tuple(Int32, Nullable(String), Float64)" , new Object [] {7 , null , 9.5d }},
190+
191+ // The Map value path: the Nullable map value sits between the key and a trailing
192+ // Float64, so a dropped value-marker misaligns the float.
193+ {"Tuple(Int32, Map(String, Nullable(String)), Float64)" , Arrays .asList (7 , newMap ("k" , "v" ), 9.5d )},
194+ {"Tuple(Int32, Map(String, Nullable(Int32)), Float64)" , Arrays .asList (7 , newMap ("k" , 32 ), 9.5d )},
195+ {"Tuple(Int32, Map(String, Nullable(Float64)), Float64)" , Arrays .asList (7 , newMap ("k" , 2.5d ), 9.5d )},
196+ {"Tuple(Int32, Map(String, Nullable(UUID)), Float64)" , Arrays .asList (7 , newMap ("k" , uuid ), 9.5d )},
197+
198+ // Null elements/values still serialize a single null-marker byte; the trailing
199+ // Float64 confirms the following data stays aligned.
200+ {"Tuple(Int32, Nullable(String), Float64)" , Arrays .asList (7 , null , 9.5d )},
201+ {"Tuple(Int32, Nullable(Int32), Nullable(String), Float64)" , Arrays .asList (7 , null , null , 9.5d )},
202+ {"Tuple(Int32, Map(String, Nullable(String)), Float64)" , Arrays .asList (7 , newMap ("k" , null ), 9.5d )},
197203
198204 // Containers compose: marker handling threads through nested Tuple/Map/Array,
199- // including Array(Tuple(Nullable)) which is how Nested columns are encoded.
200- {"Tuple(String, Map(String, Nullable(String)))" , Arrays .asList ("id" , newMap ("k1" , "v1" , "k2" , null ))},
201- {"Tuple(Nullable(String), Map(String, Nullable(Int32)))" , Arrays .asList ("opt" , newMap ("k" , 7 ))},
202- {"Array(Tuple(Nullable(String), String))" , Arrays .asList (Arrays .asList ("a" , "b" ), Arrays .asList (null , "c" ))},
203- {"Tuple(Array(Nullable(Int32)), String)" , Arrays .asList (Arrays .asList (1 , null , 3 ), "tail" )},
204-
205- // Contrast: non-nullable nested elements must keep serializing without a marker.
206- {"Tuple(Int32, String)" , Arrays .asList (7 , "tail" )},
207- {"Map(String, String)" , newMap ("k" , "v" )},
205+ // including Array(Tuple(Nullable)) which is how Nested columns are encoded. A
206+ // trailing Float64 after each nested container detects misalignment.
207+ {"Array(Tuple(Int32, Nullable(String), Float64))" ,
208+ Arrays .asList (Arrays .asList (7 , "a" , 9.5d ), Arrays .asList (7 , null , 8.5d ))},
209+ {"Tuple(String, Map(String, Nullable(Int32)), Float64)" ,
210+ Arrays .asList ("id" , newMap ("k1" , 7 , "k2" , null ), 9.5d )},
211+ {"Tuple(Array(Nullable(Int32)), Float64)" , Arrays .asList (Arrays .asList (1 , null , 3 ), 9.5d )},
212+
213+ // Contrast: non-nullable nested elements must keep serializing without a marker,
214+ // so these rows round-trip identically with or without the fix.
215+ {"Tuple(Int32, String, Float64)" , Arrays .asList (7 , "tail" , 9.5d )},
216+ {"Tuple(Int32, Map(String, String), Float64)" , Arrays .asList (7 , newMap ("k" , "v" ), 9.5d )},
208217 };
209218 }
210219
0 commit comments