Skip to content

Commit f27cff9

Browse files
aldumkiendangjtjeferreira
authored
MS SQL - take 3 (#94)
This PR builds on the work in #29 and #60 by @kiendang and @jtjeferreira. What it does: - rebase previous works on top of current `main` - fix table name escape - fix `DataTypesTests` and `OptionalsTests` with the point of contention being the lack of a dedicated boolean column type - add more tests (inserting both values of boolean, updating BIT columns) and document any result deviations How this is achieved: T-SQL (the dialect used by MS SQL Server) does have `Boolean`s as part of filter expressions, but not as column data, with `BIT`s being used as a substitute. Hence, these uses which are both `Boolean` on the Scala side, need to be distinguished. To this end, a marker was introduced in `Context`, explicitly designating `InsertValues`, `InsertSelect`, `InsertColumns` and `Update` expression as using them as values. This was done globally, because overloading would lead to a lot more boilerplate, while simply disregarding the flags value leave the existing dialects intact. Known omissions: - `ExprBooleanOps` for `BIT` values: small edge case, it's possible and there's syntax for it (bitwise operators), but it needs another branching of the above kind, to not break the normal OR/AND/NOT syntax. Bitwise operators are also already tested as part of NumericOps. - RETURNING: this is not a thing in T-SQL, however it does have OUTPUT, which could be used to achieve similar results - ON CONFLICT: similarly, not present in T-SQL, there is MERGE in the standard, but the opinions are mixed on whether it achieves the adequate level of isolation as Postgres's ON CONFLICT does. - LATERAL JOIN: again, very Postgres, maybe CROSS APPLY could be used to achieve similar effect. - bare VALUES: not supported. However, it is not it's own `Dialect` trait yet, which would indicate this more cleanly, --------- Co-authored-by: Kien Dang <[email protected]> Co-authored-by: João Ferreira <[email protected]>
1 parent 34678e2 commit f27cff9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2008
-396
lines changed

build.mill

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ trait CommonBase extends ScalaModule with PublishModule with ScalafixModule { co
5454
ivy"org.postgresql:postgresql:42.6.0",
5555
ivy"org.testcontainers:mysql:1.19.1",
5656
ivy"mysql:mysql-connector-java:8.0.33",
57+
ivy"org.testcontainers:mssqlserver:1.19.1",
58+
ivy"com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre11",
5759
ivy"com.zaxxer:HikariCP:5.1.0"
5860
)
5961

docs/reference.md

Lines changed: 144 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,10 @@ dbClient.transaction { db =>
236236
LocalDate.parse("2000-01-01")
237237
)
238238
)
239-
assert(generatedKeys == Seq(4, 5))
239+
if (!this.isInstanceOf[MsSqlSuite])
240+
assert(generatedKeys == Seq(4, 5))
241+
else
242+
assert(generatedKeys == Seq(5))
240243

