Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
radicle: Make SQLite pragmas configurable
Yorgos Saslis committed 1 month ago
commit f4aee203abb15130745c5195bbc32fd373286991
parent 6cc3da9
7 files changed +256 -2
modified crates/radicle-cli/examples/rad-config.md
@@ -311,6 +311,10 @@ $ rad config schema
            "default": "block"
          }
        },
+
        "database": {
+
          "description": "Database configuration.",
+
          "$ref": "#/$defs/Config"
+
        },
        "secret": {
          "description": "Path to a file containing an Ed25519 secret key, in OpenSSH format, i.e./nwith the `-----BEGIN OPENSSH PRIVATE KEY-----` header. The corresponding/npublic key will be used as the Node ID./n/nA decryption password cannot be configured, but passed at runtime via/nthe environment variable `RAD_PASSPHRASE`.",
          "type": [
@@ -660,6 +664,62 @@ $ rad config schema
          "const": "all"
        }
      ]
+
    },
+
    "Config": {
+
      "description": "Database configuration.",
+
      "type": "object",
+
      "properties": {
+
        "sqlite": {
+
          "description": "SQLite configuration.",
+
          "$ref": "#/$defs/SqliteConfig"
+
        }
+
      },
+
      "required": [
+
        "sqlite"
+
      ]
+
    },
+
    "SqliteConfig": {
+
      "description": "SQLite database configuration.",
+
      "type": "object",
+
      "properties": {
+
        "pragma": {
+
          "$ref": "#/$defs/Pragma"
+
        }
+
      }
+
    },
+
    "Pragma": {
+
      "description": "Global SQLite pragma statements to make in order to configure SQLite itself,/nsee <https://sqlite.org/pragma.html>.",
+
      "type": "object",
+
      "properties": {
+
        "journalMode": {
+
          "$ref": "#/$defs/JournalMode"
+
        },
+
        "synchronous": {
+
          "$ref": "#/$defs/Synchronous"
+
        }
+
      }
+
    },
+
    "JournalMode": {
+
      "description": "Value for a `journal_mode` pragma statement./nFor a description of all variants please refer to/n<https://sqlite.org/pragma.html#pragma_journal_mode>./nNote that when SQLite documentation talks about /"the application/",/nthe application linked against this crate, e.g. Radicle Node, Radicle CLI,/nand others, is meant.",
+
      "type": "string",
+
      "enum": [
+
        "DELETE",
+
        "TRUNCATE",
+
        "PERSIST",
+
        "MEMORY",
+
        "WAL",
+
        "OFF"
+
      ]
+
    },
+
    "Synchronous": {
+
      "description": "Value for a `synchronous` pragma statement./nFor a description of all variants please refer to/n<https://sqlite.org/pragma.html#pragma_synchronous>.",
+
      "type": "string",
+
      "enum": [
+
        "EXTRA",
+
        "FULL",
+
        "NORMAL",
+
        "OFF"
+
      ]
    }
  }
}
modified crates/radicle-node/src/runtime.rs
@@ -177,7 +177,8 @@ impl Runtime {
        log::info!(target: "node", "Opening node database..");
        let db = home
            .database_mut()?
-
            .journal_mode(node::db::sqlite_ext::JournalMode::WAL)?
+
            .journal_mode(config.database.sqlite.pragma.journal_mode)?
+
            .synchronous(config.database.sqlite.pragma.synchronous)?
            .init(
                &id,
                announcement.features,
modified crates/radicle/CHANGELOG.md
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

+
- SQLite configuration is modeled as `radicle::node::db::config::Config`
+
  and can be configured via `radicle::profile::config::Config`.
+
  The two pragmas `journal_mode` and `synchronous` are exposed this way.
+

### Changed

### Removed
modified crates/radicle/src/node/config.rs
@@ -509,6 +509,9 @@ pub struct Config {
    /// Default seeding policy.
    #[serde(default)]
    pub seeding_policy: DefaultSeedingPolicy,
+
    /// Database configuration.
+
    #[serde(default, skip_serializing_if = "crate::serde_ext::is_default")]
+
    pub database: node::db::config::Config,
    /// Extra fields that aren't supported.
    #[serde(flatten, skip_serializing)]
    pub extra: json::Map<String, json::Value>,
@@ -545,6 +548,7 @@ impl Config {
            workers: Workers::default(),
            log: LogLevel::default(),
            seeding_policy: DefaultSeedingPolicy::default(),
+
            database: node::db::config::Config::default(),
            extra: json::Map::default(),
            secret: None,
        }
modified crates/radicle/src/node/db.rs
@@ -20,6 +20,7 @@ use crate::node::{
};
use crate::sql::transaction;

+
pub mod config;
pub mod sqlite_ext;

/// How long to wait for the database lock to be released before failing a read.
added crates/radicle/src/node/db/config.rs
@@ -0,0 +1,183 @@
+
use super::sqlite_ext::*;
+

+
const DEFAULT_JOURNAL_MODE: JournalMode = JournalMode::WAL;
+
const DEFAULT_SYNCHRONOUS: Synchronous = Synchronous::FULL;
+

+
/// Database configuration.
+
#[derive(Debug, Default, Copy, Clone, PartialEq, ::serde::Serialize, ::serde::Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+
pub struct Config {
+
    /// SQLite configuration.
+
    #[serde(skip_serializing_if = "crate::serde_ext::is_default")]
+
    pub sqlite: SqliteConfig,
+
}
+

+
/// SQLite database configuration.
+
#[derive(Debug, Default, Copy, Clone, PartialEq, ::serde::Serialize, ::serde::Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+
pub struct SqliteConfig {
+
    #[serde(default, skip_serializing_if = "crate::serde_ext::is_default")]
+
    pub pragma: Pragma,
+
}
+

+
/// Global SQLite pragma statements to make in order to configure SQLite itself,
+
/// see <https://sqlite.org/pragma.html>.
+
#[derive(Debug, Copy, Clone, PartialEq, ::serde::Serialize, ::serde::Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+
pub struct Pragma {
+
    #[serde(
+
        default = "serde::journal_mode::default",
+
        skip_serializing_if = "serde::journal_mode::is_default"
+
    )]
+
    pub journal_mode: JournalMode,
+

+
    #[serde(
+
        default = "serde::synchronous::default",
+
        skip_serializing_if = "serde::synchronous::is_default"
+
    )]
