add nullability info to Describe

implement nullability check for Postgres as a query on pg_attribute

implement type name fetching for Postgres as part of `describe()`

add nullability for describe() to MySQL

improve errors with unknown result column type IDs in `query!()`

run cargo fmt and fix warnings

improve error when feature gates for chrono/uuid types is not turned on

workflows/rust: add step to UI-test missing optional features

improve error for unsupported/feature-gated input parameter types

fix `PgConnection::get_type_names()` for empty type IDs list

fix `tests::mysql::test_describe()` on MariaDB 10.4

copy-edit unsupported/feature-gated type errors in `query!()`

Postgres: fix SQL type of string array

closes #107
closes #17

Co-Authored-By: Anthony Dodd <Dodd.AnthonyJosiah@gmail.com>
This commit is contained in:
Austin Bonander 2020-01-24 20:24:39 -08:00 committed by Ryan Leckey
parent 59cf900348
commit 4163388298
33 changed files with 654 additions and 64 deletions

View file

@ -120,6 +120,16 @@ jobs:
env:
DATABASE_URL: postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/postgres
# UI feature gate tests: async-std
- run: cargo test --no-default-features --features 'runtime-async-std postgres macros tls'
env:
DATABASE_URL: postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/postgres
# UI feature gate tests: tokio
- run: cargo test --no-default-features --features 'runtime-tokio postgres macros tls'
env:
DATABASE_URL: postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/postgres
mysql:
needs: build
runs-on: ubuntu-latest
@ -176,6 +186,21 @@ jobs:
# NOTE: Github Actions' YML parser doesn't handle multiline strings correctly
DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem
# UI feature gate tests: async-std
- run: cargo test --no-default-features --features 'runtime-async-std mysql macros tls' --test ui-tests
env:
# pass the path to the CA that the MySQL service generated
# NOTE: Github Actions' YML parser doesn't handle multiline strings correctly
DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem
# UI feature gate tests: tokio
- run: cargo test --no-default-features --features 'runtime-tokio mysql macros tls' --test ui-tests
env:
# pass the path to the CA that the MySQL service generated
# NOTE: Github Actions' YML parser doesn't handle multiline strings correctly
DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem
mariadb:
needs: build
runs-on: ubuntu-latest
@ -225,3 +250,13 @@ jobs:
- run: cargo test --no-default-features --features 'runtime-tokio mysql macros uuid chrono tls'
env:
DATABASE_URL: mariadb://root:password@localhost:${{ job.services.mariadb.ports[3306] }}/sqlx
# UI feature gate tests: async-std
- run: cargo test --no-default-features --features 'runtime-async-std mysql macros tls'
env:
DATABASE_URL: mariadb://root:password@localhost:${{ job.services.mariadb.ports[3306] }}/sqlx
# UI feature gate tests: tokio
- run: cargo test --no-default-features --features 'runtime-tokio mysql macros tls'
env:
DATABASE_URL: mariadb://root:password@localhost:${{ job.services.mariadb.ports[3306] }}/sqlx

View file

@ -40,6 +40,8 @@ where
pub name: Option<Box<str>>,
pub table_id: Option<DB::TableId>,
pub type_info: DB::TypeInfo,
/// Whether or not the column cannot be `NULL` (or if that is even knowable).
pub non_null: Option<bool>,
}
impl<DB> Debug for Column<DB>
@ -53,6 +55,7 @@ where
.field("name", &self.name)
.field("table_id", &self.table_id)
.field("type_id", &self.type_info)
.field("nonnull", &self.non_null)
.finish()
}
}

View file

