Skip to content

Commit 7b6bd1d

Browse files
author
Glubus
committed
feat: macros to gen insert and by_id
1 parent b3b0bf8 commit 7b6bd1d

18 files changed

Lines changed: 276 additions & 271 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "db"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
edition = "2021"
55

66
[dependencies]

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
pub mod macros;
12
pub mod models;
23
pub mod utils;

src/macros.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//! Reusable macros to generate common SQLx queries without boilerplate.
2+
//!
3+
//! These macros leverage `sqlx::QueryBuilder` so we don't need fully literal SQL
4+
//! at compile-time (which `query!`/`query_as!` require).
5+
6+
/// Define an async function that fetches a row by its `id` using `sqlx::QueryBuilder`.
7+
///
8+
/// Parameters:
9+
/// - `fn_name`: function name to generate
10+
/// - `table_lit`: table name as a string literal, e.g. "score_metadata"
11+
/// - `RowType`: fully-qualified row type path
12+
/// - `columns_lit`: columns to select as a string literal
13+
///
14+
/// Example:
15+
/// define_by_id!(find_by_id, "score_metadata", crate::models::score_metadata::types::ScoreMetadataRow,
16+
/// "id, skin, pause_count, started_at, ended_at, time_paused, score, accuracy, max_combo, perfect, count_300, count_100, count_50, count_miss, count_katu, count_geki, created_at");
17+
#[macro_export]
18+
macro_rules! define_by_id {
19+
( $fn_name:ident, $table_lit:literal, $RowType:path, $columns_lit:literal ) => {
20+
pub async fn $fn_name(
21+
pool: &sqlx::PgPool,
22+
id: i32,
23+
) -> Result<Option<$RowType>, sqlx::Error> {
24+
let mut builder = sqlx::QueryBuilder::<sqlx::Postgres>::new("SELECT ");
25+
builder.push($columns_lit);
26+
builder.push(" FROM ");
27+
builder.push($table_lit);
28+
builder.push(" WHERE id = ");
29+
builder.push_bind(id);
30+
builder
31+
.build_query_as::<$RowType>()
32+
.fetch_optional(pool)
33+
.await
34+
}
35+
};
36+
}
37+
38+
/// Define an async INSERT function that returns the auto-generated `id` (i32).
39+
///
40+
/// Parameters:
41+
/// - `fn_name`: function name to generate
42+
/// - `table_lit`: table name as a string literal
43+
/// - `RowType`: fully-qualified row type path of the input struct
44+
/// - field list: one or more identifiers which are taken from `row.field` in order
45+
///
46+
/// Example:
47+
/// define_insert_returning_id!(insert, "score_metadata", crate::models::score_metadata::types::ScoreMetadataRow,
48+
/// skin, pause_count, started_at, ended_at, time_paused, score, accuracy, max_combo, perfect, count_300, count_100, count_50, count_miss, count_katu, count_geki);
49+
#[macro_export]
50+
macro_rules! define_insert_returning_id {
51+
( $fn_name:ident, $table_lit:literal, $RowType:path, $($field:ident),+ $(,)? ) => {
52+
pub async fn $fn_name(
53+
pool: &sqlx::PgPool,
54+
row: $RowType,
55+
) -> Result<i32, sqlx::Error> {
56+
let mut builder = sqlx::QueryBuilder::<sqlx::Postgres>::new("INSERT INTO ");
57+
builder.push($table_lit);
58+
builder.push(" (");
59+
builder.push( stringify!($($field),+) );
60+
builder.push(", created_at) VALUES (");
61+
let mut separated = builder.separated(", ");
62+
$( separated.push_bind(row.$field); )+
63+
separated.push("NOW()");
64+
// let `separated` drop naturally; explicit drop is unnecessary
65+
builder.push(") RETURNING id");
66+
67+
let rec = builder.build().fetch_one(pool).await?;
68+
use sqlx::Row;
69+
let id: i32 = rec.try_get("id")?;
70+
Ok(id)
71+
}
72+
};
73+
}
74+
75+
// Internal helper to generate a placeholder list like "$1, $2, $3"
76+
// Note: sqlx::query! requires a fully literal SQL string. Building a dynamic
77+
// placeholder list within concat! isn't straightforward in macro_rules without
78+
// counting. Therefore, for portability across many Rust versions and to keep
79+
// things simple, prefer the `define_insert_returning_row!` variant where the
80+
// caller provides the RETURNING columns list. For returning id, we will emit a
81+
// concrete, explicit SQL through another macro.
82+
83+
/// Define an async INSERT function that returns the full row using `QueryBuilder`.
84+
/// The caller provides the explicit RETURNING columns list literal.
85+
///
86+
/// Parameters:
87+
/// - `fn_name`: function name to generate
88+
/// - `table_lit`: table name as a string literal
89+
/// - `RowType`: fully-qualified row type path to be returned
90+
/// - field list: one or more identifiers which are taken from `row.field` in order
91+
/// - `;` then `returning_columns_lit`: string literal of columns to return
92+
///
93+
/// Example:
94+
/// define_insert_returning_row!(insert, "score", crate::models::score::types::ScoreRow,
95+
/// user_id, beatmap_id, score_metadata_id, replay_id, rate, hwid, mods, hash, rank, status;
96+
/// "id, user_id, beatmap_id, score_metadata_id, replay_id, rate, hwid, mods, hash, rank, status, created_at");
97+
#[macro_export]
98+
macro_rules! define_insert_returning_row {
99+
( $fn_name:ident, $table_lit:literal, $RowType:path, $($field:ident),+ ; $returning_columns_lit:literal ) => {
100+
pub async fn $fn_name(
101+
pool: &sqlx::PgPool,
102+
row: $RowType,
103+
) -> Result<$RowType, sqlx::Error> {
104+
let mut builder = sqlx::QueryBuilder::<sqlx::Postgres>::new("INSERT INTO ");
105+
builder.push($table_lit);
106+
builder.push(" (");
107+
builder.push( stringify!($($field),+) );
108+
builder.push(", created_at) VALUES (");
109+
let mut separated = builder.separated(", ");
110+
$( separated.push_bind(row.$field); )+
111+
separated.push("NOW()");
112+
// let `separated` drop naturally; explicit drop is unnecessary
113+
builder.push(") RETURNING ");
114+
builder.push($returning_columns_lit);
115+
116+
builder
117+
.build_query_as::<$RowType>()
118+
.fetch_one(pool)
119+
.await
120+
}
121+
};
122+
}

