Show HN: Parse your Postgres queries into a fully-typed AST in TypeScript (github.com)
1. Each CREATE statement needs to be in topological order, so pg-nano's dev command can execute them without issue.
2. pg-nano has a plugin system like Vite that allows SQL generation based on the parsed schema.
Probably to the surprise of no one, working with an untyped AST feels like you're back in the days of JavaScript, because well... you are. Most of you know by now just how great TypeScript and static types in general are, especially if you appreciate SQL.
So why is this project worth sharing with you?
Well, writing the AST type definitions by hand would have taken me way too much time. It would also be a bear to keep up-to-date as Postgres continues to evolve.
To my surprise, I discovered that libpg_query, the C library used under-the-hood, includes JSON definitions in their /srcdata/ folder. I figured I could use them to generate the type definitions. Genius, right? Okay... maybe not genius, but still cool, I think.
You see, those JSON definitions provided by libpg_query? They don't exactly contain the TypeScript definitions (was that obvious?). No, no. I had to translate them into TypeScript definitions. (I'm sure you could have done it, yes yes. But did you? No siree bob)
It was pain-staking, but overall really not too hard. Time-consuming? Yes, but not as much as writing the type definitions by hand. So... was it worth it? Only time will tell. I hope you find it as useful as I do. And that's all I've got, so thanks for reading.
P.S. The build for Windows is broken, so if anyone could lend a hand, you would be a true hero.
[1]: https://github.com/pg-nano/pg-nano (not ready for production use)
21 comments
[ 4.7 ms ] story [ 61.7 ms ] threadAs an aside, do you think it's possible to use your libraries to get the return type of an arbitrary postgres query, even if it's dynamic? I have a library that requires users to write the return type of queries manually always, and I'd like to automate that step.
> do you think it's possible to use your libraries to get the return type of an arbitrary postgres query, even if it's dynamic?
Yes it is. I've solved that exact problem in pg-nano. I use the `describePrepared` feature of libpq: https://github.com/pg-nano/pg-nano/blob/4cca3dbe6be609c479e4...
One thing I really like about Prisma is only updating my schema and having migrations generated as the "diff".
The main thing holding me back from Prisma is precisely what you like about it - if the migrations are auto-generated and I can't edit them afterward, I won't be able to do what I need to.
As far as migrations go, pg-nano is taking the same “schema diffing” approach that I assume Prisma does, where the active schema of your Postgres instance is compared to the desired schema (defined via SQL files in pg-nano's case) and a migration plan is generated from there. In the context of migrating a non-local Postgres instance, pg-nano still has some R&D to do.
Suggestion: check out the Slonik library for Postgres. It encourages writing raw SQL using string template literals (i.e. sql`select foo from bar where zed = ${someParam}`). It also supports strong typing of results, but the programmer needs to create their own Zod validators manually to check this and get strong typing support.
Seems like this tool could essentially pre-create the types for any raw Postgres SQL statement?
I think this approach of using raw SQL is much better than a custom query builder like kysely, where half the time I'm just trying to translate the SQL that I know in my head to some custom, library-specific API. But the benefit of using kysely is the automatic typing support. Being able to use raw SQL and get query types "for free" would be amazing.
It's a syntax parser that produces an AST. So only information explicitly defined in the syntax is available. To infer input/output types for an arbitrary SQL command, you need introspection (the most fool-proof way being PQdescribePrepared[1]).
> Being able to use raw SQL and get query types "for free" would be amazing.
That's basically what pg-nano does, but you need to use Postgres functions, rather than "$1" or "?" placeholder templating. Of course, some people prefer co-locating their raw SQL inside their TypeScript files, in which case, pg-nano is not for them.
[1]: https://www.postgresql.org/docs/current/libpq-exec.html#LIBP...
There's also the DEFERRABLE constraint setting[1] for one-to-one relationships (which can usually be avoided via a joint reference table). This pattern should already work in pg-nano.
[1]: https://www.postgresql.org/docs/current/sql-set-constraints....
Should typescript code be written as .mts files, with "type = module" in package.json?
What test layout works best? (i.e. __tests__ in project root? filename.test.mts in the same directory as code?)
Are there any good examples of jest.config.mts (mjs?) and tsconfig.json?
Is the typescript compiler supposed to build the test files too? Is it correct to have a ./build/ in your project root, with all built files including tests under that? Do you then strip out the tests when deploying?
This would be targeting an AWS Lambda environment or similar, not browser based so no bundling, is that correct?
Relevant npm packages: - @jest/globals (I use these to import `describe`, `test`, `expect` and other test-related functions) - ts-jest
My tsconfig.json:
and my jest config (jest.config.ts): I have my tests set up under the `__tests__` directory in project root as you noted, and use a `.test.ts` suffix on all relevant test files. Doing this, ts-jest handles the actual transpilation + execution of the tests (just by running jest), and you don't have to worry about including them in your built solution. I have a separate `tsconfig.build.json` for actually building my project (this... is probably inefficient... but it works well for me :P).I think this library is going to set a new standard for db integration!
(v15 exports these, I think they still need to be added to v16).
I've worked with the maintainers of libpg-query-node and they are very friendly and open to improvements. My suggestion would be to work with them to upstream useful changes vs. forking.
So the simplest, stupid check for injection is to parse the query and see if multiple STMT's are found where only one was intended.
Better checks can easily be imagined.
"C library for accessing the PostgreSQL parser outside of the server environment"
https://github.com/pganalyze/libpg_query?tab=readme-ov-file#...