Sqlc: Compile SQL to type-safe code
sqlc.dev
sqlc.dev
There is so much more to rdbms (especially pg) than just joins - common table expressions, window functions, various views, let alone all the extensibility - extensions, custom types, even enums.
All of that can enable writing performant, type safe and very compact applications.
I am yet to see libs that embrace the elegance of it all - I’ve attempted this once - https://github.com/ivank/potygen but didn’t get much traction. I’m now just waiting for someone more determined to pick up those ideas - a client lib that exposes the type safety and intellisence at compile time and allows you to easily compose those sql queries.
I think this project has some ways to go to reach that though, but thankfully it is a step in the right direction.
Differences:
* jOOQ is an eDSL with an optional schema-to-classes generator
* you write jOOQ queries in Java (or Kotlin as we do)
* there's quite a bit of type-safety added when using the generator: the schema needs to be match the queries you write or you get compile errors
* jOOQ queries are built are run time adding a little overhead that sqlc does not
* writing jOOQ is very close writing SQL (a very thin abstraction), sqlc is "just SQL" it seems
Lmao. At least they provide a "slim" flavor without it.
quite a lot of ideas what you can do with it.
I didn't look forward to working with C# initially but Linq has been a fresh air since your statements more or less map 1:1 to SQL and probably quite overlooked because of "Microsoft"(and that Linq with old EF could have nasty surprises).
What people don't know/realize is that because Linq expressions in C# are left "uncompiled" they can be passed to the SQL layers and converted to idiomatic SQL, so you have all the typesafety of C# and regular C# code but get SQL code that is executed on the server (there is some minor impedance mismatch but it's minor enough and mainly with strings).
Many devs however would consider creating functions and stored procedures a bridge too far to cross.
If we figured out how to do type inference and go-to-definition and all the other nice LSP stuff across language boundaries in a good way, I'd hope the things you mention would all get a lot more widely used.
In terms of actually using SQL more effectively, I think the ideal is just a small utility to use reflection and map a ResultSet (or equivalent) into a strongly typed object using reflection.
Even inserts can get tricky because there are so many different knobs you can tune. It's pretty rare that people just need to insert into a single table with no additional selects beforehand which means there is room to play around with preemptive locking, isolation level, etc.
If you get that stuff out of the way, you can focus on the real problems like design, xact isolation, and performance, for which there's tons of conflicting advice rather than an agreed-upon approach. And then there's sharding. It's hard enough already.
Personally I haven't found the need for type safety in code, or even the code-SQL boundary. The DB tables have types, as does my OpenAPI or Protobuf or whatever API spec. That's basically everything already. If something slips past my tests, it's because the tests are bad, and stronger typing wouldn't have helped.
So they can be utilized to save us some work.
It is a query builder (not an ORM), that (ab)-uses the Typescript type system to give you full type safety, intellisense, autocomplete etc.
Crucially it doesn’t require any build/compile step for your queries which is fantastic.
(How does the IDE do the autocomplete on strings? Will the compiler also catch "bad strings"?)
Otherwise it looks much like jOOQ but without the "jOOQ generator" (which adds most of the type safety).
I expect those `"a" | "b" | "c"` to be expressed as an enum, not strings and |s.
But hey, why not!
The more distinctive features in TypeScript would be what you can to with keyof/typeof, indexed access types, conditional types and especially mapped types.
Out of curiosity, what do you mean by dynamically? Isn't that just a case statement, or am I misunderstanding?
You can do this in Python too..?
there is kysely-codegen which generates types based on the actual database schema.
I added support for a bunch of postgres fancy stuff in a previous app, it wasn’t too difficult
jOOQ and jOOQ-like API's in other languages are the Right Thing in most cases, IMO. I've worked with very abstract ORM's, I've worked with raw SQL, and I've worked with a lot of things somewhere in between and that's my conclusion.
- Create structs for your custom queries - Create structs for your DB tables if you point it at a DB
Not perfect and doesn't maintain the same type if you want a subset of columns but works for the majority of cases.
Edit: Also, curious about how this would work with a progressive schema rollout across environments - e.g., staging vs. prod DB. Do you need to “wait” for your new column to hit prod before you can use it in unit tests?
We allow customers to run old versions if our program against an upgraded database, and to do that we just don't do destructive schema changes.
Unit tests don't run direct against prod usually, but regardless they would be run after migrations to a database of (production schema+migrations). Each environment - dev, test, staging, prod has its own db. Even spinning up an ephemeral db per test is possible, and easy with containers.
YMMV as system complexity increases, but by then there should be whole team(s) managing the issue
I wish go-jet would also start supporting duckdb, since I am exploring more local-first DB apps using sqlite, with duckdb as the query engine.
Example C code (requires ECPG pre-compilation step):
EXEC SQL BEGIN DECLARE SECTION; int v1; VARCHAR v2; EXEC SQL END DECLARE SECTION;
...
EXEC SQL DECLARE foo CURSOR FOR SELECT a, b FROM test;
...
do { ... EXEC SQL FETCH NEXT FROM foo INTO :v1, :v2; ... } while (...);
sqlc lacking support for dynamic query generation is just absolutely baffling. Composition of query fragments is, like, one of the main reasons you use a query builder - it's very hard to do in raw SQL! If you don't need that feature, why are you even messing around with SQL code on the application side? Just write and call stored procedures in the database! People will think you're a time traveler from the 1980's but it works!
Golang's SQL ecosystem is... pretty miserable, really. I mean, I look at what jOOQ can do and I weep at the state of things over in go. In golang everything goes through database/sql because that's the "standard" solution, but database/sql is a huge mess with a long track record of disastrous bugs (like the possibility of accidentally running queries outside of the intended transaction) that makes it very hard to take advantage of feature-rich databases like postgres. pgx (postgres connection library) is actually quite good when you use its native API, but because everything has to go through the database/sql interface it gets pretty crippled with a lot of annoyances (try mapping postgres arrays or jsonb documents into structs and you'll see what I mean).
I ended up adding custom value types to wrap our JSON (actually Protobuf) values: https://gist.github.com/Cyberax/07486a2264e29d95ed8c67e002f9... - we codegenerate them from Protobuf descriptions.
I can't get behind something like sqlc because if it doesn't support your language or SQL dialect or the feature you need you're worse off than not using it.
If you want to use a database-specific connector API, like pgx for postgres for example, you usually have to roll your own query execution (including parameter binding) and quite a lot of the result mapping too. I'd expect that to be a significant amount of work, but what's worse is that having that convenience done for you is a big reason for using a library like Jet in the first place.
So you're either stuck with the limitations of database/sql, or you don't get to enjoy a lot of the benefits that a mature database library brings. I don't like it.
If Jet provided a tool to translate their API to/from SQL I might consider it though.
someJetStatement.DebugSql()
All Jet-generated Statement objects have the .DebugSql() method. It returns the generated query as a string with all the bound parameters inlined[0] so you can just print it or grab it with the debugger and copypaste it into your query console.[0]: that is, it translates the query it'd actually execute, which would look like
WHERE foo = $1
into WHERE foo = 'bound parameter value'
with some rudimentary escaping to avoid the most obvious SQL injection problems (don't use it to actually run queries in production, obviously!!).I mean if I start writing a complex query in a DB client and then want to translate it to Jet.
Example from the sqlc creator (https://github.com/sqlc-dev/sqlc/discussions/364#discussionc...):
-- name: FilterFoo :many
SELECT * FROM foo
WHERE fk = @fk
AND (CASE WHEN @is_bar::bool THEN bar = @bar ELSE TRUE END)
AND (CASE WHEN @lk_bar::bool THEN bar LIKE @bar ELSE TRUE END)
AND (CASE WHEN @is_baz::bool THEN baz = @baz ELSE TRUE END)
AND (CASE WHEN @lk_baz::bool THEN baz LIKE @baz ELSE TRUE END)
ORDER BY
CASE WHEN @bar_asc::bool THEN bar END asc,
CASE WHEN @bar_desc::bool THEN bar END desc,
CASE WHEN @baz_asc::bool THEN baz END asc,
CASE WHEN @baz_desc::bool THEN baz END desc;https://github.com/helpwave/services/tree/main/services/task...
Imho ORMs are not worth it. Too much to learn (often through nasty surprises) for too little benefit.
https://docs.sqlc.dev/en/latest/reference/language-support.h...
Surprisingly not Java, but probably pretty easy to add support for more.
Cornucopia generates rust code.
We don't need to generate code and we get the same type safety from queries written in plain SQL.
If anything, I wish there was a SQLx in other languages I have to support.
Approach-wise, I've always felt that traditional imperative migration tools are an especially bad fit for stored procedures. I wrote a post about this a little while back: https://www.skeema.io/blog/2023/10/24/stored-proc-deployment...
I remember using https://sqlfairy.sourceforge.net/ back in the day to help test locally against mysql/postgres, but deploy to oracle. There were hiccups, but it felt like the world would mostly converge onto smaller differences between the offerings and that kicking off a test database on every build should be easier as time goes on.
Instead, it feels like we have done as much as we can to make all of this harder. I loved AWS Athena when I was able to use it, but trying to figure out a local database for testing that supported the same dialect it used seemed basically impossible. It was baffling.
If I was demanding equivalent speed, it would be one thing. I am fine needing an integration environment for that. I only want a test environment to show queries return as expected. Is also good documentation for reporting focused queries.
I really should just change my main complaint to be that most options should have an easy to setup and tear down local equivalent for testing.
Because "standard SQL", assuming it means ANSI SQL, is quite limited on its own. Every relational database has its own syntax for DDLs. It's not an exaggeration to say each mainstream relational database has its own SQL dialect (e.g. MySQL and SQL Server have ISNULL but ANSI SQL has COALESCE). There is no way to portably write a query that e.g. constructs a table with a foreign key constraint. The best libraries can do is target the dialects they care about or the dialects they expect everybody to use (e.g. MySQL and PostgreSQL).
Actually, what really drives me bonkers is when there are no local options. The aggregate functions such as https://trino.io/docs/current/functions/aggregate.html#appro... are super convenient for reporting purposes. And I can't think of any reason I can't run some of those queries against trivial data locally to show/confirm what a report is supposed to be doing.
This kind of generates the type safe queries for you, which is the end goal. But then why don't developers use the query builder instead? Why have an unnecessary generation step?
I feel like a good query builder ORM is more than enough and more straightforward than this. What am I missing?
Many times I’ve been stuck in a place where I knew exactly how to write the SQL for it but had to spend several minutes studying the ORM docs to learn how to express it.
SQLc also has the advantage of static checking your queries.
It has some other disadvantages so it’s not all rosy.
I just want type-safe SQL templates that return native data structures. Sqlc provides that, and I've quite enjoyed working with it.
Coincidentally, we just released support for this in Prisma a few weeks ago: https://www.prisma.io/blog/announcing-typedsql-make-your-raw...