aven_core/db/
inspection.rs1use std::fs;
2use std::io::Read as _;
3use std::path::Path;
4use std::sync::Arc;
5use std::time::Duration;
6
7use sqlx::Row;
8use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
9use tokio::sync::Mutex;
10
11use super::{Database, MIGRATOR, shm_path, wal_path};
12
13#[derive(Debug, Clone)]
14pub struct DatabaseInspection {
15 pub exists: bool,
16 pub is_file: bool,
17 pub header_is_sqlite: bool,
18 pub read_only_permissions: bool,
19 pub wal_exists: bool,
20 pub shm_exists: bool,
21 pub schema_version: Option<i64>,
22 pub latest_schema_version: Option<i64>,
23 pub pending_migrations: Vec<i64>,
24 pub unsupported_future_schema: bool,
25 pub failed_migration: Option<i64>,
26 pub open_error: Option<String>,
27}
28
29impl DatabaseInspection {
30 fn unavailable(latest_schema_version: Option<i64>) -> Self {
31 Self {
32 exists: false,
33 is_file: false,
34 header_is_sqlite: false,
35 read_only_permissions: false,
36 wal_exists: false,
37 shm_exists: false,
38 schema_version: None,
39 latest_schema_version,
40 pending_migrations: Vec::new(),
41 unsupported_future_schema: false,
42 failed_migration: None,
43 open_error: None,
44 }
45 }
46
47 pub fn supports_runtime_checks(&self) -> bool {
48 self.open_error.is_none()
49 && self.failed_migration.is_none()
50 && !self.unsupported_future_schema
51 && self.pending_migrations.is_empty()
52 }
53}
54
55pub struct InspectedDatabase {
56 pub inspection: DatabaseInspection,
57 pub database: Option<Database>,
58}
59
60impl Database {
61 pub fn latest_schema_version() -> Option<i64> {
62 MIGRATOR.iter().map(|migration| migration.version).max()
63 }
64
65 pub async fn inspect(path: &Path) -> InspectedDatabase {
71 let latest = Self::latest_schema_version();
72 let mut inspection = DatabaseInspection::unavailable(latest);
73 inspection.wal_exists = wal_path(path).exists();
74 inspection.shm_exists = shm_path(path).exists();
75
76 let metadata = match fs::metadata(path) {
77 Ok(metadata) => metadata,
78 Err(error) => {
79 inspection.open_error = Some(if error.kind() == std::io::ErrorKind::NotFound {
80 "database file does not exist".to_string()
81 } else {
82 format!("database metadata is unavailable: {}", error.kind())
83 });
84 return InspectedDatabase {
85 inspection,
86 database: None,
87 };
88 }
89 };
90 inspection.exists = true;
91 inspection.is_file = metadata.is_file();
92 inspection.read_only_permissions = metadata.permissions().readonly();
93 if !inspection.is_file {
94 inspection.open_error = Some("database path is not a regular file".to_string());
95 return InspectedDatabase {
96 inspection,
97 database: None,
98 };
99 }
100
101 let mut header = [0_u8; 16];
102 inspection.header_is_sqlite = fs::File::open(path)
103 .and_then(|mut file| file.read_exact(&mut header))
104 .is_ok()
105 && &header == b"SQLite format 3\0";
106 if !inspection.header_is_sqlite {
107 inspection.open_error = Some("database header is not SQLite format 3".to_string());
108 return InspectedDatabase {
109 inspection,
110 database: None,
111 };
112 }
113
114 let inspection_dir = match tempfile::Builder::new().prefix("aven-doctor-").tempdir() {
115 Ok(dir) => Arc::new(dir),
116 Err(error) => {
117 inspection.open_error = Some(format!(
118 "could not create isolated inspection storage: {error}"
119 ));
120 return InspectedDatabase {
121 inspection,
122 database: None,
123 };
124 }
125 };
126 let snapshot_path = inspection_dir.path().join("database.sqlite");
127 if let Err(error) = fs::copy(path, &snapshot_path) {
128 inspection.open_error =
129 Some(format!("could not copy database for inspection: {error}"));
130 return InspectedDatabase {
131 inspection,
132 database: None,
133 };
134 }
135 for (source, target) in [
136 (wal_path(path), wal_path(&snapshot_path)),
137 (shm_path(path), shm_path(&snapshot_path)),
138 ] {
139 if source.exists()
140 && let Err(error) = fs::copy(&source, &target)
141 {
142 inspection.open_error = Some(format!(
143 "could not copy SQLite sidecar {} for inspection: {error}",
144 source.display()
145 ));
146 return InspectedDatabase {
147 inspection,
148 database: None,
149 };
150 }
151 }
152
153 let options = SqliteConnectOptions::new()
154 .filename(&snapshot_path)
155 .foreign_keys(true)
156 .busy_timeout(Duration::from_secs(5));
157 let pool = match SqlitePoolOptions::new()
158 .min_connections(1)
159 .max_connections(1)
160 .idle_timeout(None)
161 .max_lifetime(None)
162 .connect_with(options)
163 .await
164 {
165 Ok(pool) => pool,
166 Err(error) => {
167 inspection.open_error = Some(format!("SQLite read-only open failed: {error}"));
168 return InspectedDatabase {
169 inspection,
170 database: None,
171 };
172 }
173 };
174
175 let migration_rows = sqlx::query("SELECT version, success FROM _sqlx_migrations")
176 .fetch_all(&pool)
177 .await;
178 let mut applied = Vec::new();
179 match migration_rows {
180 Ok(rows) => {
181 for row in rows {
182 let version = row.try_get::<i64, _>("version").unwrap_or_default();
183 if row.try_get::<bool, _>("success").unwrap_or(false) {
184 applied.push(version);
185 } else {
186 inspection.failed_migration = Some(version);
187 }
188 }
189 inspection.schema_version = applied.iter().copied().max().or(Some(0));
190 }
191 Err(error) => {
192 let missing_table = error
193 .as_database_error()
194 .is_some_and(|error| error.code().as_deref() == Some("1"));
195 if missing_table {
196 inspection.schema_version = Some(0);
197 } else {
198 inspection.open_error = Some(format!("could not inspect schema: {error}"));
199 }
200 }
201 }
202 inspection.pending_migrations = MIGRATOR
203 .iter()
204 .filter(|migration| !applied.contains(&migration.version))
205 .map(|migration| migration.version)
206 .collect();
207 inspection.unsupported_future_schema = inspection
208 .schema_version
209 .zip(latest)
210 .is_some_and(|(current, latest)| current > latest);
211
212 let database = inspection.supports_runtime_checks().then(|| Database {
213 pool,
214 writer: Arc::new(Mutex::new(())),
215 path: path.to_path_buf(),
216 file_identity: fs::canonicalize(path).ok(),
217 _inspection_dir: Some(inspection_dir),
218 });
219 InspectedDatabase {
220 inspection,
221 database,
222 }
223 }
224}