Skip to content

Commit 60e0216

Browse files
committed
Database and DB Migrations
1 parent d182264 commit 60e0216

21 files changed

Lines changed: 766 additions & 10 deletions
Lines changed: 381 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,384 @@
11
---
22
title: Database and DB Migrations
33
slug: database-and-db-migrations
4-
---
4+
---
5+
6+
## Database Design
7+
8+
| Column Name | Datatype | Not Null | Primary Key |
9+
|----------------|-------------|----------|-------------|
10+
| created_at | TIMESTAMPTZ | ✅ | |
11+
| updated_at | TIMESTAMPTZ | ✅ | |
12+
| id | UUID | ✅ | ✅ |
13+
| published_date | DATE | ✅ | |
14+
| status | SMALLINT | ✅ | |
15+
| title | TEXT | ✅ | |
16+
| description | TEXT | | |
17+
| image_url | TEXT | | |
18+
19+
## Add `jiff`, `uuid`, `toasty` and `toasty-cli`
20+
21+
Run following `cargo add` commands from the `book_service` folder.
22+
23+
```shell
24+
cargo add jiff
25+
cargo add uuid -F v7,fast-rng
26+
cargo add toasty -F postgresql,migration,jiff
27+
cargo add toasty-cli
28+
```
29+
30+
## Initial Model
31+
32+
### Add Initial `book.rs`
33+
34+
Add a new file `book_service/src/models/book.rs`.
35+
36+
```rust
37+
use jiff::{Timestamp, civil::Date};
38+
use uuid::Uuid;
39+
40+
#[derive(Debug, toasty::Model)]
41+
pub struct Book {
42+
#[auto]
43+
pub created_at: Timestamp,
44+
45+
#[auto]
46+
pub updated_at: Timestamp,
47+
48+
#[key]
49+
#[auto]
50+
pub id: Uuid,
51+
52+
pub published_date: Date,
53+
54+
pub status: BookStatus,
55+
56+
pub title: String,
57+
58+
pub description: Option<String>,
59+
60+
pub image_url: Option<String>,
61+
}
62+
63+
#[derive(Debug, Clone, Copy, PartialEq, toasty::Embed)]
64+
#[column(type = u8)]
65+
pub enum BookStatus {
66+
#[column(variant = 0)]
67+
Pending,
68+
#[column(variant = 1)]
69+
Verified,
70+
}
71+
```
72+
73+
### Re-exporting `Book` and `BookStatus`
74+
75+
Add a new file `book_service/src/models/mod.rs`.
76+
77+
```rust
78+
mod book;
79+
80+
pub use book::{Book, BookStatus};
81+
```
82+
83+
Update `book_service/src/lib.rs` to add `pub mod models;`.
84+
85+
```rust
86+
mod config;
87+
mod state;
88+
89+
pub mod models;
90+
pub mod routes;
91+
92+
pub use config::{AppConf, ServerConf};
93+
pub use state::AppState;
94+
```
95+
96+
## Add Initial `compose.yml`
97+
98+
Add a new file `book_service/compose.yml`.
99+
100+
```yaml
101+
services:
102+
103+
db:
104+
image: postgres:18-alpine
105+
container_name: book-service-db
106+
environment:
107+
POSTGRES_DB: book-service-db
108+
POSTGRES_USER: book-service-user
109+
POSTGRES_PASSWORD: book-service-pass
110+
ports:
111+
- "5432:5432"
112+
healthcheck:
113+
test: [ "CMD-SHELL", "pg_isready -U book-service-user -d book-service-db" ]
114+
interval: 3s
115+
timeout: 5s
116+
retries: 5
117+
restart: always
118+
```
119+
120+
## Database Configuration
121+
122+
### Update `.env`
123+
124+
Update `book_service/.env` to add environment variables, that are related to database configuration.
125+
126+
```env
127+
DB_PROTOCOL=postgres
128+
DB_HOST=db
129+
DB_PORT=5432
130+
DB_USER=book-service-user
131+
DB_PASS=book-service-pass
132+
DB_NAME=book-service-db
133+
```
134+
135+
### Update `config.rs`
136+
137+
Update `book_service/src/config.rs` to add `DbConf`.
138+
139+
```rust
140+
use envconfig::Envconfig;
141+
#[derive(Envconfig)]
142+
pub struct AppConf {
143+
#[envconfig(nested)]
144+
pub server: ServerConf,
145+
146+
#[envconfig(nested)]
147+
pub db: DbConf,
148+
}
149+
150+
// impl AppConf and ServerConf
151+
152+
#[derive(Envconfig)]
153+
pub struct DbConf {
154+
#[envconfig(from = "DB_PROTOCOL", default = "postgres")]
155+
pub protocol: String,
156+
#[envconfig(from = "DB_HOST")]
157+
pub host: String,
158+
#[envconfig(from = "DB_PORT", default = "5432")]
159+
pub port: u16,
160+
#[envconfig(from = "DB_USER")]
161+
pub user: String,
162+
#[envconfig(from = "DB_PASS")]
163+
pub password: String,
164+
#[envconfig(from = "DB_NAME")]
165+
pub db_name: String,
166+
}
167+
168+
impl DbConf {
169+
pub fn init() -> Self {
170+
Self::init_from_env().expect("Failed to load configuration! Check the .env file.")
171+
}
172+
173+
pub fn to_database_url(&self) -> String {
174+
format!(
175+
"{}://{}:{}@{}:{}/{}",
176+
self.protocol, self.user, self.password, self.host, self.port, self.db_name
177+
)
178+
}
179+
}
180+
```
181+
182+
### Re-exporting `DbConf` Struct
183+
184+
Update `book_service/src/lib.rs` to re-export `DbConf`.
185+
186+
```rust
187+
mod config;
188+
mod state;
189+
190+
pub mod models;
191+
pub mod routes;
192+
193+
pub use config::{AppConf, DbConf, ServerConf};
194+
pub use state::AppState;
195+
```
196+
197+
## Migration CLI
198+
199+
### Add Migration CLI
200+
201+
Add a new file `book_service/src/bin/migration.rs`.
202+
203+
```rust
204+
use book_service::{DbConf, models::*};
205+
use toasty_cli::{Config, ToastyCli};
206+
207+
#[tokio::main]
208+
async fn main() -> anyhow::Result<()> {
209+
let db_url = DbConf::init().to_database_url();
210+
let config = Config::load()?;
211+
let db = toasty::Db::builder().models(toasty::models!(Book)).connect(&db_url).await?;
212+
213+
let cli = ToastyCli::with_config(db, config);
214+
215+
cli.parse_and_run().await?;
216+
217+
Ok(())
218+
}
219+
```
220+
221+
Update `book_service/Cargo.toml` to add the migration CLI `[[bin]]`.
222+
223+
```toml
224+
[[bin]]
225+
name = "migration"
226+
path = "src/bin/migration.rs"
227+
```
228+
229+
### Add `Toasty.toml`
230+
231+
Add a new file `book_service/Toasty.toml`.
232+
233+
```toml
234+
[migration]
235+
path = "toasty"
236+
prefix_style = "Sequential"
237+
checksums = false
238+
statement_breakpoints = true
239+
```
240+
241+
## Update justfile
242+
243+
Update `book_service/justfile` to add `migration` and `docker` commands.
244+
245+
```justfile
246+
export CARGO_TARGET_DIR := "../../target"
247+
248+
db_host := "localhost"
249+
250+
## help, lint, check, ..., app
251+
252+
# Run DB migration CLI
253+
migration *cmd:
254+
@export $(grep -v '^#' .env | xargs) && \
255+
DB_HOST={{ db_host }} \
256+
cargo run --bin migration -- migration {{ cmd }}
257+
258+
# Run docker commands
259+
docker *cmd:
260+
docker {{ cmd }}
261+
```
262+
263+
## Generate & Apply DB Migrations
264+
265+
> [!tip]
266+
> We need to run the local database first. Then, run the migration CLI commands.
267+
> ```
268+
> Commands:
269+
> apply Apply pending migrations to the database
270+
> generate Generate a new migration based on schema changes
271+
> snapshot Print the current schema snapshot file
272+
> drop Drop a migration from the history
273+
> reset Reset the database (drop all tables) and optionally re-apply migrations
274+
> help Print this message or the help of the given subcommand(s)
275+
> ```
276+
277+
Due to our justfiles, we can run the following commands from the project root:
278+
279+
- `just book docker compose up`: to start the local database
280+
- `just book migration`: to see the available commands
281+
- `just book migration generate`: to generate database migrations
282+
- `just book migration apply`: to apply database migrations
283+
284+
`just book migration generate` command generates a new folder, `book_service/toasty`, with the following files.
285+
286+
{{< tabs count=3 label="Select File" >}}
287+
{{< tab label="history.toml" >}}
288+
```toml {title="crates/book_service/toasty/history.toml"}
289+
version = 1
290+
291+
[[migrations]]
292+
id = 1234567890123456789
293+
name = "0000_migration.sql"
294+
snapshot_name = "0000_snapshot.toml"
295+
```
296+
{{< /tab >}}
297+
{{< tab label="0000_migration.sql" >}}
298+
```sql {title="crates/book_service/toasty/migrations/0000_migration.sql"}
299+
CREATE TABLE "books" (
300+
"created_at" TIMESTAMPTZ(6) NOT NULL,
301+
"updated_at" TIMESTAMPTZ(6) NOT NULL,
302+
"id" UUID NOT NULL,
303+
"published_date" DATE NOT NULL,
304+
"status" SMALLINT NOT NULL,
305+
"title" TEXT NOT NULL,
306+
"description" TEXT,
307+
"image_url" TEXT,
308+
PRIMARY KEY ("id")
309+
);
310+
```
311+
{{< /tab >}}
312+
{{< tab label="0000_snapshot.toml" >}}
313+
```toml {title="crates/book_service/toasty/snapshots/0000_snapshot.toml"}
314+
version = 1
315+
316+
[schema]
317+
318+
[[schema.tables]]
319+
id = 0
320+
name = "books"
321+
primary_key = { columns = [{ table = 0, index = 2 }], index = { table = 0, index = 0 } }
322+
323+
[[schema.tables.columns]]
324+
id = { table = 0, index = 0 }
325+
name = "created_at"
326+
ty = "Timestamp"
327+
storage_ty = { Timestamp = 6 }
328+
329+
[[schema.tables.columns]]
330+
id = { table = 0, index = 1 }
331+
name = "updated_at"
332+
ty = "Timestamp"
333+
storage_ty = { Timestamp = 6 }
334+
335+
[[schema.tables.columns]]
336+
id = { table = 0, index = 2 }
337+
name = "id"
338+
ty = "Uuid"
339+
storage_ty = "Uuid"
340+
341+
[[schema.tables.columns]]
342+
id = { table = 0, index = 3 }
343+
name = "published_date"
344+
ty = "Date"
345+
storage_ty = "Date"
346+
347+
[[schema.tables.columns]]
348+
id = { table = 0, index = 4 }
349+
name = "status"
350+
ty = "U8"
351+
storage_ty = { UnsignedInteger = 1 }
352+
353+
[[schema.tables.columns]]
354+
id = { table = 0, index = 5 }
355+
name = "title"
356+
ty = "String"
357+
storage_ty = "Text"
358+
359+
[[schema.tables.columns]]
360+
id = { table = 0, index = 6 }
361+
name = "description"
362+
ty = "String"
363+
storage_ty = "Text"
364+
nullable = true
365+
366+
[[schema.tables.columns]]
367+
id = { table = 0, index = 7 }
368+
name = "image_url"
369+
ty = "String"
370+
storage_ty = "Text"
371+
nullable = true
372+
373+
[[schema.tables.indices]]
374+
id = { table = 0, index = 0 }
375+
name = "index_books_by_id"
376+
on = 0
377+
columns = [{ column = { table = 0, index = 2 }, op = "Eq", scope = "Partition" }]
378+
unique = true
379+
primary_key = true
380+
```
381+
{{< /tab >}}
382+
{{< /tabs >}}
383+
384+
Run `just book migration apply` to apply database migrations to the local database.

