veloren_server/persistence/
mod.rs

1//! DB operations and schema migrations
2
3// Touch this comment if changes only include .sql files and no .rs so that
4// migration happens.
5// meow~
6
7pub(in crate::persistence) mod character;
8pub mod character_loader;
9pub mod character_updater;
10mod diesel_to_rusqlite;
11pub mod error;
12mod json_models;
13mod models;
14
15use crate::persistence::character_updater::PetPersistenceData;
16use common::comp;
17use refinery::Report;
18use rusqlite::{Connection, OpenFlags};
19use std::{
20    fs,
21    ops::Deref,
22    path::PathBuf,
23    sync::{Arc, RwLock},
24    time::Duration,
25};
26use tracing::info;
27
28// re-export waypoint parser for use to look up location names in character list
29pub(crate) use character::parse_waypoint;
30
31/// A struct of the components that are persisted to the DB for each character
32#[derive(Debug)]
33pub struct PersistedComponents {
34    pub body: comp::Body,
35    pub hardcore: Option<comp::Hardcore>,
36    pub stats: comp::Stats,
37    pub skill_set: comp::SkillSet,
38    pub inventory: comp::Inventory,
39    pub waypoint: Option<comp::Waypoint>,
40    pub pets: Vec<PetPersistenceData>,
41    pub active_abilities: comp::ActiveAbilities,
42    pub map_marker: Option<comp::MapMarker>,
43}
44
45pub type EditableComponents = (comp::Body,);
46
47// See: https://docs.rs/refinery/0.5.0/refinery/macro.embed_migrations.html
48// This macro is called at build-time, and produces the necessary migration info
49// for the `run_migrations` call below.
50mod embedded {
51    use refinery::embed_migrations;
52    embed_migrations!("./src/migrations");
53}
54
55/// A database connection blessed by Veloren.
56pub(crate) struct VelorenConnection {
57    connection: Connection,
58    sql_log_mode: SqlLogMode,
59}
60
61impl VelorenConnection {
62    fn new(connection: Connection) -> Self {
63        Self {
64            connection,
65            sql_log_mode: SqlLogMode::Disabled,
66        }
67    }
68
69    /// Updates the SQLite log mode if DatabaseSetting.sql_log_mode has changed
70    pub fn update_log_mode(&mut self, database_settings: &Arc<RwLock<DatabaseSettings>>) {
71        let settings = database_settings
72            .read()
73            .expect("DatabaseSettings RwLock was poisoned");
74        if self.sql_log_mode == settings.sql_log_mode {
75            return;
76        }
77
78        set_log_mode(&mut self.connection, settings.sql_log_mode);
79        self.sql_log_mode = settings.sql_log_mode;
80
81        info!(
82            "SQL log mode for connection changed to {:?}",
83            settings.sql_log_mode
84        );
85    }
86}
87
88impl Deref for VelorenConnection {
89    type Target = Connection;
90
91    fn deref(&self) -> &Connection { &self.connection }
92}
93
94fn set_log_mode(connection: &mut Connection, sql_log_mode: SqlLogMode) {
95    // Rusqlite's trace and profile logging are mutually exclusive and cannot be
96    // used together
97    match sql_log_mode {
98        SqlLogMode::Trace => {
99            connection.trace(Some(rusqlite_trace_callback));
100        },
101        SqlLogMode::Profile => {
102            connection.profile(Some(rusqlite_profile_callback));
103        },
104        SqlLogMode::Disabled => {
105            connection.trace(None);
106            connection.profile(None);
107        },
108    };
109}
110
111#[derive(Clone)]
112pub struct DatabaseSettings {
113    pub db_dir: PathBuf,
114    pub sql_log_mode: SqlLogMode,
115}
116
117#[derive(Clone, Copy, PartialEq, Eq)]
118pub enum ConnectionMode {
119    ReadOnly,
120    ReadWrite,
121}
122
123#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
124pub enum SqlLogMode {
125    /// Logging is disabled
126    #[default]
127    Disabled,
128    /// Records timings for each SQL statement
129    Profile,
130    /// Prints all executed SQL statements
131    Trace,
132}
133
134impl SqlLogMode {
135    pub fn variants() -> [&'static str; 3] { ["disabled", "profile", "trace"] }
136}
137
138impl core::str::FromStr for SqlLogMode {
139    type Err = &'static str;
140
141    fn from_str(s: &str) -> Result<Self, Self::Err> {
142        match s {
143            "disabled" => Ok(Self::Disabled),
144            "profile" => Ok(Self::Profile),
145            "trace" => Ok(Self::Trace),
146            _ => Err("Could not parse SqlLogMode"),
147        }
148    }
149}
150
151#[expect(clippy::to_string_trait_impl)]
152impl ToString for SqlLogMode {
153    fn to_string(&self) -> String {
154        match self {
155            SqlLogMode::Disabled => "disabled",
156            SqlLogMode::Profile => "profile",
157            SqlLogMode::Trace => "trace",
158        }
159        .into()
160    }
161}
162
163/// Runs any pending database migrations. This is executed during server startup
164pub fn run_migrations(settings: &DatabaseSettings) {
165    let mut conn = establish_connection(settings, ConnectionMode::ReadWrite);
166
167    diesel_to_rusqlite::migrate_from_diesel(&mut conn)
168        .expect("One-time migration from Diesel to Refinery failed");
169
170    // If migrations fail to run, the server cannot start since the database will
171    // not be in the required state.
172    let report: Report = embedded::migrations::runner()
173        .set_abort_divergent(false)
174        .run(&mut conn.connection)
175        .expect("Database migrations failed, server startup aborted");
176
177    let applied_migrations = report.applied_migrations().len();
178    info!("Applied {} database migrations", applied_migrations);
179}
180
181/// Runs after the migrations. In some cases, it can reclaim a significant
182/// amount of space (reported 30%)
183pub fn vacuum_database(settings: &DatabaseSettings) {
184    let conn = establish_connection(settings, ConnectionMode::ReadWrite);
185
186    conn.execute("VACUUM main", [])
187        .expect("Database vacuuming failed, server startup aborted");
188
189    info!("Database vacuumed");
190}
191
192// These callbacks use info logging because they are never enabled by default,
193// only when explicitly turned on via CLI arguments or interactive CLI commands.
194// Setting them to anything other than info would remove the ability to get SQL
195// logging from a running server that wasn't started at higher than info.
196fn rusqlite_trace_callback(log_message: &str) {
197    info!("{}", log_message);
198}
199fn rusqlite_profile_callback(log_message: &str, dur: Duration) {
200    info!("{} Duration: {:?}", log_message, dur);
201}
202
203pub(crate) fn establish_connection(
204    settings: &DatabaseSettings,
205    connection_mode: ConnectionMode,
206) -> VelorenConnection {
207    fs::create_dir_all(&settings.db_dir)
208        .unwrap_or_else(|_| panic!("Failed to create saves directory: {:?}", &settings.db_dir));
209
210    let open_flags = OpenFlags::SQLITE_OPEN_PRIVATE_CACHE
211        | OpenFlags::SQLITE_OPEN_NO_MUTEX
212        | match connection_mode {
213            ConnectionMode::ReadWrite => {
214                OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_READ_WRITE
215            },
216            ConnectionMode::ReadOnly => OpenFlags::SQLITE_OPEN_READ_ONLY,
217        };
218
219    let connection = Connection::open_with_flags(settings.db_dir.join("db.sqlite"), open_flags)
220        .unwrap_or_else(|err| {
221            panic!(
222                "Error connecting to {}, Error: {:?}",
223                settings.db_dir.join("db.sqlite").display(),
224                err
225            )
226        });
227
228    let mut veloren_connection = VelorenConnection::new(connection);
229
230    let connection = &mut veloren_connection.connection;
231
232    set_log_mode(connection, settings.sql_log_mode);
233    veloren_connection.sql_log_mode = settings.sql_log_mode;
234
235    rusqlite::vtab::array::load_module(connection).expect("Failed to load sqlite array module");
236
237    connection.set_prepared_statement_cache_capacity(100);
238
239    // Use Write-Ahead-Logging for improved concurrency: https://sqlite.org/wal.html
240    // Set a busy timeout (in ms): https://sqlite.org/c3ref/busy_timeout.html
241    connection
242        .pragma_update(None, "foreign_keys", "ON")
243        .expect("Failed to set foreign_keys PRAGMA");
244    connection
245        .pragma_update(None, "journal_mode", "WAL")
246        .expect("Failed to set journal_mode PRAGMA");
247    connection
248        .pragma_update(None, "busy_timeout", "250")
249        .expect("Failed to set busy_timeout PRAGMA");
250
251    veloren_connection
252}