Litdb – type safe SQL for JavaScript/TS
litdb.dev
litdb.dev
I don't really know enough about Kysely yet to make an informed opinion between those two. If you know more than me, can you give me your take??
Edit: Hmmm perhaps based on the primary author's other repos (https://github.com/mythz) it looks like they're a fan of C#. Perhaps it's the LINQ-like syntax that separates them the most.
I've been using a lot of bun:sqlite [2] lately which has an amazing DX and lets you create lots of stand-alone .ts scripts (i.e. without deps) to access SQLite DB's. The only issue is that I didn't want all my SQL queries to be coupled to a single driver, so I created litdb to provide a RDBMS-agnostic API + Query Builders so all my queries could easily be run on different DBs.
TypeScript has an amazingly powerful type system which let me build the ideal abstraction I wanted where I could use expressive SQL Expressions but still have typed references to our App's classes (tables) / properties (columns) to benefit from static analysis/intelli-sense during development whilst making it safe to refactor / find references / etc.
Things that are hard/impossible in C# is easy in TypeScript, e.g. the QueryBuilders lets you have a variable number of generic args which isn't possible in C# also it was much easier to support composable queries [3] than trying to combine multiple LINQ queries with shared references.
[1] https://docs.servicestack.net/ormlite/
But in my view there's still some scope for improvement in expressive-ness (compared to something like LINQ). A future area of exploration could be to add a parser to the mix, so that we can write:
const jane = db.contacts.one(c => c.email === janeEmail);
instead of: const jane = db.one<Contact>($.from(Contact).where(c => $`${c.email} = ${janeEmail}`))!
The former is so naturally typesafe, without having to resort to advanced typescript tricks. I got half way there [1], before dropping the project due to work pressure (but many years back, so the code is quite stale). Parsers were viable with caching back then. These days, performance is not going to be problem whatever you do. If anyone is interested, happy to exchange notes.[1]: https://github.com/isotropy/isotropy-db/blob/master/src/test...
Your proposed API would require augmenting the driver with an application schema which would couple drivers to both litdb implementations and an application's schema where it would no longer be suitable for querying outside of your Application schema. Also not a fan of global Application schemas, i.e. litdb classes are self encapsulating so you could create a class at runtime and execute it without having to register it with a global application schema.
With that said you could implement a similar API to what you want with something like:
class AppDb {
constructor(public $:ReturnType<typeof Sql.create>) {}
contacts() { return this.$.from(Contact) }
}
const app = new AppDb($)
Which you could then use like: db.one(app.contacts.where(c => $`${c.email} = ${janeEmail}`))
I also prefer APIs to be SQL-like (i.e. instead of inventing a custom object model) where APIs and Typed Query Builders designed around SQL so it's intuitive how to construct queries and what SQL a query would execute. At the moment there's a clear separation of Query Builders which generates SQL + Params and the drivers which just executes them.BTW you don't need the generic type if you only have "1 table without a custom select", i.e. this will implicitly return a Contact instance:
const jane = db.one($.from(Contact).where(c => $`${c.email} = ${janeEmail}`))
Specifying `one<Contact>` generic type is similar to `as Contact` inert type hint, i.e. it informs TS what the shape of the returned object is but doesn't change behavior, it's preferable to use `.into(Contact)` which explicitly returns a `Contact` instance. const jane = db.one($.from(Contact).where(c => $`${c.email} = ${janeEmail}`).into(Contact))But my suggestion doesn't couple the driver and the Application/query builder. Here's a simplified example.
const db = {
// table() is a factory that returns (for example) PostgresTable
// or MySQLTable or even MongoTable based on config
customers: table()
}
This has some other interesting properties - for example, you could stub these tables with pure JS lists for tests. The broad applicability of this approach has been proven to work by EF being the defacto DB access method in the .Net world (over 15 years now).It's only the defacto DB access method in .NET because that's what Microsoft's EF library chose and .NET ecosystem just uses the default MS option but EF's influence doesn't extend outside of .NET. AFAIK no other .NET ORM does this including our .NET ORM [1] which litdb is the spiritual port of.
Nothing wrong with your approach, I was just arguing that language-native query patterns (such as customers.filter(c => c.country === "Chile")) can be appealing. And at the same time, reachable with current JS/TS tooling.
But like you said, it may not be the direction litdb wants to go.
For example,
import { contacts } from "db"
const jane = contacts.one(c => c.email === janeEmail)
Marco-like magic can be confusing, as we learned from Svelte. Especially when it's half-baked like the one you are suggesting.Another drawback is that you'll need to compile the code twice to run it: once with the custom parser and once with the TypeScript compiler.
When I'm giving examples on a forum thread, I try to provide one that's simpler to understand and captures the essence of the argument. I am not making any claims on completeness.
> Marco-like magic can be confusing, as we learned from Svelte. Especially when it's half-baked like the one you are suggesting.
Svelte invents new syntax, which in my view isn't great. This is pure JS, and it could even run in a browser against arrays (for tables).
Add:
> That won't be possible without a lot of work. You'll need to tweak the TypeScript compiler to make the parser comprehend exported variables.
Not required. I was suggesting that the expression could be parsed at runtime. There are various options, with different tradeoffs.
Good idea. That might be possible, though not every JavaScript runtime implements Function.prototype.toString. I'm not sure if Node does.
const aFunction = arg => arg.a === arg.b
const aFunctionSrc = aFunction.toString()
console.log(aFunctionSrc) // 'arg => arg.a === arg.b'
const transformedFunctionSrc = transform(aFunctionSrc)
const transformedFunction = new Function(transformedFunctionSrc)
console.log(transformedFunction) // [Function: anonymous]
// Cache the transformed function in a WeakMap
// WeakMap values are garbage collected when their keys are garbage collected
const functionMap = new WeakMap()
functionMap.set(aFunction, transformedFunction)
console.log(functionMap.get(aFunction)) // [Function: anonymous]If the problem is seen as an editor or build-time problem, rather than a library one, you don't have to learn anything new, and you can save the weight in your dependencies.
A fusion of ts-safeql [0] and postgres_lsp [1] is the closest I've seen to solving this as an editor/build problem.
You can find it here: https://github.com/nikeee/sequelts
One downside of course is that AFAIK typescript is not powerful enough to match all queries to types, so my idea was to build a .d.ts file that maps the queries with the result.
ts-safeql seems to work with a lsp plugin? I didn't dig into how lsp and plugin works, so I cannot speak of if my approach will work with it, but I suppose no, because it needs to generate files.
Drizzle also uses its own custom query language e.g.
.where(eq(countries.id, 10))
Whereas litdb lets you use the full expressiveness of SQL but ensures all references are typed: .where(c => $`${c.id} = 10`)
[1] https://orm.drizzle.team/docs/overviewSeems a strange design choice for a library that claims to offer a type-safe sql builder.
SELECT * from Contact where id = '1'
With that said you can achieve something similar in litdb with a custom expression: const eq = <T,V>(ref:(x:T)=>V, value:V) => (x:T) => $`${ref(x)} = ${value}`
Which will type check that the value matches the column type: .where(eq(c => c.id, 2))
and fail type check when they don't: .where(eq(c => c.id, '2'))
Examples of other custom expressions: https://litdb.dev/#composableMy current project uses PostGIS which uses opaque types for storing geometry. Geometry columns are added to tables via a function instead of traditional alter table syntax, and select/where clauses on geometry columns need to use PostGIS functions to render the column into useable data.
Unless a system like Litdb includes an easy way to provide type definitions for function return types, it won't be usable with an extension like PostGIS without heavy use of escape hatches, at which point most of the value is lost.
PostGIS uses a lot of functions like ST_AsEWKT, ST_AsMVT or ST_AsGeoJSON [1] to marshal data. While ST_AsGeoJSON will always return "text", ideally you'd want an invocation of ST_AsGeoJSON to return JSON to your JavaScript, but this wouldn't be true of all "text".
Even better, you would want to declare the structure of the returned JSON via a TypeScript type. GeoJSON is a structured format, so this would likely be a generic GeoJSON type wrapped around a custom type for the specific structure you expect for each geometry type / query.
Anyway, it's a tough problem to solve without introducing TS versions of each specialty function, which would be a large effort for an extension the size of PostGIS. For now I use typed raw queries via the sql<T>`` escape hatch provided by kysely, but if your library made this more ergonomic/safe I'd consider switching!
[1] https://github.com/porsager/postgres
[2] https://github.com/litdb/litdb/discussions/categories/ideas
Raw SQL (or PL/pgSQL which can be quite powerful) with generated, type-safe client "bindings" (TypeScript functions). It also includes "declarative SQL schema" (instantly update the schema of your dev DB on file save). Generated migrations and "seed scripts" are also on the roadmap.
If any of that interests you, check it out (https://github.com/pg-nano/pg-nano). I would also be happy to discuss it with you on Discord (@aleclarson).
SELECT
COUNT(col_a) as count
WHERE
count > 0
Kysely uses (from -> select -> where), and allows joins and selects in multiple places, like (from -> join -> select -> join -> select -> where).I'm building a middle ground of sorts. Raw SQL (or PL/pgSQL which can be quite powerful) with generated, type-safe client "bindings" (TypeScript functions). It also includes "declarative SQL schema" (instantly update the schema of your dev DB on file save). Generated migrations and "seed scripts" are also on the roadmap.
If any of that interests you, check it out (https://github.com/pg-nano/pg-nano). I would also love to discuss it with whoever's reading this on Discord (@aleclarson).
My IDE already cries from how slow tsc / the ts language server is.
This supports static verification of queries, your code doesn't need to run to validate the type is correct.
I don't care to argue either way for tests vs static types (I write both frankly), just want the distinction to be clear.
You can use real SQL and get type-safety just using typescript’s “… as MyType”. Of course it blows up if the database doesn’t match up, but so does this, I think. (Yes, you may want some mechanisms to validate the data has the expected form.)
The idea is that your App models represents your database's schema which your App's logic is bound to. You can also use litdb schema APIs [1] to create your database tables so that they're in sync.
Futhermore, a lot of the value of these systems is to provide type safety within the query. You choose a typed column in your select clause, and then in your join or where clause the column type is inferred, and warns you if you attempt a comparison that doesn't work with that type.
If the purpose of TypeScript is to add type safety to your logic, why wouldn't you want type safety in the logic that happens to be database queries? I haven't used this library but have used kysely which seems very similar, and all the benefits I enjoy from TypeScript, I now enjoy in my SQL.
> It seems like a step back to have to write SQL in Javascript syntax with a non-standard API.
Like TypeScript, this is an attempt to add type safety and hinting to an untyped language: SQL. With that in mind, some compromises seem inevitable.
The problem is you’re writing queries in Litdb’s Javascript/typescript-based query language instead of SQL, but the types it provides still aren’t real — you need to ensure they match the data some other way, just like with “as”.
What the types here really accomplish is code-completion. That’s nice but there are ways to do that without giving up real SQL. Not to mention databases have their own type systems, which differ from each other and typescript. Maybe they nail all the mapping, but I suspect their are misses (probably by not allowing valid things, since typescript is generally more strict that dbms’s).
It's quite similar to working with a web API. You can invent all the types you want, maybe generated from an OpenAPI schema, but if the server sends something different, TypeScript can't help you. That's not what TypeScript is for.
At the end of the day, most non-scalar TypeScript types "aren't real". Objects can be mutated at runtime, libraries can ship incorrect types, TS can be mixed with JS, etc. We try to introduce types as early as possible to catch a wide swath of possible errors, but where it's really important you still need to verify at runtime.
Raw SQL (or PL/pgSQL which can be quite powerful) with generated, type-safe client "bindings" (TypeScript functions). It also includes "declarative SQL schema" (instantly update the schema of your dev DB on file save). Generated migrations and "seed scripts" are also on the roadmap.
If any of that interests you, check it out (https://github.com/pg-nano/pg-nano). I would also love to discuss it with whoever's reading this on Discord (@aleclarson).
For example, the issue asking for deferrable constraint support for Postgres in drizzle has been open for about a year: https://github.com/drizzle-team/drizzle-orm/issues/1429
In any non-trivial application, you are increasingly likely to run into something that your database supports but your query-builder does not. What will you do then?
It's sad, but unfortunately the most reliable approach seems to be to write raw SQL in a DAO package/library, then using integration tests (DAO + database) to confirm behavior.
This particular feature seems to have nothing to do with the query builder even, just the migration system. You can always write just those specific migrations manually.
1. https://github.com/manifold-systems/manifold/blob/master/man...
EDIT: They don't even support, not to not mention, CTE! Come on!
I was expecting something more like sqlx, which I believe provides compile-time checking for SQL queries without a DSL, but for Rust
1. https://github.com/manifold-systems/manifold/blob/master/man...