‎docs/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!doctype html><html lang=en-US><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=description content="Rust Programming Language Tutorials for Everyone!"><meta name=author content="Dumindu Madunuwan"><meta name=theme-color content="#ffffff" media="(prefers-color-scheme: light)"><meta name=theme-color content="#101010" media="(prefers-color-scheme: dark)"><title>Learning Rust · Rust Programming Language Tutorials for Everyone!</title><link rel=canonical href=https://learning-rust.github.io/><link rel=stylesheet href=/assets/css/home.min.2f64b534694f7783ee00c07c45f130ef260d59af33aefe9e41a6606a53c5de38.css integrity><link href=https://learning-rust.github.io/pagefind/pagefind-component-ui.css rel=stylesheet><link rel=manifest href=/manifest.json><link rel=icon href=/favicon/favicon.ico><link rel=icon href=/favicon/favicon-16x16.png sizes=16x16 type=image/png><link rel=icon href=/favicon/favicon-32x32.png sizes=32x32 type=image/png><link rel=apple-touch-icon href=/favicon/apple-touch-icon.png sizes=180x180><script async src="https://www.googletagmanager.com/gtag/js?id=G-FZHQCXSZ89"></script><script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}gtag("js",new Date),gtag("config","G-FZHQCXSZ89")</script><meta property="og:title" content="Learning Rust"><meta property="og:description" content="Learning Rust - Rust Programming Language Tutorials for Everyone!"><meta property="og:type" content="article"><meta property="og:url" content="https://learning-rust.github.io/"><meta property="og:image" content="https://learning-rust.github.io/og.jpg"><meta property="og:site_name" content="Learning Rust"><meta name=twitter:card content="summary_large_image"><meta name=twitter:title content="Learning Rust"><meta name=twitter:description content="Learning Rust - Rust Programming Language Tutorials for Everyone!"><meta name=twitter:image content="https://learning-rust.github.io/og.jpg"><script type=application/ld+json>{"@context":"https://schema.org","@type":"Article","headline":"Learning Rust","description":"Learning Rust - Rust Programming Language Tutorials for Everyone!","image":"https:\/\/learning-rust.github.io\/og.jpg","author":{"@type":"Person","name":"Dumindu Madunuwan","url":"https:\/\/github.com\/dumindu"},"publisher":{"@type":"Organization","name":"Learning Rust","logo":{"@type":"ImageObject","url":"https:\/\/learning-rust.github.io\/"}},"dateModified":"2026-08-14T17:29:20Z","mainEntityOfPage":{"@type":"WebPage","@id":"https:\/\/learning-rust.github.io\/"}}</script></head><body><div id=content-wrapper><header id=site-header><div id=site-header-logo><a href=https://learning-rust.github.io/><img height=52px src=https://learning-rust.github.io/logo.svg alt="Site Logo"></a></div><div id=site-header-actions><div id=search-box><pagefind-modal-trigger compact=true></pagefind-modal-trigger><pagefind-modal></pagefind-modal></div><div id=theme-dropdown class=dropdown><button class=dropdown-btn aria-haspopup=menu aria-label="Select the theme">
1+
<!doctype html><html lang=en-US><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=description content="Rust Programming Language Tutorials for Everyone!"><meta name=author content="Dumindu Madunuwan"><meta name=theme-color content="#ffffff" media="(prefers-color-scheme: light)"><meta name=theme-color content="#101010" media="(prefers-color-scheme: dark)"><title>Learning Rust · Rust Programming Language Tutorials for Everyone!</title><link rel=canonical href=https://learning-rust.github.io/><link rel=stylesheet href=/assets/css/home.min.2f64b534694f7783ee00c07c45f130ef260d59af33aefe9e41a6606a53c5de38.css integrity><link href=https://learning-rust.github.io/pagefind/pagefind-component-ui.css rel=stylesheet><link rel=manifest href=/manifest.json><link rel=icon href=/favicon/favicon.ico><link rel=icon href=/favicon/favicon-16x16.png sizes=16x16 type=image/png><link rel=icon href=/favicon/favicon-32x32.png sizes=32x32 type=image/png><link rel=apple-touch-icon href=/favicon/apple-touch-icon.png sizes=180x180><script async src="https://www.googletagmanager.com/gtag/js?id=G-FZHQCXSZ89"></script><script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}gtag("js",new Date),gtag("config","G-FZHQCXSZ89")</script><meta property="og:title" content="Learning Rust"><meta property="og:description" content="Learning Rust - Rust Programming Language Tutorials for Everyone!"><meta property="og:type" content="article"><meta property="og:url" content="https://learning-rust.github.io/"><meta property="og:image" content="https://learning-rust.github.io/og.jpg"><meta property="og:site_name" content="Learning Rust"><meta name=twitter:card content="summary_large_image"><meta name=twitter:title content="Learning Rust"><meta name=twitter:description content="Learning Rust - Rust Programming Language Tutorials for Everyone!"><meta name=twitter:image content="https://learning-rust.github.io/og.jpg"><script type=application/ld+json>{"@context":"https://schema.org","@type":"Article","headline":"Learning Rust","description":"Learning Rust - Rust Programming Language Tutorials for Everyone!","image":"https:\/\/learning-rust.github.io\/og.jpg","author":{"@type":"Person","name":"Dumindu Madunuwan","url":"https:\/\/github.com\/dumindu"},"publisher":{"@type":"Organization","name":"Learning Rust","logo":{"@type":"ImageObject","url":"https:\/\/learning-rust.github.io\/"}},"dateModified":"2026-08-14T17:33:05Z","mainEntityOfPage":{"@type":"WebPage","@id":"https:\/\/learning-rust.github.io\/"}}</script></head><body><div id=content-wrapper><header id=site-header><div id=site-header-logo><a href=https://learning-rust.github.io/><img height=52px src=https://learning-rust.github.io/logo.svg alt="Site Logo"></a></div><div id=site-header-actions><div id=search-box><pagefind-modal-trigger compact=true></pagefind-modal-trigger><pagefind-modal></pagefind-modal></div><div id=theme-dropdown class=dropdown><button class=dropdown-btn aria-haspopup=menu aria-label="Select the theme">
22
<span><svg class="icon" height="20" viewBox="0 -960 960 960" width="20"><path d="M480-96q-79 0-149-30t-122.5-82.5T126-331 96-480q0-80 30.5-149.5t84-122 125-82.5T488-864q78 0 146.5 27T754-763t80.5 110T864-518q0 96-67 163t-163 67h-68q-8 0-14 5t-6 13q0 15 15 25t15 53q0 37-27 66.5T480-96zm0-384zm-173.5 18.5Q324-479 324-504t-17.5-42.5T264-564t-42.5 17.5T204-504t17.5 42.5T264-444t42.5-17.5zm120-144Q444-623 444-648t-17.5-42.5T384-708t-42.5 17.5T324-648t17.5 42.5T384-588t42.5-17.5zm192 0Q636-623 636-648t-17.5-42.5T576-708t-42.5 17.5T516-648t17.5 42.5T576-588t42.5-17.5zm120 144Q756-479 756-504t-17.5-42.5T696-564t-42.5 17.5T636-504t17.5 42.5T696-444t42.5-17.5zM480-168q11 0 17.5-8.5T504-192q0-16-15-28t-15-50 26.5-64 64.5-26h69q66 0 112-46t46-112q0-115-88.5-194.5T488-792q-134 0-227 91t-93 221 91 221 221 91z"/></svg>
33
</span><span><svg class="icon" height="20" viewBox="0 -960 960 960" width="20"><path d="m480-246 85.91-85.91Q577-343 591.5-343t25.5 11 11 25.5-10.93 25.43L505-169q-5.4 5-11.7 7.5t-13.5 2.5-13.5-2.5T455-169L342.93-281.07Q332-292 332.5-306.5T344-332t25.5-11 25.47 11.09L480-246zm0-468-85.91 85.91Q383-617 368.5-617T343-628t-11-25.5 10.93-25.43L455-791q5.4-5 11.7-7.5t13.5-2.5 13.5 2.5T505-791l112.07 112.07Q628-668 628-654t-11 25-25.5 11-25.59-10.97L480-714z"/></svg></span></button><ul role=menu class=dropdown-menu><li role=menuitem><button title=Light class=color-scheme data-value=light style=background:#fff;color:#000>
44
<span style=color:#000>Aa</span></button></li><li role=menuitem><button title=dark class=color-scheme data-value=dark style=background:#101010;color:#fff>

0 commit comments

Comments
 (0)