diff --git a/src/main/scala/minisql/Meta.scala b/src/main/scala/minisql/Meta.scala new file mode 100644 index 0000000..38d8fb9 --- /dev/null +++ b/src/main/scala/minisql/Meta.scala @@ -0,0 +1,3 @@ +package minisql + +type QueryMeta diff --git a/src/main/scala/minisql/ParamEncoder.scala b/src/main/scala/minisql/ParamEncoder.scala index a55c0a3..05ef348 100644 --- a/src/main/scala/minisql/ParamEncoder.scala +++ b/src/main/scala/minisql/ParamEncoder.scala @@ -1,8 +1,23 @@ package minisql +import scala.util.Try + trait ParamEncoder[E] { type Stmt def setParam(s: Stmt, idx: Int, v: E): Unit } + +trait ColumnDecoder[X] { + + type DBRow + + def decode(row: DBRow, idx: Int): Try[X] +} + +object ColumnDecoder { + type Aux[R, X] = ColumnDecoder[X] { + type DBRow = R + } +} diff --git a/src/main/scala/minisql/Quoted.scala b/src/main/scala/minisql/Quoted.scala new file mode 100644 index 0000000..c7c9c7f --- /dev/null +++ b/src/main/scala/minisql/Quoted.scala @@ -0,0 +1,86 @@ +package minisql + +import minisql.* +import minisql.idiom.* +import minisql.parsing.* +import minisql.util.* +import minisql.ast.{Ast, Entity, Map, Property, Ident, Filter, given} +import scala.quoted.* +import scala.compiletime.* +import scala.compiletime.ops.string.* +import scala.collection.immutable.{Map => IMap} + +opaque type Quoted <: Ast = Ast + +opaque type Query[E] <: Quoted = Quoted + +opaque type EntityQuery[E] <: Query[E] = Query[E] + +object EntityQuery { + extension [E](inline e: EntityQuery[E]) { + inline def map[E1](inline f: E => E1): EntityQuery[E1] = { + transform(e)(f)(Map.apply) + } + + inline def filter(inline f: E => Boolean): EntityQuery[E] = { + transform(e)(f)(Filter.apply) + } + } +} + +private inline def transform[A, B](inline q1: Quoted)( + inline f: A => B +)(inline fast: (Ast, Ident, Ast) => Ast): Quoted = { + fast(q1, f.param0, f.body) +} + +inline def query[E](inline table: String): EntityQuery[E] = + Entity(table, Nil) + +extension [A, B](inline f1: A => B) { + private inline def param0 = parsing.parseParamAt(f1, 0) + private inline def body = parsing.parseBody(f1) +} + +extension [A1, A2, B](inline f1: (A1, A2) => B) { + private inline def param0 = parsing.parseParamAt(f1, 0) + private inline def param1 = parsing.parseParamAt(f1, 1) + private inline def body = parsing.parseBody(f1) +} + +def lift[X](x: X)(using e: ParamEncoder[X]): X = throw NonQuotedException() + +class NonQuotedException extends Exception("Cannot be used at runtime") + +private[minisql] inline def compile[I <: Idiom, N <: NamingStrategy]( + inline q: Quoted, + inline idiom: I, + inline naming: N +): Statement = ${ compileImpl[I, N]('q, 'idiom, 'naming) } + +private def compileImpl[I <: Idiom, N <: NamingStrategy]( + q: Expr[Quoted], + idiom: Expr[I], + n: Expr[N] +)(using Quotes, Type[I], Type[N]): Expr[Statement] = { + import quotes.reflect.* + q.value match { + case Some(ast) => + val idiom = LoadObject[I].getOrElse( + report.errorAndAbort(s"Idiom not known at compile") + ) + + val naming = LoadNaming + .static[N] + .getOrElse(report.errorAndAbort(s"NamingStrategy not known at compile")) + + val stmt = idiom.translate(ast)(using naming) + Expr(stmt._2) + case None => + report.info("Dynamic Query") + '{ + $idiom.translate($q)(using $n)._2 + } + + } +} diff --git a/src/main/scala/minisql/context/Context.scala b/src/main/scala/minisql/context/Context.scala new file mode 100644 index 0000000..47f5f2e --- /dev/null +++ b/src/main/scala/minisql/context/Context.scala @@ -0,0 +1,66 @@ +package minisql.context + +import scala.deriving.* +import scala.compiletime.* +import scala.util.Try +import minisql.util.* +import minisql.idiom.{Idiom, Statement} +import minisql.{NamingStrategy, ParamEncoder} +import minisql.ColumnDecoder + +trait Context[I <: Idiom, N <: NamingStrategy] { selft => + + val idiom: I + val naming: NamingStrategy + + type DBStatement + type DBRow + type DBResultSet + + trait RowExtract[A] { + def extract(row: DBRow): Try[A] + } + + object RowExtract { + + private class ExtractorImpl[A]( + decoders: IArray[Any], + m: Mirror.ProductOf[A] + ) extends RowExtract[A] { + def extract(row: DBRow): Try[A] = { + val decodedFields = decoders.zipWithIndex.traverse { + case (d, i) => + d.asInstanceOf[Decoder[?]].decode(row, i) + } + decodedFields.map { vs => + m.fromProduct(Tuple.fromIArray(vs)) + } + } + } + + inline given [P <: Product](using m: Mirror.ProductOf[P]): RowExtract[P] = { + val decoders = summonAll[Tuple.Map[m.MirroredElemTypes, Decoder]] + ExtractorImpl(decoders.toIArray.asInstanceOf, m) + } + } + + type Encoder[X] = ParamEncoder[X] { + type Stmt = DBStatement + } + + type Decoder[X] = ColumnDecoder.Aux[DBRow, X] + + type DBIO[X] = ( + statement: Statement, + params: (Any, Encoder[?]), + extract: RowExtract[X] + ) + + inline def io[E]( + inline q: minisql.Query[E] + )(using r: RowExtract[E]): DBIO[Seq[E]] = { + val statement = minisql.compile(q, idiom, naming) + ??? + } + +} diff --git a/src/main/scala/minisql/dsl.scala b/src/main/scala/minisql/dsl.scala deleted file mode 100644 index ace3d8f..0000000 --- a/src/main/scala/minisql/dsl.scala +++ /dev/null @@ -1,45 +0,0 @@ -package minisql.dsl - -import minisql.* -import minisql.parsing.* -import minisql.ast.{Ast, Entity, Map, Property, Ident, given} -import scala.quoted.* -import scala.compiletime.* -import scala.compiletime.ops.string.* -import scala.collection.immutable.{Map => IMap} - -opaque type Quoted <: Ast = Ast - -opaque type Query[E] <: Quoted = Quoted - -opaque type EntityQuery[E] <: Query[E] = Query[E] - -extension [E](inline e: EntityQuery[E]) { - inline def map[E1](inline f: E => E1): EntityQuery[E1] = { - transform(e)(f)(Map.apply) - } -} - -private inline def transform[A, B](inline q1: Quoted)( - inline f: A => B -)(inline fast: (Ast, Ident, Ast) => Ast): Quoted = { - fast(q1, f.param0, f.body) -} - -inline def query[E](inline table: String): EntityQuery[E] = - Entity(table, Nil) - -extension [A, B](inline f1: A => B) { - private inline def param0 = parsing.parseParamAt(f1, 0) - private inline def body = parsing.parseBody(f1) -} - -extension [A1, A2, B](inline f1: (A1, A2) => B) { - private inline def param0 = parsing.parseParamAt(f1, 0) - private inline def param1 = parsing.parseParamAt(f1, 1) - private inline def body = parsing.parseBody(f1) -} - -def lift[X](x: X)(using e: ParamEncoder[X]): X = throw NonQuotedException() - -class NonQuotedException extends Exception("Cannot be used at runtime") diff --git a/src/main/scala/minisql/idiom/Idiom.scala b/src/main/scala/minisql/idiom/Idiom.scala index 7e0cd01..43b6110 100644 --- a/src/main/scala/minisql/idiom/Idiom.scala +++ b/src/main/scala/minisql/idiom/Idiom.scala @@ -14,7 +14,7 @@ trait Idiom extends Capabilities { def liftingPlaceholder(index: Int): String - def translate(ast: Ast)(implicit naming: NamingStrategy): (Ast, Statement) + def translate(ast: Ast)(using naming: NamingStrategy): (Ast, Statement) def format(queryString: String): String = queryString diff --git a/src/main/scala/minisql/idiom/ReifyStatement.scala b/src/main/scala/minisql/idiom/ReifyStatement.scala index a3e8902..aea8322 100644 --- a/src/main/scala/minisql/idiom/ReifyStatement.scala +++ b/src/main/scala/minisql/idiom/ReifyStatement.scala @@ -63,6 +63,6 @@ object ReifyStatement { emptySetContainsToken: Token => Token, liftMap: SMap[String, (Any, Any)] ): (Token) = { - statement + ??? } } diff --git a/src/main/scala/minisql/parsing/LiftParsing.scala b/src/main/scala/minisql/parsing/LiftParsing.scala index c01df89..9a0f32b 100644 --- a/src/main/scala/minisql/parsing/LiftParsing.scala +++ b/src/main/scala/minisql/parsing/LiftParsing.scala @@ -3,7 +3,7 @@ package minisql.parsing import scala.quoted.* import minisql.ParamEncoder import minisql.ast -import minisql.dsl.* +import minisql.* private[parsing] def liftParsing( astParser: => Parser[ast.Ast] diff --git a/src/main/scala/minisql/parsing/OperationParsing.scala b/src/main/scala/minisql/parsing/OperationParsing.scala index df93010..4425dd5 100644 --- a/src/main/scala/minisql/parsing/OperationParsing.scala +++ b/src/main/scala/minisql/parsing/OperationParsing.scala @@ -7,7 +7,7 @@ import minisql.ast.{ NumericOperator, BooleanOperator } -import minisql.dsl.* +import minisql.* import scala.quoted._ private[parsing] def operationParsing( diff --git a/src/main/scala/minisql/util/CollectTry.scala b/src/main/scala/minisql/util/CollectTry.scala index f0ee506..74a6984 100644 --- a/src/main/scala/minisql/util/CollectTry.scala +++ b/src/main/scala/minisql/util/CollectTry.scala @@ -1,6 +1,24 @@ package minisql.util -import scala.util.Try +import scala.util.* + +extension [A](xs: IArray[A]) { + private[minisql] def traverse[B](f: A => Try[B]): Try[IArray[B]] = { + val out = IArray.newBuilder[Any] + var left: Option[Throwable] = None + xs.foreach { (v) => + if (!left.isDefined) { + f(v) match { + case Failure(e) => + left = Some(e) + case Success(r) => + out += r + } + } + } + left.toLeft(out.result().asInstanceOf).toTry + } +} object CollectTry { def apply[T](list: List[Try[T]]): Try[List[T]] = diff --git a/src/main/scala/minisql/util/LoadObject.scala b/src/main/scala/minisql/util/LoadObject.scala index 83bbec0..8c13c8f 100644 --- a/src/main/scala/minisql/util/LoadObject.scala +++ b/src/main/scala/minisql/util/LoadObject.scala @@ -5,10 +5,15 @@ import scala.util.Try object LoadObject { + def apply[T](using Quotes, Type[T]): Try[T] = { + import quotes.reflect.* + apply(TypeRepr.of[T]) + } + def apply[T](using Quotes)(ot: quotes.reflect.TypeRepr): Try[T] = Try { import quotes.reflect.* val moduleClsName = ot.typeSymbol.companionModule.moduleClass.fullName - val moduleCls = Class.forName(moduleClsName) + val moduleCls = Class.forName(moduleClsName) val field = moduleCls .getFields() .find { f => diff --git a/src/test/scala/minisql/parsing/ParsingSuite.scala b/src/test/scala/minisql/parsing/ParsingSuite.scala index 043346d..b90f9f8 100644 --- a/src/test/scala/minisql/parsing/ParsingSuite.scala +++ b/src/test/scala/minisql/parsing/ParsingSuite.scala @@ -4,11 +4,35 @@ import minisql.ast.* class ParsingSuite extends munit.FunSuite { - inline def testParseInline(inline x: Any, ast: Ast) = { + test("Ident") { + val x = 1 assertEquals(Parsing.parse(x), Ident("x")) } - test("Ident") { - val x = 1 + test("NumericOperator.+") { + val a = 1 + val b = 2 + assertEquals( + Parsing.parse(a + b), + BinaryOperation(Ident("a"), NumericOperator.+, Ident("b")) + ) + } + + test("NumericOperator.-") { + val a = 1 + val b = 2 + assertEquals( + Parsing.parse(a - b), + BinaryOperation(Ident("a"), NumericOperator.-, Ident("b")) + ) + } + + test("NumericOperator.*") { + val a = 1 + val b = 2 + assertEquals( + Parsing.parse(a * b), + BinaryOperation(Ident("a"), NumericOperator.*, Ident("b")) + ) } } diff --git a/src/test/scala/minisql/parsing/QuerySuite.scala b/src/test/scala/minisql/parsing/QuerySuite.scala new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/test/scala/minisql/parsing/QuerySuite.scala @@ -0,0 +1 @@ +