Skip to content

Commit

Permalink
Merge pull request #220 from takapi327/feature/2024-05-Create-query-h…
Browse files Browse the repository at this point in the history
…elper

Feature/2024 05 create query helper
  • Loading branch information
takapi327 authored May 29, 2024
2 parents 16098d0 + f84dd7b commit f788608
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 4 deletions.
5 changes: 3 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ lazy val dsl = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.module("dsl", "Projects that provide a way to connect to the database")
.settings(
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-effect" % "3.5.4",
"org.scalatest" %%% "scalatest" % "3.2.18" % Test
"org.typelevel" %%% "cats-effect" % "3.5.4",
"org.scalatest" %%% "scalatest" % "3.2.18" % Test,
"org.typelevel" %%% "munit-cats-effect" % "2.0.0" % Test
)
)
.dependsOn(queryBuilder)
Expand Down
155 changes: 154 additions & 1 deletion module/ldbc-dsl/src/main/scala/ldbc/dsl/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@

package ldbc

import cats.{ Foldable, Functor, Reducible }
import cats.data.NonEmptyList
import cats.syntax.all.*

import cats.effect.{ IO, Sync }

import ldbc.sql.*
import ldbc.dsl.syntax.*

package object dsl:
Expand All @@ -16,7 +21,155 @@ package object dsl:
extends StringContextSyntax[F],
ConnectionSyntax[F],
QuerySyntax[F],
CommandSyntax[F]
CommandSyntax[F]:

// The following helper functions for building SQL models are rewritten from doobie fragments for ldbc SQL models.
// see: https://github.com/tpolecat/doobie/blob/main/modules/core/src/main/scala/doobie/util/fragments.scala

/** Returns `VALUES (v0), (v1), ...`. */
def values[M[_]: Reducible, T](vs: M[T])(using Parameter[F, T]): SQL[F] =
q"VALUES" ++ comma(vs.toNonEmptyList.map(v => parentheses(sql"$v")))

/** Returns `VALUES (s0, s1, ...)`. */
def values[M[_]: Reducible](vs: M[SQL[F]]): SQL[F] =
q"VALUES" ++ parentheses(comma(vs.toNonEmptyList))

/** Returns `(sql IN (v0, v1, ...))`. */
def in[T](s: SQL[F], v0: T, v1: T, vs: T*)(using Parameter[F, T]): SQL[F] =
in(s, NonEmptyList(v0, v1 :: vs.toList))

/** Returns `(sql IN (s0, s1, ...))`. */
def in[M[_]: Reducible: Functor, T](s: SQL[F], vs: M[T])(using Parameter[F, T]): SQL[F] =
parentheses(s ++ q" IN " ++ parentheses(comma(vs.map(v => p"$v"))))

def inOpt[M[_]: Foldable, T](s: SQL[F], vs: M[T])(using Parameter[F, T]): Option[SQL[F]] =
NonEmptyList.fromFoldable(vs).map(nel => in(s, nel))

/** Returns `(sql NOT IN (v0, v1, ...))`. */
def notIn[T](s: SQL[F], v0: T, v1: T, vs: T*)(using Parameter[F, T]): SQL[F] =
notIn(s, NonEmptyList(v0, v1 :: vs.toList))

/** Returns `(sql NOT IN (v0, v1, ...))`, or `true` for empty `fs`. */
def notIn[M[_]: Reducible: Functor, T](s: SQL[F], vs: M[T])(using Parameter[F, T]): SQL[F] =
parentheses(s ++ q" NOT IN " ++ parentheses(comma(vs.map(v => p"$v"))))

def notInOpt[M[_]: Foldable, T](s: SQL[F], vs: M[T])(using Parameter[F, T]): Option[SQL[F]] =
NonEmptyList.fromFoldable(vs).map(nel => notIn(s, nel))

/** Returns `(s1 AND s2 AND ... sn)` for a non-empty collection. */
def and[M[_]: Reducible](ss: M[SQL[F]], grouping: Boolean = true): SQL[F] =
val expr = ss.reduceLeftTo(s => parentheses(s))((s1, s2) => s1 ++ q" AND " ++ parentheses(s2))
if grouping then parentheses(expr) else expr

/** Returns `(s1 AND s2 AND ... sn)`. */
def and(s1: SQL[F], s2: SQL[F], ss: SQL[F]*): SQL[F] =
and(NonEmptyList(s1, s2 :: ss.toList))

