@@ -1665,52 +1665,342 @@ mod tests {
16651665 assert ! ( updated_paths. contains( & ColumnName :: new( [ "identifier" ] ) ) ) ;
16661666 assert ! ( updated_paths. contains( & ColumnName :: new( [ "user" , "full_name" ] ) ) ) ;
16671667 }
1668+ #[ test]
1669+ fn test_array_element_struct_field_changes ( ) {
1670+ let before = StructType :: new_unchecked ( [ create_field_with_id (
1671+ "items" ,
1672+ DataType :: Array ( Box :: new ( ArrayType :: new (
1673+ DataType :: try_struct_type ( [
1674+ create_field_with_id ( "name" , DataType :: STRING , false , 2 ) ,
1675+ create_field_with_id ( "removed_field" , DataType :: INTEGER , true , 3 ) ,
1676+ ] )
1677+ . unwrap ( ) ,
1678+ true ,
1679+ ) ) ) ,
1680+ false ,
1681+ 1 ,
1682+ ) ] ) ;
1683+
1684+ let after = StructType :: new_unchecked ( [ create_field_with_id (
1685+ "items" ,
1686+ DataType :: Array ( Box :: new ( ArrayType :: new (
1687+ DataType :: try_struct_type ( [
1688+ create_field_with_id ( "title" , DataType :: STRING , false , 2 ) , // Renamed!
1689+ create_field_with_id ( "added_field" , DataType :: STRING , true , 4 ) , // Added!
1690+ ] )
1691+ . unwrap ( ) ,
1692+ true ,
1693+ ) ) ) ,
1694+ false ,
1695+ 1 ,
1696+ ) ] ) ;
1697+
1698+ let diff = SchemaDiffArgs {
1699+ before : & before,
1700+ after : & after,
1701+ }
1702+ . compute_diff ( )
1703+ . unwrap ( ) ;
1704+
1705+ assert_eq ! ( diff. added_fields. len( ) , 1 ) ;
1706+ assert_eq ! ( diff. removed_fields. len( ) , 1 ) ;
1707+ assert_eq ! ( diff. updated_fields. len( ) , 1 ) ;
1708+
1709+ // Check added field
1710+ assert_eq ! (
1711+ diff. added_fields[ 0 ] . path,
1712+ ColumnName :: new( [ "items" , "element" , "added_field" ] )
1713+ ) ;
1714+
1715+ // Check removed field
1716+ assert_eq ! (
1717+ diff. removed_fields[ 0 ] . path,
1718+ ColumnName :: new( [ "items" , "element" , "removed_field" ] )
1719+ ) ;
1720+
1721+ // Check updated field (rename)
1722+ let update = & diff. updated_fields [ 0 ] ;
1723+ assert_eq ! ( update. path, ColumnName :: new( [ "items" , "element" , "title" ] ) ) ;
1724+ assert_eq ! ( update. change_types, vec![ FieldChangeType :: Renamed ] ) ;
1725+
1726+ assert ! ( !diff. has_breaking_changes( ) ) ; // Removal is safe, rename is safe
1727+ }
16681728
16691729 #[ test]
1670- fn test_top_level_and_nested_change_filters ( ) {
1671- // Test that top_level_changes and nested_changes correctly filter by path depth.
1672- // This test manually constructs a SchemaDiff to exercise the filtering logic.
1730+ fn test_doubly_nested_array_type_change ( ) {
1731+ // Test that we can detect type changes in doubly nested arrays: array<array<int>> -> array<array<double>>
1732+ let before = StructType :: new_unchecked ( [ create_field_with_id (
1733+ "matrix" ,
1734+ DataType :: Array ( Box :: new ( ArrayType :: new (
1735+ DataType :: Array ( Box :: new ( ArrayType :: new ( DataType :: INTEGER , false ) ) ) ,
1736+ false ,
1737+ ) ) ) ,
1738+ false ,
1739+ 1 ,
1740+ ) ] ) ;
16731741
1674- let top_level_field = create_field_with_id ( "name" , DataType :: STRING , false , 1 ) ;
1675- let nested_field = create_field_with_id ( "street" , DataType :: STRING , false , 2 ) ;
1676- let deeply_nested_field = create_field_with_id ( "city" , DataType :: STRING , false , 3 ) ;
1742+ let after = StructType :: new_unchecked ( [ create_field_with_id (
1743+ "matrix" ,
1744+ DataType :: Array ( Box :: new ( ArrayType :: new (
1745+ DataType :: Array ( Box :: new ( ArrayType :: new ( DataType :: DOUBLE , false ) ) ) ,
1746+ false ,
1747+ ) ) ) ,
1748+ false ,
1749+ 1 ,
1750+ ) ] ) ;
16771751
1678- // Create a diff with mixed top-level and nested changes
1679- let diff = SchemaDiff {
1680- added_fields : vec ! [
1681- FieldChange {
1682- field: top_level_field. clone( ) ,
1683- path: ColumnName :: new( [ "name" ] ) , // Top-level (depth 1)
1684- } ,
1685- FieldChange {
1686- field: nested_field. clone( ) ,
1687- path: ColumnName :: new( [ "address" , "street" ] ) , // Nested (depth 2)
1688- } ,
1689- ] ,
1690- removed_fields : vec ! [ FieldChange {
1691- field: deeply_nested_field. clone( ) ,
1692- path: ColumnName :: new( [ "user" , "address" , "city" ] ) , // Deeply nested (depth 3)
1693- } ] ,
1694- updated_fields : vec ! [ ] ,
1695- has_breaking_changes : false ,
1696- } ;
1752+ let diff = SchemaDiffArgs {
1753+ before : & before,
1754+ after : & after,
1755+ }
1756+ . compute_diff ( )
1757+ . unwrap ( ) ;
16971758
1698- // Test top_level_changes - should only return depth 1 fields
1699- let ( top_added, top_removed, top_updated) = diff. top_level_changes ( ) ;
1700- assert_eq ! ( top_added. len( ) , 1 ) ;
1701- assert_eq ! ( top_added[ 0 ] . path, ColumnName :: new( [ "name" ] ) ) ;
1702- assert_eq ! ( top_removed. len( ) , 0 ) ;
1703- assert_eq ! ( top_updated. len( ) , 0 ) ;
1759+ // The entire field should be reported as TypeChanged since we can't recurse into
1760+ // non-struct array elements (no field IDs at intermediate levels)
1761+ assert_eq ! ( diff. added_fields. len( ) , 0 ) ;
1762+ assert_eq ! ( diff. removed_fields. len( ) , 0 ) ;
1763+ assert_eq ! ( diff. updated_fields. len( ) , 1 ) ;
1764+ assert_eq ! ( diff. updated_fields[ 0 ] . path, ColumnName :: new( [ "matrix" ] ) ) ;
1765+ assert_eq ! (
1766+ diff. updated_fields[ 0 ] . change_types,
1767+ vec![ FieldChangeType :: TypeChanged ]
1768+ ) ;
17041769
1705- // Test nested_changes - should only return depth > 1 fields
1706- let ( nested_added, nested_removed, nested_updated) = diff. nested_changes ( ) ;
1707- assert_eq ! ( nested_added. len( ) , 1 ) ;
1708- assert_eq ! ( nested_added[ 0 ] . path, ColumnName :: new( [ "address" , "street" ] ) ) ;
1709- assert_eq ! ( nested_removed. len( ) , 1 ) ;
1770+ assert ! ( diff. has_breaking_changes( ) ) ; // Type change is breaking
1771+ }
1772+
1773+ #[ test]
1774+ fn test_nested_array_nullability_loosened ( ) {
1775+ // Test: array<array<int> not null> -> array<array<int>>
1776+ // Outer array element nullability loosened (safe change)
1777+ let before = StructType :: new_unchecked ( [ create_field_with_id (
1778+ "matrix" ,
1779+ DataType :: Array ( Box :: new ( ArrayType :: new (
1780+ DataType :: Array ( Box :: new ( ArrayType :: new ( DataType :: INTEGER , false ) ) ) ,
1781+ false , // Outer array elements are non-nullable
1782+ ) ) ) ,
1783+ false ,
1784+ 1 ,
1785+ ) ] ) ;
1786+
1787+ let after = StructType :: new_unchecked ( [ create_field_with_id (
1788+ "matrix" ,
1789+ DataType :: Array ( Box :: new ( ArrayType :: new (
1790+ DataType :: Array ( Box :: new ( ArrayType :: new ( DataType :: INTEGER , false ) ) ) ,
1791+ true , // Outer array elements now nullable
1792+ ) ) ) ,
1793+ false ,
1794+ 1 ,
1795+ ) ] ) ;
1796+
1797+ let diff = SchemaDiffArgs {
1798+ before : & before,
1799+ after : & after,
1800+ }
1801+ . compute_diff ( )
1802+ . unwrap ( ) ;
1803+
1804+ assert_eq ! ( diff. added_fields. len( ) , 0 ) ;
1805+ assert_eq ! ( diff. removed_fields. len( ) , 0 ) ;
1806+ assert_eq ! ( diff. updated_fields. len( ) , 1 ) ;
1807+ assert_eq ! ( diff. updated_fields[ 0 ] . path, ColumnName :: new( [ "matrix" ] ) ) ;
17101808 assert_eq ! (
1711- nested_removed [ 0 ] . path ,
1712- ColumnName :: new ( [ "user" , "address" , "city" ] )
1809+ diff . updated_fields [ 0 ] . change_types ,
1810+ vec! [ FieldChangeType :: ContainerNullabilityLoosened ]
17131811 ) ;
1714- assert_eq ! ( nested_updated. len( ) , 0 ) ;
1812+ assert ! ( !diff. has_breaking_changes( ) ) ; // Loosening is safe
1813+ }
1814+
1815+ #[ test]
1816+ fn test_nested_array_nullability_tightened ( ) {
1817+ // Test: array<array<int>> -> array<array<int> not null>
1818+ // Outer array element nullability tightened (breaking change)
1819+ let before = StructType :: new_unchecked ( [ create_field_with_id (
1820+ "matrix" ,
1821+ DataType :: Array ( Box :: new ( ArrayType :: new (
1822+ DataType :: Array ( Box :: new ( ArrayType :: new ( DataType :: INTEGER , false ) ) ) ,
1823+ true , // Outer array elements are nullable
1824+ ) ) ) ,
1825+ false ,
1826+ 1 ,
1827+ ) ] ) ;
1828+
1829+ let after = StructType :: new_unchecked ( [ create_field_with_id (
1830+ "matrix" ,
1831+ DataType :: Array ( Box :: new ( ArrayType :: new (
1832+ DataType :: Array ( Box :: new ( ArrayType :: new ( DataType :: INTEGER , false ) ) ) ,
1833+ false , // Outer array elements now non-nullable
1834+ ) ) ) ,
1835+ false ,
1836+ 1 ,
1837+ ) ] ) ;
1838+
1839+ let diff = SchemaDiffArgs {
1840+ before : & before,
1841+ after : & after,
1842+ }
1843+ . compute_diff ( )
1844+ . unwrap ( ) ;
1845+
1846+ assert_eq ! ( diff. added_fields. len( ) , 0 ) ;
1847+ assert_eq ! ( diff. removed_fields. len( ) , 0 ) ;
1848+ assert_eq ! ( diff. updated_fields. len( ) , 1 ) ;
1849+ assert_eq ! ( diff. updated_fields[ 0 ] . path, ColumnName :: new( [ "matrix" ] ) ) ;
1850+ assert_eq ! (
1851+ diff. updated_fields[ 0 ] . change_types,
1852+ vec![ FieldChangeType :: ContainerNullabilityTightened ]
1853+ ) ;
1854+ assert ! ( diff. has_breaking_changes( ) ) ; // Tightening is breaking
1855+ }
1856+
1857+ #[ test]
1858+ fn test_nested_array_inner_nullability_loosened ( ) {
1859+ // Test: array<array<int not null>> -> array<array<int>>
1860+ // Inner array element nullability loosened (safe change)
1861+ let before = StructType :: new_unchecked ( [ create_field_with_id (
1862+ "matrix" ,
1863+ DataType :: Array ( Box :: new ( ArrayType :: new (
1864+ DataType :: Array ( Box :: new ( ArrayType :: new ( DataType :: INTEGER , false ) ) ) , // Inner elements non-nullable
1865+ false ,
1866+ ) ) ) ,
1867+ false ,
1868+ 1 ,
1869+ ) ] ) ;
1870+
1871+ let after = StructType :: new_unchecked ( [ create_field_with_id (
1872+ "matrix" ,
1873+ DataType :: Array ( Box :: new ( ArrayType :: new (
1874+ DataType :: Array ( Box :: new ( ArrayType :: new ( DataType :: INTEGER , true ) ) ) , // Inner elements now nullable
1875+ false ,
1876+ ) ) ) ,
1877+ false ,
1878+ 1 ,
1879+ ) ] ) ;
1880+
1881+ let diff = SchemaDiffArgs {
1882+ before : & before,
1883+ after : & after,
1884+ }
1885+ . compute_diff ( )
1886+ . unwrap ( ) ;
1887+
1888+ assert_eq ! ( diff. added_fields. len( ) , 0 ) ;
1889+ assert_eq ! ( diff. removed_fields. len( ) , 0 ) ;
1890+ assert_eq ! ( diff. updated_fields. len( ) , 1 ) ;
1891+ assert_eq ! ( diff. updated_fields[ 0 ] . path, ColumnName :: new( [ "matrix" ] ) ) ;
1892+ assert_eq ! (
1893+ diff. updated_fields[ 0 ] . change_types,
1894+ vec![ FieldChangeType :: ContainerNullabilityLoosened ]
1895+ ) ;
1896+ assert ! ( !diff. has_breaking_changes( ) ) ; // Loosening is safe
1897+ }
1898+
1899+ #[ test]
1900+ fn test_nested_array_inner_nullability_tightened ( ) {
1901+ // Test: array<array<int>> -> array<array<int not null>>
1902+ // Inner array element nullability tightened (breaking change)
1903+ let before = StructType :: new_unchecked ( [ create_field_with_id (
1904+ "matrix" ,
1905+ DataType :: Array ( Box :: new ( ArrayType :: new (
1906+ DataType :: Array ( Box :: new ( ArrayType :: new ( DataType :: INTEGER , true ) ) ) , // Inner elements nullable
1907+ false ,
1908+ ) ) ) ,
1909+ false ,
1910+ 1 ,
1911+ ) ] ) ;
1912+
1913+ let after = StructType :: new_unchecked ( [ create_field_with_id (
1914+ "matrix" ,
1915+ DataType :: Array ( Box :: new ( ArrayType :: new (
1916+ DataType :: Array ( Box :: new ( ArrayType :: new ( DataType :: INTEGER , false ) ) ) , // Inner elements now non-nullable
1917+ false ,
1918+ ) ) ) ,
1919+ false ,
1920+ 1 ,
1921+ ) ] ) ;
1922+
1923+ let diff = SchemaDiffArgs {
1924+ before : & before,
1925+ after : & after,
1926+ }
1927+ . compute_diff ( )
1928+ . unwrap ( ) ;
1929+
1930+ assert_eq ! ( diff. added_fields. len( ) , 0 ) ;
1931+ assert_eq ! ( diff. removed_fields. len( ) , 0 ) ;
1932+ assert_eq ! ( diff. updated_fields. len( ) , 1 ) ;
1933+ assert_eq ! ( diff. updated_fields[ 0 ] . path, ColumnName :: new( [ "matrix" ] ) ) ;
1934+ assert_eq ! (
1935+ diff. updated_fields[ 0 ] . change_types,
1936+ vec![ FieldChangeType :: ContainerNullabilityTightened ]
1937+ ) ;
1938+ assert ! ( diff. has_breaking_changes( ) ) ; // Tightening is breaking
1939+ }
1940+
1941+ #[ test]
1942+ fn test_array_nullability_loosened ( ) {
1943+ let before = StructType :: new_unchecked ( [ create_field_with_id (
1944+ "items" ,
1945+ DataType :: Array ( Box :: new ( ArrayType :: new ( DataType :: STRING , false ) ) ) , // Non-nullable elements
1946+ false ,
1947+ 1 ,
1948+ ) ] ) ;
1949+
1950+ let after = StructType :: new_unchecked ( [ create_field_with_id (
1951+ "items" ,
1952+ DataType :: Array ( Box :: new ( ArrayType :: new ( DataType :: STRING , true ) ) ) , // Nullable elements now
1953+ false ,
1954+ 1 ,
1955+ ) ] ) ;
1956+
1957+ let diff = SchemaDiffArgs {
1958+ before : & before,
1959+ after : & after,
1960+ }
1961+ . compute_diff ( )
1962+ . unwrap ( ) ;
1963+
1964+ assert_eq ! ( diff. added_fields. len( ) , 0 ) ;
1965+ assert_eq ! ( diff. removed_fields. len( ) , 0 ) ;
1966+ assert_eq ! ( diff. updated_fields. len( ) , 1 ) ;
1967+ assert_eq ! (
1968+ diff. updated_fields[ 0 ] . change_types,
1969+ vec![ FieldChangeType :: ContainerNullabilityLoosened ]
1970+ ) ;
1971+ assert ! ( !diff. has_breaking_changes( ) ) ; // Loosening is safe
1972+ }
1973+
1974+ #[ test]
1975+ fn test_array_nullability_tightened ( ) {
1976+ let before = StructType :: new_unchecked ( [ create_field_with_id (
1977+ "items" ,
1978+ DataType :: Array ( Box :: new ( ArrayType :: new ( DataType :: STRING , true ) ) ) , // Nullable elements
1979+ false ,
1980+ 1 ,
1981+ ) ] ) ;
1982+
1983+ let after = StructType :: new_unchecked ( [ create_field_with_id (
1984+ "items" ,
1985+ DataType :: Array ( Box :: new ( ArrayType :: new ( DataType :: STRING , false ) ) ) , // Non-nullable now
1986+ false ,
1987+ 1 ,
1988+ ) ] ) ;
1989+
1990+ let diff = SchemaDiffArgs {
1991+ before : & before,
1992+ after : & after,
1993+ }
1994+ . compute_diff ( )
1995+ . unwrap ( ) ;
1996+
1997+ assert_eq ! ( diff. added_fields. len( ) , 0 ) ;
1998+ assert_eq ! ( diff. removed_fields. len( ) , 0 ) ;
1999+ assert_eq ! ( diff. updated_fields. len( ) , 1 ) ;
2000+ assert_eq ! (
2001+ diff. updated_fields[ 0 ] . change_types,
2002+ vec![ FieldChangeType :: ContainerNullabilityTightened ]
2003+ ) ;
2004+ assert ! ( diff. has_breaking_changes( ) ) ; // Tightening is breaking
17152005 }
17162006}
0 commit comments