1use rusqlite::{Connection, OpenFlags};
2use serde::Deserialize;
3use std::cell::RefCell;
4use std::path::{Path, PathBuf};
5use std::sync::Mutex;
6
7#[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
22pub type Blob = Vec<u8>;
24
25pub trait Turbosql {
27 fn insert(&self) -> Result<i64, Error>;
29 fn insert_mut(&mut self) -> Result<i64, Error>;
31 fn insert_batch<T: AsRef<Self>>(rows: &[T]) -> Result<(), Error>;
33 fn update(&self) -> Result<usize, Error>;
35 fn update_batch<T: AsRef<Self>>(rows: &[T]) -> Result<(), Error>;
37 fn delete(&self) -> Result<usize, Error>;
39}
40
41#[derive(thiserror::Error, Debug)]
43pub enum Error {
44 #[error(transparent)]
46 Rusqlite(#[from] rusqlite::Error),
47 #[error(transparent)]
49 SerdeJson(#[from] serde_json::Error),
50 #[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
70pub fn now_ms() -> i64 {
72 std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as i64
73}
74
75pub fn now_µs() -> i64 {
77 std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_micros() as i64
78}
79
80pub 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 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 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 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 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 conn.execute("COMMIT", params![]).unwrap();
180}
181
182#[derive(Debug)]
184pub struct CheckpointResult {
185 pub busy: i64,
187 pub log: i64,
189 pub checkpointed: i64,
191}
192
193pub 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 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
277pub 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}