Skip to content

Kotlin prototype #300

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

Merged
merged 22 commits into from
Feb 12, 2020
Merged

Kotlin prototype #300

merged 22 commits into from
Feb 12, 2020

Conversation

mightyguava
Copy link
Contributor

@mightyguava mightyguava commented Jan 26, 2020

Updated TODO:

I wanted to learn more about how sqlc works, so I took a stab at #287 anyways to see how hard it would be.

Here's a prototype implementation for Kotlin. It's pretty rough around the edges but tests pass, and I was able to generate and compile all of the existing examples in Kotlin. authors has tests written in Kotlin to confirm that the generated code works. I haven't gotten to the rest. The sqlc codebase is pretty easy to work in! I'm impressed.

I basically just copied gen.go into ktgen.go and renamed all the functions. It should really be its own package, but that can come if this PR actually has a chance of getting merged 😄While the code follows the same patterns as the Go generator, there were enough changes that I'm not sure much code reuse is possible.

I split out the generated examples and a working gradle project because it would pollute this PR too much. They can be viewed here: mightyguava#1

Things that work:

  • Primitives
  • NULL primitives
  • Generating schema, param, and return classes
  • one/many/exec queries

Things that compile but I haven't tested:

  • Enums
  • Dates
  • NULLS for these types
  • Arrays
  • execRow queries

Things that don't work:

  • Type overrides - there's no fancy Scan() interface in JDBC to support arbitrary types, so this'll require generating code to call Adapter classes for individual types. Not hard, probably, just didn't seem worthwhile exploring in a prototype
  • JSON tags - no such thing in Java/Kotlin
  • MySQL - that part of the codebase seems to be iterating rapidly, so I decided to just look at Postgres for now.

Things that work differently:

  • Numbered parameters - JDBC seems to only support ? as placeholders. I had to write a HACK to convert things like $1 to ?. This is buggy and terrible. It also means that you can't use $1 multiple times in the same query to reference the same value. If the AST was available in the generator part then I can probably figure out how to do something better.
  • I only generate a single QueriesImpl.kt for the queries instead one for each sql file. This is because you can't have a class span multiple files in Kotlin.
  • Prepared statements - According to both JDBC and Postgres JDBC Driver docs, prepared statements are implicitly cached, so the EmitPreparedQueries option is likely unneeded here.

Things to think about:

  • The code is in Kotlin, meaning Java projects can use it, but they need to have the Kotlin compiler. An alternative is to have sqlc just generate a self-contained module that will produce a jar for the project to depend on. I could have generated Java instead, but that would have resulted in at least 5X more generated LOC of mostly boilerplate (though I guess technically everything sqlc generates is boilerplate). I also just don't like writing Java.
  • Remove the interface option, make it required: Unlike Go, interfaces are basically mandatory in the JVM if you want to do proper unit testing. You can't just create interfaces for arbitrary classes.
  • I haven't actually spent that much time working with JDBC before, so some of the generated code could be questionable. More tests will help tell!

Please take a look! While I'd love for this code to get merged, I understand if you don't want to take on the additional maintenance burden that another language would add. Though, I wouldn't mind helping to maintain it.

@mightyguava
Copy link
Contributor Author

Ooh, I have some ideas around DRYing the 2 languages by taking advantage of embedded structs. Better left for a follow up though, and they do say the best time to dedupe is when you have the 3rd copy.


// HACK: jdbc doesn't support numbered parameters, so we need to transform them to question marks...
// But there's no access to the SQL parser here, so we just do a dumb regexp replace instead. This won't work if
// the literal strings contain matching values, but good enough for a prototype.
Copy link

@alecthomas alecthomas Jan 26, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW you don't need a full parser, just a lexer that correctly tokenises strings and comments. Though that in itself might be a challenge across multiple dialects :. OTOH it would be just cleaner to do it with an AST.

@kyleconroy
Copy link
Collaborator

Wow! The fact that you were able to hack this together is really impressive. And here I thought TypeScript would be the second language we support.

I understand if you don't want to take on the additional maintenance burden that another
language would add.

There are a few things that will need to change before we can merge in Kotlin support. The first is a new version of the configuration file that supports multiple language backends. You already noticed that most of the package options don't make sense for Kotlin. I'm tracking that in #302

The interface between parsing, type-checking, and code-generation is still in flux. I don't think it will be frozen any time soon. Have a package for each language is probably a good approach going forward.

that part of the codebase seems to be iterating rapidly, so I decided to just look at Postgres for now.

That's fine, as the first version that ships will be marked experimental. Before we expose it to everyone, we'll need to add MySQL support.

I haven't actually spent that much time working with JDBC before, so some of the generated code could be questionable. More tests will help tell!

I haven't touched Java in years and have never written Kotlin. It would be fantastic if we could get a small group of opinionated Kotlin developers to look at the generated code and make suggestions. Kotlin also has a more featureful type system than Go, so it's possible that the generated code can be more clever.

I had to write a HACK to convert things like $1 to ?