src/models/beatmap/impl.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::query::{exists_by_checksum, find_by_id, get_beatmapset_id, insert::insert};
1+
use super::query::{exists_by_checksum, find_by_beatmapset_id, find_by_id, insert::insert};
22
use super::types::BeatmapRow;
33
use sqlx::PgPool;
44

@@ -15,10 +15,10 @@ impl BeatmapRow {
1515
exists_by_checksum(pool, checksum).await
1616
}
1717

18-
pub async fn get_beatmapset_id(
18+
pub async fn find_by_beatmapset_id(
1919
pool: &PgPool,
2020
beatmap_id: i32,
2121
) -> Result<Option<i32>, sqlx::Error> {
22-
get_beatmapset_id(pool, beatmap_id).await
22+
find_by_beatmapset_id(pool, beatmap_id).await
2323
}
2424
}

src/models/beatmap/query/by_id.rs

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
1+
use crate::define_by_id;
12
use crate::models::beatmap::types::BeatmapRow;
23
use sqlx::{Error as SqlxError, PgPool};
34

4-
pub async fn find_by_id(pool: &PgPool, id: i32) -> Result<Option<BeatmapRow>, SqlxError> {
5-
sqlx::query_as!(
6-
BeatmapRow,
7-
r#"
8-
SELECT * FROM beatmap WHERE id = $1
9-
"#,
10-
id
11-
)
12-
.fetch_optional(pool)
13-
.await
14-
}
5+
define_by_id!(find_by_id, "beatmap", BeatmapRow,
6+
"id, osu_id, beatmapset_id, difficulty, difficulty_rating, count_circles,
7+
count_sliders, count_spinners, max_combo, drain_time, total_time, bpm, cs,
8+
ar, od, hp, mode, status, file_md5, file_path, created_at");
159

16-
pub async fn get_beatmapset_id(pool: &PgPool, beatmap_id: i32) -> Result<Option<i32>, SqlxError> {
17-
let row = sqlx::query!(
10+
pub async fn find_by_beatmapset_id(pool: &PgPool, id: i32) -> Result<Option<i32>, SqlxError> {
11+
Ok(sqlx::query!(
1812
"SELECT beatmapset_id FROM beatmap WHERE id = $1",
19-
beatmap_id
13+
id
2014
)
2115
.fetch_optional(pool)
22-
.await?;
23-
Ok(row.and_then(|r| r.beatmapset_id))
24-
}
16+
.await?
17+
.and_then(|r| r.beatmapset_id)
18+
)
19+
}

src/models/beatmap/query/insert.rs

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,27 @@
1+
use crate::define_insert_returning_id;
12
use crate::models::beatmap::types::BeatmapRow;
2-
use sqlx::{Error as SqlxError, PgPool};
33

4-
pub async fn insert(pool: &PgPool, beatmap: BeatmapRow) -> Result<i32, SqlxError> {
5-
Ok(sqlx::query!(
6-
r#"
7-
INSERT INTO beatmap (
8-
osu_id, beatmapset_id, difficulty, difficulty_rating,
9-
count_circles, count_sliders, count_spinners, max_combo,
10-
drain_time, total_time, bpm, cs, ar, od, hp, mode,
11-
status, file_md5, file_path
12-
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19)
13-
RETURNING id
14-
"#,
15-
beatmap.osu_id,
16-
beatmap.beatmapset_id,
17-
beatmap.difficulty,
18-
beatmap.difficulty_rating,
19-
beatmap.count_circles,
20-
beatmap.count_sliders,
21-
beatmap.count_spinners,
22-
beatmap.max_combo,
23-
beatmap.drain_time,
24-
beatmap.total_time,
25-
beatmap.bpm,
26-
beatmap.cs,
27-
beatmap.ar,
28-
beatmap.od,
29-
beatmap.hp,
30-
beatmap.mode,
31-
beatmap.status,
32-
beatmap.file_md5,
33-
beatmap.file_path
34-
)
35-
.fetch_one(pool)
36-
.await?
37-
.id)
38-
}
4+
define_insert_returning_id!(
5+
insert,
6+
"beatmap",
7+
BeatmapRow,
8+
osu_id,
9+
beatmapset_id,
10+
difficulty,
11+
difficulty_rating,
12+
count_circles,
13+
count_sliders,
14+
count_spinners,
15+
max_combo,
16+
drain_time,
17+
total_time,
18+
bpm,
19+
cs,
20+
ar,
21+
od,
22+
hp,
23+
mode,
24+
status,
25+
file_md5,
26+
file_path
27+
);

src/models/beatmap/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use super::validators::{
77
validate_od, validate_status,
88
};
99

10-
#[derive(Clone, Debug, Validate)]
10+
#[derive(Clone, Debug, sqlx::FromRow, Validate)]
1111
pub struct BeatmapRow {
1212
#[validate(range(min = 1))]
1313
pub id: i32,
Lines changed: 30 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,35 @@
11
use crate::models::beatmapset::types::BeatmapsetRow;
2+
use sqlx::QueryBuilder;
23
use sqlx::{Error as SqlxError, PgPool};
34

45
pub async fn insert(pool: &PgPool, beatmapset: BeatmapsetRow) -> Result<i32, SqlxError> {
5-
Ok(sqlx::query!(
6-
r#"
7-
INSERT INTO beatmapset (
8-
osu_id, artist, artist_unicode, title, title_unicode, creator, source,
9-
tags, has_video, has_storyboard, is_explicit, is_featured,
10-
cover_url, preview_url, osu_file_url
11-
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
12-
ON CONFLICT (osu_id) DO UPDATE SET
13-
artist = EXCLUDED.artist,
14-
artist_unicode = EXCLUDED.artist_unicode,
15-
title = EXCLUDED.title,
16-
title_unicode = EXCLUDED.title_unicode,
17-
creator = EXCLUDED.creator,
18-
source = EXCLUDED.source,
19-
tags = EXCLUDED.tags,
20-
has_video = EXCLUDED.has_video,
21-
has_storyboard = EXCLUDED.has_storyboard,
22-
is_explicit = EXCLUDED.is_explicit,
23-
is_featured = EXCLUDED.is_featured,
24-
cover_url = EXCLUDED.cover_url,
25-
preview_url = EXCLUDED.preview_url,
26-
osu_file_url = EXCLUDED.osu_file_url,
27-
updated_at = now()
28-
RETURNING id
29-
"#,
30-
beatmapset.osu_id,
31-
beatmapset.artist,
32-
beatmapset.artist_unicode.as_deref(),
33-
beatmapset.title,
34-
beatmapset.title_unicode.as_deref(),
35-
beatmapset.creator,
36-
beatmapset.source.as_deref(),
37-
beatmapset.tags.as_deref(),
38-
beatmapset.has_video,
39-
beatmapset.has_storyboard,
40-
beatmapset.is_explicit,
41-
beatmapset.is_featured,
42-
beatmapset.cover_url.as_deref(),
43-
beatmapset.preview_url.as_deref(),
44-
beatmapset.osu_file_url.as_deref()
45-
)
46-
.fetch_one(pool)
47-
.await?
48-
.id)
6+
let mut builder = QueryBuilder::<sqlx::Postgres>::new(
7+
"INSERT INTO beatmapset (osu_id, artist, artist_unicode, title, title_unicode, creator, source, tags, has_video, has_storyboard, is_explicit, is_featured, cover_url, preview_url, osu_file_url) VALUES ("
8+
);
9+
10+
let mut separated = builder.separated(", ");
11+
separated.push_bind(beatmapset.osu_id);
12+
separated.push_bind(beatmapset.artist);
13+
separated.push_bind(beatmapset.artist_unicode);
14+
separated.push_bind(beatmapset.title);
15+
separated.push_bind(beatmapset.title_unicode);
16+
separated.push_bind(beatmapset.creator);
17+
separated.push_bind(beatmapset.source);
18+
separated.push_bind(beatmapset.tags);
19+
separated.push_bind(beatmapset.has_video);
20+
separated.push_bind(beatmapset.has_storyboard);
21+
separated.push_bind(beatmapset.is_explicit);
22+
separated.push_bind(beatmapset.is_featured);
23+
separated.push_bind(beatmapset.cover_url);
24+
separated.push_bind(beatmapset.preview_url);
25+
separated.push_bind(beatmapset.osu_file_url);
26+
// separated will go out of scope here
27+
builder.push(") ON CONFLICT (osu_id) DO UPDATE SET ");
28+
builder.push(
29+
"artist = EXCLUDED.artist, artist_unicode = EXCLUDED.artist_unicode, title = EXCLUDED.title, title_unicode = EXCLUDED.title_unicode, creator = EXCLUDED.creator, source = EXCLUDED.source, tags = EXCLUDED.tags, has_video = EXCLUDED.has_video, has_storyboard = EXCLUDED.has_storyboard, is_explicit = EXCLUDED.is_explicit, is_featured = EXCLUDED.is_featured, cover_url = EXCLUDED.cover_url, preview_url = EXCLUDED.preview_url, osu_file_url = EXCLUDED.osu_file_url, updated_at = NOW() RETURNING id",
30+
);
31+
32+
let rec = builder.build().fetch_one(pool).await?;
33+
use sqlx::Row;
34+
rec.try_get("id")
4935
}

src/models/failed_query/impl.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ use super::FailedQueryRow;
33
use sqlx::{Error as SqlxError, PgPool};
44

55
impl FailedQueryRow {
6-
pub async fn insert(pool: &PgPool, hash: &str) -> Result<i32, SqlxError> {
7-
insert(pool, hash).await
6+
pub async fn insert(self, pool: &PgPool) -> Result<i32, SqlxError> {
7+
insert(pool, self).await
88
}
99

1010
pub async fn exists_by_hash(pool: &PgPool, hash: &str) -> Result<bool, SqlxError> {
Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,5 @@
1-
use sqlx::{Error as SqlxError, PgPool};
1+
use crate::define_insert_returning_id;
2+
use crate::models::failed_query::types::FailedQueryRow;
3+
// no extra imports needed
24

3-
pub async fn insert(pool: &PgPool, hash: &str) -> Result<i32, SqlxError> {
4-
Ok(sqlx::query!(
5-
r#"
6-
INSERT INTO failed_query (hash)
7-
VALUES ($1)
8-
RETURNING id
9-
"#,
10-
hash
11-
)
12-
.fetch_one(pool)
13-
.await?
14-
.id)
15-
}
5+
define_insert_returning_id!(insert, "failed_query", FailedQueryRow, hash);

0 commit comments

Comments
 (0)