From 3aad380777665469631bb431f536b74b624382ef Mon Sep 17 00:00:00 2001 From: Jakub Czuchnowski Date: Tue, 28 Jun 2022 22:44:16 +0200 Subject: [PATCH 01/10] Update zio to 2.0.0 and zio-schema to 0.2.0 (#710) --- build.sbt | 4 ++-- .../test/scala/zio/sql/postgresql/PostgresSqlModuleSpec.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index b89b9e329..f77d228c6 100644 --- a/build.sbt +++ b/build.sbt @@ -23,8 +23,8 @@ addCommandAlias("fmtOnce", "all scalafmtSbt scalafmt test:scalafmt") addCommandAlias("fmt", "fmtOnce;fmtOnce") addCommandAlias("check", "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck") -val zioVersion = "2.0.0-RC6" -val zioSchemaVersion = "0.1.9" +val zioVersion = "2.0.0" +val zioSchemaVersion = "0.2.0" val testcontainersVersion = "1.17.2" val testcontainersScalaVersion = "0.40.8" val logbackVersion = "1.2.11" diff --git a/postgres/src/test/scala/zio/sql/postgresql/PostgresSqlModuleSpec.scala b/postgres/src/test/scala/zio/sql/postgresql/PostgresSqlModuleSpec.scala index eb78ed3f1..f5e47982d 100644 --- a/postgres/src/test/scala/zio/sql/postgresql/PostgresSqlModuleSpec.scala +++ b/postgres/src/test/scala/zio/sql/postgresql/PostgresSqlModuleSpec.scala @@ -641,7 +641,7 @@ object PostgresSqlModuleSpec extends PostgresRunnableSpec with DbSchema { .values((UUID.randomUUID(), "Charles", "Dent", Some(LocalDate.of(2022, 1, 31)))) val insertNone = insertInto(persons)(personId, fName, lName, dob) - .values((UUID.randomUUID(), "Martin", "Harvey", None)) + .values((UUID.randomUUID(), "Martin", "Harvey", Option.empty[LocalDate])) val insertNone2 = insertInto(persons)(personId, fName, lName, dob) .values(personValue) From e4c54c4d04e66737b4cdc6c7dfab0bf429a9141f Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow <16665913+mschuwalow@users.noreply.github.com> Date: Tue, 28 Jun 2022 22:56:24 +0200 Subject: [PATCH 02/10] Add SqlTransaction type (#692) * Add SqlTransaction type * fix test * fix conflicts Co-authored-by: Jakub Czuchnowski --- .../main/scala/zio/sql/ExprSyntaxModule.scala | 27 ++++ .../scala/zio/sql/SqlDriverLiveModule.scala | 24 +++- .../scala/zio/sql/TransactionModule.scala | 130 ------------------ jdbc/src/main/scala/zio/sql/jdbc.scala | 26 ++-- .../scala/zio/sql/mysql/TransactionSpec.scala | 22 +-- .../zio/sql/postgresql/TransactionSpec.scala | 27 ++-- 6 files changed, 80 insertions(+), 176 deletions(-) create mode 100644 jdbc/src/main/scala/zio/sql/ExprSyntaxModule.scala delete mode 100644 jdbc/src/main/scala/zio/sql/TransactionModule.scala diff --git a/jdbc/src/main/scala/zio/sql/ExprSyntaxModule.scala b/jdbc/src/main/scala/zio/sql/ExprSyntaxModule.scala new file mode 100644 index 000000000..5092fbaf6 --- /dev/null +++ b/jdbc/src/main/scala/zio/sql/ExprSyntaxModule.scala @@ -0,0 +1,27 @@ +package zio.sql + +import zio._ +import zio.stream.ZStream +import zio.schema.Schema + +trait ExprSyntaxModule { self: Jdbc => + implicit final class ReadSyntax[A](self: Read[A]) { + def run: ZStream[SqlTransaction, Exception, A] = + ZStream.serviceWithStream(_.read(self)) + } + + implicit final class DeleteSyntax(self: Delete[_]) { + def run: ZIO[SqlTransaction, Exception, Int] = + ZIO.serviceWithZIO(_.delete(self)) + } + + implicit final class InsertSyntax[A: Schema](self: Insert[_, A]) { + def run: ZIO[SqlTransaction, Exception, Int] = + ZIO.serviceWithZIO(_.insert(self)) + } + + implicit final class UpdatedSyntax(self: Update[_]) { + def run: ZIO[SqlTransaction, Exception, Int] = + ZIO.serviceWithZIO(_.update(self)) + } +} diff --git a/jdbc/src/main/scala/zio/sql/SqlDriverLiveModule.scala b/jdbc/src/main/scala/zio/sql/SqlDriverLiveModule.scala index 716bb6cce..e282d14d7 100644 --- a/jdbc/src/main/scala/zio/sql/SqlDriverLiveModule.scala +++ b/jdbc/src/main/scala/zio/sql/SqlDriverLiveModule.scala @@ -5,6 +5,7 @@ import java.sql._ import zio._ import zio.stream.{ Stream, ZStream } import zio.schema.Schema +import zio.IO trait SqlDriverLiveModule { self: Jdbc => private[sql] trait SqlDriverCore { @@ -128,13 +129,28 @@ trait SqlDriverLiveModule { self: Jdbc => def insert[A: Schema](insert: List[Insert[_, A]]): IO[Exception, List[Int]] = ZIO.scoped(pool.connection.flatMap(insertOnBatch(insert, _))) - override def transact[R, A](tx: ZTransaction[R, Exception, A]): ZIO[R, Throwable, A] = - ZIO.scoped[R] { + override def transaction: ZLayer[Any, Exception, SqlTransaction] = + ZLayer.scoped { for { connection <- pool.connection _ <- ZIO.attemptBlocking(connection.setAutoCommit(false)).refineToOrDie[Exception] - a <- tx.run(Txn(connection, self)) - } yield a + _ <- ZIO.addFinalizerExit(c => + ZIO.attempt(if (c.isSuccess) connection.commit() else connection.rollback()).ignore + ) + } yield new SqlTransaction { + def delete(delete: Delete[_]): IO[Exception, Int] = + deleteOn(delete, connection) + + def update(update: Update[_]): IO[Exception, Int] = + updateOn(update, connection) + + def read[A](read: Read[A]): Stream[Exception, A] = + readOn(read, connection) + + def insert[A: Schema](insert: Insert[_, A]): IO[Exception, Int] = + insertOn(insert, connection) + + } } } } diff --git a/jdbc/src/main/scala/zio/sql/TransactionModule.scala b/jdbc/src/main/scala/zio/sql/TransactionModule.scala deleted file mode 100644 index c33e60bb6..000000000 --- a/jdbc/src/main/scala/zio/sql/TransactionModule.scala +++ /dev/null @@ -1,130 +0,0 @@ -package zio.sql - -import java.sql._ - -import zio.{ Tag => ZTag, _ } -import zio.stream._ -import zio.schema.Schema - -trait TransactionModule { self: Jdbc => - private[sql] sealed case class Txn(connection: Connection, sqlDriverCore: SqlDriverCore) - - sealed case class ZTransaction[-R: ZTag, +E, +A](unwrap: ZIO[(R, Txn), E, A]) { self => - def map[B](f: A => B): ZTransaction[R, E, B] = - ZTransaction(self.unwrap.map(f)) - - def flatMap[R1 <: R: ZTag, E1 >: E, B]( - f: A => ZTransaction[R1, E1, B] - ): ZTransaction[R1, E1, B] = - ZTransaction(self.unwrap.flatMap(a => f(a).unwrap)) - - private[sql] def run(txn: Txn)(implicit - ev: E <:< Throwable - ): ZIO[R, Throwable, A] = - for { - r <- ZIO.environment[R] - a <- self.unwrap - .mapError(ev) - .provideLayer(ZLayer.succeed((r.get, txn))) - .absorb - .tapBoth( - _ => - ZIO - .attemptBlocking(txn.connection.rollback()) - .refineToOrDie[Throwable], - _ => - ZIO - .attemptBlocking(txn.connection.commit()) - .refineToOrDie[Throwable] - ) - } yield a - - def zip[R1 <: R: ZTag, E1 >: E, B](tx: ZTransaction[R1, E1, B]): ZTransaction[R1, E1, (A, B)] = - zipWith[R1, E1, B, (A, B)](tx)((_, _)) - - def zipWith[R1 <: R: ZTag, E1 >: E, B, C]( - tx: ZTransaction[R1, E1, B] - )(f: (A, B) => C): ZTransaction[R1, E1, C] = - for { - a <- self - b <- tx - } yield f(a, b) - - def *>[R1 <: R: ZTag, E1 >: E, B](tx: ZTransaction[R1, E1, B]): ZTransaction[R1, E1, B] = - self.flatMap(_ => tx) - - // named alias for *> - def zipRight[R1 <: R: ZTag, E1 >: E, B](tx: ZTransaction[R1, E1, B]): ZTransaction[R1, E1, B] = - self *> tx - - def <*[R1 <: R: ZTag, E1 >: E, B](tx: ZTransaction[R1, E1, B]): ZTransaction[R1, E1, A] = - self.flatMap(a => tx.map(_ => a)) - - // named alias for <* - def zipLeft[R1 <: R: ZTag, E1 >: E, B](tx: ZTransaction[R1, E1, B]): ZTransaction[R1, E1, A] = - self <* tx - - def catchAllCause[R1 <: R: ZTag, E1 >: E, A1 >: A]( - f: Cause[E1] => ZTransaction[R1, E1, A1] - ): ZTransaction[R1, E1, A1] = - ZTransaction(self.unwrap.catchAllCause(cause => f(cause).unwrap)) - } - - object ZTransaction { - def apply[A]( - read: self.Read[A] - ): ZTransaction[Any, Exception, zio.stream.Stream[Exception, A]] = - txn.flatMap { case Txn(connection, coreDriver) => - // FIXME: Find a way to NOT load the whole result set into memory at once!!! - val stream = - coreDriver.readOn[A](read, connection) - - ZTransaction.fromEffect(stream.runCollect.map(ZStream.fromIterable(_))) - } - - def apply(update: self.Update[_]): ZTransaction[Any, Exception, Int] = - txn.flatMap { case Txn(connection, coreDriver) => - ZTransaction.fromEffect(coreDriver.updateOn(update, connection)) - } - - def batchUpdate(update: List[self.Update[_]]): ZTransaction[Any, Exception, List[Int]] = - txn.flatMap { case Txn(connection, coreDriver) => - ZTransaction.fromEffect(coreDriver.updateOnBatch(update, connection)) - } - - def apply[Z: Schema](insert: self.Insert[_, Z]): ZTransaction[Any, Exception, Int] = - txn.flatMap { case Txn(connection, coreDriver) => - ZTransaction.fromEffect(coreDriver.insertOn(insert, connection)) - } - - def batchInsert[Z: Schema](insert: List[self.Insert[_, Z]]): ZTransaction[Any, Exception, List[Int]] = - txn.flatMap { case Txn(connection, coreDriver) => - ZTransaction.fromEffect(coreDriver.insertOnBatch(insert, connection)) - } - - def apply(delete: self.Delete[_]): ZTransaction[Any, Exception, Int] = - txn.flatMap { case Txn(connection, coreDriver) => - ZTransaction.fromEffect(coreDriver.deleteOn(delete, connection)) - } - - def batchDelete(delete: List[self.Delete[_]]): ZTransaction[Any, Exception, List[Int]] = - txn.flatMap { case Txn(connection, coreDriver) => - ZTransaction.fromEffect(coreDriver.deleteOnBatch(delete, connection)) - } - - def succeed[A](a: => A): ZTransaction[Any, Nothing, A] = fromEffect(ZIO.succeed(a)) - - def fail[E](e: => E): ZTransaction[Any, E, Nothing] = fromEffect(ZIO.fail(e)) - - def halt[E](e: => Cause[E]): ZTransaction[Any, E, Nothing] = fromEffect(ZIO.failCause(e)) - - def fromEffect[R: ZTag, E, A](zio: ZIO[R, E, A]): ZTransaction[R, E, A] = - ZTransaction(for { - tuple <- ZIO.service[(R, Txn)] - a <- zio.provideLayer(ZLayer.succeed((tuple._1))) - } yield a) - - private val txn: ZTransaction[Any, Nothing, Txn] = - ZTransaction(ZIO.service[(Any, Txn)].map(_._2)) - } -} diff --git a/jdbc/src/main/scala/zio/sql/jdbc.scala b/jdbc/src/main/scala/zio/sql/jdbc.scala index cd4f6c044..1c69d7f94 100644 --- a/jdbc/src/main/scala/zio/sql/jdbc.scala +++ b/jdbc/src/main/scala/zio/sql/jdbc.scala @@ -1,10 +1,10 @@ package zio.sql -import zio.{ Tag => ZTag, _ } +import zio._ import zio.stream._ import zio.schema.Schema -trait Jdbc extends zio.sql.Sql with TransactionModule with JdbcInternalModule with SqlDriverLiveModule { +trait Jdbc extends zio.sql.Sql with JdbcInternalModule with SqlDriverLiveModule with ExprSyntaxModule { trait SqlDriver { def delete(delete: Delete[_]): IO[Exception, Int] @@ -16,11 +16,11 @@ trait Jdbc extends zio.sql.Sql with TransactionModule with JdbcInternalModule wi def read[A](read: Read[A]): Stream[Exception, A] - def transact[R, A](tx: ZTransaction[R, Exception, A]): ZIO[R, Throwable, A] - def insert[A: Schema](insert: Insert[_, A]): IO[Exception, Int] def insert[A: Schema](insert: List[Insert[_, A]]): IO[Exception, List[Int]] + + def transaction: ZLayer[Any, Exception, SqlTransaction] } object SqlDriver { @@ -28,10 +28,17 @@ trait Jdbc extends zio.sql.Sql with TransactionModule with JdbcInternalModule wi ZLayer(ZIO.serviceWith[ConnectionPool](new SqlDriverLive(_))) } - def execute[R <: SqlDriver: ZTag, A]( - tx: ZTransaction[R, Exception, A] - ): ZIO[R, Throwable, A] = - ZIO.serviceWithZIO(_.transact(tx)) + trait SqlTransaction { + + def delete(delete: Delete[_]): IO[Exception, Int] + + def update(update: Update[_]): IO[Exception, Int] + + def read[A](read: Read[A]): Stream[Exception, A] + + def insert[A: Schema](insert: Insert[_, A]): IO[Exception, Int] + + } def execute[A](read: Read[A]): ZStream[SqlDriver, Exception, A] = ZStream.serviceWithStream(_.read(read)) @@ -53,4 +60,7 @@ trait Jdbc extends zio.sql.Sql with TransactionModule with JdbcInternalModule wi def executeBatchUpdate(update: List[Update[_]]): ZIO[SqlDriver, Exception, List[Int]] = ZIO.serviceWithZIO(_.update(update)) + + val transact: ZLayer[SqlDriver, Exception, SqlTransaction] = + ZLayer(ZIO.serviceWith[SqlDriver](_.transaction)).flatten } diff --git a/mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala b/mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala index 77ca17f6c..8ec641761 100644 --- a/mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala +++ b/mysql/src/test/scala/zio/sql/mysql/TransactionSpec.scala @@ -15,35 +15,25 @@ object TransactionSpec extends MysqlRunnableSpec with ShopSchema { test("Transaction is returning the last value") { val query = select(customerId) from customers - val result = execute( - ZTransaction(query) *> ZTransaction(query) + val result = transact( + query.run.runCount *> query.run.runCount ) val assertion = result - .flatMap(_.runCount) .map(count => assertTrue(count == 5)) .orDie assertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) }, - test("Transaction is failing") { - val query = select(customerId) from customers - - val result = execute( - ZTransaction(query) *> ZTransaction.fail(new Exception("failing")) *> ZTransaction(query) - ).mapError(_.getMessage) - - assertZIO(result.flip)(equalTo("failing")).mapErrorCause(cause => Cause.stackless(cause.untraced)) - }, test("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).map(identity[UUID](_)).runCount - _ <- execute( - ZTransaction(deleteQuery) *> ZTransaction.fail(new Exception("this is error")) *> ZTransaction(query) + _ <- transact( + deleteQuery.run *> ZIO.fail(new Exception("this is error")) *> query.run.runCount ).catchAllCause(_ => ZIO.unit) remainingCustomersCount <- execute(query).map(identity[UUID](_)).runCount } yield (allCustomersCount, remainingCustomersCount)) @@ -54,11 +44,11 @@ object TransactionSpec extends MysqlRunnableSpec with ShopSchema { val query = select(customerId) from customers val deleteQuery = deleteFrom(customers).where(verified === false) - val tx = ZTransaction(deleteQuery) + val tx = deleteQuery.run val result = (for { allCustomersCount <- execute(query).map(identity[UUID](_)).runCount - _ <- execute(tx) + _ <- transact(tx) remainingCustomersCount <- execute(query).map(identity[UUID](_)).runCount } yield (allCustomersCount, remainingCustomersCount)) diff --git a/postgres/src/test/scala/zio/sql/postgresql/TransactionSpec.scala b/postgres/src/test/scala/zio/sql/postgresql/TransactionSpec.scala index afdc90583..6b1614fb2 100644 --- a/postgres/src/test/scala/zio/sql/postgresql/TransactionSpec.scala +++ b/postgres/src/test/scala/zio/sql/postgresql/TransactionSpec.scala @@ -15,32 +15,23 @@ object TransactionSpec extends PostgresRunnableSpec with DbSchema { test("Transaction is returning the last value") { val query = select(customerId) from customers - val result = execute( - ZTransaction(query) *> ZTransaction(query) + val result = transact( + query.run.runCount *> query.run.runCount ) - val assertion = assertZIO(result.flatMap(_.runCount))(equalTo(5L)).orDie + val assertion = assertZIO(result)(equalTo(5L)).orDie assertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) }, - test("Transaction is failing") { - val query = select(customerId) from customers - - val result = execute( - ZTransaction(query) *> ZTransaction.fail(new Exception("failing")) *> ZTransaction(query) - ).mapError(_.getMessage) - - assertZIO(result.flip)(equalTo("failing")).mapErrorCause(cause => Cause.stackless(cause.untraced)) - }, - test("Transaction failed and didn't deleted rows") { + test("Transaction failed and didn't delete rows") { val query = select(customerId) from customers val deleteQuery = deleteFrom(customers).where(verified === false) val result = (for { allCustomersCount <- execute(query).runCount - _ <- execute( - ZTransaction(deleteQuery) *> ZTransaction.fail(new Exception("this is error")) *> ZTransaction(query) - ).catchAllCause(_ => ZIO.unit) + _ <- transact { + deleteQuery.run *> ZIO.fail(new Exception("this is error")) *> query.run.runCount + }.catchAllCause(_ => ZIO.unit) remainingCustomersCount <- execute(query).runCount } yield (allCustomersCount, remainingCustomersCount)) @@ -50,11 +41,11 @@ object TransactionSpec extends PostgresRunnableSpec with DbSchema { val query = select(customerId) from customers val deleteQuery = deleteFrom(customers).where(verified === false) - val tx = ZTransaction(deleteQuery) + val tx = transact(deleteQuery.run) val result = (for { allCustomersCount <- execute(query).runCount - _ <- execute(tx) + _ <- tx remainingCustomersCount <- execute(query).runCount } yield (allCustomersCount, remainingCustomersCount)) From 9e317ff2e2821af3e88f1c8f8cb8a0253dd9f8f5 Mon Sep 17 00:00:00 2001 From: PeiZ <74068135+peixunzhang@users.noreply.github.com> Date: Sun, 3 Jul 2022 00:03:47 +0200 Subject: [PATCH 03/10] Implement renderInsert for Oracle (#695) * Implement renderInsert for Oracle * fix error * fix error * add test * fix test * fix error * fix error * add blank * fix blank * fix * fix * fix * fix datetime * Add tests for inserting and fix bugs (#1) * wip * wip * wip * formatting * Insert tests * Update oracle/src/test/scala/zio/sql/oracle/OracleSqlModuleSpec.scala Co-authored-by: PeiZ <74068135+peixunzhang@users.noreply.github.com> * fix compiler error * fmt Co-authored-by: Jaro Regec Co-authored-by: Maxim Schuwalow <16665913+mschuwalow@users.noreply.github.com> --- .../test/scala/zio/sql/JdbcRunnableSpec.scala | 10 + .../zio/sql/oracle/OracleRenderModule.scala | 231 +++++++++++++++++- .../zio/sql/oracle/OracleSqlModule.scala | 61 +++++ oracle/src/test/resources/shop_schema.sql | 34 ++- .../zio/sql/oracle/OracleSqlModuleSpec.scala | 200 ++++++++++++++- .../scala/zio/sql/oracle/ShopSchema.scala | 59 ++++- 6 files changed, 582 insertions(+), 13 deletions(-) diff --git a/jdbc/src/test/scala/zio/sql/JdbcRunnableSpec.scala b/jdbc/src/test/scala/zio/sql/JdbcRunnableSpec.scala index 86ccfb653..d72bd8c0c 100644 --- a/jdbc/src/test/scala/zio/sql/JdbcRunnableSpec.scala +++ b/jdbc/src/test/scala/zio/sql/JdbcRunnableSpec.scala @@ -4,6 +4,9 @@ import com.dimafeng.testcontainers.JdbcDatabaseContainer import zio.test.TestEnvironment import zio.{ Scope, ZIO, ZLayer } import zio.test.ZIOSpecDefault +import zio.prelude.AssociativeBoth +import zio.test.Gen +import zio.prelude.Covariant import com.dimafeng.testcontainers.SingleContainer import java.util.Properties import zio.test.Spec @@ -55,6 +58,13 @@ trait JdbcRunnableSpec extends ZIOSpecDefault with Jdbc { SqlDriver.live ) + protected implicit def genInstances[R] + : AssociativeBoth[({ type T[A] = Gen[R, A] })#T] with Covariant[({ type T[+A] = Gen[R, A] })#T] = + new AssociativeBoth[({ type T[A] = Gen[R, A] })#T] with Covariant[({ type T[+A] = Gen[R, A] })#T] { + def map[A, B](f: A => B): Gen[R, A] => Gen[R, B] = _.map(f) + def both[A, B](fa: => Gen[R, A], fb: => Gen[R, B]): Gen[R, (A, B)] = fa.zip(fb) + } + private[this] def testContainer: ZIO[Scope, Throwable, SingleContainer[_] with JdbcDatabaseContainer] = ZIO.acquireRelease { ZIO.attemptBlocking { diff --git a/oracle/src/main/scala/zio/sql/oracle/OracleRenderModule.scala b/oracle/src/main/scala/zio/sql/oracle/OracleRenderModule.scala index 37a64dfbc..744ffd771 100644 --- a/oracle/src/main/scala/zio/sql/oracle/OracleRenderModule.scala +++ b/oracle/src/main/scala/zio/sql/oracle/OracleRenderModule.scala @@ -1,10 +1,21 @@ package zio.sql.oracle import zio.schema.Schema +import zio.schema.DynamicValue +import zio.schema.StandardType +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.OffsetTime +import java.time.ZonedDateTime import zio.sql.driver.Renderer import zio.sql.driver.Renderer.Extensions - +import zio.Chunk import scala.collection.mutable +import java.time.OffsetDateTime +import java.time.YearMonth +import java.time.Duration trait OracleRenderModule extends OracleSqlModule { self => @@ -14,7 +25,11 @@ trait OracleRenderModule extends OracleSqlModule { self => builder.toString } - override def renderInsert[A: Schema](insert: self.Insert[_, A]): String = ??? + override def renderInsert[A: Schema](insert: self.Insert[_, A]): String = { + val builder = new StringBuilder + buildInsertString(insert, builder) + builder.toString() + } override def renderRead(read: self.Read[_]): String = { val builder = new StringBuilder @@ -232,6 +247,218 @@ trait OracleRenderModule extends OracleSqlModule { self => val _ = builder.append(" (").append(values.mkString(",")).append(") ") // todo fix needs escaping } + private def buildInsertString[A: Schema](insert: self.Insert[_, A], builder: StringBuilder): Unit = { + + builder.append("INSERT INTO ") + renderTable(insert.table, builder) + + builder.append(" (") + renderColumnNames(insert.sources, builder) + builder.append(") ") + + renderInsertValues(insert.values, builder) + } + + private def renderTable(table: Table, builder: StringBuilder): Unit = table match { + case Table.DerivedTable(read, name) => + builder.append(" ( ") + builder.append(renderRead(read.asInstanceOf[Read[_]])) + builder.append(" ) ") + builder.append(name) + () + case Table.DialectSpecificTable(_) => ??? // there are no extensions for Oracle + case Table.Joined(joinType, left, right, on) => + renderTable(left, builder) + val joinTypeRepr = joinType match { + case JoinType.Inner => " INNER JOIN " + case JoinType.LeftOuter => " LEFT JOIN " + case JoinType.RightOuter => " RIGHT JOIN " + case JoinType.FullOuter => " OUTER JOIN " + } + builder.append(joinTypeRepr) + renderTable(right, builder) + builder.append(" ON ") + buildExpr(on, builder) + builder.append(" ") + () + case source: Table.Source => + builder.append(source.name) + () + } + private def renderColumnNames(sources: SelectionSet[_], builder: StringBuilder): Unit = + sources match { + case SelectionSet.Empty => () // table is a collection of at least ONE column + case SelectionSet.Cons(columnSelection, tail) => + val _ = columnSelection.name.map { name => + builder.append(name) + } + tail.asInstanceOf[SelectionSet[_]] match { + case SelectionSet.Empty => () + case next @ SelectionSet.Cons(_, _) => + builder.append(", ") + renderColumnNames(next.asInstanceOf[SelectionSet[_]], builder) + } + } + private def renderInsertValues[A](values: Seq[A], builder: StringBuilder)(implicit schema: Schema[A]): Unit = + values.toList match { + case head :: Nil => + builder.append("SELECT ") + renderInsertValue(head, builder) + builder.append(" FROM DUAL") + () + case head :: next => + builder.append("SELECT ") + renderInsertValue(head, builder) + builder.append(" FROM DUAL UNION ALL ") + renderInsertValues(next, builder) + case Nil => () + } + + def renderInsertValue[Z](z: Z, builder: StringBuilder)(implicit schema: Schema[Z]): Unit = + schema.toDynamic(z) match { + case DynamicValue.Record(listMap) => + listMap.values.toList match { + case head :: Nil => renderDynamicValue(head, builder) + case head :: next => + renderDynamicValue(head, builder) + builder.append(", ") + renderDynamicValues(next, builder) + case Nil => () + } + case value => renderDynamicValue(value, builder) + } + + def renderDynamicValue(dynValue: DynamicValue, builder: StringBuilder): Unit = + dynValue match { + case DynamicValue.Primitive(value, typeTag) => + // need to do this since StandardType is invariant in A + import StandardType._ + StandardType.fromString(typeTag.tag) match { + case Some(v) => + v match { + case BigDecimalType => + builder.append(value) + () + case StandardType.InstantType(formatter) => + builder.append( + s"""TO_TIMESTAMP_TZ('${formatter.format( + value.asInstanceOf[Instant] + )}', 'SYYYY-MM-DD"T"HH24:MI:SS.FF9TZH:TZM')""" + ) + () + case CharType => + builder.append(s"'${value}'") + () + case IntType => + builder.append(value) + () + case BinaryType => + val chunk = value.asInstanceOf[Chunk[Object]] + builder.append("'") + for (b <- chunk) + builder.append(String.format("%02x", b)) + builder.append(s"'") + () + case StandardType.LocalDateTimeType(formatter) => + builder.append( + s"""TO_TIMESTAMP('${formatter.format( + value.asInstanceOf[LocalDateTime] + )}', 'SYYYY-MM-DD"T"HH24:MI:SS.FF9')""" + ) + () + case StandardType.YearMonthType => + val yearMonth = value.asInstanceOf[YearMonth] + builder.append(s"INTERVAL '${yearMonth.getYear}-${yearMonth.getMonth.getValue}' YEAR(4) TO MONTH") + () + case DoubleType => + builder.append(value) + () + case StandardType.OffsetDateTimeType(formatter) => + builder.append( + s"""TO_TIMESTAMP_TZ('${formatter.format( + value.asInstanceOf[OffsetDateTime] + )}', 'SYYYY-MM-DD"T"HH24:MI:SS.FF9TZH:TZM')""" + ) + () + case StandardType.ZonedDateTimeType(formatter) => + builder.append( + s"""TO_TIMESTAMP_TZ('${formatter.format( + value.asInstanceOf[ZonedDateTime] + )}', 'SYYYY-MM-DD"T"HH24:MI:SS.FF9 TZR')""" + ) + () + case UUIDType => + builder.append(s"'${value}'") + () + case ShortType => + builder.append(value) + () + case StandardType.LocalTimeType(_) => + val localTime = value.asInstanceOf[LocalTime] + builder.append( + s"INTERVAL '${localTime.getHour}:${localTime.getMinute}:${localTime.getSecond}.${localTime.getNano}' HOUR TO SECOND(9)" + ) + () + case StandardType.OffsetTimeType(formatter) => + builder.append( + s"TO_TIMESTAMP_TZ('${formatter.format(value.asInstanceOf[OffsetTime])}', 'HH24:MI:SS.FF9TZH:TZM')" + ) + () + case LongType => + builder.append(value) + () + case StringType => + builder.append(s"'${value}'") + () + case StandardType.LocalDateType(formatter) => + builder.append(s"DATE '${formatter.format(value.asInstanceOf[LocalDate])}'") + () + case BoolType => + val b = value.asInstanceOf[Boolean] + if (b) { + builder.append('1') + } else { + builder.append('0') + } + () + case FloatType => + builder.append(value) + () + case StandardType.DurationType => + val duration = value.asInstanceOf[Duration] + val days = duration.toDays() + val hours = duration.toHours() % 24 + val minutes = duration.toMinutes() % 60 + val seconds = duration.getSeconds % 60 + val nanos = duration.getNano + builder.append(s"INTERVAL '$days $hours:$minutes:$seconds.$nanos' DAY(9) TO SECOND(9)") + () + case _ => + throw new IllegalStateException("unsupported") + } + case None => () + } + case DynamicValue.Tuple(left, right) => + renderDynamicValue(left, builder) + builder.append(", ") + renderDynamicValue(right, builder) + case DynamicValue.SomeValue(value) => renderDynamicValue(value, builder) + case DynamicValue.NoneValue => + builder.append("null") + () + case _ => () + } + + def renderDynamicValues(dynValues: List[DynamicValue], builder: StringBuilder): Unit = + dynValues match { + case head :: Nil => renderDynamicValue(head, builder) + case head :: tail => + renderDynamicValue(head, builder) + builder.append(", ") + renderDynamicValues(tail, builder) + case Nil => () + } + private def buildExprList(expr: Read.ExprSet[_], builder: StringBuilder): Unit = expr match { case Read.ExprSet.ExprCons(head, tail) => diff --git a/oracle/src/main/scala/zio/sql/oracle/OracleSqlModule.scala b/oracle/src/main/scala/zio/sql/oracle/OracleSqlModule.scala index 64d7b5ccb..e6b17ff45 100644 --- a/oracle/src/main/scala/zio/sql/oracle/OracleSqlModule.scala +++ b/oracle/src/main/scala/zio/sql/oracle/OracleSqlModule.scala @@ -1,11 +1,72 @@ package zio.sql.oracle import zio.sql.Sql +import zio.schema.Schema +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.time.LocalDateTime +import java.time.YearMonth +import java.sql.ResultSet +import scala.util.Try +import java.time.Duration +import java.time.ZonedDateTime +import java.time.LocalTime +import java.time.Instant +import java.time.OffsetTime +import java.time.OffsetDateTime trait OracleSqlModule extends Sql { self => + import ColumnSet._ + + override type TypeTagExtension[+A] = OracleTypeTag[A] + + trait OracleTypeTag[+A] extends Tag[A] with Decodable[A] + + object OracleTypeTag { + implicit case object TYearMonth extends OracleTypeTag[YearMonth] { + def decode(column: Int, resultSet: ResultSet): Either[DecodingError, YearMonth] = + Try(YearMonth.parse(resultSet.getString(column))) + .fold( + _ => Left(DecodingError.UnexpectedNull(column)), + r => Right(r) + ) + } + implicit case object TDuration extends OracleTypeTag[Duration] { + def decode(column: Int, resultSet: ResultSet): Either[DecodingError, Duration] = + Try(Duration.parse(resultSet.getString(column))) + .fold( + _ => Left(DecodingError.UnexpectedNull(column)), + r => Right(r) + ) + } + } object OracleFunctionDef { val Sind = FunctionDef[Double, Double](FunctionName("sind")) } + implicit val instantSchema = + Schema.primitive[Instant](zio.schema.StandardType.InstantType(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) + + implicit val localDateSchema = + Schema.primitive[LocalDate](zio.schema.StandardType.LocalDateType(DateTimeFormatter.ISO_LOCAL_DATE)) + + implicit val localTimeSchema = + Schema.primitive[LocalTime](zio.schema.StandardType.LocalTimeType(DateTimeFormatter.ISO_LOCAL_TIME)) + + implicit val localDateTimeSchema = + Schema.primitive[LocalDateTime](zio.schema.StandardType.LocalDateTimeType(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) + + implicit val offsetTimeSchema = + Schema.primitive[OffsetTime](zio.schema.StandardType.OffsetTimeType(DateTimeFormatter.ISO_OFFSET_TIME)) + + implicit val offsetDateTimeSchema = + Schema.primitive[OffsetDateTime](zio.schema.StandardType.OffsetDateTimeType(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) + + implicit val zonedDatetimeSchema = + Schema.primitive[ZonedDateTime](zio.schema.StandardType.ZonedDateTimeType(DateTimeFormatter.ISO_ZONED_DATE_TIME)) + + def yearMonth(name: String): Singleton[YearMonth, name.type] = singleton[YearMonth, name.type](name) + + def duration(name: String): Singleton[Duration, name.type] = singleton[Duration, name.type](name) } diff --git a/oracle/src/test/resources/shop_schema.sql b/oracle/src/test/resources/shop_schema.sql index ebfa197b6..09ac9b119 100644 --- a/oracle/src/test/resources/shop_schema.sql +++ b/oracle/src/test/resources/shop_schema.sql @@ -37,8 +37,34 @@ create table order_details unit_price Number(15,2) not null ); -insert all - into customers (id, first_name, last_name, verified, dob) values ('60b01fc9-c902-4468-8d49-3c0f989def37', 'Ronald', 'Russell', 1, TO_DATE('1983-01-05','YYYY-MM-DD')) +create table all_types +( + id varchar(36) not null primary key, + bytearray blob, + bigdecimal number not null, + boolean_ number(1) not null, + char_ varchar(4) not null, + double_ number not null, + float_ float not null, + instant timestamp with time zone not null, + int_ integer not null, + optional_int integer, + localdate date not null, + localdatetime timestamp not null, + localtime interval day to second not null, + long_ integer not null, + offsetdatetime timestamp with time zone not null, + offsettime timestamp with time zone not null, + short integer not null, + string clob not null, + uuid varchar(36) not null, + zoneddatetime timestamp with time zone not null, + yearmonth interval year(4) to month not null, + duration interval day(9) to second(9) not null +); + +insert all + into customers (id, first_name, last_name, verified, dob) values ('60b01fc9-c902-4468-8d49-3c0f989def37', 'Ronald', 'Russell', 1, TO_DATE('1983-01-05','YYYY-MM-DD')) into customers (id, first_name, last_name, verified, dob) values ('f76c9ace-be07-4bf3-bd4c-4a9c62882e64', 'Terrence', 'Noel', 1, TO_DATE('1999-11-02','YYYY-MM-DD')) into customers (id, first_name, last_name, verified, dob) values ('784426a5-b90a-4759-afbb-571b7a0ba35e', 'Mila', 'Paterso', 1, TO_DATE('1990-11-16','YYYY-MM-DD')) into customers (id, first_name, last_name, verified, dob) values ('df8215a2-d5fd-4c6c-9984-801a1b3a2a0b', 'Alana', 'Murray', 1, TO_DATE('1995-11-12','YYYY-MM-DD')) @@ -106,7 +132,7 @@ insert all ('04912093-cc2e-46ac-b64c-1bd7bb7758c3', '60b01fc9-c902-4468-8d49-3c0f989def37', TO_DATE('2019-03-25', 'YYYY-MM-DD')) into orders (id, customer_id, order_date) - values + values ('a243fa42-817a-44ec-8b67-22193d212d82', '60b01fc9-c902-4468-8d49-3c0f989def37', TO_DATE('2018-06-04', 'YYYY-MM-DD')) into orders (id, customer_id, order_date) @@ -452,4 +478,4 @@ insert all into order_details(order_id, product_id, quantity, unit_price) values ('5883CB62-D792-4EE3-ACBC-FE85B6BAA998', 'D5137D3A-894A-4109-9986-E982541B434F', 1, 55.00) -select * from dual; \ No newline at end of file +select * from dual; diff --git a/oracle/src/test/scala/zio/sql/oracle/OracleSqlModuleSpec.scala b/oracle/src/test/scala/zio/sql/oracle/OracleSqlModuleSpec.scala index 84b4b87f7..40007cd4d 100644 --- a/oracle/src/test/scala/zio/sql/oracle/OracleSqlModuleSpec.scala +++ b/oracle/src/test/scala/zio/sql/oracle/OracleSqlModuleSpec.scala @@ -5,12 +5,19 @@ import zio.test.TestAspect._ import zio.test._ import scala.language.postfixOps +import java.util.UUID +import java.time.format.DateTimeFormatter +import zio.schema.Schema +import zio.prelude._ +import java.time.{ LocalDate, LocalDateTime, Month, Year, YearMonth, ZoneOffset, ZonedDateTime } object OracleSqlModuleSpec extends OracleRunnableSpec with ShopSchema { import Customers._ + import Orders._ + import AllTypes._ - override def specLayered: Spec[SqlDriver, Exception] = suite("Oracle module")( + override def specLayered: Spec[SqlDriver with TestConfig with Sized, Exception] = suite("Oracle module")( test("Can update selected rows") { /** @@ -57,6 +64,195 @@ object OracleSqlModuleSpec extends OracleRunnableSpec with ShopSchema { val result = execute(query) assertZIO(result)(equalTo(expected)) - } + }, + test("Can insert rows") { + final case class CustomerRow( + id: UUID, + dateOfBirth: LocalDate, + firstName: String, + lastName: String, + verified: Boolean + ) + implicit val customerRowSchema = + Schema.CaseClass5[UUID, LocalDate, String, String, Boolean, CustomerRow]( + Schema.Field("id", Schema.primitive[UUID](zio.schema.StandardType.UUIDType)), + Schema.Field( + "dateOfBirth", + Schema.primitive[LocalDate](zio.schema.StandardType.LocalDateType(DateTimeFormatter.ISO_DATE)) + ), + Schema.Field("firstName", Schema.primitive[String](zio.schema.StandardType.StringType)), + Schema.Field("lastName", Schema.primitive[String](zio.schema.StandardType.StringType)), + Schema.Field("verified", Schema.primitive[Boolean](zio.schema.StandardType.BoolType)), + CustomerRow.apply, + _.id, + _.dateOfBirth, + _.firstName, + _.lastName, + _.verified + ) + + val rows = List( + CustomerRow(UUID.randomUUID(), LocalDate.ofYearDay(2001, 8), "Peter", "Parker", true), + CustomerRow(UUID.randomUUID(), LocalDate.ofYearDay(1980, 2), "Stephen", "Strange", false) + ) + + val command = insertInto(customers)( + customerId, + dob, + fName, + lName, + verified + ).values(rows) + + assertZIO(execute(command))(equalTo(2)) + }, + test("Can insert tuples") { + + val rows = List( + ( + UUID.randomUUID(), + UUID.randomUUID(), + LocalDate.of(2022, 1, 1) + ), + ( + UUID.randomUUID(), + UUID.randomUUID(), + LocalDate.of(2022, 1, 5) + ) + ) + + val command = insertInto(orders)( + orderId, + fkCustomerId, + orderDate + ).values(rows) + + assertZIO(execute(command))(equalTo(2)) + }, + test("Can insert all supported types") { + val sqlMinDateTime = LocalDateTime.of(-4713, 1, 1, 0, 0) + val sqlMaxDateTime = LocalDateTime.of(9999, 12, 31, 23, 59) + + val sqlInstant = + Gen.instant(sqlMinDateTime.toInstant(ZoneOffset.MIN), sqlMaxDateTime.toInstant(ZoneOffset.MAX)) + + val sqlYear = Gen.int(-4713, 9999).filter(_ != 0).map(Year.of) + + val sqlLocalDate = for { + year <- sqlYear + month <- Gen.int(1, 12) + maxLen = if (!year.isLeap && month == 2) 28 else Month.of(month).maxLength + day <- Gen.int(1, maxLen) + } yield LocalDate.of(year.getValue, month, day) + + val sqlYearMonth = for { + year <- sqlYear + month <- Gen.int(1, 12) + } yield YearMonth.of(year.getValue(), month) + + val sqlLocalDateTime = + Gen.localDateTime(sqlMinDateTime, sqlMaxDateTime) + + val sqlZonedDateTime = for { + dateTime <- sqlLocalDateTime + zoneId <- Gen.zoneId + } yield ZonedDateTime.of(dateTime, zoneId) + + val sqlOffsetTime = + Gen.offsetTime.filter(_.getOffset.getTotalSeconds % 60 == 0) + + val sqlOffsetDateTime = + Gen + .offsetDateTime(sqlMinDateTime.atOffset(ZoneOffset.MIN), sqlMaxDateTime.atOffset(ZoneOffset.MAX)) + .filter(_.getOffset.getTotalSeconds % 60 == 0) + + val gen = ( + Gen.uuid, + Gen.chunkOf(Gen.byte), + Gen.bigDecimal(Long.MinValue, Long.MaxValue), + Gen.boolean, + Gen.char, + Gen.double, + Gen.float, + sqlInstant, + Gen.int, + Gen.option(Gen.int), + sqlLocalDate, + sqlLocalDateTime, + Gen.localTime, + Gen.long, + sqlOffsetDateTime, + sqlOffsetTime, + Gen.short, + Gen.string, + Gen.uuid, + sqlZonedDateTime, + sqlYearMonth, + Gen.finiteDuration + ).tupleN + check(gen) { row => + val insert = insertInto(allTypes)( + id, + bytearrayCol, + bigdecimalCol, + booleanCol, + charCol, + doubleCol, + floatCol, + instantCol, + intCol, + optionalIntCol, + localdateCol, + localdatetimeCol, + localtimeCol, + longCol, + offsetdatetimeCol, + offsettimeCol, + shortCol, + stringCol, + uuidCol, + zonedDatetimeCol, + yearMonthCol, + durationCol + ).values(row) + + // TODO: ensure we can read values back correctly + // val read = + // select( + // id ++ + // bytearrayCol ++ + // bigdecimalCol ++ + // booleanCol ++ + // charCol ++ + // doubleCol ++ + // floatCol ++ + // instantCol ++ + // intCol ++ + // optionalIntCol ++ + // localdateCol ++ + // localdatetimeCol ++ + // localtimeCol ++ + // longCol ++ + // offsetdatetimeCol ++ + // offsettimeCol ++ + // shortCol ++ + // stringCol ++ + // uuidCol ++ + // zonedDatetimeCol ++ + // yearMonthCol ++ + // durationCol + // ).from(allTypes) + + val delete = deleteFrom(allTypes).where(id === row._1) + + for { + _ <- execute(insert) + // result <- execute(read).runHead + _ <- execute(delete) + // } yield assert(result)(isSome(equalTo(row))) + } yield assertCompletes + + } + } @@ samples(1) @@ retries(0) @@ shrinks(0) ) @@ sequential } diff --git a/oracle/src/test/scala/zio/sql/oracle/ShopSchema.scala b/oracle/src/test/scala/zio/sql/oracle/ShopSchema.scala index 177831047..ec7eaf9d0 100644 --- a/oracle/src/test/scala/zio/sql/oracle/ShopSchema.scala +++ b/oracle/src/test/scala/zio/sql/oracle/ShopSchema.scala @@ -1,18 +1,16 @@ package zio.sql.oracle -import zio.sql.Jdbc - -trait ShopSchema extends Jdbc { self => +trait ShopSchema extends OracleSqlModule { self => import self.ColumnSet._ object Customers { val customers = (uuid("id") ++ localDate("dob") ++ string("first_name") ++ string("last_name") ++ - boolean("verified") ++ zonedDateTime("Created_timestamp")) + boolean("verified")) .table("customers") - val (customerId, dob, fName, lName, verified, createdTimestamp) = customers.columns + val (customerId, dob, fName, lName, verified) = customers.columns } object Orders { val orders = (uuid("id") ++ uuid("customer_id") ++ localDate("order_date")).table("orders") @@ -41,4 +39,55 @@ trait ShopSchema extends Jdbc { self => val (fkOrderId, fkProductId, quantity, unitPrice) = orderDetails.columns } + + object AllTypes { + val allTypes = + (uuid("id") ++ + byteArray("bytearray") ++ + bigDecimal("bigdecimal") ++ + boolean("boolean_") ++ + char("char_") ++ + double("double_") ++ + float("float_") ++ + instant("instant") ++ + int("int_") ++ + (int("optional_int") @@ ColumnSetAspect.nullable) ++ + localDate("localdate") ++ + localDateTime("localdatetime") ++ + localTime("localtime") ++ + long("long_") ++ + offsetDateTime("offsetdatetime") ++ + offsetTime("offsettime") ++ + short("short") ++ + string("string") ++ + uuid("uuid") ++ + zonedDateTime("zoneddatetime") ++ + yearMonth("yearmonth") ++ + duration("duration")).table("all_types") + + val ( + id, + bytearrayCol, + bigdecimalCol, + booleanCol, + charCol, + doubleCol, + floatCol, + instantCol, + intCol, + optionalIntCol, + localdateCol, + localdatetimeCol, + localtimeCol, + longCol, + offsetdatetimeCol, + offsettimeCol, + shortCol, + stringCol, + uuidCol, + zonedDatetimeCol, + yearMonthCol, + durationCol + ) = allTypes.columns + } } From 5594dca56265ba97c63de3dd97f01376e5fc0755 Mon Sep 17 00:00:00 2001 From: walesho <92475279+walesho@users.noreply.github.com> Date: Sat, 2 Jul 2022 23:58:48 +0100 Subject: [PATCH 04/10] Added Date Trunc for Postgres (#697) Co-authored-by: Jaro Regec Co-authored-by: Jakub Czuchnowski --- .../sql/postgresql/PostgresSqlModule.scala | 1 + .../zio/sql/postgresql/FunctionDefSpec.scala | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/postgres/src/main/scala/zio/sql/postgresql/PostgresSqlModule.scala b/postgres/src/main/scala/zio/sql/postgresql/PostgresSqlModule.scala index 337147121..443917026 100644 --- a/postgres/src/main/scala/zio/sql/postgresql/PostgresSqlModule.scala +++ b/postgres/src/main/scala/zio/sql/postgresql/PostgresSqlModule.scala @@ -252,6 +252,7 @@ trait PostgresSqlModule extends Sql { self => val Chr = FunctionDef[Int, String](FunctionName("chr")) val CurrentDate = Expr.ParenlessFunctionCall0[LocalDate](FunctionName("current_date")) val CurrentTime = Expr.ParenlessFunctionCall0[OffsetTime](FunctionName("current_time")) + val DateTrunc = FunctionDef[(String, Instant), LocalDateTime](FunctionName("date_trunc")) val Decode = FunctionDef[(String, String), Chunk[Byte]](FunctionName("decode")) val Degrees = FunctionDef[Double, Double](FunctionName("degrees")) val Div = FunctionDef[(Double, Double), Double](FunctionName("div")) diff --git a/postgres/src/test/scala/zio/sql/postgresql/FunctionDefSpec.scala b/postgres/src/test/scala/zio/sql/postgresql/FunctionDefSpec.scala index 7d96f1b8e..2d0936f95 100644 --- a/postgres/src/test/scala/zio/sql/postgresql/FunctionDefSpec.scala +++ b/postgres/src/test/scala/zio/sql/postgresql/FunctionDefSpec.scala @@ -1058,6 +1058,34 @@ object FunctionDefSpec extends PostgresRunnableSpec with DbSchema { val result = typeCheck("execute((select(CharLength(Customers.fName))).to[Int, Int](identity))") assertZIO(dummyUsage *> result)(isLeft) + }, + test("date-trunc woth hour") { + val someInstant = Instant.parse("1997-05-07T10:15:30.00Z") + val query = select(DateTrunc("hour", someInstant)) + val testResult = execute(query) + + val assertion = + for { + r <- testResult.runCollect + } yield assert(timestampFormatter.format(r.head))( + Assertion.matchesRegex("[0-9]{4}-[0-9]{2}-[0-9]{2} 10:00:00.0000") + ) + + assertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) + }, + test("date-trunc with minute") { + val someInstant = Instant.parse("1997-05-07T10:15:30.00Z") + val query = select(DateTrunc("minute", someInstant)) + val testResult = execute(query) + + val assertion = + for { + r <- testResult.runCollect + } yield assert(timestampFormatter.format(r.head))( + Assertion.matchesRegex("[0-9]{4}-[0-9]{2}-[0-9]{2} 10:15:00.0000") + ) + + assertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) } ) @@ timeout(5.minutes) } From 72353a2e326a4d93b6dfb5054112b8ae6f8174ae Mon Sep 17 00:00:00 2001 From: Jakub Czuchnowski Date: Sun, 3 Jul 2022 01:46:07 +0200 Subject: [PATCH 05/10] Remove initial Literal in Where clause in Oracle module (#711) --- .../zio/sql/oracle/OracleRenderModule.scala | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/oracle/src/main/scala/zio/sql/oracle/OracleRenderModule.scala b/oracle/src/main/scala/zio/sql/oracle/OracleRenderModule.scala index 744ffd771..c1e22b34d 100644 --- a/oracle/src/main/scala/zio/sql/oracle/OracleRenderModule.scala +++ b/oracle/src/main/scala/zio/sql/oracle/OracleRenderModule.scala @@ -177,6 +177,20 @@ trait OracleRenderModule extends OracleSqlModule { self => val _ = builder.append(")") } + /** + * Drops the initial Litaral(true) present at the start of every WHERE expressions by default + * and proceeds to the rest of Expr's. + */ + private def buildWhereExpr[A, B](expr: self.Expr[_, A, B], builder: mutable.StringBuilder): Unit = expr match { + case Expr.Literal(true) => () + case Expr.Binary(_, b, _) => + builder.append(" WHERE ") + buildExpr(b, builder) + case _ => + builder.append(" WHERE ") + buildExpr(expr, builder) + } + private def buildReadString(read: self.Read[_], builder: StringBuilder): Unit = read match { case Read.Mapped(read, _) => buildReadString(read, builder) @@ -198,12 +212,7 @@ trait OracleRenderModule extends OracleSqlModule { self => builder.append(" FROM ") buildTable(t, builder) } - whereExpr match { - case Expr.Literal(true) => () - case _ => - builder.append(" WHERE ") - buildExpr(whereExpr, builder) - } + buildWhereExpr(whereExpr, builder) groupByExprs match { case Read.ExprSet.ExprCons(_, _) => builder.append(" GROUP BY ") @@ -556,12 +565,7 @@ trait OracleRenderModule extends OracleSqlModule { self => private def buildDeleteString(delete: Delete[_], builder: mutable.StringBuilder): Unit = { builder.append("DELETE FROM ") buildTable(delete.table, builder) - delete.whereExpr match { - case Expr.Literal(true) => () - case _ => - builder.append(" WHERE ") - buildExpr(delete.whereExpr, builder) - } + buildWhereExpr(delete.whereExpr, builder) } private[oracle] object OracleRender { @@ -573,8 +577,7 @@ trait OracleRenderModule extends OracleSqlModule { self => buildTable(table, render.builder) render(" SET ") renderSet(set) - render(" WHERE ") - buildExpr(whereExpr, render.builder) + buildWhereExpr(whereExpr, render.builder) } def renderSet(set: List[Set[_, _]])(implicit render: Renderer): Unit = From edda2a3102c3c24e3ea12b03f416a6e594cece55 Mon Sep 17 00:00:00 2001 From: Jakub Czuchnowski Date: Sun, 3 Jul 2022 02:25:27 +0200 Subject: [PATCH 06/10] Remove initial Literal in Where clause in SQL Server module (#712) --- .../sql/sqlserver/SqlServerRenderModule.scala | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/sqlserver/src/main/scala/zio/sql/sqlserver/SqlServerRenderModule.scala b/sqlserver/src/main/scala/zio/sql/sqlserver/SqlServerRenderModule.scala index 29cf49d5c..3f3ab6339 100644 --- a/sqlserver/src/main/scala/zio/sql/sqlserver/SqlServerRenderModule.scala +++ b/sqlserver/src/main/scala/zio/sql/sqlserver/SqlServerRenderModule.scala @@ -60,12 +60,7 @@ trait SqlServerRenderModule extends SqlServerSqlModule { self => render(" FROM ") buildTable(t) } - whereExpr match { - case Expr.Literal(true) => () - case _ => - render(" WHERE ") - buildExpr(whereExpr) - } + buildWhereExpr(whereExpr) groupByExprs match { case Read.ExprSet.ExprCons(_, _) => render(" GROUP BT ") @@ -96,7 +91,7 @@ trait SqlServerRenderModule extends SqlServerSqlModule { self => render(" (", values.mkString(","), ") ") // todo fix needs escaping } - def buildExpr[A, B](expr: self.Expr[_, A, B])(implicit render: Renderer): Unit = expr match { + private def buildExpr[A, B](expr: self.Expr[_, A, B])(implicit render: Renderer): Unit = expr match { case Expr.Subselect(subselect) => render(" (") render(renderRead(subselect)) @@ -126,7 +121,11 @@ trait SqlServerRenderModule extends SqlServerSqlModule { self => case Expr.Relational(left, right, op) => buildExpr(left) render(" ", op.symbol, " ") - buildExpr(right) + right.asInstanceOf[Expr[_, A, B]] match { + case Expr.Literal(true) => val _ = render("1") + case Expr.Literal(false) => val _ = render("0") + case otherValue => buildExpr(otherValue) + } case Expr.In(value, set) => buildExpr(value) renderReadImpl(set) @@ -247,7 +246,7 @@ trait SqlServerRenderModule extends SqlServerSqlModule { self => render(")") } - def buildExprList(expr: Read.ExprSet[_])(implicit render: Renderer): Unit = + private def buildExprList(expr: Read.ExprSet[_])(implicit render: Renderer): Unit = expr match { case Read.ExprSet.ExprCons(head, tail) => buildExpr(head) @@ -259,7 +258,8 @@ trait SqlServerRenderModule extends SqlServerSqlModule { self => } case Read.ExprSet.NoExpr => () } - def buildOrderingList(expr: List[Ordering[Expr[_, _, _]]])(implicit render: Renderer): Unit = + + private def buildOrderingList(expr: List[Ordering[Expr[_, _, _]]])(implicit render: Renderer): Unit = expr match { case head :: tail => head match { @@ -277,7 +277,7 @@ trait SqlServerRenderModule extends SqlServerSqlModule { self => case Nil => () } - def buildSelection(selectionSet: SelectionSet[_])(implicit render: Renderer): Unit = + private def buildSelection(selectionSet: SelectionSet[_])(implicit render: Renderer): Unit = selectionSet match { case cons0 @ SelectionSet.Cons(_, _) => object Dummy { @@ -295,7 +295,7 @@ trait SqlServerRenderModule extends SqlServerSqlModule { self => case SelectionSet.Empty => () } - def buildColumnSelection[A, B](columnSelection: ColumnSelection[A, B])(implicit render: Renderer): Unit = + private def buildColumnSelection[A, B](columnSelection: ColumnSelection[A, B])(implicit render: Renderer): Unit = columnSelection match { case ColumnSelection.Constant(value, name) => render(value.toString()) // todo fix escaping @@ -317,7 +317,7 @@ trait SqlServerRenderModule extends SqlServerSqlModule { self => } } - def buildTable(table: Table)(implicit render: Renderer): Unit = + private def buildTable(table: Table)(implicit render: Renderer): Unit = table match { case Table.DerivedTable(read, name) => @@ -356,15 +356,24 @@ trait SqlServerRenderModule extends SqlServerSqlModule { self => render(" ") } + /** + * Drops the initial Litaral(true) present at the start of every WHERE expressions by default + * and proceeds to the rest of Expr's. + */ + private def buildWhereExpr[A, B](expr: self.Expr[_, A, B])(implicit render: Renderer): Unit = expr match { + case Expr.Literal(true) => () + case Expr.Binary(_, b, _) => + render(" WHERE ") + buildExpr(b) + case _ => + render(" WHERE ") + buildExpr(expr) + } + def renderDeleteImpl(delete: Delete[_])(implicit render: Renderer) = { render("DELETE FROM ") buildTable(delete.table) - delete.whereExpr match { - case Expr.Literal(true) => () - case _ => - render(" WHERE ") - buildExpr(delete.whereExpr) - } + buildWhereExpr(delete.whereExpr) } // TODO https://github.com/zio/zio-sql/issues/160 From 75dce9979deb2492c4c55da2552fc6306c9a02bf Mon Sep 17 00:00:00 2001 From: Jakub Czuchnowski Date: Sun, 3 Jul 2022 13:38:24 +0200 Subject: [PATCH 07/10] Remove initial Literal in Where clause in Postgres module (#713) * Remove initial Literal in Where clause in Postgres module * Fix test * Fix test --- .../scala/zio/sql/mysql/MysqlModuleSpec.scala | 7 +-- .../sql/postgresql/PostgresRenderModule.scala | 53 ++++++++++--------- .../zio/sql/postgresql/DeleteBatchSpec.scala | 30 +++++------ 3 files changed, 47 insertions(+), 43 deletions(-) diff --git a/mysql/src/test/scala/zio/sql/mysql/MysqlModuleSpec.scala b/mysql/src/test/scala/zio/sql/mysql/MysqlModuleSpec.scala index db77a4473..8308060e5 100644 --- a/mysql/src/test/scala/zio/sql/mysql/MysqlModuleSpec.scala +++ b/mysql/src/test/scala/zio/sql/mysql/MysqlModuleSpec.scala @@ -4,12 +4,13 @@ import java.time._ import java.time.format.DateTimeFormatter import java.util.UUID -import scala.language.postfixOps - import zio._ import zio.schema._ import zio.test._ import zio.test.Assertion._ +import zio.test.TestAspect._ + +import scala.language.postfixOps object MysqlModuleSpec extends MysqlRunnableSpec with ShopSchema { @@ -270,6 +271,6 @@ object MysqlModuleSpec extends MysqlRunnableSpec with ShopSchema { println(renderUpdate(query)) assertZIO(execute(query))(equalTo(1)) } - ) + ) @@ sequential } diff --git a/postgres/src/main/scala/zio/sql/postgresql/PostgresRenderModule.scala b/postgres/src/main/scala/zio/sql/postgresql/PostgresRenderModule.scala index 456294af3..88655dc20 100644 --- a/postgres/src/main/scala/zio/sql/postgresql/PostgresRenderModule.scala +++ b/postgres/src/main/scala/zio/sql/postgresql/PostgresRenderModule.scala @@ -47,7 +47,7 @@ trait PostgresRenderModule extends PostgresSqlModule { self => renderInsertValues(insert.values) } - def renderInsertValues[A](col: Seq[A])(implicit render: Renderer, schema: Schema[A]): Unit = + private def renderInsertValues[A](col: Seq[A])(implicit render: Renderer, schema: Schema[A]): Unit = // TODO any performance penalty because of toList ? col.toList match { case head :: Nil => @@ -62,7 +62,7 @@ trait PostgresRenderModule extends PostgresSqlModule { self => case Nil => () } - def renderInserValue[Z](z: Z)(implicit render: Renderer, schema: Schema[Z]): Unit = + private def renderInserValue[Z](z: Z)(implicit render: Renderer, schema: Schema[Z]): Unit = schema.toDynamic(z) match { case DynamicValue.Record(listMap) => listMap.values.toList match { @@ -76,7 +76,7 @@ trait PostgresRenderModule extends PostgresSqlModule { self => case value => renderDynamicValue(value) } - def renderDynamicValues(dynValues: List[DynamicValue])(implicit render: Renderer): Unit = + private def renderDynamicValues(dynValues: List[DynamicValue])(implicit render: Renderer): Unit = dynValues match { case head :: Nil => renderDynamicValue(head) case head :: tail => @@ -87,7 +87,7 @@ trait PostgresRenderModule extends PostgresSqlModule { self => } // TODO render each type according to their specifics & test it - def renderDynamicValue(dynValue: DynamicValue)(implicit render: Renderer): Unit = + private def renderDynamicValue(dynValue: DynamicValue)(implicit render: Renderer): Unit = dynValue match { case DynamicValue.Primitive(value, typeTag) => // need to do this since StandardType is invariant in A @@ -143,7 +143,7 @@ trait PostgresRenderModule extends PostgresSqlModule { self => case _ => () } - def renderColumnNames(sources: SelectionSet[_])(implicit render: Renderer): Unit = + private def renderColumnNames(sources: SelectionSet[_])(implicit render: Renderer): Unit = sources match { case SelectionSet.Empty => () // table is a collection of at least ONE column case SelectionSet.Cons(columnSelection, tail) => @@ -161,12 +161,7 @@ trait PostgresRenderModule extends PostgresSqlModule { self => 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) - } + renderWhereExpr(delete.whereExpr) } def renderUpdateImpl(update: Update[_])(implicit render: Renderer) = @@ -176,11 +171,10 @@ trait PostgresRenderModule extends PostgresSqlModule { self => renderTable(table) render(" SET ") renderSet(set) - render(" WHERE ") - renderExpr(whereExpr) + renderWhereExpr(whereExpr) } - def renderSet(set: List[Set[_, _]])(implicit render: Renderer): Unit = + private def renderSet(set: List[Set[_, _]])(implicit render: Renderer): Unit = set match { case head :: tail => renderSetLhs(head.lhs) @@ -394,12 +388,7 @@ trait PostgresRenderModule extends PostgresSqlModule { self => render(" FROM ") renderTable(t) } - whereExpr match { - case Expr.Literal(true) => () - case _ => - render(" WHERE ") - renderExpr(whereExpr) - } + renderWhereExpr(whereExpr) groupByExprs match { case Read.ExprSet.ExprCons(_, _) => render(" GROUP BY ") @@ -438,7 +427,7 @@ trait PostgresRenderModule extends PostgresSqlModule { self => render(" (", values.mkString(","), ") ") // todo fix needs escaping } - def renderExprList(expr: Read.ExprSet[_])(implicit render: Renderer): Unit = + private def renderExprList(expr: Read.ExprSet[_])(implicit render: Renderer): Unit = expr match { case Read.ExprSet.ExprCons(head, tail) => renderExpr(head) @@ -451,7 +440,7 @@ trait PostgresRenderModule extends PostgresSqlModule { self => case Read.ExprSet.NoExpr => () } - def renderOrderingList(expr: List[Ordering[Expr[_, _, _]]])(implicit render: Renderer): Unit = + private def renderOrderingList(expr: List[Ordering[Expr[_, _, _]]])(implicit render: Renderer): Unit = expr match { case head :: tail => head match { @@ -469,7 +458,7 @@ trait PostgresRenderModule extends PostgresSqlModule { self => case Nil => () } - def renderSelection[A](selectionSet: SelectionSet[A])(implicit render: Renderer): Unit = + private def renderSelection[A](selectionSet: SelectionSet[A])(implicit render: Renderer): Unit = selectionSet match { case cons0 @ SelectionSet.Cons(_, _) => object Dummy { @@ -487,7 +476,7 @@ trait PostgresRenderModule extends PostgresSqlModule { self => case SelectionSet.Empty => () } - def renderColumnSelection[A, B](columnSelection: ColumnSelection[A, B])(implicit render: Renderer): Unit = + private def renderColumnSelection[A, B](columnSelection: ColumnSelection[A, B])(implicit render: Renderer): Unit = columnSelection match { case ColumnSelection.Constant(value, name) => render(value) // todo fix escaping @@ -507,7 +496,7 @@ trait PostgresRenderModule extends PostgresSqlModule { self => } } - def renderTable(table: Table)(implicit render: Renderer): Unit = + private def renderTable(table: Table)(implicit render: Renderer): Unit = table match { case Table.DialectSpecificTable(tableExtension) => tableExtension match { @@ -538,5 +527,19 @@ trait PostgresRenderModule extends PostgresSqlModule { self => renderExpr(on) render(" ") } + + /** + * Drops the initial Litaral(true) present at the start of every WHERE expressions by default + * and proceeds to the rest of Expr's. + */ + private def renderWhereExpr[A, B](expr: self.Expr[_, A, B])(implicit render: Renderer): Unit = expr match { + case Expr.Literal(true) => () + case Expr.Binary(_, b, _) => + render(" WHERE ") + renderExpr(b) + case _ => + render(" WHERE ") + renderExpr(expr) + } } } diff --git a/postgres/src/test/scala/zio/sql/postgresql/DeleteBatchSpec.scala b/postgres/src/test/scala/zio/sql/postgresql/DeleteBatchSpec.scala index 5aeb77ac4..22413297b 100644 --- a/postgres/src/test/scala/zio/sql/postgresql/DeleteBatchSpec.scala +++ b/postgres/src/test/scala/zio/sql/postgresql/DeleteBatchSpec.scala @@ -2,6 +2,7 @@ package zio.sql.postgresql import zio.Cause import zio.test.Assertion._ +import zio.test.TestAspect._ import zio.test._ import java.time.{ LocalDate, ZonedDateTime } @@ -70,25 +71,24 @@ object DeleteBatchSpec extends PostgresRunnableSpec with DbSchema { val allCustomer = List(c1, c2, c3, c4) val data = allCustomer.map(Customer.unapply(_).get) - val query = insertInto(customers)(ALL).values(data) - val resultInsert = execute(query) + val insertStmt = insertInto(customers)(ALL).values(data) + val insertResult = execute(insertStmt) - val insertAssertion = for { - r <- resultInsert - } yield assert(r)(equalTo(4)) - insertAssertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) + val selectStmt = select(ALL).from(customers) + val selectResult = execute(selectStmt.to((Customer.apply _).tupled)).runCollect - val selectAll = select(ALL).from(customers) - val result_ = execute(selectAll.to((Customer.apply _).tupled)).runCollect + val expected = 8 // 4 customers are in the db alredy and we insert additional 4 in this test - val assertion_ = for { - x <- result_ - updated = x.toList.map(delete_) - result <- executeBatchDelete(updated).map(l => l.fold(0)((a, b) => a + b)) - } yield assert(result)(equalTo(4)) - assertion_.mapErrorCause(cause => Cause.stackless(cause.untraced)) + val assertion = for { + _ <- insertResult + customers <- selectResult + deletes = customers.toList.map(delete_) + result <- executeBatchDelete(deletes).map(l => l.fold(0)((a, b) => a + b)) + } yield assert(result)(equalTo(expected)) + + assertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) } - ) + ) @@ sequential } From fa81e092f0a62f8e529193df86fbec10c3f74ae4 Mon Sep 17 00:00:00 2001 From: jczuchnowski Date: Sun, 3 Jul 2022 13:52:41 +0200 Subject: [PATCH 08/10] Remove initial Literal in Where clause in MySQL module --- .../zio/sql/mysql/MysqlRenderModule.scala | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/mysql/src/main/scala/zio/sql/mysql/MysqlRenderModule.scala b/mysql/src/main/scala/zio/sql/mysql/MysqlRenderModule.scala index 504ecd92e..17481a48f 100644 --- a/mysql/src/main/scala/zio/sql/mysql/MysqlRenderModule.scala +++ b/mysql/src/main/scala/zio/sql/mysql/MysqlRenderModule.scala @@ -49,12 +49,7 @@ trait MysqlRenderModule extends MysqlSqlModule { self => 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) - } + renderWhereExpr(delete.whereExpr) } def renderUpdateImpl(update: Update[_])(implicit render: Renderer) = @@ -64,8 +59,7 @@ trait MysqlRenderModule extends MysqlSqlModule { self => renderTable(table) render(" SET ") renderSet(set) - render(" WHERE ") - renderExpr(whereExpr) + renderWhereExpr(whereExpr) } def renderReadImpl(read: self.Read[_])(implicit render: Renderer): Unit = @@ -89,12 +83,7 @@ trait MysqlRenderModule extends MysqlSqlModule { self => render(" FROM ") renderTable(t) } - whereExpr match { - case Expr.Literal(true) => () - case _ => - render(" WHERE ") - renderExpr(whereExpr) - } + renderWhereExpr(whereExpr) groupByExprs match { case Read.ExprSet.ExprCons(_, _) => render(" GROUP BY ") @@ -511,7 +500,7 @@ trait MysqlRenderModule extends MysqlSqlModule { self => case Read.ExprSet.NoExpr => () } - def renderOrderingList(expr: List[Ordering[Expr[_, _, _]]])(implicit render: Renderer): Unit = + private def renderOrderingList(expr: List[Ordering[Expr[_, _, _]]])(implicit render: Renderer): Unit = expr match { case head :: tail => head match { @@ -529,6 +518,20 @@ trait MysqlRenderModule extends MysqlSqlModule { self => } case Nil => () } + + /** + * Drops the initial Litaral(true) present at the start of every WHERE expressions by default + * and proceeds to the rest of Expr's. + */ + private def renderWhereExpr[A, B](expr: self.Expr[_, A, B])(implicit render: Renderer): Unit = expr match { + case Expr.Literal(true) => () + case Expr.Binary(_, b, _) => + render(" WHERE ") + renderExpr(b) + case _ => + render(" WHERE ") + renderExpr(expr) + } } } From aa2bd0069b6680077309326bd97c4eed386ffbc4 Mon Sep 17 00:00:00 2001 From: jczuchnowski Date: Sun, 3 Jul 2022 13:55:00 +0200 Subject: [PATCH 09/10] Add operator tests in OracleModule spec --- .../test/scala/zio/sql/sqlserver/SqlServerModuleSpec.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sqlserver/src/test/scala/zio/sql/sqlserver/SqlServerModuleSpec.scala b/sqlserver/src/test/scala/zio/sql/sqlserver/SqlServerModuleSpec.scala index 8f357568e..1072caf1b 100644 --- a/sqlserver/src/test/scala/zio/sql/sqlserver/SqlServerModuleSpec.scala +++ b/sqlserver/src/test/scala/zio/sql/sqlserver/SqlServerModuleSpec.scala @@ -97,6 +97,9 @@ object SqlServerModuleSpec extends SqlServerRunnableSpec with DbSchema { test("Can select with property unary operator") { customerSelectJoseAssertion(verified isNotTrue) }, + test("Can select with property binary operator with Boolean") { + customerSelectJoseAssertion(verified === false) + }, test("Can select with property binary operator with UUID") { customerSelectJoseAssertion(customerId === UUID.fromString("636ae137-5b1a-4c8c-b11f-c47c624d9cdc")) }, @@ -118,6 +121,9 @@ object SqlServerModuleSpec extends SqlServerRunnableSpec with DbSchema { test("Can select with property binary operator with Instant") { customerSelectJoseAssertion(dob === Instant.parse("1987-03-23T00:00:00Z")) }, + test("Can select with mutltiple binary operators") { + customerSelectJoseAssertion(verified === false && fName === "Jose" && lName === "Wiggins") + }, test("Can select from single table with limit, offset and order by") { case class Customer(id: UUID, fname: String, lname: String, dateOfBirth: LocalDate) From 7a449e6c3fe212bd047dd2c462bb213663ddbcd0 Mon Sep 17 00:00:00 2001 From: Jakub Czuchnowski Date: Sun, 3 Jul 2022 14:14:13 +0200 Subject: [PATCH 10/10] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 011d54b2f..af1971f3b 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,10 @@ Connection pool | :white_check_mark: Feature | PostgreSQL | SQL Server | Oracle | MySQL :------------ | :-------------| :-------------|:-------------------| :------------- Render Read | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -Render Delete | :heavy_check_mark: | :heavy_check_mark: | :white_check_mark: | :white_check_mark: | -Render Update | :heavy_check_mark: | | :white_check_mark: | :white_check_mark: | -Render Insert | :heavy_check_mark: | | | :white_check_mark: | -Functions | :heavy_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | +Render Delete | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +Render Update | :heavy_check_mark: | | :heavy_check_mark: | :heavy_check_mark: | +Render Insert | :heavy_check_mark: | | :heavy_check_mark: | :heavy_check_mark: | +Functions | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | Types | :white_check_mark: | | | :white_check_mark: | Operators | | | | |