From 984456156ffb40cc47d6dd88c64ede41d479169a Mon Sep 17 00:00:00 2001 From: lovasoa Date: Sun, 28 Jan 2024 13:48:48 +0100 Subject: [PATCH 1/4] add support for custom sqlite functions --- sqlx-core/src/sqlite/connection/function.rs | 238 ++++++++++++++++++++ sqlx-core/src/sqlite/connection/mod.rs | 7 + sqlx-core/src/sqlite/mod.rs | 1 + sqlx-core/src/sqlite/options/connect.rs | 6 +- sqlx-core/src/sqlite/options/mod.rs | 32 +++ tests/sqlite/sqlite.db | Bin 36864 -> 36864 bytes tests/sqlite/sqlite.rs | 40 ++++ 7 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 sqlx-core/src/sqlite/connection/function.rs diff --git a/sqlx-core/src/sqlite/connection/function.rs b/sqlx-core/src/sqlite/connection/function.rs new file mode 100644 index 0000000000..e1bd35c52e --- /dev/null +++ b/sqlx-core/src/sqlite/connection/function.rs @@ -0,0 +1,238 @@ +use std::ffi::{c_char, CString}; +use std::os::raw::{c_int, c_void}; +use std::sync::Arc; + +use libsqlite3_sys::{ + sqlite3_context, sqlite3_create_function_v2, sqlite3_result_blob, sqlite3_result_double, + sqlite3_result_error, sqlite3_result_int, sqlite3_result_int64, sqlite3_result_null, + sqlite3_result_text, sqlite3_user_data, sqlite3_value, + sqlite3_value_type, SQLITE_DETERMINISTIC, SQLITE_DIRECTONLY, SQLITE_OK, + SQLITE_TRANSIENT, SQLITE_UTF8, +}; + +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::{BoxDynError, Error}; +use crate::sqlite::type_info::DataType; +use crate::sqlite::SqliteArgumentValue; +use crate::sqlite::SqliteTypeInfo; +use crate::sqlite::SqliteValue; +use crate::sqlite::Sqlite; +use crate::sqlite::{connection::handle::ConnectionHandle, SqliteError}; +use crate::value::Value; + +pub trait SqliteCallable: Send + Sync { + unsafe fn call_boxed_closure( + &self, + ctx: *mut sqlite3_context, + argc: c_int, + argv: *mut *mut sqlite3_value, + ); + // number of arguments + fn arg_count(&self) -> i32; +} + +pub struct SqliteFunctionCtx { + ctx: *mut sqlite3_context, + argument_values: Vec, +} + +impl SqliteFunctionCtx { + /// Creates a new `SqliteFunctionCtx` from the given raw SQLite function context. + /// The context is used to access the arguments passed to the function. + /// Safety: the context must be valid and argc must be the number of arguments passed to the function. + unsafe fn new(ctx: *mut sqlite3_context, argc: c_int, argv: *mut *mut sqlite3_value) -> Self { + let count = usize::try_from(argc).expect("invalid argument count"); + let argument_values = (0..count) + .map(|i| { + let raw = *argv.add(i); + let data_type_code = sqlite3_value_type(raw); + let value_type_info = SqliteTypeInfo(DataType::from_code(data_type_code)); + SqliteValue::new(raw, value_type_info) + }) + .collect::>(); + Self { + ctx, + argument_values, + } + } + + /// Returns the argument at the given index, or panics if the argument number is out of bounds or + /// the argument cannot be decoded as the requested type. + pub fn get_arg<'q, T: Decode<'q, Sqlite>>(&'q self, index: usize) -> T { + self.try_get_arg::(index) + .expect("invalid argument index") + } + + /// Returns the argument at the given index, or `None` if the argument number is out of bounds or + /// the argument cannot be decoded as the requested type. + pub fn try_get_arg<'q, T: Decode<'q, Sqlite>>(&'q self, index: usize) -> Result { + if let Some(value) = self.argument_values.get(index) { + let value_ref = value.as_ref(); + T::decode(value_ref) + } else { + Err("invalid argument index".into()) + } + } + + pub fn set_result<'q, R: Encode<'q, Sqlite>>(&self, result: R) { + unsafe { + let mut arg_buffer: Vec> = Vec::with_capacity(1); + if let IsNull::Yes = result.encode(&mut arg_buffer) { + sqlite3_result_null(self.ctx); + } else { + let arg = arg_buffer.pop().unwrap(); + match arg { + SqliteArgumentValue::Null => { + sqlite3_result_null(self.ctx); + } + SqliteArgumentValue::Text(text) => { + sqlite3_result_text( + self.ctx, + text.as_ptr() as *const c_char, + text.len() as c_int, + SQLITE_TRANSIENT(), + ); + } + SqliteArgumentValue::Blob(blob) => { + sqlite3_result_blob( + self.ctx, + blob.as_ptr() as *const c_void, + blob.len() as c_int, + SQLITE_TRANSIENT(), + ); + } + SqliteArgumentValue::Double(double) => { + sqlite3_result_double(self.ctx, double); + } + SqliteArgumentValue::Int(int) => { + sqlite3_result_int(self.ctx, int); + } + SqliteArgumentValue::Int64(int64) => { + sqlite3_result_int64(self.ctx, int64); + } + } + } + } + } + + pub fn set_error(&self, error_str: &str) { + let error_str = CString::new(error_str).expect("invalid error string"); + unsafe { + sqlite3_result_error( + self.ctx, + error_str.as_ptr(), + error_str.as_bytes().len() as c_int, + ); + } + } +} + +impl SqliteCallable for F { + unsafe fn call_boxed_closure( + &self, + ctx: *mut sqlite3_context, + argc: c_int, + argv: *mut *mut sqlite3_value, + ) { + let ctx = SqliteFunctionCtx::new(ctx, argc, argv); + (*self)(&ctx); + } + fn arg_count(&self) -> i32 { + -1 + } +} + +#[derive(Clone)] +pub struct Function { + name: CString, + func: Arc, + /// the function always returns the same result given the same inputs + pub deterministic: bool, + /// the function may only be invoked from top-level SQL, and cannot be used in VIEWs or TRIGGERs nor in schema structures such as CHECK constraints, DEFAULT clauses, expression indexes, partial indexes, or generated columns. + pub direct_only: bool, + call: + unsafe extern "C" fn(ctx: *mut sqlite3_context, argc: c_int, argv: *mut *mut sqlite3_value), +} + +impl std::fmt::Debug for Function { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Function") + .field("name", &self.name) + .field("deterministic", &self.deterministic) + .finish_non_exhaustive() + } +} + +impl Function { + pub fn new(name: N, func: F) -> Self + where + N: Into>, + F: SqliteCallable + Send + Sync + 'static, + { + Function { + name: CString::new(name).expect("invalid function name"), + func: Arc::new(func), + deterministic: false, + direct_only: false, + call: call_boxed_closure::, + } + } + + pub(crate) fn create(&self, handle: &mut ConnectionHandle) -> Result<(), Error> { + let raw_f = Arc::into_raw(Arc::clone(&self.func)); + let r = unsafe { + sqlite3_create_function_v2( + handle.as_ptr(), + self.name.as_ptr(), + self.func.arg_count(), // number of arguments + self.sqlite_flags(), + raw_f as *mut c_void, + Some(self.call), + None, // no step function for scalar functions + None, // no final function for scalar functions + None, // no need to free the function + ) + }; + + if r == SQLITE_OK { + Ok(()) + } else { + Err(Error::Database(Box::new(SqliteError::new(handle.as_ptr())))) + } + } + + fn sqlite_flags(&self) -> c_int { + let mut flags = SQLITE_UTF8; + if self.deterministic { + flags |= SQLITE_DETERMINISTIC; + } + if self.direct_only { + flags |= SQLITE_DIRECTONLY; + } + flags + } + + pub fn deterministic(mut self) -> Self { + self.deterministic = true; + self + } + + pub fn direct_only(mut self) -> Self { + self.direct_only = true; + self + } +} + +unsafe extern "C" fn call_boxed_closure( + ctx: *mut sqlite3_context, + argc: c_int, + argv: *mut *mut sqlite3_value, +) { + let data = sqlite3_user_data(ctx); + let boxed_f: *mut F = data as *mut F; + debug_assert!(!boxed_f.is_null()); + let expected_argc = (*boxed_f).arg_count(); + debug_assert!(expected_argc == -1 || argc == expected_argc); + (*boxed_f).call_boxed_closure(ctx, argc, argv); +} diff --git a/sqlx-core/src/sqlite/connection/mod.rs b/sqlx-core/src/sqlite/connection/mod.rs index 35e5f20012..d5d274b685 100644 --- a/sqlx-core/src/sqlite/connection/mod.rs +++ b/sqlx-core/src/sqlite/connection/mod.rs @@ -19,6 +19,7 @@ use crate::sqlite::{Sqlite, SqliteConnectOptions}; use crate::transaction::Transaction; pub(crate) mod collation; +pub(crate) mod function; pub(crate) mod describe; pub(crate) mod establish; pub(crate) mod execute; @@ -222,6 +223,12 @@ impl LockedSqliteHandle<'_> { ) -> Result<(), Error> { collation::create_collation(&mut self.guard.handle, name, compare) } + + /// Create a user-defined function. + /// See [`SqliteConnectOptions::create_function()`] for details. + pub fn create_function(&mut self, function: function::Function) -> Result<(), Error> { + function.create(&mut self.guard.handle) + } } impl Drop for ConnectionState { diff --git a/sqlx-core/src/sqlite/mod.rs b/sqlx-core/src/sqlite/mod.rs index d12c9e5f02..d786fdae2a 100644 --- a/sqlx-core/src/sqlite/mod.rs +++ b/sqlx-core/src/sqlite/mod.rs @@ -20,6 +20,7 @@ use std::sync::atomic::AtomicBool; pub use transaction::SqliteTransactionManager; pub use type_info::SqliteTypeInfo; pub use value::{SqliteValue, SqliteValueRef}; +pub use connection::function::{Function, SqliteFunctionCtx}; use crate::describe::Describe; use crate::error::Error; diff --git a/sqlx-core/src/sqlite/options/connect.rs b/sqlx-core/src/sqlite/options/connect.rs index 21a61a00ad..e6b46b7f4f 100644 --- a/sqlx-core/src/sqlite/options/connect.rs +++ b/sqlx-core/src/sqlite/options/connect.rs @@ -20,12 +20,16 @@ impl ConnectOptions for SqliteConnectOptions { // Execute PRAGMAs conn.execute(&*self.pragma_string()).await?; - if !self.collations.is_empty() { + if !self.collations.is_empty() || !self.functions.is_empty(){ let mut locked = conn.lock_handle().await?; for collation in &self.collations { collation.create(&mut locked.guard.handle)?; } + + for function in &self.functions { + function.create(&mut locked.guard.handle)?; + } } Ok(conn) diff --git a/sqlx-core/src/sqlite/options/mod.rs b/sqlx-core/src/sqlite/options/mod.rs index ddeafa4997..1d7b548719 100644 --- a/sqlx-core/src/sqlite/options/mod.rs +++ b/sqlx-core/src/sqlite/options/mod.rs @@ -18,6 +18,7 @@ pub use synchronous::SqliteSynchronous; use crate::common::DebugFn; use crate::sqlite::connection::collation::Collation; +use crate::sqlite::connection::function::Function; use indexmap::IndexMap; /// Options and flags which can be used to configure a SQLite connection. @@ -76,6 +77,7 @@ pub struct SqliteConnectOptions { pub(crate) row_channel_size: usize, pub(crate) collations: Vec, + pub(crate) functions: Vec, pub(crate) serialized: bool, pub(crate) thread_name: Arc String + Send + Sync + 'static>>, @@ -181,6 +183,7 @@ impl SqliteConnectOptions { pragmas, extensions: Default::default(), collations: Default::default(), + functions: Default::default(), serialized: false, thread_name: Arc::new(DebugFn(|id| format!("sqlx-sqlite-worker-{}", id))), command_channel_size: 50, @@ -342,6 +345,35 @@ impl SqliteConnectOptions { self } + /// Add a custom function for use in SQL statements. + /// If a function with the same name already exists, it will be replaced. + /// See [`sqlite3_create_function_v2()`](https://www.sqlite.org/c3ref/create_function.html) for details. + /// + /// ### Example + /// + /// #### Unicode handling + /// + /// By default, SQLite does not handle unicode in functions like `lower` or `upper`. + /// To prevent binary bloat, it advises application developers to implement their own + /// unicode-aware functions. + /// + /// This is how you would implement a unicode-aware `lower` function: + /// + /// ```rust + /// # use sqlx_core_oldapi::error::Error; + /// use std::str::FromStr; + /// use sqlx::sqlite::{SqliteConnectOptions, SqliteConnection}; + /// # fn options() -> Result { + /// let options = SqliteConnectOptions::from_str("sqlite://data.db")? + /// .function(Function::new("lower", |s: &str| s.to_lowercase()).deterministic()); + /// # Ok(options) + /// # } + /// + pub fn function(mut self, func: Function) -> Self { + self.functions.push(func); + self + } + /// Set to `true` to signal to SQLite that the database file is on read-only media. /// /// If enabled, SQLite assumes the database file _cannot_ be modified, even by higher diff --git a/tests/sqlite/sqlite.db b/tests/sqlite/sqlite.db index 130103331be365bf376274ff0c90c6c86c8025a5..f3b3713cb444a567e4313ada6c90f54477cb3cda 100644 GIT binary patch delta 638 zcmZY4zfZzo5XN!jZRq7c_BkLFH$m6ev)tiwhlCOd-a}5og2J zRnpDb)!otm!JX)17%-PyKEpScTy8Q?Pv+@Y%@TohX1W#U)Vo@!7xkpZ%2y>-P7yQ*fk+*MH@Bng?|9kJABEiy)aJ?Msl}7SQ}ZuH{~mAfHJhh8Pc@#ZJXLr~K9BPm5g5yvej4Y anyhow::Result<()> { + use sqlx_oldapi::sqlite::{Function, SqliteFunctionCtx}; + let mut conn = new::().await?; + { + let mut handle = conn.lock_handle().await?; + handle.create_function(Function::new("my_sum", |ctx: &SqliteFunctionCtx| { + ctx.set_result(ctx.get_arg::(0) + ctx.get_arg::(1)) + }))?; + } + + let row: SqliteRow = conn.fetch_one("SELECT my_sum(2,3)").await?; + let name: i32 = row.try_get(0)?; + assert_eq!(name, 5); + Ok(()) +} + +#[sqlx_macros::test] +async fn it_supports_upper_function() -> anyhow::Result<()> { + use sqlx_oldapi::sqlite::{Function, SqliteFunctionCtx}; + let mut conn = new::().await?; + { + let mut handle = conn.lock_handle().await?; + handle.create_function( + Function::new("upper", |ctx: &SqliteFunctionCtx| { + let original = ctx.get_arg::(0); + let uppercased = original.to_uppercase(); + ctx.set_result(uppercased) + }) + .deterministic() + .direct_only(), + )?; + } + + let row: SqliteRow = conn.fetch_one("SELECT upper('héhé')").await?; + let name: String = row.try_get(0)?; + assert_eq!(name, "HÉHÉ"); + Ok(()) +} + #[sqlx_macros::test] async fn it_caches_statements() -> anyhow::Result<()> { let mut conn = new::().await?; From 29e27778ca42df7cf48af74d8f18a21c53074bb3 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Sun, 28 Jan 2024 13:51:07 +0100 Subject: [PATCH 2/4] fmt --- sqlx-core/src/sqlite/connection/function.rs | 12 +++++++----- sqlx-core/src/sqlite/connection/mod.rs | 2 +- sqlx-core/src/sqlite/mod.rs | 2 +- sqlx-core/src/sqlite/options/connect.rs | 2 +- sqlx-core/src/sqlite/options/mod.rs | 12 ++++++------ 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/sqlx-core/src/sqlite/connection/function.rs b/sqlx-core/src/sqlite/connection/function.rs index e1bd35c52e..cc49adbe4b 100644 --- a/sqlx-core/src/sqlite/connection/function.rs +++ b/sqlx-core/src/sqlite/connection/function.rs @@ -5,19 +5,18 @@ use std::sync::Arc; use libsqlite3_sys::{ sqlite3_context, sqlite3_create_function_v2, sqlite3_result_blob, sqlite3_result_double, sqlite3_result_error, sqlite3_result_int, sqlite3_result_int64, sqlite3_result_null, - sqlite3_result_text, sqlite3_user_data, sqlite3_value, - sqlite3_value_type, SQLITE_DETERMINISTIC, SQLITE_DIRECTONLY, SQLITE_OK, - SQLITE_TRANSIENT, SQLITE_UTF8, + sqlite3_result_text, sqlite3_user_data, sqlite3_value, sqlite3_value_type, + SQLITE_DETERMINISTIC, SQLITE_DIRECTONLY, SQLITE_OK, SQLITE_TRANSIENT, SQLITE_UTF8, }; use crate::decode::Decode; use crate::encode::{Encode, IsNull}; use crate::error::{BoxDynError, Error}; use crate::sqlite::type_info::DataType; +use crate::sqlite::Sqlite; use crate::sqlite::SqliteArgumentValue; use crate::sqlite::SqliteTypeInfo; use crate::sqlite::SqliteValue; -use crate::sqlite::Sqlite; use crate::sqlite::{connection::handle::ConnectionHandle, SqliteError}; use crate::value::Value; @@ -66,7 +65,10 @@ impl SqliteFunctionCtx { /// Returns the argument at the given index, or `None` if the argument number is out of bounds or /// the argument cannot be decoded as the requested type. - pub fn try_get_arg<'q, T: Decode<'q, Sqlite>>(&'q self, index: usize) -> Result { + pub fn try_get_arg<'q, T: Decode<'q, Sqlite>>( + &'q self, + index: usize, + ) -> Result { if let Some(value) = self.argument_values.get(index) { let value_ref = value.as_ref(); T::decode(value_ref) diff --git a/sqlx-core/src/sqlite/connection/mod.rs b/sqlx-core/src/sqlite/connection/mod.rs index d5d274b685..9a43e3f9c2 100644 --- a/sqlx-core/src/sqlite/connection/mod.rs +++ b/sqlx-core/src/sqlite/connection/mod.rs @@ -19,12 +19,12 @@ use crate::sqlite::{Sqlite, SqliteConnectOptions}; use crate::transaction::Transaction; pub(crate) mod collation; -pub(crate) mod function; pub(crate) mod describe; pub(crate) mod establish; pub(crate) mod execute; mod executor; mod explain; +pub(crate) mod function; mod handle; mod worker; diff --git a/sqlx-core/src/sqlite/mod.rs b/sqlx-core/src/sqlite/mod.rs index d786fdae2a..e6349d9532 100644 --- a/sqlx-core/src/sqlite/mod.rs +++ b/sqlx-core/src/sqlite/mod.rs @@ -7,6 +7,7 @@ pub use arguments::{SqliteArgumentValue, SqliteArguments}; pub use column::SqliteColumn; +pub use connection::function::{Function, SqliteFunctionCtx}; pub use connection::{LockedSqliteHandle, SqliteConnection}; pub use database::Sqlite; pub use error::SqliteError; @@ -20,7 +21,6 @@ use std::sync::atomic::AtomicBool; pub use transaction::SqliteTransactionManager; pub use type_info::SqliteTypeInfo; pub use value::{SqliteValue, SqliteValueRef}; -pub use connection::function::{Function, SqliteFunctionCtx}; use crate::describe::Describe; use crate::error::Error; diff --git a/sqlx-core/src/sqlite/options/connect.rs b/sqlx-core/src/sqlite/options/connect.rs index e6b46b7f4f..26fb02c4cb 100644 --- a/sqlx-core/src/sqlite/options/connect.rs +++ b/sqlx-core/src/sqlite/options/connect.rs @@ -20,7 +20,7 @@ impl ConnectOptions for SqliteConnectOptions { // Execute PRAGMAs conn.execute(&*self.pragma_string()).await?; - if !self.collations.is_empty() || !self.functions.is_empty(){ + if !self.collations.is_empty() || !self.functions.is_empty() { let mut locked = conn.lock_handle().await?; for collation in &self.collations { diff --git a/sqlx-core/src/sqlite/options/mod.rs b/sqlx-core/src/sqlite/options/mod.rs index 1d7b548719..d79f58d55d 100644 --- a/sqlx-core/src/sqlite/options/mod.rs +++ b/sqlx-core/src/sqlite/options/mod.rs @@ -348,17 +348,17 @@ impl SqliteConnectOptions { /// Add a custom function for use in SQL statements. /// If a function with the same name already exists, it will be replaced. /// See [`sqlite3_create_function_v2()`](https://www.sqlite.org/c3ref/create_function.html) for details. - /// + /// /// ### Example - /// + /// /// #### Unicode handling - /// + /// /// By default, SQLite does not handle unicode in functions like `lower` or `upper`. /// To prevent binary bloat, it advises application developers to implement their own /// unicode-aware functions. - /// + /// /// This is how you would implement a unicode-aware `lower` function: - /// + /// /// ```rust /// # use sqlx_core_oldapi::error::Error; /// use std::str::FromStr; @@ -368,7 +368,7 @@ impl SqliteConnectOptions { /// .function(Function::new("lower", |s: &str| s.to_lowercase()).deterministic()); /// # Ok(options) /// # } - /// + /// pub fn function(mut self, func: Function) -> Self { self.functions.push(func); self From b8c9166de4479be50d4bb6b5c9749b164ab3cad7 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Sun, 28 Jan 2024 14:17:15 +0100 Subject: [PATCH 3/4] update doctests --- sqlx-core/src/sqlite/options/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sqlx-core/src/sqlite/options/mod.rs b/sqlx-core/src/sqlite/options/mod.rs index d79f58d55d..36d44cc117 100644 --- a/sqlx-core/src/sqlite/options/mod.rs +++ b/sqlx-core/src/sqlite/options/mod.rs @@ -362,10 +362,14 @@ impl SqliteConnectOptions { /// ```rust /// # use sqlx_core_oldapi::error::Error; /// use std::str::FromStr; - /// use sqlx::sqlite::{SqliteConnectOptions, SqliteConnection}; + /// use sqlx::sqlite::{SqliteConnectOptions, SqliteConnection, SqliteFunctionCtx}; /// # fn options() -> Result { /// let options = SqliteConnectOptions::from_str("sqlite://data.db")? - /// .function(Function::new("lower", |s: &str| s.to_lowercase()).deterministic()); + /// .function(Function::new("lower", |ctx: &SqliteFunctionCtx| { + /// let s = ctx.get_arg::(0); + /// let result = s.to_lowercase(); + /// ctx.set_result(result); + /// }).deterministic()); /// # Ok(options) /// # } /// From 05bb02a492931b964a5a3fff35d7b724c0774009 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Sun, 28 Jan 2024 16:45:35 +0100 Subject: [PATCH 4/4] fix doctest --- CHANGELOG.md | 5 +++++ sqlx-core/src/sqlite/options/mod.rs | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f523769da7..d5a40bbfeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.6.19 + + - Added support for user-defined sqlite functions + - Upgraded SQLite to [3.45.0](https://www.sqlite.org/releaselog/3_45_0.html) + ## 0.6.18 - Avoid systematically attaching a (potentially empty) arguments list to Query objects created with sqlx::query diff --git a/sqlx-core/src/sqlite/options/mod.rs b/sqlx-core/src/sqlite/options/mod.rs index 36d44cc117..602dfe42c7 100644 --- a/sqlx-core/src/sqlite/options/mod.rs +++ b/sqlx-core/src/sqlite/options/mod.rs @@ -362,7 +362,7 @@ impl SqliteConnectOptions { /// ```rust /// # use sqlx_core_oldapi::error::Error; /// use std::str::FromStr; - /// use sqlx::sqlite::{SqliteConnectOptions, SqliteConnection, SqliteFunctionCtx}; + /// use sqlx::sqlite::{SqliteConnectOptions, SqliteConnection, SqliteFunctionCtx, Function}; /// # fn options() -> Result { /// let options = SqliteConnectOptions::from_str("sqlite://data.db")? /// .function(Function::new("lower", |ctx: &SqliteFunctionCtx| { @@ -373,7 +373,7 @@ impl SqliteConnectOptions { /// # Ok(options) /// # } /// - pub fn function(mut self, func: Function) -> Self { + pub fn function(mut self, func: Function) -> Self { self.functions.push(func); self }