From 0130fe1479e16c537855ca62430cb7c95e6d5fd0 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Sat, 14 Mar 2020 14:57:26 -0700 Subject: [PATCH] sqlite: implement describe --- Cargo.lock | 1 - sqlx-core/Cargo.toml | 3 +- sqlx-core/src/sqlite/arguments.rs | 3 +- sqlx-core/src/sqlite/connection.rs | 7 ++-- sqlx-core/src/sqlite/cursor.rs | 6 --- sqlx-core/src/sqlite/error.rs | 2 +- sqlx-core/src/sqlite/executor.rs | 64 ++++++++++++++++++++++++++---- sqlx-core/src/sqlite/row.rs | 5 +-- sqlx-core/src/sqlite/statement.rs | 63 +++++++++++++++++++++-------- sqlx-core/src/sqlite/types/bool.rs | 2 +- sqlx-core/src/sqlite/types/mod.rs | 29 +++++++++----- tests/postgres-raw.rs | 12 ------ tests/sqlite-raw.rs | 15 ------- tests/sqlite.rs | 41 +++++++++++++++++++ 14 files changed, 172 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 452c2439..d69a78e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1660,7 +1660,6 @@ dependencies = [ "generic-array", "hex", "hmac", - "libc", "libsqlite3-sys", "log", "matches", diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml index a63b1d62..8bd43940 100644 --- a/sqlx-core/Cargo.toml +++ b/sqlx-core/Cargo.toml @@ -17,7 +17,7 @@ default = [ "runtime-async-std" ] unstable = [] postgres = [ "md-5", "sha2", "base64", "sha-1", "rand", "hmac" ] mysql = [ "sha-1", "sha2", "generic-array", "num-bigint", "base64", "digest", "rand" ] -sqlite = [ "libc", "libsqlite3-sys" ] +sqlite = [ "libsqlite3-sys" ] tls = [ "async-native-tls" ] runtime-async-std = [ "async-native-tls/runtime-async-std", "async-std" ] runtime-tokio = [ "async-native-tls/runtime-tokio", "tokio" ] @@ -50,7 +50,6 @@ sha2 = { version = "0.8.1", default-features = false, optional = true } tokio = { version = "0.2.13", default-features = false, features = [ "dns", "fs", "time", "tcp" ], optional = true } url = { version = "2.1.1", default-features = false } uuid = { version = "0.8.1", default-features = false, optional = true, features = [ "std" ] } -libc = { version = "0.2", optional = true } # [dependencies.libsqlite3-sys] diff --git a/sqlx-core/src/sqlite/arguments.rs b/sqlx-core/src/sqlite/arguments.rs index 21763b28..8bddda43 100644 --- a/sqlx-core/src/sqlite/arguments.rs +++ b/sqlx-core/src/sqlite/arguments.rs @@ -1,6 +1,7 @@ use core::ffi::c_void; -use libc::c_int; +use std::os::raw::c_int; + use libsqlite3_sys::{ sqlite3_bind_blob, sqlite3_bind_double, sqlite3_bind_int, sqlite3_bind_int64, sqlite3_bind_null, sqlite3_bind_text, SQLITE_OK, diff --git a/sqlx-core/src/sqlite/connection.rs b/sqlx-core/src/sqlite/connection.rs index c53b2746..6a918239 100644 --- a/sqlx-core/src/sqlite/connection.rs +++ b/sqlx-core/src/sqlite/connection.rs @@ -3,7 +3,6 @@ use core::ptr::{null, null_mut, NonNull}; use std::collections::HashMap; use std::convert::TryInto; use std::ffi::CString; -use std::sync::Arc; use futures_core::future::BoxFuture; use futures_util::future; @@ -22,7 +21,6 @@ pub struct SqliteConnection { pub(super) handle: NonNull, pub(super) statements: Vec, pub(super) statement_by_query: HashMap, - pub(super) columns_by_query: HashMap>>, } // SAFE: A sqlite3 handle is safe to access from multiple threads provided @@ -70,7 +68,6 @@ fn establish(url: crate::Result) -> crate::Result { handle: NonNull::new(handle).unwrap(), statements: Vec::with_capacity(10), statement_by_query: HashMap::with_capacity(10), - columns_by_query: HashMap::new(), }) } @@ -81,7 +78,9 @@ impl Connect for SqliteConnection { Self: Sized, { let url = url.try_into(); - Box::pin(spawn_blocking(move || establish(url))) + let conn = establish(url); + + Box::pin(future::ready(conn)) } } diff --git a/sqlx-core/src/sqlite/cursor.rs b/sqlx-core/src/sqlite/cursor.rs index cb836c66..1b1951af 100644 --- a/sqlx-core/src/sqlite/cursor.rs +++ b/sqlx-core/src/sqlite/cursor.rs @@ -1,6 +1,3 @@ -use std::collections::HashMap; -use std::sync::Arc; - use futures_core::future::BoxFuture; use crate::connection::ConnectionSource; @@ -22,7 +19,6 @@ enum State<'q> { pub struct SqliteCursor<'c, 'q> { source: ConnectionSource<'c, SqliteConnection>, - columns: Arc, usize>>, state: State<'q>, } @@ -36,7 +32,6 @@ impl<'c, 'q> Cursor<'c, 'q> for SqliteCursor<'c, 'q> { { Self { source: ConnectionSource::Pool(pool.clone()), - columns: Arc::default(), state: State::Query(query.into_parts()), } } @@ -48,7 +43,6 @@ impl<'c, 'q> Cursor<'c, 'q> for SqliteCursor<'c, 'q> { { Self { source: ConnectionSource::Connection(conn.into()), - columns: Arc::default(), state: State::Query(query.into_parts()), } } diff --git a/sqlx-core/src/sqlite/error.rs b/sqlx-core/src/sqlite/error.rs index 6f7e0078..b0b98e57 100644 --- a/sqlx-core/src/sqlite/error.rs +++ b/sqlx-core/src/sqlite/error.rs @@ -1,7 +1,7 @@ use crate::error::DatabaseError; -use libc::c_int; use libsqlite3_sys::sqlite3_errstr; use std::ffi::CStr; +use std::os::raw::c_int; pub struct SqliteError { #[allow(dead_code)] diff --git a/sqlx-core/src/sqlite/executor.rs b/sqlx-core/src/sqlite/executor.rs index fe9ac940..94da669f 100644 --- a/sqlx-core/src/sqlite/executor.rs +++ b/sqlx-core/src/sqlite/executor.rs @@ -3,14 +3,13 @@ use futures_core::future::BoxFuture; use libsqlite3_sys::sqlite3_changes; use crate::cursor::Cursor; -use crate::describe::Describe; +use crate::describe::{Column, Describe}; use crate::executor::{Execute, Executor, RefExecutor}; use crate::maybe_owned::MaybeOwned; -use crate::sqlite::arguments::SqliteArguments; use crate::sqlite::cursor::SqliteCursor; use crate::sqlite::statement::{SqliteStatement, Step}; -use crate::sqlite::{Sqlite, SqliteConnection}; -use std::collections::HashMap; +use crate::sqlite::types::SqliteType; +use crate::sqlite::{Sqlite, SqliteConnection, SqliteTypeInfo}; impl SqliteConnection { pub(super) fn prepare( @@ -74,7 +73,7 @@ impl Executor for SqliteConnection { Box::pin(async move { let mut statement = self.prepare(query, arguments.is_some())?; - let mut statement_ = statement.resolve(&mut self.statements); + let statement_ = statement.resolve(&mut self.statements); if let Some(arguments) = &mut arguments { statement_.bind(arguments)?; @@ -102,8 +101,59 @@ impl Executor for SqliteConnection { where E: Execute<'q, Self::Database>, { - // Box::pin(async move { self.describe(query.into_parts().0).await }) - todo!() + Box::pin(async move { + let (query, _) = query.into_parts(); + let mut statement = self.prepare(query, false)?; + let statement = statement.resolve(&mut self.statements); + + // First let's attempt to describe what we can about parameter types + // Which happens to just be the count, heh + let num_params = statement.params(); + let params = vec![ + SqliteTypeInfo { + r#type: SqliteType::Null, + affinity: None, + }; + num_params + ] + .into_boxed_slice(); + + // Next, collect (return) column types and names + let num_columns = statement.num_columns(); + let mut columns = Vec::with_capacity(num_columns); + for i in 0..num_columns { + let name = statement.column_name(i); + let decl = statement.column_decltype(i); + + let r#type = match decl { + None => SqliteType::Null, + Some(decl) => match &*decl.to_ascii_lowercase() { + "bool" | "boolean" => SqliteType::Boolean, + "clob" | "text" => SqliteType::Text, + "blob" => SqliteType::Blob, + "real" | "double" | "double precision" | "float" => SqliteType::Float, + _ if decl.contains("int") => SqliteType::Integer, + _ if decl.contains("char") => SqliteType::Text, + _ => SqliteType::Null, + }, + }; + + columns.push(Column { + name: Some(name.into()), + non_null: None, + table_id: None, + type_info: SqliteTypeInfo { + r#type, + affinity: None, + }, + }) + } + + Ok(Describe { + param_types: params, + result_columns: columns.into_boxed_slice(), + }) + }) } } diff --git a/sqlx-core/src/sqlite/row.rs b/sqlx-core/src/sqlite/row.rs index 9345d7a0..3537884f 100644 --- a/sqlx-core/src/sqlite/row.rs +++ b/sqlx-core/src/sqlite/row.rs @@ -1,8 +1,5 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use libc::c_int; use libsqlite3_sys::sqlite3_data_count; +use std::os::raw::c_int; use crate::database::HasRow; use crate::row::{ColumnIndex, Row}; diff --git a/sqlx-core/src/sqlite/statement.rs b/sqlx-core/src/sqlite/statement.rs index 62ea37b5..bb6a0b10 100644 --- a/sqlx-core/src/sqlite/statement.rs +++ b/sqlx-core/src/sqlite/statement.rs @@ -4,11 +4,13 @@ use core::ptr::{null_mut, NonNull}; use std::collections::HashMap; use std::ffi::CStr; +use std::os::raw::c_int; use libsqlite3_sys::{ - sqlite3, sqlite3_column_count, sqlite3_column_name, sqlite3_finalize, sqlite3_prepare_v3, - sqlite3_reset, sqlite3_step, sqlite3_stmt, SQLITE_DONE, SQLITE_OK, SQLITE_PREPARE_NO_VTAB, - SQLITE_PREPARE_PERSISTENT, SQLITE_ROW, + sqlite3, sqlite3_bind_parameter_count, sqlite3_column_count, sqlite3_column_decltype, + sqlite3_column_name, sqlite3_finalize, sqlite3_prepare_v3, sqlite3_reset, sqlite3_step, + sqlite3_stmt, SQLITE_DONE, SQLITE_OK, SQLITE_PREPARE_NO_VTAB, SQLITE_PREPARE_PERSISTENT, + SQLITE_ROW, }; use crate::sqlite::SqliteArguments; @@ -75,24 +77,46 @@ impl SqliteStatement { }) } + pub(super) fn num_columns(&self) -> usize { + // https://sqlite.org/c3ref/column_count.html + #[allow(unsafe_code)] + let count = unsafe { sqlite3_column_count(self.handle.as_ptr()) }; + count as usize + } + + pub(super) fn column_name(&self, index: usize) -> &str { + // https://sqlite.org/c3ref/column_name.html + #[allow(unsafe_code)] + let name = + unsafe { CStr::from_ptr(sqlite3_column_name(self.handle.as_ptr(), index as c_int)) }; + + name.to_str().unwrap() + } + + pub(super) fn column_decltype(&self, index: usize) -> Option<&str> { + // https://sqlite.org/c3ref/column_name.html + #[allow(unsafe_code)] + let name = unsafe { + let ptr = sqlite3_column_decltype(self.handle.as_ptr(), index as c_int); + + if ptr.is_null() { + None + } else { + Some(CStr::from_ptr(ptr)) + } + }; + + name.map(|s| s.to_str().unwrap()) + } + pub(super) fn columns<'a>(&'a self) -> impl Deref> + 'a { RefMut::map(self.columns.borrow_mut(), |columns| { columns.get_or_insert_with(|| { - // https://sqlite.org/c3ref/column_count.html - #[allow(unsafe_code)] - let count = unsafe { sqlite3_column_count(self.handle.as_ptr()) }; - let count = count as usize; - + let count = self.num_columns(); let mut columns = HashMap::with_capacity(count); for i in 0..count { - // https://sqlite.org/c3ref/column_name.html - #[allow(unsafe_code)] - let name = - unsafe { CStr::from_ptr(sqlite3_column_name(self.handle.as_ptr(), 0)) }; - let name = name.to_str().unwrap().to_owned(); - - columns.insert(name, i); + columns.insert(self.column_name(i).to_owned(), i); } columns @@ -100,6 +124,13 @@ impl SqliteStatement { }) } + pub(super) fn params(&self) -> usize { + // https://www.hwaci.com/sw/sqlite/c3ref/bind_parameter_count.html + #[allow(unsafe_code)] + let num = unsafe { sqlite3_bind_parameter_count(self.handle.as_ptr()) }; + num as usize + } + pub(super) fn bind(&mut self, arguments: &mut SqliteArguments) -> crate::Result<()> { for (index, value) in arguments.values.iter().enumerate() { value.bind(self, index + 1)?; @@ -125,8 +156,8 @@ impl SqliteStatement { let status = unsafe { sqlite3_step(self.handle.as_ptr()) }; match status { - SQLITE_ROW => Ok(Step::Row), SQLITE_DONE => Ok(Step::Done), + SQLITE_ROW => Ok(Step::Row), status => { return Err(SqliteError::new(status).into()); diff --git a/sqlx-core/src/sqlite/types/bool.rs b/sqlx-core/src/sqlite/types/bool.rs index 67eb7df4..4d014671 100644 --- a/sqlx-core/src/sqlite/types/bool.rs +++ b/sqlx-core/src/sqlite/types/bool.rs @@ -6,7 +6,7 @@ use crate::types::Type; impl Type for bool { fn type_info() -> SqliteTypeInfo { - SqliteTypeInfo::new(SqliteType::Integer, SqliteTypeAffinity::Numeric) + SqliteTypeInfo::new(SqliteType::Boolean, SqliteTypeAffinity::Numeric) } } diff --git a/sqlx-core/src/sqlite/types/mod.rs b/sqlx-core/src/sqlite/types/mod.rs index 084dac7c..cfeb2c6e 100644 --- a/sqlx-core/src/sqlite/types/mod.rs +++ b/sqlx-core/src/sqlite/types/mod.rs @@ -16,14 +16,17 @@ mod str; pub(crate) enum SqliteType { Integer = 1, Float = 2, + Text = 3, Blob = 4, Null = 5, - Text = 3, + + // Non-standard extensions + Boolean, } // https://www.sqlite.org/datatype3.html#type_affinity #[derive(Debug, PartialEq, Clone, Copy)] -enum SqliteTypeAffinity { +pub(crate) enum SqliteTypeAffinity { Text, Numeric, Integer, @@ -33,24 +36,28 @@ enum SqliteTypeAffinity { #[derive(Debug, Clone)] pub struct SqliteTypeInfo { - r#type: SqliteType, - affinity: SqliteTypeAffinity, + pub(crate) r#type: SqliteType, + pub(crate) affinity: Option, } impl SqliteTypeInfo { fn new(r#type: SqliteType, affinity: SqliteTypeAffinity) -> Self { - Self { r#type, affinity } + Self { + r#type, + affinity: Some(affinity), + } } } impl Display for SqliteTypeInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self.affinity { - SqliteTypeAffinity::Text => "TEXT", - SqliteTypeAffinity::Numeric => "NUMERIC", - SqliteTypeAffinity::Integer => "INTEGER", - SqliteTypeAffinity::Real => "REAL", - SqliteTypeAffinity::Blob => "BLOB", + f.write_str(match self.r#type { + SqliteType::Null => "NULL", + SqliteType::Text => "TEXT", + SqliteType::Boolean => "BOOLEAN", + SqliteType::Integer => "INTEGER", + SqliteType::Float => "DOUBLE", + SqliteType::Blob => "BLOB", }) } } diff --git a/tests/postgres-raw.rs b/tests/postgres-raw.rs index 0e37ff53..f8b98853 100644 --- a/tests/postgres-raw.rs +++ b/tests/postgres-raw.rs @@ -18,18 +18,6 @@ async fn test_empty_query() -> anyhow::Result<()> { Ok(()) } -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] -async fn test_multi_cursor() -> anyhow::Result<()> { - let mut conn = new::().await?; - - let query = format!("SELECT {} as _1", 10); - let _cursor1 = conn.fetch(&*query); - let _cursor2 = conn.fetch(&*query); - - Ok(()) -} - /// Test a simple select expression. This should return the row. #[cfg_attr(feature = "runtime-async-std", async_std::test)] #[cfg_attr(feature = "runtime-tokio", tokio::test)] diff --git a/tests/sqlite-raw.rs b/tests/sqlite-raw.rs index 865653c3..be0b0545 100644 --- a/tests/sqlite-raw.rs +++ b/tests/sqlite-raw.rs @@ -2,18 +2,3 @@ use sqlx::{Cursor, Executor, Row, Sqlite}; use sqlx_test::new; - -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] -async fn test_multi_cursor() -> anyhow::Result<()> { - let mut conn = new::().await?; - - let query = format!("SELECT {} as _1", 10); - let mut cursor1 = conn.fetch(&*query); - let mut cursor2 = conn.fetch(&*query); - // let row = cursor.next().await?.unwrap(); - - // assert!(5i32 == row.try_get::(0)?); - - Ok(()) -} diff --git a/tests/sqlite.rs b/tests/sqlite.rs index 659391bc..de1d8ccb 100644 --- a/tests/sqlite.rs +++ b/tests/sqlite.rs @@ -53,3 +53,44 @@ CREATE TEMPORARY TABLE users (id INTEGER PRIMARY KEY) Ok(()) } + +#[cfg_attr(feature = "runtime-async-std", async_std::test)] +#[cfg_attr(feature = "runtime-tokio", tokio::test)] +async fn it_describes() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let _ = conn + .execute( + r#" +CREATE TEMPORARY TABLE describe_test ( + _1 int primary key, + _2 text not null, + _3 blob, + _4 boolean, + _5 float, + _6 varchar(255), + _7 double, + _8 bigint +) + "#, + ) + .await?; + + let describe = conn + .describe("select nt.*, false from describe_test nt") + .await?; + + assert_eq!(describe.result_columns[0].type_info.to_string(), "INTEGER"); + assert_eq!(describe.result_columns[1].type_info.to_string(), "TEXT"); + assert_eq!(describe.result_columns[2].type_info.to_string(), "BLOB"); + assert_eq!(describe.result_columns[3].type_info.to_string(), "BOOLEAN"); + assert_eq!(describe.result_columns[4].type_info.to_string(), "DOUBLE"); + assert_eq!(describe.result_columns[5].type_info.to_string(), "TEXT"); + assert_eq!(describe.result_columns[6].type_info.to_string(), "DOUBLE"); + assert_eq!(describe.result_columns[7].type_info.to_string(), "INTEGER"); + + // Expressions can not be described + assert_eq!(describe.result_columns[8].type_info.to_string(), "NULL"); + + Ok(()) +}