1use std::{
2 ops::{Deref, DerefMut},
3 time::{Duration, Instant},
4};
5
6use crate::sqlite_pool::{self, Committable, SqliteConn};
7use once_cell::sync::Lazy;
8use rusqlite::{
9 Connection, Error, Result, Transaction, functions::FunctionFlags, params,
10 trace::TraceEventCodes,
11};
12use serde_json::Value;
13use tempfile::TempDir;
14use tracing::{error, info, trace, warn};
15
16pub type SqlitePool = sqlite_pool::Pool<CrConn>;
17pub type SqlitePoolError = sqlite_pool::PoolError;
18
19const CRSQL_EXT_GENERIC_NAME: &str = "crsqlite";
20
21#[cfg(target_os = "macos")]
22pub const CRSQL_EXT_FILENAME: &str = "crsqlite.dylib";
23#[cfg(target_os = "linux")]
24pub const CRSQL_EXT_FILENAME: &str = "crsqlite.so";
25
26#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
27pub const CRSQL_EXT: &[u8] = include_bytes!("../crsqlite-darwin-aarch64.dylib");
28#[cfg(all(target_arch = "x86_64", target_os = "linux"))]
29pub const CRSQL_EXT: &[u8] = include_bytes!("../crsqlite-linux-x86_64.so");
30#[cfg(all(target_arch = "aarch64", target_os = "linux"))]
31pub const CRSQL_EXT: &[u8] = include_bytes!("../crsqlite-linux-aarch64.so");
32
33static CRSQL_EXT_DIR: Lazy<TempDir> = Lazy::new(|| {
37 let dir = TempDir::new().expect("could not create temp dir!");
38 std::fs::write(dir.path().join(CRSQL_EXT_GENERIC_NAME), CRSQL_EXT)
39 .expect("could not write crsql ext file");
40 dir
41});
42
43pub fn rusqlite_to_crsqlite_write(conn: rusqlite::Connection) -> rusqlite::Result<CrConn> {
44 let conn = rusqlite_to_crsqlite(conn)?;
45 conn.execute_batch("PRAGMA cache_size = -32000;")?;
46
47 Ok(conn)
48}
49
50pub fn rusqlite_to_crsqlite(mut conn: rusqlite::Connection) -> rusqlite::Result<CrConn> {
51 init_cr_conn(&mut conn)?;
52 setup_conn(&conn)?;
53 add_to_connection(&conn)?;
54
55 const SLOW_THRESHOLD: Duration = Duration::from_secs(1);
56 conn.trace_v2(
57 TraceEventCodes::SQLITE_TRACE_PROFILE,
58 Some(|event| {
59 if let rusqlite::trace::TraceEvent::Profile(stmt_ref, duration) = event
60 && duration >= SLOW_THRESHOLD
61 {
62 warn!("SLOW query {duration:?} => {}", stmt_ref.sql());
63 }
64 }),
65 );
66
67 Ok(CrConn(conn))
68}
69
70#[derive(Debug)]
71pub struct CrConn(Connection);
72
73impl CrConn {
74 pub fn init(mut conn: Connection) -> Result<Self, rusqlite::Error> {
75 init_cr_conn(&mut conn)?;
76 Ok(Self(conn))
77 }
78
79 pub fn immediate_transaction(&mut self) -> rusqlite::Result<Transaction<'_>> {
80 self.0
81 .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
82 }
83}
84
85impl SqliteConn for CrConn {
86 fn conn(&self) -> &rusqlite::Connection {
87 &self.0
88 }
89}
90
91impl Deref for CrConn {
92 type Target = Connection;
93
94 fn deref(&self) -> &Self::Target {
95 &self.0
96 }
97}
98
99impl DerefMut for CrConn {
100 fn deref_mut(&mut self) -> &mut Self::Target {
101 &mut self.0
102 }
103}
104
105impl Drop for CrConn {
106 fn drop(&mut self) {
107 if let Err(e) = self.execute_batch("select crsql_finalize();") {
108 error!("could not crsql_finalize: {e}");
109 }
110 }
111}
112
113impl Committable for CrConn {
114 fn commit(self) -> Result<(), rusqlite::Error> {
115 Ok(())
116 }
117
118 fn savepoint(&mut self) -> Result<rusqlite::Savepoint<'_>, rusqlite::Error> {
119 Err(rusqlite::Error::ModuleError(String::from(
120 "cannot create savepoint from connection",
121 )))
122 }
123}
124
125fn init_cr_conn(conn: &mut Connection) -> Result<(), rusqlite::Error> {
126 let ext_dir = &CRSQL_EXT_DIR;
127 trace!(
128 "loading crsqlite extension from path: {}",
129 ext_dir.path().display()
130 );
131 unsafe {
132 trace!("enabled loading extension");
133 conn.load_extension_enable()?;
134 conn.load_extension(
135 ext_dir.path().join(CRSQL_EXT_GENERIC_NAME),
136 Some("sqlite3_crsqlite_init"),
137 )?;
138 conn.load_extension_disable()?;
139 }
140 trace!("loaded crsqlite extension");
141
142 Ok(())
143}
144
145pub fn setup_conn(conn: &Connection) -> Result<(), rusqlite::Error> {
146 conn.execute_batch(
148 r#"
149 PRAGMA journal_mode = WAL;
150 PRAGMA journal_size_limit = 1073741824;
151 PRAGMA synchronous = NORMAL;
152 PRAGMA recursive_triggers = ON;
153 PRAGMA mmap_size = 8589934592; -- 8GB
154 "#,
155 )?;
156
157 rusqlite::vtab::series::load_module(conn)?;
158
159 Ok(())
160}
161
162pub trait Migration {
163 fn migrate(&self, tx: &Transaction) -> rusqlite::Result<()>;
164}
165
166impl<F> Migration for F
167where
168 F: Fn(&Transaction) -> rusqlite::Result<()>,
169{
170 fn migrate(&self, tx: &Transaction) -> rusqlite::Result<()> {
171 self(tx)
172 }
173}
174
175const SCHEMA_VERSION_KEY: &str = "schema_version";
176
177pub fn migration_version(tx: &Transaction) -> Option<usize> {
179 #[allow(deprecated)] tx.query_row(
181 "SELECT value FROM __corro_state WHERE key = ?",
182 [SCHEMA_VERSION_KEY],
183 |row| row.get::<_, i64>(0),
184 )
185 .map(|v| v as usize)
186 .ok()
187}
188
189pub fn set_migration_version(tx: &Transaction, v: usize) -> rusqlite::Result<usize> {
191 tx.execute(
192 "INSERT OR REPLACE INTO __corro_state VALUES (?, ?)",
193 params![SCHEMA_VERSION_KEY, v],
194 )
195}
196
197pub fn migrate(conn: &mut Connection, migrations: Vec<Box<dyn Migration>>) -> rusqlite::Result<()> {
199 let target_version = migrations.len();
200
201 let tx = conn.transaction()?;
202
203 let skip_n = migration_version(&tx).unwrap_or_default();
205
206 if skip_n > migrations.len() {
207 warn!(
208 "Skipping migrations, database is at migration version {skip_n} which is greater than {}",
209 migrations.len()
210 );
211 return Ok(());
212 }
213
214 for (i, migration) in migrations.into_iter().skip(skip_n).enumerate() {
215 let new_version = skip_n + i;
216 info!("Applying migration to v{new_version}");
217 let start = Instant::now();
218 migration.migrate(&tx)?;
219 info!("Applied v{new_version} in {:?}", start.elapsed());
220 }
221 set_migration_version(&tx, target_version)?;
222
223 tx.commit()?;
224 Ok(())
225}
226
227fn add_to_connection(db: &Connection) -> Result<()> {
229 add_corro_json_contains(db)?;
230
231 Ok(())
232}
233
234fn add_corro_json_contains(db: &Connection) -> Result<()> {
238 db.create_scalar_function(
239 "corro_json_contains",
240 2,
241 FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
242 move |ctx| {
243 assert_eq!(ctx.len(), 2, "called with unexpected number of arguments");
244
245 let selector: Value = serde_json::from_str(ctx.get_raw(0).as_str()?)
246 .map_err(|e| Error::UserFunctionError(e.into()))?;
247 let object: Value = serde_json::from_str(ctx.get_raw(1).as_str()?)
248 .map_err(|e| Error::UserFunctionError(e.into()))?;
249
250 Ok(corro_json_contains(selector, object))
251 },
252 )
253}
254
255fn corro_json_contains(selector: Value, object: Value) -> bool {
258 match (selector, object) {
259 (Value::Object(s_map), Value::Object(mut o_map)) => {
260 for (s_k, s_v) in s_map {
261 if let Some(o_v) = o_map.remove(&s_k) {
262 if !corro_json_contains(s_v, o_v) {
263 return false;
264 }
265 } else {
266 return false;
267 }
268 }
269
270 true
271 }
272 (s, o) => s == o,
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use crate::sqlite_pool::Config;
279 use crate::sqlite_pool::InterruptibleTransaction;
280 use futures::{TryStreamExt, stream::FuturesUnordered};
281 use rusqlite::{Connection, Result};
282 use tokio::task::block_in_place;
283
284 use super::*;
285
286 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
287 async fn concurrent_writes() -> Result<(), Box<dyn std::error::Error>> {
288 let tmpdir = tempfile::TempDir::new()?;
289
290 let pool = sqlite_pool::Config::new(tmpdir.path().join("test.db"))
291 .max_size(1)
292 .create_pool_transform(rusqlite_to_crsqlite)?;
293
294 {
295 let conn = pool.get().await?;
296
297 conn.execute_batch(
298 "
299 CREATE TABLE foo (a INTEGER NOT NULL PRIMARY KEY, b INTEGER);
300 SELECT crsql_as_crr('foo');
301 ",
302 )?;
303 }
304
305 let total: i64 = 1000;
306 let per_worker: i64 = 5;
307
308 let futs = FuturesUnordered::from_iter((0..total).map(|_| {
309 let pool = pool.clone();
310 async move {
311 tokio::spawn(async move {
312 let _: () = FuturesUnordered::from_iter((0..per_worker).map(|_| {
313 let pool = pool.clone();
314 async move {
315 let conn = pool.get().await?;
316 block_in_place(|| {
317 conn.prepare_cached(
318 "INSERT INTO foo (a, b) VALUES (random(), random())",
319 )?
320 .execute(())?;
321 Ok::<_, TestError>(())
322 })?;
323 Ok::<_, TestError>(())
324 }
325 }))
326 .try_collect()
327 .await?;
328 Ok::<_, TestError>(())
329 })
330 .await??;
331 Ok::<_, TestError>(())
332 }
333 }));
334
335 let _: () = futs.try_collect().await?;
336
337 let conn = pool.get().await?;
338
339 let count: i64 = conn.query_row("SELECT COUNT(*) FROM foo;", (), |row| row.get(0))?;
340
341 assert_eq!(count, total * per_worker);
342
343 Ok(())
344 }
345
346 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
347 async fn test_interruptible_transaction() -> Result<(), Box<dyn std::error::Error>> {
348 let tmpdir = tempfile::tempdir()?;
349
350 let path = tmpdir.path().join("db.sqlite");
351 let pool = Config::new(path)
352 .max_size(1)
353 .create_pool_transform(rusqlite_to_crsqlite)?;
354
355 let mut conn = pool.get().await.unwrap();
356 conn.execute_batch(
357 "CREATE TABLE IF NOT EXISTS testsbool (
358 id INTEGER NOT NULL PRIMARY KEY,
359 b boolean not null default false
360 ); SELECT crsql_as_crr('testsbool')",
361 )?;
362
363 {
364 let tx = conn.transaction()?;
365 let timeout = Some(tokio::time::Duration::from_millis(5));
366 let itx = InterruptibleTransaction::new(tx, timeout, "test_interruptible_transaction");
367 let res = itx.execute("INSERT INTO testsbool (id) WITH RECURSIVE cte(id) AS ( SELECT random() UNION ALL SELECT random() FROM cte LIMIT 100000000 ) SELECT id FROM cte;", &[]);
368
369 assert!(res.is_err_and(
370 |e| e.sqlite_error_code() == Some(rusqlite::ErrorCode::OperationInterrupted)
371 ));
372 }
373
374 let count: i64 = conn.query_row("SELECT COUNT(*) FROM testsbool;", (), |row| row.get(0))?;
375 assert_eq!(count, 0);
376 Ok(())
377 }
378
379 fn get_conn() -> Connection {
380 let conn = Connection::open_in_memory().expect("cannot open in-memory connection");
381 super::add_corro_json_contains(&conn)
382 .expect("cannot add corrosion functions to connection");
383
384 conn
385 }
386
387 fn query_corro_json_contains(conn: &Connection, obj1: &str, obj2: &str) -> Result<bool> {
388 conn.query_row("SELECT corro_json_contains(?1, ?2)", [obj1, obj2], |row| {
389 row.get(0)
390 })
391 }
392
393 #[test]
394 fn test_corro_json_contains() {
395 let conn = get_conn();
396
397 assert_eq!(Ok(true), query_corro_json_contains(&conn, "{}", "{}"));
399 assert_eq!(
401 Ok(true),
402 query_corro_json_contains(&conn, "{}", r#"{"key": "value"}"#)
403 );
404 assert_ne!(
406 Ok(true),
407 query_corro_json_contains(&conn, r#"{"key": "value"}"#, "{}")
408 );
409
410 assert_eq!(
412 Ok(true),
413 query_corro_json_contains(&conn, r#"{"key": "value"}"#, r#"{"key": "value"}"#)
414 );
415 assert_eq!(
417 Ok(true),
418 query_corro_json_contains(
419 &conn,
420 r#"{"key": "value"}"#,
421 r#"{"key": "value", "key2": "value2"}"#
422 )
423 );
424 assert_ne!(
426 Ok(true),
427 query_corro_json_contains(&conn, r#"{"key": "value"}"#, r#"{"key": "wrong value"}"#)
428 );
429
430 assert_eq!(
432 Ok(true),
433 query_corro_json_contains(
434 &conn,
435 r#"{"metadata": { "key": "value"} }"#,
436 r#"{"metadata": { "key": "value"} }"#
437 )
438 );
439 assert_ne!(
441 Ok(true),
442 query_corro_json_contains(
443 &conn,
444 r#"{"metadata": { "key": "value"} }"#,
445 r#"{"metadata": { "key": "wrong value"} }"#
446 )
447 );
448 }
449
450 #[derive(Debug, thiserror::Error)]
451 enum TestError {
452 #[error(transparent)]
453 Rusqlite(#[from] rusqlite::Error),
454 #[error(transparent)]
455 Bb8Rusqlite(#[from] SqlitePoolError),
456 #[error(transparent)]
457 Join(#[from] tokio::task::JoinError),
458 }
459}