@ -4,11 +4,11 @@ use std::sync::Arc;
use futures_core::future::BoxFuture;
use futures_core::stream::BoxStream;
use crate::describe::{Column, Describe};
use crate::describe::{Column, Describe, Nullability};
use crate::executor::Executor;
use crate::mysql::protocol::{
Capabilities, ColumnCount, ColumnDefinition, ComQuery, ComStmtExecute, ComStmtPrepare,
ComStmtPrepareOk, Cursor, Decode, EofPacket, OkPacket, Row, TypeId,
ComStmtPrepareOk, Cursor, Decode, EofPacket, FieldFlags, OkPacket, Row, TypeId,
};
use crate::mysql::{MySql, MySqlArguments, MySqlConnection, MySqlRow, MySqlTypeInfo};
@ -253,10 +253,12 @@ impl MySqlConnection {
for _ in 0..prepare_ok.columns {
let column = ColumnDefinition::decode(self.receive().await?.packet())?;
result_columns.push(Column::<MySql> {
type_info: MySqlTypeInfo::from_column_def(&column),
name: column.column_alias.or(column.column),
table_id: column.table_alias.or(column.table),
non_null: Some(column.flags.contains(FieldFlags::NOT_NULL)),
});
}

View file

@ -1,10 +1,36 @@
use std::fmt::{self, Debug, Display, Formatter};
// https://dev.mysql.com/doc/dev/mysql-server/8.0.12/binary__log__types_8h.html
// https://mariadb.com/kb/en/library/resultset/#field-types
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TypeId(pub u8);
macro_rules! type_id_consts {
($(
pub const $name:ident: TypeId = TypeId($id:literal);
)*) => (
impl TypeId {
$(pub const $name: TypeId = TypeId($id);)*
#[doc(hidden)]
pub fn type_name(&self) -> &'static str {
match self.0 {
$($id => stringify!($name),)*
_ => "<unknown>"
}
}
}
)
}
impl Display for TypeId {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{} ({:#x})", self.type_name(), self.0)
}
}
// https://github.com/google/mysql/blob/c01fc2134d439282a21a2ddf687566e198ddee28/include/mysql_com.h#L429
impl TypeId {
type_id_consts! {
pub const NULL: TypeId = TypeId(6);
// String: CHAR, VARCHAR, TEXT
@ -23,6 +49,7 @@ impl TypeId {
pub const SMALL_INT: TypeId = TypeId(2);
pub const INT: TypeId = TypeId(3);
pub const BIG_INT: TypeId = TypeId(8);
pub const MEDIUM_INT: TypeId = TypeId(9);
// Numeric: FLOAT, DOUBLE
pub const FLOAT: TypeId = TypeId(4);

View file

@ -49,12 +49,28 @@ impl MySqlTypeInfo {
char_set: def.char_set,
}
}
#[doc(hidden)]
pub fn type_name(&self) -> &'static str {
self.id.type_name()
}
#[doc(hidden)]
pub fn type_feature_gate(&self) -> Option<&'static str> {
match self.id {
TypeId::DATE | TypeId::TIME | TypeId::DATETIME | TypeId::TIMESTAMP => Some("chrono"),
_ => None,
}
}
}
impl Display for MySqlTypeInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// TODO: Should we attempt to render the type *name* here?
write!(f, "{}", self.id.0)
if self.id.type_name() != "<unknown>" {
write!(f, "{}", self.id.type_name())
} else {
write!(f, "ID {:#x}", self.id.0)
}
}
}

View file

