@@ -33,8 +33,10 @@ class BaseBuilder
3333{
3434 use ConditionalTrait;
3535
36- protected const SELECT_LOCK_FOR_UPDATE = 'forUpdate ' ;
37- protected const SELECT_LOCK_SHARED = 'shared ' ;
36+ protected const SELECT_LOCK_FOR_UPDATE = 'forUpdate ' ;
37+ protected const SELECT_LOCK_SHARED = 'shared ' ;
38+ protected const SELECT_LOCK_WAIT_NOWAIT = 'nowait ' ;
39+ protected const SELECT_LOCK_WAIT_SKIP_LOCKED = 'skipLocked ' ;
3840
3941 /**
4042 * Reset DELETE data flag
@@ -119,6 +121,11 @@ class BaseBuilder
119121 */
120122 protected ?string $ QBSelectLock = null ;
121123
124+ /**
125+ * QB SELECT lock wait behavior
126+ */
127+ protected ?string $ QBSelectLockWait = null ;
128+
122129 /**
123130 * QB SELECT aggregate helper flag
124131 */
@@ -2012,6 +2019,26 @@ public function sharedLock(): static
20122019 return $ this ;
20132020 }
20142021
2022+ /**
2023+ * Fails immediately when selected rows cannot be locked.
2024+ */
2025+ public function nowait (): static
2026+ {
2027+ $ this ->QBSelectLockWait = self ::SELECT_LOCK_WAIT_NOWAIT ;
2028+
2029+ return $ this ;
2030+ }
2031+
2032+ /**
2033+ * Skips selected rows that cannot be locked immediately.
2034+ */
2035+ public function skipLocked (): static
2036+ {
2037+ $ this ->QBSelectLockWait = self ::SELECT_LOCK_WAIT_SKIP_LOCKED ;
2038+
2039+ return $ this ;
2040+ }
2041+
20152042 /**
20162043 * Sets the OFFSET value
20172044 *
@@ -2275,18 +2302,22 @@ protected function doExists(bool $reset = true)
22752302 */
22762303 protected function compileExists (): string
22772304 {
2305+ $ this ->assertSelectLockWaitHasLock ();
2306+
22782307 // ORDER BY and SELECT locks are unnecessary for checking row existence,
22792308 // and can produce invalid or surprising SQL on some drivers.
2280- $ orderBy = $ this ->QBOrderBy ;
2281- $ limit = $ this ->QBLimit ;
2282- $ offset = $ this ->QBOffset ;
2283- $ selectLock = $ this ->QBSelectLock ;
2284- $ select = $ this ->QBSelect ;
2285- $ noEscape = $ this ->QBNoEscape ;
2286- $ needsSubquery = $ this ->QBSelectUsesAggregate || $ this ->QBUnion !== [] || $ this ->QBGroupBy !== [] || $ this ->QBHaving !== [] || $ this ->QBOffset !== false ;
2287-
2288- $ this ->QBOrderBy = null ;
2289- $ this ->QBSelectLock = null ;
2309+ $ orderBy = $ this ->QBOrderBy ;
2310+ $ limit = $ this ->QBLimit ;
2311+ $ offset = $ this ->QBOffset ;
2312+ $ selectLock = $ this ->QBSelectLock ;
2313+ $ selectLockWait = $ this ->QBSelectLockWait ;
2314+ $ select = $ this ->QBSelect ;
2315+ $ noEscape = $ this ->QBNoEscape ;
2316+ $ needsSubquery = $ this ->QBSelectUsesAggregate || $ this ->QBUnion !== [] || $ this ->QBGroupBy !== [] || $ this ->QBHaving !== [] || $ this ->QBOffset !== false ;
2317+
2318+ $ this ->QBOrderBy = null ;
2319+ $ this ->QBSelectLock = null ;
2320+ $ this ->QBSelectLockWait = null ;
22902321
22912322 if (! $ needsSubquery && $ this ->QBLimit !== 0 ) {
22922323 $ this ->QBLimit = 1 ;
@@ -2304,12 +2335,13 @@ protected function compileExists(): string
23042335
23052336 return $ this ->compileSelect ('SELECT 1 ' );
23062337 } finally {
2307- $ this ->QBOrderBy = $ orderBy ;
2308- $ this ->QBLimit = $ limit ;
2309- $ this ->QBOffset = $ offset ;
2310- $ this ->QBSelectLock = $ selectLock ;
2311- $ this ->QBSelect = $ select ;
2312- $ this ->QBNoEscape = $ noEscape ;
2338+ $ this ->QBOrderBy = $ orderBy ;
2339+ $ this ->QBLimit = $ limit ;
2340+ $ this ->QBOffset = $ offset ;
2341+ $ this ->QBSelectLock = $ selectLock ;
2342+ $ this ->QBSelectLockWait = $ selectLockWait ;
2343+ $ this ->QBSelect = $ select ;
2344+ $ this ->QBNoEscape = $ noEscape ;
23132345 }
23142346 }
23152347
@@ -2321,6 +2353,8 @@ protected function compileExists(): string
23212353 */
23222354 public function countAllResults (bool $ reset = true )
23232355 {
2356+ $ this ->assertSelectLockWaitHasLock ();
2357+
23242358 // ORDER BY usage is often problematic here (most notably
23252359 // on Microsoft SQL Server) and ultimately unnecessary
23262360 // for selecting COUNT(*) ...
@@ -2333,11 +2367,13 @@ public function countAllResults(bool $reset = true)
23332367 }
23342368
23352369 // We cannot use a LIMIT when getting the single row COUNT(*) result
2336- $ limit = $ this ->QBLimit ;
2337- $ selectLock = $ this ->QBSelectLock ;
2370+ $ limit = $ this ->QBLimit ;
2371+ $ selectLock = $ this ->QBSelectLock ;
2372+ $ selectLockWait = $ this ->QBSelectLockWait ;
23382373
2339- $ this ->QBLimit = false ;
2340- $ this ->QBSelectLock = null ;
2374+ $ this ->QBLimit = false ;
2375+ $ this ->QBSelectLock = null ;
2376+ $ this ->QBSelectLockWait = null ;
23412377
23422378 try {
23432379 if ($ this ->QBDistinct === true || ! empty ($ this ->QBGroupBy )) {
@@ -2352,7 +2388,8 @@ public function countAllResults(bool $reset = true)
23522388 $ sql = $ this ->compileSelect ($ this ->countString . $ this ->db ->protectIdentifiers ('numrows ' ));
23532389 }
23542390 } finally {
2355- $ this ->QBSelectLock = $ selectLock ;
2391+ $ this ->QBSelectLock = $ selectLock ;
2392+ $ this ->QBSelectLockWait = $ selectLockWait ;
23562393 }
23572394
23582395 if ($ this ->testMode ) {
@@ -3778,6 +3815,8 @@ protected function compileSelect($selectOverride = false): string
37783815 */
37793816 protected function compileSelectLock (): string
37803817 {
3818+ $ this ->assertSelectLockWaitHasLock ();
3819+
37813820 if ($ this ->QBSelectLock === null ) {
37823821 return '' ;
37833822 }
@@ -3793,9 +3832,37 @@ protected function compileSelectLock(): string
37933832 self ::SELECT_LOCK_FOR_UPDATE => "\nFOR UPDATE " ,
37943833 self ::SELECT_LOCK_SHARED => "\nFOR SHARE " ,
37953834 default => throw new DatabaseException ('Query Builder has an invalid SELECT lock mode. ' ),
3835+ } . $ this ->compileSelectLockWait ();
3836+ }
3837+
3838+ /**
3839+ * Compile the SELECT lock wait behavior.
3840+ */
3841+ protected function compileSelectLockWait (): string
3842+ {
3843+ return match ($ this ->QBSelectLockWait ) {
3844+ self ::SELECT_LOCK_WAIT_NOWAIT => ' NOWAIT ' ,
3845+ self ::SELECT_LOCK_WAIT_SKIP_LOCKED => ' SKIP LOCKED ' ,
3846+ null => '' ,
3847+ default => throw new DatabaseException ('Query Builder has an invalid SELECT lock wait behavior. ' ),
37963848 };
37973849 }
37983850
3851+ /**
3852+ * Ensures SELECT lock wait behavior has a pessimistic lock to modify.
3853+ */
3854+ protected function assertSelectLockWaitHasLock (): void
3855+ {
3856+ if ($ this ->QBSelectLock !== null || $ this ->QBSelectLockWait === null ) {
3857+ return ;
3858+ }
3859+
3860+ throw new DatabaseException (sprintf (
3861+ 'Query Builder does not support %s() without lockForUpdate() or sharedLock(). ' ,
3862+ $ this ->selectLockWaitMethod (),
3863+ ));
3864+ }
3865+
37993866 /**
38003867 * Returns the public method name for the current SELECT lock mode.
38013868 */
@@ -3808,6 +3875,18 @@ protected function selectLockMethod(): string
38083875 };
38093876 }
38103877
3878+ /**
3879+ * Returns the public method name for the current SELECT lock wait behavior.
3880+ */
3881+ protected function selectLockWaitMethod (): string
3882+ {
3883+ return match ($ this ->QBSelectLockWait ) {
3884+ self ::SELECT_LOCK_WAIT_NOWAIT => 'nowait ' ,
3885+ self ::SELECT_LOCK_WAIT_SKIP_LOCKED => 'skipLocked ' ,
3886+ default => 'selectLockWait ' ,
3887+ };
3888+ }
3889+
38113890 /**
38123891 * Checks if the ignore option is supported by
38133892 * the Database Driver for the specific statement.
@@ -4152,6 +4231,7 @@ protected function resetSelect()
41524231 'QBLimit ' => false ,
41534232 'QBOffset ' => false ,
41544233 'QBSelectLock ' => null ,
4234+ 'QBSelectLockWait ' => null ,
41554235 'QBSelectUsesAggregate ' => false ,
41564236 'QBUnion ' => [],
41574237 ]);
0 commit comments