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