SQLite: implement column nullability checking

This commit is contained in:
Austin Bonander 2020-03-27 19:28:47 -07:00 committed by Ryan Leckey
parent fe00c0d619
commit 4ffa7f0e01
3 changed files with 80 additions and 7 deletions

View file

@ -163,7 +163,7 @@ impl Executor for SqliteConnection {
columns.push(Column {
name: Some(name.into()),
non_null: None,
non_null: statement.column_not_null(i)?,
table_id: None,
type_info: r#type.map(|r#type| SqliteTypeInfo {
r#type,

View file

@ -5,19 +5,23 @@ use core::ptr::{null, null_mut, NonNull};
use std::collections::HashMap;
use std::ffi::CStr;
use std::os::raw::c_int;
use std::ptr;
use libsqlite3_sys::{
sqlite3_bind_parameter_count, sqlite3_clear_bindings, sqlite3_column_count,
sqlite3_column_decltype, sqlite3_column_name, sqlite3_data_count, 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_column_database_name, sqlite3_column_decltype, sqlite3_column_name,
sqlite3_column_origin_name, sqlite3_column_table_name, sqlite3_data_count, sqlite3_finalize,
sqlite3_prepare_v3, sqlite3_reset, sqlite3_step, sqlite3_stmt, sqlite3_table_column_metadata,
SQLITE_DONE, SQLITE_OK, SQLITE_PREPARE_NO_VTAB, SQLITE_PREPARE_PERSISTENT, SQLITE_ROW,
};
use crate::sqlite::connection::SqliteConnectionHandle;
use crate::sqlite::worker::Worker;
use crate::error::DatabaseError;
use crate::sqlite::SqliteError;
use crate::sqlite::{SqliteArguments, SqliteConnection};
use bitflags::_core::str::from_utf8_unchecked;
/// Return values from [SqliteStatement::step].
pub(super) enum Step {
@ -162,6 +166,55 @@ impl Statement {
name.map(|s| s.to_str().unwrap())
}
pub(super) fn column_not_null(&mut self, index: usize) -> crate::Result<Option<bool>> {
unsafe {
// https://sqlite.org/c3ref/column_database_name.html
//
// ### Note
// The returned string is valid until the prepared statement is destroyed using
// sqlite3_finalize() or until the statement is automatically reprepared by the
// first call to sqlite3_step() for a particular run or until the same information
// is requested again in a different encoding.
let db_name = sqlite3_column_database_name(self.handle(), index as c_int);
let table_name = sqlite3_column_table_name(self.handle(), index as c_int);
let origin_name = sqlite3_column_origin_name(self.handle(), index as c_int);
if db_name.is_null() || table_name.is_null() || origin_name.is_null() {
return Ok(None);
}
let mut not_null: c_int = 0;
// https://sqlite.org/c3ref/table_column_metadata.html
let status = sqlite3_table_column_metadata(
self.connection.0.as_ptr(),
db_name,
table_name,
origin_name,
// function docs state to provide NULL for return values you don't care about
ptr::null_mut(),
ptr::null_mut(),
&mut not_null,
ptr::null_mut(),
ptr::null_mut(),
);
if status != SQLITE_OK {
// implementation note: the docs for sqlite3_table_column_metadata() specify
// that an error can be returned if the column came from a view; however,
// experimentally we found that the above functions give us the true origin
// for columns in views that came from real tables and so we should never hit this
// error; for view columns that are expressions we are given NULL for their origins
// so we don't need special handling for that case either.
//
// this is confirmed in the `tests/sqlite-macros.rs` integration test
return Err(SqliteError::from_connection(self.connection.0.as_ptr()).into());
}
Ok(Some(not_null != 0))
}
}
pub(super) fn params(&mut self) -> usize {
// https://www.hwaci.com/sw/sqlite/c3ref/bind_parameter_count.html
let num = unsafe { sqlite3_bind_parameter_count(self.handle()) };

View file

@ -12,6 +12,7 @@ async fn macro_select() -> anyhow::Result<()> {
assert_eq!(1, account.id);
assert_eq!("Herp Derpinson", account.name);
assert_eq!(account.is_active, None);
Ok(())
}
@ -30,6 +31,7 @@ async fn macro_select_bind() -> anyhow::Result<()> {
assert_eq!(1, account.id);
assert_eq!("Herp Derpinson", account.name);
assert_eq!(account.is_active, None);
Ok(())
}
@ -38,7 +40,7 @@ async fn macro_select_bind() -> anyhow::Result<()> {
struct RawAccount {
id: i32,
name: String,
is_active: bool,
is_active: Option<bool>,
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
@ -50,8 +52,26 @@ async fn test_query_as_raw() -> anyhow::Result<()> {
.fetch_one(&mut conn)
.await?;
assert_eq!(1, account.id);
assert_eq!("Herp Derpinson", account.name);
assert_eq!(account.id, 1);
assert_eq!(account.name, "Herp Derpinson");
assert_eq!(account.is_active, None);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn macro_select_from_view() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let account = sqlx::query!("SELECT id, name, is_active from accounts_view")
.fetch_one(&mut conn)
.await?;
// SQLite tells us the true origin of these columns even through the view
assert_eq!(account.id, 1);
assert_eq!(account.name, "Herp Derpinson");
assert_eq!(account.is_active, None);
Ok(())
}