From 6aec4a2874c4e5af7958cca1a9f6a0e0bf3f41a8 Mon Sep 17 00:00:00 2001 From: Tobias Pfeifer Date: Sun, 28 Mar 2021 21:38:39 +0200 Subject: [PATCH 1/6] Add 'format' function to PostgresModule #184 --- .../zio/sql/postgresql/PostgresModule.scala | 6 ++ .../zio/sql/postgresql/FunctionDefSpec.scala | 102 +++++++++++++++++- 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/postgres/src/main/scala/zio/sql/postgresql/PostgresModule.scala b/postgres/src/main/scala/zio/sql/postgresql/PostgresModule.scala index 3d00da854..e78ff2105 100644 --- a/postgres/src/main/scala/zio/sql/postgresql/PostgresModule.scala +++ b/postgres/src/main/scala/zio/sql/postgresql/PostgresModule.scala @@ -227,6 +227,12 @@ trait PostgresModule extends Jdbc { self => FunctionDef[Timestampz, Timestampz](FunctionName("make_timestamptz")) val Encode = FunctionDef[(Chunk[Byte], String), String](FunctionName("encode")) val Decode = FunctionDef[(String, String), Chunk[Byte]](FunctionName("decode")) + val Format0 = FunctionDef[String, String](FunctionName("format")) // TODO: varargs + val Format1 = FunctionDef[(String, Any), String](FunctionName("format")) + val Format2 = FunctionDef[(String, Any, Any), String](FunctionName("format")) + val Format3 = FunctionDef[(String, Any, Any, Any), String](FunctionName("format")) + val Format4 = FunctionDef[(String, Any, Any, Any, Any), String](FunctionName("format")) + val Format5 = FunctionDef[(String, Any, Any, Any, Any, Any), String](FunctionName("format")) } override def renderRead(read: self.Read[_]): String = { diff --git a/postgres/src/test/scala/zio/sql/postgresql/FunctionDefSpec.scala b/postgres/src/test/scala/zio/sql/postgresql/FunctionDefSpec.scala index e399fb3a5..5c1d946f7 100644 --- a/postgres/src/test/scala/zio/sql/postgresql/FunctionDefSpec.scala +++ b/postgres/src/test/scala/zio/sql/postgresql/FunctionDefSpec.scala @@ -151,7 +151,105 @@ object FunctionDefSpec extends PostgresRunnableSpec with ShopSchema { } yield assert(r.head)(equalTo(expected)) assertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) - } + }, + suite("format function")( + testM("format0") { + import Expr._ + + val query = select(Format0("Person")) from customers + + val expected = Seq( + "Person", + "Person", + "Person", + "Person", + "Person" + ) + + val testResult = execute(query.to[String, String](identity)) + collectAndCompare(expected, testResult) + }, + testM("format1") { + import Expr._ + + val query = select(Format1("Person: %s", Customers.fName)) from customers + + val expected = Seq( + "Person: Ronald", + "Person: Terrence", + "Person: Mila", + "Person: Alana", + "Person: Jose" + ) + + val testResult = execute(query.to[String, String](identity)) + collectAndCompare(expected, testResult) + }, + testM("format2") { + import Expr._ + + val query = select(Format2("Person: %s %s", Customers.fName, Customers.lName)) from customers + + val expected = Seq( + "Person: Ronald Russell", + "Person: Terrence Noel", + "Person: Mila Paterso", + "Person: Alana Murray", + "Person: Jose Wiggins" + ) + + val testResult = execute(query.to[String, String](identity)) + collectAndCompare(expected, testResult) + }, + testM("format3") { + import Expr._ + + val query = select(Format3("Person: %s %s with double quoted %I ", Customers.fName, Customers.lName, "identi fier")) from customers + + val expected = Seq( + s"""Person: Ronald Russell with double quoted "identi fier" """, + s"""Person: Terrence Noel with double quoted "identi fier" """, + s"""Person: Mila Paterso with double quoted "identi fier" """, + s"""Person: Alana Murray with double quoted "identi fier" """, + s"""Person: Jose Wiggins with double quoted "identi fier" """ + ) + + val testResult = execute(query.to[String, String](identity)) + collectAndCompare(expected, testResult) + }, + testM("format4") { + import Expr._ + + val query = select(Format4("Person: %s %s with null-literal %L and non-null-literal %L ", Customers.fName, Customers.lName, "FIXME: NULL", "literal")) from customers + + val expected = Seq( + s"""Person: Ronald Russell with null-literal 'FIXME: NULL' and non-null-literal 'literal' """, + s"""Person: Terrence Noel with null-literal 'FIXME: NULL' and non-null-literal 'literal' """, + s"""Person: Mila Paterso with null-literal 'FIXME: NULL' and non-null-literal 'literal' """, + s"""Person: Alana Murray with null-literal 'FIXME: NULL' and non-null-literal 'literal' """, + s"""Person: Jose Wiggins with null-literal 'FIXME: NULL' and non-null-literal 'literal' """ + ) + + val testResult = execute(query.to[String, String](identity)) + collectAndCompare(expected, testResult) + }, + testM("format5") { + import Expr._ + + val query = select(Format5("Person: %s %s with more arguments than placeholders: %I %L ", Customers.fName, Customers.lName, "identifier", Reverse(Customers.fName), "unused")) from customers + + val expected = Seq( + s"""Person: Ronald Russell with more arguments than placeholders: identifier 'dlanoR' """, + s"""Person: Terrence Noel with more arguments than placeholders: identifier 'ecnerreT' """, + s"""Person: Mila Paterso with more arguments than placeholders: identifier 'aliM' """, + s"""Person: Alana Murray with more arguments than placeholders: identifier 'analA' """, + s"""Person: Jose Wiggins with more arguments than placeholders: identifier 'esoJ' """ + ) + + val testResult = execute(query.to[String, String](identity)) + collectAndCompare(expected, testResult) + } + ) ), testM("abs") { val query = select(Abs(-3.14159)) from customers @@ -358,7 +456,7 @@ object FunctionDefSpec extends PostgresRunnableSpec with ShopSchema { r <- testResult.runCollect } yield assert(r.head)( matchesRegex( - "[A-Za-z]{3}\\s[A-Za-z]{3}\\s[0-9]{2}\\s(2[0-3]|[01][0-9]):[0-5][0-9]:[0-5][0-9].[0-9]{6}\\s[0-9]{4}\\s[A-Za-z]{3}" + "[A-Za-z]{3}\\s[A-Za-z]{3}\\s[0-9]{2}\\s(2[0-3]|[01][0-9]):[0-5][0-9]:[0-5][0-9].[0-9]{6}\\s[0-9]{4}\\s[A-Za-z]{3,4}" ) ) assertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) From b1671e5c1cd87bbfd9751238c75f2d978b56b00e Mon Sep 17 00:00:00 2001 From: Tobias Pfeifer Date: Sun, 28 Mar 2021 22:14:41 +0200 Subject: [PATCH 2/6] fix formatting --- .../zio/sql/postgresql/PostgresModule.scala | 12 ++++----- .../zio/sql/postgresql/FunctionDefSpec.scala | 25 ++++++++++++++++--- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/postgres/src/main/scala/zio/sql/postgresql/PostgresModule.scala b/postgres/src/main/scala/zio/sql/postgresql/PostgresModule.scala index e78ff2105..9cbdec184 100644 --- a/postgres/src/main/scala/zio/sql/postgresql/PostgresModule.scala +++ b/postgres/src/main/scala/zio/sql/postgresql/PostgresModule.scala @@ -227,12 +227,12 @@ trait PostgresModule extends Jdbc { self => FunctionDef[Timestampz, Timestampz](FunctionName("make_timestamptz")) val Encode = FunctionDef[(Chunk[Byte], String), String](FunctionName("encode")) val Decode = FunctionDef[(String, String), Chunk[Byte]](FunctionName("decode")) - val Format0 = FunctionDef[String, String](FunctionName("format")) // TODO: varargs - val Format1 = FunctionDef[(String, Any), String](FunctionName("format")) - val Format2 = FunctionDef[(String, Any, Any), String](FunctionName("format")) - val Format3 = FunctionDef[(String, Any, Any, Any), String](FunctionName("format")) - val Format4 = FunctionDef[(String, Any, Any, Any, Any), String](FunctionName("format")) - val Format5 = FunctionDef[(String, Any, Any, Any, Any, Any), String](FunctionName("format")) + val Format0 = FunctionDef[String, String](FunctionName("format")) // TODO: varargs + val Format1 = FunctionDef[(String, Any), String](FunctionName("format")) + val Format2 = FunctionDef[(String, Any, Any), String](FunctionName("format")) + val Format3 = FunctionDef[(String, Any, Any, Any), String](FunctionName("format")) + val Format4 = FunctionDef[(String, Any, Any, Any, Any), String](FunctionName("format")) + val Format5 = FunctionDef[(String, Any, Any, Any, Any, Any), String](FunctionName("format")) } override def renderRead(read: self.Read[_]): String = { diff --git a/postgres/src/test/scala/zio/sql/postgresql/FunctionDefSpec.scala b/postgres/src/test/scala/zio/sql/postgresql/FunctionDefSpec.scala index 5c1d946f7..fb900d8a3 100644 --- a/postgres/src/test/scala/zio/sql/postgresql/FunctionDefSpec.scala +++ b/postgres/src/test/scala/zio/sql/postgresql/FunctionDefSpec.scala @@ -204,7 +204,9 @@ object FunctionDefSpec extends PostgresRunnableSpec with ShopSchema { testM("format3") { import Expr._ - val query = select(Format3("Person: %s %s with double quoted %I ", Customers.fName, Customers.lName, "identi fier")) from customers + val query = select( + Format3("Person: %s %s with double quoted %I ", Customers.fName, Customers.lName, "identi fier") + ) from customers val expected = Seq( s"""Person: Ronald Russell with double quoted "identi fier" """, @@ -220,7 +222,15 @@ object FunctionDefSpec extends PostgresRunnableSpec with ShopSchema { testM("format4") { import Expr._ - val query = select(Format4("Person: %s %s with null-literal %L and non-null-literal %L ", Customers.fName, Customers.lName, "FIXME: NULL", "literal")) from customers + val query = select( + Format4( + "Person: %s %s with null-literal %L and non-null-literal %L ", + Customers.fName, + Customers.lName, + "FIXME: NULL", + "literal" + ) + ) from customers val expected = Seq( s"""Person: Ronald Russell with null-literal 'FIXME: NULL' and non-null-literal 'literal' """, @@ -236,7 +246,16 @@ object FunctionDefSpec extends PostgresRunnableSpec with ShopSchema { testM("format5") { import Expr._ - val query = select(Format5("Person: %s %s with more arguments than placeholders: %I %L ", Customers.fName, Customers.lName, "identifier", Reverse(Customers.fName), "unused")) from customers + val query = select( + Format5( + "Person: %s %s with more arguments than placeholders: %I %L ", + Customers.fName, + Customers.lName, + "identifier", + Reverse(Customers.fName), + "unused" + ) + ) from customers val expected = Seq( s"""Person: Ronald Russell with more arguments than placeholders: identifier 'dlanoR' """, From ce5281b3df49bc72de1d720442d751a117eee642 Mon Sep 17 00:00:00 2001 From: Regis Kuckaertz Date: Sun, 4 Apr 2021 18:02:30 +0100 Subject: [PATCH 3/6] Provide some more MySQL support --- README.md | 8 +- .../scala/zio/sql/mysql/MysqlModule.scala | 525 +++++++++++------- .../test/scala/zio/sql/mysql/DeleteSpec.scala | 24 + .../scala/zio/sql/mysql/FunctionDefSpec.scala | 68 ++- .../scala/zio/sql/mysql/TransactionSpec.scala | 65 +++ .../scala/zio/sql/postgresql/DeleteSpec.scala | 2 +- 6 files changed, 486 insertions(+), 206 deletions(-) create mode 100644 mysql/src/test/scala/zio/sql/mysql/DeleteSpec.scala create mode 100644 mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala diff --git a/README.md b/README.md index e05e9be00..475a6ec73 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,11 @@ Connection pool | :white_check_mark: Feature | PostgreSQL | SQL Server | Oracle | MySQL :------------ | :-------------| :-------------| :-------------| :------------- -Render Read | :white_check_mark: | :white_check_mark: | | -Render Delete | :white_check_mark: | | | -Render Update | :white_check_mark: | | | +Render Read | :white_check_mark: | :white_check_mark: | | :white_check_mark: | +Render Delete | :white_check_mark: | | | :white_check_mark: | +Render Update | :white_check_mark: | | | :white_check_mark: | Render Insert | | | | -Functions | :white_check_mark: | | | +Functions | :white_check_mark: | | | :white_check_mark: | Types | | | | Operators | | | | diff --git a/mysql/src/main/scala/zio/sql/mysql/MysqlModule.scala b/mysql/src/main/scala/zio/sql/mysql/MysqlModule.scala index 58945d58e..6e935144d 100644 --- a/mysql/src/main/scala/zio/sql/mysql/MysqlModule.scala +++ b/mysql/src/main/scala/zio/sql/mysql/MysqlModule.scala @@ -1,222 +1,341 @@ package zio.sql.mysql -import zio.sql.Jdbc +import zio.sql.{ Jdbc, Renderer } +import zio.Chunk +import java.time.Year +import java.sql.ResultSet trait MysqlModule extends Jdbc { self => + override type TypeTagExtension[+A] = MysqlSpecific.MysqlTypeTag[A] + + object MysqlSpecific { + trait MysqlTypeTag[+A] extends Tag[A] with Decodable[A] + + object MysqlTypeTag { + implicit case object TYear extends MysqlTypeTag[Year] { + override def decode(column: Either[Int, String], resultSet: ResultSet): Either[DecodingError, Year] = + scala.util + .Try(Year.of(column.fold(resultSet.getByte(_), resultSet.getByte(_)).toInt)) + .fold( + _ => Left(DecodingError.UnexpectedNull(column)), + r => Right(r) + ) + } + + } + } + object MysqlFunctionDef { - val Sind = FunctionDef[Double, Double](FunctionName("sind")) + val Crc32 = FunctionDef[String, Long](FunctionName("crc32")) + val Degrees = FunctionDef[Double, Double](FunctionName("degrees")) + val Log2 = FunctionDef[Double, Double](FunctionName("log2")) + val Log10 = FunctionDef[Double, Double](FunctionName("log10")) + val Pi = Expr.FunctionCall0[Double](FunctionDef[Any, Double](FunctionName("pi"))) } override def renderRead(read: self.Read[_]): String = { - val builder = new StringBuilder + implicit val render: Renderer = Renderer() + MysqlRenderModule.renderReadImpl(read) + render.toString + } + + override def renderDelete(delete: self.Delete[_]): String = { + implicit val render: Renderer = Renderer() + MysqlRenderModule.renderDeleteImpl(delete) + render.toString + } - def buildExpr[A, B](expr: self.Expr[_, A, B]): Unit = expr match { - case Expr.Source(tableName, column) => - val _ = builder.append(tableName).append(".").append(column.name) - case Expr.Unary(base, op) => - val _ = builder.append(" ").append(op.symbol) - buildExpr(base) - case Expr.Property(base, op) => - buildExpr(base) - val _ = builder.append(" ").append(op.symbol) - case Expr.Binary(left, right, op) => - buildExpr(left) - builder.append(" ").append(op.symbol).append(" ") - buildExpr(right) - case Expr.Relational(left, right, op) => - buildExpr(left) - builder.append(" ").append(op.symbol).append(" ") - buildExpr(right) - case Expr.In(value, set) => - buildExpr(value) - buildReadString(set) - case Expr.Literal(value) => - val _ = builder.append(value.toString) //todo fix escaping - case Expr.AggregationCall(param, aggregation) => - builder.append(aggregation.name.name) - builder.append("(") - buildExpr(param) - val _ = builder.append(")") - case Expr.ParenlessFunctionCall0(functionName) => - val _ = builder.append(functionName.name) - case Expr.FunctionCall0(function) => - builder.append(function.name.name) - builder.append("(") - val _ = builder.append(")") - case Expr.FunctionCall1(param, function) => - builder.append(function.name.name) - builder.append("(") - buildExpr(param) - val _ = builder.append(")") - case Expr.FunctionCall2(param1, param2, function) => - builder.append(function.name.name) - builder.append("(") - buildExpr(param1) - builder.append(",") - buildExpr(param2) - val _ = builder.append(")") - case Expr.FunctionCall3(param1, param2, param3, function) => - builder.append(function.name.name) - builder.append("(") - buildExpr(param1) - builder.append(",") - buildExpr(param2) - builder.append(",") - buildExpr(param3) - val _ = builder.append(")") - case Expr.FunctionCall4(param1, param2, param3, param4, function) => - builder.append(function.name.name) - builder.append("(") - buildExpr(param1) - builder.append(",") - buildExpr(param2) - builder.append(",") - buildExpr(param3) - builder.append(",") - buildExpr(param4) - val _ = builder.append(")") - case Expr.FunctionCall5(param1, param2, param3, param4, param5, function) => - builder.append(function.name.name) - builder.append("(") - buildExpr(param1) - builder.append(",") - buildExpr(param2) - builder.append(",") - buildExpr(param3) - builder.append(",") - buildExpr(param4) - builder.append(",") - buildExpr(param5) - val _ = builder.append(")") - case Expr.FunctionCall6(param1, param2, param3, param4, param5, param6, function) => - builder.append(function.name.name) - builder.append("(") - buildExpr(param1) - builder.append(",") - buildExpr(param2) - builder.append(",") - buildExpr(param3) - builder.append(",") - buildExpr(param4) - builder.append(",") - buildExpr(param5) - builder.append(",") - buildExpr(param6) - val _ = builder.append(")") - case Expr.FunctionCall7(param1, param2, param3, param4, param5, param6, param7, function) => - builder.append(function.name.name) - builder.append("(") - buildExpr(param1) - builder.append(",") - buildExpr(param2) - builder.append(",") - buildExpr(param3) - builder.append(",") - buildExpr(param4) - builder.append(",") - buildExpr(param5) - builder.append(",") - buildExpr(param6) - builder.append(",") - buildExpr(param7) - val _ = builder.append(")") + override def renderUpdate(update: self.Update[_]): String = { + implicit val render: Renderer = Renderer() + MysqlRenderModule.renderUpdateImpl(update) + render.toString + } + + object MysqlRenderModule { + def renderDeleteImpl(delete: Delete[_])(implicit render: Renderer) = { + render("DELETE FROM ") + renderTable(delete.table) + delete.whereExpr match { + case Expr.Literal(true) => () + case _ => + render(" WHERE ") + renderExpr(delete.whereExpr) + } } - def buildReadString(read: self.Read[_]): Unit = - read match { - case Read.Mapped(read, _) => buildReadString(read) + def renderUpdateImpl(update: Update[_])(implicit render: Renderer) = + update match { + case Update(table, set, whereExpr) => + render("UPDATE ") + renderTable(table) + render(" SET ") + renderSet(set) + render(" WHERE ") + renderExpr(whereExpr) + } + def renderReadImpl(read: self.Read[_])(implicit render: Renderer): Unit = + read match { + case Read.Mapped(read, _) => + renderReadImpl(read) case read0 @ Read.Select(_, _, _, _, _, _, _, _) => - object Dummy { - type F - type A - type B <: SelectionSet[A] - } + object Dummy { type F; type A; type B <: SelectionSet[A] } val read = read0.asInstanceOf[Read.Select[Dummy.F, Dummy.A, Dummy.B]] import read._ - builder.append("SELECT ") - buildSelection(selection.value) - builder.append(" FROM ") - buildTable(table) + render("SELECT ") + renderSelection(selection.value) + render(" FROM ") + renderTable(table) whereExpr match { case Expr.Literal(true) => () case _ => - builder.append(" WHERE ") - buildExpr(whereExpr) + render(" WHERE ") + renderExpr(whereExpr) } groupBy match { case _ :: _ => - builder.append(" GROUP BY ") - buildExprList(groupBy) + render(" GROUP BY ") + renderExprList(groupBy) havingExpr match { case Expr.Literal(true) => () case _ => - builder.append(" HAVING ") - buildExpr(havingExpr) + render(" HAVING ") + renderExpr(havingExpr) } case Nil => () } orderBy match { case _ :: _ => - builder.append(" ORDER BY ") - buildOrderingList(orderBy) + render(" ORDER BY ") + renderOrderingList(orderBy) case Nil => () } limit match { - case Some(limit) => - builder.append(" LIMIT ").append(limit) + case Some(limit) => render(" LIMIT ", limit) case None => () } offset match { - case Some(offset) => - val _ = builder.append(" OFFSET ").append(offset) + case Some(offset) => render(" OFFSET ", offset) case None => () } case Read.Union(left, right, distinct) => - buildReadString(left) - builder.append(" UNION ") - if (!distinct) builder.append("ALL ") - buildReadString(right) + renderReadImpl(left) + render(" UNION ") + if (!distinct) render("ALL ") + renderReadImpl(right) case Read.Literal(values) => - val _ = builder.append(" (").append(values.mkString(",")).append(") ") //todo fix needs escaping + render(" (", values.mkString(","), ") ") //todo fix needs escaping } - def buildExprList(expr: List[Expr[_, _, _]]): Unit = - expr match { + private def renderSet(set: List[Set[_, _]])(implicit render: Renderer): Unit = + set match { case head :: tail => - buildExpr(head) - tail match { - case _ :: _ => - builder.append(", ") - buildExprList(tail) - case Nil => () + renderExpr(head.lhs) + render(" = ") + renderExpr(head.rhs) + tail.foreach { setEq => + render(", ") + renderExpr(setEq.lhs) + render(" = ") + renderExpr(setEq.rhs) } - case Nil => () + case Nil => //TODO restrict Update to not allow empty set } - def buildOrderingList(expr: List[Ordering[Expr[_, _, _]]]): Unit = - expr match { - case head :: tail => - head match { - case Ordering.Asc(value) => buildExpr(value) - case Ordering.Desc(value) => - buildExpr(value) - builder.append(" DESC") - } - tail match { - case _ :: _ => - builder.append(", ") - buildOrderingList(tail) - case Nil => () + + private def renderTable(table: Table)(implicit render: Renderer): Unit = + table match { + //The outer reference in this type test cannot be checked at run time?! + case sourceTable: self.Table.Source => + render(sourceTable.name) + case Table.Joined(joinType, left, right, on) => + renderTable(left) + render(joinType match { + case JoinType.Inner => " INNER JOIN " + case JoinType.LeftOuter => " LEFT JOIN " + case JoinType.RightOuter => " RIGHT JOIN " + case JoinType.FullOuter => " OUTER JOIN " + }) + renderTable(right) + render(" ON ") + renderExpr(on) + render(" ") + } + + private def renderExpr[A, B](expr: self.Expr[_, A, B])(implicit render: Renderer): Unit = expr match { + case Expr.Source(tableName, column) => render(tableName, ".", column.name) + case Expr.Unary(base, op) => + render(" ", op.symbol) + renderExpr(base) + case Expr.Property(base, op) => + renderExpr(base) + render(" ", op.symbol) + case Expr.Binary(left, right, op) => + renderExpr(left) + render(" ", op.symbol, " ") + renderExpr(right) + case Expr.Relational(left, right, op) => + renderExpr(left) + render(" ", op.symbol, " ") + renderExpr(right) + case Expr.In(value, set) => + renderExpr(value) + renderReadImpl(set) + case lit: Expr.Literal[_] => renderLit(lit) + case Expr.AggregationCall(p, aggregation) => + render(aggregation.name.name, "(") + renderExpr(p) + render(")") + case Expr.ParenlessFunctionCall0(fn) => + val _ = render(fn.name) + case Expr.FunctionCall0(fn) => + render(fn.name.name) + render("(") + val _ = render(")") + case Expr.FunctionCall1(p, fn) => + render(fn.name.name, "(") + renderExpr(p) + render(")") + case Expr.FunctionCall2(p1, p2, fn) => + render(fn.name.name, "(") + renderExpr(p1) + render(",") + renderExpr(p2) + render(")") + case Expr.FunctionCall3(p1, p2, p3, fn) => + render(fn.name.name, "(") + renderExpr(p1) + render(",") + renderExpr(p2) + render(",") + renderExpr(p3) + render(")") + case Expr.FunctionCall4(p1, p2, p3, p4, fn) => + render(fn.name.name, "(") + renderExpr(p1) + render(",") + renderExpr(p2) + render(",") + renderExpr(p3) + render(",") + renderExpr(p4) + render(")") + case Expr.FunctionCall5(param1, param2, param3, param4, param5, function) => + render(function.name.name) + render("(") + renderExpr(param1) + render(",") + renderExpr(param2) + render(",") + renderExpr(param3) + render(",") + renderExpr(param4) + render(",") + renderExpr(param5) + render(")") + case Expr.FunctionCall6(param1, param2, param3, param4, param5, param6, function) => + render(function.name.name) + render("(") + renderExpr(param1) + render(",") + renderExpr(param2) + render(",") + renderExpr(param3) + render(",") + renderExpr(param4) + render(",") + renderExpr(param5) + render(",") + renderExpr(param6) + render(")") + case Expr.FunctionCall7( + param1, + param2, + param3, + param4, + param5, + param6, + param7, + function + ) => + render(function.name.name) + render("(") + renderExpr(param1) + render(",") + renderExpr(param2) + render(",") + renderExpr(param3) + render(",") + renderExpr(param4) + render(",") + renderExpr(param5) + render(",") + renderExpr(param6) + render(",") + renderExpr(param7) + render(")") + } + + private def renderLit[A, B](lit: self.Expr.Literal[_])(implicit render: Renderer): Unit = { + import MysqlSpecific.MysqlTypeTag._ + import TypeTag._ + lit.typeTag match { + case TDialectSpecific(tt) => + tt match { + case tt @ TYear => + render(tt.cast(lit.value)) + case _: MysqlSpecific.MysqlTypeTag[_] => ??? } - case Nil => () + case TByteArray => + render( + lit.value.asInstanceOf[Chunk[Byte]].map("""\%02X""" format _).mkString("x'", "", "'") + ) // todo fix `cast` infers correctly but map doesn't work for some reason + case tt @ TChar => + render("'", tt.cast(lit.value), "'") //todo is this the same as a string? fix escaping + case tt @ TInstant => + render("TIMESTAMP '", tt.cast(lit.value), "'") + case tt @ TLocalDate => + render("DATE '", tt.cast(lit.value), "'") + case tt @ TLocalDateTime => + render("DATE '", tt.cast(lit.value), "'") + case tt @ TLocalTime => + render("TIME '", tt.cast(lit.value), "'") + case tt @ TOffsetDateTime => + render("DATE '", tt.cast(lit.value), "'") + case tt @ TOffsetTime => + render("TIME '", tt.cast(lit.value), "'") + case tt @ TUUID => + render("'", tt.cast(lit.value), "'") + case tt @ TZonedDateTime => + render("DATE '", tt.cast(lit.value), "'") + case TByte => + render(lit.value) + case TBigDecimal => + render(lit.value) + case TBoolean => + render(lit.value) + case TDouble => + render(lit.value) + case TFloat => + render(lit.value) + case TInt => + render(lit.value) + case TLong => + render(lit.value) + case TShort => + render(lit.value) + case TString => + render("'", lit.value, "'") //todo fix escaping + case _ => + render(lit.value) //todo fix add TypeTag.Nullable[_] => } + } - def buildSelection[A](selectionSet: SelectionSet[A]): Unit = + private def renderSelection[A](selectionSet: SelectionSet[A])(implicit render: Renderer): Unit = selectionSet match { case cons0 @ SelectionSet.Cons(_, _) => object Dummy { @@ -226,59 +345,65 @@ trait MysqlModule extends Jdbc { self => } val cons = cons0.asInstanceOf[SelectionSet.Cons[Dummy.Source, Dummy.A, Dummy.B]] import cons._ - buildColumnSelection(head) + renderColumnSelection(head) if (tail != SelectionSet.Empty) { - builder.append(", ") - buildSelection(tail) + render(", ") + renderSelection(tail) } case SelectionSet.Empty => () } - def buildColumnSelection[A, B](columnSelection: ColumnSelection[A, B]): Unit = + private def renderColumnSelection[A, B](columnSelection: ColumnSelection[A, B])(implicit render: Renderer): Unit = columnSelection match { case ColumnSelection.Constant(value, name) => - builder.append(value.toString()) //todo fix escaping + render(value) //todo fix escaping name match { - case Some(name) => - val _ = builder.append(" AS ").append(name) + case Some(name) => render(" AS ", name) case None => () } case ColumnSelection.Computed(expr, name) => - buildExpr(expr) + renderExpr(expr) name match { case Some(name) => Expr.exprName(expr) match { - case Some(sourceName) if name != sourceName => - val _ = builder.append(" AS ").append(name) + case Some(sourceName) if name != sourceName => render(" AS ", name) case _ => () } case _ => () //todo what do we do if we don't have a name? } } - def buildTable(table: Table): Unit = - table match { - //The outer reference in this type test cannot be checked at run time?! - case sourceTable: self.Table.Source => - val _ = builder.append(sourceTable.name) - case Table.Joined(joinType, left, right, on) => - buildTable(left) - builder.append(joinType match { - case JoinType.Inner => " INNER JOIN " - case JoinType.LeftOuter => " LEFT JOIN " - case JoinType.RightOuter => " RIGHT JOIN " - case JoinType.FullOuter => " OUTER JOIN " - }) - buildTable(right) - builder.append(" ON ") - buildExpr(on) - val _ = builder.append(" ") - } - buildReadString(read) - builder.toString() - } - override def renderDelete(delete: self.Delete[_]): String = ??? + private def renderExprList(expr: List[Expr[_, _, _]])(implicit render: Renderer): Unit = + expr match { + case head :: tail => + renderExpr(head) + tail match { + case _ :: _ => + render(", ") + renderExprList(tail) + case Nil => () + } + case Nil => () + } - override def renderUpdate(update: self.Update[_]): String = ??? + def renderOrderingList(expr: List[Ordering[Expr[_, _, _]]])(implicit render: Renderer): Unit = + expr match { + case head :: tail => + head match { + case Ordering.Asc(value) => + renderExpr(value) + case Ordering.Desc(value) => + renderExpr(value) + render(" DESC") + } + tail match { + case _ :: _ => + render(", ") + renderOrderingList(tail) + case Nil => () + } + case Nil => () + } + } } diff --git a/mysql/src/test/scala/zio/sql/mysql/DeleteSpec.scala b/mysql/src/test/scala/zio/sql/mysql/DeleteSpec.scala new file mode 100644 index 000000000..50ae73d95 --- /dev/null +++ b/mysql/src/test/scala/zio/sql/mysql/DeleteSpec.scala @@ -0,0 +1,24 @@ +package zio.sql.mysql + +import zio.Cause +import zio.test.Assertion._ +import zio.test._ + +object DeleteSpec extends MysqlRunnableSpec with ShopSchema { + + import Customers._ + + override def specLayered = suite("Postgres module delete")( + testM("Can delete from single table with a condition") { + val query = deleteFrom(customers).where(verified.isNotTrue) + + val result = execute(query) + + val assertion = for { + r <- result + } yield assert(r)(equalTo(1)) + + assertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) + } + ) +} diff --git a/mysql/src/test/scala/zio/sql/mysql/FunctionDefSpec.scala b/mysql/src/test/scala/zio/sql/mysql/FunctionDefSpec.scala index 8a748ae72..70dd9f969 100644 --- a/mysql/src/test/scala/zio/sql/mysql/FunctionDefSpec.scala +++ b/mysql/src/test/scala/zio/sql/mysql/FunctionDefSpec.scala @@ -8,10 +8,11 @@ object FunctionDefSpec extends MysqlRunnableSpec with ShopSchema { import Customers._ import FunctionDef._ + import MysqlFunctionDef._ override def specLayered = suite("Mysql FunctionDef")( testM("lower") { - val query = select(Lower("first_name")) from customers limit (1) + val query = select(Lower(fName)) from customers limit (1) val expected = "ronald" @@ -47,6 +48,71 @@ object FunctionDefSpec extends MysqlRunnableSpec with ShopSchema { r <- testResult.runCollect } yield assert(r.head)(equalTo(expected)) + assertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) + }, + testM("crc32") { + val query = select(Crc32("MySQL")) from customers + + val expected = 3259397556L + + val testResult = execute(query.to[Long, Long](identity)) + + val assertion = for { + r <- testResult.runCollect + } yield assert(r.head)(equalTo(expected)) + + assertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) + }, + testM("degrees") { + val query = select(Degrees(Math.PI)) from customers + + val expected = 180d + + val testResult = execute(query.to[Double, Double](identity)) + + val assertion = for { + r <- testResult.runCollect + } yield assert(r.head)(equalTo(expected)) + + assertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) + }, + testM("log2") { + val query = select(Log2(8d)) from customers + + val expected = 3d + + val testResult = execute(query.to[Double, Double](identity)) + + val assertion = for { + r <- testResult.runCollect + } yield assert(r.head)(equalTo(expected)) + + assertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) + }, + testM("log10") { + val query = select(Log10(1000000d)) from customers + + val expected = 6d + + val testResult = execute(query.to[Double, Double](identity)) + + val assertion = for { + r <- testResult.runCollect + } yield assert(r.head)(equalTo(expected)) + + assertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) + }, + testM("pi") { + val query = select(Pi) from customers + + val expected = 3.141593d + + val testResult = execute(query.to[Double, Double](identity)) + + val assertion = for { + r <- testResult.runCollect + } yield assert(r.head)(equalTo(expected)) + assertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) } ) diff --git a/mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala b/mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala new file mode 100644 index 000000000..adac195f0 --- /dev/null +++ b/mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala @@ -0,0 +1,65 @@ +package zio.sql.mysql + +import java.util.UUID + +import zio._ +import zio.test.Assertion._ +import zio.test._ +import zio.test.TestAspect.sequential +import zio.sql.mysql.MysqlRunnableSpec + +object TransactionSpec extends MysqlRunnableSpec with ShopSchema { + + import Customers._ + + override def specLayered = suite("Postgres module")( + testM("Transaction is returning the last value") { + val query = select(customerId) from customers + + val result = execute( + ZTransaction(query) *> ZTransaction(query) + ).use(ZIO.succeed(_)) + + val assertion = assertM(result.flatMap(_.runCount))(equalTo(5L)).orDie + + assertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) + }, + testM("Transaction is failing") { + val query = select(customerId) from customers + + val result = execute( + ZTransaction(query) *> ZTransaction.fail(new Exception("failing")) *> ZTransaction(query) + ).mapError(_.getMessage).use(ZIO.succeed(_)) + + assertM(result.flip)(equalTo("failing")).mapErrorCause(cause => Cause.stackless(cause.untraced)) + }, + testM("Transaction failed and didn't deleted rows") { + val query = select(customerId) from customers + val deleteQuery = deleteFrom(customers).where(verified === false) + + val result = (for { + allCustomersCount <- execute(query.to(identity[UUID](_))).runCount.toManaged_ + _ <- execute( + ZTransaction(deleteQuery) *> ZTransaction.fail(new Exception("this is error")) *> ZTransaction(query) + ).catchAllCause(_ => ZManaged.succeed("continue")) + remainingCustomersCount <- execute(query.to(identity[UUID](_))).runCount.toManaged_ + } yield (allCustomersCount, remainingCustomersCount)).use(ZIO.succeed(_)) + + assertM(result)(equalTo((5L, 5L))).mapErrorCause(cause => Cause.stackless(cause.untraced)) + }, + testM("Transaction succeeded and deleted rows") { + val query = select(customerId) from customers + val deleteQuery = deleteFrom(customers).where(verified === false) + + val tx = ZTransaction(deleteQuery) + + val result = (for { + allCustomersCount <- execute(query.to(identity[UUID](_))).runCount.toManaged_ + _ <- execute(tx) + remainingCustomersCount <- execute(query.to(identity[UUID](_))).runCount.toManaged_ + } yield (allCustomersCount, remainingCustomersCount)).use(ZIO.succeed(_)) + + assertM(result)(equalTo((5L, 4L))).mapErrorCause(cause => Cause.stackless(cause.untraced)) + } + ) @@ sequential +} diff --git a/postgres/src/test/scala/zio/sql/postgresql/DeleteSpec.scala b/postgres/src/test/scala/zio/sql/postgresql/DeleteSpec.scala index f55160b9b..b2982352d 100644 --- a/postgres/src/test/scala/zio/sql/postgresql/DeleteSpec.scala +++ b/postgres/src/test/scala/zio/sql/postgresql/DeleteSpec.scala @@ -1,4 +1,4 @@ -package zio.sql.postgresql +package zio.sql.mysql import zio.Cause import zio.test.Assertion._ From c5e80c29cca188c3e41f3dd77d2745515ed0bde5 Mon Sep 17 00:00:00 2001 From: Regis Kuckaertz Date: Sun, 4 Apr 2021 18:04:17 +0100 Subject: [PATCH 4/6] naming --- mysql/src/test/scala/zio/sql/mysql/DeleteSpec.scala | 2 +- mysql/src/test/scala/zio/sql/mysql/FunctionDefSpec.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mysql/src/test/scala/zio/sql/mysql/DeleteSpec.scala b/mysql/src/test/scala/zio/sql/mysql/DeleteSpec.scala index 50ae73d95..2586cf91d 100644 --- a/mysql/src/test/scala/zio/sql/mysql/DeleteSpec.scala +++ b/mysql/src/test/scala/zio/sql/mysql/DeleteSpec.scala @@ -8,7 +8,7 @@ object DeleteSpec extends MysqlRunnableSpec with ShopSchema { import Customers._ - override def specLayered = suite("Postgres module delete")( + override def specLayered = suite("MySQL module delete")( testM("Can delete from single table with a condition") { val query = deleteFrom(customers).where(verified.isNotTrue) diff --git a/mysql/src/test/scala/zio/sql/mysql/FunctionDefSpec.scala b/mysql/src/test/scala/zio/sql/mysql/FunctionDefSpec.scala index 70dd9f969..40eec8652 100644 --- a/mysql/src/test/scala/zio/sql/mysql/FunctionDefSpec.scala +++ b/mysql/src/test/scala/zio/sql/mysql/FunctionDefSpec.scala @@ -10,7 +10,7 @@ object FunctionDefSpec extends MysqlRunnableSpec with ShopSchema { import FunctionDef._ import MysqlFunctionDef._ - override def specLayered = suite("Mysql FunctionDef")( + override def specLayered = suite("MySQL FunctionDef")( testM("lower") { val query = select(Lower(fName)) from customers limit (1) From 3f66b2d0100343971e45a33f8be6e66cb44be355 Mon Sep 17 00:00:00 2001 From: Regis Kuckaertz Date: Sun, 4 Apr 2021 18:14:24 +0100 Subject: [PATCH 5/6] woops --- mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala | 1 - postgres/src/test/scala/zio/sql/postgresql/DeleteSpec.scala | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala b/mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala index adac195f0..eed63da2b 100644 --- a/mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala +++ b/mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala @@ -6,7 +6,6 @@ import zio._ import zio.test.Assertion._ import zio.test._ import zio.test.TestAspect.sequential -import zio.sql.mysql.MysqlRunnableSpec object TransactionSpec extends MysqlRunnableSpec with ShopSchema { diff --git a/postgres/src/test/scala/zio/sql/postgresql/DeleteSpec.scala b/postgres/src/test/scala/zio/sql/postgresql/DeleteSpec.scala index b2982352d..f55160b9b 100644 --- a/postgres/src/test/scala/zio/sql/postgresql/DeleteSpec.scala +++ b/postgres/src/test/scala/zio/sql/postgresql/DeleteSpec.scala @@ -1,4 +1,4 @@ -package zio.sql.mysql +package zio.sql.postgresql import zio.Cause import zio.test.Assertion._ From 78c8c6673f6176107539019d564325a42ae54e55 Mon Sep 17 00:00:00 2001 From: Regis Kuckaertz <629976+regiskuckaertz@users.noreply.github.com> Date: Wed, 7 Apr 2021 08:26:20 +0100 Subject: [PATCH 6/6] Update mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala --- mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala b/mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala index eed63da2b..b1e01fe41 100644 --- a/mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala +++ b/mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala @@ -11,7 +11,7 @@ object TransactionSpec extends MysqlRunnableSpec with ShopSchema { import Customers._ - override def specLayered = suite("Postgres module")( + override def specLayered = suite("MySQL module")( testM("Transaction is returning the last value") { val query = select(customerId) from customers