Skip to content

feat: Added 'FOR UPDATE SKIP LOCKED' to SelectQuery#243

Open
bbldn wants to merge 1 commit into
cycle:2.xfrom
bbldn:feature/expose-skip-locked
Open

feat: Added 'FOR UPDATE SKIP LOCKED' to SelectQuery#243
bbldn wants to merge 1 commit into
cycle:2.xfrom
bbldn:feature/expose-skip-locked

Conversation

@bbldn
Copy link
Copy Markdown
Contributor

@bbldn bbldn commented Mar 2, 2026

  • Added 'FOR UPDATE SKIP LOCKED' and 'FOR UPDATE NOWAIT' to SelectQuery

🤔 Why?

Issues240

@bbldn bbldn requested review from a team as code owners March 2, 2026 00:35
Copy link
Copy Markdown
Member

@roxblnfk roxblnfk left a comment

Choose a reason for hiding this comment

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

I think it's time to increase min PHP version to 8.1 and use enums.

/**
 * Row-level lock strength.
 * PG-specific modes fallback to Update/Share on other drivers.
 */
enum LockMode
{
    /** Exclusive lock. Blocks UPDATE, DELETE, SELECT FOR UPDATE/SHARE. */
    case Update;

    /** Shared lock. Blocks UPDATE, DELETE, SELECT FOR UPDATE. Allows FOR SHARE. */
    case Share;

    /**
     * PG only. Like Update, but doesn't block FOR KEY SHARE.
     * Use when not modifying PK/FK columns.
     * Fallback: Update (MySQL, MSSQL), noop (SQLite).
     */
    case NoKeyUpdate;

    /**
     * PG only. Weakest lock — blocks only DELETE and PK/FK updates.
     * Fallback: Share (MySQL, MSSQL), noop (SQLite).
     */
    case KeyShare;
}

/**
 * Lock wait behavior when row is already locked.
 */
enum LockBehavior
{
    /** Default. Block until lock is released. */
    case Wait;

    /**
     * Fail immediately if row is locked.
     * PG: NOWAIT, MySQL: NOWAIT, MSSQL: SET LOCK_TIMEOUT 0.
     * SQLite: NotSupportedException.
     */
    case NoWait;

    /**
     * Skip locked rows, return only unlocked. Useful for job queues.
     * PG: SKIP LOCKED, MySQL: SKIP LOCKED (8.0+), MSSQL: READPAST hint.
     * SQLite: NotSupportedException.
     */
    case SkipLocked;
}

interface SelectQuery
{
    // ...

    /**
     * Add row-level locking clause.
     *
     * @param LockMode $mode Lock strength. SQLite: ignored, uses BEGIN IMMEDIATE.
     * @param LockBehavior $behavior Wait strategy. SQLite: only Wait supported.
     */
    public function forUpdate(
        LockMode $mode = LockMode::Update,
        LockBehavior $behavior = LockBehavior::Wait,
    ): self;
}

// Usage
$query->forUpdate(LockMode::Update, LockBehavior::SkipLocked);

Feel free to use enums in this PR, the PHP version must be bumped separately.

@bbldn bbldn requested a review from a team as a code owner March 2, 2026 23:48
@bbldn bbldn force-pushed the feature/expose-skip-locked branch 2 times, most recently from ca3dbb6 to 1cfa7f8 Compare March 2, 2026 23:58
@bbldn
Copy link
Copy Markdown
Contributor Author

bbldn commented Mar 3, 2026

Ready. Maybe we should add method lock($mode, $behavior) and deprecate forUpdate()? And rename field $forUpdate to $lock?

@roxblnfk
Copy link
Copy Markdown
Member

roxblnfk commented Mar 3, 2026

I think this API will be better:

->forUpdate(LockBehavior $behavior = LockBehavior::Wait): static
->forShare(LockBehavior $behavior = LockBehavior::Wait): static

