From cd25ed9c8ab3a0bdff2d1a7a6387979768eab24b Mon Sep 17 00:00:00 2001 From: Arseniy Zhizhelev Date: Fri, 22 Mar 2024 19:44:51 +0300 Subject: [PATCH 1/8] Add arbitrary arity tuple Quat --- .../scala/io/getquill/quat/QuatMaking.scala | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/quill-sql/src/main/scala/io/getquill/quat/QuatMaking.scala b/quill-sql/src/main/scala/io/getquill/quat/QuatMaking.scala index e81c88ca..a88f3bc5 100644 --- a/quill-sql/src/main/scala/io/getquill/quat/QuatMaking.scala +++ b/quill-sql/src/main/scala/io/getquill/quat/QuatMaking.scala @@ -187,6 +187,25 @@ trait QuatMakingBase { None } + object ArbitraryArityTupleType { + def unapply(using Quotes)(tpe: quotes.reflect.TypeRepr): Option[List[quotes.reflect.TypeRepr]] = + if (tpe.is[Tuple]) + Some(tupleParts(tpe)) + else + None + + @tailrec + def tupleParts(using Quotes)(tpe: quotes.reflect.TypeRepr, accum: List[quotes.reflect.TypeRepr] = Nil): List[quotes.reflect.TypeRepr] = + tpe.asType match { + case '[h *: t] => + val htpe = quotes.reflect.TypeRepr.of[h] + val ttpe = quotes.reflect.TypeRepr.of[t] + tupleParts(ttpe, htpe :: accum) + case '[EmptyTuple] => + accum.reverse + } + } + object OptionType { def unapply(using Quotes)(tpe: quotes.reflect.TypeRepr): Option[quotes.reflect.TypeRepr] = { import quotes.reflect._ @@ -381,6 +400,10 @@ trait QuatMakingBase { case CaseClassBaseType(name, fields) if !existsEncoderFor(tpe) || tpe <:< TypeRepr.of[Udt] => Quat.Product(name, fields.map { case (fieldName, fieldType) => (fieldName, parseType(fieldType)) }) + case ArbitraryArityTupleType(tupleParts) => + Quat.Product("Tuple", tupleParts.zipWithIndex.map { case (fieldType, idx) => (s"_${idx + 1}", parseType(fieldType)) }) + + //Quat.Product // If we are already inside a bounded type, treat an arbitrary type as a interface list case ArbitraryBaseType(name, fields) if (boundedInterfaceType) => Quat.Product(name, fields.map { case (fieldName, fieldType) => (fieldName, parseType(fieldType)) }) From 3753bd75f0fa0ad6e0631ee18168ff66431b9e65 Mon Sep 17 00:00:00 2001 From: Arseniy Zhizhelev Date: Fri, 22 Mar 2024 22:49:16 +0300 Subject: [PATCH 2/8] Decode arbitrary arity tuple --- .../io/getquill/generic/GenericDecoder.scala | 21 +++++++++++ .../io/getquill/ArbitraryTupleSpec.scala | 36 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala diff --git a/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala b/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala index 39370868..749a3947 100644 --- a/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala +++ b/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala @@ -192,6 +192,11 @@ object GenericDecoder { TypeRepr.of[T] <:< TypeRepr.of[Option[Any]] } + private def isTuple[T: Type](using Quotes) = { + import quotes.reflect._ + TypeRepr.of[T] <:< TypeRepr.of[Tuple] + } + private def isBuiltInType[T: Type](using Quotes) = { import quotes.reflect._ isOption[T] || (TypeRepr.of[T] <:< TypeRepr.of[Seq[_]]) @@ -207,6 +212,22 @@ object GenericDecoder { case '[Option[tpe]] => decodeOptional[tpe, ResultRow, Session](index, baseIndex, resultRow, session) } + } else if (isTuple[T]) { + val decoderIndex = overriddenIndex.getOrElse(elementIndex) + Type.of[T] match { + case '[EmptyTuple] => + FlattenData(Type.of[T], '{ EmptyTuple }, '{false}, index) + case '[h *: t] => + val FlattenData(htpe, hvalue, hnullCheck, headIndex) = decode[h, ResultRow, Session]( index, baseIndex, resultRow, session, overriddenIndex) + val nextIndex = headIndex + 1 + val FlattenData(ttpe, tvalue, tnullCheck, lastIndex) = decode[t, ResultRow, Session](nextIndex, baseIndex, resultRow, session, overriddenIndex) + val hhvalue = hvalue.asExprOf[h] + val ttvalue = tvalue.asExprOf[t] + // TODO speedup construction via ConstructDecoded + // val m = Expr.summon[Mirror.ProductOf[T]] + // ConstructDecoded(types, terms, m) + FlattenData(Type.of[h *: t], '{$hhvalue *: $ttvalue}, '{$hnullCheck || $tnullCheck}, lastIndex) + } } else { // specifically if there is a decoder found, allow optional override of the index via a resolver val decoderIndex = overriddenIndex.getOrElse(elementIndex) diff --git a/quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala b/quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala new file mode 100644 index 00000000..b3a8b697 --- /dev/null +++ b/quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala @@ -0,0 +1,36 @@ +package io.getquill + +import io.getquill.context.mirror.{MirrorSession, Row} +import io.getquill.generic.TupleMember + +class ArbitraryTupleSpec extends Spec { + + val ctx = new MirrorContext(PostgresDialect, Literal) + import ctx._ + + type MyRow1 = (Int, String) + type MyRow2 = Int *: String *: EmptyTuple + + "ordinary tuple" in { + inline def q = quote{ + querySchema[MyRow1]("my_table", t => t._1 -> "int_field", t => t._2 -> "string_field") + } + + val result = ctx.run(q) + + result.extractor(Row(123, "St"), MirrorSession.default) mustEqual + (123, "St") + } + + "arbitrary long tuple" in { + inline def q = quote{ + querySchema[MyRow2]("my_table", t => t._1 -> "int_field", t => t._2 -> "string_field") + } + + val result = ctx.run(q) + + result.extractor(Row(123, "St"), MirrorSession.default) mustEqual + (123, "St") + } + +} From 748d619763f2a866f538b7a8c761d12cd873050a Mon Sep 17 00:00:00 2001 From: Arseniy Zhizhelev Date: Tue, 26 Mar 2024 23:40:00 +0300 Subject: [PATCH 3/8] Use runtime.Tuples to construct Tuple --- .../io/getquill/generic/GenericDecoder.scala | 110 ++++++++++++------ 1 file changed, 76 insertions(+), 34 deletions(-) diff --git a/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala b/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala index 749a3947..835d67f6 100644 --- a/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala +++ b/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala @@ -125,6 +125,31 @@ object GenericDecoder { } } // end flatten + // similar to flatten but without labels + @tailrec + def values[ResultRow: Type, Session: Type, Types: Type]( + index: Int, + baseIndex: Expr[Int], + resultRow: Expr[ResultRow], + session: Expr[Session] + )(accum: List[FlattenData] = List())(using Quotes): List[FlattenData] = { + import quotes.reflect.{Term => QTerm, _} + + Type.of[Types] match { + case '[tpe *: types] if Expr.summon[GenericDecoder[ResultRow, Session, tpe, DecodingType.Specific]].isEmpty => + val result = decode[tpe, ResultRow, Session](index, baseIndex, resultRow, session) + val nextIndex = result.index + 1 + values[ResultRow, Session, types](nextIndex, baseIndex, resultRow, session)(result +: accum) + case '[tpe *: types] => + val result = decode[tpe, ResultRow, Session](index, baseIndex, resultRow, session, None) + val nextIndex = index + 1 + values[ResultRow, Session, types](nextIndex, baseIndex, resultRow, session)(result +: accum) + case '[EmptyTuple] => accum + + case typesTup => report.throwError("Cannot Derive Product during Values extraction:\n" + typesTup) + } + } // end values + def decodeOptional[T: Type, ResultRow: Type, Session: Type](index: Int, baseIndex: Expr[Int], resultRow: Expr[ResultRow], session: Expr[Session])(using Quotes): FlattenData = { import quotes.reflect._ // Try to summon a specific optional from the context, this may not exist since @@ -162,10 +187,10 @@ object GenericDecoder { // they will be the constructor and/or any other field-decoders: // List((new Name(Decoder("Joe") || Decoder("Bloggs")), Decoder(123)) // This is what needs to be fed into the constructor of the outer-entity i.e. - // new Person((new Name(Decoder("Joe") || Decoder("Bloggs")), Decoder(123)) - val productElments = flattenData.map(_.decodedExpr) + // new Person((new Name(Decoder("Joe") || Decoder("Bloggs")), DecodConstructDecodeder(123)) + val productElements = flattenData.map(_.decodedExpr) // actually doing the construction i.e. `new Person(...)` - val constructed = ConstructDecoded[T](types, productElments, m) + val constructed = ConstructDecoded[T](types, productElements, m) // E.g. for Person("Joe", 123) the List(q"!nullChecker(0,row)", q"!nullChecker(1,row)") columns // that eventually turn into List(!NullChecker("Joe"), !NullChecker(123)) columns. @@ -213,21 +238,11 @@ object GenericDecoder { decodeOptional[tpe, ResultRow, Session](index, baseIndex, resultRow, session) } } else if (isTuple[T]) { - val decoderIndex = overriddenIndex.getOrElse(elementIndex) - Type.of[T] match { - case '[EmptyTuple] => - FlattenData(Type.of[T], '{ EmptyTuple }, '{false}, index) - case '[h *: t] => - val FlattenData(htpe, hvalue, hnullCheck, headIndex) = decode[h, ResultRow, Session]( index, baseIndex, resultRow, session, overriddenIndex) - val nextIndex = headIndex + 1 - val FlattenData(ttpe, tvalue, tnullCheck, lastIndex) = decode[t, ResultRow, Session](nextIndex, baseIndex, resultRow, session, overriddenIndex) - val hhvalue = hvalue.asExprOf[h] - val ttvalue = tvalue.asExprOf[t] - // TODO speedup construction via ConstructDecoded - // val m = Expr.summon[Mirror.ProductOf[T]] - // ConstructDecoded(types, terms, m) - FlattenData(Type.of[h *: t], '{$hhvalue *: $ttvalue}, '{$hnullCheck || $tnullCheck}, lastIndex) - } + val flattenData = values[ResultRow, Session, T](index, baseIndex, resultRow, session)().reverse + val elementTerms = flattenData.map(_.decodedExpr) // expressions that represent values for tuple elements + val constructed = '{scala.runtime.Tuples.fromArray(${Varargs(elementTerms)}.toArray[Any](Predef.summon[ClassTag[Any]]).asInstanceOf[Array[Object]]).asInstanceOf[T]} + val nullChecks = flattenData.map(_._3).reduce((a, b) => '{ $a || $b }) + FlattenData(Type.of[T], constructed, nullChecks, flattenData.last.index) } else { // specifically if there is a decoder found, allow optional override of the index via a resolver val decoderIndex = overriddenIndex.getOrElse(elementIndex) @@ -362,22 +377,49 @@ object ConstructDecoded { val tpe = TypeRepr.of[T] val constructor = TypeRepr.of[T].typeSymbol.primaryConstructor // If we are a tuple, we can easily construct it - if (tpe <:< TypeRepr.of[Tuple]) { - val construct = - Apply( - TypeApply( - Select(New(TypeTree.of[T]), constructor), - types.map { tpe => - tpe match { - case '[tt] => TypeTree.of[tt] - } - } - ), - terms.map(_.asTerm) - ) - // println(s"=========== Create from Tuple Constructor ${Format.Expr(construct.asExprOf[T])} ===========") - construct.asExprOf[T] - // If we are a case class with no generic parameters, we can easily construct it + if (tpe <:< TypeRepr.of[EmptyTuple]) { + '{EmptyTuple} + } else if (tpe <:< TypeRepr.of[Tuple]) { + '{scala.runtime.Tuples.fromIArray(IArray(${Varargs(terms)})).asInstanceOf[T]} + // val t = + // Varargs + // Type.of[T] match { + // case '[h *: t] => + // val construct = Type.of[t] match { + // case '[EmptyTuple] => + // println(" ConstructDecoded: t=et") + // Apply( + // TypeApply( + // Select(New(TypeTree.of[Tuple1[h]]), constructor), + // types.map { tpe => + // tpe match { + // case '[tt] => TypeTree.of[tt] + // } + // } + // ), + // terms.map(_.asTerm) + // ) + // case _ => + // val typeTrees = types.map { tpe => + // tpe match { + // case '[tt] => TypeTree.of[tt] + // } + // } + // val terms2 = terms.map(_.asTerm) + // val ttt = TypeTree.of[T] + + // println(s" ConstructDecoded: t=t; ttt = $ttt; typeTrees = $typeTrees; terms2 = $terms2") + // Apply( + // Select(New(TypeTree.of[IArray[Object]]), constructor), + // // VarArgs + // terms2 + // ) + // } + // println(s"=========== Create from Tuple Constructor: Tree: $construct ===========") + // println(s"=========== Create from Tuple Constructor ${Format.Expr(construct.asExprOf[T])} ===========") + // construct.asExprOf[T] + // // If we are a case class with no generic parameters, we can easily construct it + // } } else if (tpe.classSymbol.exists(_.flags.is(Flags.Case)) && !constructor.paramSymss.exists(_.exists(_.isTypeParam))) { val construct = Apply( From 71958d9e3d3846554eefbbcdb4dd92797406d660 Mon Sep 17 00:00:00 2001 From: Arseniy Zhizhelev Date: Tue, 16 Apr 2024 09:20:45 +0300 Subject: [PATCH 4/8] Fat finger fix --- .../src/main/scala/io/getquill/generic/GenericDecoder.scala | 3 ++- quill-sql/src/main/scala/io/getquill/quat/QuatMaking.scala | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala b/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala index 835d67f6..6a625c0c 100644 --- a/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala +++ b/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala @@ -187,7 +187,7 @@ object GenericDecoder { // they will be the constructor and/or any other field-decoders: // List((new Name(Decoder("Joe") || Decoder("Bloggs")), Decoder(123)) // This is what needs to be fed into the constructor of the outer-entity i.e. - // new Person((new Name(Decoder("Joe") || Decoder("Bloggs")), DecodConstructDecodeder(123)) + // new Person((new Name(Decoder("Joe") || Decoder("Bloggs")), Decoder(123)) val productElements = flattenData.map(_.decodedExpr) // actually doing the construction i.e. `new Person(...)` val constructed = ConstructDecoded[T](types, productElements, m) @@ -381,6 +381,7 @@ object ConstructDecoded { '{EmptyTuple} } else if (tpe <:< TypeRepr.of[Tuple]) { '{scala.runtime.Tuples.fromIArray(IArray(${Varargs(terms)})).asInstanceOf[T]} + // Alternative version: // val t = // Varargs // Type.of[T] match { diff --git a/quill-sql/src/main/scala/io/getquill/quat/QuatMaking.scala b/quill-sql/src/main/scala/io/getquill/quat/QuatMaking.scala index a88f3bc5..bdda1183 100644 --- a/quill-sql/src/main/scala/io/getquill/quat/QuatMaking.scala +++ b/quill-sql/src/main/scala/io/getquill/quat/QuatMaking.scala @@ -403,7 +403,6 @@ trait QuatMakingBase { case ArbitraryArityTupleType(tupleParts) => Quat.Product("Tuple", tupleParts.zipWithIndex.map { case (fieldType, idx) => (s"_${idx + 1}", parseType(fieldType)) }) - //Quat.Product // If we are already inside a bounded type, treat an arbitrary type as a interface list case ArbitraryBaseType(name, fields) if (boundedInterfaceType) => Quat.Product(name, fields.map { case (fieldName, fieldType) => (fieldName, parseType(fieldType)) }) From 306c5246862c5fd93c8fd7209f6e60259a335618 Mon Sep 17 00:00:00 2001 From: Arseniy Zhizhelev Date: Sun, 28 Apr 2024 21:05:29 +0300 Subject: [PATCH 5/8] Support beta reduction with arbitrary tuples --- .../io/getquill/parser/ParserHelpers.scala | 6 ++++++ .../scala/io/getquill/ArbitraryTupleSpec.scala | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/quill-sql/src/main/scala/io/getquill/parser/ParserHelpers.scala b/quill-sql/src/main/scala/io/getquill/parser/ParserHelpers.scala index b9a69d38..e0c912cd 100644 --- a/quill-sql/src/main/scala/io/getquill/parser/ParserHelpers.scala +++ b/quill-sql/src/main/scala/io/getquill/parser/ParserHelpers.scala @@ -490,6 +490,12 @@ object ParserHelpers { binds.zipWithIndex.flatMap { case (bind, idx) => tupleBindsPath(bind, path :+ s"_${idx + 1}") } + case Unapply(TypeApply(Select(TIdent("*:"), "unapply"), types), implicits, List(h, t)) => + List( + tupleBindsPath(h, path :+ s"head"), + tupleBindsPath(h, path :+ s"tail") + ) + .flatten // If it's a "case _ => ..." then that just translates into the body expression so we don't // need a clause to beta reduction over the entire partial-function case TIdent("_") => diff --git a/quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala b/quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala index b3a8b697..64825321 100644 --- a/quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala +++ b/quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala @@ -33,4 +33,21 @@ class ArbitraryTupleSpec extends Spec { (123, "St") } + "get field of arbitrary long tuple" in { + inline def q = quote{ + querySchema[MyRow2]("my_table", t => t._1 -> "int_field", t => t._2 -> "string_field") + } + + inline def g = quote{ + q.map{t => t match { + case h *: tail => h + } + } + } + val result = ctx.run(g) + + result.extractor(Row(123, "St"), MirrorSession.default) mustEqual + (123) + } + } From c577120f04e54b36008460c16d696af175ca4dec Mon Sep 17 00:00:00 2001 From: Arseniy Zhizhelev Date: Tue, 30 Apr 2024 21:19:32 +0300 Subject: [PATCH 6/8] Support empty tuple decoding --- .../scala/io/getquill/generic/GenericDecoder.scala | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala b/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala index 6a625c0c..3fa74d68 100644 --- a/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala +++ b/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala @@ -238,11 +238,15 @@ object GenericDecoder { decodeOptional[tpe, ResultRow, Session](index, baseIndex, resultRow, session) } } else if (isTuple[T]) { - val flattenData = values[ResultRow, Session, T](index, baseIndex, resultRow, session)().reverse - val elementTerms = flattenData.map(_.decodedExpr) // expressions that represent values for tuple elements - val constructed = '{scala.runtime.Tuples.fromArray(${Varargs(elementTerms)}.toArray[Any](Predef.summon[ClassTag[Any]]).asInstanceOf[Array[Object]]).asInstanceOf[T]} - val nullChecks = flattenData.map(_._3).reduce((a, b) => '{ $a || $b }) - FlattenData(Type.of[T], constructed, nullChecks, flattenData.last.index) + if (TypeRepr.of[T] <:< TypeRepr.of[EmptyTuple]) { + FlattenData(Type.of[T], '{ EmptyTuple }, '{ false }, index) + } else { + val flattenData = values[ResultRow, Session, T](index, baseIndex, resultRow, session)().reverse + val elementTerms = flattenData.map(_.decodedExpr) // expressions that represent values for tuple elements + val constructed = '{ scala.runtime.Tuples.fromArray(${ Varargs(elementTerms) }.toArray[Any](Predef.summon[ClassTag[Any]]).asInstanceOf[Array[Object]]).asInstanceOf[T] } + val nullChecks = flattenData.map(_._3).reduce((a, b) => '{ $a || $b }) + FlattenData(Type.of[T], constructed, nullChecks, flattenData.last.index) + } } else { // specifically if there is a decoder found, allow optional override of the index via a resolver val decoderIndex = overriddenIndex.getOrElse(elementIndex) From 8690ffa7ed378f122062a552dbfd71ddee3ac03d Mon Sep 17 00:00:00 2001 From: Arseniy Zhizhelev Date: Sun, 5 May 2024 23:15:29 +0300 Subject: [PATCH 7/8] Parse arbitrary tuples construction --- .../scala/io/getquill/parser/Parser.scala | 122 ++++++++++++++++++ .../io/getquill/ArbitraryTupleSpec.scala | 101 ++++++++++++--- 2 files changed, 207 insertions(+), 16 deletions(-) diff --git a/quill-sql/src/main/scala/io/getquill/parser/Parser.scala b/quill-sql/src/main/scala/io/getquill/parser/Parser.scala index f120fe48..be033eb5 100644 --- a/quill-sql/src/main/scala/io/getquill/parser/Parser.scala +++ b/quill-sql/src/main/scala/io/getquill/parser/Parser.scala @@ -53,6 +53,7 @@ trait ParserLibrary extends ParserFactory { protected def functionParser(using Quotes, TranspileConfig) = ParserChain.attempt(FunctionParser(_)) protected def functionApplyParser(using Quotes, TranspileConfig) = ParserChain.attempt(FunctionApplyParser(_)) protected def valParser(using Quotes, TranspileConfig) = ParserChain.attempt(ValParser(_)) + protected def arbitraryTupleParser(using Quotes, TranspileConfig) = ParserChain.attempt(ArbitraryTupleBlockParser(_)) protected def blockParser(using Quotes, TranspileConfig) = ParserChain.attempt(BlockParser(_)) protected def extrasParser(using Quotes, TranspileConfig) = ParserChain.attempt(ExtrasParser(_)) protected def operationsParser(using Quotes, TranspileConfig) = ParserChain.attempt(OperationsParser(_)) @@ -88,6 +89,7 @@ trait ParserLibrary extends ParserFactory { .orElse(functionParser) // decided to have it be it's own parser unlike Quill3 .orElse(patMatchParser) .orElse(valParser) + .orElse(arbitraryTupleParser) .orElse(blockParser) .orElse(operationsParser) .orElse(extrasParser) @@ -153,6 +155,126 @@ class ValParser(val rootParse: Parser)(using Quotes, TranspileConfig) case Unseal(ValDefTerm(ast)) => ast } } +/** + * Matches `runtime.Tuples.cons(head,tail)`. + */ +object TupleCons { + def unapply(using Quotes)(t: quotes.reflect.Term): Option[(quotes.reflect.Term, quotes.reflect.Term)] = { + import quotes.reflect.* + t match { + case Apply(Select(Select(Ident("runtime"), "Tuples"), "cons"), List(head, tail)) => + Some((head, tail)) + case _ => + None + } + } +} + +/** + * Matches inner.asInstanceOf[T]: T + */ +object AsInstanceOf { + def unapply(using Quotes)(term: quotes.reflect.Term): Option[quotes.reflect.Term] = { + import quotes.reflect._ + term match { + case TypeApply(Select(inner, "asInstanceOf"), _) => Some(inner) + case _ => None + } + } +} + +/** + * Matches an inlined call to `Tuple.*:`: + * {{{ + * { + * val Tuple_this: scala.Tuple$package.EmptyTuple.type = scala.Tuple$package.EmptyTuple + * + * (scala.runtime.Tuples.cons(i, Tuple_this).asInstanceOf[scala.*:[scala.Int, scala.Tuple$package.EmptyTuple.type]]: scala.*:[scala.Int, scala.Tuple$package.EmptyTuple]) + * } + * }}} + */ +object ArbitraryTupleConstructionInlined { + def unapply(using Quotes)(t: quotes.reflect.Term): Option[(quotes.reflect.Term, quotes.reflect.Term)] = { + import quotes.reflect.{Ident => TIdent, *} + t match { + case + Inlined( + // Some(Apply( + // TypeApply( + // Select( + // Inlined(_,_, + // Typed(AsInstanceOf(TupleCons(a,b)), _) + // ), + // "*:" + // ), + // _ + // ), + // _ + // )), + _, + List(ValDef("Tuple_this", _, Some(prevTuple))), + Typed(AsInstanceOf(TupleCons(head, TIdent("Tuple_this"))), _) + ) => + Some((head, prevTuple)) + case _ => None + } + } +} + +/** + * Parses a few cases of arbitrary tuples. + * + * Scala 3 produces a few different trees for arbitrary tuples. Method `*:` is marked as inline. + * Under the hood it actually invokes `Tuples.cons` function: + * {{{ + * inline def *: [H, This >: this.type <: Tuple] (x: H): H *: This = + * runtime.Tuples.cons(x, this).asInstanceOf[H *: This] + * }}} + * So, at least we have to match Tuples.cons. + * However, it's not the only variation. Scala also produces a block with intermediate val `Tuple_this` definitions: + * {{{ + * { + * val Tuple_this: scala.Tuple$package.EmptyTuple.type = scala.Tuple$package.EmptyTuple + * val `Tuple_thisâ‚‚`: scala.*:[java.lang.String, scala.Tuple$package.EmptyTuple] = (scala.runtime.Tuples.cons("", Tuple_this).asInstanceOf[scala.*:[java.lang.String, scala.Tuple$package.EmptyTuple.type]]: scala.*:[java.lang.String, scala.Tuple$package.EmptyTuple]) + * + * (scala.runtime.Tuples.cons(1, `Tuple_thisâ‚‚`).asInstanceOf[scala.*:[scala.Int, scala.*:[java.lang.String, scala.Tuple$package.EmptyTuple]]]: scala.*:[scala.Int, scala.*:[java.lang.String, scala.Tuple$package.EmptyTuple]]) + * } + * }}} + */ +class ArbitraryTupleBlockParser(val rootParse: Parser)(using Quotes, TranspileConfig) + extends Parser(rootParse) + with PatternMatchingValues { + + import quotes.reflect.{Block => TBlock, Ident => TIdent, _} + + def attempt = { + case '{EmptyTuple} => + ast.Tuple(List()) + case '{$a *: EmptyTuple} => + val aAst = rootParse(a) + ast.Tuple(List(aAst)) + case inlined@Unseal(ArbitraryTupleConstructionInlined(singleValue, prevTuple)) => + //printExpr(inlined, "singleValue ArbitraryTupleConstructionInlined") + val headAst = rootParse(singleValue.asExpr) + val prevTupleAst = rootParse(prevTuple.asExpr) + prevTupleAst match { + case ast.Tuple(lst) => ast.Tuple(headAst :: lst) + case _ => + throw IllegalArgumentException(s"Unexpected tuple ast ${prevTupleAst}") + } + case block@Unseal(TBlock(parts, ArbitraryTupleConstructionInlined(head,Typed(AsInstanceOf(TupleCons(head2,TIdent("Tuple_this"))), _)) )) if (parts.length > 0) => + //println(s"Large block. parts.size=${parts.size}; parts:\n- ${parts.mkString("\n- ")}") + val headAst = rootParse(head.asExpr) + val head2Ast = rootParse(head2.asExpr) + val partsAsts = headAst :: head2Ast :: parts.reverse.flatMap{ + case ValDef("Tuple_this", tpe, Some(TIdent("EmptyTuple"))) => List() + case ValDef("Tuple_this", tpe, Some(Typed(AsInstanceOf(TupleCons(next,TIdent("Tuple_this"))), _))) => List(rootParse(next.asExpr)) + case ValDef("Tuple_this", tpe, Some(unknown)) => + throw IllegalArgumentException(s"Unexpected Tuple_this = ${unknown.show}") + } + Tuple(partsAsts) + } +} class BlockParser(val rootParse: Parser)(using Quotes, TranspileConfig) extends Parser(rootParse) diff --git a/quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala b/quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala index 64825321..bc0ac871 100644 --- a/quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala +++ b/quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala @@ -1,5 +1,6 @@ package io.getquill +import io.getquill.context.ExecutionType.Static import io.getquill.context.mirror.{MirrorSession, Row} import io.getquill.generic.TupleMember @@ -11,37 +12,47 @@ class ArbitraryTupleSpec extends Spec { type MyRow1 = (Int, String) type MyRow2 = Int *: String *: EmptyTuple - "ordinary tuple" in { - inline def q = quote{ - querySchema[MyRow1]("my_table", t => t._1 -> "int_field", t => t._2 -> "string_field") - } + inline def myRow1Query = quote { + querySchema[MyRow1]("my_table", t => t._1 -> "int_field", t => t._2 -> "string_field") + } - val result = ctx.run(q) + inline def myRow2Query = quote { + querySchema[MyRow2]("my_table", t => t._1 -> "int_field", t => t._2 -> "string_field") + } + + "ordinary tuple" in { + val result = ctx.run(myRow1Query) + result.string mustEqual "SELECT x.int_field, x.string_field FROM my_table x" result.extractor(Row(123, "St"), MirrorSession.default) mustEqual (123, "St") } - "arbitrary long tuple" in { - inline def q = quote{ - querySchema[MyRow2]("my_table", t => t._1 -> "int_field", t => t._2 -> "string_field") + "ordinary tuple swap" in { + + transparent inline def swapped: Quoted[EntityQuery[(String, Int)]] = quote { + myRow1Query.map { + case (i, s) => (s, i) + } } - val result = ctx.run(q) + val result = ctx.run(swapped) + + result.string mustEqual "SELECT x$1.string_field AS _1, x$1.int_field AS _2 FROM my_table x$1" + require(result.extractor(Row("St", 123), MirrorSession.default) == ("St", 123)) + } + + "arbitrary long tuple" in { + val result = ctx.run(myRow2Query) result.extractor(Row(123, "St"), MirrorSession.default) mustEqual (123, "St") } "get field of arbitrary long tuple" in { - inline def q = quote{ - querySchema[MyRow2]("my_table", t => t._1 -> "int_field", t => t._2 -> "string_field") - } - inline def g = quote{ - q.map{t => t match { - case h *: tail => h - } + myRow2Query.map{ + case h *: tail => h } } val result = ctx.run(g) @@ -50,4 +61,62 @@ class ArbitraryTupleSpec extends Spec { (123) } + "decode empty tuple" in { + inline def g = quote { + myRow2Query.map{ + case (_, _) => EmptyTuple + } + } + + val result = ctx.run(g) + + result.extractor(Row(123, "St"), MirrorSession.default) mustEqual EmptyTuple + } + + "construct tuple1" in { + inline def g = quote { + myRow1Query.map { + case (i, s) => i *: EmptyTuple + } + } + + val result = ctx.run(g) + + result.string mustEqual "SELECT x$1.int_field AS _1 FROM my_table x$1" + result.extractor(Row(123, "St"), MirrorSession.default) mustEqual + Tuple1(123) + } + + "construct arbitrary tuple" in { + inline def g = quote { + myRow1Query.map { + case (i, s) => s *: i *: EmptyTuple + } + } + val result = ctx.run(g) + + result.string mustEqual "SELECT x$1.string_field AS _1, x$1.int_field AS _2 FROM my_table x$1" + result.extractor(Row("St", 123), MirrorSession.default) mustEqual ("St", 123) + + } + + "constant arbitrary tuple" in { + inline def g = quote { + 123 *: "St" *: true *: (3.14 *: EmptyTuple) + } + val result = ctx.run(g) + result.string mustEqual "SELECT 123 AS _1, 'St' AS _2, true AS _3, 3.14 AS _4" + result.info.executionType mustEqual Static + result.extractor(Row(123, "St", true, 3.14), MirrorSession.default) mustEqual (123, "St", true, 3.14) + } + + "constant arbitrary tuple 1" in { + inline def g = quote { + (3.14 *: EmptyTuple) + } + val result = ctx.run(g) + result.string mustEqual "SELECT 3.14 AS _1" + result.info.executionType mustEqual Static + result.extractor(Row(3.14), MirrorSession.default) mustEqual Tuple1(3.14) + } } From 53daedbaf48fd30bb77215cd8a3c55a9eda1d5f1 Mon Sep 17 00:00:00 2001 From: Arseniy Zhizhelev Date: Wed, 8 May 2024 09:49:26 +0300 Subject: [PATCH 8/8] Remove commented out code --- .../io/getquill/generic/GenericDecoder.scala | 41 +------------------ .../scala/io/getquill/parser/Parser.scala | 20 ++------- 2 files changed, 4 insertions(+), 57 deletions(-) diff --git a/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala b/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala index 3fa74d68..b73e0f5d 100644 --- a/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala +++ b/quill-sql/src/main/scala/io/getquill/generic/GenericDecoder.scala @@ -385,46 +385,7 @@ object ConstructDecoded { '{EmptyTuple} } else if (tpe <:< TypeRepr.of[Tuple]) { '{scala.runtime.Tuples.fromIArray(IArray(${Varargs(terms)})).asInstanceOf[T]} - // Alternative version: - // val t = - // Varargs - // Type.of[T] match { - // case '[h *: t] => - // val construct = Type.of[t] match { - // case '[EmptyTuple] => - // println(" ConstructDecoded: t=et") - // Apply( - // TypeApply( - // Select(New(TypeTree.of[Tuple1[h]]), constructor), - // types.map { tpe => - // tpe match { - // case '[tt] => TypeTree.of[tt] - // } - // } - // ), - // terms.map(_.asTerm) - // ) - // case _ => - // val typeTrees = types.map { tpe => - // tpe match { - // case '[tt] => TypeTree.of[tt] - // } - // } - // val terms2 = terms.map(_.asTerm) - // val ttt = TypeTree.of[T] - - // println(s" ConstructDecoded: t=t; ttt = $ttt; typeTrees = $typeTrees; terms2 = $terms2") - // Apply( - // Select(New(TypeTree.of[IArray[Object]]), constructor), - // // VarArgs - // terms2 - // ) - // } - // println(s"=========== Create from Tuple Constructor: Tree: $construct ===========") - // println(s"=========== Create from Tuple Constructor ${Format.Expr(construct.asExprOf[T])} ===========") - // construct.asExprOf[T] - // // If we are a case class with no generic parameters, we can easily construct it - // } + // If we are a case class with no generic parameters, we can easily construct it } else if (tpe.classSymbol.exists(_.flags.is(Flags.Case)) && !constructor.paramSymss.exists(_.exists(_.isTypeParam))) { val construct = Apply( diff --git a/quill-sql/src/main/scala/io/getquill/parser/Parser.scala b/quill-sql/src/main/scala/io/getquill/parser/Parser.scala index be033eb5..588c2fd1 100644 --- a/quill-sql/src/main/scala/io/getquill/parser/Parser.scala +++ b/quill-sql/src/main/scala/io/getquill/parser/Parser.scala @@ -199,21 +199,9 @@ object ArbitraryTupleConstructionInlined { t match { case Inlined( - // Some(Apply( - // TypeApply( - // Select( - // Inlined(_,_, - // Typed(AsInstanceOf(TupleCons(a,b)), _) - // ), - // "*:" - // ), - // _ - // ), - // _ - // )), - _, - List(ValDef("Tuple_this", _, Some(prevTuple))), - Typed(AsInstanceOf(TupleCons(head, TIdent("Tuple_this"))), _) + _, + List(ValDef("Tuple_this", _, Some(prevTuple))), + Typed(AsInstanceOf(TupleCons(head, TIdent("Tuple_this"))), _) ) => Some((head, prevTuple)) case _ => None @@ -254,7 +242,6 @@ class ArbitraryTupleBlockParser(val rootParse: Parser)(using Quotes, TranspileCo val aAst = rootParse(a) ast.Tuple(List(aAst)) case inlined@Unseal(ArbitraryTupleConstructionInlined(singleValue, prevTuple)) => - //printExpr(inlined, "singleValue ArbitraryTupleConstructionInlined") val headAst = rootParse(singleValue.asExpr) val prevTupleAst = rootParse(prevTuple.asExpr) prevTupleAst match { @@ -263,7 +250,6 @@ class ArbitraryTupleBlockParser(val rootParse: Parser)(using Quotes, TranspileCo throw IllegalArgumentException(s"Unexpected tuple ast ${prevTupleAst}") } case block@Unseal(TBlock(parts, ArbitraryTupleConstructionInlined(head,Typed(AsInstanceOf(TupleCons(head2,TIdent("Tuple_this"))), _)) )) if (parts.length > 0) => - //println(s"Large block. parts.size=${parts.size}; parts:\n- ${parts.mkString("\n- ")}") val headAst = rootParse(head.asExpr) val head2Ast = rootParse(head2.asExpr) val partsAsts = headAst :: head2Ast :: parts.reverse.flatMap{