refactor: remove session auth, add DbType enum, native async broker

- Remove `auth`, `sessions-db` features and all tower-sessions/tower-sessions-sqlx-store deps
- Delete `src/session.rs` (InfraSessionStore)
- `ServerConfig`: remove `session_secret` field and `attach_session_layer()` helper
- `db.rs`: add explicit `DbType` enum to `DatabaseConfig`; `connect()` now matches on
  `db_type` instead of sniffing URL prefixes; remove `connect_sqlite()` standalone fn
- `broker/mod.rs`: replace `#[async_trait]` with native async fn (Rust 2024 AFIT)
- `broker/nats.rs`: remove `#[async_trait]` to match trait definition
- `ai/embeddings.rs`: remove sync `generate_embedding()` (wasteful thread spawn);
  keep only `generate_embedding_async()` as the public API
- Default features: `["logging", "db-sqlx", "auth", "http"]` → `["logging"]`
- Bump version to 0.1.11

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 01:36:52 +01:00
parent 7a72f5f54a
commit 0ea9aa7870
9 changed files with 53 additions and 296 deletions

121
Cargo.lock generated
View File

@@ -533,17 +533,6 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@@ -1775,7 +1764,7 @@ dependencies = [
[[package]] [[package]]
name = "k-core" name = "k-core"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-nats", "async-nats",
@@ -1789,12 +1778,9 @@ dependencies = [
"serde", "serde",
"sqlx", "sqlx",
"thiserror 2.0.17", "thiserror 2.0.17",
"time",
"tokio", "tokio",
"tower 0.5.2", "tower 0.5.2",
"tower-http", "tower-http",
"tower-sessions",
"tower-sessions-sqlx-store",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"uuid", "uuid",
@@ -1878,7 +1864,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [ dependencies = [
"scopeguard", "scopeguard",
"serde",
] ]
[[package]] [[package]]
@@ -2958,25 +2943,6 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rmp"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
dependencies = [
"num-traits",
]
[[package]]
name = "rmp-serde"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
dependencies = [
"rmp",
"serde",
]
[[package]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.9" version = "0.9.9"
@@ -3479,7 +3445,6 @@ dependencies = [
"sha2", "sha2",
"smallvec 1.15.1", "smallvec 1.15.1",
"thiserror 2.0.17", "thiserror 2.0.17",
"time",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tracing", "tracing",
@@ -3564,7 +3529,6 @@ dependencies = [
"sqlx-core", "sqlx-core",
"stringprep", "stringprep",
"thiserror 2.0.17", "thiserror 2.0.17",
"time",
"tracing", "tracing",
"uuid", "uuid",
"whoami", "whoami",
@@ -3604,7 +3568,6 @@ dependencies = [
"sqlx-core", "sqlx-core",
"stringprep", "stringprep",
"thiserror 2.0.17", "thiserror 2.0.17",
"time",
"tracing", "tracing",
"uuid", "uuid",
"whoami", "whoami",
@@ -3631,7 +3594,6 @@ dependencies = [
"serde_urlencoded", "serde_urlencoded",
"sqlx-core", "sqlx-core",
"thiserror 2.0.17", "thiserror 2.0.17",
"time",
"tracing", "tracing",
"url", "url",
"uuid", "uuid",
@@ -4063,22 +4025,6 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "tower-cookies"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36"
dependencies = [
"axum-core 0.5.6",
"cookie",
"futures-util",
"http",
"parking_lot",
"pin-project-lite",
"tower-layer",
"tower-service",
]
[[package]] [[package]]
name = "tower-http" name = "tower-http"
version = "0.6.8" version = "0.6.8"
@@ -4110,71 +4056,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tower-sessions"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3"
dependencies = [
"async-trait",
"http",
"time",
"tokio",
"tower-cookies",
"tower-layer",
"tower-service",
"tower-sessions-core",
"tower-sessions-memory-store",
"tracing",
]
[[package]]
name = "tower-sessions-core"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b"
dependencies = [
"async-trait",
"axum-core 0.5.6",
"base64 0.22.1",
"futures",
"http",
"parking_lot",
"rand 0.8.5",
"serde",
"serde_json",
"thiserror 2.0.17",
"time",
"tokio",
"tracing",
]
[[package]]
name = "tower-sessions-memory-store"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb05909f2e1420135a831dd5df9f5596d69196d0a64c3499ca474c4bd3d33242"
dependencies = [
"async-trait",
"time",
"tokio",
"tower-sessions-core",
]
[[package]]
name = "tower-sessions-sqlx-store"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e054622079f57fc1a7d6a6089c9334f963d62028fe21dc9eddd58af9a78480b3"
dependencies = [
"async-trait",
"rmp-serde",
"sqlx",
"thiserror 1.0.69",
"time",
"tower-sessions-core",
]
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.44" version = "0.1.44"

View File

@@ -1,20 +1,18 @@
[package] [package]
name = "k-core" name = "k-core"
version = "0.1.10" version = "0.1.11"
edition = "2024" edition = "2024"
[features] [features]
default = ["logging", "db-sqlx", "auth", "http"] default = ["logging"]
logging = ["dep:tracing", "dep:tracing-subscriber"] logging = ["dep:tracing", "dep:tracing-subscriber"]
db-sqlx = ["dep:sqlx"] db-sqlx = ["dep:sqlx"]
postgres = ["db-sqlx", "sqlx/postgres", "tower-sessions-sqlx-store?/postgres"] postgres = ["db-sqlx", "sqlx/postgres"]
sqlite = ["db-sqlx", "sqlx/sqlite", "tower-sessions-sqlx-store?/sqlite"] sqlite = ["db-sqlx", "sqlx/sqlite"]
auth = ["dep:tower-sessions"]
sessions-db = ["auth", "dep:tower-sessions-sqlx-store"]
ai = ["dep:fastembed", "dep:qdrant-client"] ai = ["dep:fastembed", "dep:qdrant-client"]
broker = [] broker = []
broker-nats = ["broker", "dep:async-nats", "dep:futures-util", "dep:futures-core"] broker-nats = ["broker", "dep:async-nats", "dep:futures-util", "dep:futures-core"]
http = ["dep:axum", "dep:tower", "dep:tower-http", "dep:time", "logging"] http = ["dep:axum", "dep:tower", "dep:tower-http", "logging"]
[dependencies] [dependencies]
# Error handling # Error handling
@@ -22,7 +20,7 @@ thiserror = "2.0.17"
anyhow = "1.0.100" anyhow = "1.0.100"
# Async # Async
tokio = { version = "1.48.0", features = ["full"]} tokio = { version = "1.48.0", features = ["full"] }
# Database # Database
sqlx = { version = "0.8.6", features = [ sqlx = { version = "0.8.6", features = [
@@ -39,10 +37,6 @@ tracing-subscriber = { version = "0.3.22", features = [
"fmt", "fmt",
], optional = true } ], optional = true }
# Auth
tower-sessions = { version = "0.14.0", optional = true }
tower-sessions-sqlx-store = { version = "0.15", optional = true}
# Utils # Utils
chrono = { version = "0.4.42", features = ["serde"] } chrono = { version = "0.4.42", features = ["serde"] }
uuid = { version = "1.19.0", features = ["v4", "serde"] } uuid = { version = "1.19.0", features = ["v4", "serde"] }
@@ -59,8 +53,7 @@ async-nats = { version = "0.45", optional = true }
# HTTP # HTTP
axum = { version = "0.8.8", features = ["macros"], optional = true } axum = { version = "0.8.8", features = ["macros"], optional = true }
tower = { version = "0.5.2", optional = true } tower = { version = "0.5.2", optional = true }
tower-http = { version = "0.6.2", features = ["cors", "trace"], optional = true} tower-http = { version = "0.6.2", features = ["cors", "trace"], optional = true }
time = { version = "0.3", optional = true }
futures-util = { version = "0.3", optional = true } futures-util = { version = "0.3", optional = true }
futures-core = {version = "0.3", optional = true } futures-core = { version = "0.3", optional = true }

View File

@@ -20,33 +20,7 @@ impl FastEmbedAdapter {
}) })
} }
pub fn generate_embedding(&self, text: &str) -> anyhow::Result<Vec<f32>> { /// Generate an embedding asynchronously using a blocking thread pool.
let model = self.model.clone();
let text = text.to_string();
// FastEmbed is blocking, so we run it in a blocking task if we are in an async context,
// but since this method signature doesn't force async, we wrap the internal logic.
// For strictly async usage in k-core:
let embeddings = std::thread::scope(|s| {
s.spawn(|| {
let mut model = model
.lock()
.map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
model
.embed(vec![text], None)
.map_err(|e| anyhow::anyhow!("Embed error: {}", e))
})
.join()
.map_err(|_| anyhow::anyhow!("Thread join error"))?
})?;
embeddings
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("No embedding generated"))
}
/// Async wrapper for use in async contexts (like Axum handlers)
pub async fn generate_embedding_async(&self, text: &str) -> anyhow::Result<Vec<f32>> { pub async fn generate_embedding_async(&self, text: &str) -> anyhow::Result<Vec<f32>> {
let model = self.model.clone(); let model = self.model.clone();
let text = text.to_string(); let text = text.to_string();

View File

@@ -1,11 +1,9 @@
use async_trait::async_trait;
use futures_core::Stream; use futures_core::Stream;
use std::pin::Pin; use std::pin::Pin;
#[cfg(feature = "broker-nats")] #[cfg(feature = "broker-nats")]
pub mod nats; pub mod nats;
#[async_trait]
pub trait MessageBroker: Send + Sync { pub trait MessageBroker: Send + Sync {
async fn publish(&self, topic: &str, payload: Vec<u8>) -> anyhow::Result<()>; async fn publish(&self, topic: &str, payload: Vec<u8>) -> anyhow::Result<()>;

View File

@@ -1,5 +1,4 @@
use super::MessageBroker; use super::MessageBroker;
use async_trait::async_trait;
use futures_util::StreamExt; use futures_util::StreamExt;
use std::pin::Pin; use std::pin::Pin;
@@ -15,7 +14,6 @@ impl NatsBroker {
} }
} }
#[async_trait]
impl MessageBroker for NatsBroker { impl MessageBroker for NatsBroker {
async fn publish(&self, topic: &str, payload: Vec<u8>) -> anyhow::Result<()> { async fn publish(&self, topic: &str, payload: Vec<u8>) -> anyhow::Result<()> {
self.client self.client
@@ -35,7 +33,6 @@ impl MessageBroker for NatsBroker {
.await .await
.map_err(|e| anyhow::anyhow!("NATS subscribe error: {}", e))?; .map_err(|e| anyhow::anyhow!("NATS subscribe error: {}", e))?;
// Map NATS Message to generic Vec<u8>
let stream = subscriber.map(|msg| msg.payload.to_vec()); let stream = subscriber.map(|msg| msg.payload.to_vec());
Ok(Box::pin(stream)) Ok(Box::pin(stream))

View File

@@ -8,9 +8,22 @@ use sqlx::Sqlite;
#[cfg(feature = "postgres")] #[cfg(feature = "postgres")]
use sqlx::Postgres; use sqlx::Postgres;
/// Explicitly declares which database engine to connect to.
///
/// Using an explicit type avoids URL-prefix sniffing in `connect()`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DbType {
#[default]
#[cfg_attr(not(feature = "sqlite"), allow(dead_code))]
Sqlite,
#[cfg_attr(not(feature = "postgres"), allow(dead_code))]
Postgres,
}
/// Universal Database Configuration /// Universal Database Configuration
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DatabaseConfig { pub struct DatabaseConfig {
pub db_type: DbType,
pub url: String, pub url: String,
pub max_connections: u32, pub max_connections: u32,
pub min_connections: u32, pub min_connections: u32,
@@ -22,6 +35,7 @@ impl Default for DatabaseConfig {
#[cfg(feature = "sqlite")] #[cfg(feature = "sqlite")]
{ {
Self { Self {
db_type: DbType::Sqlite,
url: "sqlite::memory:".to_string(), url: "sqlite::memory:".to_string(),
max_connections: 5, max_connections: 5,
min_connections: 1, min_connections: 1,
@@ -32,6 +46,7 @@ impl Default for DatabaseConfig {
#[cfg(all(not(feature = "sqlite"), feature = "postgres"))] #[cfg(all(not(feature = "sqlite"), feature = "postgres"))]
{ {
Self { Self {
db_type: DbType::Postgres,
url: "postgres://localhost:5432/mydb".to_string(), url: "postgres://localhost:5432/mydb".to_string(),
max_connections: 5, max_connections: 5,
min_connections: 1, min_connections: 1,
@@ -41,6 +56,7 @@ impl Default for DatabaseConfig {
#[cfg(not(any(feature = "sqlite", feature = "postgres")))] #[cfg(not(any(feature = "sqlite", feature = "postgres")))]
Self { Self {
db_type: DbType::Sqlite,
url: "".to_string(), url: "".to_string(),
max_connections: 5, max_connections: 5,
min_connections: 1, min_connections: 1,
@@ -50,8 +66,9 @@ impl Default for DatabaseConfig {
} }
impl DatabaseConfig { impl DatabaseConfig {
pub fn new(url: impl Into<String>) -> Self { pub fn new(db_type: DbType, url: impl Into<String>) -> Self {
Self { Self {
db_type,
url: url.into(), url: url.into(),
..Default::default() ..Default::default()
} }
@@ -60,8 +77,9 @@ impl DatabaseConfig {
#[cfg(feature = "sqlite")] #[cfg(feature = "sqlite")]
pub fn in_memory() -> Self { pub fn in_memory() -> Self {
Self { Self {
db_type: DbType::Sqlite,
url: "sqlite::memory:".to_string(), url: "sqlite::memory:".to_string(),
max_connections: 1, // SQLite in-memory is single-connection max_connections: 1,
min_connections: 1, min_connections: 1,
..Default::default() ..Default::default()
} }
@@ -69,7 +87,6 @@ impl DatabaseConfig {
} }
/// A wrapper around various DB pools. /// A wrapper around various DB pools.
/// The Template uses this type so it doesn't care if it's Sqlite or Postgres.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum DatabasePool { pub enum DatabasePool {
#[cfg(feature = "sqlite")] #[cfg(feature = "sqlite")]
@@ -78,36 +95,33 @@ pub enum DatabasePool {
Postgres(Pool<Postgres>), Postgres(Pool<Postgres>),
} }
/// The single entry point for connecting to any DB. /// Connect to a database using the explicit `db_type` from `DatabaseConfig`.
pub async fn connect(config: &DatabaseConfig) -> Result<DatabasePool, sqlx::Error> { pub async fn connect(config: &DatabaseConfig) -> Result<DatabasePool, sqlx::Error> {
// 1. Try Postgres if the feature is enabled AND the URL looks like postgres match config.db_type {
#[cfg(feature = "postgres")] #[cfg(feature = "postgres")]
if config.url.starts_with("postgres://") || config.url.starts_with("postgresql://") { DbType::Postgres => {
let pool = sqlx::postgres::PgPoolOptions::new() let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(config.max_connections) .max_connections(config.max_connections)
.acquire_timeout(config.acquire_timeout) .acquire_timeout(config.acquire_timeout)
.connect(&config.url) .connect(&config.url)
.await?; .await?;
return Ok(DatabasePool::Postgres(pool)); Ok(DatabasePool::Postgres(pool))
} }
// 2. Fallback to Sqlite if the feature is enabled
#[cfg(feature = "sqlite")] #[cfg(feature = "sqlite")]
{ DbType::Sqlite => {
let pool = sqlx::sqlite::SqlitePoolOptions::new() let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(config.max_connections) .max_connections(config.max_connections)
.acquire_timeout(config.acquire_timeout) .acquire_timeout(config.acquire_timeout)
.connect(&config.url) .connect(&config.url)
.await?; .await?;
Ok(DatabasePool::Sqlite(pool)) Ok(DatabasePool::Sqlite(pool))
} }
#[cfg(not(feature = "sqlite"))] #[allow(unreachable_patterns)]
{ _ => Err(sqlx::Error::Configuration(
Err(sqlx::Error::Configuration(
"No supported database features enabled".into(), "No supported database features enabled".into(),
)) )),
} }
} }
@@ -130,7 +144,3 @@ impl DatabasePool {
} }
} }
} }
#[cfg(feature = "sqlite")]
pub async fn connect_sqlite(url: &str) -> Result<Pool<Sqlite>, sqlx::Error> {
sqlx::sqlite::SqlitePoolOptions::new().connect(url).await
}

View File

@@ -1,11 +1,9 @@
use axum::Router; use axum::Router;
use tower_http::cors::CorsLayer; use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tower_sessions::{Expiry, SessionManagerLayer, SessionStore};
pub struct ServerConfig { pub struct ServerConfig {
pub cors_origins: Vec<String>, pub cors_origins: Vec<String>,
pub session_secret: Option<String>,
} }
pub fn apply_standard_middleware(app: Router, config: &ServerConfig) -> Router { pub fn apply_standard_middleware(app: Router, config: &ServerConfig) -> Router {
@@ -24,7 +22,6 @@ pub fn apply_standard_middleware(app: Router, config: &ServerConfig) -> Router {
]) ])
.allow_credentials(true); .allow_credentials(true);
// Configure CORS origins
let mut allowed_origins = Vec::new(); let mut allowed_origins = Vec::new();
for origin in &config.cors_origins { for origin in &config.cors_origins {
if let Ok(value) = origin.parse::<axum::http::HeaderValue>() { if let Ok(value) = origin.parse::<axum::http::HeaderValue>() {
@@ -38,15 +35,3 @@ pub fn apply_standard_middleware(app: Router, config: &ServerConfig) -> Router {
app.layer(cors).layer(TraceLayer::new_for_http()) app.layer(cors).layer(TraceLayer::new_for_http())
} }
/// Helper to attach a session layer with standard K-Suite configuration
pub fn attach_session_layer<S>(app: Router, store: S) -> Router
where
S: SessionStore + Clone + Send + Sync + 'static,
{
let session_layer = SessionManagerLayer::new(store)
.with_secure(false) // Set to true if you handle HTTPS termination in the app
.with_expiry(Expiry::OnInactivity(time::Duration::days(7)));
app.layer(session_layer)
}

View File

@@ -11,8 +11,6 @@ pub mod ai;
#[cfg(feature = "broker")] #[cfg(feature = "broker")]
pub mod broker; pub mod broker;
#[cfg(feature = "auth")]
pub mod session;
#[cfg(feature = "http")] #[cfg(feature = "http")]
pub mod http; pub mod http;

View File

@@ -1,79 +0,0 @@
// Only compile this module if we have the store dependency AND sqlx enabled
#[cfg(all(feature = "sessions-db", feature = "db-sqlx"))]
pub mod store {
use async_trait::async_trait;
use tower_sessions::{
SessionStore,
session::{Id, Record},
session_store,
};
// We only import what is actually enabled by features
#[cfg(feature = "postgres")]
use tower_sessions_sqlx_store::PostgresStore;
#[cfg(feature = "sqlite")]
use tower_sessions_sqlx_store::SqliteStore;
#[derive(Clone, Debug)]
pub enum InfraSessionStore {
#[cfg(feature = "sqlite")]
Sqlite(SqliteStore),
#[cfg(feature = "postgres")]
Postgres(PostgresStore),
// Fallback variant to allow compilation if sessions-db is enabled but no generic DB feature is selected
// (Though in practice you should ensure your config validates this)
#[allow(dead_code)]
Unused,
}
#[async_trait]
impl SessionStore for InfraSessionStore {
async fn save(&self, session_record: &Record) -> session_store::Result<()> {
match self {
#[cfg(feature = "sqlite")]
Self::Sqlite(store) => store.save(session_record).await,
#[cfg(feature = "postgres")]
Self::Postgres(store) => store.save(session_record).await,
_ => Err(session_store::Error::Backend(
"No database feature enabled".to_string(),
)),
}
}
async fn load(&self, session_id: &Id) -> session_store::Result<Option<Record>> {
match self {
#[cfg(feature = "sqlite")]
Self::Sqlite(store) => store.load(session_id).await,
#[cfg(feature = "postgres")]
Self::Postgres(store) => store.load(session_id).await,
_ => Err(session_store::Error::Backend(
"No database feature enabled".to_string(),
)),
}
}
async fn delete(&self, session_id: &Id) -> session_store::Result<()> {
match self {
#[cfg(feature = "sqlite")]
Self::Sqlite(store) => store.delete(session_id).await,
#[cfg(feature = "postgres")]
Self::Postgres(store) => store.delete(session_id).await,
_ => Err(session_store::Error::Backend(
"No database feature enabled".to_string(),
)),
}
}
}
impl InfraSessionStore {
pub async fn migrate(&self) -> anyhow::Result<()> {
match self {
#[cfg(feature = "sqlite")]
Self::Sqlite(store) => store.migrate().await.map_err(|e| anyhow::anyhow!(e)),
#[cfg(feature = "postgres")]
Self::Postgres(store) => store.migrate().await.map_err(|e| anyhow::anyhow!(e)),
_ => Ok(()), // No migration needed for no-op
}
}
}
}