Skip to content

Commit b6ecd91

Browse files
committed
experimenting with new API
1 parent 39d653c commit b6ecd91

File tree

14 files changed

+517
-15
lines changed

14 files changed

+517
-15
lines changed

build.sbt

+17
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,21 @@ lazy val core = crossProject(JVMPlatform, JSPlatform)
4343
)
4444
)
4545

46+
lazy val next = crossProject(JVMPlatform, JSPlatform)
47+
.crossType(CrossType.Pure)
48+
.in(file("next"))
49+
.settings(name := "skunk-tables-next",
50+
libraryDependencies ++=
51+
Seq(
52+
"org.tpolecat" %% "skunk-core" % skunk,
53+
"org.tpolecat" %% "skunk-circe" % skunk,
54+
"io.github.iltotore" %% "iron" % iron,
55+
"io.github.iltotore" %% "iron-circe" % iron,
56+
"io.github.kitlangton" %% "quotidian" % quotidian,
57+
"org.scalameta" %% "munit" % munit % Test,
58+
"org.typelevel" %% "munit-cats-effect-3" % munitCE % Test
59+
)
60+
)
61+
62+
4663
lazy val docs = project.in(file("site")).enablePlugins(TypelevelSitePlugin)

core/src/main/scala/skunk/tables/ColumnSelect.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import cats.data.NonEmptyList
2323

2424
import quotidian.{MacroMirror, MirrorElem}
2525

26-
import skunk.tables.internal.{MacroTable, Constants, MacroColumn}
26+
import skunk.tables.internal.{MacroTable, MacroColumn}
2727

2828
/** `Columns` is a "selectable" trait, which means the members of it are created dynamically at
2929
* compile-time. Every member maps a member of case class (`T` param from `Table[T]`) into a

core/src/main/scala/skunk/tables/Table.scala

+24-5
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,9 @@ package skunk.tables
1919
import scala.annotation.tailrec
2020
import scala.language.implicitConversions
2121
import scala.quoted.*
22-
2322
import skunk.{Codec, Decoder, Fragment, Void}
2423
import skunk.implicits.*
25-
26-
import skunk.tables.internal.{TableBuilder, TwiddleTCN}
24+
import skunk.tables.internal.{MacroTable, TableBuilder, TwiddleTCN}
2725

2826
/** `Table` is the core entity for skunk-tables API. It links a product type `T` to a Postgres table
2927
* with specific name and constraints and provides several utility methods to query that table in a
@@ -43,7 +41,9 @@ trait Table[T <: Product]:
4341
/** Table name */
4442
def name: Table.Name
4543

46-
/** Union type of all coumn names */
44+
type Name
45+
46+
/** Union type of all column names */
4747
type ColumnName
4848

4949
/** Flat tuple of Scala types used in columns */
@@ -92,14 +92,33 @@ trait Table[T <: Product]:
9292

9393
/** All column names, in their order */
9494
def getColumnNames: List[ColumnName] =
95-
getColumns.map(_.n.toString).asInstanceOf[List[ColumnName]]
95+
getColumns.map(_.n).asInstanceOf[List[ColumnName]]
9696

9797
override def toString: String =
9898
s"Table($name, $select)"
9999

100100
private def getColumns: List[TypedColumn[?, ?, ?, ?]] =
101101
typedColumns.toList.asInstanceOf[List[TypedColumn[?, ?, ?, ?]]]
102102

