Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ lazy val core = crossProject(JVMPlatform)
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "2.9.0",
"org.typelevel" %% "cats-effect" % "3.4.9",
"org.typelevel" %% "literally" % "1.1.0",
"org.scalameta" %% "munit" % "0.7.29" % Test,
"org.typelevel" %% "munit-cats-effect-3" % "1.0.7" % Test,
"com.google.cloud" % "google-cloud-bigquery" % "2.24.5",
Expand Down
21 changes: 21 additions & 0 deletions core/src/main/scala-2/no/nrk/bigquery/syntax.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package no.nrk.bigquery

import org.typelevel.literally.Literally

object syntax {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

follow-up PR with moving interpolators and other syntax instances here.


implicit class BQLiteralMacros(val sc: StringContext) extends AnyVal {
def ident(args: Any*): Ident = macro IdentLiteral.make
}

object IdentLiteral extends Literally[Ident] {

def validate(c: Context)(s: String): Either[String, c.Expr[Ident]] = {
import c.universe.Quasiquote
Ident.fromString(s).map(_ => c.Expr(q"Ident.unsafeFromString($s)"))
}

def make(c: Context)(args: c.Expr[Any]*): c.Expr[Ident] = apply(c)(args: _*)
}

}
15 changes: 15 additions & 0 deletions core/src/main/scala-3/no/nrk/bigquery/syntax.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package no.nrk.bigquery

import org.typelevel.literally.Literally

