Skip to content

Commit 510725c

Browse files
arnaud-daroussingaelrenoux-datadomenovakov-alexey
authored
#206 Reproduce derivation of generic case class issue & fix for Scala 2 (#207)
* Reproduce derivation of generic case class issue & fix it for Scala 2 * fix generic case class issue for Scala 3 by disabling cache for abstract type params Co-authored-by: Gaël Renoux <[email protected]> * Simplifies condition Co-authored-by: Gaël Renoux <[email protected]> * Update modules/scala-api/src/test/scala-3/org/apache/flinkx/api/GenericCaseClassScala3Test.scala Co-authored-by: Arnaud <[email protected]> --------- Co-authored-by: Gaël Renoux <[email protected]> Co-authored-by: novakov-alexey <[email protected]>
1 parent ac599c5 commit 510725c

File tree

6 files changed

+153
-20
lines changed

6 files changed

+153
-20
lines changed

modules/scala-api/src/main/scala-2/org/apache/flinkx/api/LowPrioImplicits.scala

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package org.apache.flinkx.api
22

3-
import org.apache.flinkx.api.serializer.{CoproductSerializer, ScalaCaseClassSerializer, ScalaCaseObjectSerializer}
4-
import org.apache.flinkx.api.typeinfo.{CoproductTypeInformation, ProductTypeInformation}
53
import magnolia1.{CaseClass, Magnolia, SealedTrait}
64
import org.apache.flink.api.common.ExecutionConfig
75
import org.apache.flink.api.common.typeinfo.TypeInformation
6+
import org.apache.flinkx.api.serializer.{CoproductSerializer, ScalaCaseClassSerializer, ScalaCaseObjectSerializer}
7+
import org.apache.flinkx.api.typeinfo.{CoproductTypeInformation, ProductTypeInformation}
88

99
import scala.collection.mutable
1010
import scala.language.experimental.macros
@@ -21,7 +21,7 @@ private[api] trait LowPrioImplicits {
2121
def join[T <: Product: ClassTag: TypeTag](
2222
ctx: CaseClass[TypeInformation, T]
2323
): TypeInformation[T] = {
24-
val cacheKey = typeName(ctx.typeName)
24+
val cacheKey = typeName[T]
2525
cache.get(cacheKey) match {
2626
case Some(cached) => cached.asInstanceOf[TypeInformation[T]]
2727
case None =>
@@ -45,8 +45,8 @@ private[api] trait LowPrioImplicits {
4545
}
4646
}
4747

48-
def split[T: ClassTag](ctx: SealedTrait[TypeInformation, T]): TypeInformation[T] = {
49-
val cacheKey = typeName(ctx.typeName)
48+
def split[T: ClassTag : TypeTag](ctx: SealedTrait[TypeInformation, T]): TypeInformation[T] = {
49+
val cacheKey = typeName[T]
5050
cache.get(cacheKey) match {
5151
case Some(cached) => cached.asInstanceOf[TypeInformation[T]]
5252
case None =>
@@ -61,8 +61,7 @@ private[api] trait LowPrioImplicits {
6161
}
6262
}
6363

64-
private def typeName(tn: magnolia1.TypeName): String =
65-
s"${tn.full}[${tn.typeArguments.map(typeName).mkString(",")}]"
64+
private def typeName[T: TypeTag]: String = typeOf[T].toString
6665

6766
implicit def deriveTypeInformation[T]: TypeInformation[T] = macro Magnolia.gen[T]
6867
}

modules/scala-api/src/main/scala-3/org/apache/flinkx/api/LowPrioImplicits.scala

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ private[api] trait LowPrioImplicits extends TaggedDerivation[TypeInformation]:
2626
classTag: ClassTag[T],
2727
typeTag: TypeTag[T]
2828
): Typeclass[T] =
29-
val cacheKey = typeName(ctx.typeInfo)
30-
cache.get(cacheKey) match
29+
val useCache = typeTag.isCachable
30+
val cacheKey = typeTag.toString
31+
(if useCache then cache.get(cacheKey) else None) match
3132
case Some(cached) =>
3233
cached.asInstanceOf[TypeInformation[T]]
3334

@@ -47,15 +48,16 @@ private[api] trait LowPrioImplicits extends TaggedDerivation[TypeInformation]:
4748
fieldNames = ctx.params.map(_.label),
4849
ser = serializer
4950
).asInstanceOf[TypeInformation[T]]
50-
cache.put(cacheKey, ti)
51+
if useCache then cache.put(cacheKey, ti)
5152
ti
5253

5354
override def split[T](ctx: SealedTrait[Typeclass, T])(using
5455
classTag: ClassTag[T],
5556
typeTag: TypeTag[T]
5657
): Typeclass[T] =
57-
val cacheKey = typeName(ctx.typeInfo)
58-
cache.get(cacheKey) match
58+
val useCache = typeTag.isCachable
59+
val cacheKey = typeTag.toString
60+
(if useCache then cache.get(cacheKey) else None) match
5961
case Some(cached) =>
6062
cached.asInstanceOf[TypeInformation[T]]
6163

@@ -66,14 +68,11 @@ private[api] trait LowPrioImplicits extends TaggedDerivation[TypeInformation]:
6668
)
6769
val clazz = classTag.runtimeClass.asInstanceOf[Class[T]]
6870
val ti = new CoproductTypeInformation[T](clazz, serializer)
69-
cache.put(cacheKey, ti)
71+
if useCache then cache.put(cacheKey, ti)
7072
ti
7173