@ -74,7 +74,7 @@ fn parse_row_description(rd: RowDescription) -> (HashMap<Box<str>, usize>, Vec<T
// Used to describe the incoming results
// We store the column map in an Arc and share it among all rows
async fn describe(
async fn expect_desc(
conn: &mut PgConnection,
) -> crate::Result<(HashMap<Box<str>, usize>, Vec<TypeFormat>)> {
let description: Option<_> = loop {
@ -108,7 +108,7 @@ async fn get_or_describe(
if !conn.cache_statement_columns.contains_key(&statement)
|| !conn.cache_statement_formats.contains_key(&statement)
{
let (columns, formats) = describe(conn).await?;
let (columns, formats) = expect_desc(conn).await?;
conn.cache_statement_columns
.insert(statement, Arc::new(columns));

View file

@ -1,12 +1,20 @@
use futures_core::future::BoxFuture;
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use futures_core::future::BoxFuture;
use futures_util::{stream, StreamExt, TryStreamExt};
use crate::arguments::Arguments;
use crate::cursor::Cursor;
use crate::describe::{Column, Describe};
use crate::executor::{Execute, Executor, RefExecutor};
use crate::postgres::protocol::{
self, CommandComplete, Message, ParameterDescription, RowDescription, StatementId, TypeFormat,
self, CommandComplete, Field, Message, ParameterDescription, RowDescription, StatementId,
TypeFormat, TypeId,
};
use crate::postgres::{PgArguments, PgConnection, PgCursor, PgTypeInfo, Postgres};
use crate::postgres::types::SharedStr;
use crate::postgres::{PgArguments, PgConnection, PgCursor, PgRow, PgTypeInfo, Postgres};
use crate::row::Row;
impl PgConnection {
pub(crate) fn write_simple_query(&mut self, query: &str) {
@ -132,13 +140,14 @@ impl PgConnection {
&'e mut self,
query: &'q str,
) -> crate::Result<Describe<Postgres>> {
self.is_ready = false;
let statement = self.write_prepare(query, &Default::default());
self.write_describe(protocol::Describe::Statement(statement));
self.write_sync();
self.stream.flush().await?;
self.wait_until_ready().await?;
let params = loop {
match self.stream.read().await? {
@ -171,29 +180,149 @@ impl PgConnection {
}
};
self.wait_until_ready().await?;
let result_fields = result.map_or_else(Default::default, |r| r.fields);
// TODO: cache this result
let type_names = self
.get_type_names(
params
.ids
.iter()
.cloned()
.chain(result_fields.iter().map(|field| field.type_id)),
)
.await?;
Ok(Describe {
param_types: params
.ids
.iter()
.map(|id| PgTypeInfo::new(*id))
.map(|id| PgTypeInfo::new(*id, &type_names[&id.0]))
.collect::<Vec<_>>()
.into_boxed_slice(),
result_columns: result
.map(|r| r.fields)
.unwrap_or_default()
.into_vec()
.into_iter()
// TODO: Should [Column] just wrap [protocol::Field] ?
.map(|field| Column {
name: field.name,
table_id: field.table_id,
type_info: PgTypeInfo::new(field.type_id),
})
.collect::<Vec<_>>()
result_columns: self
.map_result_columns(result_fields, type_names)
.await?
.into_boxed_slice(),
})
}
async fn get_type_names(
&mut self,
ids: impl IntoIterator<Item = TypeId>,
) -> crate::Result<HashMap<u32, SharedStr>> {
let type_ids: HashSet<u32> = ids.into_iter().map(|id| id.0).collect::<HashSet<u32>>();
if type_ids.is_empty() {
return Ok(HashMap::new());
}
// uppercase type names are easier to visually identify
let mut query = "select types.type_id, UPPER(pg_type.typname) from (VALUES ".to_string();
let mut args = PgArguments::default();
let mut pushed = false;
// TODO: dedup this with the one below, ideally as an API we can export
for (i, (&type_id, bind)) in type_ids.iter().zip((1..).step_by(2)).enumerate() {
if pushed {
query += ", ";
}
pushed = true;
let _ = write!(query, "(${}, ${})", bind, bind + 1);
// not used in the output but ensures are values are sorted correctly
args.add(i as i32);
args.add(type_id as i32);
}
query += ") as types(idx, type_id) \
inner join pg_catalog.pg_type on pg_type.oid = type_id \
order by types.idx";
crate::query::query(&query)
.bind_all(args)
.map(|row: PgRow| -> crate::Result<(u32, SharedStr)> {
Ok((
row.get::<i32, _>(0)? as u32,
row.get::<String, _>(1)?.into(),
))
})
.fetch(self)
.try_collect()
.await
}
async fn map_result_columns(
&mut self,
fields: Box<[Field]>,
type_names: HashMap<u32, SharedStr>,
) -> crate::Result<Vec<Column<Postgres>>> {
if fields.is_empty() {
return Ok(vec![]);
}
let mut query = "select col.idx, pg_attribute.attnotnull from (VALUES ".to_string();
let mut pushed = false;
let mut args = PgArguments::default();
for (i, (field, bind)) in fields.iter().zip((1..).step_by(3)).enumerate() {
if pushed {
query += ", ";
}
pushed = true;
let _ = write!(
query,
"(${}::int4, ${}::int4, ${}::int2)",
bind,
bind + 1,
bind + 2
);
args.add(i as i32);
args.add(field.table_id.map(|id| id as i32));
args.add(field.column_id);
}
query += ") as col(idx, table_id, col_idx) \
left join pg_catalog.pg_attribute on table_id is not null and attrelid = table_id and attnum = col_idx \
order by col.idx;";
log::trace!("describe pg_attribute query: {:#?}", query);
crate::query::query(&query)
.bind_all(args)
.map(|row: PgRow| {
let idx = row.get::<i32, _>(0)?;
let non_null = row.get::<Option<bool>, _>(1)?;
Ok((idx, non_null))
})
.fetch(self)
.zip(stream::iter(fields.into_vec().into_iter().enumerate()))
.map(|(row, (fidx, field))| -> crate::Result<Column<_>> {
let (idx, non_null) = row?;
if idx != fidx as i32 {
return Err(
protocol_err!("missing field from query, field: {:?}", field).into(),
);
}
Ok(Column {
name: field.name,
table_id: field.table_id,
type_info: PgTypeInfo::new(field.type_id, &type_names[&field.type_id.0]),
non_null,
})
})
.try_collect()
.await
}
// Poll messages from Postgres, counting the rows affected, until we finish the query
// This must be called directly after a call to [PgConnection::execute]
async fn affected_rows(&mut self) -> crate::Result<u64> {

View file

@ -1,3 +1,5 @@
use std::io::Write;
use crate::io::BufMut;
use crate::postgres::protocol::Encode;
@ -7,10 +9,7 @@ pub struct StatementId(pub u32);
impl Encode for StatementId {
fn encode(&self, buf: &mut Vec<u8>) {
if self.0 != 0 {
buf.put_str("__sqlx_statement_");
// TODO: Use [itoa]
buf.put_str_nul(&self.0.to_string());
let _ = write!(buf, "__sqlx_statement_{}\0", self.0);
} else {
buf.put_str_nul("");
}

View file

@ -1,4 +1,4 @@
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TypeId(pub(crate) u32);
#[allow(dead_code)]

View file

@ -10,13 +10,13 @@ use crate::types::Type;
impl Type<Postgres> for bool {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::BOOL)
PgTypeInfo::new(TypeId::BOOL, "BOOL")
}
}
impl Type<Postgres> for [bool] {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_BOOL)
PgTypeInfo::new(TypeId::ARRAY_BOOL, "BOOL[]")
}
}

View file

@ -9,13 +9,13 @@ use crate::types::Type;
impl Type<Postgres> for [u8] {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::BYTEA)
PgTypeInfo::new(TypeId::BYTEA, "BYTEA")
}
}
impl Type<Postgres> for [&'_ [u8]] {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_BYTEA)
PgTypeInfo::new(TypeId::ARRAY_BYTEA, "BYTEA[]")
}
}

View file

@ -15,19 +15,19 @@ use crate::Error;
impl Type<Postgres> for NaiveTime {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::TIME)
PgTypeInfo::new(TypeId::TIME, "TIME")
}
}
impl Type<Postgres> for NaiveDate {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::DATE)
PgTypeInfo::new(TypeId::DATE, "DATE")
}
}
impl Type<Postgres> for NaiveDateTime {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::TIMESTAMP)
PgTypeInfo::new(TypeId::TIMESTAMP, "TIMESTAMP")
}
}
@ -36,25 +36,25 @@ where
Tz: TimeZone,
{
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::TIMESTAMPTZ)
PgTypeInfo::new(TypeId::TIMESTAMPTZ, "TIMESTAMPTZ")
}
}
impl Type<Postgres> for [NaiveTime] {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_TIME)
PgTypeInfo::new(TypeId::ARRAY_TIME, "TIME[]")
}
}
impl Type<Postgres> for [NaiveDate] {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_DATE)
PgTypeInfo::new(TypeId::ARRAY_DATE, "DATE[]")
}
}
impl Type<Postgres> for [NaiveDateTime] {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_TIMESTAMP)
PgTypeInfo::new(TypeId::ARRAY_TIMESTAMP, "TIMESTAMP[]")
}
}
@ -63,7 +63,7 @@ where
Tz: TimeZone,
{
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_TIMESTAMPTZ)
PgTypeInfo::new(TypeId::ARRAY_TIMESTAMPTZ, "TIMESTAMP[]")
}
}

