Skip to content

Commit f4cb325

Browse files
committed
Fully remove NNA from Nullability. Make a better place for nullable ops
1 parent 2e58959 commit f4cb325

File tree

12 files changed

+121
-137
lines changed

12 files changed

+121
-137
lines changed

common/src/main/scala/dataprism/platform/QueryPlatform.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ trait QueryPlatform { platform =>
1818

1919
type Nullability[A] <: NullabilityBase[A]
2020
trait NullabilityBase[A]:
21-
type NNA
2221
type N[_]
2322

2423
def wrapOption[B](n: N[B]): Option[B]

common/src/main/scala/dataprism/platform/sql/value/SqlDbValuesBase.scala

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ package dataprism.platform.sql.value
22

33
import scala.annotation.targetName
44
import scala.util.NotGiven
5-
65
import dataprism.platform.MapRes
76
import dataprism.platform.sql.SqlQueryPlatformBase
87
import dataprism.sharedast.{SelectAst, SqlExpr}
9-
import dataprism.sql.{SqlNull, SqlStr}
8+
import dataprism.sql.{Nullable, SqlNull, SqlStr}
109
import perspective.*
1110

1211
trait SqlDbValuesBase extends SqlQueryPlatformBase { platform =>
@@ -50,19 +49,15 @@ trait SqlDbValuesBase extends SqlQueryPlatformBase { platform =>
5049
def castDbVal(dbVal: DbValue[A]): DbValue[N[A]] = dbVal.asInstanceOf[DbValue[N[A]]]
5150

5251
object Nullability:
53-
type Aux[A, NNA0, N0[_]] = Nullability[A] { type N[B] = N0[B]; type NNA = NNA0 }
52+
type Aux[A, N0[_]] = Nullability[A] { type N[B] = N0[B] }
5453

55-
given notNull[A](using NotGiven[SqlNull <:< A]): Nullability.Aux[A, A, Id] = new Nullability[A]:
54+
given notNull[A](using NotGiven[SqlNull <:< A]): Nullability.Aux[A, Id] = new Nullability[A]:
5655
type N[B] = B
57-
type NNA = A
5856

5957
override def isNullable: Boolean = false
6058

6159
override def wrapOption[B](n: B): Option[B] = Some(n)
62-
override def nullableToOption[B](n: Nullable[B]): Option[B] = {
63-
import dataprism.sql.sqlNullSyntax.*
64-
n.toOption
65-
}
60+
override def nullableToOption[B](n: Nullable[B]): Option[B] = Nullable.syntax(n).toOption
6661

6762
override def wrapDbVal[B](dbVal: DbValue[B]): DbValue[B] = dbVal
6863
override def wrapType[B](tpe: Type[B])(using NotGiven[SqlNull <:< B]): Type[B] = tpe.notNullChoice.notNull
@@ -75,20 +70,13 @@ trait SqlDbValuesBase extends SqlQueryPlatformBase { platform =>
7570
unaryOp: UnaryOp[V, R]
7671
)(using NotGiven[SqlNull <:< V], NotGiven[SqlNull <:< R]): UnaryOp[V, R] = unaryOp
7772

78-
given nullable[A >: (SqlNull | NN), NN]: Nullability.Aux[A, NN, Nullable] = new Nullability[A]:
73+
given nullable[A >: SqlNull]: Nullability.Aux[A, Nullable] = new Nullability[A]:
7974
type N[B] = B | SqlNull
80-
type NNA = NN
8175

8276
override def isNullable: Boolean = true
8377

84-
override def wrapOption[B](n: B | SqlNull): Option[B] = {
85-
import dataprism.sql.sqlNullSyntax.*
86-
n.toOption
87-
}
88-
override def nullableToOption[B](n: Nullable[B | SqlNull]): Option[B] = {
89-
import dataprism.sql.sqlNullSyntax.*
90-
n.toOption
91-
}
78+
override def wrapOption[B](n: B | SqlNull): Option[B] = Nullable.syntax(n).toOption
79+
override def nullableToOption[B](n: Nullable[B | SqlNull]): Option[B] = Nullable.syntax(n).toOption
9280

9381
override def wrapDbVal[B](dbVal: DbValue[B]): DbValue[B | SqlNull] = dbVal.asSome
9482
override def wrapType[B](tpe: Type[B])(using NotGiven[SqlNull <:< B]): Type[B | SqlNull] =

common/src/main/scala/dataprism/sql/sqlNull.scala

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,38 @@ import cats.{Applicative, Functor}
77
sealed trait SqlNull
88
object SqlNull extends SqlNull
99
type Nullable[A] = A | SqlNull
10+
object Nullable {
11+
opaque type NullableSyntax[A] = A | SqlNull
12+
def syntax[A](v: A | SqlNull)(using NotGiven[SqlNull <:< A]): NullableSyntax[A & v.type] = v
13+
def unsafeSyntax[A](v: A | SqlNull): NullableSyntax[A] = v
1014

11-
object sqlNullSyntax {
12-
extension [A](v: A | SqlNull)(using NotGiven[SqlNull <:< A])
13-
def unsafeGet: A & v.type =
14-
if v == SqlNull then throw new NoSuchElementException("SqlNull.unsafeGet") else v.asInstanceOf[A & v.type]
15+
extension [A](v: NullableSyntax[A]) {
16+
def unsafeGet: A =
17+
if v == SqlNull then throw new NoSuchElementException("SqlNull.unsafeGet") else v.asInstanceOf[A]
18+
19+
def orSqlNull: A | SqlNull = v
1520

16-
def orNull: (A & v.type) | Null = v match
21+
def orNull: A | Null = v match
1722
case SqlNull => null
1823
case _ => v.unsafeGet
1924

20-
def toOption: Option[A & v.type] = v match
25+
def toOption: Option[A] = v match
2126
case SqlNull => None
2227
case _ => Some(v.unsafeGet)
2328

24-
def map[B](f: (A & v.type) => B): B | SqlNull = v match
29+
def map[B](f: A => B)(using NotGiven[SqlNull <:< B]): NullableSyntax[B] = v match
2530
case SqlNull => SqlNull
2631
case _ => f(v.unsafeGet)
2732

28-
def flatMap[B](f: (A & v.type) => B | SqlNull): B | SqlNull = v match
33+
def flatMap[B](f: A => NullableSyntax[B])(using NotGiven[SqlNull <:< B]): NullableSyntax[B] = v match
2934
case SqlNull => SqlNull
3035
case _ => f(v.unsafeGet)
3136

32-
def fold[B](ifNull: => B)(f: (A & v.type) => B): B = v match
37+
def fold[B](ifNull: => B)(f: A => B): B = v match
3338
case SqlNull => ifNull
3439
case _ => f(v.unsafeGet)
3540

36-
def orElse[B >: A](default: B | SqlNull): B | SqlNull = v match
41+
def orElse[B >: A](default: NullableSyntax[B])(using NotGiven[SqlNull <:< A]): NullableSyntax[B] = v match
3742
case SqlNull => default
3843
case _ => v.unsafeGet
3944

@@ -45,15 +50,16 @@ object sqlNullSyntax {
4550
case SqlNull => false
4651
case _ => v.unsafeGet == elem
4752

48-
def map2[B, C](that: B | SqlNull)(f: (A, B) => C): C | SqlNull = (v, that) match
53+
def map2[B, C](that: NullableSyntax[B])(f: (A, B) => C): NullableSyntax[C] = (v, that) match
4954
case (SqlNull, _) => SqlNull
5055
case (_, SqlNull) => SqlNull
5156
case (_, _) => f(v.unsafeGet, that.unsafeGet)
57+
}
5258

53-
given Functor[Nullable] with Applicative[Nullable] with {
54-
override def pure[A](x: A): Nullable[A] = x
59+
given Functor[NullableSyntax] with Applicative[NullableSyntax] with {
60+
override def pure[A](x: A): NullableSyntax[A] = x
5561

56-
override def ap[A, B](ff: Nullable[A => B])(fa: Nullable[A]): Nullable[B] =
62+
override def ap[A, B](ff: NullableSyntax[A => B])(fa: NullableSyntax[A]): NullableSyntax[B] =
5763
ff.flatMap(f => fa.map(a => f(a)))
5864
}
5965
}

common/src/main/scala/dataprism/sql/types.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,9 @@ class NullabilityTypeChoiceNoArr[Codec[_], A](
3434
def imap[B](
3535
f: A => B
3636
)(g: B => A)(using NotGiven[SqlNull <:< B], Invariant[Codec]): NullabilityTypeChoiceNoArr[Codec, B] =
37-
import dataprism.sql.sqlNullSyntax.*
3837
NullabilityTypeChoiceNoArr(
3938
notNullCodec.imap(f)(g),
40-
nullableCodec.imap[B | SqlNull](_.map(f))(_.map(g))
39+
nullableCodec.imap[B | SqlNull](v => Nullable.syntax(v).map(f).orSqlNull)(v => Nullable.syntax(v).map(g).orSqlNull)
4140
)
4241

4342
class NullabilityTypeChoiceArr[Codec[_], Arr[_], A, DimensionE <: Int](

common/src/test/scala/dataprism/PlatformDbValueSuite.scala

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import cats.{Apply, Show}
1010
import dataprism.PlatformFunSuite.DbToTest
1111
import dataprism.platform.sql.SqlQueryPlatform
1212
import dataprism.platform.sql.value.SqlBitwiseOps
13-
import dataprism.sql.{NullabilityTypeChoice, SqlNull}
13+
import dataprism.sql.{NullabilityTypeChoice, Nullable, SqlNull}
1414
import org.scalacheck.cats.implicits.*
1515
import org.scalacheck.{Arbitrary, Gen}
1616
import perspective.Id
@@ -35,7 +35,7 @@ trait PlatformDbValueSuite[Codec0[_], Platform <: SqlQueryPlatform { type Codec[
3535
def testEquality[A, N[_]: Apply](
3636
tpe: Type[N[A]],
3737
gen: Gen[N[A]]
38-
)(using N: Nullability.Aux[N[A], A, N])(using Show[N[A]]): Unit =
38+
)(using N: Nullability.Aux[N[A], N])(using Show[N[A]]): Unit =
3939
typeTest("Equality", tpe):
4040
configuredForall((gen, gen).tupled): (a: N[A], b: N[A]) =>
4141
Select(
@@ -107,7 +107,6 @@ trait PlatformDbValueSuite[Codec0[_], Platform <: SqlQueryPlatform { type Codec[
107107
testEquality[A, Id](tpe, gen)
108108

109109
def testEqualityNullable[A: Show](tpe: Type[A], gen: Gen[A])(using NotGiven[SqlNull <:< A]): Unit =
110-
import dataprism.sql.sqlNullSyntax.given
111110
testEquality[A, Nullable](
112111
tpe.choice.asInstanceOf[NullabilityTypeChoice[Codec, A, tpe.Dimension]].nullable,
113112
sqlNullGen(gen)
@@ -144,7 +143,7 @@ trait PlatformDbValueSuite[Codec0[_], Platform <: SqlQueryPlatform { type Codec[
144143
SqlNumericSumAverage[N[A], SumResult, AvgResult],
145144
Show[N[A]]
146145
)(
147-
using N: Nullability.Aux[N[A], A, N]
146+
using N: Nullability.Aux[N[A], N]
148147
): Unit =
149148
import Numeric.Implicits.*
150149
import Ordering.Implicits.*
@@ -176,8 +175,6 @@ trait PlatformDbValueSuite[Codec0[_], Platform <: SqlQueryPlatform { type Codec[
176175
expect(((a * b) - abMul).abs <= delta),
177176
expect(((b * a) - baMul).abs <= delta),
178177
expect {
179-
import dataprism.sql.sqlNullSyntax.*
180-
181178
val lhs = if b == Numeric[N[A]].zero then None else N.wrapOption(a / b)
182179
val rhs: Option[A] = N.nullableToOption(abDiv)
183180

@@ -186,8 +183,6 @@ trait PlatformDbValueSuite[Codec0[_], Platform <: SqlQueryPlatform { type Codec[
186183
.getOrElse(lhs.isEmpty && rhs.isEmpty)
187184
},
188185
expect {
189-
import dataprism.sql.sqlNullSyntax.*
190-
191186
val lhs = if a == Numeric[N[A]].zero then None else N.wrapOption(b / a)
192187
val rhs: Option[A] = N.nullableToOption(baDiv)
193188

@@ -241,7 +236,7 @@ trait PlatformDbValueSuite[Codec0[_], Platform <: SqlQueryPlatform { type Codec[
241236
Ordering[N[A]],
242237
SqlOrdered[N[A]],
243238
Show[N[A]]
244-
)(using N: Nullability.Aux[N[A], A, N]): Unit =
239+
)(using N: Nullability.Aux[N[A], N]): Unit =
245240
import Ordering.Implicits.*
246241
typeTest("Ordered", tpe):
247242
configuredForall((gen, gen).tupled): (a: N[A], b: N[A]) =>
@@ -274,14 +269,12 @@ trait PlatformDbValueSuite[Codec0[_], Platform <: SqlQueryPlatform { type Codec[
274269
_ <- log.debug(s"Got max=${maxMin._1} min=${maxMin._2}")
275270
_ <- log.debug(s"Got greatest=${greatestLeast._1} least=${greatestLeast._2}")
276271
yield
277-
import dataprism.sql.sqlNullSyntax.*
278-
279272
val (max, min) = maxMin
280273
val (greatest, least) = greatestLeast
281274
val someVs = vs.toList.flatMap(N.wrapOption(_))
282275
Seq(
283-
expect.same(someVs.maxOption, max.toOption),
284-
expect.same(someVs.minOption, min.toOption),
276+
expect.same(someVs.maxOption, Nullable.syntax(max).toOption),
277+
expect.same(someVs.minOption, Nullable.syntax(min).toOption),
285278
expect.same(
286279
if leastGreatestBubbleNulls && someVs.length != vs.length then None
287280
else N.wrapOption(vs.toList.filter(_ != SqlNull).max),
@@ -306,7 +299,6 @@ trait PlatformDbValueSuite[Codec0[_], Platform <: SqlQueryPlatform { type Codec[
306299
cats.Order[Nullable[A]],
307300
SqlOrdered[Nullable[A]]
308301
): Unit =
309-
import dataprism.sql.sqlNullSyntax.given
310302
testOrdered[A, Nullable](
311303
tpe.choice.asInstanceOf[NullabilityTypeChoice[Codec, A, tpe.Dimension]].nullable,
312304
sqlNullGen(gen)
@@ -338,10 +330,10 @@ trait PlatformDbValueSuite[Codec0[_], Platform <: SqlQueryPlatform { type Codec[
338330
)
339331
).runOne[F]
340332
.map: (abOr, baOr, abAnd, baAnd, aNot, bNot, abnOr, banOr, abnAnd, banAnd, anNot, bnNot) =>
341-
import dataprism.sql.sqlNullSyntax.{*, given}
342333
import cats.syntax.all.*
343334

344-
def sqlNullWhen[A](cond: Boolean)(v: A) = if cond then v else SqlNull
335+
def sqlNullWhen[A](cond: Boolean)(v: A)(using NotGiven[SqlNull <:< A]): Nullable.NullableSyntax[A] =
336+
Nullable.unsafeSyntax(if cond then v else SqlNull)
345337

346338
Seq(
347339
expect.same(a || b, abOr),
@@ -351,18 +343,28 @@ trait PlatformDbValueSuite[Codec0[_], Platform <: SqlQueryPlatform { type Codec[
351343
expect.same(!a, aNot),
352344
expect.same(!b, bNot),
353345
//
354-
expect.same(sqlNullWhen(an.contains(true) || bn.contains(true))(true).orElse(an.map2(bn)(_ || _)), abnOr),
355-
expect.same(sqlNullWhen(an.contains(true) || bn.contains(true))(true).orElse(bn.map2(an)(_ || _)), banOr),
356346
expect.same(
357-
sqlNullWhen(an.contains(false) || bn.contains(false))(false).orElse(an.map2(bn)(_ && _)),
347+
sqlNullWhen(Nullable.syntax(an).contains(true) || Nullable.syntax(bn).contains(true))(true)
348+
.orElse(Nullable.syntax(an).map2(Nullable.syntax(bn))(_ || _)),
349+
abnOr
350+
),
351+
expect.same(
352+
sqlNullWhen(Nullable.syntax(an).contains(true) || Nullable.syntax(bn).contains(true))(true)
353+
.orElse(Nullable.syntax(bn).map2(Nullable.syntax(an))(_ || _)),
354+
banOr
355+
),
356+
expect.same(
357+
sqlNullWhen(Nullable.syntax(an).contains(false) || Nullable.syntax(bn).contains(false))(false)
358+
.orElse(Nullable.syntax(an).map2(Nullable.syntax(bn))(_ && _)),
358359
abnAnd
359360
),
360361
expect.same(
361-
sqlNullWhen(an.contains(false) || bn.contains(false))(false).orElse(bn.map2(an)(_ && _)),
362+
sqlNullWhen(Nullable.syntax(an).contains(false) || Nullable.syntax(bn).contains(false))(false)
363+
.orElse(Nullable.syntax(bn).map2(Nullable.syntax(an))(_ && _)),
362364
banAnd
363365
),
364-
expect.same(an.map(!_), anNot),
365-
expect.same(bn.map(!_), bnNot)
366+
expect.same(Nullable.syntax(an).map(!_), anNot),
367+
expect.same(Nullable.syntax(bn).map(!_), bnNot)
366368
).combineAll
367369

368370
def testNullOps[A: Show](t: Type[A | SqlNull], gen: Gen[A]): Unit = typeTest("NullOps", t):
@@ -385,16 +387,17 @@ trait PlatformDbValueSuite[Codec0[_], Platform <: SqlQueryPlatform { type Codec[
385387
)
386388
).runOne[F]
387389
.map: (r1, r2, r3, r4, r5, r6, r7, r8) =>
388-
import dataprism.sql.sqlNullSyntax.{*, given}
390+
val o123 = (Nullable.syntax(o1), Nullable.syntax(o2), Nullable.syntax(o3))
391+
389392
Seq(
390393
expect.same(o1, r1),
391394
expect.same(o1, r2),
392-
expect.same(o1.flatMap(_ => o2), r3),
395+
expect.same(Nullable.syntax(o1).flatMap(_ => Nullable.syntax(o2)), r3),
393396
expect.same(o1, r4),
394397
expect.same(SqlNull, r5),
395-
expect.same((o1, o2, o3).mapN((n1, _, _) => n1), r6),
396-
expect.same((o1, o2, o3).mapN((_, n2, _) => n2), r7),
397-
expect.same((o1, o2, o3).mapN((_, _, n3) => n3), r8)
398+
expect.same(o123.mapN((n1, _, _) => n1), r6),
399+
expect.same(o123.mapN((_, n2, _) => n2), r7),
400+
expect.same(o123.mapN((_, _, n3) => n3), r8)
398401
).combineAll
399402

400403
dbTest("CaseBoolean"):
@@ -421,7 +424,7 @@ trait PlatformDbValueSuite[Codec0[_], Platform <: SqlQueryPlatform { type Codec[
421424
)(
422425
using show: Show[N[A]],
423426
bw: bitwisePlatform.Api.SqlBitwise[N[A]],
424-
N: bitwisePlatform.Nullability.Aux[N[A], A, N],
427+
N: bitwisePlatform.Nullability.Aux[N[A], N],
425428
bool: algebra.lattice.Bool[A]
426429
): Unit = typeTest("BitwiseOps", tpe):
427430
import bitwisePlatform.Api.*

common/src/test/scala/dataprism/PlatformFunSuite.scala

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import cats.Show
44
import cats.effect.{IO, Resource}
55
import dataprism.PlatformFunSuite.DbToTest
66
import dataprism.platform.sql.SqlQueryPlatform
7-
import dataprism.sql.{Db, SqlNull}
7+
import dataprism.sql.{Db, Nullable, SqlNull}
88
import org.scalacheck.{Arbitrary, Gen}
99
import weaver.scalacheck.Checkers
1010
import weaver.{Expectations, IOSuite, Log, TestName}
@@ -24,8 +24,7 @@ trait PlatformFunSuite[Codec0[_], Platform <: SqlQueryPlatform { type Codec[A] =
2424
given [A](using a: Arbitrary[A]): Arbitrary[A | SqlNull] = Arbitrary(sqlNullGen(a.arbitrary))
2525

2626
given [A](using s: Show[A]): Show[A | SqlNull] = {
27-
import dataprism.sql.sqlNullSyntax.*
28-
Show.show(_.fold("SqlNull")(s.show))
27+
Show.show(v => Nullable.syntax(v).fold("SqlNull")(s.show))
2928
}
3029

3130
def dbTest(name: TestName)(run: DbType ?=> IO[Expectations]): Unit = test(name): db =>

0 commit comments

Comments
 (0)