7274
final inline implicit def deriveTypeInformation[T](implicit
7375
m: Mirror.Of[T],
7476
classTag: ClassTag[T],
7577
typeTag: TypeTag[T]
7678
): TypeInformation[T] = derived
77-
78-
private def typeName(ti: TypeInfo): String =
79-
s"${ti.full}[${ti.typeParams.map(typeName).mkString(",")}]"

modules/scala-api/src/main/scala-3/org/apache/flinkx/api/TypeTag.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import scala.quoted.*
66
trait TypeTag[A]:
77
// Is the type a module, i.e. is it a case object?
88
def isModule: Boolean
9+
def isCachable: Boolean
10+
def toString: String
911

1012
object TypeTag:
1113
def apply[A: TypeTag]: TypeTag[A] = summon

modules/scala-api/src/main/scala-3/org/apache/flinkx/api/TypeTagMacro.scala

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,20 @@ object TypeTagMacro:
77
def gen[A: Type](using q: Quotes): Expr[TypeTag[A]] =
88
import q.reflect.*
99

10-
val A = TypeRepr.of[A]
11-
val symA = A.typeSymbol
12-
val flagsA = symA.flags
13-
val isModuleExpr = Expr(flagsA.is(Flags.Module))
10+
val A = TypeRepr.of[A]
11+
val symA = A.typeSymbol
12+
val flagsA = symA.flags
13+
val isModuleExpr = Expr(flagsA.is(Flags.Module))
14+
val isCachableExpr = Expr(A match {
15+
// this type is not cachable if one of its type args is abstract
16+
case a: AppliedType => !a.args.exists { t => t.typeSymbol.isAbstractType }
17+
case _ => true
18+
})
19+
val toStringExpr = Expr(A.show)
1420

