server: adopt sqlx-offline; convert db::idempotency to compile-checked SQL (ultra-fuzz Run #1 --deep Phase 4)
Mirror mt-db's playbook: enable the sqlx `macros` feature and commit the server's
.sqlx offline query cache (un-ignored like multithreaded/.sqlx), so the server's
compile-time-checked queries build without a live DB on every host and the Sando
gate. This also fixes a latent gap — the pre-existing db::email_signups query!
macros had no committed cache, so the crate couldn't build offline at all.
Convert db::idempotency (3 queries) from runtime sqlx::query() strings to
query!/query_as! macros validated against the real schema at build time. First
money-path module; the rest follow.
Gate: SQLX_OFFLINE=true build + lib clippy clean; idempotency workflow tests
(6) green offline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9 files changed,
+155 insertions,
-16 deletions
| 58 |
58 |
|
sando/daemon/releases/
|
| 59 |
59 |
|
sando/daemon/cargo-target/
|
| 60 |
60 |
|
|
| 61 |
|
- |
# mt-db uses sqlx compile-time macros; its offline query cache must be committed
|
| 62 |
|
- |
# so build hosts (and the Sando gate) compile without a live DB.
|
|
61 |
+ |
# mt-db and the server use sqlx compile-time macros; their offline query cache
|
|
62 |
+ |
# must be committed so build hosts (and the Sando gate) compile without a live DB.
|
| 63 |
63 |
|
!multithreaded/.sqlx/
|
|
64 |
+ |
!server/.sqlx/
|
|
1 |
+ |
{
|
|
2 |
+ |
"db_name": "PostgreSQL",
|
|
3 |
+ |
"query": "DELETE FROM idempotency_keys WHERE created_at < NOW() - INTERVAL '24 hours'",
|
|
4 |
+ |
"describe": {
|
|
5 |
+ |
"columns": [],
|
|
6 |
+ |
"parameters": {
|
|
7 |
+ |
"Left": []
|
|
8 |
+ |
},
|
|
9 |
+ |
"nullable": []
|
|
10 |
+ |
},
|
|
11 |
+ |
"hash": "45a5c6823b2107d40dae97ba132bc3e1f683e5af43f0b7186489b4ec1012e2e6"
|
|
12 |
+ |
}
|
|
1 |
+ |
{
|
|
2 |
+ |
"db_name": "PostgreSQL",
|
|
3 |
+ |
"query": "SELECT status_code, response_body FROM idempotency_keys WHERE key = $1 AND user_id = $2 AND method = $3 AND path = $4",
|
|
4 |
+ |
"describe": {
|
|
5 |
+ |
"columns": [
|
|
6 |
+ |
{
|
|
7 |
+ |
"ordinal": 0,
|
|
8 |
+ |
"name": "status_code",
|
|
9 |
+ |
"type_info": "Int2"
|
|
10 |
+ |
},
|
|
11 |
+ |
{
|
|
12 |
+ |
"ordinal": 1,
|
|
13 |
+ |
"name": "response_body",
|
|
14 |
+ |
"type_info": "Text"
|
|
15 |
+ |
}
|
|
16 |
+ |
],
|
|
17 |
+ |
"parameters": {
|
|
18 |
+ |
"Left": [
|
|
19 |
+ |
"Text",
|
|
20 |
+ |
"Uuid",
|
|
21 |
+ |
"Text",
|
|
22 |
+ |
"Text"
|
|
23 |
+ |
]
|
|
24 |
+ |
},
|
|
25 |
+ |
"nullable": [
|
|
26 |
+ |
false,
|
|
27 |
+ |
false
|
|
28 |
+ |
]
|
|
29 |
+ |
},
|
|
30 |
+ |
"hash": "5bb0f0d537f75635202a3acc37101cdb2327aedde7d0a110357b856687e36685"
|
|
31 |
+ |
}
|
|
1 |
+ |
{
|
|
2 |
+ |
"db_name": "PostgreSQL",
|
|
3 |
+ |
"query": "INSERT INTO idempotency_keys (key, user_id, method, path, status_code, response_body)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (key, user_id, method, path) DO NOTHING",
|
|
4 |
+ |
"describe": {
|
|
5 |
+ |
"columns": [],
|
|
6 |
+ |
"parameters": {
|
|
7 |
+ |
"Left": [
|
|
8 |
+ |
"Text",
|
|
9 |
+ |
"Uuid",
|
|
10 |
+ |
"Text",
|
|
11 |
+ |
"Text",
|
|
12 |
+ |
"Int2",
|
|
13 |
+ |
"Text"
|
|
14 |
+ |
]
|
|
15 |
+ |
},
|
|
16 |
+ |
"nullable": []
|
|
17 |
+ |
},
|
|
18 |
+ |
"hash": "615ef0e996461da6943ed45802bc1a98c0b7b8ccc1f7cfe50566e44f620928eb"
|
|
19 |
+ |
}
|
|
1 |
+ |
{
|
|
2 |
+ |
"db_name": "PostgreSQL",
|
|
3 |
+ |
"query": "\n INSERT INTO email_signups (email, source)\n VALUES ($1, $2)\n ON CONFLICT (email) DO UPDATE SET email = EXCLUDED.email\n RETURNING id\n ",
|
|
4 |
+ |
"describe": {
|
|
5 |
+ |
"columns": [
|
|
6 |
+ |
{
|
|
7 |
+ |
"ordinal": 0,
|
|
8 |
+ |
"name": "id",
|
|
9 |
+ |
"type_info": "Uuid"
|
|
10 |
+ |
}
|
|
11 |
+ |
],
|
|
12 |
+ |
"parameters": {
|
|
13 |
+ |
"Left": [
|
|
14 |
+ |
"Text",
|
|
15 |
+ |
"Varchar"
|
|
16 |
+ |
]
|
|
17 |
+ |
},
|
|
18 |
+ |
"nullable": [
|
|
19 |
+ |
false
|
|
20 |
+ |
]
|
|
21 |
+ |
},
|
|
22 |
+ |
"hash": "a515b612f62ccae3f613e6b3e6f4954a347f41cea6737150ca9736c5efed2594"
|
|
23 |
+ |
}
|
|
1 |
+ |
{
|
|
2 |
+ |
"db_name": "PostgreSQL",
|
|
3 |
+ |
"query": "SELECT COUNT(*) FROM email_signups",
|
|
4 |
+ |
"describe": {
|
|
5 |
+ |
"columns": [
|
|
6 |
+ |
{
|
|
7 |
+ |
"ordinal": 0,
|
|
8 |
+ |
"name": "count",
|
|
9 |
+ |
"type_info": "Int8"
|
|
10 |
+ |
}
|
|
11 |
+ |
],
|
|
12 |
+ |
"parameters": {
|
|
13 |
+ |
"Left": []
|
|
14 |
+ |
},
|
|
15 |
+ |
"nullable": [
|
|
16 |
+ |
null
|
|
17 |
+ |
]
|
|
18 |
+ |
},
|
|
19 |
+ |
"hash": "c0afc028ccde9b67ce6a8275cf0faad7672269a5d316464b82b2782bb137fc25"
|
|
20 |
+ |
}
|
|
1 |
+ |
{
|
|
2 |
+ |
"db_name": "PostgreSQL",
|
|
3 |
+ |
"query": "\n SELECT email, source,\n created_at as \"created_at: chrono::DateTime<chrono::Utc>\"\n FROM email_signups\n ORDER BY created_at DESC\n LIMIT 500\n ",
|
|
4 |
+ |
"describe": {
|
|
5 |
+ |
"columns": [
|
|
6 |
+ |
{
|
|
7 |
+ |
"ordinal": 0,
|
|
8 |
+ |
"name": "email",
|
|
9 |
+ |
"type_info": "Text"
|
|
10 |
+ |
},
|
|
11 |
+ |
{
|
|
12 |
+ |
"ordinal": 1,
|
|
13 |
+ |
"name": "source",
|
|
14 |
+ |
"type_info": "Varchar"
|
|
15 |
+ |
},
|
|
16 |
+ |
{
|
|
17 |
+ |
"ordinal": 2,
|
|
18 |
+ |
"name": "created_at: chrono::DateTime<chrono::Utc>",
|
|
19 |
+ |
"type_info": "Timestamptz"
|
|
20 |
+ |
}
|
|
21 |
+ |
],
|
|
22 |
+ |
"parameters": {
|
|
23 |
+ |
"Left": []
|
|
24 |
+ |
},
|
|
25 |
+ |
"nullable": [
|
|
26 |
+ |
false,
|
|
27 |
+ |
false,
|
|
28 |
+ |
false
|
|
29 |
+ |
]
|
|
30 |
+ |
},
|
|
31 |
+ |
"hash": "c21e33476fadb61eb586bb1b8399caa95b5e4850bc358711003edcc46abd12e9"
|
|
32 |
+ |
}
|
| 35 |
35 |
|
dotenvy = "0.15.7"
|
| 36 |
36 |
|
|
| 37 |
37 |
|
# Database
|
| 38 |
|
- |
sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres", "uuid", "chrono", "migrate"] }
|
|
38 |
+ |
sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres", "uuid", "chrono", "migrate", "macros"] }
|
| 39 |
39 |
|
uuid = { version = "1.22.0", features = ["v4", "serde"] }
|
| 40 |
40 |
|
chrono = { version = "0.4.44", features = ["serde"] }
|
| 41 |
41 |
|
|
| 21 |
21 |
|
method: &str,
|
| 22 |
22 |
|
path: &str,
|
| 23 |
23 |
|
) -> Result<Option<CachedResponse>> {
|
| 24 |
|
- |
let row = sqlx::query_as::<_, CachedResponse>(
|
|
24 |
+ |
let row = sqlx::query_as!(
|
|
25 |
+ |
CachedResponse,
|
| 25 |
26 |
|
"SELECT status_code, response_body FROM idempotency_keys WHERE key = $1 AND user_id = $2 AND method = $3 AND path = $4",
|
|
27 |
+ |
key,
|
|
28 |
+ |
user_id as UserId,
|
|
29 |
+ |
method,
|
|
30 |
+ |
path,
|
| 26 |
31 |
|
)
|
| 27 |
|
- |
.bind(key)
|
| 28 |
|
- |
.bind(user_id)
|
| 29 |
|
- |
.bind(method)
|
| 30 |
|
- |
.bind(path)
|
| 31 |
32 |
|
.fetch_optional(pool)
|
| 32 |
33 |
|
.await?;
|
| 33 |
34 |
|
|
| 46 |
47 |
|
status_code: u16,
|
| 47 |
48 |
|
response_body: &str,
|
| 48 |
49 |
|
) -> Result<()> {
|
| 49 |
|
- |
sqlx::query(
|
|
50 |
+ |
sqlx::query!(
|
| 50 |
51 |
|
r#"INSERT INTO idempotency_keys (key, user_id, method, path, status_code, response_body)
|
| 51 |
52 |
|
VALUES ($1, $2, $3, $4, $5, $6)
|
| 52 |
53 |
|
ON CONFLICT (key, user_id, method, path) DO NOTHING"#,
|
|
54 |
+ |
key,
|
|
55 |
+ |
user_id as UserId,
|
|
56 |
+ |
method,
|
|
57 |
+ |
path,
|
|
58 |
+ |
status_code as i16,
|
|
59 |
+ |
response_body,
|
| 53 |
60 |
|
)
|
| 54 |
|
- |
.bind(key)
|
| 55 |
|
- |
.bind(user_id)
|
| 56 |
|
- |
.bind(method)
|
| 57 |
|
- |
.bind(path)
|
| 58 |
|
- |
.bind(status_code as i16)
|
| 59 |
|
- |
.bind(response_body)
|
| 60 |
61 |
|
.execute(pool)
|
| 61 |
62 |
|
.await?;
|
| 62 |
63 |
|
|
| 66 |
67 |
|
/// Delete expired idempotency keys (older than 24 hours).
|
| 67 |
68 |
|
#[tracing::instrument(skip_all)]
|
| 68 |
69 |
|
pub async fn cleanup_expired(pool: &PgPool) -> Result<u64> {
|
| 69 |
|
- |
let result = sqlx::query(
|
|
70 |
+ |
let result = sqlx::query!(
|
| 70 |
71 |
|
"DELETE FROM idempotency_keys WHERE created_at < NOW() - INTERVAL '24 hours'",
|
| 71 |
72 |
|
)
|
| 72 |
73 |
|
.execute(pool)
|