object syntax {

extension (inline ctx: StringContext)
inline def ident(inline args: Any*): Ident =
${ IdentLiteral('ctx, 'args) }

object IdentLiteral extends Literally[Ident]:
def validate(s: String)(using Quotes) =
Ident.fromString(s).map(_ => '{ Ident.unsafeFromString(${ Expr(s) }) })

}
23 changes: 23 additions & 0 deletions core/src/main/scala/no/nrk/bigquery/Ident.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package no.nrk.bigquery

import cats.Show
import cats.syntax.all._

import scala.util.matching.Regex

/** an identifier in an sql statement, typically a column name or anything which shouldnt be quoted
*/
Expand All @@ -15,6 +18,26 @@ object Ident {
val keywords = Set("ALL", "AND", "ANY", "ARRAY", "AS", "ASC", "ASSERT_ROWS_MODIFIED", "AT", "BETWEEN", "BY", "CASE", "CAST", "COLLATE", "CONTAINS", "CREATE", "CROSS", "CUBE", "CURRENT", "DEFAULT", "DEFINE", "DESC", "DISTINCT", "ELSE", "END", "ENUM", "ESCAPE", "EXCEPT", "EXCLUDE", "EXISTS", "EXTRACT", "FALSE", "FETCH", "FOLLOWING", "FOR", "FROM", "FULL", "GROUP", "GROUPING", "GROUPS", "HASH", "HAVING", "IF", "IGNORE", "IN", "INNER", "INTERSECT", "INTERVAL", "INTO", "IS", "JOIN", "LATERAL", "LEFT", "LIKE", "LIMIT", "LOOKUP", "MERGE", "NATURAL", "NEW", "NO", "NOT", "NULL", "NULLS", "OF", "ON", "OR", "ORDER", "OUTER", "OVER", "PARTITION", "PRECEDING", "PROTO", "QUALIFY", "RANGE", "RECURSIVE", "RESPECT", "RIGHT", "ROLLUP", "ROWS", "SELECT", "SET", "SOME", "STRUCT", "TABLESAMPLE", "THEN", "TO", "TREAT", "TRUE", "UNBOUNDED", "UNION", "UNNEST", "USING", "WHEN", "WHERE", "WINDOW", "WITH", "WITHIN")
// format: on

private val validationRegex = {
val base = "(([[\\p{Alpha}]_][[\\p{Alnum}]_]*)|(`[^`]+`))"
new Regex(s"^$base(\\.$base)*$$")
}

def fromString(str: String): Either[String, Ident] =
validationRegex
.findAllMatchIn(str)
.toList match {
case validIdentPattern :: Nil =>
val firstIdentGroup = validIdentPattern.group(1)
if (keywords.contains(firstIdentGroup.toUpperCase()))
Left(show"Keyword `$firstIdentGroup` not allowed in the first position")
else Right(new Ident(str))
case _ => Left(show"The string `$str` is not a valid unquoted identifier")
}

def unsafeFromString(str: String): Ident =
fromString(str).fold(msg => throw new IllegalStateException(msg), identity)

implicit val bqShowIdent: BQShow[Ident] =
x =>
if (keywords(x.value.toUpperCase) || x.value.contains("-"))
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/scala/no/nrk/bigquery/implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ object implicits {
private val values: S[A]
) extends AnyVal {
def mkFragment(sep: String)(implicit T: BQShow[A]): BQSqlFrag =
mkFragment(Ident(sep).bqShow)
mkFragment(BQSqlFrag(sep))
def mkFragment(start: String, sep: String, end: String)(implicit
T: BQShow[A]
): BQSqlFrag =
mkFragment(Ident(start).bqShow, Ident(sep).bqShow, Ident(end).bqShow)
mkFragment(BQSqlFrag(start), BQSqlFrag(sep), BQSqlFrag(end))
def mkFragment(sep: BQSqlFrag)(implicit T: BQShow[A]): BQSqlFrag =
mkFragment(BQSqlFrag.Empty, sep, BQSqlFrag.Empty)
def mkFragment(start: BQSqlFrag, sep: BQSqlFrag, end: BQSqlFrag)(implicit
Expand Down
46 changes: 46 additions & 0 deletions core/src/test/scala/no/nrk/bigquery/IdentTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package no.nrk.bigquery

import munit.FunSuite

/** Examples from: https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#identifier_examples
*/
class IdentTest extends FunSuite {

test("valid identifiers") {
val invalid = List(
"_5abc.dataField", // Valid. _5abc and dataField are valid identifiers.
"`5abc`.dataField", // Valid. `5abc` and dataField are valid identifiers.
"abc5.dataField", // Valid. abc5 and dataField are valid identifiers.
"abc5.GROUP", // Valid. abc5 and GROUP are valid identifiers.
"`GROUP`.dataField", // Valid.`GROUP` and dataField are valid identifiers
"a.b.c",
"foo",
"`!`"
).map(input => Ident.fromString(input)).collect { case Left(err) => err }

assert(invalid.isEmpty, clue(invalid))
}

test("invalid identifiers") {
val valid = List(
"",
"5abc.dataField", // Invalid. 5abc is an invalid identifier because it is unquoted and starts with a number rather than a letter or underscore.
"abc5!.dataField", // Invalid. abc5! is an invalid identifier because it is unquoted and contains a character that is not a letter, number, or underscore.
"GROUP.dataField", // Invalid. GROUP is an invalid identifier because it is unquoted and is a stand-alone reserved keyword.
"abc5!dataField",
"5s",
"!s",
"!",
"``"
).map(input => Ident.fromString(input)).collect { case Right(err) => err }

assert(valid.isEmpty, clue(valid))
}

test("literal usage") {
import syntax._
val i: Ident = ident"foo.bar"
assertEquals(i, Ident("foo.bar"))
}

}
7 changes: 4 additions & 3 deletions core/src/test/scala/no/nrk/bigquery/UDFTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ package no.nrk.bigquery

import munit.FunSuite
import no.nrk.bigquery.implicits.BQShowInterpolator
import no.nrk.bigquery.syntax._

class UDFTest extends FunSuite {

test("render temporary SQL UDF") {
assertEquals(
UDF(
Ident("foo"),
ident"foo",
Seq(UDF.Param("n", BQType.FLOAT64)),
UDF.Body.Sql(bqfr"""(n + 1)"""),
Some(BQType.FLOAT64)
Expand All @@ -20,7 +21,7 @@ class UDFTest extends FunSuite {
test("render temporary javascript UDF") {
assertEquals(
UDF(
Ident("foo"),
ident"foo",
Seq(UDF.Param("n", BQType.FLOAT64)),
UDF.Body.Js("return n + 1", None),
Some(BQType.FLOAT64)
Expand All @@ -34,7 +35,7 @@ class UDFTest extends FunSuite {
test("render temporary javascript UDF with library path") {
assertEquals(
UDF(
Ident("foo"),
ident"foo",
Seq(UDF.Param("n", BQType.FLOAT64)),
UDF.Body.Js("return n + 1", Some("bucket/foo.js")),
Some(BQType.FLOAT64)
Expand Down
11 changes: 6 additions & 5 deletions docs/example_query.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ import cats.data.NonEmptyList
import Schemas.{UserEventSchema, UserSchema}
import no.nrk.bigquery._
import no.nrk.bigquery.implicits._
import no.nrk.bigquery.syntax._

object CombineQueries {

Expand All @@ -124,10 +125,10 @@ object CombineQueries {
private val middleNameFilter = bqfr"user.names.middleName is not null"

private object Idents {
val userId: Ident = Ident("event.userId")
val activity: Ident = Ident("event.activity")
val activityType: Ident = Ident("event.activity.type")
val activityValue: Ident = Ident("event.activity.value")
val userId: Ident = ident"event.userId"
val activity: Ident = ident"event.activity"
val activityType: Ident = ident"event.activity.type"
val activityValue: Ident = ident"event.activity.value"
}

def queryForUserId(userId: String): BQSqlFrag =
Expand Down Expand Up @@ -159,7 +160,7 @@ import java.time.LocalDate

class UserEventQueryTest extends BQSmokeTest(BigQueryTestClient.testClient) {

bqCheckTest("user-events-query") {
bqTypeCheckTest("user-events-query") {
UserEventQuery.daily(LocalDate.now())
}
}
Expand Down
9 changes: 6 additions & 3 deletions docs/example_udf.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ BigQuery supports UDF written in SQL and JavaScript.
```scala mdoc
import no.nrk.bigquery._
import no.nrk.bigquery.implicits._
import no.nrk.bigquery.syntax._

object MySQLUdfs {

val addOneUdf = UDF(
Ident("addOneSqlUdf"),
ident"addOneSqlUdf",
Seq(UDF.Param("n", BQType.FLOAT64)),
UDF.Body.Sql(bqfr"""(n + 1)"""),
Some(BQType.FLOAT64)
Expand All @@ -29,13 +30,14 @@ object MySQLUdfs {
**JavaScript**
```scala mdoc
import no.nrk.bigquery._
import no.nrk.bigquery.syntax._

object MyJsUdfs {

// optional library in google cloud storage
val jsLibraryGcsPath = None
val addOneUdf = UDF(
Ident("addOneJsUdf"),
ident"addOneJsUdf",
Seq(UDF.Param("n", BQType.FLOAT64)),
UDF.Body.Js("return n + 1", jsLibraryGcsPath),
Some(BQType.FLOAT64)
Expand All @@ -51,9 +53,10 @@ temporary function if it's referenced in a query.

```scala mdoc
import no.nrk.bigquery._
import no.nrk.bigquery.syntax._
import no.nrk.bigquery.implicits._

val n = Ident("n")
val n = ident"n"
val myQuery: BQSqlFrag =
bqfr"""|select
| ${MySQLUdfs.addOneUdf(n)} as sql,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package no.nrk.bigquery.testing
import munit.{FunSuite, Location}
import no.nrk.bigquery._
import no.nrk.bigquery.implicits._
import no.nrk.bigquery.syntax._
import no.nrk.bigquery.testing.BQStructuredSql.{Segment, SegmentList}

class BQStructuredSqlTest extends FunSuite {
Expand Down Expand Up @@ -93,7 +94,7 @@ class BQStructuredSqlTest extends FunSuite {
BQStructuredSql.parse(bqfr"with a as (select ')') select * from a")
val expected = BQStructuredSql(
Nil,
List(CTE(Ident("a"), BQSqlFrag("(select ')')"))),
List(CTE(ident"a", BQSqlFrag("(select ')')"))),
BQSqlFrag(" select * from a"),
"select"
)
Expand All @@ -108,8 +109,8 @@ class BQStructuredSqlTest extends FunSuite {
BQStructuredSql(
Nil,
List(
CTE(Ident("a"), BQSqlFrag("(select 1)")),
CTE(Ident("b"), BQSqlFrag("(select 2)"))
CTE(ident"a", BQSqlFrag("(select 1)")),
CTE(ident"b", BQSqlFrag("(select 2)"))
),
BQSqlFrag(" select * from a, b"),
"select"
Expand All @@ -124,8 +125,8 @@ class BQStructuredSqlTest extends FunSuite {
val expected = BQStructuredSql(
Nil,
List(
CTE(Ident("a"), BQSqlFrag("(--foo\nselect 1)")),
CTE(Ident("b"), BQSqlFrag("(select 2)"))
CTE(ident"a", BQSqlFrag("(--foo\nselect 1)")),
CTE(ident"b", BQSqlFrag("(select 2)"))
),
BQSqlFrag("/*foo*/ select * from a, b--foo"),
"select"
Expand Down