@@ -328,30 +328,83 @@ fn handle_start_tag(
328328 Ok ( ParseState :: InChangeSet ( cs) )
329329 }
330330 "dropIndex" => {
331- let index_name = get_attr ( attrs, "indexName" ) . unwrap_or_default ( ) ;
331+ let Some ( index_name) = get_attr ( attrs, "indexName" ) else {
332+ eprintln ! (
333+ "Warning: <dropIndex> missing required 'indexName' attribute, skipping"
334+ ) ;
335+ return Ok ( ParseState :: InChangeSet ( cs) ) ;
336+ } ;
332337 let schema_name = get_attr ( attrs, "schemaName" ) ;
333338 let qualified = qualify_name ( & schema_name, & index_name) ;
334339 cs. sql_parts . push ( format ! ( "DROP INDEX {};" , qualified) ) ;
335340 Ok ( ParseState :: InChangeSet ( cs) )
336341 }
337342 "addForeignKeyConstraint" => {
338- let sql = generate_add_fk_sql ( attrs) ;
343+ let Some ( base_table) = get_attr ( attrs, "baseTableName" ) else {
344+ eprintln ! (
345+ "Warning: <addForeignKeyConstraint> missing required 'baseTableName' attribute, skipping"
346+ ) ;
347+ return Ok ( ParseState :: InChangeSet ( cs) ) ;
348+ } ;
349+ let Some ( ref_table) = get_attr ( attrs, "referencedTableName" ) else {
350+ eprintln ! (
351+ "Warning: <addForeignKeyConstraint> missing required 'referencedTableName' attribute, skipping"
352+ ) ;
353+ return Ok ( ParseState :: InChangeSet ( cs) ) ;
354+ } ;
355+ let Some ( base_columns) = get_attr ( attrs, "baseColumnNames" ) else {
356+ eprintln ! (
357+ "Warning: <addForeignKeyConstraint> missing required 'baseColumnNames' attribute, skipping"
358+ ) ;
359+ return Ok ( ParseState :: InChangeSet ( cs) ) ;
360+ } ;
361+ let Some ( ref_columns) = get_attr ( attrs, "referencedColumnNames" ) else {
362+ eprintln ! (
363+ "Warning: <addForeignKeyConstraint> missing required 'referencedColumnNames' attribute, skipping"
364+ ) ;
365+ return Ok ( ParseState :: InChangeSet ( cs) ) ;
366+ } ;
367+ let sql = generate_add_fk_sql (
368+ attrs,
369+ & base_table,
370+ & base_columns,
371+ & ref_table,
372+ & ref_columns,
373+ ) ;
339374 cs. sql_parts . push ( sql) ;
340375 Ok ( ParseState :: InChangeSet ( cs) )
341376 }
342377 "addPrimaryKey" => {
343- if get_attr ( attrs, "tableName" ) . is_none ( ) {
378+ let Some ( table_name ) = get_attr ( attrs, "tableName" ) else {
344379 eprintln ! (
345380 "Warning: <addPrimaryKey> missing required 'tableName' attribute, skipping"
346381 ) ;
347382 return Ok ( ParseState :: InChangeSet ( cs) ) ;
348- }
349- let sql = generate_add_pk_sql ( attrs) ;
383+ } ;
384+ let Some ( column_names) = get_attr ( attrs, "columnNames" ) else {
385+ eprintln ! (
386+ "Warning: <addPrimaryKey> missing required 'columnNames' attribute, skipping"
387+ ) ;
388+ return Ok ( ParseState :: InChangeSet ( cs) ) ;
389+ } ;
390+ let sql = generate_add_pk_sql ( attrs, & table_name, & column_names) ;
350391 cs. sql_parts . push ( sql) ;
351392 Ok ( ParseState :: InChangeSet ( cs) )
352393 }
353394 "addUniqueConstraint" => {
354- let sql = generate_add_unique_sql ( attrs) ;
395+ let Some ( table_name) = get_attr ( attrs, "tableName" ) else {
396+ eprintln ! (
397+ "Warning: <addUniqueConstraint> missing required 'tableName' attribute, skipping"
398+ ) ;
399+ return Ok ( ParseState :: InChangeSet ( cs) ) ;
400+ } ;
401+ let Some ( column_names) = get_attr ( attrs, "columnNames" ) else {
402+ eprintln ! (
403+ "Warning: <addUniqueConstraint> missing required 'columnNames' attribute, skipping"
404+ ) ;
405+ return Ok ( ParseState :: InChangeSet ( cs) ) ;
406+ } ;
407+ let sql = generate_add_unique_sql ( attrs, & table_name, & column_names) ;
355408 cs. sql_parts . push ( sql) ;
356409 Ok ( ParseState :: InChangeSet ( cs) )
357410 }
@@ -689,32 +742,51 @@ fn generate_create_index_sql(ci: &CreateIndexState) -> String {
689742}
690743
691744/// Generate an ALTER TABLE ADD CONSTRAINT ... FOREIGN KEY SQL statement.
692- fn generate_add_fk_sql ( attrs : & [ ( String , String ) ] ) -> String {
745+ ///
746+ /// Required fields (`base_table`, `base_columns`, `ref_table`, `ref_columns`)
747+ /// are passed as pre-validated parameters. Optional fields (`constraintName`,
748+ /// schema names) are extracted from `attrs`.
749+ fn generate_add_fk_sql (
750+ attrs : & [ ( String , String ) ] ,
751+ base_table : & str ,
752+ base_columns : & str ,
753+ ref_table : & str ,
754+ ref_columns : & str ,
755+ ) -> String {
693756 let constraint_name = get_attr ( attrs, "constraintName" ) . unwrap_or_default ( ) ;
694- let base_table = get_attr ( attrs, "baseTableName" ) . unwrap_or_default ( ) ;
695757 let base_schema = get_attr ( attrs, "baseTableSchemaName" ) ;
696- let base_columns = get_attr ( attrs, "baseColumnNames" ) . unwrap_or_default ( ) ;
697- let ref_table = get_attr ( attrs, "referencedTableName" ) . unwrap_or_default ( ) ;
698758 let ref_schema = get_attr ( attrs, "referencedTableSchemaName" ) ;
699- let ref_columns = get_attr ( attrs, "referencedColumnNames" ) . unwrap_or_default ( ) ;
700759
701- let base_qualified = qualify_name ( & base_schema, & base_table) ;
702- let ref_qualified = qualify_name ( & ref_schema, & ref_table) ;
760+ let base_qualified = qualify_name ( & base_schema, base_table) ;
761+ let ref_qualified = qualify_name ( & ref_schema, ref_table) ;
703762
704- format ! (
705- "ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({});" ,
706- base_qualified, constraint_name, base_columns, ref_qualified, ref_columns
707- )
763+ if constraint_name. is_empty ( ) {
764+ format ! (
765+ "ALTER TABLE {} ADD FOREIGN KEY ({}) REFERENCES {} ({});" ,
766+ base_qualified, base_columns, ref_qualified, ref_columns
767+ )
768+ } else {
769+ format ! (
770+ "ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({});" ,
771+ base_qualified, constraint_name, base_columns, ref_qualified, ref_columns
772+ )
773+ }
708774}
709775
710776/// Generate an ALTER TABLE ADD CONSTRAINT ... PRIMARY KEY SQL statement.
711- fn generate_add_pk_sql ( attrs : & [ ( String , String ) ] ) -> String {
777+ ///
778+ /// Required fields (`table_name`, `column_names`) are passed as pre-validated
779+ /// parameters. Optional fields (`constraintName`, `schemaName`) are extracted
780+ /// from `attrs`.
781+ fn generate_add_pk_sql (
782+ attrs : & [ ( String , String ) ] ,
783+ table_name : & str ,
784+ column_names : & str ,
785+ ) -> String {
712786 let constraint_name = get_attr ( attrs, "constraintName" ) . unwrap_or_default ( ) ;
713- let table_name = get_attr ( attrs, "tableName" ) . unwrap_or_default ( ) ;
714787 let schema_name = get_attr ( attrs, "schemaName" ) ;
715- let column_names = get_attr ( attrs, "columnNames" ) . unwrap_or_default ( ) ;
716788
717- let qualified = qualify_name ( & schema_name, & table_name) ;
789+ let qualified = qualify_name ( & schema_name, table_name) ;
718790
719791 if constraint_name. is_empty ( ) {
720792 format ! (
@@ -730,13 +802,19 @@ fn generate_add_pk_sql(attrs: &[(String, String)]) -> String {
730802}
731803
732804/// Generate an ALTER TABLE ADD CONSTRAINT ... UNIQUE SQL statement.
733- fn generate_add_unique_sql ( attrs : & [ ( String , String ) ] ) -> String {
805+ ///
806+ /// Required fields (`table_name`, `column_names`) are passed as pre-validated
807+ /// parameters. Optional fields (`constraintName`, `schemaName`) are extracted
808+ /// from `attrs`.
809+ fn generate_add_unique_sql (
810+ attrs : & [ ( String , String ) ] ,
811+ table_name : & str ,
812+ column_names : & str ,
813+ ) -> String {
734814 let constraint_name = get_attr ( attrs, "constraintName" ) . unwrap_or_default ( ) ;
735- let table_name = get_attr ( attrs, "tableName" ) . unwrap_or_default ( ) ;
736815 let schema_name = get_attr ( attrs, "schemaName" ) ;
737- let column_names = get_attr ( attrs, "columnNames" ) . unwrap_or_default ( ) ;
738816
739- let qualified = qualify_name ( & schema_name, & table_name) ;
817+ let qualified = qualify_name ( & schema_name, table_name) ;
740818
741819 if constraint_name. is_empty ( ) {
742820 format ! ( "ALTER TABLE {} ADD UNIQUE ({});" , qualified, column_names)
@@ -1388,6 +1466,139 @@ mod tests {
13881466 assert ! ( units[ 0 ] . run_in_transaction) ;
13891467 }
13901468
1469+ #[ test]
1470+ fn test_add_fk_without_constraint_name ( ) {
1471+ let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1472+ <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog">
1473+ <changeSet id="1" author="dev">
1474+ <addForeignKeyConstraint
1475+ baseTableName="orders"
1476+ baseColumnNames="user_id"
1477+ referencedTableName="users"
1478+ referencedColumnNames="id"/>
1479+ </changeSet>
1480+ </databaseChangeLog>"# ;
1481+
1482+ let units = parse_changelog_xml ( xml, Path :: new ( "test.xml" ) )
1483+ . expect ( "Should parse addForeignKeyConstraint without constraintName" ) ;
1484+ assert_eq ! ( units. len( ) , 1 ) ;
1485+ let sql = & units[ 0 ] . sql ;
1486+ assert_eq ! (
1487+ sql,
1488+ "ALTER TABLE orders ADD FOREIGN KEY (user_id) REFERENCES users (id);" ,
1489+ "Expected valid FK SQL without CONSTRAINT keyword, got: {}" ,
1490+ sql
1491+ ) ;
1492+ }
1493+
1494+ #[ test]
1495+ fn test_drop_index_missing_index_name ( ) {
1496+ let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1497+ <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog">
1498+ <changeSet id="1" author="dev">
1499+ <dropIndex/>
1500+ <sql>SELECT 1;</sql>
1501+ </changeSet>
1502+ </databaseChangeLog>"# ;
1503+
1504+ let units = parse_changelog_xml ( xml, Path :: new ( "test.xml" ) )
1505+ . expect ( "Should handle dropIndex without indexName" ) ;
1506+ assert_eq ! ( units. len( ) , 1 ) ;
1507+ let sql = & units[ 0 ] . sql ;
1508+ assert ! (
1509+ !sql. contains( "DROP INDEX" ) ,
1510+ "Expected no DROP INDEX for missing indexName, got: {}" ,
1511+ sql
1512+ ) ;
1513+ assert ! (
1514+ sql. contains( "SELECT 1;" ) ,
1515+ "Expected subsequent SQL to still be processed, got: {}" ,
1516+ sql
1517+ ) ;
1518+ }
1519+
1520+ #[ test]
1521+ fn test_add_fk_missing_base_table_name ( ) {
1522+ let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1523+ <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog">
1524+ <changeSet id="1" author="dev">
1525+ <addForeignKeyConstraint
1526+ constraintName="fk_test"
1527+ baseColumnNames="user_id"
1528+ referencedTableName="users"
1529+ referencedColumnNames="id"/>
1530+ <sql>SELECT 1;</sql>
1531+ </changeSet>
1532+ </databaseChangeLog>"# ;
1533+
1534+ let units = parse_changelog_xml ( xml, Path :: new ( "test.xml" ) )
1535+ . expect ( "Should handle addForeignKeyConstraint without baseTableName" ) ;
1536+ assert_eq ! ( units. len( ) , 1 ) ;
1537+ let sql = & units[ 0 ] . sql ;
1538+ assert ! (
1539+ !sql. contains( "FOREIGN KEY" ) ,
1540+ "Expected no FK SQL for missing baseTableName, got: {}" ,
1541+ sql
1542+ ) ;
1543+ assert ! (
1544+ sql. contains( "SELECT 1;" ) ,
1545+ "Expected subsequent SQL to still be processed, got: {}" ,
1546+ sql
1547+ ) ;
1548+ }
1549+
1550+ #[ test]
1551+ fn test_add_primary_key_missing_column_names ( ) {
1552+ let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1553+ <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog">
1554+ <changeSet id="1" author="dev">
1555+ <addPrimaryKey tableName="orders"/>
1556+ <sql>SELECT 1;</sql>
1557+ </changeSet>
1558+ </databaseChangeLog>"# ;
1559+
1560+ let units = parse_changelog_xml ( xml, Path :: new ( "test.xml" ) )
1561+ . expect ( "Should handle addPrimaryKey without columnNames" ) ;
1562+ assert_eq ! ( units. len( ) , 1 ) ;
1563+ let sql = & units[ 0 ] . sql ;
1564+ assert ! (
1565+ !sql. contains( "PRIMARY KEY" ) ,
1566+ "Expected no PRIMARY KEY SQL for missing columnNames, got: {}" ,
1567+ sql
1568+ ) ;
1569+ assert ! (
1570+ sql. contains( "SELECT 1;" ) ,
1571+ "Expected subsequent SQL to still be processed, got: {}" ,
1572+ sql
1573+ ) ;
1574+ }
1575+
1576+ #[ test]
1577+ fn test_add_unique_constraint_missing_table_name ( ) {
1578+ let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1579+ <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog">
1580+ <changeSet id="1" author="dev">
1581+ <addUniqueConstraint columnNames="email" constraintName="uq_test"/>
1582+ <sql>SELECT 1;</sql>
1583+ </changeSet>
1584+ </databaseChangeLog>"# ;
1585+
1586+ let units = parse_changelog_xml ( xml, Path :: new ( "test.xml" ) )
1587+ . expect ( "Should handle addUniqueConstraint without tableName" ) ;
1588+ assert_eq ! ( units. len( ) , 1 ) ;
1589+ let sql = & units[ 0 ] . sql ;
1590+ assert ! (
1591+ !sql. contains( "UNIQUE" ) ,
1592+ "Expected no UNIQUE SQL for missing tableName, got: {}" ,
1593+ sql
1594+ ) ;
1595+ assert ! (
1596+ sql. contains( "SELECT 1;" ) ,
1597+ "Expected subsequent SQL to still be processed, got: {}" ,
1598+ sql
1599+ ) ;
1600+ }
1601+
13911602 #[ test]
13921603 fn test_malformed_xml_returns_no_units ( ) {
13931604 // Streaming XML parsers (quick-xml) don't necessarily error on truncated input;
0 commit comments