We can do this safely and correctly when parsing queries. We have the full query AST and a means to edit both the AST and the input SQL.

So, where does that leave us? I think it makes sense for this to live in your sqlc fork for the time being. Once the configuration file version has been updated, we can then reassess. In the meantime, I'd focus on porting the tests in examples/authors/db_test.go to Kotlin and getting them running on CI.

Thanks again! I'm really excited about this landing in the future.

@mightyguava
Copy link
Contributor Author

Before we expose it to everyone, we'll need to add MySQL support.

Yup, MySQL is my number 1 want aside from this. I want to contribute there too, but it seems like there are already plenty of folks on it! I know @systay and a few of the Vitess maintainers, and am currently working closely with TiDB as well, so let me know if I could be of help connecting or connecting you guys.

It would be fantastic if we could get a small group of opinionated Kotlin developers to look at the generated code and make suggestions.

Sure, I can get some folks on my end. Do you know anyone who might be interested as well?

I'd focus on porting the tests in examples/authors/db_test.go to Kotlin and getting them running on CI.

Already ported last night! Will get it running on CI and add some more tests.

@mightyguava
Copy link
Contributor Author

mightyguava#2 adds CI

@mightyguava
Copy link
Contributor Author

Ported over all the tests! Everything passes except a few UPDATE queries, because I lost parameter ordering with the $1 => ? hack. @kyleconroy could you give me some pointers on where might be a good place to rewrite the AST and the input SQL?

@cmoog
Copy link
Contributor

cmoog commented Jan 27, 2020

Nice job putting this together 👍. As far as MySQL support, when the time comes I don't think it'll be too much work to satisfy the interface you've put together. It looks like the current pattern we have of using an "xGeneratable" interface is working decently well.

To that point, I'm thinking it'd be better to put the Kotlin generation templates in their own package, then have the (r Result) methods that satisfy the interface for pg stay with the pg parser. Ideally, we'd want to separate the pg parsing code from the Go generation templates as well, but for the time being those are stuck in dinosql. Any thoughts?

@mightyguava
Copy link
Contributor Author

Thanks! Kotlin generation is already in its own package mightyguava#3.

I’m not planning to add more commits to this PR. Instead, I’m going to use the TODO at the top to link to PRs on my fork. The “kotlin-master” branch on my fork will ultimately contain all the commits to eventually merge back.

Yeah the Generatable interfaces do seem to work so far. I think there’s a bit more common functionality we can extract out, that can happen later when we move the Go generator out.

@mightyguava
Copy link
Contributor Author

mightyguava commented Jan 28, 2020

@kyleconroy could you give me some pointers on where might be a good place to rewrite the AST and the input SQL?

Actually I figured out how to do the query rewriting. #306. Would like to merge this if possible to avoid future merge conflicts, it's very nuanced

@kyleconroy
Copy link
Collaborator

New config format #302 (is this the main blocker for merging?)

The main blocker for merging is #302, "Get some Kotlin folks to review generated code", and a decision around experimental features. My feeling is that we should just document that the generated Kotlin code is experimental in the README and warn users that it may change.

@mightyguava
Copy link
Contributor Author

mightyguava commented Jan 30, 2020

I’ve asked @alecstrong (maintainer of SQLDelight), @ryanhall07 and @tirsen (2 engineers experienced with JDBC and Kotlin) to take a look at mightyguava#6.

W.r.t identifying this as experimental, putting it in the readme sounds great. If we want to go a step further, we could make the config option be “language”: “experimental-kotlin” and be extra explicit.

Let me know when you reach a decision on #302.

@AlecKazakova
Copy link

AlecKazakova commented Jan 30, 2020

going off the source generated here can the params be inlined?

data class UpdateBookISBNParams (
  val title: String,
  val tags: List<String>,
  val isbn: String,
  val bookId: Int
)

otherwise lgtm

@ryanhall07
Copy link

LGTM for the kotlin generated code in mightyguava#6

@mightyguava
Copy link
Contributor Author

going off the source generated here can the params be inlined?

done: mightyguava#8

@tirsen
Copy link

tirsen commented Feb 2, 2020

LGTM!

@kyleconroy
Copy link
Collaborator

@mightyguava The configuration changes are ready to be tested. Can you rebase this on the config-v2 branch and hook it up end to end? You'll need to add Kotlin fields to the SQLGen and Gen structs in the new config package.

Once you've confirmed that this works, I'll merge config-v2 into master and then we can merge Kotlin support into mainline.

Also, I'm not sure if you're following along to #150, but you can ignore MySQL support for Kotlin. We're going to switch parsers, which means the interface for MySQL will probably change.

@mightyguava
Copy link
Contributor Author

mightyguava commented Feb 6, 2020

You'll need to add Kotlin fields to the SQLGen and Gen structs in the new config package.

Sweet! Will do that tomorrow or Friday.

Also, I'm not sure if you're following along to #150, but you can ignore MySQL support for Kotlin. We're going to switch parsers, which means the interface for MySQL will probably change.

Yup, I proposed the parser switch actually and have been active in that thread. I look forward to full MySQL support!

