Skip to content

Commit cc9f80f

Browse files
committed
fix(sqlx): 0.9 AssertSqlSafe, Json payloads, and CRUD generator compatibility
1 parent 8fe0f66 commit cc9f80f

4 files changed

Lines changed: 28 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
### Fixed
2424

2525
- **`rcgen` 0.14 HTTP/3 dev certs**: `generate_self_signed_cert` uses `CertifiedKey { cert, signing_key }` so `--all-features` / `http3-dev` builds pass CI again.
26+
- **SQLx 0.9 jobs + CRUD generator**: Postgres job backend uses `AssertSqlSafe` and `Json` payloads; `cargo rustapi generate crud` emits SQLx 0.9-compatible SQLite handlers.
2627
- **Security Audit** (`cargo audit`) passes on the current lockfile (no high-severity `quick-xml` advisories).
2728

2829
## [0.1.551] - 2026-07-05

crates/cargo-rustapi/src/commands/generate.rs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ async fn ensure_crud_dependencies() -> Result<()> {
230230
.any(|line| line.trim_start().starts_with("sqlx "))
231231
{
232232
deps_to_add.push(
233-
"sqlx = { version = \"0.8\", default-features = false, features = [\"runtime-tokio\", \"sqlite\", \"derive\"] }",
233+
"sqlx = { version = \"0.9\", default-features = false, features = [\"runtime-tokio\", \"sqlite\", \"derive\"] }",
234234
);
235235
}
236236

@@ -269,7 +269,7 @@ const DB_RS_HEADER: &str = r#"//! Database bootstrap for generated CRUD resource
269269
//! Default columns are the standard scaffold (`id`, `name`, `description`, timestamps).
270270
271271
use sqlx::sqlite::SqlitePoolOptions;
272-
use sqlx::SqlitePool;
272+
use sqlx::{AssertSqlSafe, SqlitePool};
273273
274274
/// Default CRUD column layout for generated resources (customize per model as needed).
275275
macro_rules! crud_columns {
@@ -311,7 +311,7 @@ async fn upsert_db_resource(table: &str, singular: &str) -> Result<()> {
311311
"{header}{module_block}\
312312
async fn ensure_table(pool: &SqlitePool, table: &str, columns: &str) -> Result<(), sqlx::Error> {{
313313
let schema = format!(\"CREATE TABLE IF NOT EXISTS {{}} ({{}})\", table, columns.trim());
314-
sqlx::query(&schema).execute(pool).await?;
314+
sqlx::query(AssertSqlSafe(schema.as_str())).execute(pool).await?;
315315
Ok(())
316316
}}
317317
@@ -427,7 +427,7 @@ async fn generate_sqlx_handler(name: &str, type_name: &str, table: &str) -> Resu
427427
use crate::db::{table_mod}::{{SINGULAR, TABLE}};
428428
use crate::models::{{Create{type_name}, Update{type_name}, {type_name}}};
429429
use rustapi_rs::prelude::*;
430-
use sqlx::SqlitePool;
430+
use sqlx::{{AssertSqlSafe, SqlitePool}};
431431
432432
fn now_ts() -> String {{
433433
chrono::Utc::now().to_rfc3339()
@@ -442,10 +442,10 @@ fn db_error(err: sqlx::Error) -> ApiError {{
442442
#[rustapi_rs::tag("{type_name}")]
443443
#[rustapi_rs::summary("List all {name}")]
444444
pub async fn list(State(pool): State<SqlitePool>) -> Result<Json<Vec<{type_name}>>> {{
445-
let rows = sqlx::query_as::<_, {type_name}>(&format!(
445+
let rows = sqlx::query_as::<_, {type_name}>(AssertSqlSafe(format!(
446446
"SELECT id, name, description, created_at, updated_at FROM {{}} ORDER BY id",
447447
TABLE
448-
))
448+
)))
449449
.fetch_all(&pool)
450450
.await
451451
.map_err(db_error)?;
@@ -457,10 +457,10 @@ pub async fn list(State(pool): State<SqlitePool>) -> Result<Json<Vec<{type_name}
457457
#[rustapi_rs::tag("{type_name}")]
458458
#[rustapi_rs::summary("Get {singular} by ID")]
459459
pub async fn get(Path(id): Path<i64>, State(pool): State<SqlitePool>) -> Result<Json<{type_name}>> {{
460-
let row = sqlx::query_as::<_, {type_name}>(&format!(
460+
let row = sqlx::query_as::<_, {type_name}>(AssertSqlSafe(format!(
461461
"SELECT id, name, description, created_at, updated_at FROM {{}} WHERE id = ?",
462462
TABLE
463-
))
463+
)))
464464
.bind(id)
465465
.fetch_optional(&pool)
466466
.await
@@ -478,10 +478,10 @@ pub async fn create(
478478
Json(body): Json<Create{type_name}>,
479479
) -> Result<WithStatus<Json<{type_name}>, 201>> {{
480480
let now = now_ts();
481-
let result = sqlx::query(&format!(
481+
let result = sqlx::query(AssertSqlSafe(format!(
482482
"INSERT INTO {{}} (name, description, created_at, updated_at) VALUES (?, ?, ?, ?)",
483483
TABLE
484-
))
484+
)))
485485
.bind(&body.name)
486486
.bind(&body.description)
487487
.bind(&now)
@@ -491,10 +491,10 @@ pub async fn create(
491491
.map_err(db_error)?;
492492
493493
let id = result.last_insert_rowid();
494-
let row = sqlx::query_as::<_, {type_name}>(&format!(
494+
let row = sqlx::query_as::<_, {type_name}>(AssertSqlSafe(format!(
495495
"SELECT id, name, description, created_at, updated_at FROM {{}} WHERE id = ?",
496496
TABLE
497-
))
497+
)))
498498
.bind(id)
499499
.fetch_one(&pool)
500500
.await
@@ -511,10 +511,10 @@ pub async fn update(
511511
State(pool): State<SqlitePool>,
512512
Json(body): Json<Update{type_name}>,
513513
) -> Result<Json<{type_name}>> {{
514-
let existing = sqlx::query_as::<_, {type_name}>(&format!(
514+
let existing = sqlx::query_as::<_, {type_name}>(AssertSqlSafe(format!(
515515
"SELECT id, name, description, created_at, updated_at FROM {{}} WHERE id = ?",
516516
TABLE
517-
))
517+
)))
518518
.bind(id)
519519
.fetch_optional(&pool)
520520
.await
@@ -525,10 +525,10 @@ pub async fn update(
525525
let description = body.description.or(existing.description);
526526
let updated_at = now_ts();
527527
528-
sqlx::query(&format!(
528+
sqlx::query(AssertSqlSafe(format!(
529529
"UPDATE {{}} SET name = ?, description = ?, updated_at = ? WHERE id = ?",
530530
TABLE
531-
))
531+
)))
532532
.bind(&name)
533533
.bind(&description)
534534
.bind(&updated_at)
@@ -537,10 +537,10 @@ pub async fn update(
537537
.await
538538
.map_err(db_error)?;
539539
540-
let row = sqlx::query_as::<_, {type_name}>(&format!(
540+
let row = sqlx::query_as::<_, {type_name}>(AssertSqlSafe(format!(
541541
"SELECT id, name, description, created_at, updated_at FROM {{}} WHERE id = ?",
542542
TABLE
543-
))
543+
)))
544544
.bind(id)
545545
.fetch_one(&pool)
546546
.await
@@ -553,7 +553,7 @@ pub async fn update(
553553
#[rustapi_rs::tag("{type_name}")]
554554
#[rustapi_rs::summary("Delete {singular}")]
555555
pub async fn delete(Path(id): Path<i64>, State(pool): State<SqlitePool>) -> Result<NoContent> {{
556-
let result = sqlx::query(&format!("DELETE FROM {{}} WHERE id = ?", TABLE))
556+
let result = sqlx::query(AssertSqlSafe(format!("DELETE FROM {{}} WHERE id = ?", TABLE)))
557557
.bind(id)
558558
.execute(&pool)
559559
.await

crates/rustapi-extras/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ session-redis = ["session", "dep:redis"]
134134
# Background job processing
135135
jobs = ["dep:chrono", "dep:uuid"]
136136
jobs-redis = ["jobs", "dep:redis", "redis/script"]
137-
jobs-postgres = ["jobs", "dep:sqlx", "sqlx/postgres", "sqlx/runtime-tokio", "sqlx/tls-rustls", "sqlx/chrono", "sqlx/uuid"]
137+
jobs-postgres = ["jobs", "dep:sqlx", "sqlx/postgres", "sqlx/runtime-tokio", "sqlx/tls-rustls", "sqlx/chrono", "sqlx/uuid", "sqlx/json"]
138138

139139
# Replay (time-travel debugging)
140140
replay = ["dep:reqwest", "dep:dashmap", "dep:uuid", "dep:serde_urlencoded", "rustapi-core/replay"]

crates/rustapi-extras/src/jobs/backend/postgres.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use super::super::error::{JobError, Result};
22
use super::{JobBackend, JobRequest};
3-
use sqlx::{Pool, Postgres, Row};
3+
use sqlx::types::Json;
4+
use sqlx::{AssertSqlSafe, Pool, Postgres, Row};
45
use std::future::Future;
56
use std::pin::Pin;
67

@@ -39,7 +40,7 @@ impl PostgresBackend {
3940
self.table_name, self.table_name, self.table_name
4041
);
4142

42-
sqlx::query(&query)
43+
sqlx::query(AssertSqlSafe(query.as_str()))
4344
.execute(&self.pool)
4445
.await
4546
.map_err(|e| JobError::BackendError(e.to_string()))?;
@@ -62,10 +63,10 @@ impl JobBackend for PostgresBackend {
6263
self.table_name
6364
);
6465

65-
sqlx::query(&query)
66+
sqlx::query(AssertSqlSafe(query.as_str()))
6667
.bind(&job.id)
6768
.bind(&job.name)
68-
.bind(&job.payload)
69+
.bind(Json(&job.payload))
6970
.bind(job.created_at)
7071
.bind(job.run_at)
7172
.bind(job.attempts as i32)
@@ -98,7 +99,7 @@ impl JobBackend for PostgresBackend {
9899
self.table_name, self.table_name
99100
);
100101

101-
let row = sqlx::query(&query)
102+
let row = sqlx::query(AssertSqlSafe(query.as_str()))
102103
.fetch_optional(&self.pool)
103104
.await
104105
.map_err(|e| JobError::BackendError(e.to_string()))?;
@@ -107,7 +108,7 @@ impl JobBackend for PostgresBackend {
107108
Ok(Some(JobRequest {
108109
id: row.get("id"),
109110
name: row.get("name"),
110-
payload: row.get("payload"),
111+
payload: row.get::<Json<serde_json::Value>, _>("payload").0,
111112
created_at: row.get("created_at"),
112113
run_at: row.get("run_at"),
113114
attempts: row.get::<i32, _>("attempts") as u32,

0 commit comments

Comments
 (0)