@@ -262,7 +262,10 @@ fn test_pgm004_finding_details() {
262262fn test_pgm002_finding_details ( ) {
263263 // V003 drops idx_customers_email WITHOUT CONCURRENTLY.
264264 // V001 is replayed as baseline (creates the index), V002 and V003 are changed.
265- let findings = lint_fixture ( "all-rules" , & [ "V002__violations.sql" , "V003__more_violations.sql" ] ) ;
265+ let findings = lint_fixture (
266+ "all-rules" ,
267+ & [ "V002__violations.sql" , "V003__more_violations.sql" ] ,
268+ ) ;
266269 let pgm002: Vec < & Finding > = findings. iter ( ) . filter ( |f| f. rule_id == "PGM002" ) . collect ( ) ;
267270
268271 assert_eq ! ( pgm002. len( ) , 1 , "Expected exactly 1 PGM002 finding" ) ;
@@ -1826,3 +1829,90 @@ fn test_sarif_and_sonarqube_finding_counts_match() {
18261829 findings. len( )
18271830 ) ;
18281831}
1832+
1833+ // ===========================================================================
1834+ // Cross-file FK detection (PGM003) with fk-with-later-index fixture
1835+ // ===========================================================================
1836+
1837+ #[ test]
1838+ fn test_fk_without_index_cross_file_only_fk_changed ( ) {
1839+ // Only V002 is changed. V001 is replayed as history (creates tables).
1840+ // V002 adds FK on orders.customer_id but V003 (which adds the covering
1841+ // index) has NOT been replayed yet. PGM003 should fire because
1842+ // catalog_after has no covering index at this point.
1843+ let findings = lint_fixture ( "fk-with-later-index" , & [ "V002__add_fk.sql" ] ) ;
1844+ let pgm003: Vec < & Finding > = findings. iter ( ) . filter ( |f| f. rule_id == "PGM003" ) . collect ( ) ;
1845+
1846+ assert_eq ! (
1847+ pgm003. len( ) ,
1848+ 1 ,
1849+ "Expected exactly 1 PGM003 finding for FK without index. Got:\n {}" ,
1850+ format_findings( & findings)
1851+ ) ;
1852+ assert ! (
1853+ pgm003[ 0 ] . message. contains( "customer_id" ) ,
1854+ "PGM003 message should mention 'customer_id'. Got: {}" ,
1855+ pgm003[ 0 ] . message
1856+ ) ;
1857+ }
1858+
1859+ #[ test]
1860+ fn test_fk_with_later_index_only_index_changed ( ) {
1861+ // Only V003 is changed. V001 and V002 are replayed as history.
1862+ // The FK from V002 already exists in catalog_before, and V003 adds
1863+ // the covering index. Since V002 is not being linted, no PGM003
1864+ // should fire -- the FK was in a prior file, not in the current lint set.
1865+ let findings = lint_fixture ( "fk-with-later-index" , & [ "V003__add_index.sql" ] ) ;
1866+ let pgm003: Vec < & Finding > = findings. iter ( ) . filter ( |f| f. rule_id == "PGM003" ) . collect ( ) ;
1867+
1868+ assert ! (
1869+ pgm003. is_empty( ) ,
1870+ "PGM003 should NOT fire when only the index file is linted. Got:\n {}" ,
1871+ format_findings( & findings)
1872+ ) ;
1873+ }
1874+
1875+ #[ test]
1876+ fn test_fk_cross_file_both_changed ( ) {
1877+ // Both V002 and V003 are changed. V001 is replayed as history.
1878+ // When linting V002: FK is added but no covering index yet -> PGM003 fires.
1879+ // When linting V003: index is added, no new FK in this file -> no PGM003.
1880+ let findings = lint_fixture (
1881+ "fk-with-later-index" ,
1882+ & [ "V002__add_fk.sql" , "V003__add_index.sql" ] ,
1883+ ) ;
1884+ let pgm003: Vec < & Finding > = findings. iter ( ) . filter ( |f| f. rule_id == "PGM003" ) . collect ( ) ;
1885+
1886+ assert_eq ! (
1887+ pgm003. len( ) ,
1888+ 1 ,
1889+ "Expected exactly 1 PGM003 finding (from V002 only). Got:\n {}" ,
1890+ format_findings( & findings)
1891+ ) ;
1892+ assert ! (
1893+ pgm003[ 0 ] . message. contains( "customer_id" ) ,
1894+ "PGM003 message should mention 'customer_id'. Got: {}" ,
1895+ pgm003[ 0 ] . message
1896+ ) ;
1897+ }
1898+
1899+ #[ test]
1900+ fn test_fk_cross_file_all_changed ( ) {
1901+ // All files are changed (empty changed set). V001 creates tables (no FK,
1902+ // no finding). V002 adds FK without covering index -> PGM003 fires.
1903+ // V003 adds the covering index -> no additional PGM003.
1904+ let findings = lint_fixture ( "fk-with-later-index" , & [ ] ) ;
1905+ let pgm003: Vec < & Finding > = findings. iter ( ) . filter ( |f| f. rule_id == "PGM003" ) . collect ( ) ;
1906+
1907+ assert_eq ! (
1908+ pgm003. len( ) ,
1909+ 1 ,
1910+ "Expected exactly 1 PGM003 finding (from V002). Got:\n {}" ,
1911+ format_findings( & findings)
1912+ ) ;
1913+ assert ! (
1914+ pgm003[ 0 ] . message. contains( "customer_id" ) ,
1915+ "PGM003 message should mention 'customer_id'. Got: {}" ,
1916+ pgm003[ 0 ] . message
1917+ ) ;
1918+ }
0 commit comments