103+
// NEXT
104+
105+
import skunk.tables.ast.TableH
106+
import scala.compiletime.ops.boolean.&&
107+
108+
type AllSubtypesOfMatch[T <: Tuple, Super] <: Boolean = T match
109+
case EmptyTuple => true
110+
case h *: t =>
111+
h match
112+
case Super => AllSubtypesOfMatch[t, Super]
113+
114+
type AllSubtypesOf[T <: Tuple, Super] = AllSubtypesOfMatch[T, Super] =:= true
115+
116+
type MyColumns[T <: Tuple] = AllSubtypesOf[T, TypedColumn[?, ?, Name, ?]]
117+
118+
object next:
119+
transparent inline def sel[Cols <: Tuple: MyColumns](q: Select => Cols) =
120+
MacroTable.select[q.type, Name, TypedColumns](q)
121+
103122
/** Lower-level API to work with `Fragment` */
104123
object low:
105124

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package skunk.tables.ast
2+
3+
import cats.data.NonEmptyList
4+
import skunk.tables.ast.Select.*
5+
6+
final case class Select(toSelect: NonEmptyList[ToSelect],
7+
distinct: Boolean,
8+
from: Option[String],
9+
where: Option[ConditionTree],
10+
limit: Option[Int],
11+
orderBy: Option[OrderBy])
12+
13+
object Select:
14+
trait SelectH:
15+
type ToSelect <: NonEmptyTuple
16+
type Distinct
17+
type From
18+
type Where
19+
type Limit
20+
type OrderBy
21+
22+
type ColumnName = String
23+
type Value = String
24+
25+
enum ToSelect:
26+
case Constant(value: Value)
27+
case Column(name: ColumnName)
28+
case Function(name: String, toColumn: Option[ColumnName])
29+
30+
trait ConstantH:
31+
type Value
32+
trait ColumnH:
33+
type Name
34+
trait FunctionH:
35+
type Name
36+
type ToColumn
37+
38+
39+
enum ConditionTree:
40+
case Leaf(a: Condition)
41+
case Not(a: ConditionTree)
42+
case And(a: ConditionTree, b: ConditionTree)
43+
case Or(a: ConditionTree, b: ConditionTree)
44+
45+
final case class OrderBy(columns: NonEmptyList[ColumnName], desc: Boolean)
46+
47+
enum Operator:
48+
case LessThan
49+
case GreaterThan
50+
case LessThanOrEqualTo
51+
case GreaterThanOrEqualTo
52+
case Equal
53+
case NotEqual
54+
55+
enum Condition:
56+
case IsNull(column: ColumnName)
57+
case IsNotNull(column: ColumnName)
58+
case Op(column: ColumnName, op: Operator, value: Value)
59+
60+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package skunk.tables.ast
2+
3+
trait TableH:
4+
type NameH
5+
type ColumnsH

core/src/main/scala/skunk/tables/internal/MacroTable.scala

+57-6
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,12 @@
1717
package skunk.tables.internal
1818

1919
import java.rmi.server.ServerNotActiveException
20-
2120
import scala.deriving.Mirror
2221
import scala.Singleton as SSingleton
2322
import scala.quoted.*
24-
2523
import cats.data.NonEmptyList
26-
2724
import quotidian.{MacroMirror, MirrorElem}
28-
25+
import skunk.tables.ast.{Select, TableH}
2926
import skunk.tables.{IsColumn, TypedColumn}
3027

