Skip to content

Commit 650a656

Browse files
authored
Merge pull request silvermine#19 from pmorris-dev/dev_quality_assurance
A round of dev quality assurance
2 parents 1dcd10a + a1c9240 commit 650a656

6 files changed

Lines changed: 39 additions & 45 deletions

File tree

crates/sqlx-sqlite-conn-mgr/src/database.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ use std::sync::Arc;
1212
use std::sync::atomic::{AtomicBool, Ordering};
1313
use tracing::error;
1414

15+
/// Analysis limit for PRAGMA optimize on close.
16+
/// SQLite recommends 100-1000 for older versions; 3.46.0+ handles automatically.
17+
/// See: https://www.sqlite.org/lang_analyze.html#recommended_usage_pattern
18+
const OPTIMIZE_ANALYSIS_LIMIT: u32 = 400;
19+
1520
/// SQLite database with connection pooling for concurrent reads and optional exclusive writes.
1621
///
1722
/// Once the database is opened it can be used for read-only operations by calling `read_pool()`.
@@ -144,16 +149,11 @@ impl SqliteDatabase {
144149
drop(conn); // Close immediately after creating the file
145150
}
146151

147-
// Enable PRAGMA optimize on close as recommended by SQLite for long-lived databases.
148-
// SQLite recommends analysis_limit values between 100-1000 for older versions;
149-
// SQLite 3.46.0+ handles limits automatically.
150-
// https://www.sqlite.org/lang_analyze.html#recommended_usage_pattern
151-
//
152152
// Create read pool with read-only connections
153153
let read_options = SqliteConnectOptions::new()
154154
.filename(&path)
155155
.read_only(true)
156-
.optimize_on_close(true, 400);
156+
.optimize_on_close(true, OPTIMIZE_ANALYSIS_LIMIT);
157157

158158
let read_pool = SqlitePoolOptions::new()
159159
.max_connections(config.max_read_connections)
@@ -168,7 +168,7 @@ impl SqliteDatabase {
168168
let write_options = SqliteConnectOptions::new()
169169
.filename(&path)
170170
.read_only(false)
171-
.optimize_on_close(true, 400);
171+
.optimize_on_close(true, OPTIMIZE_ANALYSIS_LIMIT);
172172

173173
let write_conn = SqlitePoolOptions::new()
174174
.max_connections(1)
@@ -250,8 +250,12 @@ impl SqliteDatabase {
250250
// Acquire connection from pool (max=1 ensures exclusive access)
251251
let mut conn = self.write_conn.acquire().await?;
252252

253-
// Initialize WAL mode on first use (idempotent and safe)
254-
if !self.wal_initialized.load(Ordering::SeqCst) {
253+
// Initialize WAL mode on first use (atomic check-and-set)
254+
if self
255+
.wal_initialized
256+
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
257+
.is_ok()
258+
{
255259
sqlx::query("PRAGMA journal_mode = WAL")
256260
.execute(&mut *conn)
257261
.await?;
@@ -260,8 +264,6 @@ impl SqliteDatabase {
260264
sqlx::query("PRAGMA synchronous = NORMAL")
261265
.execute(&mut *conn)
262266
.await?;
263-
264-
self.wal_initialized.store(true, Ordering::SeqCst);
265267
}
266268

267269
// Return WriteGuard wrapping the pool connection

src/commands.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub struct TransactionToken {
2727
pub transaction_id: String,
2828
}
2929

30-
/// Actions that can be taken on a pausable transaction
30+
/// Actions that can be taken on an interruptible transaction
3131
#[derive(Debug, Deserialize)]
3232
#[serde(tag = "type")]
3333
pub enum TransactionAction {
@@ -264,12 +264,18 @@ pub async fn close_all(db_instances: State<'_, DbInstances>) -> Result<()> {
264264
// Collect all wrappers to close
265265
let wrappers: Vec<DatabaseWrapper> = instances.drain().map(|(_, v)| v).collect();
266266

267-
// Close each connection
267+
// Close each connection, continuing on errors to ensure all get closed
268+
let mut last_error = None;
268269
for wrapper in wrappers {
269-
wrapper.close().await?;
270+
if let Err(e) = wrapper.close().await {
271+
last_error = Some(e);
272+
}
270273
}
271274

272-
Ok(())
275+
match last_error {
276+
Some(e) => Err(e),
277+
None => Ok(()),
278+
}
273279
}
274280

275281
/// Close database connection and remove all database files
@@ -343,12 +349,8 @@ pub async fn execute_interruptible_transaction(
343349
q.execute(&mut *writer).await?;
344350
}
345351

346-
// Create abort handle for transaction cleanup on app exit
347-
let abort_handle = tokio::spawn(std::future::pending::<()>()).abort_handle();
348-
349352
// Store transaction state
350-
let tx =
351-
ActiveInterruptibleTransaction::new(db.clone(), transaction_id.clone(), writer, abort_handle);
353+
let tx = ActiveInterruptibleTransaction::new(db.clone(), transaction_id.clone(), writer);
352354

353355
active_txs.insert(db.clone(), tx).await?;
354356

src/decode.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use crate::Error;
99
///
1010
/// This function handles the type conversion from SQLite's native types
1111
/// to JSON-compatible representations.
12+
///
13+
/// Note: BLOB values are returned as base64-encoded strings since JSON
14+
/// has no native binary type. Boolean values are stored as INTEGER in SQLite.
1215
pub fn to_json(value: SqliteValueRef) -> Result<JsonValue, Error> {
1316
if value.is_null() {
1417
return Ok(JsonValue::Null);

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ pub struct MigrationEvent {
7676
pub db_path: String,
7777
/// Status: "running", "completed", "failed"
7878
pub status: String,
79-
/// Total number of migrations in the migrator (on "completed"), not just newly applied
79+
/// Total number of migrations defined in the migrator (on "completed"), not just newly applied
8080
#[serde(skip_serializing_if = "Option::is_none")]
8181
pub migration_count: Option<usize>,
8282
/// Error message (on "failed")

src/transactions.rs

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
33
use std::collections::HashMap;
44
use std::sync::Arc;
5-
use std::time::Instant;
65

76
use indexmap::IndexMap;
87
use serde::Deserialize;
@@ -20,23 +19,14 @@ pub struct ActiveInterruptibleTransaction {
2019
db_path: String,
2120
transaction_id: String,
2221
writer: WriteGuard,
23-
abort_handle: AbortHandle,
24-
created_at: Instant,
2522
}
2623

2724
impl ActiveInterruptibleTransaction {
28-
pub fn new(
29-
db_path: String,
30-
transaction_id: String,
31-
writer: WriteGuard,
32-
abort_handle: AbortHandle,
33-
) -> Self {
25+
pub fn new(db_path: String, transaction_id: String, writer: WriteGuard) -> Self {
3426
Self {
3527
db_path,
3628
transaction_id,
3729
writer,
38-
abort_handle,
39-
created_at: Instant::now(),
4030
}
4131
}
4232

@@ -48,10 +38,6 @@ impl ActiveInterruptibleTransaction {
4838
&self.transaction_id
4939
}
5040

51-
pub fn created_at(&self) -> Instant {
52-
self.created_at
53-
}
54-
5541
pub fn validate_token(&self, token_id: &str) -> Result<()> {
5642
if self.transaction_id != token_id {
5743
return Err(Error::InvalidTransactionToken);
@@ -157,15 +143,15 @@ impl ActiveInterruptibleTransactions {
157143
let mut txs = self.0.write().await;
158144
debug!("Aborting {} active interruptible transaction(s)", txs.len());
159145

160-
for (db_path, tx) in txs.iter() {
146+
for db_path in txs.keys() {
161147
debug!(
162-
"Aborting interruptible transaction for database: {}",
148+
"Dropping interruptible transaction for database: {}",
163149
db_path
164150
);
165-
tx.abort_handle.abort();
166151
}
167152

168153
// Clear all transactions to drop WriteGuards and release locks
154+
// Dropping triggers auto-rollback via Drop trait
169155
txs.clear();
170156
}
171157

src/wrapper.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ impl DatabaseWrapper {
177177
}
178178

179179
/// Execute a SELECT query expecting zero or one result
180+
///
181+
/// Returns an error if the query returns more than one row.
180182
pub async fn fetch_one(
181183
&self,
182184
query: String,
@@ -185,11 +187,7 @@ impl DatabaseWrapper {
185187
// Use read pool for queries
186188
let pool = self.inner.read_pool()?;
187189

188-
// Add LIMIT 2 to detect if query returns multiple rows
189-
// We only need to fetch up to 2 rows to know if there's more than 1
190-
let limited_query = format!("{} LIMIT 2", query.trim_end_matches(';'));
191-
192-
let mut q = sqlx::query(&limited_query);
190+
let mut q = sqlx::query(&query);
193191
for value in values {
194192
q = bind_value(q, value);
195193
}
@@ -274,7 +272,10 @@ pub(crate) fn bind_value<'a>(
274272
}
275273
}
276274

277-
/// Resolve database file path relative to app config directory
275+
/// Resolve database file path relative to app config directory.
276+
///
277+
/// Paths are joined to `app_config_dir()` (e.g., `Library/Application Support/${bundleIdentifier}` on iOS).
278+
/// Special paths like `:memory:` are passed through unchanged.
278279
fn resolve_database_path<R: Runtime>(path: &str, app: &AppHandle<R>) -> Result<PathBuf, Error> {
279280
let app_path = app
280281
.path()

0 commit comments

Comments
 (0)