1521
'{
1622
new TypeTag[A]:
1723
override lazy val isModule: Boolean = ${ isModuleExpr }
24+
override lazy val isCachable: Boolean = ${ isCachableExpr }
25+
override lazy val toString: String = ${ toStringExpr }
1826
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package org.apache.flinkx.api
2+
3+
import org.apache.flink.api.common.typeinfo.TypeInformation
4+
import org.apache.flinkx.api.serializers._
5+
import org.apache.flinkx.api.typeinfo.ProductTypeInformation
6+
import org.scalatest.flatspec.AnyFlatSpec
7+
import org.scalatest.matchers.should
8+
9+
// This import is not available in Scala 3
10+
import scala.reflect.runtime.universe.TypeTag
11+
12+
class GenericCaseClassScala2Test extends AnyFlatSpec with should.Matchers {
13+
14+
import GenericCaseClassScala2Test._
15+
16+
"Both TypeInformation of Animal Basket" should "have their respective TypeInformation of Animal" in {
17+
typeInformationOfAnimalBasketShouldHaveATypeInformationOfAnimal[Cat](classOf[Cat])
18+
typeInformationOfAnimalBasketShouldHaveATypeInformationOfAnimal[Dog](classOf[Dog])
19+
}
20+
21+
def typeInformationOfAnimalBasketShouldHaveATypeInformationOfAnimal[A <: Animal: TypeTag: TypeInformation](
22+
aClass: Class[A]
23+
): Unit = {
24+
// cacheKey=org.apache.flinkx.api.GenericCaseClassTest.Cat => OK
25+
val catInfo: TypeInformation[Cat] = implicitly[TypeInformation[Cat]]
26+
// cacheKey=org.apache.flinkx.api.GenericCaseClassTest.Dog => OK
27+
val dogInfo: TypeInformation[Dog] = implicitly[TypeInformation[Dog]]
28+
// cacheKey=org.apache.flinkx.api.GenericCaseClassTest.Cat or Dog => OK
29+
val aInfo: TypeInformation[A] = implicitly[TypeInformation[A]]
30+
// cacheKey=org.apache.flinkx.api.GenericCaseClassTest.Basket[org.apache.flinkx.api.GenericCaseClassTest.Cat] => OK
31+
val catBasketInfo: TypeInformation[Basket[Cat]] = implicitly[TypeInformation[Basket[Cat]]]
32+
// cacheKey=org.apache.flinkx.api.GenericCaseClassTest.Basket[org.apache.flinkx.api.GenericCaseClassTest.Dog] => OK
33+
val dogBasketInfo: TypeInformation[Basket[Dog]] = implicitly[TypeInformation[Basket[Dog]]]
34+
// cacheKey=org.apache.flinkx.api.GenericCaseClassTest.Basket[org.apache.flinkx.api.GenericCaseClassTest.Cat] or Dog => OK
35+
val aBasketInfo: TypeInformation[Basket[A]] = implicitly[TypeInformation[Basket[A]]]
36+
37+
if (classOf[Cat].isAssignableFrom(aClass)) {
38+
aInfo should be theSameInstanceAs catInfo
39+
aBasketInfo should be theSameInstanceAs catBasketInfo // Type info of Basket[A] finds the cached type info of Basket[Cat]
40+
}
41+
if (classOf[Dog].isAssignableFrom(aClass)) {
42+
aInfo should be theSameInstanceAs dogInfo
43+
aBasketInfo should be theSameInstanceAs dogBasketInfo // Type info of Basket[A] finds the cached type info of Basket[Dog]
44+
}
45+
catBasketInfo.asInstanceOf[ProductTypeInformation[A]].getFieldTypes()(0) should be theSameInstanceAs catInfo
46+
dogBasketInfo.asInstanceOf[ProductTypeInformation[A]].getFieldTypes()(0) should be theSameInstanceAs dogInfo
47+
// Type info of Basket[A] holds a type info of the good type (Cat or Dog) found in the cache
48+
aBasketInfo.asInstanceOf[ProductTypeInformation[A]].getFieldTypes()(0) should be theSameInstanceAs aInfo
49+
}
50+
51+
}
52+
53+
object GenericCaseClassScala2Test {
54+
55+
sealed trait Animal extends Product {
56+
def name: String
57+
}
58+
59+
case class Cat(name: String) extends Animal
60+
case class Dog(name: String) extends Animal
61+
62+
case class Basket[A <: Animal](animal: A)
63+
64+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package org.apache.flinkx.api
2+
3+
import org.apache.flink.api.common.typeinfo.TypeInformation
4+
import org.apache.flinkx.api.serializers._
5+
import org.apache.flinkx.api.typeinfo.ProductTypeInformation
6+
import org.scalatest.flatspec.AnyFlatSpec
7+
import org.scalatest.matchers.should
8+
9+
class GenericCaseClassScala3Test extends AnyFlatSpec with should.Matchers {
10+
11+
import GenericCaseClassScala3Test._
12+
13+
"Both TypeInformation of Animal Basket" should "have their respective TypeInformation of Animal" in {
14+
typeInformationOfAnimalBasketShouldHaveATypeInformationOfAnimal[Cat](classOf[Cat])
15+
typeInformationOfAnimalBasketShouldHaveATypeInformationOfAnimal[Dog](classOf[Dog])
16+
}
17+
18+
def typeInformationOfAnimalBasketShouldHaveATypeInformationOfAnimal[A <: Animal: TypeTag: TypeInformation](
19+
aClass: Class[A]
20+
): Unit = {
21+
// cacheKey=org.apache.flinkx.api.GenericCaseClassTest.Cat => OK
22+
val catInfo: TypeInformation[Cat] = deriveTypeInformation
23+
// cacheKey=org.apache.flinkx.api.GenericCaseClassTest.Dog => OK
24+
val dogInfo: TypeInformation[Dog] = deriveTypeInformation
25+
// cacheKey=org.apache.flinkx.api.GenericCaseClassTest.Cat or Dog => OK
26+
val aInfo: TypeInformation[A] = implicitly[TypeInformation[A]]
27+
// cacheKey=org.apache.flinkx.api.GenericCaseClassTest.Basket[org.apache.flinkx.api.GenericCaseClassTest.Cat] => OK
28+
val catBasketInfo: TypeInformation[Basket[Cat]] = deriveTypeInformation
29+
// cacheKey=org.apache.flinkx.api.GenericCaseClassTest.Basket[org.apache.flinkx.api.GenericCaseClassTest.Dog] => OK
30+
val dogBasketInfo: TypeInformation[Basket[Dog]] = deriveTypeInformation
31+
// cacheKey=org.apache.flinkx.api.GenericCaseClassTest.Basket[A] => Basket[A] is not cachable
32+
val aBasketInfo: TypeInformation[Basket[A]] = deriveTypeInformation
33+
34+
if (classOf[Cat].isAssignableFrom(aClass)) {
35+
aInfo should be theSameInstanceAs catInfo
36+
aBasketInfo shouldNot be theSameInstanceAs catBasketInfo // Basket[A] is not cachable
37+
}
38+
if (classOf[Dog].isAssignableFrom(aClass)) {
39+
aInfo should be theSameInstanceAs dogInfo
40+
aBasketInfo shouldNot be theSameInstanceAs dogBasketInfo // Basket[A] is not cachable
41+
}
42+
catBasketInfo.asInstanceOf[ProductTypeInformation[A]].getFieldTypes()(0) should be theSameInstanceAs catInfo
43+
dogBasketInfo.asInstanceOf[ProductTypeInformation[A]].getFieldTypes()(0) should be theSameInstanceAs dogInfo
44+
// Type info of Basket[A] is not cached, but it holds a type info of the good type (Cat or Dog) found in the cache
45+
aBasketInfo.asInstanceOf[ProductTypeInformation[A]].getFieldTypes()(0) should be theSameInstanceAs aInfo
46+
}
47+
48+
}
49+
50+
object GenericCaseClassScala3Test {
51+
52+
sealed trait Animal extends Product {
53+
def name: String
54+
}
55+
56+
case class Cat(name: String) extends Animal
57+
case class Dog(name: String) extends Animal
58+
59+
case class Basket[A <: Animal](animal: A)
60+
61+
}

0 commit comments

Comments
 (0)