@bbldn
Copy link
Copy Markdown
Contributor Author

bbldn commented Mar 4, 2026

How should the values LockMode::KeyShare and Lock::NoKeyUpdate be passed then?

@roxblnfk
Copy link
Copy Markdown
Member

roxblnfk commented Mar 4, 2026

KeyShare and NoKeyUpdate are PG specific features. They are used very rarely.
But we can expose it like this:

->forUpdate(LockBehavior::NoWait, noKey: true) // PG only, exception for others
->forShare(keyOnly: true) // PG only

@bbldn bbldn force-pushed the feature/expose-skip-locked branch from 1cfa7f8 to 3a6b51a Compare March 7, 2026 15:22
@bbldn
Copy link
Copy Markdown
Contributor Author

bbldn commented Mar 7, 2026

Yes, that's better. Ready.

@roxblnfk roxblnfk force-pushed the feature/expose-skip-locked branch from 3a6b51a to 30da851 Compare May 15, 2026 12:12
@roxblnfk
Copy link
Copy Markdown
Member

Now tests passed :)

Copy link
Copy Markdown

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 row-lock mode and wait behavior support to SelectQuery, including dialect-specific SQL for FOR UPDATE, FOR SHARE, NOWAIT, and SKIP LOCKED, addressing issue #240.

Changes:

  • Introduces LockMode and LockBehavior enums and updates SelectQuery lock state.
  • Adds compiler support for PostgreSQL/default, MySQL, SQL Server, and SQLite behavior.
  • Expands functional/unit tests for lock modes and behaviors across drivers.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/Query/SelectQuery.php Adds forShare() and enum-based forUpdate() state.
src/Query/Enum/LockMode.php Defines row-level lock strength enum.
src/Query/Enum/LockBehavior.php Defines lock wait behavior enum.
src/Driver/Compiler.php Compiles generic/PostgreSQL-style lock clauses.
src/Driver/CompilerCache.php Includes lock mode/behavior in select cache hashes.
src/Driver/MySQL/MySQLCompiler.php Compiles MySQL lock clauses and behaviors.
src/Driver/Postgres/Query/PostgresSelectQuery.php Adds PostgreSQL-specific no-key/key-share lock options.
src/Driver/SQLServer/SQLServerCompiler.php Compiles SQL Server table hints for lock modes/behaviors.
src/Driver/SQLite/SQLiteCompiler.php Continues ignoring unsupported lock clauses with new token shape.
tests/Database/Functional/Driver/Common/Query/SelectQueryTest.php Adds common lock mode/behavior SQL expectations.
tests/Database/Functional/Driver/Postgres/Query/SelectQueryTest.php Adds PostgreSQL-specific lock mode tests.
tests/Database/Functional/Driver/SQLServer/Query/SelectQueryTest.php Adds SQL Server-specific lock behavior tests.
tests/Database/Functional/Driver/SQLite/Query/SelectQueryTest.php Adds SQLite lock no-op expectations.
tests/Database/Unit/Query/Tokens/SelectQueryTest.php Updates token assertions for enum-based lock state.

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

break;
case LockMode::Update:
case LockMode::NoKeyUpdate:
$arguments[] = 'UPDLOCK';
namespace Cycle\Database\Tests\Functional\Driver\SQLite\Query;

// phpcs:ignore
use Cycle\Database\Query\Enum\LockMode;
$this->assertSameQuery("SELECT * FROM {table} ORDER BY {logs}->>'created_at' DESC", $select);
}

public function testSelectForUpdateLockModeUpdate(): void
);
}

public function testSelectForUpdateLockModeShare(): void
Comment on lines +216 to +217


@@ -0,0 +1,56 @@
<?php

@@ -0,0 +1,44 @@
<?php

Comment on lines +46 to +47
* Like LockMode::Update, but doesn't block PK/FK columns.
* (Use when not modifying PK/FK columns)
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