@@ -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 */
@@ -2020,6 +2027,26 @@ public function sharedLock(): static
20202027 return $ this ;
20212028 }
20222029
2030+ /**
2031+ * Fails immediately when selected rows cannot be locked.
2032+ */
2033+ public function nowait (): static
2034+ {
2035+ $ this ->QBSelectLockWait = self ::SELECT_LOCK_WAIT_NOWAIT ;
2036+
2037+ return $ this ;
2038+ }
2039+
2040+ /**
2041+ * Skips selected rows that cannot be locked immediately.
2042+ */
2043+ public function skipLocked (): static
2044+ {
2045+ $ this ->QBSelectLockWait = self ::SELECT_LOCK_WAIT_SKIP_LOCKED ;
2046+
2047+ return $ this ;
2048+ }
2049+
20232050 /**
20242051 * Sets the OFFSET value
20252052 *
@@ -2283,18 +2310,22 @@ protected function doExists(bool $reset = true)
22832310 */
22842311 protected function compileExists (): string
22852312 {
2313+ $ this ->assertSelectLockWaitHasLock ();
2314+
22862315 // ORDER BY and SELECT locks are unnecessary for checking row existence,
22872316 // and can produce invalid or surprising SQL on some drivers.
2288- $ orderBy = $ this ->QBOrderBy ;
2289- $ limit = $ this ->QBLimit ;
2290- $ offset = $ this ->QBOffset ;
2291- $ selectLock = $ this ->QBSelectLock ;
2292- $ select = $ this ->QBSelect ;
2293- $ noEscape = $ this ->QBNoEscape ;
2294- $ needsSubquery = $ this ->QBSelectUsesAggregate || $ this ->QBUnion !== [] || $ this ->QBGroupBy !== [] || $ this ->QBHaving !== [] || $ this ->QBOffset !== false ;
2295-
2296- $ this ->QBOrderBy = null ;
2297- $ this ->QBSelectLock = null ;
2317+ $ orderBy = $ this ->QBOrderBy ;
2318+ $ limit = $ this ->QBLimit ;
2319+ $ offset = $ this ->QBOffset ;
2320+ $ selectLock = $ this ->QBSelectLock ;
2321+ $ selectLockWait = $ this ->QBSelectLockWait ;
2322+ $ select = $ this ->QBSelect ;
2323+ $ noEscape = $ this ->QBNoEscape ;
2324+ $ needsSubquery = $ this ->QBSelectUsesAggregate || $ this ->QBUnion !== [] || $ this ->QBGroupBy !== [] || $ this ->QBHaving !== [] || $ this ->QBOffset !== false ;
2325+
2326+ $ this ->QBOrderBy = null ;
2327+ $ this ->QBSelectLock = null ;
2328+ $ this ->QBSelectLockWait = null ;
22982329
22992330 if (! $ needsSubquery && $ this ->QBLimit !== 0 ) {
23002331 $ this ->QBLimit = 1 ;
@@ -2312,12 +2343,13 @@ protected function compileExists(): string
23122343
23132344 return $ this ->compileSelect ('SELECT 1 ' );
23142345 } finally {
2315- $ this ->QBOrderBy = $ orderBy ;
2316- $ this ->QBLimit = $ limit ;
2317- $ this ->QBOffset = $ offset ;
2318- $ this ->QBSelectLock = $ selectLock ;
2319- $ this ->QBSelect = $ select ;
2320- $ this ->QBNoEscape = $ noEscape ;
2346+ $ this ->QBOrderBy = $ orderBy ;
2347+ $ this ->QBLimit = $ limit ;
2348+ $ this ->QBOffset = $ offset ;
2349+ $ this ->QBSelectLock = $ selectLock ;
2350+ $ this ->QBSelectLockWait = $ selectLockWait ;
2351+ $ this ->QBSelect = $ select ;
2352+ $ this ->QBNoEscape = $ noEscape ;
23212353 }
23222354 }
23232355
@@ -2329,6 +2361,8 @@ protected function compileExists(): string
23292361 */
23302362 public function countAllResults (bool $ reset = true )
23312363 {
2364+ $ this ->assertSelectLockWaitHasLock ();
2365+
23322366 // ORDER BY usage is often problematic here (most notably
23332367 // on Microsoft SQL Server) and ultimately unnecessary
23342368 // for selecting COUNT(*) ...
@@ -2341,11 +2375,13 @@ public function countAllResults(bool $reset = true)
23412375 }
23422376
23432377 // We cannot use a LIMIT when getting the single row COUNT(*) result
2344- $ limit = $ this ->QBLimit ;
2345- $ selectLock = $ this ->QBSelectLock ;
2378+ $ limit = $ this ->QBLimit ;
2379+ $ selectLock = $ this ->QBSelectLock ;
2380+ $ selectLockWait = $ this ->QBSelectLockWait ;
23462381
2347- $ this ->QBLimit = false ;
2348- $ this ->QBSelectLock = null ;
2382+ $ this ->QBLimit = false ;
2383+ $ this ->QBSelectLock = null ;
2384+ $ this ->QBSelectLockWait = null ;
23492385
23502386 try {
23512387 if ($ this ->QBDistinct === true || ! empty ($ this ->QBGroupBy )) {
@@ -2360,7 +2396,8 @@ public function countAllResults(bool $reset = true)
23602396 $ sql = $ this ->compileSelect ($ this ->countString . $ this ->db ->protectIdentifiers ('numrows ' ));
23612397 }
23622398 } finally {
2363- $ this ->QBSelectLock = $ selectLock ;
2399+ $ this ->QBSelectLock = $ selectLock ;
2400+ $ this ->QBSelectLockWait = $ selectLockWait ;
23642401 }
23652402
23662403 if ($ this ->testMode ) {
@@ -3786,6 +3823,8 @@ protected function compileSelect($selectOverride = false): string
37863823 */
37873824 protected function compileSelectLock (): string
37883825 {
3826+ $ this ->assertSelectLockWaitHasLock ();
3827+
37893828 if ($ this ->QBSelectLock === null ) {
37903829 return '' ;
37913830 }
@@ -3801,9 +3840,37 @@ protected function compileSelectLock(): string
38013840 self ::SELECT_LOCK_FOR_UPDATE => "\nFOR UPDATE " ,
38023841 self ::SELECT_LOCK_SHARED => "\nFOR SHARE " ,
38033842 default => throw new DatabaseException ('Query Builder has an invalid SELECT lock mode. ' ),
3843+ } . $ this ->compileSelectLockWait ();
3844+ }
3845+
3846+ /**
3847+ * Compile the SELECT lock wait behavior.
3848+ */
3849+ protected function compileSelectLockWait (): string
3850+ {
3851+ return match ($ this ->QBSelectLockWait ) {
3852+ self ::SELECT_LOCK_WAIT_NOWAIT => ' NOWAIT ' ,
3853+ self ::SELECT_LOCK_WAIT_SKIP_LOCKED => ' SKIP LOCKED ' ,
3854+ null => '' ,
3855+ default => throw new DatabaseException ('Query Builder has an invalid SELECT lock wait behavior. ' ),
38043856 };
38053857 }
38063858
3859+ /**
3860+ * Ensures SELECT lock wait behavior has a pessimistic lock to modify.
3861+ */
3862+ protected function assertSelectLockWaitHasLock (): void
3863+ {
3864+ if ($ this ->QBSelectLock !== null || $ this ->QBSelectLockWait === null ) {
3865+ return ;
3866+ }
3867+
3868+ throw new DatabaseException (sprintf (
3869+ 'Query Builder does not support %s() without lockForUpdate() or sharedLock(). ' ,
3870+ $ this ->selectLockWaitMethod (),
3871+ ));
3872+ }
3873+
38073874 /**
38083875 * Returns the public method name for the current SELECT lock mode.
38093876 */
@@ -3816,6 +3883,18 @@ protected function selectLockMethod(): string
38163883 };
38173884 }
38183885
3886+ /**
3887+ * Returns the public method name for the current SELECT lock wait behavior.
3888+ */
3889+ protected function selectLockWaitMethod (): string
3890+ {
3891+ return match ($ this ->QBSelectLockWait ) {
3892+ self ::SELECT_LOCK_WAIT_NOWAIT => 'nowait ' ,
3893+ self ::SELECT_LOCK_WAIT_SKIP_LOCKED => 'skipLocked ' ,
3894+ default => 'selectLockWait ' ,
3895+ };
3896+ }
3897+
38193898 /**
38203899 * Checks if the ignore option is supported by
38213900 * the Database Driver for the specific statement.
@@ -4160,6 +4239,7 @@ protected function resetSelect()
41604239 'QBLimit ' => false ,
41614240 'QBOffset ' => false ,
41624241 'QBSelectLock ' => null ,
4242+ 'QBSelectLockWait ' => null ,
41634243 'QBSelectUsesAggregate ' => false ,
41644244 'QBUnion ' => [],
41654245 ]);
0 commit comments