Separately, I sat down with @swankjesse today to do a deep code review and we found some good improvements to make to the generated Kotlin code. So this introduces a couple questions:

  • This will unfortunately involve creating a tiny JVM library with some interfaces and abstract classes that users of sqlc Kotlin will need to depend on. Would you be ok with creating another repo to host this as a Maven package? I can maintain it, but I think it makes most sense to stay within the same GitHub namespace (https://github.com/kyleconroy/sqlc-kotlin-runtime or something). Having a runtime library was inevitable since JDBC isn't a great db interface.
  • Since this is going to be marked as experimental, I'd like to continue on with merging this PR, after rebasing onto the config. And then adding on these breaking interface changes in a followup to avoid forking for too long. Is that ok with you?

@kyleconroy
Copy link
Collaborator

Is that ok with you?

Yep, that plans sounds great. Let me know when you've decided on the repository name and I'll go ahead and create it.

@mightyguava mightyguava changed the base branch from master to config-v2 February 7, 2020 15:38
@mightyguava
Copy link
Contributor Author

mightyguava commented Feb 7, 2020

@kyleconroy I've updated this branch with the latest changes and retargeted the PR against config-v2. Rebasing at this point wasn't really feasible (I tried) so I did a merge. Please take a look at the changes to generate.go and config.go.

Let me know when you've decided on the repository name

sqlc-kotlin-runtime is good.

Copy link
Collaborator

@kyleconroy kyleconroy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only one comment about code generation. Other than that, this looks good. I'll go ahead and merge the config-v2 into master so that you don't need to track a separate branch.

files, err := dinosql.Generate(result, combo)
var files map[string]string
var out string
if sql.Gen.Go != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The configuration format supports multiple language packages per package. This if statement means that only Go or Kotlin will be output. Code for both languages should be able to be output in a single pass.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is correct. See lines 67-80 where I permute (engine, language) into the outPair struct. The parser needs to be run once per language because kotlin needs to run the parser with different options. I split out the parser changes to #306 earlier if you want to look closer there (though it's squashed into this PR and is outdated)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I missed the fact that you were creating multiple output pairs. Looks good!

}
return nil, true
}
return &kotlin.Result{Result: q}, false
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always returning a Kotlin result here seems strange. Is this just so that you can't generate MySQL code for Kotlin?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that, and me trying to minimize my changes to the generation code in fear of nasty merge conflicts. Happy to change if you can suggest a more elegant way of doing it. (check the lang here?)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is okay for now, but something we'll probably want to change once we land a third language backend.

@kyleconroy
Copy link
Collaborator

The runtime repo has been created https://github.com/kyleconroy/sqlc-kotlin-runtime. Config-v2 has also been merged into master, so this should be good to go.

@mightyguava
Copy link
Contributor Author

Sweet, let's merge it. I'll work on the changes to use the runtime next week

@kyleconroy kyleconroy changed the base branch from config-v2 to master February 12, 2020 02:20
@kyleconroy
Copy link
Collaborator

I've changed the base back to master. You'll need to fix some merge conflicts. I'm also planning on releasing v1.0.0 in the next few days. I'd like this to be merged in for that, but I'm planning on disabling Kotlin support in that release and then turning it right back on. The reason is that I'd like the v1.0.0 release to only support version 1 of the configuration format.

@mightyguava
Copy link
Contributor Author

Done! Could you merge this PR as soon as you can? I spent almost an hour doing this merge, plus another hour doing the previous merge. It sucked.

Multiple conflicting changes were made recently that could have been avoided. The major ones were:

  1. The squash merge of Second version of the configuration format #318 and then config: Add support for YAML #336 on top made just about all the config-related code conflict
  2. parser: optionally rewrite numbered params to positional params #306 which I factored out and ask you to review and merge separately specifically avoid parser merge conflicts, ended up conflicting quite a bit with Named parameters for PostgreSQL #328 and the merge was delicate (I'm not even sure correct, but the tests pass). parser: optionally rewrite numbered params to positional params #306 should have been merged first with Named parameters for PostgreSQL #328 built on top.

I understand how hard it is to be a maintainer, and I've done my best to respond ASAP to your comments and make changes as you requested to make it a bit easier. Please be mindful that it's also hard on the contributor to make large, meaningful changes. Resolving merge conflicts is not a good use of anyone's time, especially when it could have been easily avoided with better communication and timing.

@kyleconroy
Copy link
Collaborator

kyleconroy commented Feb 12, 2020

Could you merge this PR as soon as you can?

Done!

Please be mindful that it's also hard on the contributor to make large, meaningful changes.

I completely understand this and want to thank you for getting this change over the finish line.

Resolving merge conflicts is not a good use of anyone's time, especially when it could have been easily avoided with better communication and timing.

I apologize for making your life more difficult. We've all dealt with merge conflicts and nobody enjoys fixing them.

Thanks again for all the amazing work

@kyleconroy kyleconroy merged commit 6b88846 into sqlc-dev:master Feb 12, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants