For Rust projects, instead of diesel I tend to prefer sqlx. Migrations are just files following the <datetimestamp>_description.sql
format in the projects ./migrations
directory by default, then you can either manually run them with sqlx migrate run
(or revert to revert them if you make down files too), or, as I do, you just toss in a static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
somewhere in your project source code and call MIGRATOR.run(&self.pool).await?;
to run it on the DB upon initial connection (and it bakes the migration scripts into the binary for those nice single-binary copy-and-run things).
However, you generally do both of the above steps, the latter for migrating the runtime database, the former for migrating the build-time database.
Yes, a build-time database.
However, you don’t need it to always exist, you can just make a build-time database and migrate it up (usually with sqlx database reset
to both build it, migrate it, etc… uses the local .env file for connection information), and then you can use sqlx prepare
to generate a json file of everything sqlx needs for its build time steps (and you can then commit then, only need to update it when migrations change or you write or change sql code in your source).
Now the reason for the build-time database is that sqlx is not an ORM, you literally write code like this:
let result = sqlx::query!("SELECT username, x, y, power, range, health, color_index, vote FROM tanks")
.fetch_all(&self.db.pool)
.await?;
This is just normal SQL for whatever database you are running it on (sqlite in this case, though I usually use postgres), and you can use all the optimizations and features and capabilities and anything you want for that database, no ORM-like restrictions, you have full power.
However, that wouldn’t be as… interesting as just using a normal driver directly, you’d have to do the conversions and parse out the row data and all manually, but not here. The type of result
here is a <closure:...>::Record
, a unique type, which in this case has the fields username
, x
, y
, power
, range
, health
, color_index
, and vote
, of the types that are in the actual database. I could cause a cast to something else by doing something like:
let result = sqlx::query!("SELECT username, x AS \"x: u32\", y, power, range, health, color_index, vote FROM tanks")
.fetch_all(&self.db.pool)
.await?;
Which in this case would cast whatever x is to u32
if it is castable to it, and if not (like if it’s a string, or it’s out of u32 range, then it will return an error), that’s about the extent of its ‘special’ work in the SQL syntax, just that extra decoration after the AS
name via an embedded double-quotes and a colon to cast to the rust type (and of course a trait to put on your own custom types too).
To pass in data you use whatever the syntax is for the database you are using, so for sqlite it’s just ?
, like:
let result = sqlx::query!(
"INSERT INTO users (username, nickname, password_hash) VALUES (?, ?, ?)",
username,
nickname,
password_hash,
)
.execute(&self.db.pool)
.await?;
You can use it in any place you could normally use it in sqlite, again no ORM-like restrictions, you have your full SQL power.
But what if you don’t want an unnameable Record as a result, well define your own structure and use it as the ‘as’ return type:
struct Country { country: String, count: i64 }
let countries = sqlx::query_as!(Country,
"
SELECT country, COUNT(*) as count
FROM users
GROUP BY country
WHERE organization = ?
",
organization
)
.fetch_all(&pool) // -> Vec<Country>
.await?;
No trait needed as it literally just fills it in by name casting to the type of whatever is the field type in the struct if possible (or compile-time failure of course, no runtime failures).
Of course you know the SQL ‘works’ for how its written because sqlx actually compiles the sql using the chosen driver at compile-time (which is how it gets type information from it and more, which of course it can cache in the offline file via sqlx prepare
).
And of course if you need just a single return value(s) and not a struct you can just use query_scalar!
like so:
let color_index = sqlx::query_scalar!("SELECT color_index FROM tanks WHERE username = ?", username)
.fetch_one(&self.db.pool)
.await?;
And of course color_index
here will the type of color_index
from inside the database (which you could cast too of course).
And lastly, if you need to dynamically generate SQL at runtime then just use the same function like query_scalar
or so but without the !
(so you call the function instead of the macro), they have all the same capabilities as well except you need to pass arguments via .bind(...)
calls instead of inline and you need to pass the query_as
type as a generic argument and it must implement an appropriate trait, you lose compile-time guarantees with those but you still have the full power otherwise. You can get streaming results, get results dumped as a vector (or into some other container), can get a forced one result, or an optional one, can just execute and get a row count result, among others.
And yes, of course it is fully async capable with built-in pooling (everything optional and can be done piecemeal if you so wish, but by default it’s async pooled), can start transactions, etc… etc…
This has truly become my favorite sql library among any language, been half-tempted to port it to Elixir as Elixir could do it as well…
/me really really loves compile-time checking of the SQL code like it is normal rust, get highlights in my IDE and all