3128
/** MacroTable is a class containing all information necessary for `Table` synthezis. It can be of
@@ -86,7 +83,61 @@ sealed trait MacroTable[Q <: Quotes & Singleton, A]:
8683

8784
object MacroTable:
8885

89-
/** Init phase is when `TableBuilder` knows only information derived from `A` type
86+
import skunk.tables.ast
87+
88+
extension [T <: ast.Select.SelectH](select: T)
89+
transparent inline def where[F](inline f: F) =
90+
${ whereImpl[T, F]('{f}) }
91+
92+
private def whereImpl[T: Type, F: Type](f: Expr[F])(using Quotes) =
93+
import quotes.reflect.*
94+
95+
(Type.of[T], f.asTerm) match
96+
case ('[ast.Select.SelectH { type Columns = cols; type From = from }], Inlined(_, _, Literal(constant))) =>
97+
ConstantType(constant).asType match
98+
case '[where] =>
99+
mkSelectH[cols, false, from, where]
100+
case ('[ast.Select.SelectH { type Columns = cols; type From = from }], f) =>
101+
println(TypeRepr.of[from].widen.dealias.simplified.show)
102+
println(f)
103+
???
104+
105+
inline transparent def select[F, N, C](inline f: F) =
106+
${ selectImpl[F, N, C] }
107+
108+
private def selectImpl[F: Type, N: Type, C: Type](using Quotes) =
109+
import quotes.reflect.*
110+
111+
112+
val fromType = (TypeRepr.of[C].dealias.asType, Type.of[N]) match
113+
case ('[columns], '[name]) =>
114+
val expr = '{
115+
new TableH { }.asInstanceOf[TableH {
116+
type NameH = name
117+
type ColumnsH = columns
118+
}]
119+
}
120+
expr.asTerm.tpe.asType
121+
122+
(Type.of[F], fromType) match
123+
case ('[Function1[?, cols]], '[from]) =>
124+
mkSelectH[cols, false, from, Nothing]
125+
126+
127+
private def mkSelectH[C: Type, D: Type, F: Type, W: Type](using Quotes) =
128+
import quotes.reflect.*
129+
130+
'{
131+
new ast.Select.SelectH { }.asInstanceOf[ast.Select.SelectH {
132+
type Columns = C
133+
type Distinct = D
134+
type From = F
135+
type Where = W
136+
}]
137+
}
138+
139+
140+
/** Init phase is when `TableBuilder` knows only information derived from `A` type
90141
*
91142
* @param columnMap
92143
* an ordered list of columns, where key is column name, value is a pair of `TypeRepr` of that
@@ -135,7 +186,7 @@ object MacroTable:
135186
def columnMap = columns.map(c => c.name -> c.tpe.asInstanceOf[TypeRepr])
136187

137188
/** Get a tuple of fully-typed constraints for a particular column */
138-
def getConstraints(label: String): quotes.reflect.TypeRepr =
189+
def getConstraints(label: String): TypeRepr =
139190
columns.find(column => column.name == label) match
140191
case Some(column) =>
141192
MacroColumn.constraintsTuple(quotes)(column.constraints)

core/src/main/scala/skunk/tables/internal/TableBuilder.scala

+2
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,14 @@ object TableBuilder:
165165

