From cc23f9cdd76fb926cb3853245cf16cfe9f91fb5e Mon Sep 17 00:00:00 2001 From: gwenn Date: Sun, 20 Nov 2022 18:07:17 +0100 Subject: [PATCH 01/28] Captured identifiers in SQL strings Initial draft --- Cargo.toml | 5 +++ rusqlite-macros/Cargo.toml | 16 +++++++ rusqlite-macros/src/lib.rs | 81 +++++++++++++++++++++++++++++++++++ rusqlite-macros/tests/test.rs | 22 ++++++++++ 4 files changed, 124 insertions(+) create mode 100644 rusqlite-macros/Cargo.toml create mode 100644 rusqlite-macros/src/lib.rs create mode 100644 rusqlite-macros/tests/test.rs diff --git a/Cargo.toml b/Cargo.toml index e813e47..6b609b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -139,6 +139,11 @@ bencher = "0.1" path = "libsqlite3-sys" version = "0.25.0" +# FIXME optional +[dependencies.rusqlite-macros] +path = "rusqlite-macros" +version = "0.1.0" + [[test]] name = "config_log" harness = false diff --git a/rusqlite-macros/Cargo.toml b/rusqlite-macros/Cargo.toml new file mode 100644 index 0000000..af1c040 --- /dev/null +++ b/rusqlite-macros/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "rusqlite-macros" +version = "0.1.0" +authors = ["The rusqlite developers"] +edition = "2021" +description = "Private implementation detail of rusqlite crate" +repository = "https://github.com/rusqlite/rusqlite" +license = "MIT" +categories = ["database"] + +[lib] +proc-macro = true + +[dependencies] +sqlite3-parser = { version = "0.4.0", default-features = false, features = ["YYNOERRORRECOVERY"] } +fallible-iterator = "0.2" diff --git a/rusqlite-macros/src/lib.rs b/rusqlite-macros/src/lib.rs new file mode 100644 index 0000000..a09694e --- /dev/null +++ b/rusqlite-macros/src/lib.rs @@ -0,0 +1,81 @@ +//! Private implementation details of `rusqlite`. + +use proc_macro::{Delimiter, Literal, TokenStream, TokenTree}; + +use fallible_iterator::FallibleIterator; +use sqlite3_parser::lexer::sql::Parser; +use sqlite3_parser::ast::{ParameterInfo, ToTokens}; + +// https://internals.rust-lang.org/t/custom-error-diagnostics-with-procedural-macros-on-almost-stable-rust/8113 + +#[doc(hidden)] +#[proc_macro] +pub fn __bind(input: TokenStream) -> TokenStream { + try_bind(input).unwrap_or_else(|msg| parse_ts(&format!("compile_error!({:?})", msg))) +} + +type Result = std::result::Result; + +fn try_bind(input: TokenStream) -> Result { + //eprintln!("INPUT: {:#?}", input); + let (stmt, literal) = { + let mut iter = input.into_iter(); + let stmt = iter.next().unwrap(); + let _punct = iter.next().unwrap(); + let literal = iter.next().unwrap(); + assert!(iter.next().is_none()); + (stmt, literal) + }; + + let literal = match into_literal(&literal) { + Some(it) => it, + None => return Err("expected a plain string literal".to_string()), + }; + let sql = literal.to_string(); + if !sql.starts_with('"') { + return Err("expected a plain string literal".to_string()); + } + let sql = strip_matches(&sql, "\""); + //eprintln!("SQL: {}", sql); + + let mut parser = Parser::new(sql.as_bytes()); + let ast = match parser.next() { + Ok(None) => return Err("Invalid input".to_owned()), + Err(err) => { + return Err(err.to_string()); + } + Ok(Some(ast)) => ast + }; + let mut info = ParameterInfo::default(); + if let Err(err) = ast.to_tokens(&mut info) { + return Err(err.to_string()); + } + //eprintln!("ParameterInfo.count: {:#?}", info.count); + //eprintln!("ParameterInfo.names: {:#?}", info.names); + + let mut res = TokenStream::new(); + Ok(res) +} + + +fn into_literal(ts: &TokenTree) -> Option { + match ts { + TokenTree::Literal(l) => Some(l.clone()), + TokenTree::Group(g) => match g.delimiter() { + Delimiter::None => match g.stream().into_iter().collect::>().as_slice() { + [TokenTree::Literal(l)] => Some(l.clone()), + _ => None, + }, + Delimiter::Parenthesis | Delimiter::Brace | Delimiter::Bracket => None, + }, + _ => None, + } +} + +fn strip_matches<'a>(s: &'a str, pattern: &str) -> &'a str { + s.strip_prefix(pattern).unwrap_or(s).strip_suffix(pattern).unwrap_or(s) +} + +fn parse_ts(s: &str) -> TokenStream { + s.parse().unwrap() +} diff --git a/rusqlite-macros/tests/test.rs b/rusqlite-macros/tests/test.rs new file mode 100644 index 0000000..e67100f --- /dev/null +++ b/rusqlite-macros/tests/test.rs @@ -0,0 +1,22 @@ +use rusqlite_macros::__bind; + +#[test] +fn test_literal() { + let stmt = (); + __bind!(stmt, "SELECT $name"); +} + +/* FIXME +#[test] +fn test_raw_string() { + let stmt = (); + __bind!((), r#"SELECT 1"#); +} + +#[test] +fn test_const() { + const SQL: &str = "SELECT 1"; + let stmt = (); + __bind!((), SQL); +} +*/ \ No newline at end of file From 5b20201423ed4578c0c1ede25617d3b369a9a88a Mon Sep 17 00:00:00 2001 From: gwenn Date: Sun, 4 Dec 2022 11:25:01 +0100 Subject: [PATCH 02/28] Captured identifiers in SQL strings Use `raw_bind_parameter` --- rusqlite-macros/Cargo.toml | 2 +- rusqlite-macros/src/lib.rs | 28 +++++++++++++++++++++++----- rusqlite-macros/tests/test.rs | 22 ++++++++++++++++++---- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/rusqlite-macros/Cargo.toml b/rusqlite-macros/Cargo.toml index af1c040..7dae87a 100644 --- a/rusqlite-macros/Cargo.toml +++ b/rusqlite-macros/Cargo.toml @@ -12,5 +12,5 @@ categories = ["database"] proc-macro = true [dependencies] -sqlite3-parser = { version = "0.4.0", default-features = false, features = ["YYNOERRORRECOVERY"] } +sqlite3-parser = { version = "0.5.0", default-features = false, features = ["YYNOERRORRECOVERY"] } fallible-iterator = "0.2" diff --git a/rusqlite-macros/src/lib.rs b/rusqlite-macros/src/lib.rs index a09694e..cf41258 100644 --- a/rusqlite-macros/src/lib.rs +++ b/rusqlite-macros/src/lib.rs @@ -3,8 +3,8 @@ use proc_macro::{Delimiter, Literal, TokenStream, TokenTree}; use fallible_iterator::FallibleIterator; -use sqlite3_parser::lexer::sql::Parser; use sqlite3_parser::ast::{ParameterInfo, ToTokens}; +use sqlite3_parser::lexer::sql::Parser; // https://internals.rust-lang.org/t/custom-error-diagnostics-with-procedural-macros-on-almost-stable-rust/8113 @@ -19,7 +19,7 @@ type Result = std::result::Result; fn try_bind(input: TokenStream) -> Result { //eprintln!("INPUT: {:#?}", input); let (stmt, literal) = { - let mut iter = input.into_iter(); + let mut iter = input.clone().into_iter(); let stmt = iter.next().unwrap(); let _punct = iter.next().unwrap(); let literal = iter.next().unwrap(); @@ -44,20 +44,35 @@ fn try_bind(input: TokenStream) -> Result { Err(err) => { return Err(err.to_string()); } - Ok(Some(ast)) => ast + Ok(Some(ast)) => ast, }; let mut info = ParameterInfo::default(); if let Err(err) = ast.to_tokens(&mut info) { return Err(err.to_string()); } + if info.count == 0 { + return Ok(input); + } //eprintln!("ParameterInfo.count: {:#?}", info.count); //eprintln!("ParameterInfo.names: {:#?}", info.names); + if info.count as usize != info.names.len() { + return Err("Mixing named and numbered parameters is not supported.".to_string()); + } let mut res = TokenStream::new(); + for (i, name) in info.names.iter().enumerate() { + //eprintln!("(i: {}, name: {})", i + 1, &name[1..]); + res.extend(Some(stmt.clone())); + res.extend(parse_ts(&format!( + ".raw_bind_parameter({}, &{})?;", + i + 1, + &name[1..] + ))); + } + Ok(res) } - fn into_literal(ts: &TokenTree) -> Option { match ts { TokenTree::Literal(l) => Some(l.clone()), @@ -73,7 +88,10 @@ fn into_literal(ts: &TokenTree) -> Option { } fn strip_matches<'a>(s: &'a str, pattern: &str) -> &'a str { - s.strip_prefix(pattern).unwrap_or(s).strip_suffix(pattern).unwrap_or(s) + s.strip_prefix(pattern) + .unwrap_or(s) + .strip_suffix(pattern) + .unwrap_or(s) } fn parse_ts(s: &str) -> TokenStream { diff --git a/rusqlite-macros/tests/test.rs b/rusqlite-macros/tests/test.rs index e67100f..7a97ae4 100644 --- a/rusqlite-macros/tests/test.rs +++ b/rusqlite-macros/tests/test.rs @@ -1,9 +1,23 @@ use rusqlite_macros::__bind; +type Result = std::result::Result<(), String>; + +struct Stmt; + +impl Stmt { + pub fn raw_bind_parameter(&mut self, one_based_col_index: usize, param: &str) -> Result { + let (..) = (one_based_col_index, param); + Ok(()) + } +} + #[test] -fn test_literal() { - let stmt = (); - __bind!(stmt, "SELECT $name"); +fn test_literal() -> Result { + let first_name = "El"; + let last_name = "Barto"; + let mut stmt = Stmt; + __bind!(stmt, "SELECT $first_name, $last_name"); + Ok(()) } /* FIXME @@ -19,4 +33,4 @@ fn test_const() { let stmt = (); __bind!((), SQL); } -*/ \ No newline at end of file +*/ From 78b7c521054d57b95a3fec4d0531ebb54ffd22fc Mon Sep 17 00:00:00 2001 From: gwenn Date: Mon, 26 Dec 2022 20:00:59 +0100 Subject: [PATCH 03/28] Captured identifiers in SQL strings Introduce macro_rules `prepare_and_bind` and `prepare_cached_and_bind` --- src/lib.rs | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index da07327..f0ba603 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,8 @@ pub use crate::statement::{Statement, StatementStatus}; pub use crate::transaction::{DropBehavior, Savepoint, Transaction, TransactionBehavior}; pub use crate::types::ToSql; pub use crate::version::*; +#[doc(hidden)] +pub use rusqlite_macros::__bind; mod error; @@ -219,6 +221,30 @@ macro_rules! named_params { }; } +/// Captured identifiers in SQL +#[macro_export] +macro_rules! prepare_and_bind { + ($conn:expr, $sql:literal) => {{ + #[cfg(trick_rust_analyzer_into_highlighting_interpolated_bits)] + format_args!($sql); + let mut stmt = $conn.prepare($sql)?; + $crate::__bind!(stmt, $sql); + stmt + }}; +} + +/// Captured identifiers in SQL +#[macro_export] +macro_rules! prepare_cached_and_bind { + ($conn:expr, $sql:literal) => {{ + #[cfg(trick_rust_analyzer_into_highlighting_interpolated_bits)] + format_args!($sql); + let mut stmt = $conn.prepare_cached($sql)?; + $crate::__bind!(stmt, $sql); + stmt + }}; +} + /// A typedef of the result returned by many methods. pub type Result = result::Result; @@ -2088,9 +2114,20 @@ mod test { } #[test] - pub fn db_readonly() -> Result<()> { + fn db_readonly() -> Result<()> { let db = Connection::open_in_memory()?; assert!(!db.is_readonly(MAIN_DB)?); Ok(()) } + + #[test] + fn prepare_and_bind() -> Result<()> { + let db = Connection::open_in_memory()?; + let name = "Lisa"; + let age = 8; + let mut stmt = prepare_and_bind!(db, "SELECT $name, $age;"); + let (v1, v2) = stmt.raw_query().get_expected_row().and_then(|r| Ok((r.get::<_,String>(0)?, r.get::<_,i64>(1)?)))?; + assert_eq!((v1.as_str(), v2), (name, age)); + Ok(()) + } } From b59b0ddf2e3906716abe3756f09217d57d3589ee Mon Sep 17 00:00:00 2001 From: gwenn Date: Sun, 16 Apr 2023 16:17:36 +0200 Subject: [PATCH 04/28] Bump sqlite3-parser version --- libsqlite3-sys/build.rs | 1 + rusqlite-macros/Cargo.toml | 2 +- rusqlite-macros/src/lib.rs | 1 - rusqlite-macros/tests/test.rs | 6 +++--- src/lib.rs | 7 ++++--- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/libsqlite3-sys/build.rs b/libsqlite3-sys/build.rs index e3c065a..da785e4 100644 --- a/libsqlite3-sys/build.rs +++ b/libsqlite3-sys/build.rs @@ -497,6 +497,7 @@ mod bindings { None } } + fn item_name(&self, original_item_name: &str) -> Option { original_item_name .strip_prefix("sqlite3_index_info_") diff --git a/rusqlite-macros/Cargo.toml b/rusqlite-macros/Cargo.toml index 7dae87a..5c5db10 100644 --- a/rusqlite-macros/Cargo.toml +++ b/rusqlite-macros/Cargo.toml @@ -12,5 +12,5 @@ categories = ["database"] proc-macro = true [dependencies] -sqlite3-parser = { version = "0.5.0", default-features = false, features = ["YYNOERRORRECOVERY"] } +sqlite3-parser = { version = "0.7.0", default-features = false, features = ["YYNOERRORRECOVERY"] } fallible-iterator = "0.2" diff --git a/rusqlite-macros/src/lib.rs b/rusqlite-macros/src/lib.rs index cf41258..2369639 100644 --- a/rusqlite-macros/src/lib.rs +++ b/rusqlite-macros/src/lib.rs @@ -21,7 +21,6 @@ fn try_bind(input: TokenStream) -> Result { let (stmt, literal) = { let mut iter = input.clone().into_iter(); let stmt = iter.next().unwrap(); - let _punct = iter.next().unwrap(); let literal = iter.next().unwrap(); assert!(iter.next().is_none()); (stmt, literal) diff --git a/rusqlite-macros/tests/test.rs b/rusqlite-macros/tests/test.rs index 7a97ae4..785ca9b 100644 --- a/rusqlite-macros/tests/test.rs +++ b/rusqlite-macros/tests/test.rs @@ -16,7 +16,7 @@ fn test_literal() -> Result { let first_name = "El"; let last_name = "Barto"; let mut stmt = Stmt; - __bind!(stmt, "SELECT $first_name, $last_name"); + __bind!(stmt "SELECT $first_name, $last_name"); Ok(()) } @@ -24,13 +24,13 @@ fn test_literal() -> Result { #[test] fn test_raw_string() { let stmt = (); - __bind!((), r#"SELECT 1"#); + __bind!(stmt r#"SELECT 1"#); } #[test] fn test_const() { const SQL: &str = "SELECT 1"; let stmt = (); - __bind!((), SQL); + __bind!(stmt SQL); } */ diff --git a/src/lib.rs b/src/lib.rs index 7f95735..1b3b17e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -221,7 +221,7 @@ macro_rules! prepare_and_bind { #[cfg(trick_rust_analyzer_into_highlighting_interpolated_bits)] format_args!($sql); let mut stmt = $conn.prepare($sql)?; - $crate::__bind!(stmt, $sql); + $crate::__bind!(stmt $sql); stmt }}; } @@ -233,7 +233,7 @@ macro_rules! prepare_cached_and_bind { #[cfg(trick_rust_analyzer_into_highlighting_interpolated_bits)] format_args!($sql); let mut stmt = $conn.prepare_cached($sql)?; - $crate::__bind!(stmt, $sql); + $crate::__bind!(stmt $sql); stmt }}; } @@ -923,7 +923,8 @@ impl Connection { /// /// This function is unsafe because improper use may impact the Connection. /// In particular, it should only be called on connections created - /// and owned by the caller, e.g. as a result of calling ffi::sqlite3_open(). + /// and owned by the caller, e.g. as a result of calling + /// ffi::sqlite3_open(). #[inline] pub unsafe fn from_handle_owned(db: *mut ffi::sqlite3) -> Result { let db = InnerConnection::new(db, true); From 0e369ba878c4e5915049fdfed931c6ff56af876d Mon Sep 17 00:00:00 2001 From: gwenn Date: Sun, 16 Apr 2023 18:09:15 +0200 Subject: [PATCH 05/28] Misc --- src/busy.rs | 2 +- src/lib.rs | 6 +++++- src/vtab/vtablog.rs | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/busy.rs b/src/busy.rs index b9a5d40..18fa7e2 100644 --- a/src/busy.rs +++ b/src/busy.rs @@ -1,4 +1,4 @@ -///! Busy handler (when the database is locked) +//! Busy handler (when the database is locked) use std::convert::TryInto; use std::mem; use std::os::raw::{c_int, c_void}; diff --git a/src/lib.rs b/src/lib.rs index 1b3b17e..49ab436 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2120,7 +2120,11 @@ mod test { let name = "Lisa"; let age = 8; let mut stmt = prepare_and_bind!(db, "SELECT $name, $age;"); - let (v1, v2) = stmt.raw_query().get_expected_row().and_then(|r| Ok((r.get::<_,String>(0)?, r.get::<_,i64>(1)?)))?; + let (v1, v2) = stmt + .raw_query() + .next() + .and_then(|o| o.ok_or(Error::QueryReturnedNoRows)) + .and_then(|r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?)))?; assert_eq!((v1.as_str(), v2), (name, age)); Ok(()) } diff --git a/src/vtab/vtablog.rs b/src/vtab/vtablog.rs index 1b3e1b8..f7aa1b1 100644 --- a/src/vtab/vtablog.rs +++ b/src/vtab/vtablog.rs @@ -1,4 +1,4 @@ -///! Port of C [vtablog](http://www.sqlite.org/cgi/src/finfo?name=ext/misc/vtablog.c) +//! Port of C [vtablog](http://www.sqlite.org/cgi/src/finfo?name=ext/misc/vtablog.c) use std::default::Default; use std::marker::PhantomData; use std::os::raw::c_int; From 5848c8c14745d519b78523af9948f631f6478a51 Mon Sep 17 00:00:00 2001 From: gwenn Date: Sun, 4 Jun 2023 19:08:49 +0200 Subject: [PATCH 06/28] Draft of serialize API --- Cargo.toml | 3 ++ src/error.rs | 1 - src/lib.rs | 3 ++ src/serialize.rs | 131 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 src/serialize.rs diff --git a/Cargo.toml b/Cargo.toml index 5a6c1dc..e63c337 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,8 @@ column_decltype = [] wasm32-wasi-vfs = ["libsqlite3-sys/wasm32-wasi-vfs"] # Note: doesn't support 32-bit. winsqlite3 = ["libsqlite3-sys/winsqlite3"] +# 3.23.0 +serialize = ["modern_sqlite"] # Helper feature for enabling most non-build-related optional features # or dependencies (except `session`). This is useful for running tests / clippy @@ -109,6 +111,7 @@ modern-full = [ ] bundled-full = ["modern-full", "bundled"] +default = ["serialize"] [dependencies] time = { version = "0.3.0", features = ["formatting", "macros", "parsing"], optional = true } diff --git a/src/error.rs b/src/error.rs index 797a216..2e46374 100644 --- a/src/error.rs +++ b/src/error.rs @@ -393,7 +393,6 @@ impl Error { #[cold] pub fn error_from_sqlite_code(code: c_int, message: Option) -> Error { - // TODO sqlite3_error_offset // 3.38.0, #1130 Error::SqliteFailure(ffi::Error::new(code), message) } diff --git a/src/lib.rs b/src/lib.rs index 11e1ad4..d417e3a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -119,6 +119,9 @@ mod params; mod pragma; mod raw_statement; mod row; +#[cfg(feature = "serialize")] +#[cfg_attr(docsrs, doc(cfg(feature = "serialize")))] +pub mod serialize; #[cfg(feature = "session")] #[cfg_attr(docsrs, doc(cfg(feature = "session")))] pub mod session; diff --git a/src/serialize.rs b/src/serialize.rs new file mode 100644 index 0000000..6608595 --- /dev/null +++ b/src/serialize.rs @@ -0,0 +1,131 @@ +//! Serialize a database. +use std::convert::TryInto; +use std::marker::PhantomData; +use std::ops::Deref; +use std::ptr::NonNull; + +use crate::error::error_from_handle; +use crate::ffi; +use crate::{Connection, DatabaseName, Result}; + +/// Shared serialized database +pub struct SharedData<'conn> { + phantom: PhantomData<&'conn Connection>, + ptr: NonNull, + sz: usize, +} + +/// Owned serialized database +pub struct OwnedData { + ptr: NonNull, + sz: usize, +} + +/// Serialized database +pub enum Data<'conn> { + /// Shared serialized database + Shared(SharedData<'conn>), + /// Owned serialized database + Owned(OwnedData), +} + +impl<'conn> Deref for Data<'conn> { + type Target = [u8]; + + fn deref(&self) -> &[u8] { + let (ptr, sz) = match self { + Data::Owned(OwnedData { ptr, sz }) => (ptr.as_ptr(), *sz), + Data::Shared(SharedData { ptr, sz, .. }) => (ptr.as_ptr(), *sz), + }; + unsafe { std::slice::from_raw_parts(ptr, sz) } + } +} + +impl Drop for OwnedData { + fn drop(&mut self) { + unsafe { + ffi::sqlite3_free(self.ptr.as_ptr().cast()); + } + } +} + +impl Connection { + /// Serialize a database. + pub fn serialize<'conn>(&'conn self, schema: DatabaseName<'_>) -> Result> { + let schema = schema.as_cstring()?; + let mut sz = 0; + let mut ptr: *mut u8 = unsafe { + ffi::sqlite3_serialize( + self.handle(), + schema.as_ptr(), + &mut sz, + ffi::SQLITE_SERIALIZE_NOCOPY, + ) + }; + Ok(if ptr.is_null() { + ptr = unsafe { ffi::sqlite3_serialize(self.handle(), schema.as_ptr(), &mut sz, 0) }; + if ptr.is_null() { + return Err(unsafe { error_from_handle(self.handle(), ffi::SQLITE_NOMEM) }); + } + Data::Owned(OwnedData { + ptr: NonNull::new(ptr).unwrap(), + sz: sz.try_into().unwrap(), + }) + } else { + // shared buffer + Data::Shared(SharedData { + ptr: NonNull::new(ptr).unwrap(), + sz: sz.try_into().unwrap(), + phantom: PhantomData, + }) + }) + } + + /// Deserialize a database. + pub fn deserialize( + &mut self, + schema: DatabaseName<'_>, + data: Data<'_>, + read_only: bool, + ) -> Result<()> { + let schema = schema.as_cstring()?; + let (data, sz, flags) = match data { + Data::Owned(OwnedData { ptr, sz }) => ( + ptr.as_ptr(), // FIXME double-free => mem forget + sz.try_into().unwrap(), + if read_only { + ffi::SQLITE_DESERIALIZE_FREEONCLOSE | ffi::SQLITE_DESERIALIZE_READONLY + } else { + ffi::SQLITE_DESERIALIZE_FREEONCLOSE | ffi::SQLITE_DESERIALIZE_RESIZEABLE + }, + ), + Data::Shared(SharedData { ptr, sz, .. }) => ( + ptr.as_ptr(), // FIXME lifetime of ptr must be > lifetime self + sz.try_into().unwrap(), + if read_only { + ffi::SQLITE_DESERIALIZE_READONLY + } else { + 0 + }, + ), + }; + let rc = unsafe { + ffi::sqlite3_deserialize(self.handle(), schema.as_ptr(), data, sz, sz, flags) + }; + if rc != ffi::SQLITE_OK { + return Err(unsafe { error_from_handle(self.handle(), rc) }); + } + /* TODO + if let Some(mxSize) = mxSize { + unsafe { + ffi::sqlite3_file_control( + self.handle(), + schema.as_ptr(), + ffi::SQLITE_FCNTL_SIZE_LIMIT, + &mut mxSize, + ) + }; + }*/ + Ok(()) + } +} From 67d1e34eb4ec4a56e25022d5ce44c934a04e65d7 Mon Sep 17 00:00:00 2001 From: gwenn Date: Mon, 5 Jun 2023 19:56:23 +0200 Subject: [PATCH 07/28] Serialize and deserialize database --- Cargo.toml | 1 - src/serialize.rs | 91 ++++++++++++++++++++++++++++++++---------------- 2 files changed, 61 insertions(+), 31 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e63c337..9cdc6f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,7 +111,6 @@ modern-full = [ ] bundled-full = ["modern-full", "bundled"] -default = ["serialize"] [dependencies] time = { version = "0.3.0", features = ["formatting", "macros", "parsing"], optional = true } diff --git a/src/serialize.rs b/src/serialize.rs index 6608595..e482af4 100644 --- a/src/serialize.rs +++ b/src/serialize.rs @@ -8,7 +8,7 @@ use crate::error::error_from_handle; use crate::ffi; use crate::{Connection, DatabaseName, Result}; -/// Shared serialized database +/// Shared (SQLITE_SERIALIZE_NOCOPY) serialized database pub struct SharedData<'conn> { phantom: PhantomData<&'conn Connection>, ptr: NonNull, @@ -21,9 +21,31 @@ pub struct OwnedData { sz: usize, } +impl OwnedData { + /// SAFETY: Caller must be certain that `ptr` is allocated by + /// `sqlite3_malloc`. + pub unsafe fn from_raw_nonnull(ptr: NonNull, sz: usize) -> Self { + Self { ptr, sz } + } + + fn into_raw(self) -> (*mut u8, usize) { + let raw = (self.ptr.as_ptr(), self.sz); + std::mem::forget(self); + raw + } +} + +impl Drop for OwnedData { + fn drop(&mut self) { + unsafe { + ffi::sqlite3_free(self.ptr.as_ptr().cast()); + } + } +} + /// Serialized database pub enum Data<'conn> { - /// Shared serialized database + /// Shared (SQLITE_SERIALIZE_NOCOPY) serialized database Shared(SharedData<'conn>), /// Owned serialized database Owned(OwnedData), @@ -41,14 +63,6 @@ impl<'conn> Deref for Data<'conn> { } } -impl Drop for OwnedData { - fn drop(&mut self) { - unsafe { - ffi::sqlite3_free(self.ptr.as_ptr().cast()); - } - } -} - impl Connection { /// Serialize a database. pub fn serialize<'conn>(&'conn self, schema: DatabaseName<'_>) -> Result> { @@ -85,34 +99,22 @@ impl Connection { pub fn deserialize( &mut self, schema: DatabaseName<'_>, - data: Data<'_>, + data: OwnedData, read_only: bool, ) -> Result<()> { let schema = schema.as_cstring()?; - let (data, sz, flags) = match data { - Data::Owned(OwnedData { ptr, sz }) => ( - ptr.as_ptr(), // FIXME double-free => mem forget - sz.try_into().unwrap(), - if read_only { - ffi::SQLITE_DESERIALIZE_FREEONCLOSE | ffi::SQLITE_DESERIALIZE_READONLY - } else { - ffi::SQLITE_DESERIALIZE_FREEONCLOSE | ffi::SQLITE_DESERIALIZE_RESIZEABLE - }, - ), - Data::Shared(SharedData { ptr, sz, .. }) => ( - ptr.as_ptr(), // FIXME lifetime of ptr must be > lifetime self - sz.try_into().unwrap(), - if read_only { - ffi::SQLITE_DESERIALIZE_READONLY - } else { - 0 - }, - ), + let (data, sz) = data.into_raw(); + let sz = sz.try_into().unwrap(); + let flags = if read_only { + ffi::SQLITE_DESERIALIZE_FREEONCLOSE | ffi::SQLITE_DESERIALIZE_READONLY + } else { + ffi::SQLITE_DESERIALIZE_FREEONCLOSE | ffi::SQLITE_DESERIALIZE_RESIZEABLE }; let rc = unsafe { ffi::sqlite3_deserialize(self.handle(), schema.as_ptr(), data, sz, sz, flags) }; if rc != ffi::SQLITE_OK { + // TODO sqlite3_free(data) ? return Err(unsafe { error_from_handle(self.handle(), rc) }); } /* TODO @@ -129,3 +131,32 @@ impl Connection { Ok(()) } } + +#[cfg(test)] +mod test { + use super::*; + use crate::{Connection, DatabaseName, Result}; + + #[test] + fn serialize() -> Result<()> { + let db = Connection::open_in_memory()?; + db.execute_batch("CREATE TABLE x AS SELECT 'data'")?; + let data = db.serialize(DatabaseName::Main)?; + let Data::Owned(data) = data else { panic!("expected OwnedData")}; + assert!(data.sz > 0); + Ok(()) + } + + #[test] + fn deserialize() -> Result<()> { + let src = Connection::open_in_memory()?; + src.execute_batch("CREATE TABLE x AS SELECT 'data'")?; + let data = src.serialize(DatabaseName::Main)?; + let Data::Owned(data) = data else { panic!("expected OwnedData")}; + + let mut dst = Connection::open_in_memory()?; + dst.deserialize(DatabaseName::Main, data, false)?; + dst.execute("DELETE FROM x", [])?; + Ok(()) + } +} From f0670ccaddf3fa2fc0c8ba6a6c7e3c1b015f3908 Mon Sep 17 00:00:00 2001 From: gwenn Date: Sat, 10 Jun 2023 10:55:52 +0200 Subject: [PATCH 08/28] Fix macro hygiene issue --- rusqlite-macros/src/lib.rs | 34 ++++++++++++++++++++++++++++------ src/lib.rs | 25 +++++++++++++++++++++---- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/rusqlite-macros/src/lib.rs b/rusqlite-macros/src/lib.rs index 2369639..7bfda9d 100644 --- a/rusqlite-macros/src/lib.rs +++ b/rusqlite-macros/src/lib.rs @@ -1,6 +1,6 @@ //! Private implementation details of `rusqlite`. -use proc_macro::{Delimiter, Literal, TokenStream, TokenTree}; +use proc_macro::{Delimiter, Group, Literal, Span, TokenStream, TokenTree}; use fallible_iterator::FallibleIterator; use sqlite3_parser::ast::{ParameterInfo, ToTokens}; @@ -58,15 +58,19 @@ fn try_bind(input: TokenStream) -> Result { return Err("Mixing named and numbered parameters is not supported.".to_string()); } + let call_site = literal.span(); let mut res = TokenStream::new(); for (i, name) in info.names.iter().enumerate() { //eprintln!("(i: {}, name: {})", i + 1, &name[1..]); res.extend(Some(stmt.clone())); - res.extend(parse_ts(&format!( - ".raw_bind_parameter({}, &{})?;", - i + 1, - &name[1..] - ))); + res.extend(respan( + parse_ts(&format!( + ".raw_bind_parameter({}, &{})?;", + i + 1, + &name[1..] + )), + call_site, + )); } Ok(res) @@ -93,6 +97,24 @@ fn strip_matches<'a>(s: &'a str, pattern: &str) -> &'a str { .unwrap_or(s) } +fn respan(ts: TokenStream, span: Span) -> TokenStream { + let mut res = TokenStream::new(); + for tt in ts { + let tt = match tt { + TokenTree::Ident(mut ident) => { + ident.set_span(ident.span().resolved_at(span).located_at(span)); + TokenTree::Ident(ident) + } + TokenTree::Group(group) => { + TokenTree::Group(Group::new(group.delimiter(), respan(group.stream(), span))) + } + _ => tt, + }; + res.extend(Some(tt)) + } + res +} + fn parse_ts(s: &str) -> TokenStream { s.parse().unwrap() } diff --git a/src/lib.rs b/src/lib.rs index 49ab436..3f6f1c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -215,11 +215,26 @@ macro_rules! named_params { } /// Captured identifiers in SQL +/// +/// * only SQLite `$x` / `@x` / `:x` syntax works (Rust `&x` syntax does not +/// work). +/// * `$x.y` expression does not work. +/// +/// # Example +/// +/// ```rust, no_run +/// # use rusqlite::{prepare_and_bind, Connection, Result, Statement}; +/// +/// fn misc(db: &Connection) -> Result { +/// let name = "Lisa"; +/// let age = 8; +/// let smart = true; +/// Ok(prepare_and_bind!(db, "SELECT $name, @age, :smart;")) +/// } +/// ``` #[macro_export] macro_rules! prepare_and_bind { ($conn:expr, $sql:literal) => {{ - #[cfg(trick_rust_analyzer_into_highlighting_interpolated_bits)] - format_args!($sql); let mut stmt = $conn.prepare($sql)?; $crate::__bind!(stmt $sql); stmt @@ -227,11 +242,13 @@ macro_rules! prepare_and_bind { } /// Captured identifiers in SQL +/// +/// * only SQLite `$x` / `@x` / `:x` syntax works (Rust `&x` syntax does not +/// work). +/// * `$x.y` expression does not work. #[macro_export] macro_rules! prepare_cached_and_bind { ($conn:expr, $sql:literal) => {{ - #[cfg(trick_rust_analyzer_into_highlighting_interpolated_bits)] - format_args!($sql); let mut stmt = $conn.prepare_cached($sql)?; $crate::__bind!(stmt $sql); stmt From 759471172194cd9952f338ef3a6eaac636700972 Mon Sep 17 00:00:00 2001 From: gwenn Date: Sat, 10 Jun 2023 12:05:55 +0200 Subject: [PATCH 09/28] Make rusqlite-macros optional --- Cargo.toml | 7 ++----- rusqlite-macros/Cargo.toml | 4 ++-- rusqlite-macros/src/lib.rs | 5 ----- src/lib.rs | 5 +++++ 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8c163c9..eca3e34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,6 +122,7 @@ fallible-iterator = "0.2" fallible-streaming-iterator = "0.1" uuid = { version = "1.0", optional = true } smallvec = "1.6.1" +rusqlite-macros = { path = "rusqlite-macros", version = "0.1.0", optional = true } [dev-dependencies] doc-comment = "0.3" @@ -133,16 +134,12 @@ unicase = "2.6.0" # Use `bencher` over criterion because it builds much faster and we don't have # many benchmarks bencher = "0.1" +rusqlite-macros = { path = "rusqlite-macros", version = "0.1.0" } [dependencies.libsqlite3-sys] path = "libsqlite3-sys" version = "0.26.0" -# FIXME optional -[dependencies.rusqlite-macros] -path = "rusqlite-macros" -version = "0.1.0" - [[test]] name = "config_log" harness = false diff --git a/rusqlite-macros/Cargo.toml b/rusqlite-macros/Cargo.toml index 5c5db10..3d40775 100644 --- a/rusqlite-macros/Cargo.toml +++ b/rusqlite-macros/Cargo.toml @@ -12,5 +12,5 @@ categories = ["database"] proc-macro = true [dependencies] -sqlite3-parser = { version = "0.7.0", default-features = false, features = ["YYNOERRORRECOVERY"] } -fallible-iterator = "0.2" +sqlite3-parser = { version = "0.9", default-features = false, features = ["YYNOERRORRECOVERY"] } +fallible-iterator = "0.3" diff --git a/rusqlite-macros/src/lib.rs b/rusqlite-macros/src/lib.rs index 7bfda9d..8dda22c 100644 --- a/rusqlite-macros/src/lib.rs +++ b/rusqlite-macros/src/lib.rs @@ -17,7 +17,6 @@ pub fn __bind(input: TokenStream) -> TokenStream { type Result = std::result::Result; fn try_bind(input: TokenStream) -> Result { - //eprintln!("INPUT: {:#?}", input); let (stmt, literal) = { let mut iter = input.clone().into_iter(); let stmt = iter.next().unwrap(); @@ -35,7 +34,6 @@ fn try_bind(input: TokenStream) -> Result { return Err("expected a plain string literal".to_string()); } let sql = strip_matches(&sql, "\""); - //eprintln!("SQL: {}", sql); let mut parser = Parser::new(sql.as_bytes()); let ast = match parser.next() { @@ -52,8 +50,6 @@ fn try_bind(input: TokenStream) -> Result { if info.count == 0 { return Ok(input); } - //eprintln!("ParameterInfo.count: {:#?}", info.count); - //eprintln!("ParameterInfo.names: {:#?}", info.names); if info.count as usize != info.names.len() { return Err("Mixing named and numbered parameters is not supported.".to_string()); } @@ -61,7 +57,6 @@ fn try_bind(input: TokenStream) -> Result { let call_site = literal.span(); let mut res = TokenStream::new(); for (i, name) in info.names.iter().enumerate() { - //eprintln!("(i: {}, name: {})", i + 1, &name[1..]); res.extend(Some(stmt.clone())); res.extend(respan( parse_ts(&format!( diff --git a/src/lib.rs b/src/lib.rs index 3f6f1c4..12a5d31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,7 @@ pub use crate::statement::{Statement, StatementStatus}; pub use crate::transaction::{DropBehavior, Savepoint, Transaction, TransactionBehavior}; pub use crate::types::ToSql; pub use crate::version::*; +#[cfg(feature = "rusqlite-macros")] #[doc(hidden)] pub use rusqlite_macros::__bind; @@ -232,6 +233,8 @@ macro_rules! named_params { /// Ok(prepare_and_bind!(db, "SELECT $name, @age, :smart;")) /// } /// ``` +#[cfg(feature = "rusqlite-macros")] +#[cfg_attr(docsrs, doc(cfg(feature = "rusqlite-macros")))] #[macro_export] macro_rules! prepare_and_bind { ($conn:expr, $sql:literal) => {{ @@ -246,6 +249,8 @@ macro_rules! prepare_and_bind { /// * only SQLite `$x` / `@x` / `:x` syntax works (Rust `&x` syntax does not /// work). /// * `$x.y` expression does not work. +#[cfg(feature = "rusqlite-macros")] +#[cfg_attr(docsrs, doc(cfg(feature = "rusqlite-macros")))] #[macro_export] macro_rules! prepare_cached_and_bind { ($conn:expr, $sql:literal) => {{ From 048a442bc6886a9f561dd8058aea9b3ed738dab0 Mon Sep 17 00:00:00 2001 From: gwenn Date: Sat, 10 Jun 2023 12:14:41 +0200 Subject: [PATCH 10/28] Fix test build error --- Cargo.toml | 3 +-- src/lib.rs | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eca3e34..fba0ccd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,7 +134,6 @@ unicase = "2.6.0" # Use `bencher` over criterion because it builds much faster and we don't have # many benchmarks bencher = "0.1" -rusqlite-macros = { path = "rusqlite-macros", version = "0.1.0" } [dependencies.libsqlite3-sys] path = "libsqlite3-sys" @@ -159,7 +158,7 @@ name = "exec" harness = false [package.metadata.docs.rs] -features = ["modern-full"] +features = ["modern-full", "rusqlite-macros"] all-features = false no-default-features = true default-target = "x86_64-unknown-linux-gnu" diff --git a/src/lib.rs b/src/lib.rs index 12a5d31..b390a42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2137,6 +2137,7 @@ mod test { } #[test] + #[cfg(feature = "rusqlite-macros")] fn prepare_and_bind() -> Result<()> { let db = Connection::open_in_memory()?; let name = "Lisa"; From 2e62b031bf798c159c3f8917be5e4a46925be1e6 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Wed, 26 Jul 2023 19:59:51 -0400 Subject: [PATCH 11/28] Spelling and a few more nits * fix some simple spelling mistakes * a few other minor prof-reading nits --- .github/workflows/main.yml | 2 +- Changelog.md | 14 +++++++------- src/functions.rs | 6 +++--- src/lib.rs | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2cd3b8c..1354561 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -49,7 +49,7 @@ jobs: # The `{ sharedKey: ... }` allows different actions to share the cache. # We're using a `fullBuild` key mostly as a "this needs to do the # complete" that needs to do the complete build (that is, including - # `--features 'bundled-full session buildtime_bindgen`), which is very + # `--features 'bundled-full session buildtime_bindgen'`), which is very # slow, and has several deps. - uses: Swatinem/rust-cache@v2 with: { sharedKey: fullBuild } diff --git a/Changelog.md b/Changelog.md index 6ba11d7..e3e5442 100644 --- a/Changelog.md +++ b/Changelog.md @@ -15,7 +15,7 @@ For version 0.15.0 and above, see [Releases](https://github.com/rusqlite/rusqlit * Add DropBehavior::Panic to enforce intentional commit or rollback. * Implement `sqlite3_update_hook` (#260, #328), `sqlite3_commit_hook` and `sqlite3_rollback_hook`. * Add support to unlock notification behind `unlock_notify` feature (#294, #331). -* Make `Statement::column_index` case insensitive (#330). +* Make `Statement::column_index` case-insensitive (#330). * Add comment to justify `&mut Connection` in `Transaction`. * Fix `tyvar_behind_raw_pointer` warnings. * Fix handful of clippy warnings. @@ -29,7 +29,7 @@ For version 0.15.0 and above, see [Releases](https://github.com/rusqlite/rusqlit # Version 0.13.0 (2017-11-13) * Added ToSqlConversionFailure case to Error enum. -* Now depends on chrono 0.4, bitflats 1.0, and (optionally) cc 1.0 / bindgen 0.31. +* Now depends on chrono 0.4, bitflags 1.0, and (optionally) cc 1.0 / bindgen 0.31. * The ToSql/FromSql implementations for time::Timespec now include and expect fractional seconds and timezone in the serialized string. * The RowIndex type used in Row::get is now publicly exported. @@ -61,18 +61,18 @@ For version 0.15.0 and above, see [Releases](https://github.com/rusqlite/rusqlit * Adds `version()` and `version_number()` functions for querying the version of SQLite in use. * Adds the `limits` feature, exposing `limit()` and `set_limit()` methods on `Connection`. * Updates to `libsqlite3-sys` 0.7.0, which runs rust-bindgen at build-time instead of assuming the - precense of all expected SQLite constants and functions. + presence of all expected SQLite constants and functions. * Clarifies supported SQLite versions. Running with SQLite older than 3.6.8 now panics, and some features will not compile unless a sufficiently-recent SQLite version is used. See the README for requirements of particular features. * When running with SQLite 3.6.x, rusqlite attempts to perform SQLite initialization. If it fails, - rusqlite will panic since it cannot ensure the threading mode for SQLite. This check can by + rusqlite will panic since it cannot ensure the threading mode for SQLite. This check can be skipped by calling the unsafe function `rusqlite::bypass_sqlite_initialization()`. This is technically a breaking change but is unlikely to affect anyone in practice, since prior to this version the check that rusqlite was using would cause a segfault if linked against a SQLite older than 3.7.0. * rusqlite now performs a one-time check (prior to the first connection attempt) that the runtime - SQLite version is at least as new as the SQLite version found at buildtime. This check can by + SQLite version is at least as new as the SQLite version found at buildtime. This check can be skipped by calling the unsafe function `rusqlite::bypass_sqlite_version_check()`. * Removes the `libc` dependency in favor of using `std::os::raw` @@ -137,7 +137,7 @@ For version 0.15.0 and above, see [Releases](https://github.com/rusqlite/rusqlit This behavior is more correct. Previously there were runtime checks to prevent misuse, but other changes in this release to reset statements as soon as possible introduced yet another hazard related to the lack of these lifetime connections. We were already recommending the - use of `query_map` and `query_and_then` over raw `query`; both of theose still return handles + use of `query_map` and `query_and_then` over raw `query`; both of those still return handles that implement `Iterator`. * BREAKING CHANGE: `Transaction::savepoint()` now returns a `Savepoint` instead of another `Transaction`. Unlike `Transaction`, `Savepoint`s can be rolled back while keeping the current @@ -239,7 +239,7 @@ For version 0.15.0 and above, see [Releases](https://github.com/rusqlite/rusqlit * Add `column_names()` to `SqliteStatement`. * By default, include `SQLITE_OPEN_NO_MUTEX` and `SQLITE_OPEN_URI` flags when opening a - new conneciton. + new connection. * Fix generated bindings (e.g., `sqlite3_exec` was wrong). * Use now-generated `sqlite3_destructor_type` to define `SQLITE_STATIC` and `SQLITE_TRANSIENT`. diff --git a/src/functions.rs b/src/functions.rs index 41d9d1a..7d9eeb7 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -839,7 +839,7 @@ mod test { // This implementation of a regexp scalar function uses SQLite's auxiliary data // (https://www.sqlite.org/c3ref/get_auxdata.html) to avoid recompiling the regular // expression multiple times within one query. - fn regexp_with_auxilliary(ctx: &Context<'_>) -> Result { + fn regexp_with_auxiliary(ctx: &Context<'_>) -> Result { assert_eq!(ctx.len(), 2, "called with unexpected number of arguments"); type BoxError = Box; let regexp: std::sync::Arc = ctx @@ -860,7 +860,7 @@ mod test { } #[test] - fn test_function_regexp_with_auxilliary() -> Result<()> { + fn test_function_regexp_with_auxiliary() -> Result<()> { let db = Connection::open_in_memory()?; db.execute_batch( "BEGIN; @@ -874,7 +874,7 @@ mod test { "regexp", 2, FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC, - regexp_with_auxilliary, + regexp_with_auxiliary, )?; let result: bool = db.one_column("SELECT regexp('l.s[aeiouy]', 'lisa')")?; diff --git a/src/lib.rs b/src/lib.rs index a25b9f7..146e5da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1039,7 +1039,7 @@ impl<'conn> Iterator for Batch<'conn, '_> { bitflags::bitflags! { /// Flags for opening SQLite database connections. See - /// [sqlite3_open_v2](http://www.sqlite.org/c3ref/open.html) for details. + /// [sqlite3_open_v2](https://www.sqlite.org/c3ref/open.html) for details. /// /// The default open flags are `SQLITE_OPEN_READ_WRITE | SQLITE_OPEN_CREATE /// | SQLITE_OPEN_URI | SQLITE_OPEN_NO_MUTEX`. See [`Connection::open`] for From d05c976d523f91c0cf0c584d44c724650fc3f88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jan=20Czocha=C5=84ski?= Date: Thu, 19 Jan 2023 13:24:28 +0100 Subject: [PATCH 12/28] Implement support for more `time` types This PR implements support for the following types: * `time::Time` * `time::Date` * `time::PrimitiveDateTime` --- src/types/time.rs | 313 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 287 insertions(+), 26 deletions(-) diff --git a/src/types/time.rs b/src/types/time.rs index 03b2d61..c467f84 100644 --- a/src/types/time.rs +++ b/src/types/time.rs @@ -4,12 +4,10 @@ use crate::{Error, Result}; use time::format_description::well_known::Rfc3339; use time::format_description::FormatItem; use time::macros::format_description; -use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset}; +use time::{Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset}; const PRIMITIVE_SHORT_DATE_TIME_FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"); -const PRIMITIVE_DATE_TIME_FORMAT: &[FormatItem<'_>] = - format_description!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]"); const PRIMITIVE_DATE_TIME_Z_FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]Z"); const OFFSET_SHORT_DATE_TIME_FORMAT: &[FormatItem<'_>] = format_description!( @@ -22,6 +20,19 @@ const LEGACY_DATE_TIME_FORMAT: &[FormatItem<'_>] = format_description!( "[year]-[month]-[day] [hour]:[minute]:[second]:[subsecond] [offset_hour sign:mandatory]:[offset_minute]" ); +const PRIMITIVE_DATE_TIME_FORMAT: &[FormatItem<'_>] = + format_description!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]"); +const PRIMITIVE_SHORT_DATE_TIME_FORMAT_T: &[FormatItem<'_>] = + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"); +const PRIMITIVE_DATE_TIME_FORMAT_T: &[FormatItem<'_>] = + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond]"); + +const DATE_FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]"); +const TIME_FORMAT: &[FormatItem<'_>] = format_description!("[hour]:[minute]"); +const TIME_FORMAT_SECONDS: &[FormatItem<'_>] = format_description!("[hour]:[minute]:[second]"); +const TIME_FORMAT_SECONDS_SUBSECONDS: &[FormatItem<'_>] = + format_description!("[hour]:[minute]:[second].[subsecond]"); + impl ToSql for OffsetDateTime { #[inline] fn to_sql(&self) -> Result> { @@ -66,16 +77,117 @@ impl FromSql for OffsetDateTime { } } +/// ISO 8601 calendar date without timezone => "YYYY-MM-DD" +impl ToSql for Date { + #[inline] + fn to_sql(&self) -> Result> { + let date_str = self + .format(&DATE_FORMAT) + .map_err(|err| Error::ToSqlConversionFailure(err.into()))?; + Ok(ToSqlOutput::from(date_str)) + } +} + +/// "YYYY-MM-DD" => ISO 8601 calendar date without timezone. +impl FromSql for Date { + #[inline] + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + value.as_str().and_then(|s| { + Date::parse(s, &DATE_FORMAT).map_err(|err| FromSqlError::Other(err.into())) + }) + } +} + +/// ISO 8601 time without timezone => "HH:MM:SS.SSS" +impl ToSql for Time { + #[inline] + fn to_sql(&self) -> Result> { + let time_str = self + .format(&TIME_FORMAT_SECONDS_SUBSECONDS) + .map_err(|err| Error::ToSqlConversionFailure(err.into()))?; + Ok(ToSqlOutput::from(time_str)) + } +} + +/// "HH:MM"/"HH:MM:SS"/"HH:MM:SS.SSS" => ISO 8601 time without timezone. +impl FromSql for Time { + #[inline] + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + value.as_str().and_then(|s| { + let fmt = match s.len() { + 5 => Ok(&TIME_FORMAT), + 8 => Ok(&TIME_FORMAT_SECONDS), + len if len > 9 => Ok(&TIME_FORMAT_SECONDS_SUBSECONDS), + _ => Err(FromSqlError::Other( + format!("Unknown time format: {}", s).into(), + )), + }?; + + Time::parse(s, fmt).map_err(|err| FromSqlError::Other(err.into())) + }) + } +} + +/// ISO 8601 combined date and time without timezone => +/// "YYYY-MM-DD HH:MM:SS.SSS" +impl ToSql for PrimitiveDateTime { + #[inline] + fn to_sql(&self) -> Result> { + let date_time_str = self + .format(&PRIMITIVE_DATE_TIME_FORMAT) + .map_err(|err| Error::ToSqlConversionFailure(err.into()))?; + Ok(ToSqlOutput::from(date_time_str)) + } +} + +/// Parse a `PrimitiveDateTime` in one of the following formats: +/// YYYY-MM-DD HH:MM:SS.SSS +/// YYYY-MM-DDTHH:MM:SS.SSS +/// YYYY-MM-DD HH:MM:SS +/// YYYY-MM-DDTHH:MM:SS +impl FromSql for PrimitiveDateTime { + #[inline] + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + value.as_str().and_then(|s| { + let has_t = s.len() > 10 && s.as_bytes()[10] == b'T'; + + let fmt = match (s.len(), has_t) { + (19, true) => Ok(&PRIMITIVE_SHORT_DATE_TIME_FORMAT_T), + (19, false) => Ok(&PRIMITIVE_SHORT_DATE_TIME_FORMAT), + (l, true) if l > 19 => Ok(&PRIMITIVE_DATE_TIME_FORMAT_T), + (l, false) if l > 19 => Ok(&PRIMITIVE_DATE_TIME_FORMAT), + _ => Err(FromSqlError::Other( + format!("Unknown date format: {}", s).into(), + )), + }?; + + PrimitiveDateTime::parse(s, fmt).map_err(|err| FromSqlError::Other(err.into())) + }) + } +} + #[cfg(test)] mod test { + + use crate::types::time::{PRIMITIVE_DATE_TIME_FORMAT, PRIMITIVE_DATE_TIME_FORMAT_T}; + use crate::{Connection, Result}; + use time::format_description::well_known::Rfc3339; - use time::OffsetDateTime; + use time::macros::{date, time}; + use time::{Date, OffsetDateTime, PrimitiveDateTime, Time}; + + use super::{PRIMITIVE_SHORT_DATE_TIME_FORMAT, PRIMITIVE_SHORT_DATE_TIME_FORMAT_T}; + + fn checked_memory_handle() -> Result { + let db = Connection::open_in_memory()?; + db.execute_batch("CREATE TABLE foo (t TEXT, i INTEGER, f FLOAT, b BLOB)")?; + Ok(db) + } #[test] fn test_offset_date_time() -> Result<()> { - let db = Connection::open_in_memory()?; - db.execute_batch("CREATE TABLE foo (t TEXT, i INTEGER, f FLOAT)")?; + let db = checked_memory_handle()?; let mut ts_vec = vec![]; @@ -84,10 +196,10 @@ mod test { }; ts_vec.push(make_datetime(10_000, 0)); //January 1, 1970 2:46:40 AM - ts_vec.push(make_datetime(10_000, 1000)); //January 1, 1970 2:46:40 AM (and one microsecond) + // ts_vec.push(make_datetime(10_000, 1000)); //January 1, 1970 2:46:40 AM (and one microsecond) ts_vec.push(make_datetime(1_500_391_124, 1_000_000)); //July 18, 2017 ts_vec.push(make_datetime(2_000_000_000, 2_000_000)); //May 18, 2033 - ts_vec.push(make_datetime(3_000_000_000, 999_999_999)); //January 24, 2065 + ts_vec.push(make_datetime(3_000_000_000, 999)); //January 24, 2065 ts_vec.push(make_datetime(10_000_000_000, 0)); //November 20, 2286 for ts in ts_vec { @@ -103,47 +215,154 @@ mod test { } #[test] - fn test_string_values() -> Result<()> { - let db = Connection::open_in_memory()?; - for (s, t) in vec![ + fn test_offset_date_time_parsing() -> Result<()> { + let db = checked_memory_handle()?; + let tests = vec![ ( "2013-10-07 08:23:19", - Ok(OffsetDateTime::parse("2013-10-07T08:23:19Z", &Rfc3339).unwrap()), + OffsetDateTime::parse("2013-10-07T08:23:19Z", &Rfc3339).unwrap(), ), ( "2013-10-07 08:23:19Z", - Ok(OffsetDateTime::parse("2013-10-07T08:23:19Z", &Rfc3339).unwrap()), + OffsetDateTime::parse("2013-10-07T08:23:19Z", &Rfc3339).unwrap(), ), ( "2013-10-07T08:23:19Z", - Ok(OffsetDateTime::parse("2013-10-07T08:23:19Z", &Rfc3339).unwrap()), + OffsetDateTime::parse("2013-10-07T08:23:19Z", &Rfc3339).unwrap(), ), ( "2013-10-07 08:23:19.120", - Ok(OffsetDateTime::parse("2013-10-07T08:23:19.120Z", &Rfc3339).unwrap()), + OffsetDateTime::parse("2013-10-07T08:23:19.120Z", &Rfc3339).unwrap(), ), ( "2013-10-07 08:23:19.120Z", - Ok(OffsetDateTime::parse("2013-10-07T08:23:19.120Z", &Rfc3339).unwrap()), + OffsetDateTime::parse("2013-10-07T08:23:19.120Z", &Rfc3339).unwrap(), ), ( "2013-10-07T08:23:19.120Z", - Ok(OffsetDateTime::parse("2013-10-07T08:23:19.120Z", &Rfc3339).unwrap()), + OffsetDateTime::parse("2013-10-07T08:23:19.120Z", &Rfc3339).unwrap(), ), ( "2013-10-07 04:23:19-04:00", - Ok(OffsetDateTime::parse("2013-10-07T04:23:19-04:00", &Rfc3339).unwrap()), + OffsetDateTime::parse("2013-10-07T04:23:19-04:00", &Rfc3339).unwrap(), ), ( "2013-10-07 04:23:19.120-04:00", - Ok(OffsetDateTime::parse("2013-10-07T04:23:19.120-04:00", &Rfc3339).unwrap()), + OffsetDateTime::parse("2013-10-07T04:23:19.120-04:00", &Rfc3339).unwrap(), ), ( "2013-10-07T04:23:19.120-04:00", - Ok(OffsetDateTime::parse("2013-10-07T04:23:19.120-04:00", &Rfc3339).unwrap()), + OffsetDateTime::parse("2013-10-07T04:23:19.120-04:00", &Rfc3339).unwrap(), ), - ] { - let result: Result = db.query_row("SELECT ?1", [s], |r| r.get(0)); + ]; + + for (s, t) in tests { + let result: OffsetDateTime = db.query_row("SELECT ?1", [s], |r| r.get(0))?; + assert_eq!(result, t); + } + Ok(()) + } + + #[test] + fn test_date() -> Result<()> { + let db = checked_memory_handle()?; + let date = date!(2016 - 02 - 23); + db.execute("INSERT INTO foo (t) VALUES (?1)", [date])?; + + let s: String = db.one_column("SELECT t FROM foo")?; + assert_eq!("2016-02-23", s); + let t: Date = db.one_column("SELECT t FROM foo")?; + assert_eq!(date, t); + Ok(()) + } + + #[test] + fn test_time() -> Result<()> { + let db = checked_memory_handle()?; + let time = time!(23:56:04.00001); + db.execute("INSERT INTO foo (t) VALUES (?1)", [time])?; + + let s: String = db.one_column("SELECT t FROM foo")?; + assert_eq!("23:56:04.00001", s); + let v: Time = db.one_column("SELECT t FROM foo")?; + assert_eq!(time, v); + Ok(()) + } + + #[test] + fn test_primitive_date_time() -> Result<()> { + let db = checked_memory_handle()?; + let dt = date!(2016 - 02 - 23).with_time(time!(23:56:04)); + + db.execute("INSERT INTO foo (t) VALUES (?1)", [dt])?; + + let s: String = db.one_column("SELECT t FROM foo")?; + assert_eq!("2016-02-23 23:56:04.0", s); + let v: PrimitiveDateTime = db.one_column("SELECT t FROM foo")?; + assert_eq!(dt, v); + + db.execute("UPDATE foo set b = datetime(t)", [])?; // "YYYY-MM-DD HH:MM:SS" + let hms: PrimitiveDateTime = db.one_column("SELECT b FROM foo")?; + assert_eq!(dt, hms); + Ok(()) + } + + #[test] + fn test_date_parsing() -> Result<()> { + let db = checked_memory_handle()?; + let result: Date = db.query_row("SELECT ?1", ["2013-10-07"], |r| r.get(0))?; + assert_eq!(result, date!(2013 - 10 - 07)); + Ok(()) + } + + #[test] + fn test_time_parsing() -> Result<()> { + let db = checked_memory_handle()?; + let tests = vec![ + ("08:23", time!(08:23)), + ("08:23:19", time!(08:23:19)), + ("08:23:19.111", time!(08:23:19.111)), + ]; + + for (s, t) in tests { + let result: Time = db.query_row("SELECT ?1", [s], |r| r.get(0))?; + assert_eq!(result, t); + } + Ok(()) + } + + #[test] + fn test_primitive_date_time_parsing() -> Result<()> { + let db = checked_memory_handle()?; + + let tests = vec![ + ( + "2013-10-07T08:23:19", + PrimitiveDateTime::parse( + "2013-10-07T08:23:19", + &PRIMITIVE_SHORT_DATE_TIME_FORMAT_T, + ) + .unwrap(), + ), + ( + "2013-10-07T08:23:19.111", + PrimitiveDateTime::parse("2013-10-07T08:23:19.111", &PRIMITIVE_DATE_TIME_FORMAT_T) + .unwrap(), + ), + ( + "2013-10-07 08:23:19", + PrimitiveDateTime::parse("2013-10-07 08:23:19", &PRIMITIVE_SHORT_DATE_TIME_FORMAT) + .unwrap(), + ), + ( + "2013-10-07 08:23:19.111", + PrimitiveDateTime::parse("2013-10-07 08:23:19.111", &PRIMITIVE_DATE_TIME_FORMAT) + .unwrap(), + ), + ]; + + for (s, t) in tests { + let result: PrimitiveDateTime = db.query_row("SELECT ?1", [s], |r| r.get(0))?; assert_eq!(result, t); } Ok(()) @@ -151,15 +370,57 @@ mod test { #[test] fn test_sqlite_functions() -> Result<()> { - let db = Connection::open_in_memory()?; - let result: Result = db.one_column("SELECT CURRENT_TIMESTAMP"); + let db = checked_memory_handle()?; + db.one_column::