Skip to content

Commit

Permalink
Feature/nullable types (#168)
Browse files Browse the repository at this point in the history
* WIP

* tokenize Nullable Functions

* clean up tests

* Don't make Column sealed (allow extension)

---------

Co-authored-by: Sjoerd Mulder <[email protected]>
Co-authored-by: Tayfun Oztemel <[email protected]>
  • Loading branch information
3 people authored Sep 23, 2024
1 parent 61f347d commit 2531f25
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 149 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package com.crobox.clickhouse.dsl
import com.crobox.clickhouse.dsl.marshalling.QueryValue
import com.crobox.clickhouse.dsl.schemabuilder.{ColumnType, DefaultValue, TTL}

case object EmptyColumn extends TableColumn("NULL")

trait Column {
val name: String
lazy val quoted: String = ClickhouseStatement.quoteIdentifier(name)
Expand All @@ -21,6 +19,8 @@ abstract class TableColumn[+V](val name: String) extends Column {
def as[C <: Column](alias: C): AliasedColumn[V] = AliasedColumn(this, alias.name)
}

case object EmptyColumn extends TableColumn("NULL")

case class NativeColumn[V](override val name: String,
clickhouseType: ColumnType = ColumnType.String,
defaultValue: DefaultValue = DefaultValue.NoDefault,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ trait ClickhouseColumnFunctions
with LogicalFunctions
with MathematicalFunctions
with MiscellaneousFunctions
with NullableFunctions
with RandomFunctions
with RoundingFunctions
with SplitMergeFunctions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,17 @@ trait EmptyFunctions { self: Magnets =>

sealed abstract class EmptyFunction[+V](val innerCol: Column) extends ExpressionColumn[V](innerCol)

case class Empty(col: EmptyNonEmptyCol[_]) extends EmptyFunction[Boolean](col.column)
case class NotEmpty(col: EmptyNonEmptyCol[_]) extends EmptyFunction[Boolean](col.column)
case class IsNull(col: EmptyNonEmptyCol[_]) extends EmptyFunction[Boolean](col.column)
case class IsNullable(col: EmptyNonEmptyCol[_]) extends EmptyFunction[Boolean](col.column)
case class IsNotNull(col: EmptyNonEmptyCol[_]) extends EmptyFunction[Boolean](col.column)
case class IsNotDistinctFrom(col: EmptyNonEmptyCol[_], other: EmptyNonEmptyCol[_])
extends EmptyFunction[Boolean](col.column)
case class IsZeroOrNull(col: EmptyNonEmptyCol[_]) extends EmptyFunction[Boolean](col.column)
case class IfNull(col: EmptyNonEmptyCol[_], alt: String) extends EmptyFunction[Boolean](col.column)
case class NullIf(col: EmptyNonEmptyCol[_], other: EmptyNonEmptyCol[_]) extends EmptyFunction[Boolean](col.column)
case class AssumeNotNull(col: EmptyNonEmptyCol[_]) extends EmptyFunction[Boolean](col.column)
case class ToNullable(col: EmptyNonEmptyCol[_]) extends EmptyFunction[Boolean](col.column)
case class Empty(col: EmptyNonEmptyCol[_]) extends EmptyFunction[Boolean](col.column)
case class NotEmpty(col: EmptyNonEmptyCol[_]) extends EmptyFunction[Boolean](col.column)

trait EmptyOps[C] { self: EmptyNonEmptyCol[_] =>

def empty(): Empty = Empty(self)
def notEmpty(): NotEmpty = NotEmpty(self)
def isNull(): IsNull = IsNull(self)
def isNotNull(): IsNotNull = IsNotNull(self)
def isNullable(): IsNullable = IsNullable(self)
def isNotDistinctFrom(other: EmptyNonEmptyCol[_]): IsNotDistinctFrom = IsNotDistinctFrom(self, other)
def isZeroOrNull(): IsZeroOrNull = IsZeroOrNull(self)
def ifNull(alternative: String): IfNull = IfNull(self, alternative)
def nullIf(other: EmptyNonEmptyCol[_]): NullIf = NullIf(self, other)
def assumeNotNull(): AssumeNotNull = AssumeNotNull(self)
def toNullable(): ToNullable = ToNullable(self)
def empty(): Empty = Empty(self)
def notEmpty(): NotEmpty = NotEmpty(self)

}

def empty(col: EmptyNonEmptyCol[_]): Empty = Empty(col: EmptyNonEmptyCol[_])
def notEmpty(col: EmptyNonEmptyCol[_]): NotEmpty = NotEmpty(col: EmptyNonEmptyCol[_])
def isNull(col: EmptyNonEmptyCol[_]): IsNull = IsNull(col: EmptyNonEmptyCol[_])
def isNotNull(col: EmptyNonEmptyCol[_]): IsNotNull = IsNotNull(col: EmptyNonEmptyCol[_])
def isNullable(col: EmptyNonEmptyCol[_]): IsNullable = IsNullable(col: EmptyNonEmptyCol[_])
def isNotDistinctFrom(col: EmptyNonEmptyCol[_], other: EmptyNonEmptyCol[_]): IsNotDistinctFrom =
IsNotDistinctFrom(col: EmptyNonEmptyCol[_], other: EmptyNonEmptyCol[_])
def isZeroOrNull(col: EmptyNonEmptyCol[_]): IsZeroOrNull = IsZeroOrNull(col: EmptyNonEmptyCol[_])
def ifNull(col: EmptyNonEmptyCol[_], alternative: String): IfNull = IfNull(col: EmptyNonEmptyCol[_], alternative)
def nullIf(col: EmptyNonEmptyCol[_], other: EmptyNonEmptyCol[_]): NullIf =
NullIf(col: EmptyNonEmptyCol[_], other: EmptyNonEmptyCol[_])
def assumeNotNull(col: EmptyNonEmptyCol[_]): AssumeNotNull = AssumeNotNull(col: EmptyNonEmptyCol[_])
def toNullable(col: EmptyNonEmptyCol[_]): ToNullable = ToNullable(col: EmptyNonEmptyCol[_])
def empty(col: EmptyNonEmptyCol[_]): Empty = Empty(col: EmptyNonEmptyCol[_])
def notEmpty(col: EmptyNonEmptyCol[_]): NotEmpty = NotEmpty(col: EmptyNonEmptyCol[_])

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.crobox.clickhouse.dsl.column

import java.util.UUID
import com.crobox.clickhouse.dsl.marshalling.{QueryValue, QueryValueFormats}
import com.crobox.clickhouse.dsl.marshalling.QueryValueFormats._
import com.crobox.clickhouse.dsl.marshalling.{QueryValue, QueryValueFormats}
import com.crobox.clickhouse.dsl.schemabuilder.ColumnType.SimpleColumnType
import com.crobox.clickhouse.dsl.{Const, EmptyColumn, ExpressionColumn, OperationalQuery, Table, TableColumn}
import org.joda.time.{DateTime, LocalDate}

import java.util.UUID
import scala.language.implicitConversions

trait Magnets {
Expand All @@ -15,6 +16,7 @@ trait Magnets {
with TypeCastFunctions
with StringFunctions
with EmptyFunctions
with NullableFunctions
with StringSearchFunctions
with ScalaBooleanFunctions
with ScalaStringFunctions
Expand All @@ -30,7 +32,7 @@ trait Magnets {
val column: TableColumn[C]
}

// ComparabeWith trait and Cast case class were members of ComparisonFunctions and TypeCastFunctions trait
// ComparableWith trait and Cast case class were members of ComparisonFunctions and TypeCastFunctions trait
// respectively. But placing them in the mixin traits causes Scala 3 compiler to crash. Hence, placing these
// constructs here is a workaround allowing for the codebase to be compiled with Scala 3.
trait ComparableWith[M <: Magnet[_]] {
Expand Down Expand Up @@ -64,7 +66,7 @@ trait Magnets {
* Any constant or column.
* Sidenote: The current implementation doesn't represent collections.
*/
trait ConstOrColMagnet[+C] extends Magnet[C] with ScalaBooleanFunctionOps with InOps
trait ConstOrColMagnet[+C] extends Magnet[C] with ScalaBooleanFunctionOps with InOps with NullableOps

implicit def constOrColMagnetFromCol[C](s: TableColumn[C]): ConstOrColMagnet[C] =
new ConstOrColMagnet[C] {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.crobox.clickhouse.dsl.column

import com.crobox.clickhouse.dsl.{Column, ExpressionColumn}

trait NullableFunctions { self: Magnets =>

sealed trait NullableFunction

sealed abstract class AbsNullableFunction[+V](val innerCol: Column)
extends ExpressionColumn[V](innerCol)
with NullableFunction

case class IsNull(col: ConstOrColMagnet[_]) extends AbsNullableFunction[Boolean](col.column)
case class IsNullable(col: ConstOrColMagnet[_]) extends AbsNullableFunction[Boolean](col.column)
case class IsNotNull(col: ConstOrColMagnet[_]) extends AbsNullableFunction[Boolean](col.column)
case class IsZeroOrNull(col: ConstOrColMagnet[_]) extends AbsNullableFunction[Boolean](col.column)
case class AssumeNotNull(col: ConstOrColMagnet[_]) extends AbsNullableFunction(col.column)
case class ToNullable(col: ConstOrColMagnet[_]) extends AbsNullableFunction(col.column)
case class IfNull(col: ConstOrColMagnet[_], alt: ConstOrColMagnet[_]) extends AbsNullableFunction(col.column)
case class NullIf(col: ConstOrColMagnet[_], other: ConstOrColMagnet[_]) extends AbsNullableFunction(col.column)

trait NullableOps {
self: ConstOrColMagnet[_] =>

def isNull(): IsNull = IsNull(self)
def isNullable(): IsNullable = IsNullable(self)
def isNotNull(): IsNotNull = IsNotNull(self)
def isZeroOrNull(): IsZeroOrNull = IsZeroOrNull(self)
def ifNull(alternative: ConstOrColMagnet[_]): IfNull = IfNull(self, alternative)
def nullIf(other: ConstOrColMagnet[_]): NullIf = NullIf(self, other)
def assumeNotNull(): AssumeNotNull = AssumeNotNull(self)
def toNullable(): ToNullable = ToNullable(self)

}

def isNull(col: ConstOrColMagnet[_]): IsNull = IsNull(col)
def isNullable(col: ConstOrColMagnet[_]): IsNullable = IsNullable(col)
def isNotNull(col: ConstOrColMagnet[_]): IsNotNull = IsNotNull(col)
def isZeroOrNull(col: ConstOrColMagnet[_]): IsZeroOrNull = IsZeroOrNull(col)
def ifNull(col: ConstOrColMagnet[_], alternative: ConstOrColMagnet[_]): IfNull = IfNull(col, alternative)
def nullIf(col: ConstOrColMagnet[_], other: ConstOrColMagnet[_]): NullIf = NullIf(col, other)
def assumeNotNull(col: ConstOrColMagnet[_]): AssumeNotNull = AssumeNotNull(col)
def toNullable(col: ConstOrColMagnet[_]): ToNullable = ToNullable(col)
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ trait ClickhouseTokenizerModule
with LogicalFunctionTokenizer
with MathematicalFunctionTokenizer
with MiscellaneousFunctionTokenizer
with NullableFunctionTokenizer
with RandomFunctionTokenizer
with RoundingFunctionTokenizer
with SplitMergeFunctionTokenizer
Expand Down Expand Up @@ -177,6 +178,7 @@ trait ClickhouseTokenizerModule
case col: LogicalFunction => tokenizeLogicalFunction(col)
case col: MathFuncColumn => tokenizeMathematicalFunction(col)
case col: MiscellaneousFunction => tokenizeMiscellaneousFunction(col)
case col: NullableFunction => tokenizeNullableFunction(col)
case col: RandomFunction => tokenizeRandomFunction(col)
case col: RoundingFunction => tokenizeRoundingFunction(col)
case col: SplitMergeFunction[_] => tokenizeSplitMergeFunction(col)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,5 @@ trait EmptyFunctionTokenizer {
s"${tokenizeColumn(c.column)} != 0"
case _ => s"notEmpty(${tokenizeColumn(c.column)})"
}
case IsNull(c) =>
c.column match {
case NativeColumn(_, ColumnType.UUID, _, None) if !ctx.version.minimalVersion(21, 8) =>
s"${tokenizeColumn(c.column)} != 0"
case _ => s"isNull(${tokenizeColumn(c.column)})"
}
case IsNotNull(c) => s"isNotNull(${tokenizeColumn(c.column)})"
case IsNullable(c) => s"isNullable(${tokenizeColumn(c.column)})"
case IsNotDistinctFrom(c, o) => s"isNotDistinctFrom(${tokenizeColumn(c.column)}, ${tokenizeColumn(o.column)})"
case IsZeroOrNull(c) => s"isZeroOrNull(${tokenizeColumn(c.column)})"
case IfNull(c, alt) => s"ifNull(${tokenizeColumn(c.column)}, '$alt')"
case NullIf(c, o) => s"nullIf(${tokenizeColumn(c.column)}, ${tokenizeColumn(o.column)})"
case AssumeNotNull(c) => s"assumeNotNull(${tokenizeColumn(c.column)})"
case ToNullable(c) => s"toNullable(${tokenizeColumn(c.column)})"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.crobox.clickhouse.dsl.language

import com.crobox.clickhouse.dsl._

trait NullableFunctionTokenizer {
self: ClickhouseTokenizerModule =>

protected def tokenizeNullableFunction(col: NullableFunction)(implicit ctx: TokenizeContext): String =
col match {
case IsNull(c) => s"isNull(${tokenizeColumn(c.column)})"
case IsNullable(c) => s"isNullable(${tokenizeColumn(c.column)})"
case IsNotNull(c) => s"isNotNull(${tokenizeColumn(c.column)})"
case IsZeroOrNull(c) => s"isZeroOrNull(${tokenizeColumn(c.column)})"
case AssumeNotNull(c) => s"assumeNotNull(${tokenizeColumn(c.column)})"
case ToNullable(c) => s"toNullable(${tokenizeColumn(c.column)})"
case IfNull(c, alt) => s"ifNull(${tokenizeColumn(c.column)}, ${tokenizeColumn(alt.column)})"
case NullIf(c, o) => s"nullIf(${tokenizeColumn(c.column)}, ${tokenizeColumn(o.column)})"
}
}
6 changes: 5 additions & 1 deletion dsl/src/test/scala/com/crobox/clickhouse/DslTestSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.crobox.clickhouse.dsl.language.{ClickhouseTokenizerModule, TokenizeCo
import com.crobox.clickhouse.dsl.{InternalQuery, OperationalQuery, TableColumn}
import com.crobox.clickhouse.testkit.ClickhouseMatchers
import com.typesafe.config.{Config, ConfigFactory}
import org.scalatest.BeforeAndAfterAll
import org.scalatest.{Assertion, BeforeAndAfterAll}
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
Expand Down Expand Up @@ -42,4 +42,8 @@ trait DslTestSpec
}
} else sql.substring(0, sql.indexOf(" FORMAT")).trim
}

def shouldMatch(query: OperationalQuery, expected: String): Assertion = {
toSql(query.internalQuery, None) should matchSQL(expected)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,96 +50,4 @@ class EmptyFunctionTokenizerTest extends DslTestSpec {
result2 should matchSQL(s"SELECT * FROM $database.twoTestTable WHERE uuid != 0")
}
}

it should "tokenize IsNull" in {
val expected = s"SELECT * FROM $database.twoTestTable WHERE isNull(uuid)"

val query = select(All()).from(TwoTestTable).where(nativeUUID.isNull())
toSql(query.internalQuery, None) should matchSQL(expected)

val query2 = select(All()).from(TwoTestTable).where(isNull(nativeUUID))
toSql(query2.internalQuery, None) should matchSQL(expected)
}

it should "tokenize IsNotNull" in {
val expected = s"SELECT * FROM $database.twoTestTable WHERE isNotNull(uuid)"

val query = select(All()).from(TwoTestTable).where(nativeUUID.isNotNull())
toSql(query.internalQuery, None) should matchSQL(expected)

val query2 = select(All()).from(TwoTestTable).where(isNotNull(nativeUUID))
toSql(query2.internalQuery, None) should matchSQL(expected)
}

it should "tokenize IsNullable" in {
val expected = s"SELECT isNullable(uuid) FROM $database.twoTestTable"

val query = select(nativeUUID.isNullable()).from(TwoTestTable)
toSql(query.internalQuery, None) should matchSQL(expected)

val query2 = select(isNullable(nativeUUID)).from(TwoTestTable)
toSql(query2.internalQuery, None) should matchSQL(expected)
}

it should "tokenize IsNotDistinctFrom" in {
val expected = s"SELECT isNotDistinctFrom(uuid, uuid) FROM $database.twoTestTable"

val query = select(nativeUUID.isNotDistinctFrom(nativeUUID)).from(TwoTestTable)
toSql(query.internalQuery, None) should matchSQL(expected)

val query2 = select(isNotDistinctFrom(nativeUUID, nativeUUID)).from(TwoTestTable)
toSql(query2.internalQuery, None) should matchSQL(expected)
}

it should "tokenize IsZeroOrNull" in {
val expected = s"SELECT isZeroOrNull(uuid) FROM $database.twoTestTable"

val query = select(nativeUUID.isZeroOrNull()).from(TwoTestTable)
toSql(query.internalQuery, None) should matchSQL(expected)

val query2 = select(isZeroOrNull(nativeUUID)).from(TwoTestTable)
toSql(query2.internalQuery, None) should matchSQL(expected)
}

it should "tokenize IfNull" in {
val defaultValue = "alternative"
val expected = s"SELECT ifNull(uuid, '$defaultValue') FROM $database.twoTestTable"

val query = select(nativeUUID.ifNull(defaultValue)).from(TwoTestTable)
toSql(query.internalQuery, None) should matchSQL(expected)

val query2 = select(ifNull(nativeUUID, defaultValue)).from(TwoTestTable)
toSql(query2.internalQuery, None) should matchSQL(expected)
}

it should "tokenize NullIf" in {
val expected = s"SELECT nullIf(uuid, uuid) FROM $database.twoTestTable"

val query = select(nativeUUID.nullIf(nativeUUID)).from(TwoTestTable)
toSql(query.internalQuery, None) should matchSQL(expected)

val query2 = select(nullIf(nativeUUID, nativeUUID)).from(TwoTestTable)
toSql(query2.internalQuery, None) should matchSQL(expected)
}

it should "tokenize AssumeNotNull" in {
val expected = s"SELECT assumeNotNull(uuid) FROM $database.twoTestTable"

val query = select(nativeUUID.assumeNotNull()).from(TwoTestTable)
toSql(query.internalQuery, None) should matchSQL(expected)

val query2 = select(assumeNotNull(nativeUUID)).from(TwoTestTable)
toSql(query2.internalQuery, None) should matchSQL(expected)
}

it should "tokenize ToNullable" in {
val expected = s"SELECT toNullable(uuid) FROM $database.twoTestTable"

val query = select(nativeUUID.toNullable()).from(TwoTestTable)
toSql(query.internalQuery, None) should matchSQL(expected)

val query2 = select(toNullable(nativeUUID)).from(TwoTestTable)
toSql(query2.internalQuery, None) should matchSQL(expected)
}

}
Loading

0 comments on commit 2531f25

Please sign in to comment.