166166
(allColumnsSelect.asTerm.tpe.asType,
167167
getColumnsSelect.asTerm.tpe.asType,
168+
// Type.of[N],
168169
namesUnion.asType,
169170
mTypedColumns.asTerm.tpe.asType,
170171
macroDissect.outType) match
171172
case ('[allSelectType], '[getSelectType], '[namesUnion], '[typedColumnsType], '[dissectOutType]) =>
172173
type Final = Table[P] {
173174
type TypedColumns = typedColumnsType
175+
type Name = N
174176
type Select = allSelectType
175177
type SelectGet = getSelectType
176178
type ColumnName = namesUnion

core/src/test/scala/skunk/tables/FromTableSuite.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ import munit.CatsEffectSuite
2525
class FromTableSuite extends CatsEffectSuite:
2626

2727
test("FromTable successfully synthesized for subset of columns in wrong order") {
28-
type Columns = (TypedColumn["one", Boolean, "foo", EmptyTuple],
29-
TypedColumn["two", Int, "foo", EmptyTuple],
30-
TypedColumn["three", String, "foo", EmptyTuple])
28+
type Columns = (TypedColumn["one", Boolean, "foo", EmptyTuple],
29+
TypedColumn["two", Int, "foo", EmptyTuple],
30+
TypedColumn["three", String, "foo", EmptyTuple])
3131

3232
val instance = summon[FromTable[Columns, ("three", "one")]]
3333

core/src/test/scala/skunk/tables/TableSuite.scala

+6
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ object TableSuite:
104104
case class Person(id: Long, firstName: String, age: Int)
105105
val table = Table.of[Person].withName("persons").build
106106

107+
import skunk.tables.internal.MacroTable.*
108+
109+
val col1: TypedColumn["wrong", Int, "not_persons", EmptyTuple] = TypedColumn("wrong", IsColumn[Int])
110+
val select = table.next.sel(x => (x.id, x.age))
111+
val selectWhere = select.where((x: Int) => x.toString)
112+
107113
case class PersonNew(id: Int, firstName: String, age: Int)
108114

109115
val in = CanInsert[PersonNew].into(table)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package skunk.tables
2+
3+
import scala.compiletime.{constValue, constValueTuple}
4+
import scala.Tuple.Concat
5+
6+
import java.util.UUID
7+
8+
import skunk.Codec
9+
import skunk.codec.text.varchar
10+
import skunk.codec.numeric.int4
11+
import skunk.codec.uuid.uuid
12+
13+
import Column.{Constraint, IsColumn}
14+
15+
16+
type AppendUnique[T <: Tuple, A] <: Tuple = T match
17+
case EmptyTuple => A *: T
18+
case A *: t => A *: t
19+
case h *: t => h *: AppendUnique[t, A]
20+
21+
type ContainsMatch[T <: Tuple, A] <: Boolean = T match
22+
case EmptyTuple => false
23+
case A *: t => true
24+
case h *: t => ContainsMatch[t, A]
25+
26+
type IsOptionMatch[T] <: Boolean = T match
27+
case Option[?] => true
28+
case _ => false
29+
30+
type ContainsNot[T <: Tuple, A] = ContainsMatch[T, A] =:= false
31+
32+
type IsOption[T] = IsOptionMatch[T] =:= true
33+
34+
final case class Column[N <: String & Singleton, A, C <: Tuple] private[tables] (ev: IsColumn[A]):
35+
/** When Database provides a default value */
36+
def withDefault(using ContainsNot[C, Constraint.Default.type]): Column[N, A, Constraint.Default.type *: C] =
37+
Column[N, A, Constraint.Default.type *: C](ev)
38+
39+
object Column:
40+
41+
// A hacky workaround until SIP-47 is implemented
42+
object of:
43+
trait Partial[A]:
44+
protected def isColumn: IsColumn[A]
45+
46+
def apply[N <: String & Singleton](name: N) =
47+
Column[N, A, EmptyTuple](isColumn)
48+
49+
def apply[A](using ev: IsColumn[A]) = new Partial[A]:
50+
def isColumn: IsColumn[A] = ev
51+
52+
trait IsColumn[A]:
53+
def codec: Codec[A]
54+
55+
override def toString: String = s"IsColumn.of(${codec.types.mkString(", ")})"
56+
57+
object IsColumn:
58+
def ofCodec[A](c: Codec[A]) = new IsColumn[A]:
59+
val codec = c
60+
61+
given IsColumn[UUID] = ofCodec(uuid)
62+
given IsColumn[Int] = ofCodec(int4)
63+
given IsColumn[String] = ofCodec(varchar)
64+
65+
given [A] (using IsColumn[A]): IsColumn[Option[A]] =
66+
new IsColumn[Option[A]]:
67+
val codec = summon[IsColumn[A]].codec.opt
68+
69+
enum Constraint:
70+
case Nullable
71+
case Primary
72+
case Default
73+
case Unique
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package skunk.tables
2+
3+
import cats.data.NonEmptyList
4+
import skunk.tables.internal.MacroQuery
5+
import skunk.tables.internal.MacroTable
6+
7+
object Sql:
8+
9+
// table.select(columns => (columns.id, columns.name)).where(columns => columns.age > 21)
10+
// table.select(columns => (columns.id, columns.name)).where(columns => columns.age > 21).count
11+
// table.select(columns => (columns.id, columns.name)).where(columns => columns.age > 21).orderBy(desc).limit(10)
12+
13+
// GROUPING!
14+
// table.select(columns => (columns.department, avg(columns.salary))).groupBy(columns => columns.department)
15+
// table
16+
// .select(columns => (columns.department, avg(columns.salary)))
17+
// .where(columns => columns.age > 30)
18+
// .groupBy(columns => columns.department)
19+
20+
// // OR
21+
22+
// table.query(columns => columns.age > 21).map(columns => (columns.id, columns.name))
23+
24+
// query (or select) should construct a compile-time object
25+
// that later translated to run-time object
26+
27+
case class Tbl(name: String, columns: (String, String))

0 commit comments

Comments
 (0)