-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Scala 3 support #11
Scala 3 support #11
Conversation
The Scala 3 compiler reports an error: ```scala -- [E008] Not Found Error: /Users/matt/scalasql/scalasql/core/src/SqlStr.scala:53:33 53 | if (castParams) part + s"CAST(? AS $jdbcTypeString)" else part + "?" | ^^^^^^ |value + is not a member of CharSequence, but could be made available as an extension method. | |One of the following imports might make progress towards fixing the problem: | | import scala.math.Fractional.Implicits.infixFractionalOps | import scala.math.Integral.Implicits.infixIntegralOps | import scala.math.Numeric.Implicits.infixNumericOps | import sourcecode.Text.generate ```
The Scala 3 compiler reports a warning: ```scala -- [E002] Syntax Warning: /Users/matt/scalasql/scalasql/core/src/DbApi.scala:205:8 205 | try { | ^ | A try without catch or finally is equivalent to putting | its body in a block; no exceptions are handled. 206 | val res = stream(query, fetchSize, queryTimeoutSeconds)( 207 | qr.asInstanceOf[Queryable[Q, Seq[_]]], 208 | fileName, 209 | lineNum 210 | ) 211 | if (qr.isSingleRow(query)) { 212 | val results = res.take(2).toVector 213 | assert( 214 | results.size == 1, 215 | s"Single row query must return 1 result, not ${results.size}" 216 | ) 217 | results.head.asInstanceOf[R] 218 | } else { 219 | res.toVector.asInstanceOf[R] 220 | } 221 | } ```
The Scala 3 compiler reported an error: ```scala -- [E173] Reference Error: /Users/matt/scalasql/scalasql/query/src/Query.scala:74:50 74 | def renderSql(q: Q, ctx: Context): SqlStr = q.renderSql(ctx) | ^^^^^^^^^^^ |method renderSql in trait Renderable cannot be accessed as a member of (q : Q) from class QueryQueryable. | Access to protected method renderSql not permitted because enclosing class QueryQueryable in object Query | is not a subclass of trait Renderable in object SqlStr where target is defined | |where: Q is a type in class QueryQueryable with bounds <: scalasql.query.Query[R] ```
Sorry I forgot to update the docs, just pushed that |
@@ -211,7 +211,7 @@ object SqlStr { | |||
new SqlStr(Array(s), emptyInterpArray, false, referencedExprs) | |||
|
|||
trait Renderable { | |||
protected def renderSql(ctx: Context): SqlStr | |||
private[scalasql] def renderSql(ctx: Context): SqlStr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason you changed this from protected
to private
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the Scala 3 compiler reported a reference error in Query.scala
. It makes sense to me, I'm not sure why the Scala 2 compiler didn't catch it
-- [E173] Reference Error: /Users/matt/scalasql/scalasql/query/src/Query.scala:74:50
74 | def renderSql(q: Q, ctx: Context): SqlStr = q.renderSql(ctx)
| ^^^^^^^^^^^
|method renderSql in trait Renderable cannot be accessed as a member of (q : Q) from class QueryQueryable.
| Access to protected method renderSql not permitted because enclosing class QueryQueryable in object Query
| is not a subclass of trait Renderable in object SqlStr where target is defined
|
|where: Q is a type in class QueryQueryable with bounds <: scalasql.query.Query[R]
Left one small comment. One slightly bigger ask: to demonstrate the Scala 3 stuff working, could you take the I pinged the #metaprogramming discord to see if I can find anyone to help review the Scala 3 macro stuff. It looks fine to me, but I'm not an expert. If nobody steps up to help review, I'd be happy to merge as-is given tests are passing |
Hi @mrdziuban, amazing that you found this workaround! I had a brief look at the code and it looks good to me – pretty similar to what I was trying to do in my PR otherwise. One question: do you really need to enforce |
Yeah I can take a look at this. By Scala 3 syntax do you mean using things like fewer braces?
Yeah, it's necessary because -- [E007] Type Mismatch Error: /Users/matt/scalasql/scalasql/query/src-3/TableMacro.scala:85:14
85 | Apply(
| ^
|Found: scala.quoted.Expr[V[scalasql.core.Sc]]
|Required: scala.quoted.Expr[Product]
|
|where: V is a type in method applyImpl with bounds <: [_[_$2]] =>> Any
86 | TypeApply(
87 | Select.unique(New(TypeIdent(TypeRepr.of[V].typeSymbol)), "<init>"),
88 | List(TypeTree.of[Sc])
89 | ),
90 | constructorValueParams.zipWithIndex.map { case (param, i) =>
91 | val paramTpe = paramType(param)
92 | val tpe = subParam(paramTpe, TypeRepr.of[Sc]).asType
93 | val tpe2 = subParam(paramTpe, TypeRepr.of[SqlExpr]).asType
94 | (tpe, tpe2) match {
95 | case ('[t], '[t2]) => '{ queryable[t2, t](${ Expr(i) }).construct(args) }.asTerm
96 | }
97 | }
98 | ).asExprOf[V[Sc]]
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: /Users/matt/scalasql/scalasql/query/src-3/TableMacro.scala:169:57
169 | '{ new Table.Metadata[V]($queryables, $walkLabels0, $queryable, $vExpr0) }
| ^^^^^^^^^
|Found: (queryable :
| scala.quoted.Expr[(() => Seq[String], scalasql.core.DialectTypeMappers,
| scalasql.query.Table.Metadata.QueryableProxy) =>
| scalasql.query.Table.Internal.TableQueryable[V[scalasql.core.Expr²], Product
| & V[scalasql.core.Sc]]
| ]
|)
|Required: scala.quoted.Expr[(() => Seq[String], scalasql.core.DialectTypeMappers,
| scalasql.query.Table.Metadata.QueryableProxy) =>
| scalasql.core.Queryable[V[scalasql.core.Expr²], V[scalasql.core.Sc]]]
|
|where: Expr is a class in package scala.quoted
| Expr² is a trait in package scalasql.core
| V is a type in method applyImpl with bounds <: [_[_$2]] =>> Any
|
| longer explanation available when compiling with `-explain` |
Yeah just an example without the braces should be enough to sanity check that things work on Scala 3 (and that we didn't accidentally mix up the scala versions in Mill or something) |
👍 done, I ported the |
e12ca39
to
405ab33
Compare
Apply( | ||
TypeApply( | ||
Select.unique(New(TypeIdent(TypeRepr.of[V].typeSymbol)), "<init>"), | ||
List(TypeTree.of[SqlExpr]) | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this AST-constructions of the new Foo
call be moved into a named helper method? To help avoid duplication (it appears three times in this PR) and to give it a name so it's obvious what it is without having to read through the AST structure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed this along with the deduplication of the paramTpe
, tpe
, and tpe2
logic
val paramTpe = paramType(param) | ||
val tpe = subParam(paramTpe, TypeRepr.of[Sc]) | ||
val tpe2 = subParam(paramTpe, TypeRepr.of[SqlExpr]) | ||
(tpe.asType, tpe2.asType) match { | ||
case ('[t], '[t2]) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this chain of logic be moved into a helper method? I understand there's some duplication in the Scala 2 equivalent logic, but here it's even more verbose, so I think it's worth DRYing up
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had tried this initially and ran into a compile error about leaking Quotes
or something like that. I must have been doing something wrong though because I just tried again and it worked 🎉
val queryables = '{ (dialect: DialectTypeMappers, n: Int) => | ||
{ | ||
@annotation.nowarn("msg=unused") | ||
given d: DialectTypeMappers = dialect |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we actually need the name d
here, or can we do given DialectTypeMappers = dialect
?
Also, why do we need the nowarn
annotation here, since I assume the DialectTypeMappers
instance should be used by the following generated summonInline
call?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, no need for the name, good call.
The compiler reports it as unused, I guess because in the context of the macro definition it doesn't know that it's used yet, it only knows once the result of the macro is expanded and typechecked...?
That said, I think this could trigger warnings in downstream users code if either of the following are true:
- The class has no fields, or only has fields with
TypeMapper
instances defined outside ofDialectTypeMappers
, so this instance isn't necessary -- I confirmed that this is then reported as unused after macro expansion - The class has fields with
TypeMapper
instances defined withinDialectTypeMappers
, so this instance is necessary -- in this case, if the user has the-Wunused:nowarn
option enabled, then the compiler may report this@annotation.nowarn
as one that doesn't suppress any warnings. I haven't been able to confirm this, but it's theoretically possible
To work around both of these, I updated this to ensure the given
is always used, both in the macro definition and after macro expansion:
given DialectTypeMappers = dialect
lazy val _ = summon[DialectTypeMappers]
if (isTypeParamType(param)) | ||
paramType(param) match { | ||
case AppliedType(tpeCtor, _) => | ||
tpeCtor.asType match { | ||
case '[ | ||
type t[_[_]]; t] => | ||
'{ summonInline[Table.ImplicitMetadata[t]].value.walkLabels0() } | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This whole block appears twice; could it be moved into a named helper method, with the final .walkLabels0()
or .vExpr(tableRef, dialect)
passed as a lambda or something to configure it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah good idea, updated to add a helper method for this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gave it as detailed a review as I could. Looks good to me overall, just some minor tidying and I think it's good to go.
Thanks @mrdziuban ! |
Closes #2
Macros
Just like @KuceraMartin in #3, I ran into scala/scala3#19493 and scala/scala3#19436 when trying to resolve a
TypeMapper
by importing from aDialectTypeMappers
. As a workaround, I introduced additionalimplicit def
s in theTableMapper
companion object that instead rely on an implicit instance ofDialectTypeMappers
, i.e. in a macro:Supporting changes
In addition to building out the macros in Scala 3, the following changes were necessary:
def
s aren't too far to the left -- this is to silence Scala 3 warningsCharSequence
s toString
s explicitly -- see the error the Scala 3 compiler reported heretry
block without a correspondingcatch
-- see the warning the Scala 3 compiler reported hererenderSql
asprivate[scalasql]
instead ofprotected
-- see the error the Scala 3 compiler reported here_
wildcards with?
-- this is to silence Scala 3 warningsFoo with Bar
in types withFoo & Bar
-- this is to silence Scala 3 warnings-Xsource:3
compiler option for Scala 2 -- this is necessary to use the language features mentioned in points 7 and 8-Xsource:3
. All of the warnings relate to the inferred type of the method changing between Scala 2 and 3