/** Returns `(s1 AND s2 AND ... sn)` for a non-empty collection. */
def andOpt[M[_]: Foldable](vs: M[SQL[F]], grouping: Boolean = true): Option[SQL[F]] =
NonEmptyList.fromFoldable(vs).map(nel => and(nel, grouping))

/** Returns `(s1 AND s2 AND ... sn)` for all defined sql, returning Empty SQL if there are no defined sql */
def andOpt(s1: Option[SQL[F]], s2: Option[SQL[F]], ss: Option[SQL[F]]*): Option[SQL[F]] =
andOpt((s1 :: s2 :: ss.toList).flatten)

/** Similar to andOpt, but defaults to TRUE if passed an empty collection */
def andFallbackTrue[M[_]: Foldable](ss: M[SQL[F]]): SQL[F] =
andOpt(ss).getOrElse(q"TRUE")

/** Returns `(s1 OR s2 OR ... sn)` for a non-empty collection. */
def or[M[_]: Reducible](ss: M[SQL[F]], grouping: Boolean = true): SQL[F] =
val expr = ss.reduceLeftTo(s => parentheses(s))((s1, s2) => s1 ++ q" OR " ++ parentheses(s2))
if grouping then parentheses(expr) else expr

/** Returns `(s1 OR s2 OR ... sn)`. */
def or(s1: SQL[F], s2: SQL[F], ss: SQL[F]*): SQL[F] =
or(NonEmptyList(s1, s2 :: ss.toList))

/** Returns `(s1 OR s2 OR ... sn)` for all defined sql, returning Empty SQL if there are no defined sql */
def orOpt[M[_]: Foldable](vs: M[SQL[F]], grouping: Boolean = true): Option[SQL[F]] =
NonEmptyList.fromFoldable(vs).map(nel => or(nel, grouping))

/** Returns `(s1 OR s2 OR ... sn)` for all defined sql, returning Empty SQL if there are no defined sql */
def orOpt(s1: Option[SQL[F]], s2: Option[SQL[F]], ss: Option[SQL[F]]*): Option[SQL[F]] =
orOpt((s1 :: s2 :: ss.toList).flatten)

/** Similar to orOpt, but defaults to FALSE if passed an empty collection */
def orFallbackFalse[M[_]: Foldable](ss: M[SQL[F]]): SQL[F] =
orOpt(ss).getOrElse(q"FALSE")

/** Returns `WHERE s1 AND s2 AND ... sn`. */
def whereAnd(s1: SQL[F], ss: SQL[F]*): SQL[F] =
whereAnd(NonEmptyList(s1, ss.toList))

/** Returns `WHERE s1 AND s2 AND ... sn` or the empty sql if `ss` is empty. */
def whereAnd[M[_]: Reducible](ss: M[SQL[F]]): SQL[F] =
q"WHERE " ++ and(ss, grouping = false)

/** Returns `WHERE s1 AND s2 AND ... sn` for defined `s`, if any, otherwise the empty sql. */
def whereAndOpt(s1: Option[SQL[F]], s2: Option[SQL[F]], ss: Option[SQL[F]]*): SQL[F] =
whereAndOpt((s1 :: s2 :: ss.toList).flatten)

/** Returns `WHERE s1 AND s2 AND ... sn` if collection is not empty. If collection is empty returns an empty sql. */
def whereAndOpt[M[_]: Foldable](ss: M[SQL[F]]): SQL[F] =
NonEmptyList.fromFoldable(ss) match
case Some(nel) => whereAnd(nel)
case None => q""

/** Returns `WHERE s1 OR s2 OR ... sn`. */
def whereOr(s1: SQL[F], ss: SQL[F]*): SQL[F] =
whereOr(NonEmptyList(s1, ss.toList))

/** Returns `WHERE s1 OR s2 OR ... sn` or the empty sql if `ss` is empty. */
def whereOr[M[_]: Reducible](ss: M[SQL[F]]): SQL[F] =
q"WHERE " ++ or(ss, grouping = false)

/** Returns `WHERE s1 OR s2 OR ... sn` for defined `s`, if any, otherwise the empty sql. */
def whereOrOpt(s1: Option[SQL[F]], s2: Option[SQL[F]], ss: Option[SQL[F]]*): SQL[F] =
whereOrOpt((s1 :: s2 :: ss.toList).flatten)

/** Returns `WHERE s1 OR s2 OR ... sn` if collection is not empty. If collection is empty returns an empty sql. */
def whereOrOpt[M[_]: Foldable](ss: M[SQL[F]]): SQL[F] =
NonEmptyList.fromFoldable(ss) match
case Some(nel) => whereOr(nel)
case None => q""

