jonifreeman/sqltyped

Column naming strategy

rrmckinley opened this issue · 12 comments

I have been playing with introducing a naming strategy for field names. If a query falls back to jdbc on Oracle, it produces something like "USER_ID" for the field name. A String -> String naming strategy would let the programmer turn that into "userId" by splitting on [^a-zA-Z], toLowerCase, title casing the tail and rejoining with empty string. Anything they want. An organization may want to whitelist "id" to be uppercase so the field in their case is "userID".

What do you think about this?

That's a good idea. Currently you can do "select user_id as userID ..." but of course that gets verbose and repetitious in all but trivial applications.

One implementation detail which I'm not sure how to solve though. Ideally this naming function would be passed to macro (*). The macro gets the function unevaluated in AST format, and this is the tricky part. It needs to evaluate that part of AST at compile time to be able to call the function. All previous similar attempts I've done have fallen flat on reasons I no longer remember.

(*) It would be possible to implement this after macro has generated code by mapping a polymorphic function over record but it would have a noticeable effect on compilation times I'm afraid.

Currently you can do "select user_id as userID ..."

One additional problem with sql aliases is that with JDBC fallback userID gets changed to USERID (at least on Oracle). This keeps the need for a naming strategy high.

Could some tricky bit of dependency injection be used in the sqltyped macro? Could the naming strategy be stubbed out to be replaced in a phase that happens before the sqltyped macro, but still declarable by the developer in the same project?

Maybe the runtime subproject of this project will be useful: https://github.com/adamw/macwire

Hmm.. evaluating the AST seems to work now (tried with Scala 2.10.3). Reading NamingStrategy from implicit context would be one way to support this feature.

import scala.language.experimental.macros

case class NamingStrategy(f: String => String)

def testMacro(c: Context): c.Expr[Any] = { 
  import c.universe._

  val namingStrategy: String => String = c.inferImplicitValue(typeOf[NamingStrategy], silent = true) match {
    case EmptyTree => identity _
    case tree => c.eval(c.Expr[NamingStrategy](c.resetAllAttrs(tree.duplicate))).f
  }

  val name = namingStrategy("helloWorld")

  c.Expr(Literal(Constant(name))) 
}

def test = macro testMacro

scala> test
res0: String = helloWorld

scala> implicit val namingStrategy = NamingStrategy(_.toUpperCase)

scala> test
res1: String = HELLOWORLD

This branch contains the initial implementation:

https://github.com/jonifreeman/sqltyped/tree/naming_strategy

Does it do what you expect? Configuration can be added for instance to your application's package object:

 implicit val namingStrategy = NamingStrategy(s => /* do the conversion */)

Are you having problems with complex functions? A simple s => s.toLowerCase works, but the following gives an error. object namingStrategy is not a member of package vor.package

implicit val namingStrategy = NamingStrategy { s =>
  val r = s.split("_").map(_.toLowerCase()).mkString
  r
}

Did my example work for you?

Sorry I haven't had a chance to try it out yet, been on a vacation. I will take a look at it during coming weekend. I saw similar errors when the function captured a complex outer context. But it looks like this issue is something different. Could be that compile time evaluation is too unreliable after all :(

I can reproduce that error and didn't find any workaround. It really looks like that compile time evaluation is too unreliable. Then I have no better solution in mind that requiring the user to pass naming strategy as a system property (eg. sqltyped.naming_strategy=com.example.MyNamingStrategy). The downside with that of course is that MyNamingStrategy has to be implemented in a module which is compiled before the code which uses it, and has to be added to compiler's classpath. Would you like to go down that path or do you have any better ideas in mind?

The precompiled approach seems like a fine solution for now. Is it possible for the code specified in the system property to be String => String rather than NamingStrategy(String => String)? That way, the precompiled code will not have a dependency on sqltyped.

Pushed a new version which uses precompiled naming strategy. Eg.

object MyNamingStrategy extends (String => String) {
  def apply(s: String) = ....
}

Then compile it and add to compiler's classpath. Reference to the naming function can be passed as a system property, note the trailing $ in this example. That's how scalac names objects.

System.setProperty("sqltyped.naming_strategy", "MyNamingStrategy$")

Did this solution work for you?

Merged to master now and closing the issue. Please reopen if needed.