@@ -100,87 +100,101 @@ impl TableDef {
100100 }
101101 }
102102
103- // Process inline unique and index for each column
103+ // Group columns by unique constraint name to create composite unique constraints
104+ // Use same pattern as index grouping
105+ let mut unique_groups: HashMap < String , Vec < String > > = HashMap :: new ( ) ;
106+ let mut unique_order: Vec < String > = Vec :: new ( ) ; // Preserve order of first occurrence
107+
104108 for col in & self . columns {
105- // Handle inline unique
106109 if let Some ( ref unique_val) = col. unique {
107110 match unique_val {
108111 StrOrBoolOrArray :: Str ( name) => {
109- let constraint_name = Some ( name. clone ( ) ) ;
110-
111- // Check if this unique constraint already exists
112- let exists = constraints. iter ( ) . any ( |c| {
113- if let TableConstraint :: Unique {
114- name : c_name,
115- columns,
116- } = c
117- {
118- c_name. as_ref ( ) == Some ( name)
119- && columns. len ( ) == 1
120- && columns[ 0 ] == col. name
121- } else {
122- false
123- }
124- } ) ;
112+ // Named unique constraint - group by name for composite constraints
113+ let unique_name = name. clone ( ) ;
125114
126- if !exists {
127- constraints. push ( TableConstraint :: Unique {
128- name : constraint_name,
129- columns : vec ! [ col. name. clone( ) ] ,
130- } ) ;
115+ if !unique_groups. contains_key ( & unique_name) {
116+ unique_order. push ( unique_name. clone ( ) ) ;
131117 }
118+
119+ unique_groups
120+ . entry ( unique_name)
121+ . or_default ( )
122+ . push ( col. name . clone ( ) ) ;
132123 }
133124 StrOrBoolOrArray :: Bool ( true ) => {
134- let exists = constraints. iter ( ) . any ( |c| {
135- if let TableConstraint :: Unique {
136- name : None ,
137- columns,
138- } = c
139- {
140- columns. len ( ) == 1 && columns[ 0 ] == col. name
141- } else {
142- false
143- }
144- } ) ;
125+ // Use special marker for auto-generated unique constraints (without custom name)
126+ let group_key = format ! ( "__auto_{}" , col. name) ;
145127
146- if !exists {
147- constraints. push ( TableConstraint :: Unique {
148- name : None ,
149- columns : vec ! [ col. name. clone( ) ] ,
150- } ) ;
128+ if !unique_groups. contains_key ( & group_key) {
129+ unique_order. push ( group_key. clone ( ) ) ;
151130 }
131+
132+ unique_groups
133+ . entry ( group_key)
134+ . or_default ( )
135+ . push ( col. name . clone ( ) ) ;
152136 }
153137 StrOrBoolOrArray :: Bool ( false ) => continue ,
154138 StrOrBoolOrArray :: Array ( names) => {
155139 // Array format: each element is a constraint name
156140 // This column will be part of all these named constraints
157- for constraint_name in names {
158- // Check if constraint with this name already exists
159- if let Some ( existing) = constraints. iter_mut ( ) . find ( |c| {
160- if let TableConstraint :: Unique { name : Some ( n) , .. } = c {
161- n == constraint_name
162- } else {
163- false
164- }
165- } ) {
166- // Add this column to existing composite constraint
167- if let TableConstraint :: Unique { columns, .. } = existing
168- && !columns. contains ( & col. name )
169- {
170- columns. push ( col. name . clone ( ) ) ;
171- }
172- } else {
173- // Create new constraint with this column
174- constraints. push ( TableConstraint :: Unique {
175- name : Some ( constraint_name. clone ( ) ) ,
176- columns : vec ! [ col. name. clone( ) ] ,
177- } ) ;
141+ for unique_name in names {
142+ if !unique_groups. contains_key ( unique_name. as_str ( ) ) {
143+ unique_order. push ( unique_name. clone ( ) ) ;
178144 }
145+
146+ unique_groups
147+ . entry ( unique_name. clone ( ) )
148+ . or_default ( )
149+ . push ( col. name . clone ( ) ) ;
179150 }
180151 }
181152 }
182153 }
154+ }
155+
156+ // Create unique constraints from grouped columns in order
157+ for unique_name in unique_order {
158+ let columns = unique_groups. get ( & unique_name) . unwrap ( ) . clone ( ) ;
159+
160+ // Determine if this is an auto-generated unique (from unique: true)
161+ // or a named unique (from unique: "name")
162+ let constraint_name = if unique_name. starts_with ( "__auto_" ) {
163+ // Auto-generated unique - use None so SQL generation can create the name
164+ None
165+ } else {
166+ // Named unique - preserve the custom name
167+ Some ( unique_name. clone ( ) )
168+ } ;
169+
170+ // Check if this unique constraint already exists
171+ let exists = constraints. iter ( ) . any ( |c| {
172+ if let TableConstraint :: Unique {
173+ name,
174+ columns : cols,
175+ } = c
176+ {
177+ // Match by name if both have names, otherwise match by columns
178+ match ( & constraint_name, name) {
179+ ( Some ( n1) , Some ( n2) ) => n1 == n2,
180+ ( None , None ) => cols == & columns,
181+ _ => false ,
182+ }
183+ } else {
184+ false
185+ }
186+ } ) ;
187+
188+ if !exists {
189+ constraints. push ( TableConstraint :: Unique {
190+ name : constraint_name,
191+ columns,
192+ } ) ;
193+ }
194+ }
183195
196+ // Process inline foreign_key and index for each column
197+ for col in & self . columns {
184198 // Handle inline foreign_key
185199 if let Some ( ref fk_syntax) = col. foreign_key {
186200 // Convert ForeignKeySyntax to ForeignKeyDef
@@ -539,6 +553,84 @@ mod tests {
539553 ) ) ;
540554 }
541555
556+ #[ test]
557+ fn normalize_composite_unique_from_string_name ( ) {
558+ // Test that multiple columns with the same unique constraint name
559+ // are grouped into a single composite unique constraint
560+ let mut route_col = col ( "join_route" , ColumnType :: Simple ( SimpleColumnType :: Text ) ) ;
561+ route_col. unique = Some ( StrOrBoolOrArray :: Str ( "route_provider_id" . into ( ) ) ) ;
562+
563+ let mut provider_col = col ( "provider_id" , ColumnType :: Simple ( SimpleColumnType :: Text ) ) ;
564+ provider_col. unique = Some ( StrOrBoolOrArray :: Str ( "route_provider_id" . into ( ) ) ) ;
565+
566+ let table = TableDef {
567+ name : "user" . into ( ) ,
568+ columns : vec ! [
569+ col( "id" , ColumnType :: Simple ( SimpleColumnType :: Integer ) ) ,
570+ route_col,
571+ provider_col,
572+ ] ,
573+ constraints : vec ! [ ] ,
574+ } ;
575+
576+ let normalized = table. normalize ( ) . unwrap ( ) ;
577+ assert_eq ! ( normalized. constraints. len( ) , 1 ) ;
578+ assert ! ( matches!(
579+ & normalized. constraints[ 0 ] ,
580+ TableConstraint :: Unique { name: Some ( n) , columns }
581+ if n == "route_provider_id"
582+ && columns == & [ "join_route" . to_string( ) , "provider_id" . to_string( ) ]
583+ ) ) ;
584+ }
585+
586+ #[ test]
587+ fn normalize_unique_name_mismatch_creates_both_constraints ( ) {
588+ // Test coverage for line 181: When an inline unique has a name but existing doesn't (or vice versa),
589+ // they should not match and both constraints should be created
590+ let mut email_col = col ( "email" , ColumnType :: Simple ( SimpleColumnType :: Text ) ) ;
591+ email_col. unique = Some ( StrOrBoolOrArray :: Str ( "named_unique" . into ( ) ) ) ;
592+
593+ let table = TableDef {
594+ name : "user" . into ( ) ,
595+ columns : vec ! [
596+ col( "id" , ColumnType :: Simple ( SimpleColumnType :: Integer ) ) ,
597+ email_col,
598+ ] ,
599+ constraints : vec ! [
600+ // Existing unnamed unique constraint on same column
601+ TableConstraint :: Unique {
602+ name: None ,
603+ columns: vec![ "email" . into( ) ] ,
604+ } ,
605+ ] ,
606+ } ;
607+
608+ let normalized = table. normalize ( ) . unwrap ( ) ;
609+
610+ // Should have 2 unique constraints: one named, one unnamed
611+ let unique_constraints: Vec < _ > = normalized
612+ . constraints
613+ . iter ( )
614+ . filter ( |c| matches ! ( c, TableConstraint :: Unique { .. } ) )
615+ . collect ( ) ;
616+ assert_eq ! (
617+ unique_constraints. len( ) ,
618+ 2 ,
619+ "Should keep both named and unnamed unique constraints as they don't match"
620+ ) ;
621+
622+ // Verify we have one named and one unnamed
623+ let has_named = unique_constraints. iter ( ) . any (
624+ |c| matches ! ( c, TableConstraint :: Unique { name: Some ( n) , .. } if n == "named_unique" ) ,
625+ ) ;
626+ let has_unnamed = unique_constraints
627+ . iter ( )
628+ . any ( |c| matches ! ( c, TableConstraint :: Unique { name: None , .. } ) ) ;
629+
630+ assert ! ( has_named, "Should have named unique constraint" ) ;
631+ assert ! ( has_unnamed, "Should have unnamed unique constraint" ) ;
632+ }
633+
542634 #[ test]
543635 fn normalize_inline_index_bool ( ) {
544636 let mut name_col = col ( "name" , ColumnType :: Simple ( SimpleColumnType :: Text ) ) ;
0 commit comments