View file

@ -13,13 +13,13 @@ use crate::types::Type;
impl Type<Postgres> for f32 {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::FLOAT4)
PgTypeInfo::new(TypeId::FLOAT4, "FLOAT4")
}
}
impl Type<Postgres> for [f32] {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_FLOAT4)
PgTypeInfo::new(TypeId::ARRAY_FLOAT4, "FLOAT4[]")
}
}
@ -44,13 +44,13 @@ impl<'de> Decode<'de, Postgres> for f32 {
impl Type<Postgres> for f64 {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::FLOAT8)
PgTypeInfo::new(TypeId::FLOAT8, "FLOAT8")
}
}
impl Type<Postgres> for [f64] {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_FLOAT8)
PgTypeInfo::new(TypeId::ARRAY_FLOAT8, "FLOAT8[]")
}
}

View file

@ -13,13 +13,13 @@ use crate::Error;
impl Type<Postgres> for i16 {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::INT2)
PgTypeInfo::new(TypeId::INT2, "INT2")
}
}
impl Type<Postgres> for [i16] {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_INT2)
PgTypeInfo::new(TypeId::ARRAY_INT2, "INT2[]")
}
}
@ -40,13 +40,13 @@ impl<'de> Decode<'de, Postgres> for i16 {
impl Type<Postgres> for i32 {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::INT4)
PgTypeInfo::new(TypeId::INT4, "INT4")
}
}
impl Type<Postgres> for [i32] {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_INT4)
PgTypeInfo::new(TypeId::ARRAY_INT4, "INT4[]")
}
}
@ -67,13 +67,13 @@ impl<'de> Decode<'de, Postgres> for i32 {
impl Type<Postgres> for i64 {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::INT8)
PgTypeInfo::new(TypeId::INT8, "INT8")
}
}
impl Type<Postgres> for [i64] {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_INT8)
PgTypeInfo::new(TypeId::ARRAY_INT8, "INT8[]")
}
}

