Skip to main content

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