SQL the right way
Table of Contents
Background
SQL
is a fundamental building block of almost any application.
It’s the most popular database query language.
There are 2 popular approaches to using SQL
in applications:
- ORM (Object-Relational Mapping) - A wrapper around
SQL
that abstracts the underlying interface from the user. - Raw SQL - Writing
SQL
queries directly in the application code.
We’ll discuss #2
and how to do it the right way.
sqlx
sqlx is a modern SQL
library for rust
with support for async
.
But the feature that is truly a game changer is the compile
time verification and type casting
.
Compile time verification
In sqlx if you use query!
or query_as! (and the rest of this macro
family)
it will verify your SQL
during cargo build
phase.
For example, let’s assume the following SQL
scheme:
CREATE TABLE users
(
id uuid PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
);
If we want to fetch a User
by username
we can use the following`:
match sqlx::query_as!(
User,
r#"
SELECT id, username
FROM users
WHERE username = $1
LIMIT 1"#,
username
)
#[derive(sqlx::FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct User {
pub id: Uuid,
pub username: String,
}
- If we pass
username
that isn’t aString
it will fail duringcargo build
phase. - If we have a
typo
in one of the fields it will fail duringcargo build
phase. - If we did some
migration
and forgot to update thequery
it will fail duringcargo build
phase.
I can’t stress enough how much time and effort this feature saves. It helped me tremendously, especially
on refactoring
.
Type casting
Let’s add a password
field to our User
:
CREATE TABLE users
(
id uuid PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
);
#[derive(sqlx::FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct User {
pub id: Uuid,
pub username: String,
#[serde(skip)]
pub password: Secret<String>,
}
Obviously, we would like as little exposure to password
as possible.
To prevent leakage of any sorts.
In rust
side, we can define a type
that will prevent any logger
to accidentally print it.
And will require explicit
call to expose
the secret
.
pub struct Secret<T>(T)
where
T: Clone;
impl<T> Clone for Secret<T>
where
T: Clone,
{
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Default for Secret<T>
where
T: Clone + Default,
{
fn default() -> Self {
Self(T::default())
}
}
impl<T> Display for Secret<T>
where
T: Clone,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[hidden]")
}
}
impl<T> Debug for Secret<T>
where
T: Clone,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[hidden]")
}
}
But how do we combine secret
with sqlx
?
match sqlx::query_as!(
User,
r#"
SELECT id, username, password as "password: Secret<String>"
FROM users
WHERE username = $1
LIMIT 1"#,
username
)
Here password as "password: Secret<String>"
is where the magic
happens.
This tells sqlx
to cast
the password
to Secret<String>
.
By doing so, other than query logs
(which you can mute),
password
will be hidden
from any logger
.
This example shows how sqlx
helps bring rust
powerful type
system into SQL
.
All during cargo build
phase, with little runtime surprises.