radicle: Make SQLite pragmas configurable

Expose choosing particular SQLite pragmas via configuration.
This commit is contained in:
Yorgos Saslis 2026-02-20 18:08:59 +02:00 committed by Fintan Halpenny
parent 6cc3da95e6
commit f4aee203ab
7 changed files with 256 additions and 2 deletions

View File

@ -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"
]
}
}
}

View File

@ -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,

View File

@ -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

View File

@ -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,
}

View File

@ -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.

View File

@ -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,
);
}
}
}

View File

@ -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(),