├── Changelog.md ├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── update-docs.sh ├── README.md ├── LICENSE ├── Cargo.toml ├── src └── lib.rs └── tests └── test.rs /Changelog.md: -------------------------------------------------------------------------------- 1 | ## 0.15.0 2 | - bump up rusqlite to 0.22.0 3 | - expose rusqlite from this crate 4 | - update to edition 2018 5 | 6 | ## 0.14.0 7 | - Use `&mut Connection` trait. 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | 2 | version: 2 3 | updates: 4 | - package-ecosystem: "cargo" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | day: "friday" 9 | open-pull-requests-limit: 5 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.o 3 | *.so 4 | *.rlib 5 | *.dll 6 | #Generated by rustorm 7 | #/examples/gen/** 8 | /gen/** 9 | # Executables 10 | *.exe 11 | 12 | # Generated by Cargo 13 | /target*/ 14 | Cargo.lock 15 | 16 | #generated by eclipse 17 | /.project 18 | /.DS_Store 19 | 20 | #swap files 21 | *~ 22 | *.swp 23 | *.swo 24 | #cargo fmt backup files 25 | *.bk 26 | 27 | file.db 28 | -------------------------------------------------------------------------------- /update-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cargo clean 3 | cargo doc --no-deps 4 | cd target/doc 5 | git init 6 | git config user.name "Jovansonlee Cesar" 7 | git config user.email "ivanceras@gmail.com" 8 | git add . -A 9 | git commit -m "Committing docs to github pages" 10 | git remote add origin https://github.com/ivanceras/r2d2-sqlite 11 | git checkout -b gh-pages 12 | git push --force origin gh-pages 13 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose -F bundled 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | r2d2-sqlite 2 | ============= 3 | [![Latest Version](https://img.shields.io/crates/v/r2d2-sqlite.svg)](https://crates.io/crates/r2d2-sqlite) 4 | [![Build Status](https://github.com/ivanceras/r2d2-sqlite/actions/workflows/rust.yml/badge.svg)](https://github.com/ivanceras/r2d2-sqlite/actions/workflows/rust.yml) 5 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 6 | 7 | r2d2 connection pool for sqlite based on Steven Fackler's r2d2-postgres. 8 | 9 | 10 | [Documentation](https://docs.rs/r2d2_sqlite/) 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jovansonlee Cesar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "r2d2_sqlite" 3 | version = "0.31.0" 4 | authors = [ 5 | "Jovansonlee Cesar ", 6 | "Hugo Woesthuis ", 7 | "Jerebtw ", 8 | ] 9 | license = "MIT" 10 | description = "SQLite and SQLCipher support for the r2d2 connection pool" 11 | repository = "https://github.com/ivanceras/r2d2-sqlite" 12 | documentation = "https://docs.rs/r2d2_sqlite/" 13 | keywords = ["sqlite", "r2d2", "pool"] 14 | edition = "2018" 15 | 16 | [lib] 17 | name = "r2d2_sqlite" 18 | path = "src/lib.rs" 19 | test = false 20 | 21 | 22 | [[test]] 23 | name = "test" 24 | path = "tests/test.rs" 25 | 26 | [dependencies] 27 | r2d2 = "0.8" 28 | uuid = { version = "1.0", features = ["v4", "fast-rng"] } 29 | 30 | [dependencies.rusqlite] 31 | version = "0.37" 32 | 33 | [dev-dependencies] 34 | tempfile = "3" 35 | 36 | [dev-dependencies.rusqlite] 37 | version = "0.37" 38 | features = ["trace"] 39 | 40 | 41 | [features] 42 | bundled = ["rusqlite/bundled"] 43 | bundled-sqlcipher = ["rusqlite/bundled-sqlcipher"] 44 | bundled-sqlcipher-vendored-openssl = [ 45 | "rusqlite/bundled-sqlcipher-vendored-openssl", 46 | ] 47 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | //! # Sqlite support for the `r2d2` connection pool. 3 | //! 4 | //! Library crate: [r2d2-sqlite](https://crates.io/crates/r2d2-sqlite/) 5 | //! 6 | //! Integrated with: [r2d2](https://crates.io/crates/r2d2) 7 | //! and [rusqlite](https://crates.io/crates/rusqlite) 8 | //! 9 | //! ## Example 10 | //! 11 | //! ```rust,no_run 12 | //! extern crate r2d2; 13 | //! extern crate r2d2_sqlite; 14 | //! extern crate rusqlite; 15 | //! 16 | //! use std::thread; 17 | //! use r2d2_sqlite::SqliteConnectionManager; 18 | //! use rusqlite::params; 19 | //! 20 | //! fn main() { 21 | //! let manager = SqliteConnectionManager::file("file.db"); 22 | //! let pool = r2d2::Pool::new(manager).unwrap(); 23 | //! pool.get() 24 | //! .unwrap() 25 | //! .execute("CREATE TABLE IF NOT EXISTS foo (bar INTEGER)", params![]) 26 | //! .unwrap(); 27 | //! 28 | //! (0..10) 29 | //! .map(|i| { 30 | //! let pool = pool.clone(); 31 | //! thread::spawn(move || { 32 | //! let conn = pool.get().unwrap(); 33 | //! conn.execute("INSERT INTO foo (bar) VALUES (?)", &[&i]) 34 | //! .unwrap(); 35 | //! }) 36 | //! }) 37 | //! .collect::>() 38 | //! .into_iter() 39 | //! .map(thread::JoinHandle::join) 40 | //! .collect::>() 41 | //! .unwrap() 42 | //! } 43 | //! ``` 44 | pub use rusqlite; 45 | use rusqlite::{Connection, Error, OpenFlags}; 46 | use std::fmt; 47 | use std::path::{Path, PathBuf}; 48 | use std::sync::Mutex; 49 | use uuid::Uuid; 50 | 51 | #[derive(Debug)] 52 | enum Source { 53 | File(PathBuf), 54 | Memory(String), 55 | } 56 | 57 | type InitFn = dyn Fn(&mut Connection) -> Result<(), rusqlite::Error> + Send + Sync + 'static; 58 | 59 | /// An `r2d2::ManageConnection` for `rusqlite::Connection`s. 60 | pub struct SqliteConnectionManager { 61 | source: Source, 62 | flags: OpenFlags, 63 | init: Option>, 64 | _persist: Mutex>, 65 | } 66 | 67 | impl fmt::Debug for SqliteConnectionManager { 68 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 69 | let mut builder = f.debug_struct("SqliteConnectionManager"); 70 | let _ = builder.field("source", &self.source); 71 | let _ = builder.field("flags", &self.source); 72 | let _ = builder.field("init", &self.init.as_ref().map(|_| "InitFn")); 73 | builder.finish() 74 | } 75 | } 76 | 77 | impl SqliteConnectionManager { 78 | /// Creates a new `SqliteConnectionManager` from file. 79 | /// 80 | /// See `rusqlite::Connection::open` 81 | pub fn file>(path: P) -> Self { 82 | Self { 83 | source: Source::File(path.as_ref().to_path_buf()), 84 | flags: OpenFlags::default(), 85 | init: None, 86 | _persist: Mutex::new(None), 87 | } 88 | } 89 | 90 | /// Creates a new `SqliteConnectionManager` from memory. 91 | pub fn memory() -> Self { 92 | Self { 93 | source: Source::Memory(Uuid::new_v4().to_string()), 94 | flags: OpenFlags::default(), 95 | init: None, 96 | _persist: Mutex::new(None), 97 | } 98 | } 99 | 100 | /// Converts `SqliteConnectionManager` into one that sets OpenFlags upon 101 | /// connection creation. 102 | /// 103 | /// See `rustqlite::OpenFlags` for a list of available flags. 104 | pub fn with_flags(self, flags: OpenFlags) -> Self { 105 | Self { flags, ..self } 106 | } 107 | 108 | /// Converts `SqliteConnectionManager` into one that calls an initialization 109 | /// function upon connection creation. Could be used to set PRAGMAs, for 110 | /// example. 111 | /// 112 | /// ### Example 113 | /// 114 | /// Make a `SqliteConnectionManager` that sets the `foreign_keys` pragma to 115 | /// true for every connection. 116 | /// 117 | /// ```rust,no_run 118 | /// # use r2d2_sqlite::{SqliteConnectionManager}; 119 | /// let manager = SqliteConnectionManager::file("app.db") 120 | /// .with_init(|c| c.execute_batch("PRAGMA foreign_keys=1;")); 121 | /// ``` 122 | pub fn with_init(self, init: F) -> Self 123 | where 124 | F: Fn(&mut Connection) -> Result<(), rusqlite::Error> + Send + Sync + 'static, 125 | { 126 | let init: Option> = Some(Box::new(init)); 127 | Self { init, ..self } 128 | } 129 | } 130 | 131 | impl r2d2::ManageConnection for SqliteConnectionManager { 132 | type Connection = Connection; 133 | type Error = rusqlite::Error; 134 | 135 | fn connect(&self) -> Result { 136 | match self.source { 137 | Source::File(ref path) => Connection::open_with_flags(path, self.flags), 138 | Source::Memory(ref id) => { 139 | let connection = || { 140 | Connection::open_with_flags( 141 | format!("file:{}?mode=memory&cache=shared", id), 142 | self.flags, 143 | ) 144 | }; 145 | 146 | { 147 | let mut persist = self._persist.lock().unwrap(); 148 | if persist.is_none() { 149 | *persist = Some(connection()?); 150 | } 151 | } 152 | 153 | connection() 154 | } 155 | } 156 | .map_err(Into::into) 157 | .and_then(|mut c| match self.init { 158 | None => Ok(c), 159 | Some(ref init) => init(&mut c).map(|_| c), 160 | }) 161 | } 162 | 163 | fn is_valid(&self, conn: &mut Connection) -> Result<(), Error> { 164 | conn.execute_batch("SELECT 1;").map_err(Into::into) 165 | } 166 | 167 | fn has_broken(&self, _: &mut Connection) -> bool { 168 | false 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/test.rs: -------------------------------------------------------------------------------- 1 | extern crate r2d2; 2 | extern crate r2d2_sqlite; 3 | extern crate rusqlite; 4 | extern crate tempfile; 5 | 6 | use std::sync::mpsc; 7 | use std::thread; 8 | 9 | use r2d2::ManageConnection; 10 | use tempfile::TempDir; 11 | 12 | use r2d2_sqlite::SqliteConnectionManager; 13 | use rusqlite::Connection; 14 | 15 | #[test] 16 | fn test_basic() { 17 | let manager = SqliteConnectionManager::file("file.db"); 18 | let pool = r2d2::Pool::builder().max_size(2).build(manager).unwrap(); 19 | 20 | let (s1, r1) = mpsc::channel(); 21 | let (s2, r2) = mpsc::channel(); 22 | 23 | let pool1 = pool.clone(); 24 | let t1 = thread::spawn(move || { 25 | let conn = pool1.get().unwrap(); 26 | s1.send(()).unwrap(); 27 | r2.recv().unwrap(); 28 | drop(conn); 29 | }); 30 | 31 | let pool2 = pool.clone(); 32 | let t2 = thread::spawn(move || { 33 | let conn = pool2.get().unwrap(); 34 | s2.send(()).unwrap(); 35 | r1.recv().unwrap(); 36 | drop(conn); 37 | }); 38 | 39 | t1.join().unwrap(); 40 | t2.join().unwrap(); 41 | 42 | pool.get().unwrap(); 43 | } 44 | 45 | #[test] 46 | fn test_file() { 47 | let manager = SqliteConnectionManager::file("file.db"); 48 | let pool = r2d2::Pool::builder().max_size(2).build(manager).unwrap(); 49 | 50 | let (s1, r1) = mpsc::channel(); 51 | let (s2, r2) = mpsc::channel(); 52 | 53 | let pool1 = pool.clone(); 54 | let t1 = thread::spawn(move || { 55 | let conn = pool1.get().unwrap(); 56 | let _: &Connection = &*conn; 57 | s1.send(()).unwrap(); 58 | r2.recv().unwrap(); 59 | }); 60 | 61 | let pool2 = pool.clone(); 62 | let t2 = thread::spawn(move || { 63 | let conn = pool2.get().unwrap(); 64 | s2.send(()).unwrap(); 65 | r1.recv().unwrap(); 66 | drop(conn); 67 | }); 68 | 69 | t1.join().unwrap(); 70 | t2.join().unwrap(); 71 | 72 | pool.get().unwrap(); 73 | } 74 | 75 | #[test] 76 | fn test_is_valid() { 77 | let manager = SqliteConnectionManager::file("file.db"); 78 | let pool = r2d2::Pool::builder() 79 | .max_size(1) 80 | .test_on_check_out(true) 81 | .build(manager) 82 | .unwrap(); 83 | 84 | pool.get().unwrap(); 85 | } 86 | 87 | #[test] 88 | fn test_error_handling() { 89 | //! We specify a directory as a database. This is bound to fail. 90 | let dir = TempDir::new().expect("Could not create temporary directory"); 91 | let dirpath = dir.path().to_str().unwrap(); 92 | let manager = SqliteConnectionManager::file(dirpath); 93 | assert!(manager.connect().is_err()); 94 | } 95 | 96 | #[test] 97 | fn test_with_flags() { 98 | // Open db as read only and try to modify it, it should fail 99 | let manager = SqliteConnectionManager::file("file.db") 100 | .with_flags(rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY); 101 | let pool = r2d2::Pool::builder().max_size(2).build(manager).unwrap(); 102 | let conn = pool.get().unwrap(); 103 | let result = conn.execute_batch("CREATE TABLE hello(world)"); 104 | assert!(result.is_err()); 105 | } 106 | 107 | #[test] 108 | fn test_with_init() { 109 | fn trace_sql(sql: &str) { 110 | println!("{}", sql) 111 | } 112 | 113 | // Set user_version in init, then read it back to check that it was set 114 | let manager = SqliteConnectionManager::file("file.db").with_init(|c| { 115 | c.trace(Some(trace_sql)); 116 | c.execute_batch("PRAGMA user_version=123") 117 | }); 118 | let pool = r2d2::Pool::builder().max_size(2).build(manager).unwrap(); 119 | let conn = pool.get().unwrap(); 120 | let db_version = conn 121 | .query_row( 122 | "PRAGMA user_version", 123 | &[] as &[&dyn rusqlite::types::ToSql], 124 | |r| r.get::<_, i32>(0), 125 | ) 126 | .unwrap(); 127 | assert_eq!(db_version, 123); 128 | } 129 | 130 | #[test] 131 | fn test_in_memory_db_is_shared() { 132 | let manager = SqliteConnectionManager::memory(); 133 | let pool = r2d2::Pool::builder().max_size(10).build(manager).unwrap(); 134 | 135 | pool.get() 136 | .unwrap() 137 | .execute("CREATE TABLE IF NOT EXISTS foo (bar INTEGER)", []) 138 | .unwrap(); 139 | 140 | (0..10) 141 | .map(|i: i32| { 142 | let pool = pool.clone(); 143 | std::thread::spawn(move || { 144 | let conn = pool.get().unwrap(); 145 | conn.execute("INSERT INTO foo (bar) VALUES (?)", [i]) 146 | .unwrap(); 147 | }) 148 | }) 149 | .collect::>() 150 | .into_iter() 151 | .try_for_each(std::thread::JoinHandle::join) 152 | .unwrap(); 153 | 154 | let conn = pool.get().unwrap(); 155 | let mut stmt = conn.prepare("SELECT bar from foo").unwrap(); 156 | let mut rows: Vec = stmt 157 | .query_map([], |row| row.get(0)) 158 | .unwrap() 159 | .into_iter() 160 | .flatten() 161 | .collect(); 162 | rows.sort_unstable(); 163 | assert_eq!(rows, (0..10).collect::>()); 164 | } 165 | 166 | #[test] 167 | fn test_different_in_memory_dbs_are_not_shared() { 168 | let manager1 = SqliteConnectionManager::memory(); 169 | let pool1 = r2d2::Pool::new(manager1).unwrap(); 170 | let manager2 = SqliteConnectionManager::memory(); 171 | let pool2 = r2d2::Pool::new(manager2).unwrap(); 172 | 173 | pool1 174 | .get() 175 | .unwrap() 176 | .execute_batch("CREATE TABLE foo (bar INTEGER)") 177 | .unwrap(); 178 | let result = pool2 179 | .get() 180 | .unwrap() 181 | .execute_batch("CREATE TABLE foo (bar INTEGER)"); 182 | 183 | assert!(result.is_ok()); 184 | } 185 | 186 | #[test] 187 | fn test_in_memory_db_persists() { 188 | let manager = SqliteConnectionManager::memory(); 189 | 190 | { 191 | // Normally, `r2d2::Pool` won't drop connection unless timed-out or broken. 192 | // So let's drop managed connection instead. 193 | let conn = manager.connect().unwrap(); 194 | conn.execute_batch("CREATE TABLE foo (bar INTEGER)") 195 | .unwrap(); 196 | } 197 | 198 | let conn = manager.connect().unwrap(); 199 | let mut stmt = conn.prepare("SELECT * from foo").unwrap(); 200 | let result = stmt.execute([]); 201 | assert!(result.is_ok()); 202 | } 203 | --------------------------------------------------------------------------------