mirror of
https://github.com/launchbadge/sqlx
synced 2024-11-10 06:24:16 +00:00
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:
parent
59cf900348
commit
4163388298
33 changed files with 654 additions and 64 deletions
35
.github/workflows/rust.yml
vendored
35
.github/workflows/rust.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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("");
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct TypeId(pub(crate) u32);
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
|
|
@ -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[]")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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[]")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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[]")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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[]")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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[]")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[]")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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[]")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<_>>>()?;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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<()> {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
|
|
7
tests/ui/mysql/gated/chrono.rs
Normal file
7
tests/ui/mysql/gated/chrono.rs
Normal 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");
|
||||
}
|
23
tests/ui/mysql/gated/chrono.stderr
Normal file
23
tests/ui/mysql/gated/chrono.stderr
Normal 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)
|
17
tests/ui/postgres/gated/chrono.rs
Normal file
17
tests/ui/postgres/gated/chrono.rs
Normal 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", ());
|
||||
}
|
63
tests/ui/postgres/gated/chrono.stderr
Normal file
63
tests/ui/postgres/gated/chrono.stderr
Normal 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)
|
4
tests/ui/postgres/gated/uuid.rs
Normal file
4
tests/ui/postgres/gated/uuid.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
fn main() {
|
||||
let _ = sqlx::query!("select 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid");
|
||||
let _ = sqlx::query!("select $1::uuid", ());
|
||||
}
|
15
tests/ui/postgres/gated/uuid.stderr
Normal file
15
tests/ui/postgres/gated/uuid.stderr
Normal 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)
|
5
tests/ui/postgres/unsupported-type.rs
Normal file
5
tests/ui/postgres/unsupported-type.rs
Normal 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!());
|
||||
}
|
15
tests/ui/postgres/unsupported-type.stderr
Normal file
15
tests/ui/postgres/unsupported-type.stderr
Normal 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)
|
Loading…
Reference in a new issue