Skip to content

Fix Oracle MERGE ORA-02012 error - invalid syntax#85

Merged
PhenX merged 6 commits intomainfrom
copilot/fix-oracle-upsert-error
Dec 21, 2025
Merged

Fix Oracle MERGE ORA-02012 error - invalid syntax#85
PhenX merged 6 commits intomainfrom
copilot/fix-oracle-upsert-error

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Dec 21, 2025

  • Fix Oracle MERGE syntax - remove AS keyword, wrap ON clause in parentheses
  • Add NotSupportedException for MERGE with returning entities
  • Add MergeTestsOracle.cs test file
  • Add Skip conditions for Oracle in tests using InsertReturn/InsertReturnAsync
  • Fix ORA-00904 error: validate match columns are in source data
  • Fix ORA-38104 error: exclude match columns from update set
  • Skip InsertEntities_MultipleTimes test for Oracle (uses auto-generated Id for matching)
  • Fix Oracle bulk copy destination to use temp table name instead of target table
  • Address code review feedback - simplify variable naming

All Oracle MERGE tests pass (6 passed, 18 skipped for expected reasons).

Original prompt

The oracle provider gives this error :
ORA-02012: missing USING keyword when performing UPSERT on Oracle

When using conflict resolution like :

await dbContext.ExecuteBulkInsertAsync(entities, onConflict: new OnConflictOptions
{
Match = e => new { e.Name },
Update = (inserted, excluded) => new TestCostType
{
CostType = inserted.CostType,
Amount = inserted.Amount,
CreatedAt = inserted.CreatedAt
}
});

The oracle merge SQL command is invalid.

When you fix it, please update tests, and if the feature is really not available in Oracle, ignore the related tests, and throw a comprehensive exception in the provider.

The project https://github.com/artiomchi/FlexLabs.Upsert/ seems to be able to achieve this, like that (see https://github.com/artiomchi/FlexLabs.Upsert/blob/main/src/FlexLabs.EntityFrameworkCore.Upsert/Runners/OracleUpsertCommandRunner.cs ) :

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using FlexLabs.EntityFrameworkCore.Upsert.Internal.Expressions;

namespace FlexLabs.EntityFrameworkCore.Upsert.Runners
{
///


/// Upsert command runner for the Oracle.EntityFrameworkCore provider
///

public class OracleUpsertCommandRunner : RelationalUpsertCommandRunner
{
///
public override bool Supports(string providerName) => providerName == "Oracle.EntityFrameworkCore";
///
protected override string EscapeName([NotNull] string name) => $""{name}"";
///
protected override string? SourcePrefix => "s.";
///
protected override string? TargetPrefix => "t.";
///
protected override string Parameter(int index) => $":p{index}";
///
protected override int? MaxQueryParams => 1000;

    /// <inheritdoc />
    public override string GenerateCommand(
        string tableName,
        ICollection<ICollection<(string ColumnName, ConstantValue Value, string? DefaultSql, bool AllowInserts)>> entities,
        ICollection<(string ColumnName, bool IsNullable)> joinColumns,
        ICollection<(string ColumnName, IKnownValue Value)>? updateExpressions,
        KnownExpression? updateCondition,
        bool returnResult = false)
    {
        ArgumentNullException.ThrowIfNull(entities);

        if (returnResult)
            throw new NotImplementedException("Oracle runner does not support returning the result of the upsert operation yet");

        var result = new StringBuilder();
        result.Append(CultureInfo.InvariantCulture, $"MERGE INTO {tableName} t USING (");
        foreach (var item in entities.Select((e, ind) => new {e, ind}))
        {
            result.Append("SELECT ");
            result.Append(string.Join(", ", item.e.Select(ec => string.Join(" AS ", ExpandValue(ec.Value), EscapeName(ec.ColumnName)))));
            result.Append(" FROM dual");
            if (entities.Count > 1 && item.ind != entities.Count - 1)
            {
                result.Append(" UNION ALL ");
            }
        }
        result.Append(") s ON (");
        result.Append(string.Join(" AND ", joinColumns.Select(j => $"t.{EscapeName(j.ColumnName)} = s.{EscapeName(j.ColumnName)}")));
        result.Append(") WHEN NOT MATCHED THEN INSERT (");
        result.Append(string.Join(", ", entities.First().Where(e => e.AllowInserts).Select(e => EscapeName(e.ColumnName))));
        result.Append(") VALUES (");
        result.Append(string.Join(", ", entities.First().Where(e => e.AllowInserts).Select(e => $"s.{EscapeName(e.ColumnName)}")));
        result.Append(')');
        if (updateExpressions is not null)
        {
            result.Append(" WHEN MATCHED THEN UPDATE SET ");
            result.Append(string.Join(", ", updateExpressions.Select(e => $"t.{EscapeName(e.ColumnName)} = {ExpandValue(e.Value)}")));
            if (updateCondition is not null)
            {
                result.Append(CultureInfo.InvariantCulture, $" WHERE {ExpandExpression(updateCondition)}");
            }
        }

        return result.ToString();
    }

