Skip to content

Commit 578cb6d

Browse files
authored
Merge pull request #3831 from katzyn/for_update
Add Oracle-style NOWAIT, WAIT n, and SKIP LOCKED to FOR UPDATE clause
2 parents e9807ac + 484eec4 commit 578cb6d

23 files changed

+360
-73
lines changed

h2/src/docsrc/html/changelog.html

+5-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ <h1>Change Log</h1>
2121

2222
<h2>Next Version (unreleased)</h2>
2323
<ul>
24-
<li>Issue #2671: ANY | SOME with array
24+
<li>PR #3831: Add Oracle-style NOWAIT, WAIT n, and SKIP LOCKED to FOR UPDATE clause
25+
</li>
26+
<li>RP #3830: Fix time zone of time/timestamp with time zone AT LOCAL
27+
</li>
28+
<li>PR #3822 / Issue #2671: Add quantified comparison predicates with array
2529
</li>
2630
<li>PR #3820: Add partial implementation of JSON simplified accessor
2731
</li>

h2/src/main/org/h2/command/Command.java

+7-7
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,8 @@ public ResultInterface executeQuery(long maxrows, boolean scrollable) {
195195
}
196196
return result;
197197
} catch (DbException e) {
198-
// cannot retry DDL
199-
if (isCurrentCommandADefineCommand()) {
198+
// cannot retry some commands
199+
if (!isRetryable()) {
200200
throw e;
201201
}
202202
start = filterConcurrentUpdate(e, start);
@@ -251,8 +251,8 @@ public ResultWithGeneratedKeys executeUpdate(Object generatedKeysRequest) {
251251
try {
252252
return update(generatedKeysRequest);
253253
} catch (DbException e) {
254-
// cannot retry DDL
255-
if (isCurrentCommandADefineCommand()) {
254+
// cannot retry some commands
255+
if (!isRetryable()) {
256256
throw e;
257257
}
258258
start = filterConcurrentUpdate(e, start);
@@ -373,11 +373,11 @@ public void setCanReuse(boolean canReuse) {
373373
public abstract Set<DbObject> getDependencies();
374374

375375
/**
376-
* Is the command we just tried to execute a DefineCommand (i.e. DDL).
376+
* Returns is this command can be repeated again on locking failure.
377377
*
378-
* @return true if yes
378+
* @return is this command can be repeated again on locking failure
379379
*/
380-
protected abstract boolean isCurrentCommandADefineCommand();
380+
protected abstract boolean isRetryable();
381381

382382
protected final Database getDatabase() {
383383
return session.getDatabase();

h2/src/main/org/h2/command/CommandContainer.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import java.util.Set;
1212
import org.h2.api.DatabaseEventListener;
1313
import org.h2.api.ErrorCode;
14-
import org.h2.command.ddl.DefineCommand;
1514
import org.h2.command.dml.DataChangeStatement;
1615
import org.h2.engine.Database;
1716
import org.h2.engine.DbObject;
@@ -308,7 +307,8 @@ public Set<DbObject> getDependencies() {
308307
}
309308

310309
@Override
311-
protected boolean isCurrentCommandADefineCommand() {
312-
return prepared instanceof DefineCommand;
310+
protected boolean isRetryable() {
311+
return prepared.isRetryable();
313312
}
313+
314314
}

h2/src/main/org/h2/command/CommandList.java

+11-2
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,16 @@ public Set<DbObject> getDependencies() {
120120
}
121121

122122
@Override
123-
protected boolean isCurrentCommandADefineCommand() {
124-
return command.isCurrentCommandADefineCommand();
123+
protected boolean isRetryable() {
124+
if (!command.isRetryable()) {
125+
return false;
126+
}
127+
for (Prepared prepared : commands) {
128+
if (!prepared.isRetryable()) {
129+
return false;
130+
}
131+
}
132+
return remainingCommand == null || remainingCommand.isRetryable();
125133
}
134+
126135
}

h2/src/main/org/h2/command/Parser.java

+20-3
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
import static org.h2.util.ParserUtil.YEAR;
133133
import static org.h2.util.ParserUtil._ROWID_;
134134

135+
import java.math.BigDecimal;
135136
import java.nio.charset.Charset;
136137
import java.text.Collator;
137138
import java.util.ArrayList;
@@ -227,6 +228,7 @@
227228
import org.h2.command.dml.SetTypes;
228229
import org.h2.command.dml.TransactionCommand;
229230
import org.h2.command.dml.Update;
231+
import org.h2.command.query.ForUpdate;
230232
import org.h2.command.query.Query;
231233
import org.h2.command.query.QueryOrderBy;
232234
import org.h2.command.query.Select;
@@ -2773,10 +2775,25 @@ private void parseEndOfQuery(Query command) {
27732775
do {
27742776
readIdentifierWithSchema();
27752777
} while (readIf(COMMA));
2776-
} else if (readIf("NOWAIT")) {
2777-
// TODO parser: select for update nowait: should not wait
27782778
}
2779-
command.setForUpdate(true);
2779+
ForUpdate forUpdate;
2780+
if (readIf("NOWAIT")) {
2781+
forUpdate = ForUpdate.NOWAIT;
2782+
} else if (readIf("WAIT")) {
2783+
BigDecimal timeout;
2784+
if (currentTokenType != LITERAL || (timeout = token.value(session).getBigDecimal()) == null
2785+
|| timeout.signum() < 0
2786+
|| timeout.compareTo(BigDecimal.valueOf(Integer.MAX_VALUE, 3)) > 0) {
2787+
throw DbException.getSyntaxError(sqlCommand, token.start(), "timeout (0..2147483.647)");
2788+
}
2789+
read();
2790+
forUpdate = ForUpdate.wait(timeout.movePointRight(3).intValue());
2791+
} else if (readIf("SKIP", "LOCKED")) {
2792+
forUpdate = ForUpdate.SKIP_LOCKED;
2793+
} else {
2794+
forUpdate = ForUpdate.DEFAULT;
2795+
}
2796+
command.setForUpdate(forUpdate);
27802797
} else if (readIf("READ") || readIf(FETCH)) {
27812798
read("ONLY");
27822799
}

h2/src/main/org/h2/command/Prepared.java

+10
Original file line numberDiff line numberDiff line change
@@ -495,4 +495,14 @@ public void collectDependencies(HashSet<DbObject> dependencies) {}
495495
protected final Database getDatabase() {
496496
return session.getDatabase();
497497
}
498+
499+
/**
500+
* Returns is this command can be repeated again on locking failure.
501+
*
502+
* @return is this command can be repeated again on locking failure
503+
*/
504+
public boolean isRetryable() {
505+
return true;
506+
}
507+
498508
}

h2/src/main/org/h2/command/ddl/DefineCommand.java

+5
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,9 @@ public boolean isTransactional() {
4949
return transactional;
5050
}
5151

52+
@Override
53+
public boolean isRetryable() {
54+
return false;
55+
}
56+
5257
}

h2/src/main/org/h2/command/dml/Delete.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public long update(ResultTarget deltaChangeCollector, ResultOption deltaChangeCo
5858
while (nextRow(limitRows, count)) {
5959
Row row = targetTableFilter.get();
6060
if (table.isRowLockable()) {
61-
Row lockedRow = table.lockRow(session, row);
61+
Row lockedRow = table.lockRow(session, row, -1);
6262
if (lockedRow == null) {
6363
continue;
6464
}

h2/src/main/org/h2/command/dml/MergeUsing.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public long update(ResultTarget deltaChangeCollector, ResultOption deltaChangeCo
103103
if (!nullRow) {
104104
Row targetRow = targetTableFilter.get();
105105
if (table.isRowLockable()) {
106-
Row lockedRow = table.lockRow(session, targetRow);
106+
Row lockedRow = table.lockRow(session, targetRow, -1);
107107
if (lockedRow == null) {
108108
if (previousSource != source) {
109109
missedSource = source;

h2/src/main/org/h2/command/dml/Update.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public long update(ResultTarget deltaChangeCollector, ResultOption deltaChangeCo
6767
while (nextRow(limitRows, count)) {
6868
Row oldRow = targetTableFilter.get();
6969
if (table.isRowLockable()) {
70-
Row lockedRow = table.lockRow(session, oldRow);
70+
Row lockedRow = table.lockRow(session, oldRow, -1);
7171
if (lockedRow == null) {
7272
continue;
7373
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2004-2023 H2 Group. Multiple-Licensed under the MPL 2.0,
3+
* and the EPL 1.0 (https://h2database.com/html/license.html).
4+
* Initial Developer: H2 Group
5+
*/
6+
package org.h2.command.query;
7+
8+
import org.h2.message.DbException;
9+
import org.h2.util.HasSQL;
10+
import org.h2.util.StringUtils;
11+
12+
/**
13+
* FOR UPDATE clause.
14+
*/
15+
public final class ForUpdate implements HasSQL {
16+
17+
/**
18+
* Type of FOR UPDATE clause.
19+
*/
20+
public enum Type {
21+
22+
/**
23+
* Use default lock timeout.
24+
*/
25+
DEFAULT,
26+
27+
/**
28+
* Use specified lock timeout.
29+
*/
30+
WAIT,
31+
32+
/**
33+
* Use zero timeout.
34+
*/
35+
NOWAIT,
36+
37+
/**
38+
* Skip locked rows.
39+
*/
40+
SKIP_LOCKED;
41+
42+
}
43+
44+
/**
45+
* FOR UPDATE clause without additional parameters.
46+
*/
47+
public static final ForUpdate DEFAULT = new ForUpdate(Type.DEFAULT, -1);
48+
49+
/**
50+
* FOR UPDATE NOWAIT clause.
51+
*/
52+
public static final ForUpdate NOWAIT = new ForUpdate(Type.NOWAIT, 0);
53+
54+
/**
55+
* FOR UPDATE SKIP LOCKED clause.
56+
*/
57+
public static final ForUpdate SKIP_LOCKED = new ForUpdate(Type.SKIP_LOCKED, -2);
58+
59+
/**
60+
* Returns FOR UPDATE WAIT N clause.
61+
*
62+
* @param timeoutMillis
63+
* timeout in milliseconds
64+
* @return FOR UPDATE WAIT N clause
65+
*/
66+
public static final ForUpdate wait(int timeoutMillis) {
67+
if (timeoutMillis < 0) {
68+
throw DbException.getInvalidValueException("timeout", timeoutMillis);
69+
}
70+
if (timeoutMillis == 0) {
71+
return NOWAIT;
72+
}
73+
return new ForUpdate(Type.WAIT, timeoutMillis);
74+
}
75+
76+
private final Type type;
77+
78+
private final int timeoutMillis;
79+
80+
private ForUpdate(Type type, int timeoutMillis) {
81+
this.type = type;
82+
this.timeoutMillis = timeoutMillis;
83+
}
84+
85+
/**
86+
* Returns type of FOR UPDATE clause.
87+
*
88+
* @return type of FOR UPDATE clause
89+
*/
90+
public Type getType() {
91+
return type;
92+
}
93+
94+
/**
95+
* Returns timeout in milliseconds.
96+
*
97+
* @return timeout in milliseconds for {@link Type#WAIT}, {@code 0} for
98+
* {@link Type#NOWAIT}, {@code -2} for {@link Type#SKIP_LOCKED},
99+
* {@code -1} for default timeout
100+
*/
101+
public int getTimeoutMillis() {
102+
return timeoutMillis;
103+
}
104+
105+
@Override
106+
public StringBuilder getSQL(StringBuilder builder, int sqlFlags) {
107+
builder.append(" FOR UPDATE");
108+
switch (type) {
109+
case WAIT: {
110+
builder.append(" WAIT ").append(timeoutMillis / 1_000);
111+
int millis = timeoutMillis % 1_000;
112+
if (millis > 0) {
113+
StringUtils.appendZeroPadded(builder.append('.'), 3, millis);
114+
}
115+
break;
116+
}
117+
case NOWAIT:
118+
builder.append(" NOWAIT");
119+
break;
120+
case SKIP_LOCKED:
121+
builder.append(" SKIP LOCKED");
122+
break;
123+
default:
124+
}
125+
return builder;
126+
}
127+
128+
}

h2/src/main/org/h2/command/query/Query.java

+17-3
Original file line numberDiff line numberDiff line change
@@ -279,11 +279,19 @@ public boolean hasOrder() {
279279
}
280280

281281
/**
282-
* Set the 'for update' flag.
282+
* Returns FOR UPDATE clause, if any.
283+
* @return FOR UPDATE clause or {@code null}
284+
*/
285+
public ForUpdate getForUpdate() {
286+
return null;
287+
}
288+
289+
/**
290+
* Set the FOR UPDATE clause.
283291
*
284-
* @param forUpdate the new setting
292+
* @param forUpdate the new FOR UPDATE clause
285293
*/
286-
public abstract void setForUpdate(boolean forUpdate);
294+
public abstract void setForUpdate(ForUpdate forUpdate);
287295

288296
/**
289297
* Get the column count of this query.
@@ -1033,4 +1041,10 @@ public Expression getIfSingleRow() {
10331041
return null;
10341042
}
10351043

1044+
@Override
1045+
public boolean isRetryable() {
1046+
ForUpdate forUpdate = getForUpdate();
1047+
return forUpdate == null || forUpdate.getType() == ForUpdate.Type.SKIP_LOCKED;
1048+
}
1049+
10361050
}

0 commit comments

Comments
 (0)