View file

@ -1,4 +1,6 @@
use std::fmt::{self, Debug, Display};
use std::ops::Deref;
use std::sync::Arc;
use crate::decode::Decode;
use crate::postgres::protocol::TypeId;
@ -20,11 +22,15 @@ mod uuid;
#[derive(Debug, Clone)]
pub struct PgTypeInfo {
pub(crate) id: TypeId,
pub(crate) name: Option<SharedStr>,
}
impl PgTypeInfo {
pub(crate) fn new(id: TypeId) -> Self {
Self { id }
pub(crate) fn new(id: TypeId, name: impl Into<SharedStr>) -> Self {
Self {
id,
name: Some(name.into()),
}
}
/// Create a `PgTypeInfo` from a type's object identifier.
@ -32,14 +38,34 @@ impl PgTypeInfo {
/// The object identifier of a type can be queried with
/// `SELECT oid FROM pg_type WHERE typname = <name>;`
pub fn with_oid(oid: u32) -> Self {
Self { id: TypeId(oid) }
Self {
id: TypeId(oid),
name: None,
}
}
#[doc(hidden)]
pub fn type_name(&self) -> &str {
self.name.as_deref().unwrap_or("<UNKNOWN>")
}
#[doc(hidden)]
pub fn type_feature_gate(&self) -> Option<&'static str> {
match self.id {
TypeId::DATE | TypeId::TIME | TypeId::TIMESTAMP | TypeId::TIMESTAMPTZ => Some("chrono"),
TypeId::UUID => Some("uuid"),
_ => None,
}
}
}
impl Display for PgTypeInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// TODO: Should we attempt to render the type *name* here?
write!(f, "{}", self.id.0)
if let Some(ref name) = self.name {
write!(f, "{}", *name)
} else {
write!(f, "OID {}", self.id.0)
}
}
}
@ -60,3 +86,46 @@ where
.transpose()
}
}
/// Copy of `Cow` but for strings; clones guaranteed to be cheap.
#[derive(Clone, Debug)]
pub(crate) enum SharedStr {
Static(&'static str),
Arc(Arc<str>),
}
impl Deref for SharedStr {
type Target = str;
fn deref(&self) -> &str {
match self {
SharedStr::Static(s) => s,
SharedStr::Arc(s) => s,
}
}
}
impl<'a> From<&'a SharedStr> for SharedStr {
fn from(s: &'a SharedStr) -> Self {
s.clone()
}
}
impl From<&'static str> for SharedStr {
fn from(s: &'static str) -> Self {
SharedStr::Static(s)
}
}
impl From<String> for SharedStr {
#[inline]
fn from(s: String) -> Self {
SharedStr::Arc(s.into())
}
}
impl fmt::Display for SharedStr {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.pad(self)
}
}

