phalcon/cphalcon · error · ConflictTargetColumnRequired

ON CONFLICT requires at least one conflict-target column

Error message

ON CONFLICT requires at least one conflict-target column

What it means

Dialect::onConflictUpdate() appends an `ON CONFLICT (col, ...) DO UPDATE SET ...` upsert clause to an INSERT statement. The conflict target - the column list after ON CONFLICT - is what the database matches against a unique index/constraint, so an empty conflictColumns array throws ConflictTargetColumnRequired before any SQL is built.

Source

Thrown at phalcon/Db/Dialect.zep:549

    public function refreshMaterializedView( string viewName, string schemaName = null, bool concurrent = false) -> string
    {
        throw new MaterializedViewsNotSupported();
    }

    /**
     * Appends an `ON CONFLICT (col, ...) DO UPDATE SET col = excluded.col`
     * upsert clause to the supplied INSERT statement. The syntax is the
     * SQL standard form recognized by PostgreSQL (9.5+) and SQLite (3.24+).
     * MySQL overrides this method to throw because its `ON DUPLICATE KEY
     * UPDATE` has a different shape (deferred to parser item #23).
     */
    public function onConflictUpdate( string sqlQuery,  array conflictColumns,  array updateColumns) -> string
    {
        var col;
        array assignments;

        if unlikely empty conflictColumns {
            throw new ConflictTargetColumnRequired();
        }

        if unlikely empty updateColumns {
            throw new ConflictUpdateColumnRequired();
        }

        let assignments = [];
        for col in updateColumns {
            let assignments[] = this->escape((string) col)
                . " = excluded." . this->escape((string) col);
        }

        return sqlQuery
            . " ON CONFLICT (" . this->getColumnList(conflictColumns) . ")"
            . " DO UPDATE SET " . implode(", ", assignments);
    }

    /**

View on GitHub (pinned to b7419de9cd)

Solutions

  1. Pass the conflict-target column(s): typically the primary key or a unique-index column, e.g. ['id'] or ['sku'].
  2. Ensure the target columns match a real unique index or primary key on the table, or PostgreSQL/SQLite will reject the statement.
  3. Validate count($conflictColumns) > 0 in your upsert helper and fail with an application-level message.

Example fix

// before
$sql = $dialect->onConflictUpdate($insertSql, [], ['qty = excluded.qty']); // throws ConflictTargetColumnRequired

// after
$sql = $dialect->onConflictUpdate($insertSql, ['sku'], ['qty']);
Defensive patterns

Strategy: validation

Validate before calling

if (count($conflictColumns) === 0) {
    throw new InvalidArgumentException('ON CONFLICT requires a conflict-target column');
}
$sql = $dialect->onConflictUpdate($insertSql, $conflictColumns, $updateColumns);

Type guard

function hasConflictTarget(array $conflictColumns): bool
{
    return $conflictColumns !== []
        && array_filter($conflictColumns, 'is_string') !== [];
}

Prevention

When it happens

Trigger: Calling $dialect->onConflictUpdate($insertSql, [], ['qty']) with an empty conflict-target array; building conflict columns dynamically from a unique-index lookup that returned nothing; passing the update columns in the wrong argument position.

Common situations: Upsert helpers where the conflict key list comes from config that can be empty; forgetting that the target must reference an existing UNIQUE index/PK; argument-order mixups between conflictColumns and updateColumns.

Related errors


AI-assisted analysis of phalcon/cphalcon@b7419de9cd (2026-08-21). Data as JSON: /api/errors/f1939163a1b8e7b5. Report an issue: GitHub.