Skip to content

Commit db6d28b

Browse files
committed
feat: create postgres/multi-database example
1 parent b989b36 commit db6d28b

File tree

20 files changed

+932
-0
lines changed

20 files changed

+932
-0
lines changed

.github/workflows/examples.yml

+18
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,24 @@ jobs:
175175
DATABASE_URL: postgres://postgres:password@localhost:5432/mockable-todos
176176
run: cargo run -p sqlx-example-postgres-mockable-todos
177177

178+
- name: Multi-Database (Setup)
179+
working-directory: examples/postgres/multi-database
180+
env:
181+
DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database
182+
ACCOUNTS_DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database-accounts
183+
PAYMENTS_DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database-payments
184+
run: |
185+
(cd accounts && sqlx db setup)
186+
(cd payments && sqlx db setup)
187+
sqlx db setup
188+
189+
- name: Multi-Database (Run)
190+
env:
191+
DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database
192+
ACCOUNTS_DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database-accounts
193+
PAYMENTS_DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database-payments
194+
run: cargo run -p sqlx-example-postgres-multi-database
195+
178196
- name: Multi-Tenant (Setup)
179197
working-directory: examples/postgres/multi-tenant
180198
env:

Cargo.lock

+42
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ members = [
1717
"examples/postgres/json",
1818
"examples/postgres/listen",
1919
"examples/postgres/mockable-todos",
20+
"examples/postgres/multi-database",
2021
"examples/postgres/multi-tenant",
2122
"examples/postgres/todos",
2223
"examples/postgres/transaction",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[package]
2+
name = "sqlx-example-postgres-multi-database"
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+
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
13+
14+
sqlx = { path = "../../..", version = "0.8.3", features = ["runtime-tokio", "postgres"] }
15+
16+
axum = { version = "0.8.1", features = ["macros"] }
17+
18+
color-eyre = "0.6.3"
19+
dotenvy = "0.15.7"
20+
tracing-subscriber = "0.3.19"
21+
22+
rust_decimal = "1.36.0"
23+
24+
rand = "0.8.5"
25+
26+
[dependencies.accounts]
27+
path = "accounts"
28+
package = "sqlx-example-postgres-multi-database-accounts"
29+
30+
[dependencies.payments]
31+
path = "payments"
32+
package = "sqlx-example-postgres-multi-database-payments"
33+
34+
[lints]
35+
workspace = true
+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Axum App with multi-database Database
2+
3+
This example project involves three crates, each owning a different schema in one database,
4+
with their own set of migrations.
5+
6+
* The main crate, a simple binary simulating the action of a REST API.
7+
* Owns the `public` schema (tables are referenced unqualified).
8+
* Migrations are moved to `src/migrations` using config key `migrate.migrations-dir`
9+
to visually separate them from the subcrate folders.
10+
* `accounts`: a subcrate simulating a reusable account-management crate.
11+
* Owns schema `accounts`.
12+
* `payments`: a subcrate simulating a wrapper for a payments API.
13+
* Owns schema `payments`.
14+
15+
## Note: Schema-Qualified Names
16+
17+
This example uses schema-qualified names everywhere for clarity.
18+
19+
It can be tempting to change the `search_path` of the connection (MySQL, Postgres) to eliminate the need for schema
20+
prefixes, but this can cause some really confusing issues when names conflict.
21+
22+
This example will generate a `_sqlx_migrations` table in three different schemas; if `search_path` is set
23+
to `public,accounts,payments` and the migrator for the main application attempts to reference the table unqualified,
24+
it would throw an error.
25+
26+
# Setup
27+
28+
This example requires running three different sets of migrations.
29+
30+
Ensure `sqlx-cli` is installed with Postgres and `sqlx.toml` support:
31+
32+
```
33+
cargo install sqlx-cli --features postgres,sqlx-toml
34+
```
35+
36+
Start a Postgres server (shown here using Docker, `run` command also works with `podman`):
37+
38+
```
39+
docker run -d -e POSTGRES_PASSWORD=password -p 5432:5432 --name postgres postgres:latest
40+
```
41+
42+
Create `.env` with the various database URLs or set them in your shell environment;
43+
44+
```
45+
DATABASE_URL=postgres://postgres:password@localhost/example-multi-database
46+
ACCOUNTS_DATABASE_URL=postgres://postgres:password@localhost/example-multi-database-accounts
47+
PAYMENTS_DATABASE_URL=postgres://postgres:password@localhost/example-multi-database-payments
48+
```
49+
50+
Run the following commands:
51+
52+
```
53+
(cd accounts && sqlx db setup)
54+
(cd payments && sqlx db setup)
55+
sqlx db setup
56+
```
57+
58+
It is an open question how to make this more convenient; `sqlx-cli` could gain a `--recursive` flag that checks
59+
subdirectories for `sqlx.toml` files, but that would only work for crates within the same workspace. If the `accounts`
60+
and `payments` crates were instead crates.io dependencies, we would need Cargo's help to resolve that information.
61+
62+
An issue has been opened for discussion: <https://github.com/launchbadge/sqlx/issues/3761>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "sqlx-example-postgres-multi-database-accounts"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
sqlx = { workspace = true, features = ["postgres", "time", "uuid", "macros", "sqlx-toml"] }
8+
tokio = { version = "1", features = ["rt", "sync"] }
9+
10+
argon2 = { version = "0.5.3", features = ["password-hash"] }
11+
password-hash = { version = "0.5", features = ["std"] }
12+
13+
uuid = { version = "1", features = ["serde"] }
14+
thiserror = "1"
15+
rand = "0.8"
16+
17+
time = { version = "0.3.37", features = ["serde"] }
18+
19+
serde = { version = "1.0.218", features = ["derive"] }
20+
21+
[dev-dependencies]
22+
sqlx = { workspace = true, features = ["runtime-tokio"] }
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;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
create table account
2+
(
3+
account_id uuid primary key default gen_random_uuid(),
4+
email text unique not null,
5+
password_hash text not null,
6+
created_at timestamptz not null default now(),
7+
updated_at timestamptz
8+
);
9+
10+
select trigger_updated_at('account');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
create table session
2+
(
3+
session_token text primary key, -- random alphanumeric string
4+
account_id uuid not null references account (account_id),
5+
created_at timestamptz not null default now()
6+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[common]
2+
database-url-var = "ACCOUNTS_DATABASE_URL"
3+
4+
[macros.table-overrides.'account']
5+
'account_id' = "crate::AccountId"
6+
'password_hash' = "sqlx::types::Text<password_hash::PasswordHashString>"
7+
8+
[macros.table-overrides.'session']
9+
'session_token' = "crate::SessionToken"
10+
'account_id' = "crate::AccountId"

0 commit comments

Comments
 (0)