View file

@ -12,13 +12,13 @@ use crate::Error;
impl Type<Postgres> for str {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::TEXT)
PgTypeInfo::new(TypeId::TEXT, "TEXT")
}
}
impl Type<Postgres> for [&'_ str] {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_TEXT)
PgTypeInfo::new(TypeId::ARRAY_TEXT, "TEXT[]")
}
}

View file

@ -13,13 +13,13 @@ use crate::types::Type;
impl Type<Postgres> for Uuid {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::UUID)
PgTypeInfo::new(TypeId::UUID, "UUID")
}
}
impl Type<Postgres> for [Uuid] {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_UUID)
PgTypeInfo::new(TypeId::ARRAY_UUID, "UUID[]")
}
}

View file

@ -24,10 +24,19 @@ pub trait DatabaseExt: Database {
fn param_type_for_id(id: &Self::TypeInfo) -> Option<&'static str>;
fn return_type_for_id(id: &Self::TypeInfo) -> Option<&'static str>;
fn get_feature_gate(info: &Self::TypeInfo) -> Option<&'static str>;
}
macro_rules! impl_database_ext {
($database:path { $($(#[$meta:meta])? $ty:ty $(| $input:ty)?),*$(,)? }, ParamChecking::$param_checking:ident, row = $row:path) => {
(
$database:path {
$($(#[$meta:meta])? $ty:ty $(| $input:ty)?),*$(,)?
},
ParamChecking::$param_checking:ident,
feature-types: $name:ident => $get_gate:expr,
row = $row:path
) => {
impl $crate::database::DatabaseExt for $database {
const DATABASE_PATH: &'static str = stringify!($database);
const ROW_PATH: &'static str = stringify!($row);
@ -53,6 +62,10 @@ macro_rules! impl_database_ext {
_ => None
}
}
fn get_feature_gate($name: &Self::TypeInfo) -> Option<&'static str> {
$get_gate
}
}
}
}

View file

@ -30,5 +30,6 @@ impl_database_ext! {
sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>,
},
ParamChecking::Weak,
feature-types: info => info.type_feature_gate(),
row = sqlx::mysql::MySqlRow
}

View file

@ -27,5 +27,6 @@ impl_database_ext! {
sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc> | sqlx::types::chrono::DateTime<_>,
},
ParamChecking::Strong,
feature-types: info => info.type_feature_gate(),
row = sqlx::postgres::PgRow
}

View file

@ -22,7 +22,8 @@ pub fn quote_args<DB: DatabaseExt>(
.param_types
.iter()
.zip(&*input.arg_exprs)
.map(|(type_, expr)| {
.enumerate()
.map(|(i, (type_, expr))| {
get_type_override(expr)
.or_else(|| {
Some(
@ -31,7 +32,19 @@ pub fn quote_args<DB: DatabaseExt>(
.unwrap(),
)
})
.ok_or_else(|| format!("unknown type param ID: {}", type_).into())
.ok_or_else(|| {
if let Some(feature_gate) = <DB as DatabaseExt>::get_feature_gate(&type_) {
format!(
"optional feature `{}` required for type {} of param #{}",
feature_gate,
type_,
i + 1,
)
.into()
} else {
format!("unsupported type {} for param #{}", type_, i + 1).into()
}
})
})
.collect::<crate::Result<Vec<_>>>()?;

View file

@ -6,11 +6,31 @@ use sqlx::describe::Describe;
use crate::database::DatabaseExt;
use std::fmt::{self, Display, Formatter};
pub struct RustColumn {
pub(super) ident: Ident,
pub(super) type_: TokenStream,
}
struct DisplayColumn<'a> {
// zero-based index, converted to 1-based number
idx: usize,
name: Option<&'a str>,
}
impl Display for DisplayColumn<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let num = self.idx + 1;
if let Some(name) = self.name {
write!(f, "column #{} ({:?})", num, name)
} else {
write!(f, "column #{}", num)
}
}
}
pub fn columns_to_rust<DB: DatabaseExt>(describe: &Describe<DB>) -> crate::Result<Vec<RustColumn>> {
describe
.result_columns
@ -25,7 +45,30 @@ pub fn columns_to_rust<DB: DatabaseExt>(describe: &Describe<DB>) -> crate::Resul
let ident = parse_ident(name)?;
let type_ = <DB as DatabaseExt>::return_type_for_id(&column.type_info)
.ok_or_else(|| format!("unknown type: {}", &column.type_info))?
.ok_or_else(|| {
if let Some(feature_gate) =
<DB as DatabaseExt>::get_feature_gate(&column.type_info)
{
format!(
"optional feature `{feat}` required for type {ty} of {col}",
ty = &column.type_info,
feat = feature_gate,
col = DisplayColumn {
idx: i,
name: column.name.as_deref()
}
)
} else {
format!(
"unsupported type {ty} of {col}",
ty = column.type_info,
col = DisplayColumn {
idx: i,
name: column.name.as_deref()
}
)
}
})?
.parse::<TokenStream>()
.unwrap();

View file

@ -65,6 +65,49 @@ async fn it_selects_null() -> anyhow::Result<()> {
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_describe() -> anyhow::Result<()> {
use sqlx::describe::Nullability::*;
let mut conn = connect().await?;
let _ = conn
.send(
r#"
CREATE TEMPORARY TABLE describe_test (
id int primary key auto_increment,
name text not null,
hash blob
)
"#,
)
.await?;
let describe = conn
.describe("select nt.*, false from describe_test nt")
.await?;
assert_eq!(describe.result_columns[0].nullability, NonNull);
assert_eq!(describe.result_columns[0].type_info.type_name(), "INT");
assert_eq!(describe.result_columns[1].nullability, NonNull);
assert_eq!(describe.result_columns[1].type_info.type_name(), "TEXT");
assert_eq!(describe.result_columns[2].nullability, Nullable);
assert_eq!(describe.result_columns[2].type_info.type_name(), "TEXT");
assert_eq!(describe.result_columns[3].nullability, NonNull);
let bool_ty_name = describe.result_columns[3].type_info.type_name();
// MySQL 5.7, 8 and MariaDB 10.1 return BIG_INT, MariaDB 10.4 returns INT (optimization?)
assert!(
["BIG_INT", "INT"].contains(&bool_ty_name),
"type name returned: {}",
bool_ty_name
);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn pool_immediately_fails_with_db_error() -> anyhow::Result<()> {

View file

@ -141,6 +141,39 @@ async fn pool_smoke_test() -> anyhow::Result<()> {
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_describe() -> anyhow::Result<()> {
let mut conn = connect().await?;
let _ = conn
.execute(
r#"
CREATE TEMP TABLE describe_test (
id SERIAL primary key,
name text not null,
hash bytea
)
"#,
)
.await?;
let describe = conn
.describe("select nt.*, false from describe_test nt")
.await?;
assert_eq!(describe.result_columns[0].non_null, Some(true));
assert_eq!(describe.result_columns[0].type_info.type_name(), "INT4");
assert_eq!(describe.result_columns[1].non_null, Some(true));
assert_eq!(describe.result_columns[1].type_info.type_name(), "TEXT");
assert_eq!(describe.result_columns[2].non_null, Some(false));
assert_eq!(describe.result_columns[2].type_info.type_name(), "BYTEA");
assert_eq!(describe.result_columns[3].non_null, None);
assert_eq!(describe.result_columns[3].type_info.type_name(), "BOOL");
Ok(())
}
async fn connect() -> anyhow::Result<PgConnection> {
let _ = dotenv::dotenv();
let _ = env_logger::try_init();

View file

@ -4,10 +4,24 @@ fn ui_tests() {
if cfg!(feature = "postgres") {
t.compile_fail("tests/ui/postgres/*.rs");
// UI tests for column types that require gated features
if cfg!(not(feature = "chrono")) {
t.compile_fail("tests/ui/postgres/gated/chrono.rs");
}
if cfg!(not(feature = "uuid")) {
t.compile_fail("tests/ui/postgres/gated/uuid.rs");
}
}
if cfg!(feature = "mysql") {
t.compile_fail("tests/ui/mysql/*.rs");
// UI tests for column types that require gated features
if cfg!(not(feature = "chrono")) {
t.compile_fail("tests/ui/mysql/gated/chrono.rs");
}
}
t.compile_fail("tests/ui/*.rs");

View file

@ -0,0 +1,7 @@
fn main() {
let _ = sqlx::query!("select CONVERT(now(), DATE) date");
let _ = sqlx::query!("select CONVERT(now(), TIME) time");
let _ = sqlx::query!("select CONVERT(now(), DATETIME) datetime");
}

View file

@ -0,0 +1,23 @@
error: optional feature `chrono` required for type DATE of column #1 ("date")
--> $DIR/chrono.rs:2:13
|
2 | let _ = sqlx::query!("select CONVERT(now(), DATE) date");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
error: optional feature `chrono` required for type TIME of column #1 ("time")
--> $DIR/chrono.rs:4:13
|
4 | let _ = sqlx::query!("select CONVERT(now(), TIME) time");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
error: optional feature `chrono` required for type DATETIME of column #1 ("datetime")
--> $DIR/chrono.rs:6:13
|
6 | let _ = sqlx::query!("select CONVERT(now(), DATETIME) datetime");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

View file

@ -0,0 +1,17 @@
fn main() {
let _ = sqlx::query!("select now()::date");
let _ = sqlx::query!("select now()::time");
let _ = sqlx::query!("select now()::timestamp");
let _ = sqlx::query!("select now()::timestamptz");
let _ = sqlx::query!("select $1::date", ());
let _ = sqlx::query!("select $1::time", ());
let _ = sqlx::query!("select $1::timestamp", ());
let _ = sqlx::query!("select $1::timestamptz", ());
}

View file

@ -0,0 +1,63 @@
error: optional feature `chrono` required for type DATE of column #1 ("now")
--> $DIR/chrono.rs:2:13
|
2 | let _ = sqlx::query!("select now()::date");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: optional feature `chrono` required for type TIME of column #1 ("now")
--> $DIR/chrono.rs:4:13
|
4 | let _ = sqlx::query!("select now()::time");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: optional feature `chrono` required for type TIMESTAMP of column #1 ("now")
--> $DIR/chrono.rs:6:13
|
6 | let _ = sqlx::query!("select now()::timestamp");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: optional feature `chrono` required for type TIMESTAMPTZ of column #1 ("now")
--> $DIR/chrono.rs:8:13
|
8 | let _ = sqlx::query!("select now()::timestamptz");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: optional feature `chrono` required for type DATE of param #1
--> $DIR/chrono.rs:10:13
|
10 | let _ = sqlx::query!("select $1::date", ());
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: optional feature `chrono` required for type TIME of param #1
--> $DIR/chrono.rs:12:13
|
12 | let _ = sqlx::query!("select $1::time", ());
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: optional feature `chrono` required for type TIMESTAMP of param #1
--> $DIR/chrono.rs:14:13
|
14 | let _ = sqlx::query!("select $1::timestamp", ());
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: optional feature `chrono` required for type TIMESTAMPTZ of param #1
--> $DIR/chrono.rs:16:13
|
16 | let _ = sqlx::query!("select $1::timestamptz", ());
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,4 @@
fn main() {
let _ = sqlx::query!("select 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid");
let _ = sqlx::query!("select $1::uuid", ());
}

View file

@ -0,0 +1,15 @@
error: optional feature `uuid` required for type UUID of column #1 ("uuid")
--> $DIR/uuid.rs:2:13
|
2 | let _ = sqlx::query!("select 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: optional feature `uuid` required for type UUID of param #1
--> $DIR/uuid.rs:3:13
|
3 | let _ = sqlx::query!("select $1::uuid", ());
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,5 @@
fn main() {
// we're probably not going to get around to the geometric types anytime soon
let _ = sqlx::query!("select null::circle");
let _ = sqlx::query!("select $1::circle", panic!());
}

View file

@ -0,0 +1,15 @@
error: unsupported type CIRCLE of column #1 ("circle")
--> $DIR/unsupported-type.rs:3:13
|
3 | let _ = sqlx::query!("select null::circle");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: unsupported type CIRCLE for param #1
--> $DIR/unsupported-type.rs:4:13
|
4 | let _ = sqlx::query!("select $1::circle", panic!());
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)