Skip to content

Commit 1ff3bfd

Browse files
committed
Introduce cdk-sql-common
The primary purpose of this new crate is to have a common and shared codebase for all SQL storage systems. It would force us to write standard SQL using best practices for all databases. This crate has been extracted from cashubtc#878
1 parent fb08e88 commit 1ff3bfd

78 files changed

Lines changed: 4470 additions & 3833 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ cdk-fake-wallet = { path = "./crates/cdk-fake-wallet", version = "=0.11.0" }
5454
cdk-payment-processor = { path = "./crates/cdk-payment-processor", default-features = true, version = "=0.11.0" }
5555
cdk-mint-rpc = { path = "./crates/cdk-mint-rpc", version = "=0.11.0" }
5656
cdk-redb = { path = "./crates/cdk-redb", default-features = true, version = "=0.11.0" }
57+
cdk-sql-common = { path = "./crates/cdk-sql-common", default-features = true, version = "=0.11.0" }
5758
cdk-sqlite = { path = "./crates/cdk-sqlite", default-features = true, version = "=0.11.0" }
5859
cdk-signatory = { path = "./crates/cdk-signatory", version = "=0.11.0", default-features = false }
5960
clap = { version = "4.5.31", features = ["derive"] }

crates/cdk-cli/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ async fn main() -> Result<()> {
128128
#[cfg(feature = "sqlcipher")]
129129
let sql = {
130130
match args.password {
131-
Some(pass) => WalletSqliteDatabase::new(&sql_path, pass).await?,
131+
Some(pass) => WalletSqliteDatabase::new((sql_path, pass)).await?,
132132
None => bail!("Missing database password"),
133133
}
134134
};

crates/cdk-common/src/database/mod.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,80 @@ pub use mint::{MintAuthDatabase, MintAuthTransaction};
1919
#[cfg(feature = "wallet")]
2020
pub use wallet::Database as WalletDatabase;
2121

22+
/// Data conversion error
23+
#[derive(thiserror::Error, Debug)]
24+
pub enum ConversionError {
25+
/// Missing columns
26+
#[error("Not enough elements: expected {0}, got {1}")]
27+
MissingColumn(usize, usize),
28+
29+
/// Missing parameter
30+
#[error("Missing parameter {0}")]
31+
MissingParameter(String),
32+
33+
/// Invalid db type
34+
#[error("Invalid type from db, expected {0} got {1}")]
35+
InvalidType(String, String),
36+
37+
/// Invalid data conversion in column
38+
#[error("Error converting {1}, expecting type {0}")]
39+
InvalidConversion(String, String),
40+
41+
/// Mint Url Error
42+
#[error(transparent)]
43+
MintUrl(#[from] crate::mint_url::Error),
44+
45+
/// NUT00 Error
46+
#[error(transparent)]
47+
CDKNUT00(#[from] crate::nuts::nut00::Error),
48+
49+
/// NUT01 Error
50+
#[error(transparent)]
51+
CDKNUT01(#[from] crate::nuts::nut01::Error),
52+
53+
/// NUT02 Error
54+
#[error(transparent)]
55+
CDKNUT02(#[from] crate::nuts::nut02::Error),
56+
57+
/// NUT04 Error
58+
#[error(transparent)]
59+
CDKNUT04(#[from] crate::nuts::nut04::Error),
60+
61+
/// NUT05 Error
62+
#[error(transparent)]
63+
CDKNUT05(#[from] crate::nuts::nut05::Error),
64+
65+
/// NUT07 Error
66+
#[error(transparent)]
67+
CDKNUT07(#[from] crate::nuts::nut07::Error),
68+
69+
/// NUT23 Error
70+
#[error(transparent)]
71+
CDKNUT23(#[from] crate::nuts::nut23::Error),
72+
73+
/// Secret Error
74+
#[error(transparent)]
75+
CDKSECRET(#[from] crate::secret::Error),
76+
77+
/// Serde Error
78+
#[error(transparent)]
79+
Serde(#[from] serde_json::Error),
80+
81+
/// BIP32 Error
82+
#[error(transparent)]
83+
BIP32(#[from] bitcoin::bip32::Error),
84+
85+
/// Generic error
86+
#[error(transparent)]
87+
Generic(#[from] Box<crate::Error>),
88+
}
89+
90+
impl From<crate::Error> for ConversionError {
91+
fn from(err: crate::Error) -> Self {
92+
ConversionError::Generic(Box::new(err))
93+
}
94+
}
95+
2296
/// CDK_database error
2397
#[derive(Debug, thiserror::Error)]
2498
pub enum Error {
@@ -39,6 +113,9 @@ pub enum Error {
39113
/// NUT00 Error
40114
#[error(transparent)]
41115
NUT00(#[from] crate::nuts::nut00::Error),
116+
/// NUT01 Error
117+
#[error(transparent)]
118+
NUT01(#[from] crate::nuts::nut01::Error),
42119
/// NUT02 Error
43120
#[error(transparent)]
44121
NUT02(#[from] crate::nuts::nut02::Error),
@@ -68,6 +145,38 @@ pub enum Error {
68145
/// Invalid state transition
69146
#[error("Invalid state transition")]
70147
InvalidStateTransition(crate::state::Error),
148+
149+
/// Invalid connection settings
150+
#[error("Invalid credentials {0}")]
151+
InvalidConnectionSettings(String),
152+
153+
/// Unexpected database response
154+
#[error("Invalid database response")]
155+
InvalidDbResponse,
156+
157+
/// Internal error
158+
#[error("Internal {0}")]
159+
Internal(String),
160+
161+
/// Data conversion error
162+
#[error(transparent)]
163+
Conversion(#[from] ConversionError),
164+
165+
/// Missing Placeholder value
166+
#[error("Missing placeholder value {0}")]
167+
MissingPlaceholder(String),
168+
169+
/// Unknown quote ttl
170+
#[error("Unknown quote ttl")]
171+
UnknownQuoteTTL,
172+
173+
/// Invalid UUID
174+
#[error("Invalid UUID: {0}")]
175+
InvalidUuid(String),
176+
177+
/// QuoteNotFound
178+
#[error("Quote not found")]
179+
QuoteNotFound,
71180
}
72181

73182
#[cfg(feature = "mint")]

crates/cdk-integration-tests/src/init_pure_tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ pub async fn create_and_start_test_mint() -> Result<Mint> {
234234
let temp_dir = create_temp_dir("cdk-test-sqlite-mint")?;
235235
let path = temp_dir.join("mint.db").to_str().unwrap().to_string();
236236
Arc::new(
237-
cdk_sqlite::MintSqliteDatabase::new(&path)
237+
cdk_sqlite::MintSqliteDatabase::new(path.as_str())
238238
.await
239239
.expect("Could not create sqlite db"),
240240
)
@@ -310,7 +310,7 @@ pub async fn create_test_wallet_for_mint(mint: Mint) -> Result<Wallet> {
310310
// Create a temporary directory for SQLite database
311311
let temp_dir = create_temp_dir("cdk-test-sqlite-wallet")?;
312312
let path = temp_dir.join("wallet.db").to_str().unwrap().to_string();
313-
let database = cdk_sqlite::WalletSqliteDatabase::new(&path)
313+
let database = cdk_sqlite::WalletSqliteDatabase::new(path.as_str())
314314
.await
315315
.expect("Could not create sqlite db");
316316
Arc::new(database)

crates/cdk-mintd/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ async fn setup_sqlite_database(
211211
#[cfg(feature = "sqlcipher")]
212212
let db = {
213213
// Get password from command line arguments for sqlcipher
214-
MintSqliteDatabase::new(&sql_db_path, _password.unwrap()).await?
214+
MintSqliteDatabase::new((sql_db_path, _password.unwrap())).await?
215215
};
216216
Ok(Arc::new(db))
217217
}
@@ -486,7 +486,7 @@ async fn setup_authentication(
486486
#[cfg(feature = "sqlcipher")]
487487
let password = CLIArgs::parse().password;
488488
#[cfg(feature = "sqlcipher")]
489-
let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path, password).await?;
489+
let sqlite_db = MintSqliteAuthDatabase::new((sql_db_path, password)).await?;
490490
#[cfg(not(feature = "sqlcipher"))]
491491
let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path).await?;
492492
Arc::new(sqlite_db)

crates/cdk-signatory/src/bin/cli/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ pub async fn cli_main() -> Result<()> {
108108
#[cfg(feature = "sqlcipher")]
109109
let db = {
110110
match args.password {
111-
Some(pass) => MintSqliteDatabase::new(&sql_path, pass).await?,
111+
Some(pass) => MintSqliteDatabase::new((&sql_path, pass)).await?,
112112
None => bail!("Missing database password"),
113113
}
114114
};

crates/cdk-sql-common/Cargo.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[package]
2+
name = "cdk-sql-common"
3+
version.workspace = true
4+
edition.workspace = true
5+
authors = ["CDK Developers"]
6+
description = "Generic SQL storage backend for CDK"
7+
license.workspace = true
8+
homepage = "https://github.com/cashubtc/cdk"
9+
repository = "https://github.com/cashubtc/cdk.git"
10+
rust-version.workspace = true # MSRV
11+
readme = "README.md"
12+
13+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
14+
[features]
15+
default = ["mint", "wallet", "auth"]
16+
mint = ["cdk-common/mint"]
17+
wallet = ["cdk-common/wallet"]
18+
auth = ["cdk-common/auth"]
19+
20+
[dependencies]
21+
async-trait.workspace = true
22+
cdk-common = { workspace = true, features = ["test"] }
23+
bitcoin.workspace = true
24+
thiserror.workspace = true
25+
tokio.workspace = true
26+
tracing.workspace = true
27+
serde.workspace = true
28+
serde_json.workspace = true
29+
lightning-invoice.workspace = true
30+
uuid.workspace = true

crates/cdk-sql-common/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# CDK SQL Base
2+
3+
This is a private crate offering a common framework to interact with SQL databases.
4+
5+
This crate uses standard SQL, a generic migration framework a traits to implement blocking or
6+
non-blocking clients.
7+
8+
9+
**ALPHA** This library is in early development, the API will change and should be used with caution.
10+
11+
## Features
12+
13+
The following crate feature flags are available:
14+
15+
| Feature | Default | Description |
16+
|-------------|:-------:|------------------------------------|
17+
| `wallet` | Yes | Enable cashu wallet features |
18+
| `mint` | Yes | Enable cashu mint wallet features |
19+
| `auth` | Yes | Enable cashu mint auth features |
20+
21+
22+
## License
23+
24+
This project is licensed under the [MIT License](../../LICENSE).
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,21 @@ fn main() {
1818
let dest_path = parent.join("migrations.rs");
1919
let mut out_file = File::create(&dest_path).expect("Failed to create migrations.rs");
2020

21-
writeln!(out_file, "// @generated").unwrap();
22-
writeln!(out_file, "// Auto-generated by build.rs").unwrap();
21+
let skip_name = migration_path.to_str().unwrap_or_default().len();
22+
23+
writeln!(out_file, "/// @generated").unwrap();
24+
writeln!(out_file, "/// Auto-generated by build.rs").unwrap();
2325
writeln!(out_file, "pub static MIGRATIONS: &[(&str, &str)] = &[").unwrap();
2426

2527
for path in &files {
26-
let name = path.file_name().unwrap().to_string_lossy();
28+
let rel_name = &path.to_str().unwrap().replace("\\", "/")[skip_name + 1..]; // for Windows
2729
let rel_path = &path.to_str().unwrap().replace("\\", "/")[skip_path..]; // for Windows
2830
writeln!(
2931
out_file,
30-
" (\"{name}\", include_str!(r#\".{rel_path}\"#)),"
32+
" (\"{rel_name}\", include_str!(r#\".{rel_path}\"#)),"
3133
)
3234
.unwrap();
35+
println!("cargo:rerun-if-changed={}", path.display());
3336
}
3437

3538
writeln!(out_file, "];").unwrap();
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use crate::database::DatabaseExecutor;
2+
use crate::stmt::query;
3+
4+
/// Migrates the migration generated by `build.rs`
5+
#[inline(always)]
6+
pub async fn migrate<C: DatabaseExecutor>(
7+
conn: &C,
8+
db_prefix: &str,
9+
migrations: &[(&str, &str)],
10+
) -> Result<(), cdk_common::database::Error> {
11+
query(
12+
r#"
13+
CREATE TABLE IF NOT EXISTS migrations (
14+
name TEXT PRIMARY KEY,
15+
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
16+
)
17+
"#,
18+
)?
19+
.execute(conn)
20+
.await?;
21+
22+
// Apply each migration if it hasn’t been applied yet
23+
for (name, sql) in migrations {
24+
if let Some((prefix, _)) = name.split_once(['/', '\\']) {
25+
if prefix != db_prefix {
26+
continue;
27+
}
28+
}
29+
30+
let is_missing = query("SELECT name FROM migrations WHERE name = :name")?
31+
.bind("name", name)
32+
.pluck(conn)
33+
.await?
34+
.is_none();
35+
36+
if is_missing {
37+
query(sql)?.batch(conn).await?;
38+
query(r#"INSERT INTO migrations (name) VALUES (:name)"#)?
39+
.bind("name", name)
40+
.execute(conn)
41+
.await?;
42+
}
43+
}
44+
45+
Ok(())
46+
}

0 commit comments

Comments
 (0)