1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
|
use std::{
fmt::Display,
time::{SystemTime, UNIX_EPOCH},
};
use anyhow::{Context, Result, bail};
use chrono::TimeDelta;
use log::{debug, info};
use sqlx::{Sqlite, Transaction, query};
use crate::app::App;
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub enum DbVersion {
/// The database is not yet initialized.
Empty,
/// The first database version.
/// Introduced: 2025-02-16.
Zero,
/// Introduced: 2025-02-17.
One,
}
const CURRENT_VERSION: DbVersion = DbVersion::One;
async fn set_db_version(
tx: &mut Transaction<'_, Sqlite>,
old_version: Option<DbVersion>,
new_version: DbVersion,
) -> Result<()> {
let valid_from = get_current_date();
if let Some(old_version) = old_version {
let valid_to = valid_from + 1;
let old_version = old_version.as_sql_integer();
query!(
"UPDATE version SET valid_to = ? WHERE namespace = 'yt' AND number = ?;",
valid_to,
old_version
)
.execute(&mut *(*tx))
.await?;
}
let version = new_version.as_sql_integer();
query!(
"INSERT INTO version (namespace, number, valid_from, valid_to) VALUES ('yt', ?, ?, NULL);",
version,
valid_from
)
.execute(&mut *(*tx))
.await?;
Ok(())
}
impl DbVersion {
fn as_sql_integer(self) -> i32 {
match self {
DbVersion::Empty => unreachable!("A empty version does not have an associated integer"),
DbVersion::Zero => 0,
DbVersion::One => 1,
}
}
fn from_db(number: i64, namespace: &str) -> Result<Self> {
match (number, namespace) {
(0, "yt") => Ok(DbVersion::Zero),
(1, "yt") => Ok(DbVersion::One),
(0, other) => bail!("Db version is Zero, but got unknown namespace: '{other}'"),
(1, other) => bail!("Db version is One, but got unknown namespace: '{other}'"),
(other, "yt") => bail!("Got unkown version for 'yt' namespace: {other}"),
(num, nasp) => bail!("Got unkown version number ({num}) and namespace ('{nasp}')"),
}
}
/// Try to update the database from version [`self`] to the [`CURRENT_VERSION`].
///
/// Each update is atomic, so if this function fails you are still guaranteed to have a
/// database at version `get_version`.
async fn update(self, app: &App) -> Result<()> {
match self {
DbVersion::Empty => {
let mut tx = app.database.begin().await?;
debug!("Migrate: Empty -> Zero");
sqlx::raw_sql(include_str!("./sql/00_empty_to_zero.sql"))
.execute(&mut *tx)
.await?;
set_db_version(&mut tx, None, DbVersion::Zero).await?;
tx.commit().await?;
Box::pin(Self::Zero.update(app)).await
}
DbVersion::Zero => {
let mut tx = app.database.begin().await?;
debug!("Migrate: Zero -> One");
sqlx::raw_sql(include_str!("./sql/01_zero_to_one.sql"))
.execute(&mut *tx)
.await?;
set_db_version(&mut tx, Some(DbVersion::Zero), DbVersion::One).await?;
tx.commit().await?;
Box::pin(Self::One.update(app)).await
}
// This is the current_version
DbVersion::One => {
debug!("Migrate: One -> Two");
assert_eq!(self, CURRENT_VERSION);
assert_eq!(self, get_version(app).await?);
Ok(())
}
}
}
}
impl Display for DbVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// It is a unit only enum, thus we can simply use the Debug formatting
<Self as std::fmt::Debug>::fmt(self, f)
}
}
/// Returns the current data as UNIX time stamp.
fn get_current_date() -> i64 {
let start = SystemTime::now();
let seconds_since_epoch: TimeDelta = TimeDelta::from_std(
start
.duration_since(UNIX_EPOCH)
.expect("Time went backwards"),
)
.expect("Time does not go backwards");
debug!(
"Adding a date with timestamp: {}",
seconds_since_epoch.num_seconds()
);
// All database dates should be after the UNIX_EPOCH (and thus positiv)
seconds_since_epoch.num_seconds()
}
/// Return the current database version.
///
/// # Panics
/// Only if internal assertions fail.
pub async fn get_version(app: &App) -> Result<DbVersion> {
let version_table_exists = {
let query = query!(
"SELECT 1 as result FROM sqlite_master WHERE type = 'table' AND name = 'version'"
)
.fetch_optional(&app.database)
.await?;
if let Some(output) = query {
assert_eq!(output.result, 1);
true
} else {
false
}
};
if !version_table_exists {
return Ok(DbVersion::Empty);
}
let current_version = query!(
"
SELECT namespace, number FROM version WHERE valid_to IS NULL;
"
)
.fetch_one(&app.database)
.await
.context("Failed to fetch version number")?;
DbVersion::from_db(current_version.number, current_version.namespace.as_str())
}
pub async fn migrate_db(app: &App) -> Result<()> {
let current_version = get_version(app).await?;
if current_version == CURRENT_VERSION {
return Ok(());
}
info!("Migrate database from version '{current_version}' to version '{CURRENT_VERSION}'");
current_version.update(app).await?;
Ok(())
}
|