Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Microsoft SQL #29

Draft
wants to merge 25 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ec4d13d
Add MSSQL deps
kiendang Sep 5, 2024
943d925
Add MSSQL dialect
kiendang Sep 6, 2024
f42dabb
Add MSSQL Example tests
kiendang Sep 6, 2024
ab34ba2
Add MSSQL test suite
kiendang Sep 6, 2024
9ddd775
Set MSSQL_COLLATION for UTF-8
kiendang Sep 6, 2024
88830b9
Use + for MS SQL string concatenation in tests
kiendang Sep 6, 2024
13c8830
Use datalength instead of octet_length for MS SQL in tests
kiendang Sep 6, 2024
e9ca43f
Fix some MS SQL syntaxes in tests
kiendang Sep 7, 2024
b9e52bc
Fix some Math functions for MS SQL
kiendang Sep 8, 2024
98aa1b9
Fix ORDER BY with NULL for MS SQL
kiendang Sep 10, 2024
3a6af73
Add more valid results for ORDER BY with NULL tests
kiendang Sep 10, 2024
a24e84e
Fix LIMIT and OFFSET for MS SQL
kiendang Sep 10, 2024
9530c47
Enable .take without .drop for MS SQL
kiendang Sep 10, 2024
c2dba92
Fix tests involving .take and .drop for MS SQL
kiendang Sep 10, 2024
ec7931c
Fix MS SQL numeric ops
kiendang Sep 10, 2024
1850923
Lint
kiendang Sep 12, 2024
5aafe6e
Wait for mssql container startup to complete
kiendang Sep 12, 2024
7aae613
Fix wrongly edited test case
kiendang Sep 12, 2024
1e7a8ab
Use a more idiomatic way of waiting for a log output message
kiendang Oct 18, 2024
447ea7e
Fix some MSSQL specific type casts
kiendang Oct 18, 2024
68c9df7
Add another valid expected output to test case
kiendang Oct 18, 2024
660fb61
Exclude EXCLUDE test from MSSQL tests for window functions
kiendang Oct 18, 2024
bc5dde3
Fix MSSQL ORDER BY in test
kiendang Oct 18, 2024
f9f99ac
Merge branch 'main' into mssql
kiendang Dec 3, 2024
be72d63
Update due to internal API change
kiendang Dec 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ trait ScalaSql extends Common{ common =>
ivy"org.postgresql:postgresql:42.6.0",
ivy"org.testcontainers:mysql:1.19.1",
ivy"mysql:mysql-connector-java:8.0.33",
ivy"org.testcontainers:mssqlserver:1.19.1",
ivy"com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre11",
ivy"com.zaxxer:HikariCP:5.1.0"
)

