{"record":{"id":"f1939163a1b8e7b5","repo":"phalcon/cphalcon","slug":"on-conflict-requires-at-least-one-conflict-target","errorCode":null,"errorMessage":"ON CONFLICT requires at least one conflict-target column","messagePattern":"ON CONFLICT requires at least one conflict-target column","errorType":"exception","errorClass":"ConflictTargetColumnRequired","httpStatus":null,"severity":"error","filePath":"phalcon/Db/Dialect.zep","lineNumber":549,"sourceCode":"    public function refreshMaterializedView( string viewName, string schemaName = null, bool concurrent = false) -> string\n    {\n        throw new MaterializedViewsNotSupported();\n    }\n\n    /**\n     * Appends an `ON CONFLICT (col, ...) DO UPDATE SET col = excluded.col`\n     * upsert clause to the supplied INSERT statement. The syntax is the\n     * SQL standard form recognized by PostgreSQL (9.5+) and SQLite (3.24+).\n     * MySQL overrides this method to throw because its `ON DUPLICATE KEY\n     * UPDATE` has a different shape (deferred to parser item #23).\n     */\n    public function onConflictUpdate( string sqlQuery,  array conflictColumns,  array updateColumns) -> string\n    {\n        var col;\n        array assignments;\n\n        if unlikely empty conflictColumns {\n            throw new ConflictTargetColumnRequired();\n        }\n\n        if unlikely empty updateColumns {\n            throw new ConflictUpdateColumnRequired();\n        }\n\n        let assignments = [];\n        for col in updateColumns {\n            let assignments[] = this->escape((string) col)\n                . \" = excluded.\" . this->escape((string) col);\n        }\n\n        return sqlQuery\n            . \" ON CONFLICT (\" . this->getColumnList(conflictColumns) . \")\"\n            . \" DO UPDATE SET \" . implode(\", \", assignments);\n    }\n\n    /**","sourceCodeStart":531,"sourceCodeEnd":567,"githubUrl":"https://github.com/phalcon/cphalcon/blob/b7419de9cd0a8a3f48441ead84c9f8415d463e25/phalcon/Db/Dialect.zep#L531-L567","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Pass the conflict-target column(s): typically the primary key or a unique-index column, e.g. ['id'] or ['sku'].","Ensure the target columns match a real unique index or primary key on the table, or PostgreSQL/SQLite will reject the statement.","Validate count($conflictColumns) > 0 in your upsert helper and fail with an application-level message."],"exampleFix":"// before\n$sql = $dialect->onConflictUpdate($insertSql, [], ['qty = excluded.qty']); // throws ConflictTargetColumnRequired\n\n// after\n$sql = $dialect->onConflictUpdate($insertSql, ['sku'], ['qty']);","handlingStrategy":"validation","validationCode":"if (count($conflictColumns) === 0) {\n    throw new InvalidArgumentException('ON CONFLICT requires a conflict-target column');\n}\n$sql = $dialect->onConflictUpdate($insertSql, $conflictColumns, $updateColumns);","typeGuard":"function hasConflictTarget(array $conflictColumns): bool\n{\n    return $conflictColumns !== []\n        && array_filter($conflictColumns, 'is_string') !== [];\n}","tryCatchPattern":null,"preventionTips":["Always name the conflict target explicitly - usually the primary key or a unique-index column.","Verify a matching unique index/constraint exists before generating the upsert.","Keep conflict-target and update-column lists in one config object so they cannot be swapped."],"tags":["upsert","on-conflict","dialect","postgresql","sqlite","phalcon-db"],"backgroundTag":"upsert-missing-conflict-target","analyzedSha":"b7419de9cd0a8a3f48441ead84c9f8415d463e25","analyzedAt":"2026-08-21T06:21:18.811Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}