241244
db.run(Buyer.select) ==> List(
242245
Buyer[Sc](1, "James Bond", LocalDate.parse("2001-02-03")),
@@ -1997,7 +2000,6 @@ Buyer.select
19972000
.leftJoin(ShippingInfo)(_.id `=` _.buyerId)
19982001
.map { case (b, si) => (b.name, si.map(_.shippingDate)) }
19992002
.sortBy(_._2)
2000-
.nullsFirst
20012003
```
20022004
20032005
@@ -2006,7 +2008,7 @@ Buyer.select
20062008
SELECT buyer0.name AS res_0, shipping_info1.shipping_date AS res_1
20072009
FROM buyer buyer0
20082010
LEFT JOIN shipping_info shipping_info1 ON (buyer0.id = shipping_info1.buyer_id)
2009-
ORDER BY res_1 NULLS FIRST
2011+
ORDER BY res_1
20102012
```
20112013
20122014
@@ -3395,7 +3397,7 @@ Purchase.delete(_ => true)
33953397
33963398
*
33973399
```sql
3398-
DELETE FROM purchase WHERE ?
3400+
DELETE FROM purchase
33993401
```
34003402
34013403
@@ -4003,7 +4005,7 @@ Product.select
40034005
40044006
40054007
## UpdateJoin
4006-
`UPDATE` queries that use `JOIN`s
4008+
Basic `UPDATE` queries
40074009
### UpdateJoin.join
40084010
40094011
ScalaSql supports performing `UPDATE`s with `FROM`/`JOIN` clauses using the
@@ -6951,7 +6953,7 @@ Select.delete(_ => true)
69516953
69526954
*
69536955
```sql
6954-
DELETE FROM "select" WHERE ?
6956+
DELETE FROM "select"
69556957
```
69566958
69576959
@@ -9774,7 +9776,7 @@ Expr(Bytes("Hello")).contains(Bytes("ll"))
97749776

97759777

97769778
## ExprMathOps
9777-
Math operations; supported by H2/Postgres/MySql, not supported by Sqlite
9779+
Math operations; supported by H2/Postgres/MySql/MsSql, not supported by Sqlite
97789780
### ExprMathOps.power
97799781

97809782

@@ -10111,7 +10113,7 @@ val value = DataTypes[Sc](
1011110113
myInt = 12345678,
1011210114
myBigInt = 12345678901L,
1011310115
myDouble = 3.14,
10114-
myBoolean = true,
10116+
myBoolean = false,
1011510117
myLocalDate = LocalDate.parse("2023-12-20"),
1011610118
myLocalTime = LocalTime.parse("10:15:30"),
1011710119
myLocalDateTime = LocalDateTime.parse("2011-12-03T10:15:30"),
@@ -10122,6 +10124,23 @@ val value = DataTypes[Sc](
1012210124
myEnum = MyEnum.bar
1012310125
)
1012410126
10127+
val value2 = DataTypes[Sc](
10128+
67.toByte,
10129+
mySmallInt = 32767.toShort,
10130+
myInt = 12345678,
10131+
myBigInt = 9876543210L,
10132+
myDouble = 2.71,
10133+
myBoolean = true,
10134+
myLocalDate = LocalDate.parse("2020-02-22"),
10135+
myLocalTime = LocalTime.parse("03:05:01"),
10136+
myLocalDateTime = LocalDateTime.parse("2021-06-07T02:01:03"),
10137+
myUtilDate = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS").parse("2021-06-07T02:01:03.000"),
10138+
myInstant = Instant.parse("2021-06-07T02:01:03Z"),
10139+
myVarBinary = new geny.Bytes(Array[Byte](9, 8, 7, 6, 5, 4, 3, 2)),
10140+
myUUID = new java.util.UUID(9876543210L, 1234567890L),
10141+
myEnum = MyEnum.baz
10142+
)
10143+
1012510144
db.run(
1012610145
DataTypes.insert.columns(
1012710146
_.myTinyInt := value.myTinyInt,
@@ -10140,8 +10159,26 @@ db.run(
1014010159
_.myEnum := value.myEnum
1014110160
)
1014210161
) ==> 1
10162+
db.run(
10163+
DataTypes.insert.columns(
10164+
_.myTinyInt := value2.myTinyInt,
10165+
_.mySmallInt := value2.mySmallInt,
10166+
_.myInt := value2.myInt,
10167+
_.myBigInt := value2.myBigInt,
10168+
_.myDouble := value2.myDouble,
10169+
_.myBoolean := value2.myBoolean,
10170+
_.myLocalDate := value2.myLocalDate,
10171+
_.myLocalTime := value2.myLocalTime,
10172+
_.myLocalDateTime := value2.myLocalDateTime,
10173+
_.myUtilDate := value2.myUtilDate,
10174+
_.myInstant := value2.myInstant,
10175+
_.myVarBinary := value2.myVarBinary,
10176+
_.myUUID := value2.myUUID,
10177+
_.myEnum := value2.myEnum
10178+
)
10179+
) ==> 1
1014310180
10144-
db.run(DataTypes.select) ==> Seq(value)
10181+
db.run(DataTypes.select) ==> Seq(value, value2)
1014510182
```
1014610183

1014710184

@@ -11181,6 +11218,24 @@ val rowSome = OptDataTypes[Sc](
1118111218
myUUID = Some(new java.util.UUID(1234567890L, 9876543210L)),
1118211219
myEnum = Some(MyEnum.bar)
1118311220
)
11221+
val rowSome2 = OptDataTypes[Sc](
11222+
myTinyInt = Some(67.toByte),
11223+
mySmallInt = Some(32767.toShort),
11224+
myInt = Some(23456789),
11225+
myBigInt = Some(9876543210L),
11226+
myDouble = Some(2.71),
11227+
myBoolean = Some(false),
11228+
myLocalDate = Some(LocalDate.parse("2020-02-22")),
11229+
myLocalTime = Some(LocalTime.parse("03:05:01")),
11230+
myLocalDateTime = Some(LocalDateTime.parse("2021-06-07T02:01:03")),
11231+
myUtilDate = Some(
11232+
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS").parse("2021-06-07T02:01:03.000")
11233+
),
11234+
myInstant = Some(Instant.parse("2021-06-07T02:01:03Z")),
11235+
myVarBinary = Some(new geny.Bytes(Array[Byte](9, 8, 7, 6, 5, 4, 3, 2))),
11236+
myUUID = Some(new java.util.UUID(9876543210L, 1234567890L)),
11237+
myEnum = Some(MyEnum.baz)
11238+
)
1118411239
1118511240
val rowNone = OptDataTypes[Sc](
1118611241
myTinyInt = None,
@@ -11198,12 +11253,11 @@ val rowNone = OptDataTypes[Sc](
1119811253
myUUID = None,
1119911254
myEnum = None
1120011255
)
11201-
1120211256
db.run(
11203-
OptDataTypes.insert.values(rowSome, rowNone)
11204-
) ==> 2
11257+
OptDataTypes.insert.values(rowSome, rowSome2, rowNone)
11258+
) ==> 3
1120511259
11206-
db.run(OptDataTypes.select) ==> Seq(rowSome, rowNone)
11260+
db.run(OptDataTypes.select) ==> Seq(rowSome, rowSome2, rowNone)
1120711261
```
1120811262

1120911263

@@ -12376,3 +12430,80 @@ db.concatWs(" ", "i", "am", "cow", 1337)
1237612430
```
1237712431
1237812432
12433+
12434+
## MsSqlDialect
12435+
Operations specific to working with Microsoft SQL Databases
12436+
### MsSqlDialect.top
12437+
12438+
For ScalaSql's Microsoft SQL dialect provides, the `.take(n)` operator translates
12439+
into a SQL `TOP(n)` clause
12440+
12441+
```scala
12442+
Buyer.select.take(0)
12443+
```
12444+
12445+
12446+
*
12447+
```sql
12448+
SELECT TOP(?) buyer0.id AS id, buyer0.name AS name, buyer0.date_of_birth AS date_of_birth
12449+
FROM buyer buyer0
12450+
```
12451+
12452+
12453+
12454+
*
12455+
```scala
12456+
Seq[Buyer[Sc]]()
12457+
```
12458+
12459+
12460+
12461+
### MsSqlDialect.bool vs bit
12462+
12463+
Insert rows with BIT values
12464+
12465+
```scala
12466+
db.run(
12467+
BoolTypes.insert.columns(
12468+
_.nullable := value.nullable,
12469+
_.nonNullable := value.nonNullable,
12470+
_.a := value.a,
12471+
_.b := value.b,
12472+
_.comment := value.comment
12473+
)
12474+
) ==> 1
12475+
db.run(
12476+
BoolTypes.insert.columns(
12477+
_.nullable := value2.nullable,
12478+
_.nonNullable := value2.nonNullable,
12479+
_.a := value2.a,
12480+
_.b := value2.b,
12481+
_.comment := value2.comment
12482+
)
12483+
) ==> 1
12484+
```
12485+
12486+
12487+
12488+
12489+
12490+
12491+
### MsSqlDialect.uodate BIT
12492+
12493+
12494+
12495+
```scala
12496+
BoolTypes
12497+
.update(_.a `=` 1)
12498+
.set(_.nonNullable := true)
12499+
```
12500+
12501+
12502+
*
12503+
```sql
12504+
UPDATE bool_types SET non_nullable = ? WHERE (bool_types.a = ?)
12505+
```
12506+
12507+
12508+
12509+

scalasql/core/src/Context.scala

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ trait Context {
1919
*/
2020
def exprNaming: Map[Expr.Identity, SqlStr]
2121

22+
/**
23+
* Mark [[Expr]]s as a raw value for an INSERT or UPDATE context
24+
*/
25+
def valueMarker: Boolean
26+
2227
/**
2328
* The ScalaSql configuration
2429
*/
@@ -28,9 +33,11 @@ trait Context {
2833

2934
def withFromNaming(fromNaming: Map[Context.From, String]): Context
3035
def withExprNaming(exprNaming: Map[Expr.Identity, SqlStr]): Context
36+
def markAsValue: Context
3137
}
3238

3339
object Context {
40+
3441
trait From {
3542

3643
/**
@@ -58,13 +65,18 @@ object Context {
5865
case class Impl(
5966
fromNaming: Map[From, String],
6067
exprNaming: Map[Expr.Identity, SqlStr],
68+
valueMarker: Boolean,
6169
config: Config,
6270
dialectConfig: DialectConfig
6371
) extends Context {
6472
def withFromNaming(fromNaming: Map[From, String]): Context = copy(fromNaming = fromNaming)
6573

6674
def withExprNaming(exprNaming: Map[Expr.Identity, SqlStr]): Context =
6775
copy(exprNaming = exprNaming)
76+
77+
def markAsValue: Context = copy(
78+
valueMarker = true
79+
)
6880
}
6981

7082
/**
@@ -96,7 +108,13 @@ object Context {
96108
.map { case (e, s) => (e, sql"${SqlStr.raw(newFromNaming(t), Array(e))}.$s") }
97109
}
98110

99-
Context.Impl(newFromNaming, newExprNaming, prevContext.config, prevContext.dialectConfig)
111+
Context.Impl(
112+
newFromNaming,
113+
newExprNaming,
114+
prevContext.valueMarker,
115+
prevContext.config,
116+
prevContext.dialectConfig
117+
)
100118
}
101119

102120
}

scalasql/core/src/DbApi.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ object DbApi {
131131
config: Config,
132132
dialectConfig: DialectConfig
133133
) = {
134-
val ctx = Context.Impl(Map(), Map(), config, dialectConfig)
134+
val ctx = Context.Impl(Map(), Map(), false, config, dialectConfig)
135135
val flattened = SqlStr.flatten(qr.renderSql(query, ctx))
136136
flattened
137137
}
@@ -583,7 +583,7 @@ object DbApi {
583583

584584
try {
585585
val res = block(new DbApi.SavepointImpl(savepoint, () => rollbackSavepoint(savepoint)))
586-
if (savepointStack.lastOption.exists(_ eq savepoint)) {
586+
if (dialect.supportSavepointRelease && savepointStack.lastOption.exists(_ eq savepoint)) {
587587
// Only release if this savepoint has not been rolled back,
588588
// directly or indirectly
589589
connection.releaseSavepoint(savepoint)

scalasql/core/src/DialectConfig.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package scalasql.core
33
trait DialectConfig { that =>
44
def castParams: Boolean
55
def escape(str: String): String
6+
def supportSavepointRelease: Boolean
67

78
def withCastParams(params: Boolean) = new DialectConfig {
89
def castParams: Boolean = params
910

10-
def escape(str: String): String = that.escape(str)
11+
def supportSavepointRelease = that.supportSavepointRelease
1112

13+
def escape(str: String): String = that.escape(str)
1214
}
1315
}

scalasql/query/src/CompoundSelect.scala

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ object CompoundSelect {
112112
// columns are duplicates or not, and thus what final set of rows is returned
113113
lazy val preserveAll = query.compoundOps.exists(_.op != "UNION ALL")
114114

115-
def render(liveExprs: LiveExprs) = {
115+
protected def prerender(liveExprs: LiveExprs) = {
116116
val innerLiveExprs =
117117
if (preserveAll) LiveExprs.none
118118
else liveExprs.map(_ ++ newReferencedExpressions)
@@ -138,7 +138,14 @@ object CompoundSelect {
138138
SqlStr.join(compoundStrs)
139139
}
140140

141-
lhsStr + compound + sortOpt + limitOpt + offsetOpt
141+
(lhsStr, compound, sortOpt, limitOpt, offsetOpt)
142+
}
143+
144+
def render(liveExprs: LiveExprs) = {
145+
prerender(liveExprs) match {
146+
case (lhsStr, compound, sortOpt, limitOpt, offsetOpt) =>
147+
lhsStr + compound + sortOpt + limitOpt + offsetOpt
148+
}
142149
}
143150
def orderToSqlStr(newCtx: Context) =
144151
CompoundSelect.orderToSqlStr(query.orderBy, newCtx, gap = true)

scalasql/query/src/Delete.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package scalasql.query
22

3-
import scalasql.core.DialectTypeMappers
4-
import scalasql.core.Context
5-
import scalasql.core.{Queryable, SqlStr, Expr}
3+
import scalasql.core.{Context, DialectTypeMappers, Expr, ExprsToSql, Queryable, SqlStr}
64
import scalasql.core.SqlStr.SqlStringSyntax
75

86
/**
@@ -26,6 +24,8 @@ object Delete {
2624
lazy val tableNameStr =
2725
SqlStr.raw(Table.fullIdentifier(table.value))
2826

29-
def render() = sql"DELETE FROM $tableNameStr WHERE $expr"
27+
lazy val filtersOpt = SqlStr.flatten(ExprsToSql.booleanExprs(sql" WHERE ", expr :: Nil))
28+
29+
def render() = sql"DELETE FROM $tableNameStr$filtersOpt"
3030
}
3131
}

0 commit comments

Comments
 (0)