Skip to main content

turbosql/
lib_inner.rs

1use rusqlite::{Connection, OpenFlags};
2use serde::Deserialize;
3use std::cell::RefCell;
4use std::path::{Path, PathBuf};
5use std::sync::Mutex;
6
7// these re-exports are used in macro expansions
8
9#[doc(hidden)]
10pub use once_cell::sync::Lazy;
11#[doc(hidden)]
12pub use rusqlite::{
13	self, named_params, params, types::FromSql, types::FromSqlResult, types::ToSql,
14	types::ToSqlOutput, types::Value, types::ValueRef,
15};
16#[doc(hidden)]
17pub use serde::Serialize;
18#[doc(hidden)]
19pub use serde_json;
20pub use turbosql_impl::{execute, select, update, Turbosql};
21
22/// Wrapper for `Vec<u8>` that may one day impl `Read`, `Write` and `Seek` traits.
23pub type Blob = Vec<u8>;
24
25/// `#[derive(Turbosql)]` generates impls for this trait.
26pub trait Turbosql {
27	/// Insert this row into the database. `rowid` must be `None`. On success, the new `rowid` is returned.
28	fn insert(&self) -> Result<i64, Error>;
29	/// Insert this row into the database, and update the `rowid` of the struct to match the new rowid in the database. `rowid` must be `None` on call. On success, the new `rowid` is returned.
30	fn insert_mut(&mut self) -> Result<i64, Error>;
31	/// Insert all rows in the slice into the database. All `rowid`s must be `None`. On success, returns `Ok(())`.
32	fn insert_batch<T: AsRef<Self>>(rows: &[T]) -> Result<(), Error>;
33	/// Updates this existing row in the database, based on `rowid`, which must be `Some`. All fields are overwritten in the database. On success, returns the number of rows updated, which should be 1.
34	fn update(&self) -> Result<usize, Error>;
35	/// Updates all rows in the slice in the database, based on `rowid`, which must be `Some`. All fields are overwritten in the database. On success, returns `Ok(())`.
36	fn update_batch<T: AsRef<Self>>(rows: &[T]) -> Result<(), Error>;
37	/// Deletes this existing row in the database, based on `rowid`, which must be `Some`. On success, returns the number of rows deleted, which should be 1.
38	fn delete(&self) -> Result<usize, Error>;
39}
40
41/// Error type returned by Turbosql.
42#[derive(thiserror::Error, Debug)]
43pub enum Error {
44	/// Passthrough [`rusqlite::Error`]
45	#[error(transparent)]
46	Rusqlite(#[from] rusqlite::Error),
47	/// Passthrough [`serde_json::Error`]
48	#[error(transparent)]
49	SerdeJson(#[from] serde_json::Error),
50	/// Turbosql-specific error
51	#[error("Turbosql Error: {0}")]
52	OtherError(&'static str),
53}
54
55#[allow(dead_code)]
56#[derive(Clone, Debug, Deserialize, Default)]
57struct MigrationsToml {
58	migrations_append_only: Option<Vec<String>>,
59	output_generated_schema_for_your_information_do_not_edit: Option<String>,
60}
61
62#[derive(Clone, Debug, Default)]
63struct DbPath {
64	path: Option<PathBuf>,
65	opened: bool,
66}
67
68static __DB_PATH: Lazy<Mutex<DbPath>> = Lazy::new(Default::default);
69
70/// Convenience function that returns the current time as milliseconds since UNIX epoch.
71pub fn now_ms() -> i64 {
72	std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as i64
73}
74
75/// Convenience function that returns the current time as microseconds since UNIX epoch.
76pub fn now_µs() -> i64 {
77	std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_micros() as i64
78}
79
80/// Returns the path to the database.
81pub fn db_path() -> PathBuf {
82	__TURBOSQL_DB.with(|_| {});
83	__DB_PATH.lock().unwrap().path.clone().unwrap()
84}
85
86fn run_migrations(conn: &mut Connection, path: &Path) {
87	#[cfg(doc)]
88	// if these are what's run in doctests, could add a test struct here to scaffold one-liner tests
89	let toml_decoded: MigrationsToml = MigrationsToml::default();
90	#[cfg(all(not(doc), feature = "test"))]
91	let toml_decoded: MigrationsToml =
92		toml::from_str(include_str!("../../test.migrations.toml")).unwrap();
93	#[cfg(all(not(doc), not(feature = "test")))]
94	let toml_decoded: MigrationsToml =
95		toml::from_str(include_str!(concat!(env!("OUT_DIR"), "/migrations.toml")))
96			.expect("Unable to decode embedded migrations.toml");
97
98	let target_migrations = toml_decoded.migrations_append_only.unwrap_or_default();
99
100	// filter out comments
101	let target_migrations: Vec<_> =
102		target_migrations.into_iter().filter(|m| !m.starts_with("--")).collect();
103
104	conn.execute("BEGIN EXCLUSIVE TRANSACTION", params![]).unwrap();
105
106	let _ = conn.execute("ALTER TABLE turbosql_migrations RENAME TO _turbosql_migrations", params![]);
107
108	let result = conn.query_row(
109		"SELECT sql FROM sqlite_master WHERE name = ?",
110		params!["_turbosql_migrations"],
111		|row| {
112			let sql: String = row.get(0).unwrap();
113			Ok(sql)
114		},
115	);
116
117	match result {
118		Err(rusqlite::Error::QueryReturnedNoRows) => {
119			// no migrations table exists yet, create
120			conn
121				.execute_batch(if cfg!(feature = "sqlite-compat-no-strict-tables") {
122					r#"CREATE TABLE _turbosql_migrations (rowid INTEGER PRIMARY KEY, migration TEXT NOT NULL)"#
123				} else {
124					r#"CREATE TABLE _turbosql_migrations (rowid INTEGER PRIMARY KEY, migration TEXT NOT NULL) STRICT"#
125				})
126				.expect("CREATE TABLE _turbosql_migrations");
127		}
128		Err(err) => {
129			panic!("Could not query sqlite_master table: {}", err);
130		}
131		Ok(_) => (),
132	}
133
134	let applied_migrations = conn
135		.prepare("SELECT migration FROM _turbosql_migrations ORDER BY rowid")
136		.unwrap()
137		.query_map(params![], |row| Ok(row.get(0).unwrap()))
138		.unwrap()
139		.map(|x: Result<String, _>| x.unwrap())
140		.filter(|m| !m.starts_with("--"))
141		.collect::<Vec<String>>();
142
143	// execute migrations
144
145	let mut a = applied_migrations.iter();
146	let mut t = target_migrations.iter();
147
148	loop {
149		match (a.next(), t.next()) {
150			(Some(a), Some(t)) => {
151				if a != t {
152					panic!("Mismatch in Turbosql migrations! {:?} != {:?} {:?}", a, t, path)
153				}
154			}
155			(Some(a), None) => {
156				panic!(
157					"Mismatch in Turbosql migrations! More migrations are applied than target. {:?} {:?}",
158					a, path
159				)
160			}
161			(None, Some(t)) => {
162				if !t.starts_with("--") {
163					conn.execute(t, params![]).unwrap();
164				}
165				conn.execute("INSERT INTO _turbosql_migrations(migration) VALUES(?)", params![t]).unwrap();
166			}
167			(None, None) => break,
168		}
169	}
170
171	// TODO: verify schema against output_generated_schema_for_your_information_do_not_edit
172
173	//    if sql != create_sql {
174	//     println!("{}", sql);
175	//     println!("{}", create_sql);
176	//     panic!("Turbosql sqlite schema does not match! Delete database file to continue.");
177	//    }
178
179	conn.execute("COMMIT", params![]).unwrap();
180}
181
182/// Result of a [`checkpoint`].
183#[derive(Debug)]
184pub struct CheckpointResult {
185	/// Should always be 0. (Checkpoint is run in PASSIVE mode.)
186	pub busy: i64,
187	/// The number of modified pages that have been written to the write-ahead log file.
188	pub log: i64,
189	/// The number of pages in the write-ahead log file that have been successfully moved back into the database file at the conclusion of the checkpoint.
190	pub checkpointed: i64,
191}
192
193/// Checkpoint the DB.
194/// If no other threads have open connections, this will clean up the `-wal` and `-shm` files as well.
195pub fn checkpoint() -> Result<CheckpointResult, Error> {
196	let start = std::time::Instant::now();
197	__TURBOSQL_DB.with(|_| {});
198	let db_path = __DB_PATH.lock().unwrap();
199
200	let conn = Connection::open_with_flags(
201		db_path.path.as_ref().unwrap(),
202		OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX,
203	)?;
204
205	let result = conn.query_row("PRAGMA wal_checkpoint(PASSIVE)", params![], |row| {
206		Ok(CheckpointResult { busy: row.get(0)?, log: row.get(1)?, checkpointed: row.get(2)? })
207	})?;
208
209	log::info!("db checkpointed in {:?} {:#?}", start.elapsed(), result);
210
211	Ok(result)
212}
213
214fn open_db() -> Connection {
215	let mut db_path = __DB_PATH.lock().unwrap();
216
217	if db_path.path.is_none() {
218		#[cfg(not(feature = "test"))]
219		let path = {
220			let exe_stem = std::env::current_exe().unwrap().file_stem().unwrap().to_owned();
221			let exe_stem_lossy = exe_stem.to_string_lossy();
222
223			let path = directories_next::ProjectDirs::from("org", &exe_stem_lossy, &exe_stem_lossy)
224				.unwrap()
225				.data_dir()
226				.to_owned();
227
228			std::fs::create_dir_all(&path).unwrap();
229
230			path.join(exe_stem).with_extension("sqlite")
231		};
232
233		#[cfg(feature = "test")]
234		let path = Path::new(":memory:").to_owned();
235
236		db_path.path = Some(path);
237	}
238
239	log::debug!("opening db at {:?}", db_path.path.as_ref().unwrap());
240
241	// We are handling the mutex by being thread_local, so SQLite can be opened in no-mutex mode; see:
242	// https://www.mail-archive.com/sqlite-users@mailinglists.sqlite.org/msg112907.html
243
244	let mut conn = Connection::open_with_flags(
245		db_path.path.as_ref().unwrap(),
246		OpenFlags::SQLITE_OPEN_READ_WRITE
247			| OpenFlags::SQLITE_OPEN_CREATE
248			| OpenFlags::SQLITE_OPEN_NO_MUTEX,
249	)
250	.expect("rusqlite::Connection::open_with_flags");
251
252	conn
253		.execute_batch(
254			r#"
255				PRAGMA busy_timeout=3000;
256				PRAGMA auto_vacuum=INCREMENTAL;
257				PRAGMA journal_mode=WAL;
258				PRAGMA wal_autocheckpoint=8000;
259				PRAGMA synchronous=NORMAL;
260			"#,
261		)
262		.expect("Execute PRAGMAs");
263
264	if !db_path.opened {
265		run_migrations(&mut conn, db_path.path.as_ref().unwrap());
266		db_path.opened = true;
267	}
268
269	conn
270}
271
272thread_local! {
273	#[doc(hidden)]
274	pub static __TURBOSQL_DB: RefCell<Connection> = RefCell::new(open_db());
275}
276
277/// Set the local path and filename where Turbosql will store the underlying SQLite database.
278///
279/// Must be called before any usage of Turbosql macros or will return an error.
280pub fn set_db_path(path: &Path) -> Result<(), Error> {
281	let mut db_path = __DB_PATH.lock().unwrap();
282
283	if db_path.opened {
284		return Err(Error::OtherError("Trying to set path when DB is already opened"));
285	}
286
287	db_path.path = Some(path.to_owned());
288
289	Ok(())
290}