1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//! Real `Db`-extractor proof for the `SQLite` runtime lane (issue #1614).
//!
//! The sibling `sqlite_boot_serve` test checks a connection out of the pool
//! **directly** (`pool.get()`), so it never runs the per-checkout initialization
//! SQL that the framework's [`autumn_web::db::Db`] extractor issues in
//! `Db::checkout`. That gap hid a Codex P1: `Db::checkout` unconditionally ran
//! the Postgres session GUC `SET statement_timeout = …`, which `SQLite` rejects —
//! turning **every** route that extracts `Db` into a 503 under `--features
//! sqlite`, even though a plain `pool.get()` worked fine.
//!
//! This test closes that gap by driving the **actual** `Db` extractor: a minimal
//! Axum app whose handler takes `Db` and runs a real query through it, served
//! over HTTP against a `sqlite:` target. It must answer **200** (not 503),
//! proving the checkout hot path no longer issues Postgres-only initialization
//! SQL on the `SQLite` backend.
//!
//! Run it explicitly (never via a members-enable edge — that would trip the
//! feature-unification hazard):
//!
//! ```sh
//! cargo test -p autumn-web --features sqlite --test sqlite_db_extractor
//! ```
#![cfg(feature = "sqlite")]
use autumn_web::config::DatabaseConfig;
use autumn_web::db::{Db, DbState, RuntimeConnection, create_pool};
use autumn_web::reexports::{axum, diesel, diesel_async};
use axum::Router;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use axum::routing::get;
use diesel_async::RunQueryDsl as _;
use diesel_async::pooled_connection::deadpool::Pool;
use tower::ServiceExt as _; // for `oneshot`
/// The runtime pool type. Under `--features sqlite` `RuntimeConnection` resolves
/// to `SyncConnectionWrapper<SqliteConnection>`.
type SqlitePool = Pool<RuntimeConnection>;
/// Minimal app state exposing the `SQLite` pool to the `Db` extractor.
#[derive(Clone)]
struct AppState {
pool: SqlitePool,
}
impl DbState for AppState {
fn pool(&self) -> Option<&SqlitePool> {
Some(&self.pool)
}
}
#[derive(diesel::QueryableByName)]
struct Greeting {
#[diesel(sql_type = diesel::sql_types::Text)]
message: String,
}
/// Answers `/greet` by reading the seeded row **through the real `Db`
/// extractor** — so this handler only runs if `Db::checkout` succeeded on the
/// `SQLite` backend (i.e. it did not issue Postgres-only initialization SQL).
async fn greet(mut db: Db) -> Result<String, StatusCode> {
let rows: Vec<Greeting> = diesel::sql_query("SELECT message FROM greetings WHERE id = 1")
.load(&mut *db)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
rows.into_iter()
.next()
.map(|g| g.message)
.ok_or(StatusCode::NOT_FOUND)
}
#[tokio::test]
async fn db_extractor_serves_a_sqlite_backed_request_with_200() {
// A tempfile-backed database so the seed and the served request observe the
// same data regardless of how deadpool recycles connections.
let tmp = tempfile::TempDir::new().expect("temp dir");
let db_path = tmp.path().join("extractor.db");
let url = format!("sqlite://{}", db_path.display());
// Build the real SQLite pool through the public `create_pool` path.
let config = DatabaseConfig {
url: Some(url),
..Default::default()
};
let pool: SqlitePool = create_pool(&config)
.expect("sqlite pool builds via the build_sqlite_pool path")
.expect("a url is configured");
// Apply a minimal schema and seed a row on a pooled connection.
{
let mut conn = pool.get().await.expect("checkout a sqlite connection");
diesel::sql_query("CREATE TABLE greetings (id INTEGER PRIMARY KEY, message TEXT NOT NULL)")
.execute(&mut *conn)
.await
.expect("create table on sqlite");
diesel::sql_query(
"INSERT INTO greetings (id, message) VALUES (1, 'hello via Db extractor')",
)
.execute(&mut *conn)
.await
.expect("seed row on sqlite");
}
// A minimal app whose handler extracts `Db` — this is the path that used to
// 503 under SQLite because `Db::checkout` ran `SET statement_timeout`.
let app: Router = Router::new()
.route("/greet", get(greet))
.with_state(AppState { pool });
let response = app
.oneshot(
Request::builder()
.uri("/greet")
.body(Body::empty())
.expect("build request"),
)
.await
.expect("router serves the request");
assert_eq!(
response.status(),
StatusCode::OK,
"the real `Db`-extractor route must be 200 under SQLite, not 503 from a \
Postgres-only `SET statement_timeout` in Db::checkout"
);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("read body");
assert_eq!(
&body[..],
b"hello via Db extractor",
"response body is the row SELECTed through the Db extractor"
);
}