/** Returns `SET s1, s2, ... sn`. */
def set(s1: SQL[F], ss: SQL[F]*): SQL[F] =
set(NonEmptyList(s1, ss.toList))

/** Returns `SET s1, s2, ... sn`. */
def set[M[_]: Reducible](fs: M[SQL[F]]): SQL[F] =
q"SET " ++ comma(fs)

/** Returns `(sql)`. */
def parentheses(s: SQL[F]): SQL[F] =
q"(" ++ s ++ q")"

/** Returns `s1, s2, ... sn`. */
def comma(s1: SQL[F], s2: SQL[F], ss: SQL[F]*): SQL[F] =
comma(NonEmptyList(s1, s2 :: ss.toList))

/** Returns `s1, s2, ... sn`. */
def comma[M[_]: Reducible](ss: M[SQL[F]]): SQL[F] =
ss.nonEmptyIntercalate(q",")

/** Returns `ORDER BY s1, s2, ... sn`. */
def orderBy(s1: SQL[F], ss: SQL[F]*): SQL[F] =
orderBy(NonEmptyList(s1, ss.toList))

def orderBy[M[_]: Reducible](ss: M[SQL[F]]): SQL[F] =
q"ORDER BY " ++ comma(ss)

/** Returns `ORDER BY s1, s2, ... sn` or the empty sql if `ss` is empty. */
def orderByOpt[M[_]: Foldable](ss: M[SQL[F]]): SQL[F] =
NonEmptyList.fromFoldable(ss) match
case Some(nel) => orderBy(nel)
case None => q""

/** Returns `ORDER BY s1, s2, ... sn` for defined `s`, if any, otherwise the empty sql. */
def orderByOpt(s1: Option[SQL[F]], s2: Option[SQL[F]], ss: Option[SQL[F]]*): SQL[F] =
orderByOpt((s1 :: s2 :: ss.toList).flatten)

/**
* Top-level imports provide aliases for the most commonly used types and modules. A typical starting set of imports
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,20 @@ import ldbc.dsl.Mysql
trait StringContextSyntax[F[_]: Temporal]:

extension (sc: StringContext)
inline def sql(inline args: ParameterBinder[F]*): SQL[F] =
def sql(args: ParameterBinder[F]*): SQL[F] =
val strings = sc.parts.iterator
val expressions = args.iterator
Mysql(strings.mkString("?"), expressions.toList)

def q(args: String*): SQL[F] =
val strings = sc.parts.iterator
val expressions = args.iterator
val query = strings.zipAll(expressions, "", "").foldLeft("") {
case (acc, (str, expr)) => acc + str + expr
}
Mysql(query, List.empty)

def p(args: ParameterBinder[F]*): SQL[F] =
val strings = sc.parts.iterator
val expressions = args.iterator
Mysql(strings.mkString("?"), expressions.toList)
105 changes: 105 additions & 0 deletions module/ldbc-dsl/src/test/scala/ldbc/dsl/HelperFunctionTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Copyright (c) 2023-2024 by Takahiko Tominaga
* This software is licensed under the MIT License (MIT).
* For more information see LICENSE or https://opensource.org/licenses/MIT
*/

package ldbc.dsl

import cats.data.NonEmptyList

import munit.CatsEffectSuite

import ldbc.dsl.io.*

class HelperFunctionTest extends munit.CatsEffectSuite:

test(
"The statement that constructs VALUES with multiple values of the same type will be the same as the string specified."
) {
val sql = q"INSERT INTO `table` (`column1`, `column2`) " ++ values(NonEmptyList.of(1, 2))
assertEquals(sql.statement, "INSERT INTO `table` (`column1`, `column2`) VALUES(?),(?)")
}

test("A statement that constructs VALUES in multiple sql is the same as the specified string.") {
case class Value(c1: Int, c2: String)
val vs: NonEmptyList[Value] = NonEmptyList.of(Value(1, "value1"), Value(2, "value2"))
val sql =
q"INSERT INTO `table` (`column1`, `column2`) VALUES" ++ comma(vs.map(v => parentheses(p"${ v.c1 },${ v.c2 }")))
assertEquals(sql.statement, "INSERT INTO `table` (`column1`, `column2`) VALUES(?,?),(?,?)")
}

test("The statement that constructs WHERE AND with multiple SQLs will be the same as the string specified.") {
val sql = q"SELECT * FROM `table` " ++ whereAndOpt(Some(q"column1 = ?"), Some(q"column2 = ?"))
assertEquals(sql.statement, "SELECT * FROM `table` WHERE (column1 = ?) AND (column2 = ?)")
}