+
    pub synchronous: Synchronous,
+
}
+

+
pub mod serde {
+
    use super::*;
+

+
    pub mod journal_mode {
+
        use super::*;
+

+
        pub fn default() -> JournalMode {
+
            DEFAULT_JOURNAL_MODE
+
        }
+

+
        pub fn is_default(journal_mode: &JournalMode) -> bool {
+
            matches!(journal_mode, &DEFAULT_JOURNAL_MODE)
+
        }
+
    }
+

+
    pub mod synchronous {
+
        use super::*;
+

+
        pub fn default() -> Synchronous {
+
            DEFAULT_SYNCHRONOUS
+
        }
+

+
        pub fn is_default(synchronous: &Synchronous) -> bool {
+
            matches!(synchronous, &DEFAULT_SYNCHRONOUS)
+
        }
+
    }
+
}
+

+
impl Default for Pragma {
+
    fn default() -> Self {
+
        Self {
+
            journal_mode: DEFAULT_JOURNAL_MODE,
+
            synchronous: DEFAULT_SYNCHRONOUS,
+
        }
+
    }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use crate::assert_matches;
+

+
    use super::*;
+
    use serde_json::json;
+

+
    #[test]
+
    fn database_config_valid_combinations() {
+
        let cases = [
+
            (None, None, JournalMode::WAL, Synchronous::FULL),
+
            (
+
                Some("WAL"),
+
                Some("NORMAL"),
+
                JournalMode::WAL,
+
                Synchronous::NORMAL,
+
            ),
+
            (
+
                Some("WAL"),
+
                Some("FULL"),
+
                JournalMode::WAL,
+
                Synchronous::FULL,
+
            ),
+
            (Some("WAL"), Some("OFF"), JournalMode::WAL, Synchronous::OFF),
+
            (
+
                Some("DELETE"),
+
                Some("FULL"),
+
                JournalMode::DELETE,
+
                Synchronous::FULL,
+
            ),
+
            (
+
                Some("DELETE"),
+
                Some("EXTRA"),
+
                JournalMode::DELETE,
+
                Synchronous::EXTRA,
+
            ),
+
            (
+
                Some("WAL"),
+
                Some("NORMAL"),
+
                JournalMode::WAL,
+
                Synchronous::NORMAL,
+
            ),
+
            (
+
                Some("DELETE"),
+
                Some("NORMAL"),
+
                JournalMode::DELETE,
+
                Synchronous::NORMAL,
+
            ),
+
        ];
+

+
        for (journal_mode, synchronous, expected_journal_mode, expected_synchronous) in cases {
+
            let mut config = json!({});
+

+
            if let Some(journal_mode) = journal_mode {
+
                config["pragma"]["journalMode"] = json!(journal_mode);
+
            }
+

+
            if let Some(synchronous) = synchronous {
+
                config["pragma"]["synchronous"] = json!(synchronous);
+
            }
+

+
            #[allow(clippy::unwrap_used)]
+
            let config: SqliteConfig = serde_json::from_value(config).unwrap();
+

+
            assert_eq!(
+
                config.pragma,
+
                Pragma {
+
                    journal_mode: expected_journal_mode,
+
                    synchronous: expected_synchronous
+
                }
+
            );
+
        }
+
    }
+

+
    #[test]
+
    fn invalid() {
+
        let invalid_cases = [
+
            (Some("INVALID"), Some("NORMAL"), "invalid journal_mode"),
+
            (Some("WAL"), Some("INVALID"), "invalid synchronous"),
+
            (Some("WAL"), Some("normal"), "lowercase synchronous"),
+
            (Some("Wal"), Some("NORMAL"), "mixed case journal_mode"),
+
        ];
+

+
        for (journal_mode, synchronous, description) in invalid_cases {
+
            let mut pragma = json!({});
+

+
            if let Some(journal_mode) = journal_mode {
+
                pragma["journalMode"] = json!(journal_mode);
+
            }
+

+
            if let Some(synchronous) = synchronous {
+
                pragma["synchronous"] = json!(synchronous);
+
            }
+

+
            assert_matches!(
+
                serde_json::from_value::<Pragma>(pragma),
+
                Err(_),
+
                "{}",
+
                description,
+
            );
+
        }
+
    }
+
}
modified crates/radicle/src/profile.rs
@@ -253,7 +253,8 @@ impl Profile {
        home.policies_mut()?;
        home.notifications_mut()?;
        home.database_mut()?
-
            .journal_mode(node::db::sqlite_ext::JournalMode::WAL)?
+
            .journal_mode(config.node.database.sqlite.pragma.journal_mode)?
+
            .synchronous(config.node.database.sqlite.pragma.synchronous)?
            .init(
                &public_key,
                config.node.features(),