    /// <inheritdoc />
    protected override string ExpandExpression(KnownExpression expression, Func<string, string>? expandLeftColumn = null)
    {
        ArgumentNullException.ThrowIfNull(expression);

        switch (expression.ExpressionType)
        {
            case ExpressionType.And:
            {
                var left = ExpandValue(expression.Value1, expandLeftColumn);
                var right = ExpandValue(expression.Value2!, expandLeftColumn);
                return $"BITAND({left}, {right})";
          ...

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 2 commits December 21, 2025 10:34
…heses

Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix missing USING keyword in Oracle UPSERT Fix Oracle MERGE ORA-02012 error - invalid syntax Dec 21, 2025
Copilot AI requested a review from PhenX December 21, 2025 10:47
@PhenX
Copy link
Copy Markdown
Owner

PhenX commented Dec 21, 2025

@copilot With Oracle, the InsertEntities_MultipleTimes fails with this error :

Oracle.ManagedDataAccess.Client.OracleException : ORA-00904: "EXCLUDED"."Id": invalid identifier

…updates

Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Dec 21, 2025

@copilot With Oracle, the InsertEntities_MultipleTimes fails with this error :

Oracle.ManagedDataAccess.Client.OracleException : ORA-00904: "EXCLUDED"."Id": invalid identifier

Fixed in 0548cf5. The ORA-00904: "EXCLUDED"."Id": invalid identifier error was caused by:

  1. Match columns (like auto-generated Id) not being in the USING subquery - now throws a clear error explaining the limitation
  2. Trying to update columns used in the ON clause - now excludes match columns from the UPDATE SET clause

The fix adds validation for Oracle MERGE limitations:

  • Match columns must be present in source data
  • Match columns are excluded from the UPDATE SET (Oracle ORA-38104 constraint)
  • Test InsertEntities_MultipleTimes is now skipped for Oracle when using auto-generated Id for matching

Note: There are still some behavioral issues with MERGE (incorrect row counts) that I'm investigating.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • www.example.com
    • Triggering command: /opt/oracle/product/26ai/dbhomeFree/bin/oracle oracleFREE (DESCRIPTION=(LOCAL=YES)(ADDRESS=(PROTOCOL=beq))) (dns block)
    • Triggering command: /opt/oracle/product/26ai/dbhomeFree/bin/oracle oracleFREE (DESCRIPTION=(LOCAL=YES)(ADDRESS=(PROTOCOL=beq))) json bash /usr�� (dns block)
    • Triggering command: /opt/oracle/product/26ai/dbhomeFree/bin/oracle oracleFREE (DESCRIPTION=(LOCAL=YES)(ADDRESS=(PROTOCOL=beq))) -j es --no�� 50d48e8bcfebc148--filter (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI and others added 2 commits December 21, 2025 11:14
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
@PhenX PhenX marked this pull request as ready for review December 21, 2025 11:37
@PhenX PhenX requested a review from Copilot December 21, 2025 11:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request fixes Oracle MERGE (UPSERT) syntax issues that were causing ORA-02012 and other Oracle-specific errors. The implementation now properly handles Oracle's specific MERGE syntax requirements and adds comprehensive validation and error handling.

  • Corrected Oracle MERGE statement syntax by removing AS keywords and wrapping ON clause in parentheses
  • Added validation to ensure match columns are present in source data and excluded from UPDATE SET clause
  • Added proper test coverage with skip conditions for Oracle-specific limitations

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
MergeTestsOracle.cs Adds Oracle-specific test class inheriting from base merge tests
MergeTestsBase.cs Adds Skip conditions for Oracle in tests that use InsertReturn/InsertReturnAsync strategies or auto-generated IDs for matching
OracleDialectBuilder.cs Fixes MERGE syntax (removes AS keywords, adds parentheses to ON clause), validates match columns are in source data, excludes match columns from UPDATE SET, and adds NotSupportedException for returning entities with MERGE
OracleBulkInsertProvider.cs Fixes bulk copy destination to use the provided tableName parameter (temp table) instead of tableInfo.QuotedTableName

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@PhenX PhenX merged commit 86c0766 into main Dec 21, 2025
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants