Skip to content

Commit 032757f

Browse files
authored
feat(orm): postgres support (#70)
In addition to that, this also adds tests that use Postgres, and a way to run raw SQL queries. There's some opportunity to do some deduplication between Sqlite and Postgres code, but let's wait for it until MySQL support lands.
1 parent 38123d5 commit 032757f

File tree

18 files changed

+680
-109
lines changed

18 files changed

+680
-109
lines changed

.github/workflows/rust.yml

+40-1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,42 @@ jobs:
8181
- name: Run clippy
8282
run: cargo clippy --no-deps -- -Dclippy::all -Wclippy::pedantic
8383

84+
external-deps:
85+
if: github.event_name == 'push' || github.event_name == 'schedule' ||
86+
github.event.pull_request.head.repo.full_name != github.repository
87+
88+
name: Test with external dependencies
89+
runs-on: ubuntu-latest
90+
needs: ["build"]
91+
steps:
92+
- name: Checkout source
93+
uses: actions/checkout@v4
94+
95+
- name: Install Rust toolchain
96+
uses: dtolnay/rust-toolchain@master
97+
with:
98+
toolchain: nightly
99+
100+
- name: Cache Cargo registry
101+
uses: Swatinem/rust-cache@v2
102+
103+
- name: Install cargo-nextest
104+
uses: taiki-e/install-action@v2
105+
with:
106+
tool: nextest
107+
108+
- name: Build
109+
run: cargo build
110+
111+
- name: Run the external dependencies
112+
run: docker compose up -d
113+
114+
- name: Test
115+
run: cargo nextest run --all-features -- --include-ignored
116+
117+
- name: Test docs
118+
run: cargo test --doc
119+
84120
coverage:
85121
if: github.event_name == 'push' || github.event_name == 'schedule' ||
86122
github.event.pull_request.head.repo.full_name != github.repository
@@ -101,8 +137,11 @@ jobs:
101137
- name: Cache Cargo registry
102138
uses: Swatinem/rust-cache@v2
103139

140+
- name: Run the external dependencies
141+
run: docker compose up -d
142+
104143
- name: Test
105-
run: cargo test --all-features --no-fail-fast
144+
run: cargo test --all-features --no-fail-fast -- --include-ignored
106145
env:
107146
RUSTFLAGS: "-Cinstrument-coverage"
108147

Cargo.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,11 @@ rand = "0.8"
6363
regex = "1.11"
6464
rustversion = "1"
6565
sea-query = "0.32.0-rc.2"
66-
sea-query-binder = { version = "0.7.0-rc.2", features = ["sqlx-sqlite", "with-chrono", "runtime-tokio"] }
66+
sea-query-binder = { version = "0.7.0-rc.2", features = ["sqlx-sqlite", "sqlx-postgres", "with-chrono", "runtime-tokio"] }
6767
serde = "1"
6868
sha2 = "0.11.0-pre.4"
6969
slug = "0.1"
70-
sqlx = { version = "0.8", default-features = false, features = ["macros", "json", "runtime-tokio", "sqlite", "chrono"] }
70+
sqlx = { version = "0.8", default-features = false, features = ["macros", "json", "runtime-tokio", "sqlite", "postgres", "chrono"] }
7171
subtle = "2"
7272
syn = { version = "2", features = ["full", "extra-traits"] }
7373
sync_wrapper = "1"

README.md

+18
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,24 @@ built on top of [axum](https://github.com/tokio-rs/axum).
2828
* **Secure by default** — security should be opt-out, not opt-in. Flareon takes care of making your web apps secure by
2929
default, defending it against common modern web vulnerabilities. You can focus on building your app, not securing it.
3030

31+
## Development
32+
33+
### Testing
34+
35+
Tests that require using external databases are ignored by default. In order to run them, execute the following in the
36+
root of the repository:
37+
38+
```shell
39+
docker compose up -d
40+
cargo test --all-features -- --include-ignored
41+
```
42+
43+
You can them execute the following command to stop the database:
44+
45+
```shell
46+
docker compose down
47+
```
48+
3149
## License
3250

3351
Flareon is licensed under either of the following, at your option:

compose.yml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
services:
2+
postgres:
3+
image: postgres:16-alpine
4+
container_name: flareon-postgres
5+
restart: always
6+
environment:
7+
POSTGRES_USER: flareon
8+
POSTGRES_PASSWORD: flareon
9+
ports:
10+
- "5432:5432"

flareon-cli/src/migration_generator.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -540,13 +540,14 @@ impl AppState {
540540
///
541541
/// For instance, for `use std::collections::HashMap;` the `VisibleSymbol `
542542
/// would be:
543-
///
544543
/// ```ignore
544+
/// # /*
545545
/// VisibleSymbol {
546546
/// alias: "HashMap",
547547
/// full_path: "std::collections::HashMap",
548548
/// kind: VisibleSymbolKind::Use,
549549
/// }
550+
/// # */
550551
/// ```
551552
#[derive(Debug, Clone, PartialEq, Eq)]
552553
struct VisibleSymbol {

flareon-macros/src/dbtest.rs

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use proc_macro2::TokenStream;
2+
use quote::{format_ident, quote};
3+
use syn::ItemFn;
4+
5+
pub(super) fn fn_to_dbtest(test_function_decl: ItemFn) -> syn::Result<TokenStream> {
6+
let test_fn = &test_function_decl.sig.ident;
7+
let sqlite_ident = format_ident!("{}_sqlite", test_fn);
8+
let postgres_ident = format_ident!("{}_postgres", test_fn);
9+
10+
if test_function_decl.sig.inputs.len() != 1 {
11+
return Err(syn::Error::new_spanned(
12+
test_function_decl.sig.inputs,
13+
"Database test function must have exactly one argument",
14+
));
15+
}
16+
17+
let result = quote! {
18+
#[::tokio::test]
19+
async fn #sqlite_ident() {
20+
let mut database = flareon::test::TestDatabase::new_sqlite().await.unwrap();
21+
22+
#test_fn(&mut database).await;
23+
24+
database.cleanup().await.unwrap();
25+
26+
#test_function_decl
27+
}
28+
29+
#[ignore]
30+
#[::tokio::test]
31+
async fn #postgres_ident() {
32+
let mut database = flareon::test::TestDatabase::new_postgres(stringify!(#test_fn))
33+
.await
34+
.unwrap();
35+
36+
#test_fn(&mut database).await;
37+
38+
database.cleanup().await.unwrap();
39+
40+
#test_function_decl
41+
}
42+
};
43+
Ok(result)
44+
}

flareon-macros/src/lib.rs

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod dbtest;
12
mod form;
23
mod model;
34
mod query;
@@ -7,8 +8,9 @@ use darling::Error;
78
use proc_macro::TokenStream;
89
use proc_macro_crate::crate_name;
910
use quote::quote;
10-
use syn::parse_macro_input;
11+
use syn::{parse_macro_input, ItemFn};
1112

13+
use crate::dbtest::fn_to_dbtest;
1214
use crate::form::impl_form_for_struct;
1315
use crate::model::impl_model_for_struct;
1416
use crate::query::{query_to_tokens, Query};
@@ -112,6 +114,14 @@ pub fn query(input: TokenStream) -> TokenStream {
112114
query_to_tokens(query_input).into()
113115
}
114116

117+
#[proc_macro_attribute]
118+
pub fn dbtest(_args: TokenStream, input: TokenStream) -> TokenStream {
119+
let fn_input = parse_macro_input!(input as ItemFn);
120+
fn_to_dbtest(fn_input)
121+
.unwrap_or_else(syn::Error::into_compile_error)
122+
.into()
123+
}
124+
115125
pub(crate) fn flareon_ident() -> proc_macro2::TokenStream {
116126
let flareon_crate = crate_name("flareon").expect("flareon is not present in `Cargo.toml`");
117127
match flareon_crate {

flareon/src/auth.rs

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use std::sync::Arc;
1515
use async_trait::async_trait;
1616
use chrono::{DateTime, FixedOffset};
1717
use flareon::config::SecretKey;
18+
use flareon::db::impl_postgres::PostgresValueRef;
1819
#[cfg(test)]
1920
use mockall::automock;
2021
use password_auth::VerifyError;
@@ -410,6 +411,10 @@ impl FromDbValue for PasswordHash {
410411
fn from_sqlite(value: SqliteValueRef) -> flareon::db::Result<Self> {
411412
PasswordHash::new(value.get::<String>()?).map_err(flareon::db::DatabaseError::value_decode)
412413
}
414+
415+
fn from_postgres(value: PostgresValueRef) -> flareon::db::Result<Self> {
416+
PasswordHash::new(value.get::<String>()?).map_err(flareon::db::DatabaseError::value_decode)
417+
}
413418
}
414419

415420
impl ToDbValue for PasswordHash {

flareon/src/auth/db.rs

+5-4
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,14 @@ impl DatabaseUser {
8484
///
8585
/// # #[tokio::main]
8686
/// # async fn main() -> flareon::Result<()> {
87-
/// # use flareon::test::{TestDatabaseBuilder, TestRequestBuilder};
87+
/// # use flareon::test::{TestDatabase, TestRequestBuilder};
88+
/// # let mut test_database = TestDatabase::new_sqlite().await?;
89+
/// # test_database.with_auth().run_migrations().await;
8890
/// # let request = TestRequestBuilder::get("/")
89-
/// # .with_db_auth(std::sync::Arc::new(
90-
/// # TestDatabaseBuilder::new().with_auth().build().await,
91-
/// # ))
91+
/// # .with_db_auth(test_database.database())
9292
/// # .build();
9393
/// # view(&request).await?;
94+
/// # test_database.cleanup().await?;
9495
/// # Ok(())
9596
/// # }
9697
/// ```

0 commit comments

Comments
 (0)