0.5.0
This commit is contained in:
@@ -20,6 +20,7 @@ solana-sdk-ids.workspace = true
|
||||
spl-associated-token-account-interface.workspace = true
|
||||
spl-token-2022-interface.workspace = true
|
||||
spl-token-interface.workspace = true
|
||||
sqlx.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-tungstenite.workspace = true
|
||||
tracing.workspace = true
|
||||
@@ -27,4 +28,5 @@ tracing-appender.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ pub struct KbConfig {
|
||||
pub data: KbDataConfig,
|
||||
/// Solana endpoint configuration.
|
||||
pub solana: KbSolanaConfig,
|
||||
/// Database configuration.
|
||||
pub database: KbDatabaseConfig,
|
||||
}
|
||||
|
||||
impl KbConfig {
|
||||
@@ -489,6 +491,36 @@ impl KbWsEndpointConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// SQLite configuration.
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct KbSqliteDatabaseConfig {
|
||||
/// SQLite database path.
|
||||
pub path: std::string::String,
|
||||
/// Whether the file should be created if missing.
|
||||
pub create_if_missing: bool,
|
||||
/// SQLite busy timeout in milliseconds.
|
||||
pub busy_timeout_ms: u64,
|
||||
/// Maximum pool connections.
|
||||
pub max_connections: u32,
|
||||
/// Whether the schema should be initialized automatically at startup.
|
||||
pub auto_initialize_schema: bool,
|
||||
/// Whether WAL journal mode should be enabled.
|
||||
pub use_wal: bool,
|
||||
}
|
||||
|
||||
/// Database configuration.
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct KbDatabaseConfig {
|
||||
/// Whether the database layer is enabled.
|
||||
pub enabled: bool,
|
||||
/// Selected backend.
|
||||
pub backend: crate::KbDatabaseBackend,
|
||||
/// SQLite-specific configuration.
|
||||
pub sqlite: KbSqliteDatabaseConfig,
|
||||
}
|
||||
|
||||
fn kb_workspace_root_dir() -> std::path::PathBuf {
|
||||
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
match manifest_dir.parent() {
|
||||
|
||||
23
kb_lib/src/db.rs
Normal file
23
kb_lib/src/db.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
// file: kb_lib/src/db.rs
|
||||
|
||||
//! Database facade.
|
||||
//!
|
||||
//! This module centralizes the database layer and exposes a storage API that is
|
||||
//! intentionally structured to remain evolutive across backends.
|
||||
|
||||
mod connection;
|
||||
mod dtos;
|
||||
mod entities;
|
||||
mod queries;
|
||||
mod schema;
|
||||
mod sqlite;
|
||||
mod types;
|
||||
|
||||
pub use crate::db::connection::KbDatabase;
|
||||
pub use crate::db::connection::KbDatabaseConnection;
|
||||
pub use crate::db::dtos::KbDbMetadataDto;
|
||||
pub use crate::db::entities::KbDbMetadataEntity;
|
||||
pub use crate::db::queries::get_db_metadata;
|
||||
pub use crate::db::queries::list_db_metadata;
|
||||
pub use crate::db::queries::upsert_db_metadata;
|
||||
pub use crate::db::types::KbDatabaseBackend;
|
||||
140
kb_lib/src/db/connection.rs
Normal file
140
kb_lib/src/db/connection.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
// file: kb_lib/src/db/connection.rs
|
||||
|
||||
//! Database connection facade.
|
||||
|
||||
/// Concrete database connection.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum KbDatabaseConnection {
|
||||
/// SQLite connection pool.
|
||||
Sqlite(sqlx::SqlitePool),
|
||||
}
|
||||
|
||||
/// Database facade.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KbDatabase {
|
||||
backend: crate::KbDatabaseBackend,
|
||||
database_url: std::string::String,
|
||||
connection: KbDatabaseConnection,
|
||||
}
|
||||
|
||||
impl KbDatabase {
|
||||
/// Opens a database connection without initializing the schema.
|
||||
pub async fn connect(
|
||||
config: &crate::KbDatabaseConfig,
|
||||
) -> Result<Self, crate::KbError> {
|
||||
if !config.enabled {
|
||||
return Err(crate::KbError::Config(
|
||||
"database is disabled in configuration".to_string(),
|
||||
));
|
||||
}
|
||||
match config.backend {
|
||||
crate::KbDatabaseBackend::Sqlite => {
|
||||
let database_url_result =
|
||||
crate::db::sqlite::sqlite_database_url_from_config(config);
|
||||
let database_url = match database_url_result {
|
||||
Ok(database_url) => database_url,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let pool_result = crate::db::sqlite::connect_sqlite(config).await;
|
||||
let pool = match pool_result {
|
||||
Ok(pool) => pool,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
Ok(Self {
|
||||
backend: crate::KbDatabaseBackend::Sqlite,
|
||||
database_url,
|
||||
connection: KbDatabaseConnection::Sqlite(pool),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a database connection and initializes the schema if configured.
|
||||
pub async fn connect_and_initialize(
|
||||
config: &crate::KbDatabaseConfig,
|
||||
) -> Result<Self, crate::KbError> {
|
||||
let connect_result = Self::connect(config).await;
|
||||
let database = match connect_result {
|
||||
Ok(database) => database,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if config.sqlite.auto_initialize_schema {
|
||||
let init_result = crate::db::schema::ensure_schema(&database).await;
|
||||
if let Err(error) = init_result {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
Ok(database)
|
||||
}
|
||||
|
||||
/// Returns the configured backend.
|
||||
pub fn backend(
|
||||
&self,
|
||||
) -> crate::KbDatabaseBackend {
|
||||
self.backend
|
||||
}
|
||||
|
||||
/// Returns a displayable database URL-like string.
|
||||
pub fn database_url(
|
||||
&self,
|
||||
) -> &str {
|
||||
&self.database_url
|
||||
}
|
||||
|
||||
/// Pings the database.
|
||||
pub async fn ping(
|
||||
&self,
|
||||
) -> Result<(), crate::KbError> {
|
||||
match &self.connection {
|
||||
KbDatabaseConnection::Sqlite(pool) => {
|
||||
let ping_result = sqlx::query("SELECT 1").execute(pool).await;
|
||||
match ping_result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(error) => Err(crate::KbError::Db(format!(
|
||||
"cannot ping sqlite database '{}': {}",
|
||||
self.database_url,
|
||||
error
|
||||
))),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the underlying connection enum.
|
||||
pub(crate) fn connection(
|
||||
&self,
|
||||
) -> &KbDatabaseConnection {
|
||||
&self.connection
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[tokio::test]
|
||||
async fn connect_and_ping_sqlite_database_works() {
|
||||
let tempdir = tempfile::tempdir().expect("tempdir must succeed");
|
||||
let database_path = tempdir.path().join("connection.sqlite3");
|
||||
let config = crate::KbDatabaseConfig {
|
||||
enabled: true,
|
||||
backend: crate::KbDatabaseBackend::Sqlite,
|
||||
sqlite: crate::KbSqliteDatabaseConfig {
|
||||
path: database_path.to_string_lossy().to_string(),
|
||||
create_if_missing: true,
|
||||
busy_timeout_ms: 5000,
|
||||
max_connections: 1,
|
||||
auto_initialize_schema: true,
|
||||
use_wal: true,
|
||||
},
|
||||
};
|
||||
let database = crate::KbDatabase::connect_and_initialize(&config)
|
||||
.await
|
||||
.expect("database init must succeed");
|
||||
assert_eq!(database.backend(), crate::KbDatabaseBackend::Sqlite);
|
||||
assert!(database.database_url().starts_with("sqlite://"));
|
||||
database.ping().await.expect("ping must succeed");
|
||||
let metadata = crate::get_db_metadata(&database, "schema_version")
|
||||
.await
|
||||
.expect("metadata fetch must succeed");
|
||||
assert!(metadata.is_some());
|
||||
}
|
||||
}
|
||||
7
kb_lib/src/db/dtos.rs
Normal file
7
kb_lib/src/db/dtos.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// file: kb_lib/src/db/dtos.rs
|
||||
|
||||
//! Database data transfer objects.
|
||||
|
||||
mod db_metadata;
|
||||
|
||||
pub use crate::db::dtos::db_metadata::KbDbMetadataDto;
|
||||
57
kb_lib/src/db/dtos/db_metadata.rs
Normal file
57
kb_lib/src/db/dtos/db_metadata.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
// file: kb_lib/src/db/dtos/db_metadata.rs
|
||||
|
||||
//! Metadata DTOs.
|
||||
|
||||
/// Metadata DTO used by the application layer.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbDbMetadataDto {
|
||||
/// Metadata key.
|
||||
pub key: std::string::String,
|
||||
/// Metadata value.
|
||||
pub value: std::string::String,
|
||||
/// Last update timestamp.
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl KbDbMetadataDto {
|
||||
/// Creates a new metadata DTO with the current UTC timestamp.
|
||||
pub fn new(key: std::string::String, value: std::string::String) -> Self {
|
||||
Self {
|
||||
key,
|
||||
value,
|
||||
updated_at: chrono::Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<crate::KbDbMetadataEntity> for KbDbMetadataDto {
|
||||
type Error = crate::KbError;
|
||||
|
||||
fn try_from(entity: crate::KbDbMetadataEntity) -> Result<Self, Self::Error> {
|
||||
let parsed_result = chrono::DateTime::parse_from_rfc3339(&entity.updated_at);
|
||||
let parsed = match parsed_result {
|
||||
Ok(parsed) => parsed,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Db(format!(
|
||||
"cannot parse db metadata timestamp '{}': {}",
|
||||
entity.updated_at, error
|
||||
)));
|
||||
}
|
||||
};
|
||||
Ok(Self {
|
||||
key: entity.key,
|
||||
value: entity.value,
|
||||
updated_at: parsed.with_timezone(&chrono::Utc),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KbDbMetadataDto> for crate::KbDbMetadataEntity {
|
||||
fn from(dto: KbDbMetadataDto) -> Self {
|
||||
Self {
|
||||
key: dto.key,
|
||||
value: dto.value,
|
||||
updated_at: dto.updated_at.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
}
|
||||
9
kb_lib/src/db/entities.rs
Normal file
9
kb_lib/src/db/entities.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
// file: kb_lib/src/db/entities.rs
|
||||
|
||||
//! Database entities.
|
||||
//!
|
||||
//! These types are close to persisted rows and SQL query results.
|
||||
|
||||
mod db_metadata;
|
||||
|
||||
pub use crate::db::entities::db_metadata::KbDbMetadataEntity;
|
||||
14
kb_lib/src/db/entities/db_metadata.rs
Normal file
14
kb_lib/src/db/entities/db_metadata.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
// file: kb_lib/src/db/entities/db_metadata.rs
|
||||
|
||||
//! Metadata table entity.
|
||||
|
||||
/// Persisted metadata row.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
|
||||
pub struct KbDbMetadataEntity {
|
||||
/// Metadata key.
|
||||
pub key: std::string::String,
|
||||
/// Metadata value.
|
||||
pub value: std::string::String,
|
||||
/// Last update timestamp encoded as RFC3339 UTC text.
|
||||
pub updated_at: std::string::String,
|
||||
}
|
||||
9
kb_lib/src/db/queries.rs
Normal file
9
kb_lib/src/db/queries.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
// file: kb_lib/src/db/queries.rs
|
||||
|
||||
//! Database queries.
|
||||
|
||||
mod db_metadata;
|
||||
|
||||
pub use crate::db::queries::db_metadata::get_db_metadata;
|
||||
pub use crate::db::queries::db_metadata::list_db_metadata;
|
||||
pub use crate::db::queries::db_metadata::upsert_db_metadata;
|
||||
164
kb_lib/src/db/queries/db_metadata.rs
Normal file
164
kb_lib/src/db/queries/db_metadata.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
// file: kb_lib/src/db/queries/db_metadata.rs
|
||||
|
||||
//! Queries for `kb_db_metadata`.
|
||||
|
||||
/// Inserts or updates one metadata row.
|
||||
pub async fn upsert_db_metadata(
|
||||
database: &crate::KbDatabase,
|
||||
dto: &crate::KbDbMetadataDto,
|
||||
) -> Result<(), crate::KbError> {
|
||||
let entity = crate::KbDbMetadataEntity::from(dto.clone());
|
||||
match database.connection() {
|
||||
crate::KbDatabaseConnection::Sqlite(pool) => {
|
||||
let query_result = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO kb_db_metadata (
|
||||
key,
|
||||
value,
|
||||
updated_at
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
value = excluded.value,
|
||||
updated_at = excluded.updated_at
|
||||
"#,
|
||||
)
|
||||
.bind(entity.key)
|
||||
.bind(entity.value)
|
||||
.bind(entity.updated_at)
|
||||
.execute(pool)
|
||||
.await;
|
||||
match query_result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(error) => Err(crate::KbError::Db(format!(
|
||||
"cannot upsert kb_db_metadata on sqlite: {}",
|
||||
error
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads one metadata row by key.
|
||||
pub async fn get_db_metadata(
|
||||
database: &crate::KbDatabase,
|
||||
key: &str,
|
||||
) -> Result<std::option::Option<crate::KbDbMetadataDto>, crate::KbError> {
|
||||
match database.connection() {
|
||||
crate::KbDatabaseConnection::Sqlite(pool) => {
|
||||
let query_result = sqlx::query_as::<sqlx::Sqlite, crate::KbDbMetadataEntity>(
|
||||
r#"
|
||||
SELECT
|
||||
key,
|
||||
value,
|
||||
updated_at
|
||||
FROM kb_db_metadata
|
||||
WHERE key = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(key)
|
||||
.fetch_optional(pool)
|
||||
.await;
|
||||
let entity_option = match query_result {
|
||||
Ok(entity_option) => entity_option,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Db(format!(
|
||||
"cannot read kb_db_metadata '{}' on sqlite: {}",
|
||||
key, error
|
||||
)));
|
||||
}
|
||||
};
|
||||
match entity_option {
|
||||
Some(entity) => {
|
||||
let dto_result = crate::KbDbMetadataDto::try_from(entity);
|
||||
match dto_result {
|
||||
Ok(dto) => Ok(Some(dto)),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists all metadata rows.
|
||||
pub async fn list_db_metadata(
|
||||
database: &crate::KbDatabase,
|
||||
) -> Result<std::vec::Vec<crate::KbDbMetadataDto>, crate::KbError> {
|
||||
match database.connection() {
|
||||
crate::KbDatabaseConnection::Sqlite(pool) => {
|
||||
let query_result = sqlx::query_as::<sqlx::Sqlite, crate::KbDbMetadataEntity>(
|
||||
r#"
|
||||
SELECT
|
||||
key,
|
||||
value,
|
||||
updated_at
|
||||
FROM kb_db_metadata
|
||||
ORDER BY key ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await;
|
||||
let entities = match query_result {
|
||||
Ok(entities) => entities,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Db(format!(
|
||||
"cannot list kb_db_metadata on sqlite: {}",
|
||||
error
|
||||
)));
|
||||
}
|
||||
};
|
||||
let mut dtos = std::vec::Vec::new();
|
||||
for entity in entities {
|
||||
let dto_result = crate::KbDbMetadataDto::try_from(entity);
|
||||
let dto = match dto_result {
|
||||
Ok(dto) => dto,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
dtos.push(dto);
|
||||
}
|
||||
Ok(dtos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[tokio::test]
|
||||
async fn db_metadata_roundtrip_works() {
|
||||
let tempdir = tempfile::tempdir().expect("tempdir must succeed");
|
||||
let database_path = tempdir.path().join("roundtrip.sqlite3");
|
||||
let config = crate::KbDatabaseConfig {
|
||||
enabled: true,
|
||||
backend: crate::KbDatabaseBackend::Sqlite,
|
||||
sqlite: crate::KbSqliteDatabaseConfig {
|
||||
path: database_path.to_string_lossy().to_string(),
|
||||
create_if_missing: true,
|
||||
busy_timeout_ms: 5000,
|
||||
max_connections: 1,
|
||||
auto_initialize_schema: true,
|
||||
use_wal: true,
|
||||
},
|
||||
};
|
||||
let database = crate::KbDatabase::connect_and_initialize(&config)
|
||||
.await
|
||||
.expect("database init must succeed");
|
||||
let dto = crate::KbDbMetadataDto::new("schema_version".to_string(), "0.5.0".to_string());
|
||||
crate::upsert_db_metadata(&database, &dto)
|
||||
.await
|
||||
.expect("upsert must succeed");
|
||||
let fetched = crate::get_db_metadata(&database, "schema_version")
|
||||
.await
|
||||
.expect("fetch must succeed");
|
||||
assert!(fetched.is_some());
|
||||
let fetched = fetched.expect("metadata must exist");
|
||||
assert_eq!(fetched.key, "schema_version");
|
||||
assert_eq!(fetched.value, "0.5.0");
|
||||
let listed = crate::list_db_metadata(&database)
|
||||
.await
|
||||
.expect("list must succeed");
|
||||
assert!(!listed.is_empty());
|
||||
}
|
||||
}
|
||||
39
kb_lib/src/db/schema.rs
Normal file
39
kb_lib/src/db/schema.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
// file: kb_lib/src/db/schema.rs
|
||||
|
||||
//! Database schema initialization.
|
||||
|
||||
/// Ensures that the database schema exists.
|
||||
pub(super) async fn ensure_schema(
|
||||
database: &crate::KbDatabase,
|
||||
) -> Result<(), crate::KbError> {
|
||||
match database.connection() {
|
||||
crate::KbDatabaseConnection::Sqlite(pool) => {
|
||||
let metadata_table_result = sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS kb_db_metadata (
|
||||
key TEXT NOT NULL PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await;
|
||||
if let Err(error) = metadata_table_result {
|
||||
return Err(crate::KbError::Db(format!(
|
||||
"cannot create table kb_db_metadata on sqlite: {}",
|
||||
error
|
||||
)));
|
||||
}
|
||||
let schema_version = crate::KbDbMetadataDto::new(
|
||||
"schema_version".to_string(),
|
||||
env!("CARGO_PKG_VERSION").to_string(),
|
||||
);
|
||||
let upsert_result = crate::upsert_db_metadata(database, &schema_version).await;
|
||||
if let Err(error) = upsert_result {
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
67
kb_lib/src/db/sqlite.rs
Normal file
67
kb_lib/src/db/sqlite.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
// file: kb_lib/src/db/sqlite.rs
|
||||
|
||||
//! SQLite backend helpers.
|
||||
|
||||
/// Returns a displayable SQLite database URL-like string.
|
||||
pub(crate) fn sqlite_database_url_from_config(
|
||||
config: &crate::KbDatabaseConfig,
|
||||
) -> Result<std::string::String, crate::KbError> {
|
||||
let path = config.sqlite.path.trim();
|
||||
if path.is_empty() {
|
||||
return Err(crate::KbError::Config(
|
||||
"database.sqlite.path must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(format!("sqlite://{}", path))
|
||||
}
|
||||
|
||||
/// Opens a SQLite pool according to configuration.
|
||||
pub(crate) async fn connect_sqlite(
|
||||
config: &crate::KbDatabaseConfig,
|
||||
) -> Result<sqlx::SqlitePool, crate::KbError> {
|
||||
let path = config.sqlite.path.trim();
|
||||
if path.is_empty() {
|
||||
return Err(crate::KbError::Config(
|
||||
"database.sqlite.path must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
if config.sqlite.max_connections == 0 {
|
||||
return Err(crate::KbError::Config(
|
||||
"database.sqlite.max_connections must be > 0".to_string(),
|
||||
));
|
||||
}
|
||||
let database_path = std::path::Path::new(path);
|
||||
let parent_option = database_path.parent();
|
||||
if let Some(parent) = parent_option {
|
||||
if !parent.as_os_str().is_empty() {
|
||||
let create_result = std::fs::create_dir_all(parent);
|
||||
if let Err(error) = create_result {
|
||||
return Err(crate::KbError::Db(format!(
|
||||
"cannot create sqlite parent directory '{}': {}",
|
||||
parent.display(),
|
||||
error
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut connect_options = sqlx::sqlite::SqliteConnectOptions::new()
|
||||
.filename(database_path)
|
||||
.create_if_missing(config.sqlite.create_if_missing)
|
||||
.foreign_keys(true)
|
||||
.busy_timeout(std::time::Duration::from_millis(
|
||||
config.sqlite.busy_timeout_ms,
|
||||
));
|
||||
if config.sqlite.use_wal {
|
||||
connect_options = connect_options.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal);
|
||||
}
|
||||
let pool_options =
|
||||
sqlx::sqlite::SqlitePoolOptions::new().max_connections(config.sqlite.max_connections);
|
||||
let connect_result = pool_options.connect_with(connect_options).await;
|
||||
match connect_result {
|
||||
Ok(pool) => Ok(pool),
|
||||
Err(error) => Err(crate::KbError::Db(format!(
|
||||
"cannot open sqlite database '{}': {}",
|
||||
path, error
|
||||
))),
|
||||
}
|
||||
}
|
||||
7
kb_lib/src/db/types.rs
Normal file
7
kb_lib/src/db/types.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// file: kb_lib/src/db/types.rs
|
||||
|
||||
//! Database shared types.
|
||||
|
||||
mod database_backend;
|
||||
|
||||
pub use crate::db::types::database_backend::KbDatabaseBackend;
|
||||
11
kb_lib/src/db/types/database_backend.rs
Normal file
11
kb_lib/src/db/types/database_backend.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
// file: kb_lib/src/db/types/database_backend.rs
|
||||
|
||||
//! Database backend discriminator.
|
||||
|
||||
/// Supported database backends.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum KbDatabaseBackend {
|
||||
/// SQLite backend.
|
||||
Sqlite,
|
||||
}
|
||||
@@ -20,6 +20,8 @@ pub enum KbError {
|
||||
Http(std::string::String),
|
||||
/// WebSocket transport error.
|
||||
Ws(std::string::String),
|
||||
/// Database error.
|
||||
Db(std::string::String),
|
||||
/// Invalid internal state error.
|
||||
InvalidState(std::string::String),
|
||||
/// Operation requested while the client is not connected.
|
||||
@@ -29,38 +31,38 @@ pub enum KbError {
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KbError {
|
||||
fn fmt(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Config(message) => {
|
||||
write!(formatter, "configuration error: {message}")
|
||||
},
|
||||
}
|
||||
Self::Io(message) => {
|
||||
write!(formatter, "io error: {message}")
|
||||
},
|
||||
}
|
||||
Self::Json(message) => {
|
||||
write!(formatter, "json error: {message}")
|
||||
},
|
||||
}
|
||||
Self::Tracing(message) => {
|
||||
write!(formatter, "tracing error: {message}")
|
||||
},
|
||||
}
|
||||
Self::Http(message) => {
|
||||
write!(formatter, "http error: {message}")
|
||||
},
|
||||
}
|
||||
Self::Ws(message) => {
|
||||
write!(formatter, "websocket error: {message}")
|
||||
},
|
||||
}
|
||||
Self::Db(message) => {
|
||||
write!(formatter, "db error: {}", message)
|
||||
}
|
||||
Self::InvalidState(message) => {
|
||||
write!(formatter, "invalid state: {message}")
|
||||
},
|
||||
}
|
||||
Self::NotConnected(message) => {
|
||||
write!(formatter, "not connected: {message}")
|
||||
},
|
||||
}
|
||||
Self::NotImplemented(message) => {
|
||||
write!(formatter, "not implemented: {message}")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ mod types;
|
||||
mod ws_client;
|
||||
mod rpc_ws_solana;
|
||||
mod http_pool;
|
||||
mod db;
|
||||
|
||||
pub use crate::config::KbAppConfig;
|
||||
pub use crate::config::KbConfig;
|
||||
@@ -26,6 +27,8 @@ pub use crate::config::KbHttpEndpointConfig;
|
||||
pub use crate::config::KbLoggingConfig;
|
||||
pub use crate::config::KbSolanaConfig;
|
||||
pub use crate::config::KbWsEndpointConfig;
|
||||
pub use crate::config::KbDatabaseConfig;
|
||||
pub use crate::config::KbSqliteDatabaseConfig;
|
||||
pub use crate::constants::*;
|
||||
pub use crate::error::KbError;
|
||||
pub use crate::rpc_ws::KbJsonRpcWsErrorObject;
|
||||
@@ -60,4 +63,12 @@ pub use crate::rpc_ws_solana::parse_kb_solana_ws_typed_notification;
|
||||
pub use crate::rpc_ws_solana::parse_kb_solana_ws_typed_notification_from_event;
|
||||
pub use crate::http_pool::HttpEndpointPool;
|
||||
pub use crate::http_pool::KbHttpPoolClientSnapshot;
|
||||
pub use crate::db::KbDatabase;
|
||||
pub use crate::db::KbDatabaseBackend;
|
||||
pub use crate::db::KbDatabaseConnection;
|
||||
pub use crate::db::KbDbMetadataDto;
|
||||
pub use crate::db::KbDbMetadataEntity;
|
||||
pub use crate::db::get_db_metadata;
|
||||
pub use crate::db::list_db_metadata;
|
||||
pub use crate::db::upsert_db_metadata;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user