diff --git a/.travis.yml b/.travis.yml index 7e6acca..bf4ae8b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,9 @@ scala: - 2.11.11 cache: directories: - - $HOME/.m2 - - $HOME/.ivy2 - - $HOME/.sbt/boot/scala-$TRAVIS_SCALA_VERSION + - "$HOME/.m2" + - "$HOME/.ivy2" + - "$HOME/.sbt/boot/scala-$TRAVIS_SCALA_VERSION" before_cache: - find $HOME/.ivy2 -name "ivydata-*.properties" -delete - find $HOME/.sbt -name "*.lock" -delete @@ -40,10 +40,12 @@ before_script: - curl -L -o $HOME/.sbt/launchers/0.13.16/sbt-launch.jar https://repo.typesafe.com/typesafe/ivy-releases/org.scala-sbt/sbt-launch/0.13.16/sbt-launch.jar - travis_retry sbt "++$TRAVIS_SCALA_VERSION update" script: - - sbt test +- ./build/run_tests.sh deploy: provider: pages skip_cleanup: true - github_token: $github_token + github_token: "$github_token" on: branch: develop +dd: + secure: L8zNip/aErHXM3Q8NJvv1iv9ZkP4zKYHqU0Qb0qpYOoo+MyoS5nw/4GcYl5plxexx+rSS2uJzpvwzBCDF5rzUQZf3ix3tzOFjNu1jQvgPZ/vBJHAIiaaxZ0M5rSQxf6LT0AG5TbjbXQkYxsMu+oior5yt5mNqZgzwXt0EskPuzdFIHaTp8OmCfCjnduMThKCAi+3rwAk/xZV0M+E12mQ1xq4Xt10roOyKY2I3b9MF8QrORzhfoVxoBR7gqQGN/JPxyztwtR06nnvdGRncHrDOUtkVmt7uVOo8aT3xGLff15q4blJWJ0CXWA+YGAHq3F/y3EmG9C77N/As2zVoU3GeOrf7nfj5nY0U8/Gh55C5HY/59hA0ZJwB+plt7yffKCfA4ernCo+E7JofoMzzscivmoDvyDaqBBoa9JHSfxKvN7DrwSFxhxGZvmV/CY/ezHbXjUqux53WD9P7f/8CLKaEbWJQ01um2ajrqJS7oRGp0IURqyu90soKukvM78zIId4Tx1ISoIN7sYsMZjJWbhniMXESZP/q9FTtjLfYph14/yL/13sWSluAglcpeoGwzkwbR9o/Sj2vB+EIuc1mDKJpuF1qKw1Zob+HJbqrsYhL2FeodKBRM7eZaPNeGGt6yG86Cy9MsTGgnNCWgWZGhk9lJFtz6T/3S5gyReq8L70eMk= diff --git a/README.md b/README.md index d953140..60853a3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# sql-parser +# sql-parser [![Build Status](https://travis-ci.org/outworkers/sql-parser.svg?branch=master)](https://travis-ci.org/outworkers/sql-parser) + + This is an extension of the Postgres-async project by Mauricio, to provide a type safe compile time DSL layer for enforcing domain types into SQL. diff --git a/build/run_tests.sh b/build/run_tests.sh new file mode 100755 index 0000000..3d8a14c --- /dev/null +++ b/build/run_tests.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +function run_test_suite { + if [ "${TRAVIS_SCALA_VERSION}" == "${TARGET_SCALA_VERSION}" ]; + then + echo "Running tests with coverage and report submission" + sbt ++$TRAVIS_SCALA_VERSION coverage test coverageReport coverageAggregate coveralls + testExitCode=$? + + if [ ${testExitCode} == "0" ]; + then + echo "Running tut compilation" + sbt ++$TRAVIS_SCALA_VERSION "project readme" "tut" + exit $? + else + echo "Unable to run tut compilation, test suite failed" + exit ${testExitCode} + fi + else + echo "Running tests without attempting to submit coverage reports or run tut" + sbt "plz $TRAVIS_SCALA_VERSION test" + exit $? + fi +} + +run_test_suite \ No newline at end of file diff --git a/parser/src/main/scala/anorm/PrebuiltPreparedStatement.scala b/parser/src/main/scala/anorm/PrebuiltPreparedStatement.scala new file mode 100644 index 0000000..12c9c1a --- /dev/null +++ b/parser/src/main/scala/anorm/PrebuiltPreparedStatement.scala @@ -0,0 +1,79 @@ +/* + * Copyright 2017 - 2018 Outworkers Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package anorm + +import com.github.mauricio.async.db.{Connection, QueryResult} + +import scala.concurrent.Future + +case class PrebuiltPreparedStatement( + query: String, + tokens: Map[String, Any] +)(connection: Connection) { + def onParams(pairs: (String, Any)*): PrebuiltPreparedStatement = { + copy(tokens = tokens ++ pairs)(connection) + } + + //@deprecated("This thing doens't work", "Prepared") + def on(pairs: NamedParameter*): PrebuiltPreparedStatement = { + val mapped = pairs.map(param => param.name -> param.value.show) + + copy(tokens = tokens ++ mapped)(connection) + } + + def flatten(input: Any): Any = { + if (input.isInstanceOf[Option[_]]) { + input match { + case value: Some[_] => + value.get + case _ => + null + } + } else { + input + } + } + + private[this] def bind(names: List[String]): Seq[Any] = { + names.flatMap { name => + tokens.find(_._1 == name).map { + case (_, value) => flatten(value) + } + } + } + + def execute(): Future[QueryResult] = { + SqlStatementParser.parse(query) match { + case scala.util.Success(ts) => + + val query = SqlQuery.prepare(ts, ts.names).statement + + val bindables = bind(ts.names) + + connection.sendPreparedStatement( + query, + bindables + ) + + case f @ scala.util.Failure(err) => { + Console.println("Is this not supporeted or what") + Console.println(err.getMessage) + Future.failed(err) + } + } + + } +} diff --git a/parser/src/main/scala/com/outworkers/sql/postgres/parser/DataParser.scala b/parser/src/main/scala/com/outworkers/sql/postgres/parser/DataParser.scala new file mode 100644 index 0000000..919cb4a --- /dev/null +++ b/parser/src/main/scala/com/outworkers/sql/postgres/parser/DataParser.scala @@ -0,0 +1,457 @@ +/* + * Copyright 2017 - 2018 Outworkers Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.outworkers.sql.postgres.parser + +import com.github.mauricio.async.db.RowData + +import scala.util.control.NoStackTrace + +/** Result cursor */ +trait PostgresCursor { + /** Current row */ + def row: RowData + + /** Cursor to next row */ + def next: Option[PostgresCursor] + + override lazy val toString = s"Cursor($row)" +} + + +object PostgresCursor { + def build(source: IndexedSeq[RowData]): Option[PostgresCursor] = { + if (source.isEmpty) { + None + } else { + Some { + new PostgresCursor { + /** Cursor to next row */ + override def next: Option[PostgresCursor] = build(source.tail) + + /** Current row */ + override def row: RowData = source.head + } + } + } + } +} + +/** + * Error raised when the specified `column` cannot be found in results. + * + * @param column the name of the not found column + * @param possibilities the names of the available columns + */ +case class ColumnNotFound( + column: String, + possibilities: List[String] +) extends SqlRequestError( + column, + new RuntimeException(column) with NoStackTrace +) { + + lazy val message: String = s"'$column' not found, available columns: " + + possibilities.map(_.dropWhile(_ == '.')).mkString(", ") + + override lazy val toString: String = message +} +case class SqlMappingError(reason: String) extends SqlRequestError( + reason, + new RuntimeException(reason) with NoStackTrace +) { + lazy val message = s"SqlMappingError($reason)" + override lazy val toString: String = message +} + +case class UnexpectedNullableFound(reason: String) extends SqlRequestError( + reason, + new RuntimeException(reason) with NoStackTrace +) { + lazy val message = s"UnexpectedNullableFound($reason)" + override lazy val toString: String = message +} + +case class TypeDoesNotMatch(reason: String) extends SqlRequestError( + reason, + new RuntimeException(reason) with NoStackTrace +){ + lazy val message = s"TypeDoesNotMatch($reason)" + override lazy val toString: String = message +} + +object DataParser { + + /** + * Returns a parser that fold over the row. + * + * {{{ + * val p: RowParser[List[(Any, String)]] = + * SqlParser.folder(List.empty[(Any, String)]) { (ls, v, m) => + * Right((v, m.clazz) :: ls) + * } + * }}} + */ + def folder[T](z: T)(f: (T, Any) => Either[SqlRequestError, T]): PostgresRowParser[T] = { + @annotation.tailrec + def go(data: RowData, index: Int, out: T): ParserResult[T] = + + if (data.isDefinedAt(index)) { + f(out, data(index)) match { + case Left(err) => ParseError(err) + case Right(_) => go(data, index + 1, out) + } + } else { + ParseSuccess(out) + } + + PostgresRowParser[T] { row: RowData => go(row, 0, z) } + } + + /** + * Flatten columns tuple-like. + * + * {{{ + * import anorm.SQL + * import anorm.SqlParser.{ long, str, int } + * + * val tuple: (Long, String, Int) = SQL("SELECT a, b, c FROM Test"). + * as(long("a") ~ str("b") ~ int("c") map (SqlParser.flatten) single) + * }}} + */ + def flatten[T1, T2, R](implicit f: TupleFlattener[(T1 ~ T2) => R]): ((T1 ~ T2) => R) = f.fn + + /** + * Parses specified array column as list. + * + * {{{ + * import anorm.{ SQL, SqlParser } + * + * val t: (String, List[String]) = SQL("SELECT a, sqlArrayOfString FROM test") + * .as(SqlParser.str("a") ~ SqlParser.list("sqlArrayOfString") map ( + * SqlParser.flatten) single) + * }}} + */ + def list[T](columnName: String)( + implicit c: DataType[List[T]] + ): PostgresRowParser[List[T]] = get[List[T]](columnName)(c) + + /** + * Parses specified array column as list. + * @param columnPosition from 1 to n + * + * {{{ + * import anorm.{ SQL, SqlParser } + * + * val t: (String, List[String]) = SQL("SELECT a, sqlArrayOfString FROM test") + * .as(SqlParser.str("a") ~ SqlParser.list(2) map ( + * SqlParser.flatten) single) + * }}} + */ + def list[T](columnPosition: Int)( + implicit c: DataType[List[T]] + ): PostgresRowParser[List[T]] = get[List[T]](columnPosition)(c) + + /** + * Returns row parser for column with given `name`. + * @param name Column name + * + * {{{ + * import anorm.SQL + * import anorm.SqlParser.get + * + * val title: String = SQL("SELECT title FROM Books"). + * as(get[String]("title").single) + * }}} + */ + def get[T](name: String)(implicit extractor: DataType[T]): PostgresRowParser[T] = + PostgresRowParser { row => + (for { + res <- extractor.parse(row, name) + } yield res).fold(ex => ParseError(SqlRequestError(ex)), ParseSuccess(_)) + } + + /** + * Returns row parser for column at given `position`. + * @param position Column position, from 1 to n + * + * {{{ + * import anorm.SQL + * import anorm.SqlParser.get + * + * val res: (Float, String) = // parsing columns #1 & #3 + * SQL("SELECT * FROM Test").as(get[String](1) ~ get[Float](3) map { + * case str ~ f => (f -> str) + * } *) + * }}} + */ + def get[T](position: Int)(implicit extractor: DataType[T]): PostgresRowParser[T] = + PostgresRowParser { row => + { + for { + res <- extractor.parse(row, position - 1) + } yield res + }.fold(e => ParseError(SqlRequestError(e)), a => ParseSuccess(a)) + } + + /** + * Returns row parser which true if specified `column` is found + * and matching expected `value`. + * + * {{{ + * import anorm.SQL + * import anorm.SqlParser.matches + * + * val m: Boolean = SQL("SELECT * FROM table").as(matches("a", 1.2f).single) + * // true if column |a| is found and matching 1.2f, otherwise false + * }}} + * + * @return true if matches, or false if not + */ + def matches[TT, T <: TT]( + column: String, + value: T + )(implicit c: DataType[TT]): PostgresRowParser[Boolean] = get[TT](column)(c).?.map(_.fold(false)(_ == value)) + +} + +/** Columns tuple-like */ +// Using List or HList? +final case class ~[+A, +B](_1: A, _2: B) + +object PostgresRowParser { + + def apply[A : DataType](name: String): PostgresRowParser[A] = new PostgresRowParser[A] { + override def apply(row: RowData): ParserResult[A] = DataType[A].parse(row, name) + } + + def apply[A](f: RowData => ParserResult[A]): PostgresRowParser[A] = new PostgresRowParser[A] { + def apply(row: RowData): ParserResult[A] = f(row) + } + + /** Row parser that result in successfully unchanged row. */ + object successful extends PostgresRowParser[RowData] { + def apply(row: RowData): ParserResult[RowData] = ParseSuccess(row) + } + + def failed[A](error: => ParseError): PostgresRowParser[A] = new PostgresRowParser[A] { + def apply(row: RowData): ParserResult[A] = error + } +} + +trait PostgresRowParser[+A] extends (RowData => ParserResult[A]) { parent => + + /** + * Returns a parser that will apply given function `f` + * to the result of this first parser. If the current parser is not + * successful, the new one will return encountered [[Error]]. + * + * @param f Function applied on the successful parser result + * + * {{{ + * import anorm.{ RowParser, SQL, SqlParser } + * + * val parser: RowParser[Int] = SqlParser.str("col").map(_.length) + * // Prepares a parser that first get 'col' string value, + * // and then returns the length of that + * }}} + */ + def map[B](f: A => B): PostgresRowParser[B] = PostgresRowParser(parent.andThen(_.map(f))) + + /** + * Returns parser which collects information + * from already parsed row data using `f`. + * + * @param otherwise Message returned as error if nothing can be collected using `f`. + * @param f Collecting function + */ + def collect[B](otherwise: String)(f: PartialFunction[A, B]): PostgresRowParser[B] = + PostgresRowParser(parent(_).flatMap( + f.lift(_).fold[ParserResult[B]](ParseError(SqlMappingError(otherwise)))(ParseSuccess(_))) + ) + + def flatMap[B](k: A => PostgresRowParser[B]): PostgresRowParser[B] = + PostgresRowParser(row => parent(row).flatMap(k(_)(row))) + + /** + * Combines this parser on the left of the parser `p` given as argument. + * + * @param p Parser on the right + * + * {{{ + * val populations: List[String ~ Int] = + * SQL("SELECT * FROM Country").as((str("name") ~ int("population")).*) + * }}} + */ + def ~[B](p: PostgresRowParser[B]): PostgresRowParser[A ~ B] = + PostgresRowParser(row => parent(row).flatMap(a => p(row).map(new ~(a, _)))) + + /** + * Combines this current parser with the one given as argument `p`, + * if and only if the current parser can first/on left side successfully + * parse a row, without keeping these values in parsed result. + * + * {{{ + * import anorm.{ SQL, SqlParser }, SqlParser.{ int, str } + * + * val String = SQL("SELECT * FROM test"). + * as((int("id") ~> str("val")).single) + * // row has to have an int column 'id' and a string 'val' one, + * // keeping only 'val' in result + * }}} + */ + def ~>[B](p: PostgresRowParser[B]): PostgresRowParser[B] = + PostgresRowParser(row => parent(row).flatMap(_ => p(row))) + + /** + * Combines this current parser with the one given as argument `p`, + * if and only if the current parser can first successfully + * parse a row, without keeping the values of the parser `p`. + * + * {{{ + * import anorm.{ SQL, SqlParser }, SqlParser.{ int, str } + * + * val Int = SQL("SELECT * FROM test"). + * as((int("id") <~ str("val")).single) + * // row has to have an int column 'id' and a string 'val' one, + * // keeping only 'id' in result + * }}} + */ + def <~[B](p: PostgresRowParser[B]): PostgresRowParser[A] = parent.~(p).map(_._1) + + // TODO: Scaladoc + def |[B >: A](p: PostgresRowParser[B]): PostgresRowParser[B] = PostgresRowParser { row => + this.apply(row) match { + case ParseError(_) => p(row) + case a => a + } + } + + /** + * Returns a row parser for optional column, + * that will turn missing or null column as None. + */ + def ? : PostgresRowParser[Option[A]] = PostgresRowParser { row => + this.apply(row) match { + case ParseSuccess(a) => ParseSuccess(Some(a)) + case ParseError(UnexpectedNullableFound(_)) | ParseError(_: ColumnNotFound) => + ParseSuccess(None) + case ParseError(_) => ParseSuccess(Option.empty[A]) + } + } + + /** Alias for [[flatMap]] */ + def >>[B](f: A => PostgresRowParser[B]): PostgresRowParser[B] = flatMap(f) + + /** + * Returns possibly empty list parsed from result. + * + * {{{ + * val price = 125 + * SQL"SELECT name FROM item WHERE price < \\$price".as(scalar[String].*) + * }}} + */ + def * : ResultSetParser[List[A]] = ResultSetParser.list(parent) + + /** + * Returns non empty list parse from result, + * or raise error if there is no result. + * + * {{{ + * import anorm.SQL + * import anorm.SqlParser.str + * + * val parser = str("title") ~ str("descr") + * SQL("SELECT title, descr FROM pages").as(parser.+) // at least 1 page + * }}} + */ + def + : ResultSetParser[List[A]] = ResultSetParser.nonEmptyList(parent) + + /** + * Returns a result set parser expecting exactly one row to parse. + * + * {{{ + * val b: Boolean = SQL("SELECT flag FROM Test WHERE id = :id"). + * on("id" -> 1).as(scalar[Boolean].single) + * }}} + * + * @see #singleOpt + */ + def single = ResultSetParser.single(parent) + + /** + * Returns a result set parser for none or one parsed row. + * + * {{{ + * val name: Option[String] = + * SQL("SELECT name FROM Country WHERE lang = :lang") + * .on("lang" -> "notFound").as(scalar[String].singleOpt) + * }}} + */ + def singleOpt: ResultSetParser[Option[A]] = ResultSetParser.singleOpt(parent) + +} + +/** Parses result from the cursor. */ +sealed trait ResultSetParser[+A] extends (Option[PostgresCursor] => ParserResult[A]) { + parent => + def map[B](f: A => B): ResultSetParser[B] = ResultSetParser(parent(_).map(f)) +} + +private[postgres] object ResultSetParser { + def apply[A](f: Option[PostgresCursor] => ParserResult[A]): ResultSetParser[A] = + new ResultSetParser[A] { cur => + def apply(cur: Option[PostgresCursor]): ParserResult[A] = f(cur) + } + + def list[A](p: PostgresRowParser[A]): ResultSetParser[List[A]] = { + // Performance note: sequence produces a List in reverse order, since appending to a + // List is an O(n) operation, and this is done n times, yielding O(n2) just to convert the + // result set to a List. Prepending is O(1), so we use prepend, and then reverse the result + // in the map function below. + @annotation.tailrec + def sequence(results: List[A], cur: Option[PostgresCursor]): ParserResult[List[A]] = + cur match { + case Some(c) => p(c.row) match { + case ParseSuccess(a) => sequence(a :: results, c.next) + case ParseError(msg) => ParseError(msg) + } + case _ => ParseSuccess(results.reverse) + } + + ResultSetParser { c => sequence(List.empty[A], c) } + } + + def nonEmptyList[A](p: PostgresRowParser[A]): ResultSetParser[List[A]] = + ResultSetParser(rows => + if (rows.isEmpty) ParseError(SqlMappingError("Empty Result Set")) + else list(p)(rows)) + + def single[A](p: PostgresRowParser[A]): ResultSetParser[A] = ResultSetParser { + case Some(cur) if cur.next.isEmpty => p(cur.row) + case None => ParseError(SqlMappingError("No rows when expecting a single one")) + case _ => ParseError(SqlMappingError( + "too many rows when expecting a single one")) + + } + + def singleOpt[A](p: PostgresRowParser[A]): ResultSetParser[Option[A]] = + ResultSetParser { + case Some(cur) if cur.next.isEmpty => p.map(Some(_))(cur.row) + case None => ParseSuccess(Option.empty[A]) + case _ => ParseError(SqlMappingError("Too many rows when expecting a single one")) + } + +} \ No newline at end of file diff --git a/parser/src/main/scala/com/outworkers/sql/postgres/parser/DataType.scala b/parser/src/main/scala/com/outworkers/sql/postgres/parser/DataType.scala new file mode 100644 index 0000000..89483a4 --- /dev/null +++ b/parser/src/main/scala/com/outworkers/sql/postgres/parser/DataType.scala @@ -0,0 +1,115 @@ +/* + * Copyright 2017 - 2018 Outworkers Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.outworkers.sql.postgres.parser + +import java.util.UUID + +import com.github.mauricio.async.db.RowData +import com.github.mauricio.async.db.general.ArrayRowData +import org.joda.time.DateTime + +import scala.util.Try + + +trait DataType[T] { + def parse(row: RowData, name: String): ParserResult[T] = { + + if (Option(row.apply(name)).isEmpty) { + Console.println(s"Unable to locate column $name, row only containes") + Console.println(row.asInstanceOf[ArrayRowData].columns.mkString("\n")) + } + + ParserResult(row(name).asInstanceOf[T]) + } + + def parse(row: RowData, index: Int): ParserResult[T] = { + ParserResult(row(index).asInstanceOf[T]) + } +} + +object DataType { + + implicit object StringDataType extends DataType[String] { + override def parse(row: RowData, name: String): ParserResult[String] = { + ParserResult(row(name).toString) + } + } + + implicit object IntDataType extends DataType[Int] { + override def parse(row: RowData, name: String): ParserResult[Int] = { + ParserResult(Integer.parseInt(row(name).toString)) + } + } + + implicit object DoubleDataType extends DataType[Double] { + override def parse(row: RowData, name: String): ParserResult[Double] = { + ParserResult(Double.unbox(row(name).asInstanceOf[AnyRef])) + } + } + + implicit object LongDataType extends DataType[Long] { + override def parse(row: RowData, name: String): ParserResult[Long] = { + ParserResult(Long.unbox(row(name).asInstanceOf[AnyRef])) + } + } + + implicit object ShortDataType extends DataType[Short] { + override def parse(row: RowData, name: String): ParserResult[Short] = { + ParserResult(Short.unbox(row(name).asInstanceOf[AnyRef])) + } + } + + implicit object FloatDataType extends DataType[Float] { + override def parse(row: RowData, name: String): ParserResult[Float] = { + ParserResult(Float.unbox(row(name).asInstanceOf[AnyRef])) + } + } + + + implicit object JodaDateTime extends DataType[DateTime] { + override def parse(row: RowData, name: String): ParserResult[DateTime] = { + ParserResult(row(name).asInstanceOf[org.joda.time.DateTime]) + } + } + + implicit val uuidParser: DataType[UUID] = new DataType[UUID] { + override def parse(row: RowData, name: String): ParserResult[UUID] = { + ParserResult(Try(UUID.fromString(row(name).toString))) + } + } + + def apply[T](implicit ev: DataType[T]): DataType[T] = ev + + def derive[S, T](fn: S => T)( + implicit ev: DataType[S] + ): DataType[T] = new DataType[T] { + override def parse(row: RowData, name: String): ParserResult[T] = { + ev.parse(row, name) map fn + } + } + + implicit def auto[T]: DataType[T] = new DataType[T] {} + + implicit def autoOpt[T]: DataType[Option[T]] = new DataType[Option[T]] { + override def parse(row: RowData, name: String): ParserResult[Option[T]] = { + Option(row(name)) match { + case Some(value) => ParserResult(Try(value.asInstanceOf[T]) map Some.apply) + case None => ParseSuccess(None) + } + } + } + +} \ No newline at end of file diff --git a/parser/src/main/scala/com/outworkers/sql/postgres/parser/Parser.scala b/parser/src/main/scala/com/outworkers/sql/postgres/parser/Parser.scala new file mode 100644 index 0000000..e774917 --- /dev/null +++ b/parser/src/main/scala/com/outworkers/sql/postgres/parser/Parser.scala @@ -0,0 +1,33 @@ +/* + * Copyright 2017 - 2018 Outworkers Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.outworkers.sql.postgres.parser + +import com.github.mauricio.async.db.RowData + +import scala.util.Try + +abstract class Parser[A] { outer => + + def apply(row: RowData): Try[A] + + def ~?[B](other: Parser[B]): Parser[(A, B)] = (v1: RowData) => { + for (a1 <- outer(v1); a2 <- other(v1)) yield a1 -> a2 + } +} + +object Parser { + def apply[A](implicit ev: Parser[A]): Parser[A] = ev +} \ No newline at end of file diff --git a/parser/src/main/scala/com/outworkers/sql/postgres/parser/ParserResult.scala b/parser/src/main/scala/com/outworkers/sql/postgres/parser/ParserResult.scala new file mode 100644 index 0000000..bca5697 --- /dev/null +++ b/parser/src/main/scala/com/outworkers/sql/postgres/parser/ParserResult.scala @@ -0,0 +1,82 @@ +/* + * Copyright 2017 - 2018 Outworkers Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.outworkers.sql.postgres.parser + +import scala.util.{Failure, Try} + +/** Anorm runtime exception */ +final case class SqlException(message: String) + extends Exception with scala.util.control.NoStackTrace { + override def getMessage: String = message +} + +/** Error from processing SQL */ +class SqlRequestError( + message: String, + cause: Throwable +) extends RuntimeException(message, cause) { + + /** Returns error as a failure. */ + def toFailure = Failure(SqlException(message)) +} + +object SqlRequestError { + + def apply(failure: Throwable): SqlRequestError = { + new SqlRequestError(failure.getMessage, failure) + } + + def apply[T](failure: Failure[T]): SqlRequestError = { + new SqlRequestError(failure.exception.getMessage, failure.exception) + } +} + +/** Parsed SQL result. */ +sealed trait ParserResult[+A] { self => + + def toOption: Option[A] = fold(err => Option.empty[A], Some.apply) + + def flatMap[B](k: A => ParserResult[B]): ParserResult[B] = self match { + case ParseSuccess(a) => k(a) + case e @ ParseError(_) => e + } + + def map[B](f: A => B): ParserResult[B] = self match { + case ParseSuccess(a) => ParseSuccess(f(a)) + case e @ ParseError(_) => e + } + + /** + * Either applies function `e` if result is erroneous, + * or function `f` with successful result if any. + */ + def fold[B](e: SqlRequestError => B, f: A => B): B = self match { + case ParseSuccess(a) => f(a) + case ParseError(err) => e(err) + } +} + +object ParserResult { + + def apply[T](fn: => T): ParserResult[T] = ParserResult(Try(fn)) + + def apply[T](source: Try[T]): ParserResult[T] = { + source.fold(ex => ParseError(SqlRequestError(ex)), ParseSuccess.apply) + } +} + +case class ParseError(msg: SqlRequestError) extends ParserResult[Nothing] +case class ParseSuccess[A](a: A) extends ParserResult[A] \ No newline at end of file diff --git a/parser/src/main/scala/com/outworkers/sql/postgres/parser/TupleFlattener.scala b/parser/src/main/scala/com/outworkers/sql/postgres/parser/TupleFlattener.scala new file mode 100644 index 0000000..00dad56 --- /dev/null +++ b/parser/src/main/scala/com/outworkers/sql/postgres/parser/TupleFlattener.scala @@ -0,0 +1,190 @@ +/* + * Copyright 2017 - 2018 Outworkers Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.outworkers.sql.postgres.parser + +sealed case class TupleFlattener[F](fn: F) + +/** Conversions to flatten columns to tuple. */ +object TupleFlattener extends TupleFlattenerPriority21 +// Inheritance hack to prioritize implicit conversions + +/** + * Conversion from 2-column tuple-like to [[scala.Tuple2]], + * with resolution priority 1. + */ +sealed trait TupleFlattenerPriority1 { + implicit def flattenerTo2[T1, T2]: TupleFlattener[(T1 ~ T2) => (T1, T2)] = TupleFlattener[(T1 ~ T2) => (T1, T2)] { case (c1 ~ c2) => (c1, c2) } +} + +/** + * Conversion from 3-column tuple-like to [[scala.Tuple3]], + * with resolution priority 2. + */ +sealed trait TupleFlattenerPriority2 extends TupleFlattenerPriority1 { + implicit def flattenerTo3[T1, T2, T3]: TupleFlattener[(T1 ~ T2 ~ T3) => (T1, T2, T3)] = TupleFlattener[(T1 ~ T2 ~ T3) => (T1, T2, T3)] { case (c1 ~ c2 ~ c3) => (c1, c2, c3) } +} + +/** + * Conversion from 4-column tuple-like to [[scala.Tuple4]], + * with resolution priority 3. + */ +sealed trait TupleFlattenerPriority3 extends TupleFlattenerPriority2 { + implicit def flattenerTo4[T1, T2, T3, T4]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4) => (T1, T2, T3, T4)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4) => (T1, T2, T3, T4)] { case (c1 ~ c2 ~ c3 ~ c4) => (c1, c2, c3, c4) } +} + +/** + * Conversion from 5-column tuple-like to [[scala.Tuple5]], + * with resolution priority 4. + */ +sealed trait TupleFlattenerPriority4 extends TupleFlattenerPriority3 { + implicit def flattenerTo5[T1, T2, T3, T4, T5]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5) => (T1, T2, T3, T4, T5)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5) => (T1, T2, T3, T4, T5)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5) => (c1, c2, c3, c4, c5) } +} + +/** + * Conversion from 6-column tuple-like to [[scala.Tuple6]], + * with resolution priority 5. + */ +sealed trait TupleFlattenerPriority5 extends TupleFlattenerPriority4 { + implicit def flattenerTo6[T1, T2, T3, T4, T5, T6]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6) => (T1, T2, T3, T4, T5, T6)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6) => (T1, T2, T3, T4, T5, T6)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6) => (c1, c2, c3, c4, c5, c6) } +} + +/** + * Conversion from 7-column tuple-like to [[scala.Tuple7]], + * with resolution priority 6. + */ +sealed trait TupleFlattenerPriority6 extends TupleFlattenerPriority5 { + implicit def flattenerTo7[T1, T2, T3, T4, T5, T6, T7]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7) => (T1, T2, T3, T4, T5, T6, T7)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7) => (T1, T2, T3, T4, T5, T6, T7)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6 ~ c7) => (c1, c2, c3, c4, c5, c6, c7) } +} + +/** + * Conversion from 8-column tuple-like to [[scala.Tuple8]], + * with resolution priority 7. + */ +sealed trait TupleFlattenerPriority7 extends TupleFlattenerPriority6 { + implicit def flattenerTo8[T1, T2, T3, T4, T5, T6, T7, T8]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8) => (T1, T2, T3, T4, T5, T6, T7, T8)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8) => (T1, T2, T3, T4, T5, T6, T7, T8)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6 ~ c7 ~ c8) => (c1, c2, c3, c4, c5, c6, c7, c8) } +} + +/** + * Conversion from 9-column tuple-like to [[scala.Tuple9]], + * with resolution priority 8. + */ +sealed trait TupleFlattenerPriority8 extends TupleFlattenerPriority7 { + implicit def flattenerTo9[T1, T2, T3, T4, T5, T6, T7, T8, T9]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9) => (T1, T2, T3, T4, T5, T6, T7, T8, T9)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9) => (T1, T2, T3, T4, T5, T6, T7, T8, T9)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6 ~ c7 ~ c8 ~ c9) => (c1, c2, c3, c4, c5, c6, c7, c8, c9) } +} + +/** + * Conversion from 10-column tuple-like to [[scala.Tuple10]], + * with resolution priority 9. + */ +sealed trait TupleFlattenerPriority9 extends TupleFlattenerPriority8 { + implicit def flattenerTo10[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6 ~ c7 ~ c8 ~ c9 ~ c10) => (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10) } +} + +/** + * Conversion from 11-column tuple-like to [[scala.Tuple11]], + * with resolution priority 10. + */ +sealed trait TupleFlattenerPriority10 extends TupleFlattenerPriority9 { + implicit def flattenerTo11[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6 ~ c7 ~ c8 ~ c9 ~ c10 ~ c11) => (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11) } +} + +/** + * Conversion from 12-column tuple-like to [[scala.Tuple12]], + * with resolution priority 11. + */ +sealed trait TupleFlattenerPriority11 extends TupleFlattenerPriority10 { + implicit def flattenerTo12[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6 ~ c7 ~ c8 ~ c9 ~ c10 ~ c11 ~ c12) => (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12) } +} + +/** + * Conversion from 13-column tuple-like to [[scala.Tuple13]], + * with resolution priority 12. + */ +sealed trait TupleFlattenerPriority12 extends TupleFlattenerPriority11 { + implicit def flattenerTo13[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6 ~ c7 ~ c8 ~ c9 ~ c10 ~ c11 ~ c12 ~ c13) => (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13) } +} + +/** + * Conversion from 14-column tuple-like to [[scala.Tuple14]], + * with resolution priority 13. + */ +sealed trait TupleFlattenerPriority13 extends TupleFlattenerPriority12 { + implicit def flattenerTo14[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6 ~ c7 ~ c8 ~ c9 ~ c10 ~ c11 ~ c12 ~ c13 ~ c14) => (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14) } +} + +/** + * Conversion from 15-column tuple-like to [[scala.Tuple15]], + * with resolution priority 14. + */ +sealed trait TupleFlattenerPriority14 extends TupleFlattenerPriority13 { + implicit def flattenerTo15[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14 ~ T15) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14 ~ T15) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6 ~ c7 ~ c8 ~ c9 ~ c10 ~ c11 ~ c12 ~ c13 ~ c14 ~ c15) => (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15) } +} + +/** + * Conversion from 16-column tuple-like to [[scala.Tuple16]], + * with resolution priority 15. + */ +sealed trait TupleFlattenerPriority15 extends TupleFlattenerPriority14 { + implicit def flattenerTo16[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14 ~ T15 ~ T16) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14 ~ T15 ~ T16) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6 ~ c7 ~ c8 ~ c9 ~ c10 ~ c11 ~ c12 ~ c13 ~ c14 ~ c15 ~ c16) => (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16) } +} + +/** + * Conversion from 17-column tuple-like to [[scala.Tuple17]], + * with resolution priority 16. + */ +sealed trait TupleFlattenerPriority16 extends TupleFlattenerPriority15 { + implicit def flattenerTo17[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14 ~ T15 ~ T16 ~ T17) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14 ~ T15 ~ T16 ~ T17) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6 ~ c7 ~ c8 ~ c9 ~ c10 ~ c11 ~ c12 ~ c13 ~ c14 ~ c15 ~ c16 ~ c17) => (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17) } +} + +/** + * Conversion from 18-column tuple-like to [[scala.Tuple18]], + * with resolution priority 17. + */ +sealed trait TupleFlattenerPriority17 extends TupleFlattenerPriority16 { + implicit def flattenerTo18[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14 ~ T15 ~ T16 ~ T17 ~ T18) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14 ~ T15 ~ T16 ~ T17 ~ T18) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6 ~ c7 ~ c8 ~ c9 ~ c10 ~ c11 ~ c12 ~ c13 ~ c14 ~ c15 ~ c16 ~ c17 ~ c18) => (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18) } +} + +/** + * Conversion from 19-column tuple-like to [[scala.Tuple19]], + * with resolution priority 18. + */ +sealed trait TupleFlattenerPriority18 extends TupleFlattenerPriority17 { + implicit def flattenerTo19[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14 ~ T15 ~ T16 ~ T17 ~ T18 ~ T19) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14 ~ T15 ~ T16 ~ T17 ~ T18 ~ T19) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6 ~ c7 ~ c8 ~ c9 ~ c10 ~ c11 ~ c12 ~ c13 ~ c14 ~ c15 ~ c16 ~ c17 ~ c18 ~ c19) => (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19) } +} + +/** + * Conversion from 20-column tuple-like to [[scala.Tuple20]], + * with resolution priority 19. + */ +sealed trait TupleFlattenerPriority19 extends TupleFlattenerPriority18 { + implicit def flattenerTo20[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14 ~ T15 ~ T16 ~ T17 ~ T18 ~ T19 ~ T20) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14 ~ T15 ~ T16 ~ T17 ~ T18 ~ T19 ~ T20) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6 ~ c7 ~ c8 ~ c9 ~ c10 ~ c11 ~ c12 ~ c13 ~ c14 ~ c15 ~ c16 ~ c17 ~ c18 ~ c19 ~ c20) => (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20) } +} + +/** + * Conversion from 21-column tuple-like to [[scala.Tuple21]], + * with resolution priority 20. + */ +sealed trait TupleFlattenerPriority20 extends TupleFlattenerPriority19 { + implicit def flattenerTo21[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14 ~ T15 ~ T16 ~ T17 ~ T18 ~ T19 ~ T20 ~ T21) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14 ~ T15 ~ T16 ~ T17 ~ T18 ~ T19 ~ T20 ~ T21) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6 ~ c7 ~ c8 ~ c9 ~ c10 ~ c11 ~ c12 ~ c13 ~ c14 ~ c15 ~ c16 ~ c17 ~ c18 ~ c19 ~ c20 ~ c21) => (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20, c21) } +} + +/** + * Conversion from 22-column tuple-like to [[scala.Tuple22]], + * with resolution priority 21. + */ +sealed trait TupleFlattenerPriority21 extends TupleFlattenerPriority20 { + implicit def flattenerTo22[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22]: TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14 ~ T15 ~ T16 ~ T17 ~ T18 ~ T19 ~ T20 ~ T21 ~ T22) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22)] = TupleFlattener[(T1 ~ T2 ~ T3 ~ T4 ~ T5 ~ T6 ~ T7 ~ T8 ~ T9 ~ T10 ~ T11 ~ T12 ~ T13 ~ T14 ~ T15 ~ T16 ~ T17 ~ T18 ~ T19 ~ T20 ~ T21 ~ T22) => (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22)] { case (c1 ~ c2 ~ c3 ~ c4 ~ c5 ~ c6 ~ c7 ~ c8 ~ c9 ~ c10 ~ c11 ~ c12 ~ c13 ~ c14 ~ c15 ~ c16 ~ c17 ~ c18 ~ c19 ~ c20 ~ c21 ~ c22) => (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20, c21, c22) } +} \ No newline at end of file diff --git a/parser/src/main/scala/com/outworkers/sql/postgres/parser/package.scala b/parser/src/main/scala/com/outworkers/sql/postgres/parser/package.scala new file mode 100644 index 0000000..961851b --- /dev/null +++ b/parser/src/main/scala/com/outworkers/sql/postgres/parser/package.scala @@ -0,0 +1,122 @@ +/* + * Copyright 2017 - 2018 Outworkers Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.outworkers.sql.postgres + +import java.time.{Instant, ZoneId, ZonedDateTime} + +import anorm.PrebuiltPreparedStatement +import com.github.mauricio.async.db.{Connection, QueryResult, RowData} +import com.outworkers.sql.postgres.parser._ +import org.joda.time.DateTime + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success, Try} + +package object postgres { + + implicit class ConnectionAugmenter(val connection: Connection) extends AnyVal { + def prepare(query: String): PrebuiltPreparedStatement = { + PrebuiltPreparedStatement(query, Map.empty)(connection) + } + } + + def scalar[T <: AnyVal](implicit transformer: DataType[T]): PostgresRowParser[T] = + new PostgresRowParser[T] { + def apply(row: RowData): ParserResult[T] = { + transformer.parse(row, 0) + } + } + + implicit class OptRowAugmenter(val data: Option[RowData]) extends AnyVal { + def as[T : Parser]: Try[T] = for (dt <- data.asTry; parse <- Parser[T].apply(dt)) yield parse + } + + implicit class FutureQueryAug(val source: Future[QueryResult]) extends AnyVal { + + def asBool(implicit ec: ExecutionContext): Future[Boolean] = { + source map (_.rowsAffected > 0) + } + + def asOpt[T](implicit parser: PostgresRowParser[T], ec: ExecutionContext): Future[Option[T]] = { + source flatMap { result => + result.rows match { + case Some(resultSet) => + if (resultSet.nonEmpty) { + parser(resultSet.head) match { + case ParseSuccess(value) => Future.successful(Some(value)) + case ParseError(err) => Future.failed(err) + } + } else { + Future.successful(None) + } + case None => Future.successful(None) + } + } + } + + def queryResultToCursor(res: QueryResult): Option[PostgresCursor] = { + res.rows flatMap PostgresCursor.build + } + + def as[T](parser: ResultSetParser[T])(implicit ec: ExecutionContext): Future[T] = { + source map (rows => parser(queryResultToCursor(rows))) flatMap { + case ParseSuccess(projection) => Future.successful(projection) + case ParseError(msg) => Future.failed(msg) + } + } + + def fetch[T](implicit parser: PostgresRowParser[T], ec: ExecutionContext): Future[IndexedSeq[T]] = { + source flatMap { result => + result.rows match { + case Some(resultSet) => + val futures = resultSet.map { el => + parser(el).fold(err => Future.failed(err), Future.successful) + } + + Future.sequence(futures) + case None => Future.successful(IndexedSeq.empty[T]) + } + } + } + } + + implicit class OptAug[T](val opt: Option[T]) extends AnyVal { + def asTry: Try[T] = opt.fold[Try[T]](Failure[T](new RuntimeException("Row data was empty")))(Success(_)) + } + + implicit class RowAugmenter(val data: RowData) extends AnyVal { + def as[T : Parser]: T = Parser[T].apply(data).get + + def asOpt[T : Parser]: Option[T] = Parser[T].apply(data).toOption + } + + def get[T : DataType](name: String): PostgresRowParser[T] = PostgresRowParser[T](name) + + def getOpt[T : DataType](name: String): PostgresRowParser[Option[T]] = PostgresRowParser[Option[T]](name) + + def zonedDateTime(columnName: String): PostgresRowParser[ZonedDateTime] = { + get[DateTime](columnName).map { dt => + ZonedDateTime.ofInstant(Instant.ofEpochMilli(dt.getMillis), ZoneId.of("UTC")).withFixedOffsetZone() + } + } + + def zonedDateTimeOpt(columnName: String): PostgresRowParser[Option[ZonedDateTime]] = { + get[Option[ZonedDateTime]](columnName).map(_.map(_.withFixedOffsetZone())) + } + + def flatten[T1, T2, R](implicit f: TupleFlattener[(T1 ~ T2) => R]): ((T1 ~ T2) => R) = f.fn + +} \ No newline at end of file