Skip to content

Conversation

@dereuromark
Copy link
Member

@dereuromark dereuromark commented Nov 3, 2025

Resolves cakephp/phinx#2205

Follows #939 and in general helps to implement safer idempotent seeding.

  // Current way
  $this->insert('users', [['email' => '[email protected]']]);

  // New idempotent seeding
  $this->insertOrSkip('users', [['email' => '[email protected]']]);
  // Safe to re-run - duplicates skipped!

Database Support:

  • ✅ MySQL: INSERT IGNORE
  • ✅ PostgreSQL: ON CONFLICT DO NOTHING
  • ✅ SQLite: INSERT OR IGNORE
  • ⚠️ SQL Server: Throws helpful BadMethodCallException

The current implementation also allows other modes internally in the future, including update (upsert)

Out of scope: insertOrUpdate()

INSERT ... ON DUPLICATE UPDATE - Complex

  • ✅ MySQL: Native support
  • ⚠️ PostgreSQL: Need to specify which column(s) cause the conflict
  • ⚠️ SQLite: Need to specify conflict column(s), requires 3.24.0+
  • ⚠️ SQL Server: Very complex MERGE syntax

Database Support Matrix

Feature MySQL/MariaDB PostgreSQL SQLite SQL Server
INSERT IGNORE ✅ Native ✅ Via ON CONFLICT DO NOTHING ✅ Via INSERT OR IGNORE ❌ No native support
INSERT ... ON DUPLICATE KEY UPDATE ✅ Native ✅ Via ON CONFLICT ... DO UPDATE ❌ Limited ⚠️ Via MERGE

We can still add insertOrUpdate() later if needed

  • Start with single-row upserts
  • Require explicit conflict columns for now (avoid auto-detection complexity)
  • Consider it a "power user" feature for data sync scenarios

Example future use:

// Seed with updates - sync from external API
$this->insertOrUpdate('currencies', [
    ['code' => 'USD', 'rate' => 1.0000, 'updated' => '2025-11-04'],
    ['code' => 'EUR', 'rate' => 0.9234, 'updated' => '2025-11-04'],
], ['rate', 'updated'], ['code']);  // Update rate+updated if code exists

Copy link

@yurii-zadryhun yurii-zadryhun left a comment

Choose a reason for hiding this comment

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

Great job! 👍🏻
I would also encapsulate some code, but that's out of the scope.

@dereuromark dereuromark requested a review from Copilot November 4, 2025 16:49
Copy link

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 PR adds support for an insertOrSkip() method that enables idempotent inserts by skipping rows that would cause duplicate key conflicts. The implementation uses database-specific syntax (INSERT IGNORE for MySQL, ON CONFLICT DO NOTHING for PostgreSQL, INSERT OR IGNORE for SQLite) while throwing an exception for SQL Server where this feature is not natively supported.

Key changes:

  • Added InsertMode enum with INSERT and IGNORE cases to control insert behavior
  • Implemented insertOrSkip() method in Table, SeedInterface, and BaseSeed classes
  • Modified adapter interfaces and implementations to support the new insert mode parameter
  • Added comprehensive test coverage for all database adapters

Reviewed Changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/Db/InsertMode.php New enum defining insert modes (standard INSERT vs INSERT IGNORE)
src/Db/Table.php Added insertOrSkip() method and insert mode property to support skip-on-duplicate behavior
src/SeedInterface.php Added insertOrSkip() interface method for seeder classes
src/BaseSeed.php Implemented insertOrSkip() method in base seeder class
src/Db/Adapter/AdapterInterface.php Updated insert methods to accept optional InsertMode parameter
src/Db/Adapter/AbstractAdapter.php Added getInsertPrefix() method to generate INSERT IGNORE syntax and updated insert methods
src/Db/Adapter/MysqlAdapter.php Updated docblock type annotations for consistency
src/Db/Adapter/PostgresAdapter.php Implemented PostgreSQL-specific ON CONFLICT DO NOTHING syntax via getConflictClause()
src/Db/Adapter/SqliteAdapter.php Implemented SQLite-specific INSERT OR IGNORE syntax via getInsertPrefix()
src/Db/Adapter/SqlserverAdapter.php Added exception throwing for unsupported INSERT IGNORE operation
src/Db/Adapter/AdapterWrapper.php Updated wrapper methods to pass through InsertMode parameter
src/Db/Adapter/TimedOutputAdapter.php Updated timed output methods to pass through InsertMode parameter
src/View/Helper/MigrationHelper.php Enhanced docblock type annotation for better type safety
tests/TestCase/Db/Adapter/MysqlAdapterTest.php Added three test cases covering duplicate handling, bulk inserts, and normal inserts
tests/TestCase/Db/Adapter/PostgresAdapterTest.php Added three test cases covering duplicate handling, bulk inserts, and normal inserts
tests/TestCase/Db/Adapter/SqliteAdapterTest.php Added three test cases covering duplicate handling, bulk inserts, and normal inserts
tests/TestCase/Db/Adapter/SqlserverAdapterTest.php Added test case verifying exception is thrown for unsupported operation

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

@dereuromark dereuromark mentioned this pull request Nov 8, 2025
@markstory markstory merged commit afc8b26 into 5.x Nov 8, 2025
13 checks passed
@markstory markstory deleted the 5.x-insert-or-skip branch November 8, 2025 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants