Browse Source

Add PostgreSQL support

Cesar Rodas 3 weeks ago
parent
commit
3b8c979f78
70 changed files with 1024 additions and 160 deletions
  1. 2 1
      Cargo.toml
  2. 1 1
      crates/cdk-common/src/database/mint/test.rs
  3. 1 0
      crates/cdk-integration-tests/Cargo.toml
  4. 10 0
      crates/cdk-integration-tests/src/init_pure_tests.rs
  5. 2 1
      crates/cdk-mintd/Cargo.toml
  6. 2 0
      crates/cdk-mintd/src/config.rs
  7. 11 0
      crates/cdk-mintd/src/main.rs
  8. 35 0
      crates/cdk-postgres/Cargo.toml
  9. 135 0
      crates/cdk-postgres/src/db.rs
  10. 434 0
      crates/cdk-postgres/src/lib.rs
  11. 130 0
      crates/cdk-postgres/src/value.rs
  12. 26 0
      crates/cdk-postgres/start_db_for_test.sh
  13. 4 2
      crates/cdk-sql-base/build.rs
  14. 0 38
      crates/cdk-sql-base/run_test.sh
  15. 7 18
      crates/cdk-sql-base/src/common.rs
  16. 3 0
      crates/cdk-sql-base/src/database.rs
  17. 1 2
      crates/cdk-sql-base/src/mint/auth/migrations.rs
  18. 0 0
      crates/cdk-sql-base/src/mint/auth/migrations/sqlite/20250109143347_init.sql
  19. 1 1
      crates/cdk-sql-base/src/mint/auth/mod.rs
  20. 22 42
      crates/cdk-sql-base/src/mint/migrations.rs
  21. 82 0
      crates/cdk-sql-base/src/mint/migrations/postgres/20250710212391_init.sql
  22. 20 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/1_fix_sqlx_migration.sql
  23. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20240612124932_init.sql
  24. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20240618195700_quote_state.sql
  25. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20240626092101_nut04_state.sql
  26. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20240703122347_request_lookup_id.sql
  27. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20240710145043_input_fee.sql
  28. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20240711183109_derivation_path_index.sql
  29. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20240718203721_allow_unspent.sql
  30. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20240811031111_update_mint_url.sql
  31. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20240919103407_proofs_quote_id.sql
  32. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20240923153640_melt_requests.sql
  33. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20240930101140_dleq_for_sigs.sql
  34. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20241108093102_mint_mint_quote_pubkey.sql
  35. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20250103201327_amount_to_pay_msats.sql
  36. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20250129200912_remove_mint_url.sql
  37. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20250129230326_add_config_table.sql
  38. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20250307213652_keyset_id_as_foreign_key.sql
  39. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20250406091754_mint_time_of_quotes.sql
  40. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20250406093755_mint_created_time_signature.sql
  41. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20250415093121_drop_keystore_foreign.sql
  42. 0 0
      crates/cdk-sql-base/src/mint/migrations/sqlite/20250626120251_rename_blind_message_y_to_b.sql
  43. 10 4
      crates/cdk-sql-base/src/mint/mod.rs
  44. 3 2
      crates/cdk-sql-base/src/pool.rs
  45. 2 2
      crates/cdk-sql-base/src/stmt.rs
  46. 16 32
      crates/cdk-sql-base/src/wallet/migrations.rs
  47. 0 0
      crates/cdk-sql-base/src/wallet/migrations/sqlite/20240612132920_init.sql
  48. 0 0
      crates/cdk-sql-base/src/wallet/migrations/sqlite/20240618200350_quote_state.sql
  49. 0 0
      crates/cdk-sql-base/src/wallet/migrations/sqlite/20240626091921_nut04_state.sql
  50. 0 0
      crates/cdk-sql-base/src/wallet/migrations/sqlite/20240710144711_input_fee.sql
  51. 0 0
      crates/cdk-sql-base/src/wallet/migrations/sqlite/20240810214105_mint_icon_url.sql
  52. 0 0
      crates/cdk-sql-base/src/wallet/migrations/sqlite/20240810233905_update_mint_url.sql
  53. 0 0
      crates/cdk-sql-base/src/wallet/migrations/sqlite/20240902151515_icon_url.sql
  54. 0 0
      crates/cdk-sql-base/src/wallet/migrations/sqlite/20240902210905_mint_time.sql
  55. 0 0
      crates/cdk-sql-base/src/wallet/migrations/sqlite/20241011125207_mint_urls.sql
  56. 0 0
      crates/cdk-sql-base/src/wallet/migrations/sqlite/20241108092756_wallet_mint_quote_secretkey.sql
  57. 0 0
      crates/cdk-sql-base/src/wallet/migrations/sqlite/20250214135017_mint_tos.sql
  58. 0 0
      crates/cdk-sql-base/src/wallet/migrations/sqlite/20250310111513_drop_nostr_last_checked.sql
  59. 0 0
      crates/cdk-sql-base/src/wallet/migrations/sqlite/20250314082116_allow_pending_spent.sql
  60. 0 0
      crates/cdk-sql-base/src/wallet/migrations/sqlite/20250323152040_wallet_dleq_proofs.sql
  61. 0 0
      crates/cdk-sql-base/src/wallet/migrations/sqlite/20250401120000_add_transactions_table.sql
  62. 0 0
      crates/cdk-sql-base/src/wallet/migrations/sqlite/20250616144830_add_keyset_expiry.sql
  63. 5 5
      crates/cdk-sql-base/src/wallet/mod.rs
  64. 5 5
      crates/cdk-sql-base/tests/legacy-sqlx.sql
  65. 1 1
      crates/cdk-sqlite/Cargo.toml
  66. 2 1
      crates/cdk-sqlite/src/common.rs
  67. 10 1
      crates/cdk-sqlite/src/mint/async_rusqlite.rs
  68. 36 0
      crates/cdk-sqlite/src/mint/mod.rs
  69. 4 0
      crates/cdk-sqlite/src/wallet/mod.rs
  70. 1 1
      crates/cdk-sqlite/tests/legacy-sqlx.sql

+ 2 - 1
Cargo.toml

@@ -54,8 +54,9 @@ cdk-fake-wallet = { path = "./crates/cdk-fake-wallet", version = "=0.11.0" }
 cdk-payment-processor = { path = "./crates/cdk-payment-processor", default-features = true, version = "=0.11.0" }
 cdk-payment-processor = { path = "./crates/cdk-payment-processor", default-features = true, version = "=0.11.0" }
 cdk-mint-rpc = { path = "./crates/cdk-mint-rpc", version = "=0.11.0" }
 cdk-mint-rpc = { path = "./crates/cdk-mint-rpc", version = "=0.11.0" }
 cdk-redb = { path = "./crates/cdk-redb", default-features = true, version = "=0.11.0" }
 cdk-redb = { path = "./crates/cdk-redb", default-features = true, version = "=0.11.0" }
-cdk-sqlite = { path = "./crates/cdk-sqlite", default-features = true, version = "=0.11.0" }
 cdk-sql-base = { path = "./crates/cdk-sql-base", default-features = true, version = "=0.11.0" }
 cdk-sql-base = { path = "./crates/cdk-sql-base", default-features = true, version = "=0.11.0" }
+cdk-sqlite = { path = "./crates/cdk-sqlite", default-features = true, version = "=0.11.0" }
+cdk-postgres = { path = "./crates/cdk-postgres", default-features = true, version = "=0.11.0" }
 cdk-signatory = { path = "./crates/cdk-signatory", version = "=0.11.0", default-features = false }
 cdk-signatory = { path = "./crates/cdk-signatory", version = "=0.11.0", default-features = false }
 clap = { version = "4.5.31", features = ["derive"] }
 clap = { version = "4.5.31", features = ["derive"] }
 ciborium = { version = "0.2.2", default-features = false, features = ["std"] }
 ciborium = { version = "0.2.2", default-features = false, features = ["std"] }

+ 1 - 1
crates/cdk-common/src/database/mint/test.rs

@@ -225,7 +225,7 @@ macro_rules! mint_db_test {
         mint_db_test!(test_remove_spent_proofs, $make_db_fn);
         mint_db_test!(test_remove_spent_proofs, $make_db_fn);
     };
     };
     ($name:ident, $make_db_fn:ident) => {
     ($name:ident, $make_db_fn:ident) => {
-        #[tokio::test]
+        #[tokio::test(flavor = "multi_thread", worker_threads = 10)]
         async fn $name() {
         async fn $name() {
             $crate::database::mint::test::$name($make_db_fn().await).await;
             $crate::database::mint::test::$name($make_db_fn().await).await;
         }
         }

+ 1 - 0
crates/cdk-integration-tests/Cargo.toml

@@ -25,6 +25,7 @@ cdk-cln = { workspace = true }
 cdk-lnd = { workspace = true }
 cdk-lnd = { workspace = true }
 cdk-axum = { workspace = true }
 cdk-axum = { workspace = true }
 cdk-sqlite = { workspace = true }
 cdk-sqlite = { workspace = true }
+cdk-postgres = { workspace = true }
 cdk-redb = { workspace = true }
 cdk-redb = { workspace = true }
 cdk-fake-wallet = { workspace = true }
 cdk-fake-wallet = { workspace = true }
 futures = { workspace = true, default-features = false, features = [
 futures = { workspace = true, default-features = false, features = [

+ 10 - 0
crates/cdk-integration-tests/src/init_pure_tests.rs

@@ -23,6 +23,7 @@ use cdk::util::unix_time;
 use cdk::wallet::{AuthWallet, MintConnector, Wallet, WalletBuilder};
 use cdk::wallet::{AuthWallet, MintConnector, Wallet, WalletBuilder};
 use cdk::{Amount, Error, Mint};
 use cdk::{Amount, Error, Mint};
 use cdk_fake_wallet::FakeWallet;
 use cdk_fake_wallet::FakeWallet;
+use cdk_postgres::MintPgDatabase;
 use tokio::sync::{Notify, RwLock};
 use tokio::sync::{Notify, RwLock};
 use tracing_subscriber::EnvFilter;
 use tracing_subscriber::EnvFilter;
 use uuid::Uuid;
 use uuid::Uuid;
@@ -177,6 +178,15 @@ pub async fn create_and_start_test_mint() -> Result<Mint> {
         "memory" => MintBuilder::new()
         "memory" => MintBuilder::new()
             .with_localstore(Arc::new(cdk_sqlite::mint::memory::empty().await?))
             .with_localstore(Arc::new(cdk_sqlite::mint::memory::empty().await?))
             .with_keystore(Arc::new(cdk_sqlite::mint::memory::empty().await?)),
             .with_keystore(Arc::new(cdk_sqlite::mint::memory::empty().await?)),
+        "pgsql" => {
+            let connection_str = env::var("CDK_PGSQL").expect("Postgres connection URL");
+
+            MintBuilder::new()
+                .with_localstore(Arc::new(
+                    MintPgDatabase::new(connection_str.as_str()).await?,
+                ))
+                .with_keystore(Arc::new(cdk_sqlite::mint::memory::empty().await?))
+        }
         _ => {
         _ => {
             // Create a temporary directory for SQLite database
             // Create a temporary directory for SQLite database
             let temp_dir = create_temp_dir("cdk-test-sqlite-mint")?;
             let temp_dir = create_temp_dir("cdk-test-sqlite-mint")?;

+ 2 - 1
crates/cdk-mintd/Cargo.toml

@@ -23,7 +23,7 @@ sqlcipher = ["cdk-sqlite/sqlcipher"]
 # MSRV is not committed to with swagger enabled
 # MSRV is not committed to with swagger enabled
 swagger = ["cdk-axum/swagger", "dep:utoipa", "dep:utoipa-swagger-ui"]
 swagger = ["cdk-axum/swagger", "dep:utoipa", "dep:utoipa-swagger-ui"]
 redis = ["cdk-axum/redis"]
 redis = ["cdk-axum/redis"]
-auth = ["cdk/auth", "cdk-sqlite/auth"]
+auth = ["cdk/auth", "cdk-sqlite/auth", "cdk-postgres/auth"]
 
 
 [dependencies]
 [dependencies]
 anyhow.workspace = true
 anyhow.workspace = true
@@ -35,6 +35,7 @@ cdk = { workspace = true, features = [
 cdk-sqlite = { workspace = true, features = [
 cdk-sqlite = { workspace = true, features = [
     "mint",
     "mint",
 ] }
 ] }
+cdk-postgres = { workspace = true, features = ["mint"] }
 cdk-cln = { workspace = true, optional = true }
 cdk-cln = { workspace = true, optional = true }
 cdk-lnbits = { workspace = true, optional = true }
 cdk-lnbits = { workspace = true, optional = true }
 cdk-lnd = { workspace = true, optional = true }
 cdk-lnd = { workspace = true, optional = true }

+ 2 - 0
crates/cdk-mintd/src/config.rs

@@ -190,6 +190,7 @@ pub struct GrpcProcessor {
 pub enum DatabaseEngine {
 pub enum DatabaseEngine {
     #[default]
     #[default]
     Sqlite,
     Sqlite,
+    PgSql,
 }
 }
 
 
 impl std::str::FromStr for DatabaseEngine {
 impl std::str::FromStr for DatabaseEngine {
@@ -198,6 +199,7 @@ impl std::str::FromStr for DatabaseEngine {
     fn from_str(s: &str) -> Result<Self, Self::Err> {
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         match s.to_lowercase().as_str() {
         match s.to_lowercase().as_str() {
             "sqlite" => Ok(DatabaseEngine::Sqlite),
             "sqlite" => Ok(DatabaseEngine::Sqlite),
+            "pgsql" => Ok(DatabaseEngine::PgSql),
             _ => Err(format!("Unknown database engine: {s}")),
             _ => Err(format!("Unknown database engine: {s}")),
         }
         }
     }
     }

+ 11 - 0
crates/cdk-mintd/src/main.rs

@@ -45,6 +45,7 @@ use cdk_mintd::cli::CLIArgs;
 use cdk_mintd::config::{self, DatabaseEngine, LnBackend};
 use cdk_mintd::config::{self, DatabaseEngine, LnBackend};
 use cdk_mintd::env_vars::ENV_WORK_DIR;
 use cdk_mintd::env_vars::ENV_WORK_DIR;
 use cdk_mintd::setup::LnBackendSetup;
 use cdk_mintd::setup::LnBackendSetup;
+use cdk_postgres::MintPgDatabase;
 #[cfg(feature = "auth")]
 #[cfg(feature = "auth")]
 use cdk_sqlite::mint::MintSqliteAuthDatabase;
 use cdk_sqlite::mint::MintSqliteAuthDatabase;
 use cdk_sqlite::MintSqliteDatabase;
 use cdk_sqlite::MintSqliteDatabase;
@@ -202,6 +203,13 @@ async fn setup_database(
             let keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync> = db;
             let keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync> = db;
             Ok((localstore, keystore))
             Ok((localstore, keystore))
         }
         }
+        DatabaseEngine::PgSql => {
+            let conn_str = "".to_owned();
+            let db = Arc::new(MintPgDatabase::new(conn_str.as_str()).await?);
+            let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> = db.clone();
+            let keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync> = db;
+            Ok((localstore, keystore))
+        }
     }
     }
 }
 }
 
 
@@ -518,6 +526,9 @@ async fn setup_authentication(
                 let sqlite_db = MintSqliteAuthDatabase::new(sql_db_path).await?;
                 let sqlite_db = MintSqliteAuthDatabase::new(sql_db_path).await?;
                 Arc::new(sqlite_db)
                 Arc::new(sqlite_db)
             }
             }
+            DatabaseEngine::PgSql => {
+                todo!()
+            }
         };
         };
 
 
         mint_builder = mint_builder.with_auth_localstore(auth_localstore.clone());
         mint_builder = mint_builder.with_auth_localstore(auth_localstore.clone());

+ 35 - 0
crates/cdk-postgres/Cargo.toml

@@ -0,0 +1,35 @@
+[package]
+name = "cdk-postgres"
+version.workspace = true
+edition.workspace = true
+authors = ["CDK Developers"]
+description = "PostgreSQL storage backend for CDK"
+license.workspace = true
+homepage = "https://github.com/cashubtc/cdk"
+repository = "https://github.com/cashubtc/cdk.git"
+rust-version.workspace = true                            # MSRV
+readme = "README.md"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[features]
+default = ["mint", "wallet", "auth"]
+mint = ["cdk-common/mint", "cdk-sql-base/mint"]
+wallet = ["cdk-common/wallet", "cdk-sql-base/wallet"]
+auth = ["cdk-common/auth", "cdk-sql-base/auth"]
+
+[dependencies]
+async-trait.workspace = true
+cdk-common = { workspace = true, features = ["test"] }
+bitcoin.workspace = true
+cdk-sql-base = { workspace = true }
+thiserror.workspace = true
+tokio = { workspace = true, features = ["rt-multi-thread"] }
+tracing.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+lightning-invoice.workspace = true
+uuid.workspace = true
+tokio-postgres = "0.7.13"
+futures-util = "0.3.31"
+postgres-native-tls = "0.5.1"
+once_cell.workspace = true

+ 135 - 0
crates/cdk-postgres/src/db.rs

@@ -0,0 +1,135 @@
+use cdk_common::database::Error;
+use cdk_sql_base::stmt::{Column, Statement};
+use futures_util::{pin_mut, TryStreamExt};
+use tokio_postgres::Client;
+
+use crate::value::PgValue;
+
+#[inline(always)]
+pub async fn pg_batch(conn: &Client, statement: Statement) -> Result<(), Error> {
+    let (sql, _placeholder_values) = statement.to_sql()?;
+
+    conn.batch_execute(&sql)
+        .await
+        .map_err(|e| Error::Database(Box::new(e)))
+}
+
+#[inline(always)]
+pub async fn pg_execute(conn: &Client, statement: Statement) -> Result<usize, Error> {
+    let (sql, placeholder_values) = statement.to_sql()?;
+    let prepared_statement = conn
+        .prepare(&sql)
+        .await
+        .map_err(|e| Error::Database(Box::new(e)))?;
+
+    conn.execute_raw(
+        &prepared_statement,
+        placeholder_values
+            .iter()
+            .map(|x| x.into())
+            .collect::<Vec<PgValue>>(),
+    )
+    .await
+    .map_err(|e| Error::Database(Box::new(e)))
+    .map(|x| x as usize)
+}
+
+#[inline(always)]
+pub async fn pg_fetch_one(
+    conn: &Client,
+    statement: Statement,
+) -> Result<Option<Vec<Column>>, Error> {
+    let (sql, placeholder_values) = statement.to_sql()?;
+    let prepared_statement = conn
+        .prepare(&sql)
+        .await
+        .map_err(|e| Error::Database(Box::new(e)))?;
+
+    let stream = conn
+        .query_raw(
+            &prepared_statement,
+            placeholder_values
+                .iter()
+                .map(|x| x.into())
+                .collect::<Vec<PgValue>>(),
+        )
+        .await
+        .map_err(|e| Error::Database(Box::new(e)))?;
+
+    pin_mut!(stream);
+
+    Ok(stream
+        .try_next()
+        .await
+        .map_err(|e| Error::Database(Box::new(e)))?
+        .map(|row| {
+            (0..row.len())
+                .map(|i| row.get::<_, PgValue>(i).into())
+                .collect::<Vec<_>>()
+        }))
+}
+
+#[inline(always)]
+pub async fn pg_fetch_all(conn: &Client, statement: Statement) -> Result<Vec<Vec<Column>>, Error> {
+    let (sql, placeholder_values) = statement.to_sql()?;
+    let prepared_statement = conn
+        .prepare(&sql)
+        .await
+        .map_err(|e| Error::Database(Box::new(e)))?;
+
+    let stream = conn
+        .query_raw(
+            &prepared_statement,
+            placeholder_values
+                .iter()
+                .map(|x| x.into())
+                .collect::<Vec<PgValue>>(),
+        )
+        .await
+        .map_err(|e| Error::Database(Box::new(e)))?;
+
+    pin_mut!(stream);
+
+    let mut rows = vec![];
+    while let Some(row) = stream
+        .try_next()
+        .await
+        .map_err(|e| Error::Database(Box::new(e)))?
+    {
+        rows.push(
+            (0..row.len())
+                .map(|i| row.get::<_, PgValue>(i).into())
+                .collect::<Vec<_>>(),
+        );
+    }
+
+    Ok(rows)
+}
+
+#[inline(always)]
+pub async fn gn_pluck(conn: &Client, statement: Statement) -> Result<Option<Column>, Error> {
+    let (sql, placeholder_values) = statement.to_sql()?;
+    let prepared_statement = conn
+        .prepare(&sql)
+        .await
+        .map_err(|e| Error::Database(Box::new(e)))?;
+
+    let stream = conn
+        .query_raw(
+            &prepared_statement,
+            placeholder_values
+                .iter()
+                .map(|x| x.into())
+                .collect::<Vec<PgValue>>(),
+        )
+        .await
+        .map_err(|e| Error::Database(Box::new(e)))?;
+
+    pin_mut!(stream);
+
+    Ok(stream
+        .try_next()
+        .await
+        .map_err(|e| Error::Database(Box::new(e)))?
+        .map(|row| row.get::<_, PgValue>(0).into()))
+}

+ 434 - 0
crates/cdk-postgres/src/lib.rs

@@ -0,0 +1,434 @@
+use std::fmt::Debug;
+use std::marker::PhantomData;
+use std::sync::atomic::AtomicBool;
+use std::sync::{Arc, OnceLock};
+use std::time::Duration;
+
+use cdk_common::database::Error;
+use cdk_sql_base::database::{DatabaseConnector, DatabaseExecutor, DatabaseTransaction};
+use cdk_sql_base::mint::SQLMintAuthDatabase;
+use cdk_sql_base::pool::{Pool, PooledResource, ResourceManager};
+use cdk_sql_base::stmt::{Column, Statement};
+use cdk_sql_base::{SQLMintDatabase, SQLWalletDatabase};
+use db::{gn_pluck, pg_batch, pg_execute, pg_fetch_all, pg_fetch_one};
+use tokio::sync::{Mutex, Notify};
+use tokio::time::timeout;
+use tokio_postgres::{connect, Client, Error as PgError, NoTls};
+
+mod db;
+mod value;
+
+#[derive(Debug)]
+pub struct PgConnectionPool;
+
+#[derive(Clone)]
+pub enum SslMode {
+    NoTls(NoTls),
+    NativeTls(postgres_native_tls::MakeTlsConnector),
+}
+
+impl Default for SslMode {
+    fn default() -> Self {
+        SslMode::NoTls(NoTls {})
+    }
+}
+
+impl Debug for SslMode {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let debug_text = match self {
+            Self::NoTls(_) => "NoTls",
+            Self::NativeTls(_) => "NativeTls",
+        };
+
+        write!(f, "SslMode::{debug_text}")
+    }
+}
+
+/// Postgres configuration
+#[derive(Clone, Debug)]
+pub struct PgConfig {
+    url: String,
+    tls: SslMode,
+}
+
+/// A simple wrapper for the async connect, this would trigger the `connect` in another tokio task
+/// that would eventually resolve
+#[derive(Debug)]
+pub struct FutureConnect {
+    timeout: Duration,
+    error: Arc<Mutex<Option<cdk_common::database::Error>>>,
+    result: Arc<OnceLock<Client>>,
+    notify: Arc<Notify>,
+}
+
+impl FutureConnect {
+    /// Creates a new instance
+    pub fn new(config: PgConfig, timeout: Duration, still_valid: Arc<AtomicBool>) -> Self {
+        let failed = Arc::new(Mutex::new(None));
+        let result = Arc::new(OnceLock::new());
+        let notify = Arc::new(Notify::new());
+        let error_clone = failed.clone();
+        let result_clone = result.clone();
+        let notify_clone = notify.clone();
+
+        tokio::spawn(async move {
+            match config.tls {
+                SslMode::NoTls(tls) => {
+                    let (client, connection) = match connect(&config.url, tls).await {
+                        Ok((client, connection)) => (client, connection),
+                        Err(err) => {
+                            *error_clone.lock().await =
+                                Some(cdk_common::database::Error::Database(Box::new(err)));
+                            still_valid.store(false, std::sync::atomic::Ordering::Release);
+                            notify_clone.notify_waiters();
+                            return;
+                        }
+                    };
+
+                    tokio::spawn(async move {
+                        let _ = connection.await;
+                        still_valid.store(false, std::sync::atomic::Ordering::Release);
+                    });
+
+                    let _ = result_clone.set(client);
+                    notify_clone.notify_waiters();
+                }
+                SslMode::NativeTls(tls) => {
+                    let (client, connection) = match connect(&config.url, tls).await {
+                        Ok((client, connection)) => (client, connection),
+                        Err(err) => {
+                            *error_clone.lock().await =
+                                Some(cdk_common::database::Error::Database(Box::new(err)));
+                            still_valid.store(false, std::sync::atomic::Ordering::Release);
+                            notify_clone.notify_waiters();
+                            return;
+                        }
+                    };
+
+                    tokio::spawn(async move {
+                        let _ = connection.await;
+                        still_valid.store(false, std::sync::atomic::Ordering::Release);
+                    });
+
+                    let _ = result_clone.set(client);
+                    notify_clone.notify_waiters();
+                }
+            }
+        });
+
+        Self {
+            error: failed,
+            timeout,
+            result,
+            notify,
+        }
+    }
+
+    /// Gets the wrapped instance or the connection error. The connection is returned as reference,
+    /// and the actual error is returned once, next times a generic error would be returned
+    pub async fn client(&self) -> Result<&Client, cdk_common::database::Error> {
+        if let Some(client) = self.result.get() {
+            return Ok(client);
+        }
+
+        if let Some(error) = self.error.lock().await.take() {
+            return Err(error);
+        }
+
+        if timeout(self.timeout, self.notify.notified()).await.is_err() {
+            return Err(cdk_common::database::Error::Internal("Timeout".to_owned()));
+        }
+
+        // Check result again
+        if let Some(client) = self.result.get() {
+            Ok(client)
+        } else {
+            if let Some(error) = self.error.lock().await.take() {
+                Err(error)
+            } else {
+                Err(cdk_common::database::Error::Internal(
+                    "Failed connection".to_owned(),
+                ))
+            }
+        }
+    }
+}
+
+impl ResourceManager for PgConnectionPool {
+    type Config = PgConfig;
+
+    type Resource = FutureConnect;
+
+    type Error = PgError;
+
+    fn new_resource(
+        config: &Self::Config,
+        still_valid: Arc<AtomicBool>,
+        timeout: Duration,
+    ) -> Result<Self::Resource, cdk_sql_base::pool::Error<Self::Error>> {
+        Ok(FutureConnect::new(config.to_owned(), timeout, still_valid))
+    }
+}
+
+#[derive(Debug)]
+pub struct CdkPostgres {
+    pool: Arc<Pool<PgConnectionPool>>,
+}
+
+impl From<&str> for CdkPostgres {
+    fn from(value: &str) -> Self {
+        let config = PgConfig {
+            url: value.to_owned(),
+            tls: Default::default(),
+        };
+        let pool = Pool::<PgConnectionPool>::new(config, 10, Duration::from_secs(10));
+        CdkPostgres { pool }
+    }
+}
+
+pub struct CdkPostgresTx<'a> {
+    conn: Option<PooledResource<PgConnectionPool>>,
+    done: bool,
+    _phantom: PhantomData<&'a ()>,
+}
+
+impl<'a> Drop for CdkPostgresTx<'a> {
+    fn drop(&mut self) {
+        if let Some(conn) = self.conn.take() {
+            if !self.done {
+                tokio::spawn(async move {
+                    let _ = conn
+                        .client()
+                        .await
+                        .expect("client")
+                        .batch_execute("ROLLBACK")
+                        .await;
+                });
+            }
+        }
+    }
+}
+
+impl<'a> Debug for CdkPostgresTx<'a> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "PgTx")
+    }
+}
+
+#[async_trait::async_trait]
+impl DatabaseConnector for CdkPostgres {
+    type Transaction<'a> = CdkPostgresTx<'a>;
+
+    async fn begin(&self) -> Result<Self::Transaction<'_>, Error> {
+        let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
+
+        conn.client()
+            .await?
+            .batch_execute("BEGIN TRANSACTION")
+            .await
+            .map_err(|e| Error::Database(Box::new(e)))?;
+
+        Ok(Self::Transaction {
+            conn: Some(conn),
+            done: false,
+            _phantom: PhantomData,
+        })
+    }
+}
+
+#[async_trait::async_trait]
+impl<'a> DatabaseTransaction<'a> for CdkPostgresTx<'a> {
+    async fn commit(mut self) -> Result<(), Error> {
+        self.conn
+            .as_ref()
+            .ok_or(Error::Internal("Missing connection".to_owned()))?
+            .client()
+            .await?
+            .batch_execute("COMMIT")
+            .await
+            .map_err(|e| Error::Database(Box::new(e)))?;
+        self.done = true;
+        Ok(())
+    }
+
+    async fn rollback(mut self) -> Result<(), Error> {
+        self.conn
+            .as_ref()
+            .ok_or(Error::Internal("Missing connection".to_owned()))?
+            .client()
+            .await?
+            .batch_execute("ROLLBACK")
+            .await
+            .map_err(|e| Error::Database(Box::new(e)))?;
+        self.done = true;
+        Ok(())
+    }
+}
+
+#[async_trait::async_trait]
+impl<'a> DatabaseExecutor for CdkPostgresTx<'a> {
+    fn name() -> &'static str {
+        "postgres"
+    }
+
+    async fn execute(&self, statement: Statement) -> Result<usize, Error> {
+        pg_execute(
+            self.conn
+                .as_ref()
+                .ok_or(Error::Internal("Missing connection".to_owned()))?
+                .client()
+                .await?,
+            statement,
+        )
+        .await
+    }
+
+    async fn fetch_one(&self, statement: Statement) -> Result<Option<Vec<Column>>, Error> {
+        pg_fetch_one(
+            self.conn
+                .as_ref()
+                .ok_or(Error::Internal("Missing connection".to_owned()))?
+                .client()
+                .await?,
+            statement,
+        )
+        .await
+    }
+
+    async fn fetch_all(&self, statement: Statement) -> Result<Vec<Vec<Column>>, Error> {
+        pg_fetch_all(
+            self.conn
+                .as_ref()
+                .ok_or(Error::Internal("Missing connection".to_owned()))?
+                .client()
+                .await?,
+            statement,
+        )
+        .await
+    }
+
+    async fn pluck(&self, statement: Statement) -> Result<Option<Column>, Error> {
+        gn_pluck(
+            self.conn
+                .as_ref()
+                .ok_or(Error::Internal("Missing connection".to_owned()))?
+                .client()
+                .await?,
+            statement,
+        )
+        .await
+    }
+
+    async fn batch(&self, statement: Statement) -> Result<(), Error> {
+        pg_batch(
+            self.conn
+                .as_ref()
+                .ok_or(Error::Internal("Missing connection".to_owned()))?
+                .client()
+                .await?,
+            statement,
+        )
+        .await
+    }
+}
+
+#[async_trait::async_trait]
+impl DatabaseExecutor for CdkPostgres {
+    fn name() -> &'static str {
+        "postgres"
+    }
+
+    async fn execute(&self, statement: Statement) -> Result<usize, Error> {
+        pg_execute(
+            self.pool
+                .get()
+                .map_err(|e| Error::Database(Box::new(e)))?
+                .client()
+                .await?,
+            statement,
+        )
+        .await
+    }
+
+    async fn fetch_one(&self, statement: Statement) -> Result<Option<Vec<Column>>, Error> {
+        pg_fetch_one(
+            self.pool
+                .get()
+                .map_err(|e| Error::Database(Box::new(e)))?
+                .client()
+                .await?,
+            statement,
+        )
+        .await
+    }
+
+    async fn fetch_all(&self, statement: Statement) -> Result<Vec<Vec<Column>>, Error> {
+        pg_fetch_all(
+            self.pool
+                .get()
+                .map_err(|e| Error::Database(Box::new(e)))?
+                .client()
+                .await?,
+            statement,
+        )
+        .await
+    }
+
+    async fn pluck(&self, statement: Statement) -> Result<Option<Column>, Error> {
+        gn_pluck(
+            self.pool
+                .get()
+                .map_err(|e| Error::Database(Box::new(e)))?
+                .client()
+                .await?,
+            statement,
+        )
+        .await
+    }
+
+    async fn batch(&self, statement: Statement) -> Result<(), Error> {
+        pg_batch(
+            self.pool
+                .get()
+                .map_err(|e| Error::Database(Box::new(e)))?
+                .client()
+                .await?,
+            statement,
+        )
+        .await
+    }
+}
+
+/// Mint DB implementation with PostgreSQL
+pub type MintPgDatabase = SQLMintDatabase<CdkPostgres>;
+
+/// Mint Auth database with Postgres
+#[cfg(feature = "auth")]
+pub type MintPgAuthDatabase = SQLMintAuthDatabase<CdkPostgres>;
+
+/// Mint DB implementation with PostgresSQL
+pub type WalletPgDatabase = SQLWalletDatabase<CdkPostgres>;
+
+#[cfg(test)]
+mod test {
+    use cdk_common::mint_db_test;
+    use once_cell::sync::Lazy;
+    use tokio::sync::Mutex;
+
+    use super::*;
+
+    static MIGRATION_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
+
+    async fn provide_db() -> MintPgDatabase {
+        let m = MIGRATION_LOCK.lock().await;
+        let db_url = std::env::var("DATABASE_URL")
+            .unwrap_or("host=localhost user=test password=test dbname=testdb port=5433".to_owned());
+        let db = MintPgDatabase::new(db_url.as_str())
+            .await
+            .expect("database");
+        drop(m);
+        db
+    }
+
+    mint_db_test!(provide_db);
+}

+ 130 - 0
crates/cdk-postgres/src/value.rs

@@ -0,0 +1,130 @@
+use std::fmt::Debug;
+
+use cdk_sql_base::value::Value;
+use tokio_postgres::types::{self, FromSql, ToSql};
+
+#[derive(Debug)]
+pub enum PgValue<'a> {
+    Null,
+    Integer(i64),
+    Real(f64),
+    Text(&'a str),
+    Blob(&'a [u8]),
+}
+
+impl<'a> From<&'a Value> for PgValue<'a> {
+    fn from(value: &'a Value) -> Self {
+        match value {
+            Value::Blob(b) => PgValue::Blob(b),
+            Value::Text(text) => PgValue::Text(text.as_str()),
+            Value::Null => PgValue::Null,
+            Value::Integer(i) => PgValue::Integer(*i),
+            Value::Real(r) => PgValue::Real(*r),
+        }
+    }
+}
+
+impl<'a> From<PgValue<'a>> for Value {
+    fn from(val: PgValue<'a>) -> Self {
+        match val {
+            PgValue::Blob(value) => Value::Blob(value.to_owned()),
+            PgValue::Text(value) => Value::Text(value.to_owned()),
+            PgValue::Null => Value::Null,
+            PgValue::Integer(n) => Value::Integer(n),
+            PgValue::Real(r) => Value::Real(r),
+        }
+    }
+}
+
+impl<'a> FromSql<'a> for PgValue<'a> {
+    fn accepts(_ty: &types::Type) -> bool {
+        true
+    }
+
+    fn from_sql(
+        ty: &types::Type,
+        raw: &'a [u8],
+    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
+        Ok(match *ty {
+            types::Type::VARCHAR | types::Type::TEXT | types::Type::BPCHAR | types::Type::NAME => {
+                PgValue::Text(<&str as FromSql>::from_sql(ty, raw)?)
+            }
+            types::Type::BOOL => PgValue::Integer(if <bool as FromSql>::from_sql(ty, raw)? {
+                1
+            } else {
+                0
+            }),
+            types::Type::INT2 | types::Type::INT4 | types::Type::INT8 => {
+                PgValue::Integer(<i64 as FromSql>::from_sql(ty, raw)?)
+            }
+            types::Type::BIT_ARRAY | types::Type::BYTEA | types::Type::UNKNOWN => {
+                PgValue::Blob(<&[u8] as FromSql>::from_sql(ty, raw)?)
+            }
+            _ => panic!("Unsupported type {ty:?}"),
+        })
+    }
+
+    fn from_sql_null(_ty: &types::Type) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
+        Ok(PgValue::Null)
+    }
+}
+
+impl ToSql for PgValue<'_> {
+    fn to_sql(
+        &self,
+        ty: &types::Type,
+        out: &mut types::private::BytesMut,
+    ) -> Result<types::IsNull, Box<dyn std::error::Error + Sync + Send>>
+    where
+        Self: Sized,
+    {
+        match self {
+            PgValue::Blob(blob) => blob.to_sql(ty, out),
+            PgValue::Text(text) => text.to_sql(ty, out),
+            PgValue::Null => Ok(types::IsNull::Yes),
+            PgValue::Real(r) => r.to_sql(ty, out),
+            PgValue::Integer(i) => match *ty {
+                types::Type::BOOL => (*i != 0).to_sql(ty, out),
+                types::Type::INT2 => (*i as i16).to_sql(ty, out),
+                types::Type::INT4 => (*i as i32).to_sql(ty, out),
+                _ => i.to_sql_checked(ty, out),
+            },
+        }
+    }
+
+    fn accepts(_ty: &types::Type) -> bool
+    where
+        Self: Sized,
+    {
+        true
+    }
+
+    fn encode_format(&self, ty: &types::Type) -> types::Format {
+        match self {
+            PgValue::Blob(blob) => blob.encode_format(ty),
+            PgValue::Text(text) => text.encode_format(ty),
+            PgValue::Null => types::Format::Text,
+            PgValue::Real(r) => r.encode_format(ty),
+            PgValue::Integer(i) => i.encode_format(ty),
+        }
+    }
+
+    fn to_sql_checked(
+        &self,
+        ty: &types::Type,
+        out: &mut types::private::BytesMut,
+    ) -> Result<types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
+        match self {
+            PgValue::Blob(blob) => blob.to_sql_checked(ty, out),
+            PgValue::Text(text) => text.to_sql_checked(ty, out),
+            PgValue::Null => Ok(types::IsNull::Yes),
+            PgValue::Real(r) => r.to_sql_checked(ty, out),
+            PgValue::Integer(i) => match *ty {
+                types::Type::BOOL => (*i != 0).to_sql_checked(ty, out),
+                types::Type::INT2 => (*i as i16).to_sql_checked(ty, out),
+                types::Type::INT4 => (*i as i32).to_sql_checked(ty, out),
+                _ => i.to_sql_checked(ty, out),
+            },
+        }
+    }
+}