test("The statement that constructs the IN clause with multiple values is the same as the string specified.") {
val sql = q"SELECT * FROM `table` WHERE " ++ in(q"`column`", NonEmptyList.of(1, 2))
assertEquals(sql.statement, "SELECT * FROM `table` WHERE (`column` IN (?,?))")
}

test("The statement that constructs the IN clause with multiple values is the same as the string specified.") {
val sql = q"SELECT * FROM `table` WHERE " ++ notIn(q"`column`", 1, 2)
assertEquals(sql.statement, "SELECT * FROM `table` WHERE (`column` NOT IN (?,?))")
}

test("The statement that constructs the AND clause with multiple values is the same as the string specified.") {
val sql = q"SELECT * FROM `table` WHERE" ++ and(q"`column1` = 1", q"`column2` = 2")
assertEquals(sql.statement, "SELECT * FROM `table` WHERE((`column1` = 1) AND (`column2` = 2))")
}

test("The statement that constructs the AND clause with multiple values is the same as the string specified.") {
val sql = q"SELECT * FROM `table` WHERE" ++ andOpt(Some(q"`column1` = 1"), Some(q"`column2` = 2")).getOrElse(q"")
assertEquals(sql.statement, "SELECT * FROM `table` WHERE((`column1` = 1) AND (`column2` = 2))")
}

test("The statement that constructs the OR clause with multiple values is the same as the string specified.") {
val sql = q"SELECT * FROM `table` WHERE" ++ or(NonEmptyList.of(q"`column1` = 1", q"`column2` = 2"))
assertEquals(sql.statement, "SELECT * FROM `table` WHERE((`column1` = 1) OR (`column2` = 2))")
}

test("The statement that constructs the OR clause with multiple values is the same as the string specified.") {
val sql = q"SELECT * FROM `table` WHERE" ++ orOpt(Some(q"`column1` = 1"), Some(q"`column2` = 2")).getOrElse(q"")
assertEquals(sql.statement, "SELECT * FROM `table` WHERE((`column1` = 1) OR (`column2` = 2))")
}

test("The statement that constructs WHERE AND with multiple SQLs will be the same as the string specified.") {
val sql = q"SELECT * FROM `table` " ++ whereAnd(q"column1 = ?", q"column2 = ?")
assertEquals(sql.statement, "SELECT * FROM `table` WHERE (column1 = ?) AND (column2 = ?)")
}

test("The statement that constructs WHERE AND with multiple SQLs will be the same as the string specified.") {
val sql = q"SELECT * FROM `table` " ++ whereAndOpt(Some(q"column1 = ?"), Some(q"column2 = ?"))
assertEquals(sql.statement, "SELECT * FROM `table` WHERE (column1 = ?) AND (column2 = ?)")
}

test("The statement that constructs WHERE OR with multiple SQLs will be the same as the string specified.") {
val sql = q"SELECT * FROM `table` " ++ whereOr(q"column1 = ?", q"column2 = ?")
assertEquals(sql.statement, "SELECT * FROM `table` WHERE (column1 = ?) OR (column2 = ?)")
}

test("The statement that constructs WHERE OR with multiple SQLs will be the same as the string specified.") {
val sql = q"SELECT * FROM `table` " ++ whereOrOpt(Some(q"column1 = ?"), Some(q"column2 = ?"))
assertEquals(sql.statement, "SELECT * FROM `table` WHERE (column1 = ?) OR (column2 = ?)")
}

test("The statement that constructs SET with multiple SQLs will be the same as the string specified.") {
val sql = q"UPDATE `table` " ++ set(q"column1 = ?", q"column2 = ?")
assertEquals(sql.statement, "UPDATE `table` SET column1 = ?,column2 = ?")
}

test("The statement that constructs parentheses with SQL will be the same as the string specified.") {
val sql = parentheses(q"column1 = ?")
assertEquals(sql.statement, "(column1 = ?)")
}

test("The statement that constructs comma with multiple SQLs will be the same as the string specified.") {
val sql = comma(q"column1 = ?", q"column2 = ?")
assertEquals(sql.statement, "column1 = ?,column2 = ?")
}

test("The statement that constructs ORDER BY with multiple SQLs will be the same as the string specified.") {
val sql = q"SELECT * FROM `table` " ++ orderBy(q"column1", q"column2")
assertEquals(sql.statement, "SELECT * FROM `table` ORDER BY column1,column2")
}

0 comments on commit f788608

Please sign in to comment.