@@ -35,41 +35,54 @@ public override string BuildMoveDataSql<T>(
3535 // Merge handling
3636 if ( onConflict is OnConflictOptions < T > onConflictTyped )
3737 {
38- IEnumerable < string > matchColumns ;
38+ // Oracle MERGE doesn't support returning entities
39+ if ( returnedColumns . Count != 0 )
40+ {
41+ throw new NotSupportedException ( "Oracle MERGE does not support returning entities. Use ExecuteBulkInsertAsync without returning results when using conflict resolution." ) ;
42+ }
43+
44+ IReadOnlyList < string > matchColumns ;
3945 if ( onConflictTyped . Match != null )
4046 {
41- matchColumns = GetColumns ( target , onConflictTyped . Match ) ;
47+ matchColumns = GetColumns ( target , onConflictTyped . Match ) . ToList ( ) ;
4248 }
4349 else if ( target . PrimaryKey . Length > 0 )
4450 {
45- matchColumns = target . PrimaryKey . Select ( x => x . QuotedColumName ) ;
51+ matchColumns = target . PrimaryKey . Select ( x => x . QuotedColumName ) . ToList ( ) ;
4652 }
4753 else
4854 {
4955 throw new InvalidOperationException ( "Table has no primary key that can be used for conflict detection." ) ;
5056 }
5157
52- q . AppendLine ( $ "MERGE INTO { target . QuotedTableName } AS { PseudoTableInserted } ") ;
58+ // Validate that all match columns are available in the source subquery
59+ var insertedColumnNames = insertedColumns . Select ( c => c . QuotedColumName ) . ToHashSet ( ) ;
60+ var missingMatchColumns = matchColumns . Where ( c => ! insertedColumnNames . Contains ( c ) ) . ToList ( ) ;
61+ if ( missingMatchColumns . Count != 0 )
62+ {
63+ throw new InvalidOperationException (
64+ $ "Oracle MERGE requires match columns to be present in the source data. " +
65+ $ "The following match columns are not available: { string . Join ( ", " , missingMatchColumns ) } . " +
66+ $ "This can happen when using auto-generated primary key columns for conflict detection. " +
67+ $ "Use the 'Match' option to specify non-generated columns for conflict detection, " +
68+ $ "or set 'CopyGeneratedColumns = true' if the generated column values are provided.") ;
69+ }
70+
71+ // Oracle MERGE syntax does NOT use AS for table aliases
72+ q . AppendLine ( $ "MERGE INTO { target . QuotedTableName } { PseudoTableInserted } ") ;
5373
5474 q . Append ( "USING (SELECT " ) ;
5575 q . AppendColumns ( insertedColumns ) ;
56- q . Append ( $ " FROM { source } ) AS { PseudoTableExcluded } (") ;
57- q . AppendColumns ( insertedColumns ) ;
58- q . AppendLine ( ")" ) ;
59-
60- q . Append ( "ON " ) ;
61- q . AppendJoin ( " AND " , matchColumns , ( b , col ) => b . Append ( $ "{ PseudoTableInserted } .{ col } = { PseudoTableExcluded } .{ col } ") ) ;
76+ // Oracle MERGE syntax does NOT use AS for subquery aliases
77+ q . Append ( $ " FROM { source } ) { PseudoTableExcluded } ") ;
6278 q . AppendLine ( ) ;
6379
64- if ( onConflictTyped . Update != null )
65- {
66- var columns = target . GetColumns ( false ) ;
67-
68- q . AppendLine ( "WHEN MATCHED THEN UPDATE SET " ) ;
69- q . AppendJoin ( ", " , GetUpdates ( context , target , columns , onConflictTyped . Update ) ) ;
70- q . AppendLine ( ) ;
71- }
80+ // Oracle requires ON clause conditions to be wrapped in parentheses
81+ q . Append ( "ON (" ) ;
82+ q . AppendJoin ( " AND " , matchColumns , ( b , col ) => b . Append ( $ "{ PseudoTableInserted } .{ col } = { PseudoTableExcluded } .{ col } ") ) ;
83+ q . AppendLine ( ")" ) ;
7284
85+ // Oracle MERGE syntax: WHEN NOT MATCHED clause for inserts, followed by WHEN MATCHED clause for updates
7386 q . Append ( "WHEN NOT MATCHED THEN INSERT (" ) ;
7487 q . AppendColumns ( insertedColumns ) ;
7588 q . AppendLine ( ")" ) ;
@@ -78,10 +91,33 @@ public override string BuildMoveDataSql<T>(
7891 q . AppendJoin ( ", " , insertedColumns , ( b , col ) => b . Append ( $ "{ PseudoTableExcluded } .{ col . QuotedColumName } ") ) ;
7992 q . AppendLine ( ")" ) ;
8093
81- if ( returnedColumns . Count != 0 )
94+ if ( onConflictTyped . Update != null )
8295 {
83- q . Append ( "OUTPUT " ) ;
84- q . AppendJoin ( ", " , returnedColumns , ( b , col ) => b . Append ( $ "{ PseudoTableInserted } .{ col . QuotedColumName } AS { col . QuotedColumName } ") ) ;
96+ q . Append ( "WHEN MATCHED " ) ;
97+
98+ if ( onConflictTyped . RawWhere != null || onConflictTyped . Where != null )
99+ {
100+ if ( onConflictTyped is { RawWhere : not null , Where : not null } )
101+ {
102+ throw new ArgumentException ( "Cannot specify both RawWhere and Where in OnConflictOptions." ) ;
103+ }
104+
105+ q . Append ( "AND " ) ;
106+ AppendConflictCondition ( q , target , context , onConflictTyped ) ;
107+ }
108+
109+ q . AppendLine ( "THEN UPDATE SET " ) ;
110+ // Oracle MERGE: columns in ON clause cannot be updated, so exclude match columns
111+ // Use insertedColumns instead of all columns because the USING subquery only contains insertedColumns
112+ var matchColumnSet = matchColumns . ToHashSet ( ) ;
113+ var updateableColumns = insertedColumns . Where ( c => ! matchColumnSet . Contains ( c . QuotedColumName ) ) . ToList ( ) ;
114+ if ( updateableColumns . Count == 0 )
115+ {
116+ throw new InvalidOperationException (
117+ "Oracle MERGE cannot update any columns because all available columns are used in the ON clause for conflict detection. " +
118+ "Specify different columns in the 'Match' option or use specific columns in the 'Update' expression." ) ;
119+ }
120+ q . AppendJoin ( ", " , GetUpdates ( context , target , updateableColumns , onConflictTyped . Update ) ) ;
85121 q . AppendLine ( ) ;
86122 }
87123 }
0 commit comments