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
//! Fail-closed boot guard for `database.statement_timeout` under the `SQLite`
//! backend (issue #1996, item 4).
//!
//! `SQLite` cannot enforce a per-statement wall-clock timeout through the async
//! connection wrapper: diesel's `SqliteConnection` exposes no
//! interrupt/progress-handler hook (nor the raw `sqlite3` handle) through
//! `SyncConnectionWrapper`, so a runaway query cannot be aborted mid-flight.
//! True parity is impossible, so — per the fail-closed rule — autumn-web refuses
//! to boot rather than silently ignore a configured guarantee it cannot honor:
//! `db::reject_sqlite_statement_timeout` (called from every pool-construction
//! entry point) turns a non-zero `statement_timeout` under the sqlite backend
//! into a loud, typed [`autumn_web::db::PoolError`] naming the config key.
//!
//! A `None` or zero timeout (the default) asks for no guarantee and boots
//! cleanly, so ordinary `SQLite` apps are unaffected.
//!
//! 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_statement_timeout_fail_closed
//! ```
#![cfg(feature = "sqlite")]
use std::time::Duration;
use autumn_web::config::DatabaseConfig;
use autumn_web::db::{PoolError, create_pool};
/// A non-zero `statement_timeout` under the sqlite backend must fail the boot
/// fast with a loud, specific, TYPED error — not a panic, and not a silent
/// no-op that pretends the guarantee holds.
#[test]
fn create_pool_rejects_configured_statement_timeout() {
let config = DatabaseConfig {
url: Some("sqlite::memory:".to_string()),
statement_timeout: Some(Duration::from_secs(30)),
..Default::default()
};
let Err(err) = create_pool(&config) else {
panic!("create_pool must fail-closed when statement_timeout is set under sqlite");
};
// A real typed error, not a panic-in-disguise.
assert!(
matches!(err, PoolError::UnsupportedBackend(_)),
"expected UnsupportedBackend, got: {err:?}"
);
let msg = err.to_string();
assert!(
msg.contains("database.statement_timeout"),
"error must name the config key: {msg}"
);
assert!(
msg.contains("SQLite"),
"error must mention the SQLite backend: {msg}"
);
assert!(
msg.contains("30000ms"),
"error should report the configured timeout in ms: {msg}"
);
}
/// The same rejection applies to the topology builder (primary + optional
/// replica), which is the boot path most apps actually take.
#[test]
fn create_topology_rejects_configured_statement_timeout() {
let config = DatabaseConfig {
url: Some("sqlite::memory:".to_string()),
statement_timeout: Some(Duration::from_millis(1500)),
..Default::default()
};
let Err(err) = autumn_web::db::create_topology(&config) else {
panic!("create_topology must fail-closed when statement_timeout is set under sqlite");
};
assert!(
matches!(err, PoolError::UnsupportedBackend(_)),
"expected UnsupportedBackend, got: {err:?}"
);
assert!(
err.to_string().contains("database.statement_timeout"),
"topology error must name the config key: {err}"
);
}
/// No configured timeout (the default) boots cleanly — the guard must never bite
/// an ordinary `SQLite` deployment.
#[test]
fn create_pool_without_statement_timeout_succeeds() {
let config = DatabaseConfig {
url: Some("sqlite::memory:".to_string()),
statement_timeout: None,
..Default::default()
};
let pool = create_pool(&config)
.expect("sqlite pool must build cleanly with no statement_timeout")
.expect("a url is configured");
// Building the pool is the assertion; drop it.
drop(pool);
}
/// An explicit zero timeout is the "disabled" convention (parity with
/// `ARROYO_RECORDING_RETENTION_DAYS=0` / the Postgres `statement_timeout = 0`
/// meaning) and must also boot cleanly.
#[test]
fn create_pool_with_zero_statement_timeout_succeeds() {
let config = DatabaseConfig {
url: Some("sqlite::memory:".to_string()),
statement_timeout: Some(Duration::from_secs(0)),
..Default::default()
};
let pool = create_pool(&config)
.expect("a zero statement_timeout means disabled and must build cleanly")
.expect("a url is configured");
drop(pool);
}