-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #242 from gemini-hlsw/like
switch Like to take an optional term
- Loading branch information
Showing
7 changed files
with
345 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// Copyright (c) 2016-2020 Association of Universities for Research in Astronomy, Inc. (AURA) | ||
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause | ||
|
||
package edu.gemini.grackle | ||
package sql | ||
|
||
import scala.util.matching.Regex | ||
|
||
case class Like private[sql] (x: Term[_], pattern: String, caseInsensitive: Boolean) extends Predicate { | ||
lazy val r = Like.likeToRegex(pattern, caseInsensitive) | ||
def apply(c: Cursor): Result[Boolean] = | ||
x(c).map(_ match { | ||
case s: String => r.matches(s) | ||
case Some(s: String) => r.matches(s) | ||
case None => false | ||
case other => sys.error(s"Expected value of type String or Option[String], found $other") | ||
}) | ||
def children = List(x) | ||
} | ||
|
||
object Like extends Like0 { | ||
private[sql] def apply(x: Term[_], pattern: String, caseInsensitive: Boolean): Like = | ||
new Like(x, pattern, caseInsensitive) | ||
|
||
private def likeToRegex(pattern: String, caseInsensitive: Boolean): Regex = { | ||
val csr = ("^"+pattern.replace("%", ".*").replace("_", ".")+"$") | ||
(if (caseInsensitive) s"(?i:$csr)" else csr).r | ||
} | ||
} | ||
|
||
trait Like0 { | ||
trait PossiblyOptionString[T] | ||
object PossiblyOptionString extends PossiblyOptionString0 { | ||
implicit val sInst: PossiblyOptionString[String] = new PossiblyOptionString[String] {} | ||
} | ||
trait PossiblyOptionString0 { | ||
implicit val osInst: PossiblyOptionString[Option[String]] = new PossiblyOptionString[Option[String]] {} | ||
} | ||
|
||
def apply[T](x: Term[T], pattern: String, caseInsensitive: Boolean)(implicit @annotation.nowarn ev: PossiblyOptionString[T]): Predicate = | ||
new Like(x, pattern, caseInsensitive) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
CREATE TABLE likes ( | ||
id INTEGER PRIMARY KEY, | ||
notnullable TEXT NOT NULL, | ||
nullable TEXT | ||
); | ||
|
||
COPY likes (id, notnullable, nullable) FROM STDIN WITH DELIMITER '|'; | ||
1|foo|\N | ||
2|bar|baz | ||
\. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
// Copyright (c) 2016-2020 Association of Universities for Research in Astronomy, Inc. (AURA) | ||
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause | ||
|
||
package edu.gemini.grackle.sql.test | ||
|
||
import cats.implicits._ | ||
|
||
import edu.gemini.grackle._ | ||
import edu.gemini.grackle.syntax._ | ||
import Query._, Path._, Predicate._, Value._ | ||
import QueryCompiler._ | ||
import sql.Like | ||
|
||
trait SqlLikeMapping[F[_]] extends SqlTestMapping[F] { | ||
|
||
object likes extends TableDef("likes") { | ||
val id = col("id", int4) | ||
val notNullableText = col("notnullable", text) | ||
val nullableText = col("nullable", nullable(text)) | ||
} | ||
|
||
val schema = | ||
schema""" | ||
type Query { | ||
likes: [Like!]! | ||
likeNotNullableNotNullable(pattern: String!): [Like!]! | ||
likeNotNullableNullable(pattern: String): [Like!]! | ||
likeNullableNotNullable(pattern: String!): [Like!]! | ||
likeNullableNullable(pattern: String): [Like!]! | ||
} | ||
type Like { | ||
id: Int! | ||
notNullable: String! | ||
nullable: String | ||
} | ||
""" | ||
|
||
val QueryType = schema.ref("Query") | ||
val LikeType = schema.ref("Like") | ||
|
||
val typeMappings = | ||
List( | ||
ObjectMapping( | ||
tpe = QueryType, | ||
fieldMappings = | ||
List( | ||
SqlRoot("likes"), | ||
) | ||
), | ||
ObjectMapping( | ||
tpe = LikeType, | ||
fieldMappings = | ||
List( | ||
SqlField("id", likes.id, key = true), | ||
SqlField("notNullable", likes.notNullableText), | ||
SqlField("nullable", likes.nullableText) | ||
) | ||
) | ||
) | ||
|
||
object NonNullablePattern { | ||
def unapply(value: Value): Option[String] = | ||
value match { | ||
case AbsentValue => None | ||
case StringValue(p) => Some(p) | ||
case other => sys.error(s"Expected pattern, found $other") | ||
} | ||
} | ||
|
||
object NullablePattern { | ||
def unapply(value: Value): Option[Option[String]] = | ||
value match { | ||
case AbsentValue => None | ||
case NullValue => Some(None) | ||
case StringValue(p) => Some(Some(p)) | ||
case other => sys.error(s"Expected pattern, found $other") | ||
} | ||
} | ||
|
||
def mkPredicate(t: Term[Option[String]], pattern: Option[String]): Predicate = | ||
pattern match { | ||
case None => IsNull(t, true) | ||
case Some(p) => Like(t, p, false) | ||
} | ||
|
||
override val selectElaborator: SelectElaborator = new SelectElaborator(Map( | ||
QueryType -> { | ||
case Select(f@"likeNotNullableNotNullable", List(Binding("pattern", NonNullablePattern(pattern))), child) => | ||
Rename(f, Select("likes", Nil, Filter(Like(UniquePath(List("notNullable")), pattern, false), child))).rightIor | ||
case Select(f@"likeNotNullableNullable", List(Binding("pattern", NullablePattern(pattern))), child) => | ||
Rename(f, Select("likes", Nil, Filter(mkPredicate(UniquePath(List("notNullable")), pattern), child))).rightIor | ||
case Select(f@"likeNullableNotNullable", List(Binding("pattern", NonNullablePattern(pattern))), child) => | ||
Rename(f, Select("likes", Nil, Filter(Like(UniquePath(List("nullable")), pattern, false), child))).rightIor | ||
case Select(f@"likeNullableNullable", List(Binding("pattern", NullablePattern(pattern))), child) => | ||
Rename(f, Select("likes", Nil, Filter(mkPredicate(UniquePath(List("nullable")), pattern), child))).rightIor | ||
|
||
case other => other.rightIor | ||
} | ||
)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
// Copyright (c) 2016-2020 Association of Universities for Research in Astronomy, Inc. (AURA) | ||
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause | ||
|
||
package edu.gemini.grackle.sql.test | ||
|
||
import cats.effect.IO | ||
import io.circe.Json | ||
import org.scalatest.funsuite.AnyFunSuite | ||
import cats.effect.unsafe.implicits.global | ||
|
||
import edu.gemini.grackle._ | ||
import syntax._ | ||
|
||
import grackle.test.GraphQLResponseTests.assertWeaklyEqual | ||
|
||
trait SqlLikeSpec extends AnyFunSuite { | ||
def mapping: QueryExecutor[IO, Json] | ||
|
||
test("No filter") { | ||
val query = """ | ||
query { | ||
likes { | ||
id | ||
notNullable | ||
nullable | ||
} | ||
} | ||
""" | ||
|
||
val expected = json""" | ||
{ | ||
"data" : { | ||
"likes" : [ | ||
{ | ||
"id" : 1, | ||
"notNullable" : "foo", | ||
"nullable" : null | ||
}, | ||
{ | ||
"id" : 2, | ||
"notNullable" : "bar", | ||
"nullable" : "baz" | ||
} | ||
] | ||
} | ||
} | ||
""" | ||
|
||
val res = mapping.compileAndRun(query).unsafeRunSync() | ||
//println(res) | ||
|
||
assertWeaklyEqual(res, expected) | ||
} | ||
|
||
test("Not nullable, not null") { | ||
val query = """ | ||
query { | ||
likeNotNullableNotNullable(pattern: "f%") { | ||
id | ||
notNullable | ||
nullable | ||
} | ||
} | ||
""" | ||
|
||
val expected = json""" | ||
{ | ||
"data" : { | ||
"likeNotNullableNotNullable" : [ | ||
{ | ||
"id" : 1, | ||
"notNullable" : "foo", | ||
"nullable" : null | ||
} | ||
] | ||
} | ||
} | ||
""" | ||
|
||
val res = mapping.compileAndRun(query).unsafeRunSync() | ||
//println(res) | ||
|
||
assertWeaklyEqual(res, expected) | ||
} | ||
|
||
test("Not nullable, null") { | ||
val query = """ | ||
query { | ||
likeNotNullableNullable(pattern: null) { | ||
id | ||
notNullable | ||
nullable | ||
} | ||
} | ||
""" | ||
|
||
val expected = json""" | ||
{ | ||
"data" : { | ||
"likeNotNullableNullable" : [ | ||
] | ||
} | ||
} | ||
""" | ||
|
||
val res = mapping.compileAndRun(query).unsafeRunSync() | ||
//println(res) | ||
|
||
assertWeaklyEqual(res, expected) | ||
} | ||
|
||
test("Nullable, not null") { | ||
val query = """ | ||
query { | ||
likeNullableNotNullable(pattern: "b%") { | ||
id | ||
notNullable | ||
nullable | ||
} | ||
} | ||
""" | ||
|
||
val expected = json""" | ||
{ | ||
"data" : { | ||
"likeNullableNotNullable" : [ | ||
{ | ||
"id" : 2, | ||
"notNullable" : "bar", | ||
"nullable" : "baz" | ||
} | ||
] | ||
} | ||
} | ||
""" | ||
|
||
val res = mapping.compileAndRun(query).unsafeRunSync() | ||
//println(res) | ||
|
||
assertWeaklyEqual(res, expected) | ||
} | ||
|
||
test("Nullable, null") { | ||
val query = """ | ||
query { | ||
likeNullableNullable(pattern: null) { | ||
id | ||
notNullable | ||
nullable | ||
} | ||
} | ||
""" | ||
|
||
val expected = json""" | ||
{ | ||
"data" : { | ||
"likeNullableNullable" : [ | ||
{ | ||
"id" : 1, | ||
"notNullable" : "foo", | ||
"nullable" : null | ||
} | ||
] | ||
} | ||
} | ||
""" | ||
|
||
val res = mapping.compileAndRun(query).unsafeRunSync() | ||
//println(res) | ||
|
||
assertWeaklyEqual(res, expected) | ||
} | ||
} |