Skip to content

Commit 66460ea

Browse files
committed
feat: create macros.preferred-crates example
1 parent 08dffff commit 66460ea

File tree

13 files changed

+422
-0
lines changed

13 files changed

+422
-0
lines changed

.github/workflows/examples.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,17 @@ jobs:
207207
DATABASE_URL: postgres://postgres:password@localhost:5432/multi-tenant
208208
run: cargo run -p sqlx-example-postgres-multi-tenant
209209

210+
- name: Preferred-Crates (Setup)
211+
working-directory: examples/postgres/preferred-crates
212+
env:
213+
DATABASE_URL: postgres://postgres:password@localhost:5432/preferred-crates
214+
run: sqlx migrate run
215+
216+
- name: Multi-Tenant (Run)
217+
env:
218+
DATABASE_URL: postgres://postgres:password@localhost:5432/preferred-crates
219+
run: cargo run -p sqlx-example-postgres-preferred-crates
220+
210221
- name: TODOs (Setup)
211222
working-directory: examples/postgres/todos
212223
env:

Cargo.lock

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ members = [
1919
"examples/postgres/mockable-todos",
2020
"examples/postgres/multi-database",
2121
"examples/postgres/multi-tenant",
22+
"examples/postgres/preferred-crates",
2223
"examples/postgres/todos",
2324
"examples/postgres/transaction",
2425
"examples/sqlite/todos",
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[package]
2+
name = "sqlx-example-postgres-preferred-crates"
3+
version.workspace = true
4+
license.workspace = true
5+
edition.workspace = true
6+
repository.workspace = true
7+
keywords.workspace = true
8+
categories.workspace = true
9+
authors.workspace = true
10+
11+
[dependencies]
12+
dotenvy.workspace = true
13+
14+
anyhow = "1"
15+
chrono = "0.4"
16+
serde = { version = "1", features = ["derive"] }
17+
uuid = { version = "1", features = ["serde"] }
18+
19+
[dependencies.tokio]
20+
workspace = true
21+
features = ["rt-multi-thread", "macros"]
22+
23+
[dependencies.sqlx]
24+
path = "../../.."
25+
version = "0.8"
26+
features = ["runtime-tokio", "postgres", "bigdecimal", "chrono", "derive"]
27+
28+
[dependencies.uses-rust-decimal]
29+
path = "uses-rust-decimal"
30+
package = "sqlx-example-postgres-preferred-crates-uses-rust-decimal"
31+
32+
[dependencies.uses-time]
33+
path = "uses-time"
34+
package = "sqlx-example-postgres-preferred-crates-uses-time"
35+
36+
[lints]
37+
workspace = true
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Usage of `macros.preferred-crates` in `sqlx.toml`
2+
3+
## The Problem
4+
5+
SQLx has many optional features that enable integrations for external crates to map from/to SQL types.
6+
7+
In some cases, more than one optional feature applies to the same set of types:
8+
9+
* The `chrono` and `time` features enable mapping SQL date/time types to those in these crates.
10+
* Similarly, `bigdecimal` and `rust_decimal` enable mapping for the SQL `NUMERIC` type.
11+
12+
Throughout its existence, the `query!()` family of macros has inferred which crate to use based on which optional
13+
feature was enabled. If multiple features are enabled, one takes precedent over the other: `time` over `chrono`,
14+
`rust_decimal` over `bigdecimal`, etc. The ordering is purely the result of historical happenstance and
15+
does not indicate any specific preference for one crate over another. They each have their tradeoffs.
16+
17+
This works fine when only one crate in the dependency graph depends on SQLx, but can break down if another crate
18+
in the dependency graph also depends on SQLx. Because of Cargo's [feature unification], any features enabled
19+
by this other crate are also forced on for all other crates that depend on the same version of SQLx in the same project.
20+
21+
This is intentional design on Cargo's part; features are meant to be purely additive, so it can build each transitive
22+
dependency just once no matter how many crates depend on it. Otherwise, this could result in combinatorial explosion.
23+
24+
Unfortunately for us, this means that if your project depends on SQLx and enables the `chrono` feature, but also depends
25+
on another crate that enables the `time` feature, the `query!()` macros will end up thinking that _you_ want to use
26+
the `time` crate, because they don't know any better.
27+
28+
Fixing this has historically required patching the dependency, which is annoying to maintain long-term.
29+
30+
[feature unification]: https://doc.rust-lang.org/cargo/reference/features.html#feature-unification
31+
32+
## The Solution
33+
34+
However, as of 0.9.0, SQLx has gained the ability to configure the macros through the use of a `sqlx.toml` file.
35+
36+
This includes the ability to tell the macros which crate you prefer, overriding the inference.
37+
38+
See the [`sqlx.toml`](./sqlx.toml) file in this directory for details.
39+
40+
A full reference `sqlx.toml` is also available as `sqlx-core/src/config/reference.toml`.
41+
42+
## This Example
43+
44+
This example exists both to showcase the macro configuration and also serve as a test for the functionality.
45+
46+
It consists of three crates:
47+
48+
* The root crate, which depends on SQLx and enables the `chrono` and `bigdecimal` features,
49+
* `uses-rust-decimal`, a dependency which also depends on SQLx and enables the `rust_decimal` feature,
50+
* and `uses-time`, a dependency which also depends on SQLx and enables the `time` feature.
51+
* This serves as a stand-in for `tower-sessions-sqlx-store`, which is [one of the culprits for this issue](https://github.com/launchbadge/sqlx/issues/3412#issuecomment-2277377597).
52+
53+
Given that both dependencies enable features with higher precedence, they would historically have interfered
54+
with the usage in the root crate. (Pretend that they're published to crates.io and cannot be easily changed.)
55+
However, because the root crate uses a `sqlx.toml`, the macros know exactly which crates it wants to use and everyone's happy.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[migrate]
2+
# Move `migrations/` to under `src/` to separate it from subcrates.
3+
migrations-dir = "src/migrations"
4+
5+
[macros.preferred-crates]
6+
# Keeps `time` from taking precedent even though it's enabled by a dependency.
7+
date-time = "chrono"
8+
# Same thing with `rust_decimal`
9+
numeric = "bigdecimal"
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use anyhow::Context;
2+
use chrono::{DateTime, Utc};
3+
use sqlx::{Connection, PgConnection};
4+
use std::time::Duration;
5+
use uuid::Uuid;
6+
7+
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug)]
8+
struct SessionData {
9+
user_id: Uuid,
10+
}
11+
12+
#[derive(sqlx::FromRow, Debug)]
13+
struct User {
14+
id: Uuid,
15+
username: String,
16+
password_hash: String,
17+
// Because `time` is enabled by a transitive dependency, we previously would have needed
18+
// a type override in the query to get types from `chrono`.
19+
created_at: DateTime<Utc>,
20+
updated_at: Option<DateTime<Utc>>,
21+
}
22+
23+
const SESSION_DURATION: Duration = Duration::from_secs(60 * 60); // 1 hour
24+
25+
#[tokio::main]
26+
async fn main() -> anyhow::Result<()> {
27+
let mut conn =
28+
PgConnection::connect(&dotenvy::var("DATABASE_URL").context("DATABASE_URL must be set")?)
29+
.await
30+
.context("failed to connect to DATABASE_URL")?;
31+
32+
sqlx::migrate!("./src/migrations").run(&mut conn).await?;
33+
34+
uses_rust_decimal::create_table(&mut conn).await?;
35+
uses_time::create_table(&mut conn).await?;
36+
37+
let user_id = sqlx::query!(
38+
"insert into users(username, password_hash) values($1, $2) returning id",
39+
"user_foo",
40+
"<pretend this is a password hash>",
41+
)
42+
.fetch_one(&mut conn)
43+
.await?;
44+
45+
let user = sqlx::query_as!(User, "select * from users where id = $1", user_id)
46+
.fetch_one(&mut conn)
47+
.await?;
48+
49+
let session =
50+
uses_time::create_session(&mut conn, SessionData { user_id }, SESSION_DURATION).await?;
51+
52+
let session_from_id = uses_time::get_session::<SessionData>(&mut conn, session.id)
53+
.await?
54+
.expect("expected session");
55+
56+
assert_eq!(session, session_from_id);
57+
58+
let purchase_id =
59+
uses_rust_decimal::create_purchase(&mut conn, user_id, 1234u32.into(), "Rent").await?;
60+
61+
let purchase = uses_rust_decimal::get_purchase(&mut conn, purchase_id)
62+
.await?
63+
.expect("expected purchase");
64+
65+
Ok(())
66+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
-- We try to ensure every table has `created_at` and `updated_at` columns, which can help immensely with debugging
2+
-- and auditing.
3+
--
4+
-- While `created_at` can just be `default now()`, setting `updated_at` on update requires a trigger which
5+
-- is a lot of boilerplate. These two functions save us from writing that every time as instead we can just do
6+
--
7+
-- select trigger_updated_at('<table name>');
8+
--
9+
-- after a `CREATE TABLE`.
10+
create or replace function set_updated_at()
11+
returns trigger as
12+
$$
13+
begin
14+
NEW.updated_at = now();
15+
return NEW;
16+
end;
17+
$$ language plpgsql;
18+
19+
create or replace function trigger_updated_at(tablename regclass)
20+
returns void as
21+
$$
22+
begin
23+
execute format('CREATE TRIGGER set_updated_at
24+
BEFORE UPDATE
25+
ON %s
26+
FOR EACH ROW
27+
WHEN (OLD is distinct from NEW)
28+
EXECUTE FUNCTION set_updated_at();', tablename);
29+
end;
30+
$$ language plpgsql;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
create table users(
2+
id uuid primary key default gen_random_uuid(),
3+
username text not null,
4+
password_hash text not null,
5+
created_at timestamptz not null default now(),
6+
updated_at timestamptz
7+
);
8+
9+
create unique index users_username_unique on users(lower(username));
10+
11+
select trigger_updated_at('users');
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "sqlx-example-postgres-preferred-crates-uses-rust-decimal"
3+
version.workspace = true
4+
license.workspace = true
5+
edition.workspace = true
6+
repository.workspace = true
7+
keywords.workspace = true
8+
categories.workspace = true
9+
authors.workspace = true
10+
11+
[dependencies]
12+
chrono = "0.4"
13+
rust_decimal = "1"
14+
uuid = "1"
15+
16+
[dependencies.sqlx]
17+
workspace = true
18+
features = ["runtime-tokio", "postgres", "rust_decimal", "chrono", "uuid"]
19+
20+
[lints]
21+
workspace = true
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use chrono::{DateTime, Utc};
2+
use sqlx::PgExecutor;
3+
4+
#[derive(sqlx::FromRow)]
5+
struct Purchase {
6+
pub id: Uuid,
7+
pub user_id: Uuid,
8+
pub amount: Decimal,
9+
pub description: String,
10+
pub created_at: DateTime<Utc>,
11+
}
12+
13+
pub use rust_decimal::Decimal;
14+
use uuid::Uuid;
15+
16+
pub async fn create_table(e: impl PgExecutor<'_>) -> sqlx::Result<()> {
17+
sqlx::raw_sql(
18+
// language=PostgreSQL
19+
"create table if not exists purchases( \
20+
id uuid primary key default gen_random_uuid(), \
21+
user_id uuid not null, \
22+
amount numeric not null check(amount > 0), \
23+
description text not null, \
24+
created_at timestamptz not null \
25+
);
26+
",
27+
)
28+
.execute(e)
29+
.await?;
30+
31+
Ok(())
32+
}
33+
34+
pub async fn create_purchase(
35+
e: impl PgExecutor<'_>,
36+
user_id: Uuid,
37+
amount: Decimal,
38+
description: &str,
39+
) -> sqlx::Result<Uuid> {
40+
sqlx::query_scalar("insert into purchases(user_id, amount, description) values ($1, $2, $3)")
41+
.bind(user_id)
42+
.bind(amount)
43+
.bind(description)
44+
.fetch_one(e)
45+
.await
46+
}
47+
48+
pub async fn get_purchase(e: impl PgExecutor<'_>, id: Uuid) -> sqlx::Result<Option<Purchase>> {
49+
sqlx::query_as("select * from purchases where id = $1")
50+
.bind(id)
51+
.fetch_optional(e)
52+
.await
53+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "sqlx-example-postgres-preferred-crates-uses-time"
3+
version.workspace = true
4+
license.workspace = true
5+
edition.workspace = true
6+
repository.workspace = true
7+
keywords.workspace = true
8+
categories.workspace = true
9+
authors.workspace = true
10+
11+
[dependencies]
12+
serde = "1"
13+
time = "0.3"
14+
uuid = "1"
15+
16+
[dependencies.sqlx]
17+
workspace = true
18+
features = ["runtime-tokio", "postgres", "time", "json", "uuid"]
19+
20+
[lints]
21+
workspace = true

0 commit comments

Comments
 (0)