Expand Down
11 changes: 9 additions & 2 deletions scalasql/query/src/CompoundSelect.scala
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ object CompoundSelect {
// columns are duplicates or not, and thus what final set of rows is returned
lazy val preserveAll = query.compoundOps.exists(_.op != "UNION ALL")

def render(liveExprs: LiveExprs) = {
protected def prerender(liveExprs: LiveExprs) = {
val innerLiveExprs =
if (preserveAll) LiveExprs.none
else liveExprs.map(_ ++ newReferencedExpressions)
Expand All @@ -138,7 +138,14 @@ object CompoundSelect {
SqlStr.join(compoundStrs)
}

lhsStr + compound + sortOpt + limitOpt + offsetOpt
(lhsStr, compound, sortOpt, limitOpt, offsetOpt)
}

def render(liveExprs: LiveExprs) = {
prerender(liveExprs) match {
case (lhsStr, compound, sortOpt, limitOpt, offsetOpt) =>
lhsStr + compound + sortOpt + limitOpt + offsetOpt
}
}
def orderToSqlStr(newCtx: Context) =
CompoundSelect.orderToSqlStr(query.orderBy, newCtx, gap = true)
Expand Down
270 changes: 270 additions & 0 deletions scalasql/src/dialects/MsSqlDialect.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
package scalasql.dialects

import scalasql.query.{AscDesc, GroupBy, Join, Nulls, OrderBy, SubqueryRef, Table}
import scalasql.core.{
Aggregatable,
Context,
DbApi,
DialectTypeMappers,
Expr,
Queryable,
TypeMapper,
SqlStr
}
import scalasql.{Sc, operations}
import scalasql.core.SqlStr.{Renderable, SqlStringSyntax}
import scalasql.operations.{ConcatOps, MathOps, TrimOps}

import java.time.{Instant, LocalDateTime, OffsetDateTime}
import scalasql.core.LiveExprs

trait MsSqlDialect extends Dialect {
protected def dialectCastParams = false

override implicit def IntType: TypeMapper[Int] = new MsSqlIntType
class MsSqlIntType extends IntType { override def castTypeString = "INT" }

override implicit def StringType: TypeMapper[String] = new MsSqlStringType
class MsSqlStringType extends StringType { override def castTypeString = "VARCHAR" }

override implicit def BooleanType: TypeMapper[Boolean] = new BooleanType
class MsSqlBooleanType extends BooleanType { override def castTypeString = "BIT" }

override implicit def UtilDateType: TypeMapper[java.util.Date] = new MsSqlUtilDateType
class MsSqlUtilDateType extends UtilDateType { override def castTypeString = "DATETIME2" }

override implicit def LocalDateTimeType: TypeMapper[LocalDateTime] = new MsSqlLocalDateTimeType
class MsSqlLocalDateTimeType extends LocalDateTimeType {
override def castTypeString = "DATETIME2"
}

override implicit def InstantType: TypeMapper[Instant] = new MsSqlInstantType
class MsSqlInstantType extends InstantType { override def castTypeString = "DATETIME2" }

override implicit def OffsetDateTimeType: TypeMapper[OffsetDateTime] = new MsSqlOffsetDateTimeType
class MsSqlOffsetDateTimeType extends OffsetDateTimeType {
override def castTypeString = "DATETIMEOFFSET"
}

override implicit def ExprStringOpsConv(v: Expr[String]): MsSqlDialect.ExprStringOps[String] =
new MsSqlDialect.ExprStringOps(v)

override implicit def ExprBlobOpsConv(
v: Expr[geny.Bytes]
): MsSqlDialect.ExprStringLikeOps[geny.Bytes] =
new MsSqlDialect.ExprStringLikeOps(v)

override implicit def ExprNumericOpsConv[T: Numeric: TypeMapper](
v: Expr[T]
): MsSqlDialect.ExprNumericOps[T] = new MsSqlDialect.ExprNumericOps(v)

override implicit def TableOpsConv[V[_[_]]](t: Table[V]): scalasql.dialects.TableOps[V] =
new MsSqlDialect.TableOps(t)

implicit def ExprAggOpsConv[T](v: Aggregatable[Expr[T]]): operations.ExprAggOps[T] =
new MsSqlDialect.ExprAggOps(v)

override implicit def DbApiOpsConv(db: => DbApi): MsSqlDialect.DbApiOps =
new MsSqlDialect.DbApiOps(this)
}

object MsSqlDialect extends MsSqlDialect {
class DbApiOps(dialect: DialectTypeMappers)
extends scalasql.operations.DbApiOps(dialect)
with ConcatOps
with MathOps {
override def ln[T: Numeric](v: Expr[T]): Expr[Double] = Expr { implicit ctx => sql"LOG($v)" }

override def atan2[T: Numeric](v: Expr[T], y: Expr[T]): Expr[Double] = Expr { implicit ctx =>
sql"ATN2($v, $y)"
}
}

class ExprAggOps[T](v: Aggregatable[Expr[T]]) extends scalasql.operations.ExprAggOps[T](v) {
def mkString(sep: Expr[String] = null)(implicit tm: TypeMapper[T]): Expr[String] = {
val sepRender = Option(sep).getOrElse(sql"''")
v.aggregateExpr(expr => implicit ctx => sql"STRING_AGG($expr + '', $sepRender)")
}
}

class ExprStringOps[T](v: Expr[T]) extends ExprStringLikeOps(v) with operations.ExprStringOps[T]
class ExprStringLikeOps[T](protected val v: Expr[T])
extends operations.ExprStringLikeOps(v)
with TrimOps {

override def +(x: Expr[T]): Expr[T] = Expr { implicit ctx => sql"($v + $x)" }

override def startsWith(other: Expr[T]): Expr[Boolean] = Expr { implicit ctx =>
sql"($v LIKE $other + '%')"
}

override def endsWith(other: Expr[T]): Expr[Boolean] = Expr { implicit ctx =>
sql"($v LIKE '%' + $other)"
}

override def contains(other: Expr[T]): Expr[Boolean] = Expr { implicit ctx =>
sql"($v LIKE '%' + $other + '%')"
}

override def length: Expr[Int] = Expr { implicit ctx => sql"LEN($v)" }

override def octetLength: Expr[Int] = Expr { implicit ctx => sql"DATALENGTH($v)" }

def indexOf(x: Expr[T]): Expr[Int] = Expr { implicit ctx => sql"CHARINDEX($x, $v)" }
def reverse: Expr[T] = Expr { implicit ctx => sql"REVERSE($v)" }
}

class ExprNumericOps[T: Numeric: TypeMapper](protected val v: Expr[T])
extends operations.ExprNumericOps[T](v) {
override def %[V: Numeric](x: Expr[V]): Expr[T] = Expr { implicit ctx => sql"$v % $x" }

override def mod[V: Numeric](x: Expr[V]): Expr[T] = Expr { implicit ctx => sql"$v % $x" }

override def ceil: Expr[T] = Expr { implicit ctx => sql"CEILING($v)" }
}

class TableOps[V[_[_]]](t: Table[V]) extends scalasql.dialects.TableOps[V](t) {

protected override def joinableToSelect: Select[V[Expr], V[Sc]] = {
val ref = Table.ref(t)
new SimpleSelect(
Table.metadata(t).vExpr(ref, dialectSelf).asInstanceOf[V[Expr]],
None,
None,
false,
Seq(ref),
Nil,
Nil,
None
)(
t.containerQr
)
}
}

trait Select[Q, R] extends scalasql.query.Select[Q, R] {
override def newCompoundSelect[Q, R](
lhs: scalasql.query.SimpleSelect[Q, R],
compoundOps: Seq[scalasql.query.CompoundSelect.Op[Q, R]],
orderBy: Seq[OrderBy],
limit: Option[Int],
offset: Option[Int]
)(
implicit qr: Queryable.Row[Q, R],
dialect: scalasql.core.DialectTypeMappers
): scalasql.query.CompoundSelect[Q, R] = {
new CompoundSelect(lhs, compoundOps, orderBy, limit, offset)
}

override def newSimpleSelect[Q, R](
expr: Q,
exprPrefix: Option[Context => SqlStr],
exprSuffix: Option[Context => SqlStr],
preserveAll: Boolean,
from: Seq[Context.From],
joins: Seq[Join],
where: Seq[Expr[?]],
groupBy0: Option[GroupBy]
)(
implicit qr: Queryable.Row[Q, R],
dialect: scalasql.core.DialectTypeMappers
): scalasql.query.SimpleSelect[Q, R] = {
new SimpleSelect(expr, exprPrefix, exprSuffix, preserveAll, from, joins, where, groupBy0)
}
}

class SimpleSelect[Q, R](
expr: Q,
exprPrefix: Option[Context => SqlStr],
exprSuffix: Option[Context => SqlStr],
preserveAll: Boolean,
from: Seq[Context.From],
joins: Seq[Join],
where: Seq[Expr[?]],
groupBy0: Option[GroupBy]
)(implicit qr: Queryable.Row[Q, R])
extends scalasql.query.SimpleSelect(
expr,
exprPrefix,
exprSuffix,
preserveAll,
from,
joins,
where,
groupBy0
)
with Select[Q, R] {
override def take(n: Int): scalasql.query.Select[Q, R] = {
selectWithExprPrefix(true, _ => sql"TOP($n)")
}

override def drop(n: Int): scalasql.query.Select[Q, R] = throw new Exception(
".drop must follow .sortBy"
)
}

class CompoundSelect[Q, R](
lhs: scalasql.query.SimpleSelect[Q, R],
compoundOps: Seq[scalasql.query.CompoundSelect.Op[Q, R]],
orderBy: Seq[OrderBy],
limit: Option[Int],
offset: Option[Int]
)(implicit qr: Queryable.Row[Q, R])
extends scalasql.query.CompoundSelect(lhs, compoundOps, orderBy, limit, offset)
with Select[Q, R] {
override def take(n: Int): scalasql.query.Select[Q, R] = copy(
limit = Some(limit.fold(n)(math.min(_, n))),
offset = offset.orElse(Some(0))
)

protected override def selectRenderer(prevContext: Context): SubqueryRef.Wrapped.Renderer =
new CompoundSelectRenderer(this, prevContext)
}

class CompoundSelectRenderer[Q, R](
query: scalasql.query.CompoundSelect[Q, R],
prevContext: Context
) extends scalasql.query.CompoundSelect.Renderer(query, prevContext) {
override lazy val limitOpt = SqlStr.flatten(SqlStr.opt(query.limit) { limit =>
sql" FETCH FIRST $limit ROWS ONLY"
})

override lazy val offsetOpt = SqlStr.flatten(
SqlStr.opt(query.offset.orElse(Option.when(query.limit.nonEmpty)(0))) { offset =>
sql" OFFSET $offset ROWS"
}
)

override def render(liveExprs: LiveExprs): SqlStr = {
prerender(liveExprs) match {
case (lhsStr, compound, sortOpt, limitOpt, offsetOpt) =>
lhsStr + compound + sortOpt + offsetOpt + limitOpt
}
}

override def orderToSqlStr(newCtx: Context) = {
SqlStr.optSeq(query.orderBy) { orderBys =>
val orderStr = SqlStr.join(
orderBys.map { orderBy =>
val exprStr = Renderable.renderSql(orderBy.expr)(newCtx)

(orderBy.ascDesc, orderBy.nulls) match {
case (Some(AscDesc.Asc), None | Some(Nulls.First)) => sql"$exprStr ASC"
case (Some(AscDesc.Desc), Some(Nulls.First)) =>
sql"IIF($exprStr IS NULL, 0, 1), $exprStr DESC"
case (Some(AscDesc.Asc), Some(Nulls.Last)) =>
sql"IIF($exprStr IS NULL, 1, 0), $exprStr ASC"
case (Some(AscDesc.Desc), None | Some(Nulls.Last)) => sql"$exprStr DESC"
case (None, None) => exprStr
case (None, Some(Nulls.First)) => sql"IIF($exprStr IS NULL, 0, 1), $exprStr"
case (None, Some(Nulls.Last)) => sql"IIF($exprStr IS NULL, 1, 0), $exprStr"
}
},
SqlStr.commaSep
)

sql" ORDER BY $orderStr"
}
}
}
}
3 changes: 3 additions & 0 deletions scalasql/src/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,7 @@ package object scalasql {

val SqliteDialect = dialects.SqliteDialect
type SqliteDialect = dialects.SqliteDialect

val MsSqlDialect = dialects.MsSqlDialect
type MsSqlDialect = dialects.MsSqlDialect
}
Loading
Loading