Skip to content

Commit 1843286

Browse files
committed
Add SELECT FOR UPDATE variants for Postgres and MySql
1 parent 78e0970 commit 1843286

File tree

11 files changed

+212
-11
lines changed

11 files changed

+212
-11
lines changed

docs/reference.md

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10243,7 +10243,7 @@ db.run(OptDataTypes.select) ==> Seq(rowSome, rowNone)
1024310243
Operations specific to working with Postgres Databases
1024410244
### PostgresDialect.distinctOn
1024510245
10246-
ScalaSql's Postgres dialect provides teh `.distinctOn` operator, which translates
10246+
ScalaSql's Postgres dialect provides the `.distinctOn` operator, which translates
1024710247
into a SQL `DISTINCT ON` clause
1024810248

1024910249
```scala
@@ -10276,6 +10276,38 @@ Purchase.select.distinctOn(_.shippingInfoId).sortBy(_.shippingInfoId).desc
1027610276

1027710277

1027810278

10279+
### PostgresDialect.forUpdate
10280+
10281+
ScalaSql's Postgres dialect provides the `.forUpdate` operator, which translates
10282+
into a SQL `SELECT ... FOR UPDATE` clause
10283+
10284+
```scala
10285+
Invoice.select.filter(_.id === 1).forUpdate
10286+
```
10287+
10288+
10289+
*
10290+
```sql
10291+
SELECT
10292+
invoice0.id AS id,
10293+
invoice0.total AS total,
10294+
invoice0.vendor_name AS vendor_name
10295+
FROM otherschema.invoice invoice0
10296+
WHERE (invoice0.id = ?)
10297+
FOR UPDATE
10298+
```
10299+
10300+
10301+
10302+
*
10303+
```scala
10304+
Seq(
10305+
Invoice[Sc](1, 150.4, "Siemens")
10306+
)
10307+
```
10308+
10309+
10310+
1027910311
### PostgresDialect.ltrim2
1028010312
1028110313
@@ -10480,6 +10512,38 @@ db.random
1048010512
1048110513
## MySqlDialect
1048210514
Operations specific to working with MySql Databases
10515+
### MySqlDialect.forUpdate
10516+
10517+
ScalaSql's MySql dialect provides the `.forUpdate` operator, which translates
10518+
into a SQL `SELECT ... FOR UPDATE` clause
10519+
10520+
```scala
10521+
Buyer.select.filter(_.id === 1).forUpdate
10522+
```
10523+
10524+
10525+
*
10526+
```sql
10527+
SELECT
10528+
buyer0.id AS id,
10529+
buyer0.name AS name,
10530+
buyer0.date_of_birth AS date_of_birth
10531+
FROM buyer buyer0
10532+
WHERE (buyer0.id = ?)
10533+
FOR UPDATE
10534+
```
10535+
10536+
10537+
10538+
*
10539+
```scala
10540+
Seq(
10541+
Buyer[Sc](1, "James Bond", LocalDate.parse("2001-02-03"))
10542+
)
10543+
```
10544+
10545+
10546+
1048310547
### MySqlDialect.reverse
1048410548

1048510549

scalasql/query/src/Select.scala

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,24 @@ trait Select[Q, R]
5757
protected def newSimpleSelect[Q, R](
5858
expr: Q,
5959
exprPrefix: Option[Context => SqlStr],
60+
exprSuffix: Option[Context => SqlStr],
6061
preserveAll: Boolean,
6162
from: Seq[Context.From],
6263
joins: Seq[Join],
6364
where: Seq[Expr[?]],
6465
groupBy0: Option[GroupBy]
6566
)(implicit qr: Queryable.Row[Q, R], dialect: DialectTypeMappers): SimpleSelect[Q, R] =
66-
new SimpleSelect(expr, exprPrefix, preserveAll, from, joins, where, groupBy0)
67+
new SimpleSelect(expr, exprPrefix, exprSuffix, preserveAll, from, joins, where, groupBy0)
6768

6869
def qr: Queryable.Row[Q, R]
6970

7071
/**
7172
* Causes this [[Select]] to ignore duplicate rows, translates into SQL `SELECT DISTINCT`
7273
*/
7374
def distinct: Select[Q, R] = selectWithExprPrefix(true, _ => sql"DISTINCT")
75+
7476
protected def selectWithExprPrefix(preserveAll: Boolean, s: Context => SqlStr): Select[Q, R]
77+
protected def selectWithExprSuffix(preserveAll: Boolean, s: Context => SqlStr): Select[Q, R]
7578

7679
protected def subqueryRef(implicit qr: Queryable.Row[Q, R]) = new SubqueryRef(this)
7780

@@ -227,7 +230,7 @@ trait Select[Q, R]
227230
* in this [[Select]]
228231
*/
229232
def subquery: SimpleSelect[Q, R] = {
230-
newSimpleSelect(expr, None, false, Seq(subqueryRef(qr)), Nil, Nil, None)(qr, dialect)
233+
newSimpleSelect(expr, None, None, false, Seq(subqueryRef(qr)), Nil, Nil, None)(qr, dialect)
231234
}
232235

233236
/**
@@ -278,19 +281,23 @@ object Select {
278281
lhs: Select[Q, R],
279282
expr: Q,
280283
exprPrefix: Option[Context => SqlStr],
284+
exprSuffix: Option[Context => SqlStr],
281285
preserveAll: Boolean,
282286
from: Seq[Context.From],
283287
joins: Seq[Join],
284288
where: Seq[Expr[?]],
285289
groupBy0: Option[GroupBy]
286290
)(implicit qr: Queryable.Row[Q, R], dialect: DialectTypeMappers): SimpleSelect[Q, R] =
287-
lhs.newSimpleSelect(expr, exprPrefix, preserveAll, from, joins, where, groupBy0)
291+
lhs.newSimpleSelect(expr, exprPrefix, exprSuffix, preserveAll, from, joins, where, groupBy0)
288292

289293
def toSimpleFrom[Q, R](s: Select[Q, R]) = s.selectToSimpleSelect()
290294

291295
def withExprPrefix[Q, R](s: Select[Q, R], preserveAll: Boolean, str: Context => SqlStr) =
292296
s.selectWithExprPrefix(preserveAll, str)
293297

298+
def withExprSuffix[Q, R](s: Select[Q, R], preserveAll: Boolean, str: Context => SqlStr) =
299+
s.selectWithExprSuffix(preserveAll, str)
300+
294301
implicit class ExprSelectOps[T](s: Select[Expr[T], T]) {
295302
def sorted(implicit tm: TypeMapper[T]): Select[Expr[T], T] = s.sortBy(identity)
296303
}
@@ -303,6 +310,12 @@ object Select {
303310
): Select[Q, R] =
304311
selectToSimpleSelect().selectWithExprPrefix(preserveAll, s)
305312

313+
override protected def selectWithExprSuffix(
314+
preserveAll: Boolean,
315+
s: Context => SqlStr
316+
): Select[Q, R] =
317+
selectToSimpleSelect().selectWithExprSuffix(preserveAll, s)
318+
306319
override def map[Q2, R2](f: Q => Q2)(implicit qr: Queryable.Row[Q2, R2]): Select[Q2, R2] =
307320
selectToSimpleSelect().map(f)
308321

scalasql/query/src/SimpleSelect.scala

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import scalasql.renderer.JoinsToSql
2222
class SimpleSelect[Q, R](
2323
val expr: Q,
2424
val exprPrefix: Option[Context => SqlStr],
25+
val exprSuffix: Option[Context => SqlStr],
2526
val preserveAll: Boolean,
2627
val from: Seq[Context.From],
2728
val joins: Seq[Join],
@@ -33,17 +34,21 @@ class SimpleSelect[Q, R](
3334
protected def copy[Q, R](
3435
expr: Q = this.expr,
3536
exprPrefix: Option[Context => SqlStr] = this.exprPrefix,
37+
exprSuffix: Option[Context => SqlStr] = this.exprSuffix,
3638
preserveAll: Boolean = this.preserveAll,
3739
from: Seq[Context.From] = this.from,
3840
joins: Seq[Join] = this.joins,
3941
where: Seq[Expr[?]] = this.where,
4042
groupBy0: Option[GroupBy] = this.groupBy0
4143
)(implicit qr: Queryable.Row[Q, R]) =
42-
newSimpleSelect(expr, exprPrefix, preserveAll, from, joins, where, groupBy0)
44+
newSimpleSelect(expr, exprPrefix, exprSuffix, preserveAll, from, joins, where, groupBy0)
4345

4446
def selectWithExprPrefix(preserveAll: Boolean, s: Context => SqlStr): Select[Q, R] =
4547
this.copy(exprPrefix = Some(s), preserveAll = preserveAll)
4648

49+
def selectWithExprSuffix(preserveAll: Boolean, s: Context => SqlStr): Select[Q, R] =
50+
this.copy(exprSuffix = Some(s), preserveAll = preserveAll)
51+
4752
def aggregateExpr[V: TypeMapper](
4853
f: Q => Context => SqlStr
4954
)(implicit qr2: Queryable.Row[Expr[V], V]): Expr[V] = {
@@ -111,6 +116,7 @@ class SimpleSelect[Q, R](
111116
copy(
112117
expr = newExpr,
113118
exprPrefix = exprPrefix,
119+
exprSuffix = exprSuffix,
114120
joins = joins ++ newJoins,
115121
where = where ++ newWheres
116122
)
@@ -178,6 +184,7 @@ class SimpleSelect[Q, R](
178184
copy(
179185
expr = newExpr,
180186
exprPrefix = exprPrefix,
187+
exprSuffix = exprSuffix,
181188
from = Seq(this.subqueryRef),
182189
joins = Nil,
183190
where = Nil,
@@ -287,11 +294,12 @@ object SimpleSelect {
287294
)
288295

289296
lazy val exprPrefix = SqlStr.opt(query.exprPrefix) { p => p(context) + sql" " }
297+
lazy val exprSuffix = SqlStr.opt(query.exprSuffix) { p => p(context) }
290298

291299
val tables = SqlStr
292300
.join(query.from.map(renderedFroms(_)), SqlStr.commaSep)
293301

294-
sql"SELECT " + exprPrefix + exprStr + sql" FROM " + tables + joins + filtersOpt + groupByOpt
302+
sql"SELECT " + exprPrefix + exprStr + sql" FROM " + tables + joins + filtersOpt + groupByOpt + exprSuffix
295303
}
296304

297305
}

scalasql/query/src/WithCte.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ object WithCte {
7373
lhs,
7474
expr = WithSqlExpr.get(lhs),
7575
exprPrefix = None,
76+
exprSuffix = None,
7677
preserveAll = false,
7778
from = Seq(lhsSubQueryRef),
7879
joins = Nil,

scalasql/src/dialects/H2Dialect.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ object H2Dialect extends H2Dialect {
104104
new SimpleSelect(
105105
Table.metadata(t).vExpr(ref, dialectSelf).asInstanceOf[V[Expr]],
106106
None,
107+
None,
107108
false,
108109
Seq(ref),
109110
Nil,
@@ -132,6 +133,7 @@ object H2Dialect extends H2Dialect {
132133
override def newSimpleSelect[Q, R](
133134
expr: Q,
134135
exprPrefix: Option[Context => SqlStr],
136+
exprSuffix: Option[Context => SqlStr],
135137
preserveAll: Boolean,
136138
from: Seq[Context.From],
137139
joins: Seq[Join],
@@ -141,13 +143,14 @@ object H2Dialect extends H2Dialect {
141143
implicit qr: Queryable.Row[Q, R],
142144
dialect: scalasql.core.DialectTypeMappers
143145
): scalasql.query.SimpleSelect[Q, R] = {
144-
new SimpleSelect(expr, exprPrefix, preserveAll, from, joins, where, groupBy0)
146+
new SimpleSelect(expr, exprPrefix, exprSuffix, preserveAll, from, joins, where, groupBy0)
145147
}
146148
}
147149

148150
class SimpleSelect[Q, R](
149151
expr: Q,
150152
exprPrefix: Option[Context => SqlStr],
153+
exprSuffix: Option[Context => SqlStr],
151154
preserveAll: Boolean,
152155
from: Seq[Context.From],
153156
joins: Seq[Join],
@@ -157,6 +160,7 @@ object H2Dialect extends H2Dialect {
157160
extends scalasql.query.SimpleSelect(
158161
expr,
159162
exprPrefix,
163+
exprSuffix,
160164
preserveAll,
161165
from,
162166
joins,

scalasql/src/dialects/MySqlDialect.scala

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import java.sql.PreparedStatement
4040
import java.time.{Instant, LocalDateTime}
4141
import java.util.UUID
4242
import scala.reflect.ClassTag
43+
import scalasql.query.Select
4344

4445
trait MySqlDialect extends Dialect {
4546
protected def dialectCastParams = false
@@ -116,6 +117,38 @@ trait MySqlDialect extends Dialect {
116117
implicit def ExprAggOpsConv[T](v: Aggregatable[Expr[T]]): operations.ExprAggOps[T] =
117118
new MySqlDialect.ExprAggOps(v)
118119

120+
implicit class SelectForUpdateConv[Q, R](r: Select[Q, R]) {
121+
122+
/**
123+
* SELECT .. FOR UPDATE acquires an exclusive lock, blocking other transactions from
124+
* modifying or locking the selected rows, which is for managing concurrent transactions
125+
* and ensuring data consistency in multi-step operations.
126+
*/
127+
def forUpdate: Select[Q, R] =
128+
Select.withExprSuffix(r, true, _ => sql" FOR UPDATE")
129+
130+
/**
131+
* SELECT ... FOR SHARE: Locks the selected rows for reading, allowing other transactions
132+
* to read but not modify the locked rows
133+
*/
134+
def forShare: Select[Q, R] =
135+
Select.withExprSuffix(r, true, _ => sql" FOR SHARE")
136+
137+
/**
138+
* SELECT ... FOR UPDATE NOWAIT: Immediately returns an error if the selected rows are
139+
* already locked, instead of waiting
140+
*/
141+
def forUpdateNoWait: Select[Q, R] =
142+
Select.withExprSuffix(r, true, _ => sql" FOR UPDATE NOWAIT")
143+
144+
/**
145+
* SELECT ... FOR UPDATE SKIP LOCKED: Skips any rows that are already locked by other
146+
* transactions, instead of waiting
147+
*/
148+
def forUpdateSkipLocked: Select[Q, R] =
149+
Select.withExprSuffix(r, true, _ => sql" FOR UPDATE SKIP LOCKED")
150+
}
151+
119152
override implicit def DbApiOpsConv(db: => DbApi): MySqlDialect.DbApiOps =
120153
new MySqlDialect.DbApiOps(this)
121154

@@ -207,6 +240,7 @@ object MySqlDialect extends MySqlDialect {
207240
new SimpleSelect(
208241
Table.metadata(t).vExpr(ref, dialectSelf).asInstanceOf[V[Expr]],
209242
None,
243+
None,
210244
false,
211245
Seq(ref),
212246
Nil,
@@ -309,6 +343,7 @@ object MySqlDialect extends MySqlDialect {
309343
override def newSimpleSelect[Q, R](
310344
expr: Q,
311345
exprPrefix: Option[Context => SqlStr],
346+
exprSuffix: Option[Context => SqlStr],
312347
preserveAll: Boolean,
313348
from: Seq[Context.From],
314349
joins: Seq[Join],
@@ -318,13 +353,14 @@ object MySqlDialect extends MySqlDialect {
318353
implicit qr: Queryable.Row[Q, R],
319354
dialect: scalasql.core.DialectTypeMappers
320355
): scalasql.query.SimpleSelect[Q, R] = {
321-
new SimpleSelect(expr, exprPrefix, preserveAll, from, joins, where, groupBy0)
356+
new SimpleSelect(expr, exprPrefix, exprSuffix, preserveAll, from, joins, where, groupBy0)
322357
}
323358
}
324359

325360
class SimpleSelect[Q, R](
326361
expr: Q,
327362
exprPrefix: Option[Context => SqlStr],
363+
exprSuffix: Option[Context => SqlStr],
328364
preserveAll: Boolean,
329365
from: Seq[Context.From],
330366
joins: Seq[Join],
@@ -334,6 +370,7 @@ object MySqlDialect extends MySqlDialect {
334370
extends scalasql.query.SimpleSelect(
335371
expr,
336372
exprPrefix,
373+
exprSuffix,
337374
preserveAll,
338375
from,
339376
joins,

scalasql/src/dialects/PostgresDialect.scala

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,35 @@ trait PostgresDialect extends Dialect with ReturningDialect with OnConflictOps {
5454
}
5555
}
5656

57+
implicit class SelectForUpdateConv[Q, R](r: Select[Q, R]) {
58+
59+
/**
60+
* SELECT .. FOR UPDATE acquires an exclusive lock, blocking other transactions from
61+
* modifying or locking the selected rows, which is for managing concurrent transactions
62+
* and ensuring data consistency in multi-step operations.
63+
*/
64+
def forUpdate: Select[Q, R] =
65+
Select.withExprSuffix(r, true, _ => sql" FOR UPDATE")
66+
67+
/**
68+
* SELECT ... FOR NO KEY UPDATE: A weaker lock that doesn't block inserts into child
69+
* tables with foreign key references
70+
*/
71+
def forNoKeyUpdate: Select[Q, R] =
72+
Select.withExprSuffix(r, true, _ => sql" FOR NO KEY UPDATE")
73+
74+
/**
75+
* SELECT ... FOR SHARE: Locks the selected rows for reading, allowing other transactions
76+
* to read but not modify the locked rows.
77+
*/
78+
def forShare: Select[Q, R] =
79+
Select.withExprSuffix(r, true, _ => sql" FOR SHARE")
80+
81+
/** SELECT ... FOR KEY SHARE: The weakest lock, only conflicts with FOR UPDATE */
82+
def forKeyShare: Select[Q, R] =
83+
Select.withExprSuffix(r, true, _ => sql" FOR KEY SHARE")
84+
}
85+
5786
override implicit def DbApiOpsConv(db: => DbApi): PostgresDialect.DbApiOps =
5887
new PostgresDialect.DbApiOps(this)
5988
}

0 commit comments

Comments
 (0)