+ 26 - 0
crates/cdk-postgres/start_db_for_test.sh

@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+CONTAINER_NAME="rust-test-pg"
+DB_USER="test"
+DB_PASS="test"
+DB_NAME="testdb"
+DB_PORT="5433"
+
+echo "Starting fresh PostgreSQL container..."
+docker run -d --rm \
+  --name "${CONTAINER_NAME}" \
+  -e POSTGRES_USER="${DB_USER}" \
+  -e POSTGRES_PASSWORD="${DB_PASS}" \
+  -e POSTGRES_DB="${DB_NAME}" \
+  -p ${DB_PORT}:5432 \
+  postgres:16
+
+echo "Waiting for PostgreSQL to be ready and database '${DB_NAME}' to exist..."
+until docker exec -e PGPASSWORD="${DB_PASS}" "${CONTAINER_NAME}" \
+    psql -U "${DB_USER}" -d "${DB_NAME}" -c "SELECT 1;" >/dev/null 2>&1; do
+  sleep 0.5
+done
+
+export DATABASE_URL="host=localhost user=${DB_USER} password=${DB_PASS} dbname=${DB_NAME} port=${DB_PORT}"
+

+ 4 - 2
crates/cdk-sql-base/build.rs

@@ -18,16 +18,18 @@ fn main() {
         let dest_path = parent.join("migrations.rs");
         let dest_path = parent.join("migrations.rs");
         let mut out_file = File::create(&dest_path).expect("Failed to create migrations.rs");
         let mut out_file = File::create(&dest_path).expect("Failed to create migrations.rs");
 
 
+        let skip_name = migration_path.to_str().unwrap_or_default().len();
+
         writeln!(out_file, "/// @generated").unwrap();
         writeln!(out_file, "/// @generated").unwrap();
         writeln!(out_file, "/// Auto-generated by build.rs").unwrap();
         writeln!(out_file, "/// Auto-generated by build.rs").unwrap();
         writeln!(out_file, "pub static MIGRATIONS: &[(&str, &str)] = &[").unwrap();
         writeln!(out_file, "pub static MIGRATIONS: &[(&str, &str)] = &[").unwrap();
 
 
         for path in &files {
         for path in &files {
-            let name = path.file_name().unwrap().to_string_lossy();
+            let rel_name = &path.to_str().unwrap().replace("\\", "/")[skip_name + 1..]; // for Windows
             let rel_path = &path.to_str().unwrap().replace("\\", "/")[skip_path..]; // for Windows
             let rel_path = &path.to_str().unwrap().replace("\\", "/")[skip_path..]; // for Windows
             writeln!(
             writeln!(
                 out_file,
                 out_file,
-                "    (\"{name}\", include_str!(r#\".{rel_path}\"#)),"
+                "    (\"{rel_name}\", include_str!(r#\".{rel_path}\"#)),"
             )
             )
             .unwrap();
             .unwrap();
         }
         }

+ 0 - 38
crates/cdk-sql-base/run_test.sh

@@ -1,38 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-CONTAINER_NAME="rust-test-pg"
-DB_USER="test"
-DB_PASS="test"
-DB_NAME="testdb"
-DB_PORT="5433"
-DB_URL="postgres://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/${DB_NAME}"
-
-cleanup() {
-  echo "Cleaning up..."
-  docker stop "${CONTAINER_NAME}" >/dev/null 2>&1 || true
-  docker rm "${CONTAINER_NAME}" >/dev/null 2>&1 || true
-}
-
-trap cleanup EXIT INT TERM
-
-echo "Starting fresh PostgreSQL container..."
-docker run -d --rm \
-  --name "${CONTAINER_NAME}" \
-  -e POSTGRES_USER="${DB_USER}" \
-  -e POSTGRES_PASSWORD="${DB_PASS}" \
-  -e POSTGRES_DB="${DB_NAME}" \
-  -p ${DB_PORT}:5432 \
-  -v "${PWD}/.docker/pg-init.sql:/docker-entrypoint-initdb.d/init.sql:ro" \
-  postgres:16
-
-echo "Waiting for PostgreSQL to be ready..."
-until docker exec "${CONTAINER_NAME}" pg_isready -U "${DB_USER}" >/dev/null 2>&1; do
-  sleep 0.5
-done
-
-export DATABASE_URL="${DB_URL}"
-
-echo "Running cargo tests..."
-cargo test
-

+ 7 - 18
crates/cdk-sql-base/src/common.rs

@@ -5,6 +5,7 @@ use crate::stmt::query;
 #[inline(always)]
 #[inline(always)]
 pub async fn migrate<C: DatabaseExecutor>(
 pub async fn migrate<C: DatabaseExecutor>(
     conn: &C,
     conn: &C,
+    db_prefix: &str,
     migrations: &[(&str, &str)],
     migrations: &[(&str, &str)],
 ) -> Result<(), cdk_common::database::Error> {
 ) -> Result<(), cdk_common::database::Error> {
     query(
     query(
@@ -18,26 +19,14 @@ pub async fn migrate<C: DatabaseExecutor>(
     .execute(conn)
     .execute(conn)
     .await?;
     .await?;
 
 
-    /*if query(
-        r#"select count(*) from sqlite_master where name = '_sqlx_migrations'"#,
-        [],
-        |row| row.get::<_, i32>(0),
-    )? == 1
-    {
-        tx.execute_batch(
-            r#"
-        INSERT INTO migrations
-        SELECT
-            version || '_' ||  REPLACE(description, ' ', '_') || '.sql',
-            execution_time
-        FROM _sqlx_migrations;
-        DROP TABLE _sqlx_migrations;
-        "#,
-        )?;
-    }*/
-
     // Apply each migration if it hasn’t been applied yet
     // Apply each migration if it hasn’t been applied yet
     for (name, sql) in migrations {
     for (name, sql) in migrations {
+        if let Some((prefix, _)) = name.split_once(['/', '\\']) {
+            if prefix != db_prefix {
+                continue;
+            }
+        }
+
         let is_missing = query("SELECT name FROM migrations WHERE name = :name")?
         let is_missing = query("SELECT name FROM migrations WHERE name = :name")?
             .bind("name", name)
             .bind("name", name)
             .pluck(conn)
             .pluck(conn)

+ 3 - 0
crates/cdk-sql-base/src/database.rs

@@ -11,6 +11,9 @@ use crate::stmt::{Column, Statement};
 /// This trait defines the expectations of a database execution
 /// This trait defines the expectations of a database execution
 #[async_trait::async_trait]
 #[async_trait::async_trait]
 pub trait DatabaseExecutor: Debug + Sync + Send {
 pub trait DatabaseExecutor: Debug + Sync + Send {
+    /// Database driver name
+    fn name() -> &'static str;
+
     /// Executes a query and returns the affected rows
     /// Executes a query and returns the affected rows
     async fn execute(&self, statement: Statement) -> Result<usize, Error>;
     async fn execute(&self, statement: Statement) -> Result<usize, Error>;
 
 

+ 1 - 2
crates/cdk-sql-base/src/mint/auth/migrations.rs

@@ -1,6 +1,5 @@
 /// @generated
 /// @generated
 /// Auto-generated by build.rs
 /// Auto-generated by build.rs
 pub static MIGRATIONS: &[(&str, &str)] = &[
 pub static MIGRATIONS: &[(&str, &str)] = &[
-    ("20250109143347_init.sql", include_str!(r#"./migrations/20250109143347_init.sql"#)),
-    ("20250109143347_init.sql", include_str!(r#"./migrations/sqlite/20250109143347_init.sql"#)),
+    ("sqlite/20250109143347_init.sql", include_str!(r#"./migrations/sqlite/20250109143347_init.sql"#)),
 ];
 ];

+ 0 - 0
crates/cdk-sql-base/src/mint/auth/migrations/20250109143347_init.sql → crates/cdk-sql-base/src/mint/auth/migrations/sqlite/20250109143347_init.sql


+ 1 - 1
crates/cdk-sql-base/src/mint/auth/mod.rs

@@ -45,7 +45,7 @@ where
     /// Migrate
     /// Migrate
     async fn migrate(conn: &DB) -> Result<(), Error> {
     async fn migrate(conn: &DB) -> Result<(), Error> {
         let tx = conn.begin().await?;
         let tx = conn.begin().await?;
-        migrate(&tx, MIGRATIONS).await?;
+        migrate(&tx, DB::name(), MIGRATIONS).await?;
         tx.commit().await?;
         tx.commit().await?;
         Ok(())
         Ok(())
     }
     }

+ 22 - 42
crates/cdk-sql-base/src/mint/migrations.rs

@@ -1,46 +1,26 @@
 /// @generated
 /// @generated
 /// Auto-generated by build.rs
 /// Auto-generated by build.rs
 pub static MIGRATIONS: &[(&str, &str)] = &[
 pub static MIGRATIONS: &[(&str, &str)] = &[
-    ("20240612124932_init.sql", include_str!(r#"./migrations/20240612124932_init.sql"#)),
-    ("20240618195700_quote_state.sql", include_str!(r#"./migrations/20240618195700_quote_state.sql"#)),
-    ("20240626092101_nut04_state.sql", include_str!(r#"./migrations/20240626092101_nut04_state.sql"#)),
-    ("20240703122347_request_lookup_id.sql", include_str!(r#"./migrations/20240703122347_request_lookup_id.sql"#)),
-    ("20240710145043_input_fee.sql", include_str!(r#"./migrations/20240710145043_input_fee.sql"#)),
-    ("20240711183109_derivation_path_index.sql", include_str!(r#"./migrations/20240711183109_derivation_path_index.sql"#)),
-    ("20240718203721_allow_unspent.sql", include_str!(r#"./migrations/20240718203721_allow_unspent.sql"#)),
-    ("20240811031111_update_mint_url.sql", include_str!(r#"./migrations/20240811031111_update_mint_url.sql"#)),
-    ("20240919103407_proofs_quote_id.sql", include_str!(r#"./migrations/20240919103407_proofs_quote_id.sql"#)),
-    ("20240923153640_melt_requests.sql", include_str!(r#"./migrations/20240923153640_melt_requests.sql"#)),
-    ("20240930101140_dleq_for_sigs.sql", include_str!(r#"./migrations/20240930101140_dleq_for_sigs.sql"#)),
-    ("20241108093102_mint_mint_quote_pubkey.sql", include_str!(r#"./migrations/20241108093102_mint_mint_quote_pubkey.sql"#)),
-    ("20250103201327_amount_to_pay_msats.sql", include_str!(r#"./migrations/20250103201327_amount_to_pay_msats.sql"#)),
-    ("20250129200912_remove_mint_url.sql", include_str!(r#"./migrations/20250129200912_remove_mint_url.sql"#)),
-    ("20250129230326_add_config_table.sql", include_str!(r#"./migrations/20250129230326_add_config_table.sql"#)),
-    ("20250307213652_keyset_id_as_foreign_key.sql", include_str!(r#"./migrations/20250307213652_keyset_id_as_foreign_key.sql"#)),
-    ("20250406091754_mint_time_of_quotes.sql", include_str!(r#"./migrations/20250406091754_mint_time_of_quotes.sql"#)),
-    ("20250406093755_mint_created_time_signature.sql", include_str!(r#"./migrations/20250406093755_mint_created_time_signature.sql"#)),
-    ("20250415093121_drop_keystore_foreign.sql", include_str!(r#"./migrations/20250415093121_drop_keystore_foreign.sql"#)),
-    ("20250626120251_rename_blind_message_y_to_b.sql", include_str!(r#"./migrations/20250626120251_rename_blind_message_y_to_b.sql"#)),
-    ("20250710212391_init.sql", include_str!(r#"./migrations/postgres/20250710212391_init.sql"#)),
-    ("1_fix_sqlx_migration.sql", include_str!(r#"./migrations/sqlite/1_fix_sqlx_migration.sql"#)),
-    ("20240612124932_init.sql", include_str!(r#"./migrations/sqlite/20240612124932_init.sql"#)),
-    ("20240618195700_quote_state.sql", include_str!(r#"./migrations/sqlite/20240618195700_quote_state.sql"#)),
-    ("20240626092101_nut04_state.sql", include_str!(r#"./migrations/sqlite/20240626092101_nut04_state.sql"#)),
-    ("20240703122347_request_lookup_id.sql", include_str!(r#"./migrations/sqlite/20240703122347_request_lookup_id.sql"#)),
-    ("20240710145043_input_fee.sql", include_str!(r#"./migrations/sqlite/20240710145043_input_fee.sql"#)),
-    ("20240711183109_derivation_path_index.sql", include_str!(r#"./migrations/sqlite/20240711183109_derivation_path_index.sql"#)),
-    ("20240718203721_allow_unspent.sql", include_str!(r#"./migrations/sqlite/20240718203721_allow_unspent.sql"#)),
-    ("20240811031111_update_mint_url.sql", include_str!(r#"./migrations/sqlite/20240811031111_update_mint_url.sql"#)),
-    ("20240919103407_proofs_quote_id.sql", include_str!(r#"./migrations/sqlite/20240919103407_proofs_quote_id.sql"#)),
-    ("20240923153640_melt_requests.sql", include_str!(r#"./migrations/sqlite/20240923153640_melt_requests.sql"#)),
-    ("20240930101140_dleq_for_sigs.sql", include_str!(r#"./migrations/sqlite/20240930101140_dleq_for_sigs.sql"#)),
-    ("20241108093102_mint_mint_quote_pubkey.sql", include_str!(r#"./migrations/sqlite/20241108093102_mint_mint_quote_pubkey.sql"#)),
-    ("20250103201327_amount_to_pay_msats.sql", include_str!(r#"./migrations/sqlite/20250103201327_amount_to_pay_msats.sql"#)),
-    ("20250129200912_remove_mint_url.sql", include_str!(r#"./migrations/sqlite/20250129200912_remove_mint_url.sql"#)),
-    ("20250129230326_add_config_table.sql", include_str!(r#"./migrations/sqlite/20250129230326_add_config_table.sql"#)),
-    ("20250307213652_keyset_id_as_foreign_key.sql", include_str!(r#"./migrations/sqlite/20250307213652_keyset_id_as_foreign_key.sql"#)),
-    ("20250406091754_mint_time_of_quotes.sql", include_str!(r#"./migrations/sqlite/20250406091754_mint_time_of_quotes.sql"#)),
-    ("20250406093755_mint_created_time_signature.sql", include_str!(r#"./migrations/sqlite/20250406093755_mint_created_time_signature.sql"#)),
-    ("20250415093121_drop_keystore_foreign.sql", include_str!(r#"./migrations/sqlite/20250415093121_drop_keystore_foreign.sql"#)),
-    ("20250626120251_rename_blind_message_y_to_b.sql", include_str!(r#"./migrations/sqlite/20250626120251_rename_blind_message_y_to_b.sql"#)),
+    ("postgres/20250710212391_init.sql", include_str!(r#"./migrations/postgres/20250710212391_init.sql"#)),
+    ("sqlite/1_fix_sqlx_migration.sql", include_str!(r#"./migrations/sqlite/1_fix_sqlx_migration.sql"#)),
+    ("sqlite/20240612124932_init.sql", include_str!(r#"./migrations/sqlite/20240612124932_init.sql"#)),
+    ("sqlite/20240618195700_quote_state.sql", include_str!(r#"./migrations/sqlite/20240618195700_quote_state.sql"#)),
+    ("sqlite/20240626092101_nut04_state.sql", include_str!(r#"./migrations/sqlite/20240626092101_nut04_state.sql"#)),
+    ("sqlite/20240703122347_request_lookup_id.sql", include_str!(r#"./migrations/sqlite/20240703122347_request_lookup_id.sql"#)),
+    ("sqlite/20240710145043_input_fee.sql", include_str!(r#"./migrations/sqlite/20240710145043_input_fee.sql"#)),
+    ("sqlite/20240711183109_derivation_path_index.sql", include_str!(r#"./migrations/sqlite/20240711183109_derivation_path_index.sql"#)),
+    ("sqlite/20240718203721_allow_unspent.sql", include_str!(r#"./migrations/sqlite/20240718203721_allow_unspent.sql"#)),
+    ("sqlite/20240811031111_update_mint_url.sql", include_str!(r#"./migrations/sqlite/20240811031111_update_mint_url.sql"#)),
+    ("sqlite/20240919103407_proofs_quote_id.sql", include_str!(r#"./migrations/sqlite/20240919103407_proofs_quote_id.sql"#)),
+    ("sqlite/20240923153640_melt_requests.sql", include_str!(r#"./migrations/sqlite/20240923153640_melt_requests.sql"#)),
+    ("sqlite/20240930101140_dleq_for_sigs.sql", include_str!(r#"./migrations/sqlite/20240930101140_dleq_for_sigs.sql"#)),
+    ("sqlite/20241108093102_mint_mint_quote_pubkey.sql", include_str!(r#"./migrations/sqlite/20241108093102_mint_mint_quote_pubkey.sql"#)),
+    ("sqlite/20250103201327_amount_to_pay_msats.sql", include_str!(r#"./migrations/sqlite/20250103201327_amount_to_pay_msats.sql"#)),
+    ("sqlite/20250129200912_remove_mint_url.sql", include_str!(r#"./migrations/sqlite/20250129200912_remove_mint_url.sql"#)),
+    ("sqlite/20250129230326_add_config_table.sql", include_str!(r#"./migrations/sqlite/20250129230326_add_config_table.sql"#)),
+    ("sqlite/20250307213652_keyset_id_as_foreign_key.sql", include_str!(r#"./migrations/sqlite/20250307213652_keyset_id_as_foreign_key.sql"#)),
+    ("sqlite/20250406091754_mint_time_of_quotes.sql", include_str!(r#"./migrations/sqlite/20250406091754_mint_time_of_quotes.sql"#)),
+    ("sqlite/20250406093755_mint_created_time_signature.sql", include_str!(r#"./migrations/sqlite/20250406093755_mint_created_time_signature.sql"#)),
+    ("sqlite/20250415093121_drop_keystore_foreign.sql", include_str!(r#"./migrations/sqlite/20250415093121_drop_keystore_foreign.sql"#)),
+    ("sqlite/20250626120251_rename_blind_message_y_to_b.sql", include_str!(r#"./migrations/sqlite/20250626120251_rename_blind_message_y_to_b.sql"#)),
 ];
 ];

+ 82 - 0
crates/cdk-sql-base/src/mint/migrations/postgres/20250710212391_init.sql

@@ -0,0 +1,82 @@
+CREATE TABLE keyset (
+  id TEXT PRIMARY KEY, unit TEXT NOT NULL,
+  active BOOL NOT NULL, valid_from INTEGER NOT NULL,
+  valid_to INTEGER, derivation_path TEXT NOT NULL,
+  max_order INTEGER NOT NULL, input_fee_ppk INTEGER,
+  derivation_path_index INTEGER
+);
+CREATE TABLE mint_quote (
+  id TEXT PRIMARY KEY,
+  amount INTEGER NOT NULL,
+  unit TEXT NOT NULL,
+  request TEXT NOT NULL,
+  expiry INTEGER NOT NULL,
+  state TEXT CHECK (
+    state IN (
+      'UNPAID', 'PENDING', 'PAID', 'ISSUED'
+    )
+  ) NOT NULL DEFAULT 'UNPAID',
+  request_lookup_id TEXT,
+  pubkey TEXT,
+  created_time INTEGER NOT NULL DEFAULT 0,
+  paid_time INTEGER,
+  issued_time INTEGER
+);
+CREATE TABLE melt_quote (
+  id TEXT PRIMARY KEY,
+  unit TEXT NOT NULL,
+  amount INTEGER NOT NULL,
+  request TEXT NOT NULL,
+  fee_reserve INTEGER NOT NULL,
+  expiry INTEGER NOT NULL,
+  state TEXT CHECK (
+    state IN ('UNPAID', 'PENDING', 'PAID')
+  ) NOT NULL DEFAULT 'UNPAID',
+  payment_preimage TEXT,
+  request_lookup_id TEXT,
+  msat_to_pay INTEGER,
+  created_time INTEGER NOT NULL DEFAULT 0,
+  paid_time INTEGER
+);
+CREATE TABLE melt_request (
+  id TEXT PRIMARY KEY, inputs TEXT NOT NULL,
+  outputs TEXT, method TEXT NOT NULL,
+  unit TEXT NOT NULL
+);
+CREATE TABLE config (
+  id TEXT PRIMARY KEY, value TEXT NOT NULL
+);
+CREATE TABLE IF NOT EXISTS "proof" (
+  y BYTEA PRIMARY KEY,
+  amount INTEGER NOT NULL,
+  keyset_id TEXT NOT NULL,
+  secret TEXT NOT NULL,
+  c BYTEA NOT NULL,
+  witness TEXT,
+  state TEXT CHECK (
+    state IN (
+      'SPENT', 'PENDING', 'UNSPENT', 'RESERVED',
+      'UNKNOWN'
+    )
+  ) NOT NULL,
+  quote_id TEXT,
+  created_time INTEGER NOT NULL DEFAULT 0
+);
+CREATE TABLE IF NOT EXISTS "blind_signature" (
+  y BYTEA PRIMARY KEY,
+  amount INTEGER NOT NULL,
+  keyset_id TEXT NOT NULL,
+  c BYTEA NOT NULL,
+  dleq_e TEXT,
+  dleq_s TEXT,
+  quote_id TEXT,
+  created_time INTEGER NOT NULL DEFAULT 0
+);
+CREATE INDEX unit_index ON keyset(unit);
+CREATE INDEX active_index ON keyset(active);
+CREATE INDEX request_index ON mint_quote(request);
+CREATE INDEX expiry_index ON mint_quote(expiry);
+CREATE INDEX melt_quote_state_index ON melt_quote(state);
+CREATE INDEX mint_quote_state_index ON mint_quote(state);
+CREATE UNIQUE INDEX unique_request_lookup_id_mint ON mint_quote(request_lookup_id);
+CREATE UNIQUE INDEX unique_request_lookup_id_melt ON melt_quote(request_lookup_id);

+ 20 - 0
crates/cdk-sql-base/src/mint/migrations/sqlite/1_fix_sqlx_migration.sql

@@ -0,0 +1,20 @@
+-- Migrate `_sqlx_migrations` to our new migration system
+CREATE TABLE IF NOT EXISTS _sqlx_migrations AS
+SELECT
+    '' AS version,
+    '' AS description,
+    0 AS execution_time
+WHERE 0;
+
+INSERT INTO migrations
+SELECT
+    version || '_' ||  REPLACE(description, ' ', '_') || '.sql',
+    execution_time
+FROM _sqlx_migrations
+WHERE EXISTS (
+    SELECT 1
+    FROM sqlite_master
+    WHERE type = 'table' AND name = '_sqlx_migrations'
+);
+
+DROP TABLE _sqlx_migrations;

+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20240612124932_init.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20240612124932_init.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20240618195700_quote_state.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20240618195700_quote_state.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20240626092101_nut04_state.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20240626092101_nut04_state.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20240703122347_request_lookup_id.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20240703122347_request_lookup_id.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20240710145043_input_fee.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20240710145043_input_fee.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20240711183109_derivation_path_index.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20240711183109_derivation_path_index.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20240718203721_allow_unspent.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20240718203721_allow_unspent.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20240811031111_update_mint_url.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20240811031111_update_mint_url.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20240919103407_proofs_quote_id.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20240919103407_proofs_quote_id.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20240923153640_melt_requests.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20240923153640_melt_requests.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20240930101140_dleq_for_sigs.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20240930101140_dleq_for_sigs.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20241108093102_mint_mint_quote_pubkey.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20241108093102_mint_mint_quote_pubkey.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20250103201327_amount_to_pay_msats.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20250103201327_amount_to_pay_msats.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20250129200912_remove_mint_url.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20250129200912_remove_mint_url.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20250129230326_add_config_table.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20250129230326_add_config_table.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20250307213652_keyset_id_as_foreign_key.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20250307213652_keyset_id_as_foreign_key.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20250406091754_mint_time_of_quotes.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20250406091754_mint_time_of_quotes.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20250406093755_mint_created_time_signature.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20250406093755_mint_created_time_signature.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20250415093121_drop_keystore_foreign.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20250415093121_drop_keystore_foreign.sql


+ 0 - 0
crates/cdk-sql-base/src/mint/migrations/20250626120251_rename_blind_message_y_to_b.sql → crates/cdk-sql-base/src/mint/migrations/sqlite/20250626120251_rename_blind_message_y_to_b.sql


+ 10 - 4
crates/cdk-sql-base/src/mint/mod.rs

@@ -129,7 +129,7 @@ where
     /// Migrate
     /// Migrate
     async fn migrate(conn: &DB) -> Result<(), Error> {
     async fn migrate(conn: &DB) -> Result<(), Error> {
         let tx = conn.begin().await?;
         let tx = conn.begin().await?;
-        migrate(&tx, MIGRATIONS).await?;
+        migrate(&tx, DB::name(), MIGRATIONS).await?;
         tx.commit().await?;
         tx.commit().await?;
         Ok(())
         Ok(())
     }
     }
@@ -472,6 +472,7 @@ where
             WHERE
             WHERE
                 id=:id
                 id=:id
                 AND state != :state
                 AND state != :state
+            FOR UPDATE
             "#,
             "#,
         )?
         )?
         .bind("id", quote_id.as_hyphenated().to_string())
         .bind("id", quote_id.as_hyphenated().to_string())
@@ -547,7 +548,9 @@ where
                 issued_time
                 issued_time
             FROM
             FROM
                 mint_quote
                 mint_quote
-            WHERE id = :id"#,
+            WHERE id = :id
+            FOR UPDATE
+            "#,
         )?
         )?
         .bind("id", quote_id.as_hyphenated().to_string())
         .bind("id", quote_id.as_hyphenated().to_string())
         .fetch_one(&self.inner)
         .fetch_one(&self.inner)
@@ -640,6 +643,7 @@ where
                 melt_quote
                 melt_quote
             WHERE
             WHERE
                 id=:id
                 id=:id
+            FOR UPDATE
             "#,
             "#,
         )?
         )?
         .bind("id", quote_id.as_hyphenated().to_string())
         .bind("id", quote_id.as_hyphenated().to_string())
@@ -669,7 +673,9 @@ where
                 issued_time
                 issued_time
             FROM
             FROM
                 mint_quote
                 mint_quote
-            WHERE request = :request"#,
+            WHERE request = :request
+            FOR UPDATE
+            "#,
         )?
         )?
         .bind("request", request.to_owned())
         .bind("request", request.to_owned())
         .fetch_one(&self.inner)
         .fetch_one(&self.inner)
@@ -901,7 +907,7 @@ where
 
 
         // Check any previous proof, this query should return None in order to proceed storing
         // Check any previous proof, this query should return None in order to proceed storing
         // Any result here would error
         // Any result here would error
-        match query(r#"SELECT state FROM proof WHERE y IN (:ys) LIMIT 1"#)?
+        match query(r#"SELECT state FROM proof WHERE y IN (:ys) LIMIT 1 FOR UPDATE"#)?
             .bind_vec(
             .bind_vec(
                 "ys",
                 "ys",
                 proofs
                 proofs

+ 3 - 2
crates/cdk-sql-base/src/pool.rs

@@ -30,7 +30,7 @@ pub trait ResourceManager: Debug {
     type Resource: Debug;
     type Resource: Debug;
 
 
     /// The configuration that is needed in order to create the resource
     /// The configuration that is needed in order to create the resource
-    type Config: Debug;
+    type Config: Clone + Debug;
 
 
     /// The error the resource may return when creating a new instance
     /// The error the resource may return when creating a new instance
     type Error: Debug;
     type Error: Debug;
@@ -39,6 +39,7 @@ pub trait ResourceManager: Debug {
     fn new_resource(
     fn new_resource(
         config: &Self::Config,
         config: &Self::Config,
         still_valid: Arc<AtomicBool>,
         still_valid: Arc<AtomicBool>,
+        timeout: Duration,
     ) -> Result<Self::Resource, Error<Self::Error>>;
     ) -> Result<Self::Resource, Error<Self::Error>>;
 
 
     /// The object is dropped
     /// The object is dropped
@@ -158,7 +159,7 @@ where
                 return Ok(PooledResource {
                 return Ok(PooledResource {
                     resource: Some((
                     resource: Some((
                         still_valid.clone(),
                         still_valid.clone(),
-                        RM::new_resource(&self.config, still_valid)?,
+                        RM::new_resource(&self.config, still_valid, timeout)?,
                     )),
                     )),
                     pool: self.clone(),
                     pool: self.clone(),
                 });
                 });

+ 2 - 2
crates/cdk-sql-base/src/stmt.rs

@@ -186,10 +186,10 @@ impl Statement {
                         }
                         }
                     }
                     }
                 }
                 }
-                SqlPart::Raw(raw) => Ok(raw.to_string()),
+                SqlPart::Raw(raw) => Ok(raw.trim().to_string()),
             })
             })
             .collect::<Result<Vec<String>, _>>()?
             .collect::<Result<Vec<String>, _>>()?
-            .join("");
+            .join(" ");
 
 
         Ok((sql, placeholder_values))
         Ok((sql, placeholder_values))
     }
     }

+ 16 - 32
crates/cdk-sql-base/src/wallet/migrations.rs

@@ -1,36 +1,20 @@
 /// @generated
 /// @generated
 /// Auto-generated by build.rs
 /// Auto-generated by build.rs
 pub static MIGRATIONS: &[(&str, &str)] = &[
 pub static MIGRATIONS: &[(&str, &str)] = &[
-    ("20240612132920_init.sql", include_str!(r#"./migrations/20240612132920_init.sql"#)),
-    ("20240618200350_quote_state.sql", include_str!(r#"./migrations/20240618200350_quote_state.sql"#)),
-    ("20240626091921_nut04_state.sql", include_str!(r#"./migrations/20240626091921_nut04_state.sql"#)),
-    ("20240710144711_input_fee.sql", include_str!(r#"./migrations/20240710144711_input_fee.sql"#)),
-    ("20240810214105_mint_icon_url.sql", include_str!(r#"./migrations/20240810214105_mint_icon_url.sql"#)),
-    ("20240810233905_update_mint_url.sql", include_str!(r#"./migrations/20240810233905_update_mint_url.sql"#)),
-    ("20240902151515_icon_url.sql", include_str!(r#"./migrations/20240902151515_icon_url.sql"#)),
-    ("20240902210905_mint_time.sql", include_str!(r#"./migrations/20240902210905_mint_time.sql"#)),
-    ("20241011125207_mint_urls.sql", include_str!(r#"./migrations/20241011125207_mint_urls.sql"#)),
-    ("20241108092756_wallet_mint_quote_secretkey.sql", include_str!(r#"./migrations/20241108092756_wallet_mint_quote_secretkey.sql"#)),
-    ("20250214135017_mint_tos.sql", include_str!(r#"./migrations/20250214135017_mint_tos.sql"#)),
-    ("20250310111513_drop_nostr_last_checked.sql", include_str!(r#"./migrations/20250310111513_drop_nostr_last_checked.sql"#)),
-    ("20250314082116_allow_pending_spent.sql", include_str!(r#"./migrations/20250314082116_allow_pending_spent.sql"#)),
-    ("20250323152040_wallet_dleq_proofs.sql", include_str!(r#"./migrations/20250323152040_wallet_dleq_proofs.sql"#)),
-    ("20250401120000_add_transactions_table.sql", include_str!(r#"./migrations/20250401120000_add_transactions_table.sql"#)),
-    ("20250616144830_add_keyset_expiry.sql", include_str!(r#"./migrations/20250616144830_add_keyset_expiry.sql"#)),
-    ("20240612132920_init.sql", include_str!(r#"./migrations/sqlite/20240612132920_init.sql"#)),
-    ("20240618200350_quote_state.sql", include_str!(r#"./migrations/sqlite/20240618200350_quote_state.sql"#)),
-    ("20240626091921_nut04_state.sql", include_str!(r#"./migrations/sqlite/20240626091921_nut04_state.sql"#)),
-    ("20240710144711_input_fee.sql", include_str!(r#"./migrations/sqlite/20240710144711_input_fee.sql"#)),
-    ("20240810214105_mint_icon_url.sql", include_str!(r#"./migrations/sqlite/20240810214105_mint_icon_url.sql"#)),
-    ("20240810233905_update_mint_url.sql", include_str!(r#"./migrations/sqlite/20240810233905_update_mint_url.sql"#)),
-    ("20240902151515_icon_url.sql", include_str!(r#"./migrations/sqlite/20240902151515_icon_url.sql"#)),
-    ("20240902210905_mint_time.sql", include_str!(r#"./migrations/sqlite/20240902210905_mint_time.sql"#)),
-    ("20241011125207_mint_urls.sql", include_str!(r#"./migrations/sqlite/20241011125207_mint_urls.sql"#)),
-    ("20241108092756_wallet_mint_quote_secretkey.sql", include_str!(r#"./migrations/sqlite/20241108092756_wallet_mint_quote_secretkey.sql"#)),
-    ("20250214135017_mint_tos.sql", include_str!(r#"./migrations/sqlite/20250214135017_mint_tos.sql"#)),
-    ("20250310111513_drop_nostr_last_checked.sql", include_str!(r#"./migrations/sqlite/20250310111513_drop_nostr_last_checked.sql"#)),
-    ("20250314082116_allow_pending_spent.sql", include_str!(r#"./migrations/sqlite/20250314082116_allow_pending_spent.sql"#)),
-    ("20250323152040_wallet_dleq_proofs.sql", include_str!(r#"./migrations/sqlite/20250323152040_wallet_dleq_proofs.sql"#)),
-    ("20250401120000_add_transactions_table.sql", include_str!(r#"./migrations/sqlite/20250401120000_add_transactions_table.sql"#)),
-    ("20250616144830_add_keyset_expiry.sql", include_str!(r#"./migrations/sqlite/20250616144830_add_keyset_expiry.sql"#)),
+    ("sqlite/20240612132920_init.sql", include_str!(r#"./migrations/sqlite/20240612132920_init.sql"#)),
+    ("sqlite/20240618200350_quote_state.sql", include_str!(r#"./migrations/sqlite/20240618200350_quote_state.sql"#)),
+    ("sqlite/20240626091921_nut04_state.sql", include_str!(r#"./migrations/sqlite/20240626091921_nut04_state.sql"#)),
+    ("sqlite/20240710144711_input_fee.sql", include_str!(r#"./migrations/sqlite/20240710144711_input_fee.sql"#)),
+    ("sqlite/20240810214105_mint_icon_url.sql", include_str!(r#"./migrations/sqlite/20240810214105_mint_icon_url.sql"#)),
+    ("sqlite/20240810233905_update_mint_url.sql", include_str!(r#"./migrations/sqlite/20240810233905_update_mint_url.sql"#)),
+    ("sqlite/20240902151515_icon_url.sql", include_str!(r#"./migrations/sqlite/20240902151515_icon_url.sql"#)),
+    ("sqlite/20240902210905_mint_time.sql", include_str!(r#"./migrations/sqlite/20240902210905_mint_time.sql"#)),
+    ("sqlite/20241011125207_mint_urls.sql", include_str!(r#"./migrations/sqlite/20241011125207_mint_urls.sql"#)),
+    ("sqlite/20241108092756_wallet_mint_quote_secretkey.sql", include_str!(r#"./migrations/sqlite/20241108092756_wallet_mint_quote_secretkey.sql"#)),
+    ("sqlite/20250214135017_mint_tos.sql", include_str!(r#"./migrations/sqlite/20250214135017_mint_tos.sql"#)),
+    ("sqlite/20250310111513_drop_nostr_last_checked.sql", include_str!(r#"./migrations/sqlite/20250310111513_drop_nostr_last_checked.sql"#)),
+    ("sqlite/20250314082116_allow_pending_spent.sql", include_str!(r#"./migrations/sqlite/20250314082116_allow_pending_spent.sql"#)),
+    ("sqlite/20250323152040_wallet_dleq_proofs.sql", include_str!(r#"./migrations/sqlite/20250323152040_wallet_dleq_proofs.sql"#)),
+    ("sqlite/20250401120000_add_transactions_table.sql", include_str!(r#"./migrations/sqlite/20250401120000_add_transactions_table.sql"#)),
+    ("sqlite/20250616144830_add_keyset_expiry.sql", include_str!(r#"./migrations/sqlite/20250616144830_add_keyset_expiry.sql"#)),
 ];
 ];

+ 0 - 0
crates/cdk-sql-base/src/wallet/migrations/20240612132920_init.sql → crates/cdk-sql-base/src/wallet/migrations/sqlite/20240612132920_init.sql


+ 0 - 0
crates/cdk-sql-base/src/wallet/migrations/20240618200350_quote_state.sql → crates/cdk-sql-base/src/wallet/migrations/sqlite/20240618200350_quote_state.sql


+ 0 - 0
crates/cdk-sql-base/src/wallet/migrations/20240626091921_nut04_state.sql → crates/cdk-sql-base/src/wallet/migrations/sqlite/20240626091921_nut04_state.sql


+ 0 - 0
crates/cdk-sql-base/src/wallet/migrations/20240710144711_input_fee.sql → crates/cdk-sql-base/src/wallet/migrations/sqlite/20240710144711_input_fee.sql


+ 0 - 0
crates/cdk-sql-base/src/wallet/migrations/20240810214105_mint_icon_url.sql → crates/cdk-sql-base/src/wallet/migrations/sqlite/20240810214105_mint_icon_url.sql


+ 0 - 0
crates/cdk-sql-base/src/wallet/migrations/20240810233905_update_mint_url.sql → crates/cdk-sql-base/src/wallet/migrations/sqlite/20240810233905_update_mint_url.sql


+ 0 - 0
crates/cdk-sql-base/src/wallet/migrations/20240902151515_icon_url.sql → crates/cdk-sql-base/src/wallet/migrations/sqlite/20240902151515_icon_url.sql


+ 0 - 0
crates/cdk-sql-base/src/wallet/migrations/20240902210905_mint_time.sql → crates/cdk-sql-base/src/wallet/migrations/sqlite/20240902210905_mint_time.sql


+ 0 - 0
crates/cdk-sql-base/src/wallet/migrations/20241011125207_mint_urls.sql → crates/cdk-sql-base/src/wallet/migrations/sqlite/20241011125207_mint_urls.sql


+ 0 - 0
crates/cdk-sql-base/src/wallet/migrations/20241108092756_wallet_mint_quote_secretkey.sql → crates/cdk-sql-base/src/wallet/migrations/sqlite/20241108092756_wallet_mint_quote_secretkey.sql


+ 0 - 0
crates/cdk-sql-base/src/wallet/migrations/20250214135017_mint_tos.sql → crates/cdk-sql-base/src/wallet/migrations/sqlite/20250214135017_mint_tos.sql


+ 0 - 0
crates/cdk-sql-base/src/wallet/migrations/20250310111513_drop_nostr_last_checked.sql → crates/cdk-sql-base/src/wallet/migrations/sqlite/20250310111513_drop_nostr_last_checked.sql


+ 0 - 0
crates/cdk-sql-base/src/wallet/migrations/20250314082116_allow_pending_spent.sql → crates/cdk-sql-base/src/wallet/migrations/sqlite/20250314082116_allow_pending_spent.sql


+ 0 - 0
crates/cdk-sql-base/src/wallet/migrations/20250323152040_wallet_dleq_proofs.sql → crates/cdk-sql-base/src/wallet/migrations/sqlite/20250323152040_wallet_dleq_proofs.sql


+ 0 - 0
crates/cdk-sql-base/src/wallet/migrations/20250401120000_add_transactions_table.sql → crates/cdk-sql-base/src/wallet/migrations/sqlite/20250401120000_add_transactions_table.sql


+ 0 - 0
crates/cdk-sql-base/src/wallet/migrations/20250616144830_add_keyset_expiry.sql → crates/cdk-sql-base/src/wallet/migrations/sqlite/20250616144830_add_keyset_expiry.sql


+ 5 - 5
crates/cdk-sql-base/src/wallet/mod.rs

@@ -36,14 +36,14 @@ where
     db: T,
     db: T,
 }
 }
 
 
-impl<T> SQLWalletDatabase<T>
+impl<DB> SQLWalletDatabase<DB>
 where
 where
-    T: DatabaseExecutor,
+    DB: DatabaseExecutor,
 {
 {
     /// Creates a new instance
     /// Creates a new instance
     pub async fn new<X>(db: X) -> Result<Self, Error>
     pub async fn new<X>(db: X) -> Result<Self, Error>
     where
     where
-        X: Into<T>,
+        X: Into<DB>,
     {
     {
         let db = db.into();
         let db = db.into();
         Self::migrate(&db).await?;
         Self::migrate(&db).await?;
@@ -51,8 +51,8 @@ where
     }
     }
 
 
     /// Migrate [`WalletSqliteDatabase`]
     /// Migrate [`WalletSqliteDatabase`]
-    async fn migrate(conn: &T) -> Result<(), Error> {
-        migrate(conn, migrations::MIGRATIONS).await?;
+    async fn migrate(conn: &DB) -> Result<(), Error> {
+        migrate(conn, DB::name(), migrations::MIGRATIONS).await?;
         Ok(())
         Ok(())
     }
     }
 }
 }

+ 5 - 5
crates/cdk-sql-base/tests/legacy-sqlx.sql

@@ -5,7 +5,7 @@ CREATE TABLE _sqlx_migrations (
     description TEXT NOT NULL,
     description TEXT NOT NULL,
     installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
     installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
     success BOOLEAN NOT NULL,
     success BOOLEAN NOT NULL,
-    checksum BLOB NOT NULL,
+    checksum BYTEA NOT NULL,
     execution_time BIGINT NOT NULL
     execution_time BIGINT NOT NULL
 );
 );
 INSERT INTO _sqlx_migrations VALUES(20240612124932,'init','2025-06-13 20:01:04',1,X'42664ceda25b07bca420c2f7480c90334cb8a720203c1b4b8971181d5d3afabda3171aa89c1c0c8a26421eded94b77fa',921834);
 INSERT INTO _sqlx_migrations VALUES(20240612124932,'init','2025-06-13 20:01:04',1,X'42664ceda25b07bca420c2f7480c90334cb8a720203c1b4b8971181d5d3afabda3171aa89c1c0c8a26421eded94b77fa',921834);
@@ -66,21 +66,21 @@ CREATE TABLE config (
     value TEXT NOT NULL
     value TEXT NOT NULL
 );
 );
 CREATE TABLE IF NOT EXISTS "proof" (
 CREATE TABLE IF NOT EXISTS "proof" (
-    y BLOB PRIMARY KEY,
+    y BYTEA PRIMARY KEY,
     amount INTEGER NOT NULL,
     amount INTEGER NOT NULL,
     keyset_id TEXT NOT NULL, -- no FK constraint here
     keyset_id TEXT NOT NULL, -- no FK constraint here
     secret TEXT NOT NULL,
     secret TEXT NOT NULL,
-    c BLOB NOT NULL,
+    c BYTEA NOT NULL,
     witness TEXT,
     witness TEXT,
     state TEXT CHECK (state IN ('SPENT', 'PENDING', 'UNSPENT', 'RESERVED', 'UNKNOWN')) NOT NULL,
     state TEXT CHECK (state IN ('SPENT', 'PENDING', 'UNSPENT', 'RESERVED', 'UNKNOWN')) NOT NULL,
     quote_id TEXT,
     quote_id TEXT,
     created_time INTEGER NOT NULL DEFAULT 0
     created_time INTEGER NOT NULL DEFAULT 0
 );
 );
 CREATE TABLE IF NOT EXISTS "blind_signature" (
 CREATE TABLE IF NOT EXISTS "blind_signature" (
-    y BLOB PRIMARY KEY,
+    y BYTEA PRIMARY KEY,
     amount INTEGER NOT NULL,
     amount INTEGER NOT NULL,
     keyset_id TEXT NOT NULL,  -- FK removed
     keyset_id TEXT NOT NULL,  -- FK removed
-    c BLOB NOT NULL,
+    c BYTEA NOT NULL,
     dleq_e TEXT,
     dleq_e TEXT,
     dleq_s TEXT,
     dleq_s TEXT,
     quote_id TEXT,
     quote_id TEXT,

+ 1 - 1
crates/cdk-sqlite/Cargo.toml

@@ -25,7 +25,7 @@ bitcoin.workspace = true
 cdk-sql-base = { workspace = true }
 cdk-sql-base = { workspace = true }
 rusqlite = { version = "0.31", features = ["bundled"]}
 rusqlite = { version = "0.31", features = ["bundled"]}
 thiserror.workspace = true
 thiserror.workspace = true
-tokio.workspace = true
+tokio = { workspace = true, features = ["rt-multi-thread"]}
 tracing.workspace = true
 tracing.workspace = true
 serde.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 serde_json.workspace = true

+ 2 - 1
crates/cdk-sqlite/src/common.rs

@@ -7,7 +7,7 @@ use cdk_sql_base::value::Value;
 use rusqlite::Connection;
 use rusqlite::Connection;
 
 
 /// The config need to create a new SQLite connection
 /// The config need to create a new SQLite connection
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub struct Config {
 pub struct Config {
     path: Option<String>,
     path: Option<String>,
     password: Option<String>,
     password: Option<String>,
@@ -27,6 +27,7 @@ impl ResourceManager for SqliteConnectionManager {
     fn new_resource(
     fn new_resource(
         config: &Self::Config,
         config: &Self::Config,
         _still_valid: Arc<AtomicBool>,
         _still_valid: Arc<AtomicBool>,
+        _timeout: Duration,
     ) -> Result<Self::Resource, pool::Error<Self::Error>> {
     ) -> Result<Self::Resource, pool::Error<Self::Error>> {
         let conn = if let Some(path) = config.path.as_ref() {
         let conn = if let Some(path) = config.path.as_ref() {
             Connection::open(path)?
             Connection::open(path)?

+ 10 - 1
crates/cdk-sqlite/src/mint/async_rusqlite.rs

@@ -1,4 +1,4 @@
-//! Async and concurrent rusqlite
+//! Async, pipelined rusqlite client
 use std::marker::PhantomData;
 use std::marker::PhantomData;
 use std::path::PathBuf;
 use std::path::PathBuf;
 use std::sync::atomic::{AtomicUsize, Ordering};
 use std::sync::atomic::{AtomicUsize, Ordering};
@@ -127,6 +127,7 @@ fn process_query(conn: &Connection, statement: InnerStatement) -> Result<DbRespo
     let start = Instant::now();
     let start = Instant::now();
     let expected_response = statement.expected_response;
     let expected_response = statement.expected_response;
     let (sql, placeholder_values) = statement.to_sql()?;
     let (sql, placeholder_values) = statement.to_sql()?;
+    let sql = sql.trim_end_matches("FOR UPDATE");
 
 
     let mut stmt = conn.prepare_cached(&sql)?;
     let mut stmt = conn.prepare_cached(&sql)?;
     for (i, value) in placeholder_values.into_iter().enumerate() {
     for (i, value) in placeholder_values.into_iter().enumerate() {
@@ -470,6 +471,10 @@ impl DatabaseConnector for AsyncRusqlite {
 
 
 #[async_trait::async_trait]
 #[async_trait::async_trait]
 impl DatabaseExecutor for AsyncRusqlite {
 impl DatabaseExecutor for AsyncRusqlite {
+    fn name() -> &'static str {
+        "sqlite"
+    }
+
     async fn fetch_one(&self, mut statement: InnerStatement) -> Result<Option<Vec<Column>>, Error> {
     async fn fetch_one(&self, mut statement: InnerStatement) -> Result<Option<Vec<Column>>, Error> {
         let (sender, receiver) = oneshot::channel();
         let (sender, receiver) = oneshot::channel();
         statement.expected_response = ExpectedSqlResponse::SingleRow;
         statement.expected_response = ExpectedSqlResponse::SingleRow;
@@ -620,6 +625,10 @@ impl<'a> DatabaseTransaction<'a> for Transaction<'a> {
 
 
 #[async_trait::async_trait]
 #[async_trait::async_trait]
 impl DatabaseExecutor for Transaction<'_> {
 impl DatabaseExecutor for Transaction<'_> {
+    fn name() -> &'static str {
+        "sqlite"
+    }
+
     async fn fetch_one(&self, mut statement: InnerStatement) -> Result<Option<Vec<Column>>, Error> {
     async fn fetch_one(&self, mut statement: InnerStatement) -> Result<Option<Vec<Column>>, Error> {
         let (sender, receiver) = oneshot::channel();
         let (sender, receiver) = oneshot::channel();
         statement.expected_response = ExpectedSqlResponse::SingleRow;
         statement.expected_response = ExpectedSqlResponse::SingleRow;

+ 36 - 0
crates/cdk-sqlite/src/mint/mod.rs

@@ -16,13 +16,49 @@ pub type MintSqliteAuthDatabase = SQLMintAuthDatabase<async_rusqlite::AsyncRusql
 
 
 #[cfg(test)]
 #[cfg(test)]
 mod test {
 mod test {
+    use std::fs::remove_file;
+
     use cdk_common::mint_db_test;
     use cdk_common::mint_db_test;
+    use cdk_sql_base::stmt::query;
 
 
     use super::*;
     use super::*;
+    use crate::mint::async_rusqlite::AsyncRusqlite;
 
 
     async fn provide_db() -> MintSqliteDatabase {
     async fn provide_db() -> MintSqliteDatabase {
         memory::empty().await.unwrap()
         memory::empty().await.unwrap()
     }
     }
 
 
     mint_db_test!(provide_db);
     mint_db_test!(provide_db);
+
+    #[tokio::test]
+    async fn open_legacy_and_migrate() {
+        let file = format!(
+            "{}/db.sqlite",
+            std::env::temp_dir().to_str().unwrap_or_default()
+        );
+
+        {
+            let _ = remove_file(&file);
+            #[cfg(not(feature = "sqlcipher"))]
+            let conn: AsyncRusqlite = file.as_str().into();
+            #[cfg(feature = "sqlcipher")]
+            let conn: AsyncRusqlite = (file.as_str(), "test".to_owned()).into();
+
+            query(include_str!("../../tests/legacy-sqlx.sql"))
+                .expect("query")
+                .execute(&conn)
+                .await
+                .expect("create former db failed");
+        }
+
+        #[cfg(not(feature = "sqlcipher"))]
+        let conn = MintSqliteDatabase::new(file.as_str()).await;
+
+        #[cfg(feature = "sqlcipher")]
+        let conn = MintSqliteDatabase::new((file.as_str(), "test".to_owned())).await;
+
+        assert!(conn.is_ok(), "Failed with {:?}", conn.unwrap_err());
+
+        let _ = remove_file(&file);
+    }
 }
 }

+ 4 - 0
crates/cdk-sqlite/src/wallet/mod.rs

@@ -41,6 +41,10 @@ impl SimpleAsyncRusqlite {
 
 
 #[async_trait::async_trait]
 #[async_trait::async_trait]
 impl DatabaseExecutor for SimpleAsyncRusqlite {
 impl DatabaseExecutor for SimpleAsyncRusqlite {
+    fn name() -> &'static str {
+        "sqlite"
+    }
+
     async fn execute(&self, statement: Statement) -> Result<usize, Error> {
     async fn execute(&self, statement: Statement) -> Result<usize, Error> {
         let conn = self.0.get().map_err(|e| Error::Database(Box::new(e)))?;
         let conn = self.0.get().map_err(|e| Error::Database(Box::new(e)))?;
         let mut stmt = self
         let mut stmt = self

+ 1 - 1
crates/cdk-sqlite/tests/legacy-sqlx.sql

@@ -70,7 +70,7 @@ CREATE TABLE IF NOT EXISTS "proof" (
     amount INTEGER NOT NULL,
     amount INTEGER NOT NULL,
     keyset_id TEXT NOT NULL, -- no FK constraint here
     keyset_id TEXT NOT NULL, -- no FK constraint here
     secret TEXT NOT NULL,
     secret TEXT NOT NULL,
-    c BLOB NOT NULL,
+    c BLOBNOT NULL,
     witness TEXT,
     witness TEXT,
     state TEXT CHECK (state IN ('SPENT', 'PENDING', 'UNSPENT', 'RESERVED', 'UNKNOWN')) NOT NULL,
     state TEXT CHECK (state IN ('SPENT', 'PENDING', 'UNSPENT', 'RESERVED', 'UNKNOWN')) NOT NULL,
     quote_id TEXT,
     quote_id TEXT,