Skip to content

Commit 30064af

Browse files
authored
Merge pull request #3234 from dolthub/elian/9873
dolthub/dolt#9873: Add tests for `FOR UPDATE OF`
2 parents 8d288d2 + cb85145 commit 30064af

File tree

3 files changed

+90
-0
lines changed

3 files changed

+90
-0
lines changed

enginetest/queries/script_queries.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package queries
1616

1717
import (
18+
"fmt"
1819
"math"
1920
"time"
2021

@@ -121,6 +122,71 @@ type ScriptTestAssertion struct {
121122
// Unlike other engine tests, ScriptTests must be self-contained. No other tables are created outside the definition of
122123
// the tests.
123124
var ScriptTests = []ScriptTest{
125+
{
126+
// https://github.com/dolthub/dolt/issues/9873
127+
// TODO: `FOR UPDATE OF` (`FOR UPDATE` in general) is currently a no-op: https://www.dolthub.com/blog/2023-10-23-hold-my-beer/
128+
Name: "FOR UPDATE OF syntax support tests",
129+
Dialect: "mysql",
130+
SetUpScript: []string{
131+
"CREATE TABLE task_instance (id INT PRIMARY KEY, task_id VARCHAR(255), dag_id VARCHAR(255), run_id VARCHAR(255), state VARCHAR(50), queued_by_job_id INT)",
132+
"CREATE TABLE job (id INT PRIMARY KEY, state VARCHAR(50))",
133+
"CREATE TABLE dag_run (dag_id VARCHAR(255), run_id VARCHAR(255), state VARCHAR(50))",
134+
"CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(50))",
135+
"INSERT INTO task_instance VALUES (1, 'task1', 'dag1', 'run1', 'running', 1)",
136+
"INSERT INTO job VALUES (1, 'running')",
137+
"INSERT INTO dag_run VALUES ('dag1', 'run1', 'running')",
138+
"INSERT INTO t VALUES (1, 'test')",
139+
},
140+
Assertions: []ScriptTestAssertion{
141+
{
142+
Query: `SELECT task_instance.id, task_instance.task_id, task_instance.dag_id, task_instance.run_id
143+
FROM task_instance INNER JOIN job ON job.id = task_instance.queued_by_job_id INNER JOIN dag_run ON dag_run.dag_id = task_instance.dag_id AND dag_run.run_id = task_instance.run_id
144+
WHERE task_instance.state IN ('running', 'queued', 'scheduled') AND NOT (job.state <=> 'running') AND dag_run.state = 'running' FOR UPDATE OF task_instance SKIP LOCKED`,
145+
Expected: []sql.Row{},
146+
},
147+
{
148+
Query: "SELECT * FROM t FOR UPDATE",
149+
Expected: []sql.Row{{1, "test"}},
150+
},
151+
{
152+
Query: "SELECT * FROM t FOR UPDATE OF t",
153+
Expected: []sql.Row{{1, "test"}},
154+
},
155+
{
156+
Query: "SELECT * FROM t FOR UPDATE OF t SKIP LOCKED",
157+
Expected: []sql.Row{{1, "test"}},
158+
},
159+
{
160+
Query: "SELECT * FROM t FOR UPDATE OF t NOWAIT",
161+
Expected: []sql.Row{{1, "test"}},
162+
},
163+
{
164+
Query: "SELECT * FROM task_instance t1, job t2 FOR UPDATE OF t1, t2",
165+
Expected: []sql.Row{{1, "task1", "dag1", "run1", "running", 1, 1, "running"}},
166+
},
167+
{
168+
Query: "SELECT * FROM t FOR UPDATE OF nonexistent_table",
169+
ExpectedErr: sql.ErrUnresolvedTableLock,
170+
},
171+
{
172+
Query: "SELECT * FROM t FOR UPDATE OF t, nonexistent_table",
173+
ExpectedErr: sql.ErrUnresolvedTableLock,
174+
ExpectedErrStr: fmt.Sprintf(sql.ErrUnresolvedTableLock.Message, "nonexistent_table"),
175+
},
176+
{
177+
Query: "SELECT * FROM t FOR UPDATE OF",
178+
ExpectedErr: sql.ErrSyntaxError,
179+
},
180+
{
181+
Query: "SELECT * FROM t FOR UPDATE test",
182+
ExpectedErr: sql.ErrSyntaxError,
183+
},
184+
{
185+
Query: "SELECT * FROM t FOR UPDATE",
186+
Expected: []sql.Row{{1, "test"}},
187+
},
188+
},
189+
},
124190
{
125191
// https://github.com/dolthub/dolt/issues/9872
126192
Name: "TEXT(m) syntax support",

sql/errors.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,9 @@ var (
950950

951951
// ErrTruncatedIncorrect is thrown when converting a value results in portions of the data to be trimmed.
952952
ErrTruncatedIncorrect = errors.NewKind("Truncated incorrect %s value: %v")
953+
954+
// ErrUnresolvedTableLock is returned when a FOR UPDATE OF clause references a table that doesn't exist in the query context.
955+
ErrUnresolvedTableLock = errors.NewKind("unresolved table name `%s` in locking clause.")
953956
)
954957

955958
// CastSQLError returns a *mysql.SQLError with the error code and in some cases, also a SQL state, populated for the

sql/planbuilder/select.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ func (b *Builder) buildSelect(inScope *scope, s *ast.Select) (outScope *scope) {
123123
outScope.node = l
124124
}
125125

126+
b.buildForUpdateOf(s.Lock, fromScope)
127+
126128
return
127129
}
128130

@@ -238,3 +240,22 @@ func (b *Builder) renameSource(scope *scope, table string, cols []string) {
238240
scope.cols[i] = c
239241
}
240242
}
243+
244+
// buildForUpdateOf builds the `FOR UPDATE OF` clause, ensuring that all tables listed are
245+
// present in the clause. `FOR UPDATE` in general is a no-op, so `FOR UPDATE OF` is
246+
// also a no-op: https://www.dolthub.com/blog/2023-10-23-hold-my-beer/
247+
// TODO: implement actual row-level locking for `FOR UPDATE` clauses in general.
248+
func (b *Builder) buildForUpdateOf(lock *ast.Lock, fromScope *scope) {
249+
if lock == nil {
250+
return
251+
}
252+
253+
for _, tableName := range lock.Tables {
254+
tableNameStr := tableName.Name.String()
255+
256+
if !fromScope.hasTable(tableNameStr) {
257+
b.handleErr(sql.ErrUnresolvedTableLock.New(tableNameStr))
258+
return
259+
}
260+
}
261+
}

0 commit comments

Comments
 (0)