mirror of
https://github.com/launchbadge/sqlx.git
synced 2025-10-04 08:15:39 +00:00
add support for building in "decoupled" mode
This commit is contained in:
parent
a9fb19b37d
commit
6913695588
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -1928,10 +1928,13 @@ dependencies = [
|
|||||||
"dotenv",
|
"dotenv",
|
||||||
"futures 0.3.4",
|
"futures 0.3.4",
|
||||||
"heck",
|
"heck",
|
||||||
"lazy_static",
|
"hex",
|
||||||
|
"once_cell",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"syn",
|
"syn",
|
||||||
"tokio 0.2.13",
|
"tokio 0.2.13",
|
||||||
|
@ -39,6 +39,9 @@ default = [ "macros", "runtime-async-std" ]
|
|||||||
macros = [ "sqlx-macros" ]
|
macros = [ "sqlx-macros" ]
|
||||||
tls = [ "sqlx-core/tls" ]
|
tls = [ "sqlx-core/tls" ]
|
||||||
|
|
||||||
|
# offline building support in `sqlx-macros`
|
||||||
|
offline = ["sqlx-macros/offline", "sqlx-core/offline"]
|
||||||
|
|
||||||
# intended mainly for CI and docs
|
# intended mainly for CI and docs
|
||||||
all = [ "tls", "all-database", "all-type" ]
|
all = [ "tls", "all-database", "all-type" ]
|
||||||
all-database = [ "mysql", "sqlite", "postgres" ]
|
all-database = [ "mysql", "sqlite", "postgres" ]
|
||||||
|
@ -32,6 +32,9 @@ runtime-tokio = [ "async-native-tls/runtime-tokio", "tokio" ]
|
|||||||
# intended for internal benchmarking, do not use
|
# intended for internal benchmarking, do not use
|
||||||
bench = []
|
bench = []
|
||||||
|
|
||||||
|
# support offline/decoupled building (enables serialization of `Describe`)
|
||||||
|
offline = ["serde"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-native-tls = { version = "0.3.2", default-features = false, optional = true }
|
async-native-tls = { version = "0.3.2", default-features = false, optional = true }
|
||||||
async-std = { version = "1.5.0", features = [ "unstable" ], optional = true }
|
async-std = { version = "1.5.0", features = [ "unstable" ], optional = true }
|
||||||
|
@ -7,6 +7,14 @@ use crate::database::Database;
|
|||||||
/// The return type of [`Executor::describe`].
|
/// The return type of [`Executor::describe`].
|
||||||
///
|
///
|
||||||
/// [`Executor::describe`]: crate::executor::Executor::describe
|
/// [`Executor::describe`]: crate::executor::Executor::describe
|
||||||
|
#[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "offline",
|
||||||
|
serde(bound(
|
||||||
|
serialize = "DB::TypeInfo: serde::Serialize, Column<DB>: serde::Serialize",
|
||||||
|
deserialize = "DB::TypeInfo: serde::de::DeserializeOwned, Column<DB>: serde::de::DeserializeOwned"
|
||||||
|
))
|
||||||
|
)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct Describe<DB>
|
pub struct Describe<DB>
|
||||||
where
|
where
|
||||||
@ -35,6 +43,14 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A single column of a result set.
|
/// A single column of a result set.
|
||||||
|
#[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "offline",
|
||||||
|
serde(bound(
|
||||||
|
serialize = "DB::TableId: serde::Serialize, DB::TypeInfo: serde::Serialize",
|
||||||
|
deserialize = "DB::TableId: serde::de::DeserializeOwned, DB::TypeInfo: serde::de::DeserializeOwned"
|
||||||
|
))
|
||||||
|
)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct Column<DB>
|
pub struct Column<DB>
|
||||||
where
|
where
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// https://dev.mysql.com/doc/dev/mysql-server/8.0.12/binary__log__types_8h.html
|
// 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
|
// https://mariadb.com/kb/en/library/resultset/#field-types
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct TypeId(pub u8);
|
pub struct TypeId(pub u8);
|
||||||
|
|
||||||
// https://github.com/google/mysql/blob/c01fc2134d439282a21a2ddf687566e198ddee28/include/mysql_com.h#L429
|
// https://github.com/google/mysql/blob/c01fc2134d439282a21a2ddf687566e198ddee28/include/mysql_com.h#L429
|
||||||
|
@ -4,6 +4,7 @@ use crate::mysql::protocol::{ColumnDefinition, FieldFlags, TypeId};
|
|||||||
use crate::types::TypeInfo;
|
use crate::types::TypeInfo;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
|
#[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct MySqlTypeInfo {
|
pub struct MySqlTypeInfo {
|
||||||
pub(crate) id: TypeId,
|
pub(crate) id: TypeId,
|
||||||
pub(crate) is_unsigned: bool,
|
pub(crate) is_unsigned: bool,
|
||||||
|
@ -2,6 +2,7 @@ use crate::postgres::types::try_resolve_type_name;
|
|||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct TypeId(pub(crate) u32);
|
pub struct TypeId(pub(crate) u32);
|
||||||
|
|
||||||
// DEVELOPER PRO TIP: find builtin type OIDs easily by grepping this file
|
// DEVELOPER PRO TIP: find builtin type OIDs easily by grepping this file
|
||||||
|
@ -9,6 +9,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
/// Type information for a Postgres SQL type.
|
/// Type information for a Postgres SQL type.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct PgTypeInfo {
|
pub struct PgTypeInfo {
|
||||||
pub(crate) id: Option<TypeId>,
|
pub(crate) id: Option<TypeId>,
|
||||||
pub(crate) name: SharedStr,
|
pub(crate) name: SharedStr,
|
||||||
@ -186,8 +187,38 @@ impl From<String> for SharedStr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<SharedStr> for String {
|
||||||
|
fn from(s: SharedStr) -> Self {
|
||||||
|
String::from(&*s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for SharedStr {
|
impl fmt::Display for SharedStr {
|
||||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
fmt.pad(self)
|
fmt.pad(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// manual impls because otherwise things get a little screwy with lifetimes
|
||||||
|
#[cfg(feature = "offline")]
|
||||||
|
impl<'de> serde::Deserialize<'de> for SharedStr {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, <D as serde::Deserializer<'de>>::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Ok(String::deserialize(deserializer)?.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "offline")]
|
||||||
|
impl serde::Serialize for SharedStr {
|
||||||
|
fn serialize<S>(
|
||||||
|
&self,
|
||||||
|
serializer: S,
|
||||||
|
) -> Result<<S as serde::Serializer>::Ok, <S as serde::Serializer>::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,6 +4,7 @@ use crate::types::TypeInfo;
|
|||||||
|
|
||||||
// https://www.sqlite.org/c3ref/c_blob.html
|
// https://www.sqlite.org/c3ref/c_blob.html
|
||||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
|
#[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub(crate) enum SqliteType {
|
pub(crate) enum SqliteType {
|
||||||
Integer = 1,
|
Integer = 1,
|
||||||
Float = 2,
|
Float = 2,
|
||||||
@ -16,6 +17,7 @@ pub(crate) enum SqliteType {
|
|||||||
|
|
||||||
// https://www.sqlite.org/datatype3.html#type_affinity
|
// https://www.sqlite.org/datatype3.html#type_affinity
|
||||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
|
#[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub(crate) enum SqliteTypeAffinity {
|
pub(crate) enum SqliteTypeAffinity {
|
||||||
Text,
|
Text,
|
||||||
Numeric,
|
Numeric,
|
||||||
@ -25,6 +27,7 @@ pub(crate) enum SqliteTypeAffinity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct SqliteTypeInfo {
|
pub struct SqliteTypeInfo {
|
||||||
pub(crate) r#type: SqliteType,
|
pub(crate) r#type: SqliteType,
|
||||||
pub(crate) affinity: Option<SqliteTypeAffinity>,
|
pub(crate) affinity: Option<SqliteTypeAffinity>,
|
||||||
|
@ -28,6 +28,14 @@ impl<'s> TryFrom<&'s String> for Url {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<url::Url> for Url {
|
||||||
|
type Error = url::ParseError;
|
||||||
|
|
||||||
|
fn try_from(value: url::Url) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Url(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Url {
|
impl Url {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn as_str(&self) -> &str {
|
pub(crate) fn as_str(&self) -> &str {
|
||||||
|
@ -18,32 +18,38 @@ proc-macro = true
|
|||||||
[features]
|
[features]
|
||||||
default = [ "runtime-async-std" ]
|
default = [ "runtime-async-std" ]
|
||||||
|
|
||||||
runtime-async-std = [ "sqlx/runtime-async-std", "async-std" ]
|
runtime-async-std = [ "sqlx-core/runtime-async-std", "async-std" ]
|
||||||
runtime-tokio = [ "sqlx/runtime-tokio", "tokio", "lazy_static" ]
|
runtime-tokio = [ "sqlx-core/runtime-tokio", "tokio", "once_cell" ]
|
||||||
|
|
||||||
|
# offline building support
|
||||||
|
offline = ["sqlx-core/offline", "serde", "serde_json", "hex", "sha2"]
|
||||||
|
|
||||||
# database
|
# database
|
||||||
mysql = [ "sqlx/mysql" ]
|
mysql = [ "sqlx-core/mysql" ]
|
||||||
postgres = [ "sqlx/postgres" ]
|
postgres = [ "sqlx-core/postgres" ]
|
||||||
sqlite = [ "sqlx/sqlite" ]
|
sqlite = [ "sqlx-core/sqlite" ]
|
||||||
|
|
||||||
# type
|
# type
|
||||||
bigdecimal = [ "sqlx/bigdecimal" ]
|
bigdecimal = [ "sqlx-core/bigdecimal" ]
|
||||||
chrono = [ "sqlx/chrono" ]
|
chrono = [ "sqlx-core/chrono" ]
|
||||||
time = [ "sqlx/time" ]
|
time = [ "sqlx-core/time" ]
|
||||||
ipnetwork = [ "sqlx/ipnetwork" ]
|
ipnetwork = [ "sqlx-core/ipnetwork" ]
|
||||||
uuid = [ "sqlx/uuid" ]
|
uuid = [ "sqlx-core/uuid" ]
|
||||||
json = [ "sqlx/json", "serde_json" ]
|
json = [ "sqlx-core/json", "serde_json" ]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-std = { version = "1.5.0", default-features = false, optional = true }
|
async-std = { version = "1.5.0", default-features = false, optional = true }
|
||||||
tokio = { version = "0.2.13", default-features = false, features = [ "rt-threaded" ], optional = true }
|
tokio = { version = "0.2.13", default-features = false, features = [ "rt-threaded" ], optional = true }
|
||||||
dotenv = { version = "0.15.0", default-features = false }
|
dotenv = { version = "0.15.0", default-features = false }
|
||||||
futures = { version = "0.3.4", default-features = false, features = [ "executor" ] }
|
futures = { version = "0.3.4", default-features = false, features = [ "executor" ] }
|
||||||
|
hex = { version = "0.4.2", optional = true }
|
||||||
heck = "0.3"
|
heck = "0.3"
|
||||||
proc-macro2 = { version = "1.0.9", default-features = false }
|
proc-macro2 = { version = "1.0.9", default-features = false }
|
||||||
sqlx = { version = "0.3.5", default-features = false, path = "../sqlx-core", package = "sqlx-core" }
|
sqlx-core = { version = "0.3.5", default-features = false, path = "../sqlx-core" }
|
||||||
|
serde = { version = "1.0", optional = true }
|
||||||
serde_json = { version = "1.0", features = [ "raw_value" ], optional = true }
|
serde_json = { version = "1.0", features = [ "raw_value" ], optional = true }
|
||||||
|
sha2 = { version = "0.8.1", optional = true }
|
||||||
syn = { version = "1.0.16", default-features = false, features = [ "full" ] }
|
syn = { version = "1.0.16", default-features = false, features = [ "full" ] }
|
||||||
quote = { version = "1.0.2", default-features = false }
|
quote = { version = "1.0.2", default-features = false }
|
||||||
url = { version = "2.1.1", default-features = false }
|
url = { version = "2.1.1", default-features = false }
|
||||||
lazy_static = { version = "1.4.0", optional = true }
|
once_cell = { version = "1.3", features = ["std"], optional = true }
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use sqlx::database::Database;
|
use sqlx_core::database::Database;
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@ -10,6 +10,7 @@ pub enum ParamChecking {
|
|||||||
pub trait DatabaseExt: Database {
|
pub trait DatabaseExt: Database {
|
||||||
const DATABASE_PATH: &'static str;
|
const DATABASE_PATH: &'static str;
|
||||||
const ROW_PATH: &'static str;
|
const ROW_PATH: &'static str;
|
||||||
|
const NAME: &'static str;
|
||||||
|
|
||||||
const PARAM_CHECKING: ParamChecking;
|
const PARAM_CHECKING: ParamChecking;
|
||||||
|
|
||||||
@ -34,23 +35,25 @@ macro_rules! impl_database_ext {
|
|||||||
$($(#[$meta:meta])? $ty:ty $(| $input:ty)?),*$(,)?
|
$($(#[$meta:meta])? $ty:ty $(| $input:ty)?),*$(,)?
|
||||||
},
|
},
|
||||||
ParamChecking::$param_checking:ident,
|
ParamChecking::$param_checking:ident,
|
||||||
feature-types: $name:ident => $get_gate:expr,
|
feature-types: $ty_info:ident => $get_gate:expr,
|
||||||
row = $row:path
|
row = $row:path,
|
||||||
|
name = $db_name:literal
|
||||||
) => {
|
) => {
|
||||||
impl $crate::database::DatabaseExt for $database {
|
impl $crate::database::DatabaseExt for $database {
|
||||||
const DATABASE_PATH: &'static str = stringify!($database);
|
const DATABASE_PATH: &'static str = stringify!($database);
|
||||||
const ROW_PATH: &'static str = stringify!($row);
|
const ROW_PATH: &'static str = stringify!($row);
|
||||||
const PARAM_CHECKING: $crate::database::ParamChecking = $crate::database::ParamChecking::$param_checking;
|
const PARAM_CHECKING: $crate::database::ParamChecking = $crate::database::ParamChecking::$param_checking;
|
||||||
|
const NAME: &'static str = $db_name;
|
||||||
|
|
||||||
fn param_type_for_id(info: &Self::TypeInfo) -> Option<&'static str> {
|
fn param_type_for_id(info: &Self::TypeInfo) -> Option<&'static str> {
|
||||||
match () {
|
match () {
|
||||||
$(
|
$(
|
||||||
$(#[$meta])?
|
$(#[$meta])?
|
||||||
_ if <$ty as sqlx::types::Type<$database>>::type_info() == *info => Some(input_ty!($ty $(, $input)?)),
|
_ if <$ty as sqlx_core::types::Type<$database>>::type_info() == *info => Some(input_ty!($ty $(, $input)?)),
|
||||||
)*
|
)*
|
||||||
$(
|
$(
|
||||||
$(#[$meta])?
|
$(#[$meta])?
|
||||||
_ if sqlx::types::TypeInfo::compatible(&<$ty as sqlx::types::Type<$database>>::type_info(), &info) => Some(input_ty!($ty $(, $input)?)),
|
_ if sqlx_core::types::TypeInfo::compatible(&<$ty as sqlx_core::types::Type<$database>>::type_info(), &info) => Some(input_ty!($ty $(, $input)?)),
|
||||||
)*
|
)*
|
||||||
_ => None
|
_ => None
|
||||||
}
|
}
|
||||||
@ -60,17 +63,17 @@ macro_rules! impl_database_ext {
|
|||||||
match () {
|
match () {
|
||||||
$(
|
$(
|
||||||
$(#[$meta])?
|
$(#[$meta])?
|
||||||
_ if <$ty as sqlx::types::Type<$database>>::type_info() == *info => return Some(stringify!($ty)),
|
_ if <$ty as sqlx_core::types::Type<$database>>::type_info() == *info => return Some(stringify!($ty)),
|
||||||
)*
|
)*
|
||||||
$(
|
$(
|
||||||
$(#[$meta])?
|
$(#[$meta])?
|
||||||
_ if sqlx::types::TypeInfo::compatible(&<$ty as sqlx::types::Type<$database>>::type_info(), &info) => return Some(stringify!($ty)),
|
_ if sqlx_core::types::TypeInfo::compatible(&<$ty as sqlx_core::types::Type<$database>>::type_info(), &info) => return Some(stringify!($ty)),
|
||||||
)*
|
)*
|
||||||
_ => None
|
_ => None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_feature_gate($name: &Self::TypeInfo) -> Option<&'static str> {
|
fn get_feature_gate($ty_info: &Self::TypeInfo) -> Option<&'static str> {
|
||||||
$get_gate
|
$get_gate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
impl_database_ext! {
|
impl_database_ext! {
|
||||||
sqlx::mysql::MySql {
|
sqlx_core::mysql::MySql {
|
||||||
u8,
|
u8,
|
||||||
u16,
|
u16,
|
||||||
u32,
|
u32,
|
||||||
@ -18,33 +18,34 @@ impl_database_ext! {
|
|||||||
Vec<u8>,
|
Vec<u8>,
|
||||||
|
|
||||||
#[cfg(all(feature = "chrono", not(feature = "time")))]
|
#[cfg(all(feature = "chrono", not(feature = "time")))]
|
||||||
sqlx::types::chrono::NaiveTime,
|
sqlx_core::types::chrono::NaiveTime,
|
||||||
|
|
||||||
#[cfg(all(feature = "chrono", not(feature = "time")))]
|
#[cfg(all(feature = "chrono", not(feature = "time")))]
|
||||||
sqlx::types::chrono::NaiveDate,
|
sqlx_core::types::chrono::NaiveDate,
|
||||||
|
|
||||||
#[cfg(all(feature = "chrono", not(feature = "time")))]
|
#[cfg(all(feature = "chrono", not(feature = "time")))]
|
||||||
sqlx::types::chrono::NaiveDateTime,
|
sqlx_core::types::chrono::NaiveDateTime,
|
||||||
|
|
||||||
#[cfg(all(feature = "chrono", not(feature = "time")))]
|
#[cfg(all(feature = "chrono", not(feature = "time")))]
|
||||||
sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>,
|
sqlx_core::types::chrono::DateTime<sqlx_core::types::chrono::Utc>,
|
||||||
|
|
||||||
#[cfg(feature = "time")]
|
#[cfg(feature = "time")]
|
||||||
sqlx::types::time::Time,
|
sqlx_core::types::time::Time,
|
||||||
|
|
||||||
#[cfg(feature = "time")]
|
#[cfg(feature = "time")]
|
||||||
sqlx::types::time::Date,
|
sqlx_core::types::time::Date,
|
||||||
|
|
||||||
#[cfg(feature = "time")]
|
#[cfg(feature = "time")]
|
||||||
sqlx::types::time::PrimitiveDateTime,
|
sqlx_core::types::time::PrimitiveDateTime,
|
||||||
|
|
||||||
#[cfg(feature = "time")]
|
#[cfg(feature = "time")]
|
||||||
sqlx::types::time::OffsetDateTime,
|
sqlx_core::types::time::OffsetDateTime,
|
||||||
|
|
||||||
#[cfg(feature = "bigdecimal")]
|
#[cfg(feature = "bigdecimal")]
|
||||||
sqlx::types::BigDecimal,
|
sqlx_core::types::BigDecimal,
|
||||||
},
|
},
|
||||||
ParamChecking::Weak,
|
ParamChecking::Weak,
|
||||||
feature-types: info => info.type_feature_gate(),
|
feature-types: info => info.type_feature_gate(),
|
||||||
row = sqlx::mysql::MySqlRow
|
row = sqlx_core::mysql::MySqlRow,
|
||||||
|
name = "MySQL/MariaDB"
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
impl_database_ext! {
|
impl_database_ext! {
|
||||||
sqlx::postgres::Postgres {
|
sqlx_core::postgres::Postgres {
|
||||||
bool,
|
bool,
|
||||||
String | &str,
|
String | &str,
|
||||||
i8,
|
i8,
|
||||||
@ -13,37 +13,37 @@ impl_database_ext! {
|
|||||||
Vec<u8> | &[u8],
|
Vec<u8> | &[u8],
|
||||||
|
|
||||||
#[cfg(feature = "uuid")]
|
#[cfg(feature = "uuid")]
|
||||||
sqlx::types::Uuid,
|
sqlx_core::types::Uuid,
|
||||||
|
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
sqlx::types::chrono::NaiveTime,
|
sqlx_core::types::chrono::NaiveTime,
|
||||||
|
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
sqlx::types::chrono::NaiveDate,
|
sqlx_core::types::chrono::NaiveDate,
|
||||||
|
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
sqlx::types::chrono::NaiveDateTime,
|
sqlx_core::types::chrono::NaiveDateTime,
|
||||||
|
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc> | sqlx::types::chrono::DateTime<_>,
|
sqlx_core::types::chrono::DateTime<sqlx_core::types::chrono::Utc> | sqlx_core::types::chrono::DateTime<_>,
|
||||||
|
|
||||||
#[cfg(feature = "time")]
|
#[cfg(feature = "time")]
|
||||||
sqlx::types::time::Time,
|
sqlx_core::types::time::Time,
|
||||||
|
|
||||||
#[cfg(feature = "time")]
|
#[cfg(feature = "time")]
|
||||||
sqlx::types::time::Date,
|
sqlx_core::types::time::Date,
|
||||||
|
|
||||||
#[cfg(feature = "time")]
|
#[cfg(feature = "time")]
|
||||||
sqlx::types::time::PrimitiveDateTime,
|
sqlx_core::types::time::PrimitiveDateTime,
|
||||||
|
|
||||||
#[cfg(feature = "time")]
|
#[cfg(feature = "time")]
|
||||||
sqlx::types::time::OffsetDateTime,
|
sqlx_core::types::time::OffsetDateTime,
|
||||||
|
|
||||||
#[cfg(feature = "bigdecimal")]
|
#[cfg(feature = "bigdecimal")]
|
||||||
sqlx::types::BigDecimal,
|
sqlx_core::types::BigDecimal,
|
||||||
|
|
||||||
#[cfg(feature = "ipnetwork")]
|
#[cfg(feature = "ipnetwork")]
|
||||||
sqlx::types::ipnetwork::IpNetwork,
|
sqlx_core::types::ipnetwork::IpNetwork,
|
||||||
|
|
||||||
#[cfg(feature = "json")]
|
#[cfg(feature = "json")]
|
||||||
serde_json::Value,
|
serde_json::Value,
|
||||||
@ -61,41 +61,42 @@ impl_database_ext! {
|
|||||||
|
|
||||||
|
|
||||||
#[cfg(feature = "uuid")]
|
#[cfg(feature = "uuid")]
|
||||||
Vec<sqlx::types::Uuid> | &[sqlx::types::Uuid],
|
Vec<sqlx_core::types::Uuid> | &[sqlx_core::types::Uuid],
|
||||||
|
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
Vec<sqlx::types::chrono::NaiveTime> | &[sqlx::types::sqlx::types::chrono::NaiveTime],
|
Vec<sqlx_core::types::chrono::NaiveTime> | &[sqlx_core::types::sqlx_core::types::chrono::NaiveTime],
|
||||||
|
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
Vec<sqlx::types::chrono::NaiveDate> | &[sqlx::types::chrono::NaiveDate],
|
Vec<sqlx_core::types::chrono::NaiveDate> | &[sqlx_core::types::chrono::NaiveDate],
|
||||||
|
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
Vec<sqlx::types::chrono::NaiveDateTime> | &[sqlx::types::chrono::NaiveDateTime],
|
Vec<sqlx_core::types::chrono::NaiveDateTime> | &[sqlx_core::types::chrono::NaiveDateTime],
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
// #[cfg(feature = "chrono")]
|
// #[cfg(feature = "chrono")]
|
||||||
// Vec<sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>> | &[sqlx::types::chrono::DateTime<_>],
|
// Vec<sqlx_core::types::chrono::DateTime<sqlx_core::types::chrono::Utc>> | &[sqlx_core::types::chrono::DateTime<_>],
|
||||||
|
|
||||||
#[cfg(feature = "time")]
|
#[cfg(feature = "time")]
|
||||||
Vec<sqlx::types::time::Time> | &[sqlx::types::time::Time],
|
Vec<sqlx_core::types::time::Time> | &[sqlx_core::types::time::Time],
|
||||||
|
|
||||||
#[cfg(feature = "time")]
|
#[cfg(feature = "time")]
|
||||||
Vec<sqlx::types::time::Date> | &[sqlx::types::time::Date],
|
Vec<sqlx_core::types::time::Date> | &[sqlx_core::types::time::Date],
|
||||||
|
|
||||||
#[cfg(feature = "time")]
|
#[cfg(feature = "time")]
|
||||||
Vec<sqlx::types::time::PrimitiveDateTime> | &[sqlx::types::time::PrimitiveDateTime],
|
Vec<sqlx_core::types::time::PrimitiveDateTime> | &[sqlx_core::types::time::PrimitiveDateTime],
|
||||||
|
|
||||||
#[cfg(feature = "time")]
|
#[cfg(feature = "time")]
|
||||||
Vec<sqlx::types::time::OffsetDateTime> | &[sqlx::types::time::OffsetDateTime],
|
Vec<sqlx_core::types::time::OffsetDateTime> | &[sqlx_core::types::time::OffsetDateTime],
|
||||||
|
|
||||||
#[cfg(feature = "bigdecimal")]
|
#[cfg(feature = "bigdecimal")]
|
||||||
Vec<sqlx::types::BigDecimal> | &[sqlx::types::BigDecimal],
|
Vec<sqlx_core::types::BigDecimal> | &[sqlx_core::types::BigDecimal],
|
||||||
|
|
||||||
#[cfg(feature = "ipnetwork")]
|
#[cfg(feature = "ipnetwork")]
|
||||||
Vec<sqlx::types::ipnetwork::IpNetwork> | &[sqlx::types::ipnetwork::IpNetwork],
|
Vec<sqlx_core::types::ipnetwork::IpNetwork> | &[sqlx_core::types::ipnetwork::IpNetwork],
|
||||||
|
|
||||||
},
|
},
|
||||||
ParamChecking::Strong,
|
ParamChecking::Strong,
|
||||||
feature-types: info => info.type_feature_gate(),
|
feature-types: info => info.type_feature_gate(),
|
||||||
row = sqlx::postgres::PgRow
|
row = sqlx_core::postgres::PgRow,
|
||||||
|
name = "PostgreSQL"
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
impl_database_ext! {
|
impl_database_ext! {
|
||||||
sqlx::sqlite::Sqlite {
|
sqlx_core::sqlite::Sqlite {
|
||||||
bool,
|
bool,
|
||||||
i32,
|
i32,
|
||||||
i64,
|
i64,
|
||||||
@ -10,5 +10,6 @@ impl_database_ext! {
|
|||||||
},
|
},
|
||||||
ParamChecking::Weak,
|
ParamChecking::Weak,
|
||||||
feature-types: _info => None,
|
feature-types: _info => None,
|
||||||
row = sqlx::sqlite::SqliteRow
|
row = sqlx_core::sqlite::SqliteRow,
|
||||||
|
name = "SQLite"
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,6 @@ use quote::quote;
|
|||||||
#[cfg(feature = "runtime-async-std")]
|
#[cfg(feature = "runtime-async-std")]
|
||||||
use async_std::task::block_on;
|
use async_std::task::block_on;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
type Error = Box<dyn std::error::Error>;
|
type Error = Box<dyn std::error::Error>;
|
||||||
@ -26,23 +24,6 @@ mod runtime;
|
|||||||
|
|
||||||
use query_macros::*;
|
use query_macros::*;
|
||||||
|
|
||||||
#[cfg(feature = "runtime-tokio")]
|
|
||||||
lazy_static::lazy_static! {
|
|
||||||
static ref BASIC_RUNTIME: tokio::runtime::Runtime = {
|
|
||||||
tokio::runtime::Builder::new()
|
|
||||||
.threaded_scheduler()
|
|
||||||
.enable_io()
|
|
||||||
.enable_time()
|
|
||||||
.build()
|
|
||||||
.expect("failed to build tokio runtime")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "runtime-tokio")]
|
|
||||||
fn block_on<F: std::future::Future>(future: F) -> F::Output {
|
|
||||||
BASIC_RUNTIME.enter(|| futures::executor::block_on(future))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn macro_result(tokens: proc_macro2::TokenStream) -> TokenStream {
|
fn macro_result(tokens: proc_macro2::TokenStream) -> TokenStream {
|
||||||
quote!(
|
quote!(
|
||||||
macro_rules! macro_result {
|
macro_rules! macro_result {
|
||||||
@ -52,76 +33,11 @@ fn macro_result(tokens: proc_macro2::TokenStream) -> TokenStream {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! async_macro (
|
#[proc_macro]
|
||||||
($db:ident, $input:ident: $ty:ty => $expr:expr) => {{
|
pub fn expand_query(input: TokenStream) -> TokenStream {
|
||||||
let $input = match syn::parse::<$ty>($input) {
|
let input = syn::parse_macro_input!(input as QueryMacroInput);
|
||||||
Ok(input) => input,
|
|
||||||
Err(e) => return macro_result(e.to_compile_error()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let res: Result<proc_macro2::TokenStream> = block_on(async {
|
match query_macros::expand_input(input) {
|
||||||
use sqlx::connection::Connect;
|
|
||||||
|
|
||||||
// If a .env file exists at CARGO_MANIFEST_DIR, load environment variables from this,
|
|
||||||
// otherwise fallback to default dotenv behaviour.
|
|
||||||
if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
|
|
||||||
let env_path = PathBuf::from(dir).join(".env");
|
|
||||||
if env_path.exists() {
|
|
||||||
dotenv::from_path(&env_path)
|
|
||||||
.map_err(|e| format!("failed to load environment from {:?}, {}", env_path, e))?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let db_url = Url::parse(&dotenv::var("DATABASE_URL").map_err(|_| "DATABASE_URL not set")?)?;
|
|
||||||
|
|
||||||
match db_url.scheme() {
|
|
||||||
#[cfg(feature = "sqlite")]
|
|
||||||
"sqlite" => {
|
|
||||||
let $db = sqlx::sqlite::SqliteConnection::connect(db_url.as_str())
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("failed to connect to database: {}", e))?;
|
|
||||||
|
|
||||||
$expr.await
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "sqlite"))]
|
|
||||||
"sqlite" => Err(format!(
|
|
||||||
"DATABASE_URL {} has the scheme of a SQLite database but the `sqlite` \
|
|
||||||
feature of sqlx was not enabled",
|
|
||||||
db_url
|
|
||||||
).into()),
|
|
||||||
#[cfg(feature = "postgres")]
|
|
||||||
"postgresql" | "postgres" => {
|
|
||||||
let $db = sqlx::postgres::PgConnection::connect(db_url.as_str())
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("failed to connect to database: {}", e))?;
|
|
||||||
|
|
||||||
$expr.await
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "postgres"))]
|
|
||||||
"postgresql" | "postgres" => Err(format!(
|
|
||||||
"DATABASE_URL {} has the scheme of a Postgres database but the `postgres` \
|
|
||||||
feature of sqlx was not enabled",
|
|
||||||
db_url
|
|
||||||
).into()),
|
|
||||||
#[cfg(feature = "mysql")]
|
|
||||||
"mysql" | "mariadb" => {
|
|
||||||
let $db = sqlx::mysql::MySqlConnection::connect(db_url.as_str())
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("failed to connect to database: {}", e))?;
|
|
||||||
|
|
||||||
$expr.await
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "mysql"))]
|
|
||||||
"mysql" | "mariadb" => Err(format!(
|
|
||||||
"DATABASE_URL {} has the scheme of a MySQL/MariaDB database but the `mysql` \
|
|
||||||
feature of sqlx was not enabled",
|
|
||||||
db_url
|
|
||||||
).into()),
|
|
||||||
scheme => Err(format!("unexpected scheme {:?} in DATABASE_URL {}", scheme, db_url).into()),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
match res {
|
|
||||||
Ok(ts) => ts.into(),
|
Ok(ts) => ts.into(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if let Some(parse_err) = e.downcast_ref::<syn::Error>() {
|
if let Some(parse_err) = e.downcast_ref::<syn::Error>() {
|
||||||
@ -132,61 +48,6 @@ macro_rules! async_macro (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
|
||||||
);
|
|
||||||
|
|
||||||
#[proc_macro]
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
pub fn query(input: TokenStream) -> TokenStream {
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
async_macro!(db, input: QueryMacroInput => expand_query(input, db, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro]
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
pub fn query_unchecked(input: TokenStream) -> TokenStream {
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
async_macro!(db, input: QueryMacroInput => expand_query(input, db, false))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro]
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
pub fn query_file(input: TokenStream) -> TokenStream {
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
async_macro!(db, input: QueryMacroInput => expand_query_file(input, db, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro]
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
pub fn query_file_unchecked(input: TokenStream) -> TokenStream {
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
async_macro!(db, input: QueryMacroInput => expand_query_file(input, db, false))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro]
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
pub fn query_as(input: TokenStream) -> TokenStream {
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
async_macro!(db, input: QueryAsMacroInput => expand_query_as(input, db, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro]
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
pub fn query_file_as(input: TokenStream) -> TokenStream {
|
|
||||||
async_macro!(db, input: QueryAsMacroInput => expand_query_file_as(input, db, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro]
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
pub fn query_as_unchecked(input: TokenStream) -> TokenStream {
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
async_macro!(db, input: QueryAsMacroInput => expand_query_as(input, db, false))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro]
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
pub fn query_file_as_unchecked(input: TokenStream) -> TokenStream {
|
|
||||||
async_macro!(db, input: QueryAsMacroInput => expand_query_file_as(input, db, false))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_derive(Encode, attributes(sqlx))]
|
#[proc_macro_derive(Encode, attributes(sqlx))]
|
||||||
|
@ -3,7 +3,7 @@ use syn::spanned::Spanned;
|
|||||||
use syn::Expr;
|
use syn::Expr;
|
||||||
|
|
||||||
use quote::{quote, quote_spanned, ToTokens};
|
use quote::{quote, quote_spanned, ToTokens};
|
||||||
use sqlx::describe::Describe;
|
use sqlx_core::describe::Describe;
|
||||||
|
|
||||||
use crate::database::{DatabaseExt, ParamChecking};
|
use crate::database::{DatabaseExt, ParamChecking};
|
||||||
use crate::query_macros::QueryMacroInput;
|
use crate::query_macros::QueryMacroInput;
|
||||||
@ -13,7 +13,6 @@ use crate::query_macros::QueryMacroInput;
|
|||||||
pub fn quote_args<DB: DatabaseExt>(
|
pub fn quote_args<DB: DatabaseExt>(
|
||||||
input: &QueryMacroInput,
|
input: &QueryMacroInput,
|
||||||
describe: &Describe<DB>,
|
describe: &Describe<DB>,
|
||||||
checked: bool,
|
|
||||||
) -> crate::Result<TokenStream> {
|
) -> crate::Result<TokenStream> {
|
||||||
let db_path = DB::db_path();
|
let db_path = DB::db_path();
|
||||||
|
|
||||||
@ -25,7 +24,7 @@ pub fn quote_args<DB: DatabaseExt>(
|
|||||||
|
|
||||||
let arg_name = &input.arg_names;
|
let arg_name = &input.arg_names;
|
||||||
|
|
||||||
let args_check = if checked && DB::PARAM_CHECKING == ParamChecking::Strong {
|
let args_check = if input.checked && DB::PARAM_CHECKING == ParamChecking::Strong {
|
||||||
describe
|
describe
|
||||||
.param_types
|
.param_types
|
||||||
.iter()
|
.iter()
|
||||||
|
175
sqlx-macros/src/query_macros/data.rs
Normal file
175
sqlx-macros/src/query_macros/data.rs
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
use sqlx_core::connection::{Connect, Connection};
|
||||||
|
use sqlx_core::database::Database;
|
||||||
|
use sqlx_core::describe::Describe;
|
||||||
|
use sqlx_core::executor::{Executor, RefExecutor};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
|
use crate::database::DatabaseExt;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use std::fs::File;
|
||||||
|
use syn::export::Span;
|
||||||
|
|
||||||
|
// TODO: enable serialization
|
||||||
|
#[cfg_attr(feature = "offline", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "offline",
|
||||||
|
serde(bound(
|
||||||
|
serialize = "Describe<DB>: serde::Serialize",
|
||||||
|
deserialize = "Describe<DB>: serde::de::DeserializeOwned"
|
||||||
|
))
|
||||||
|
)]
|
||||||
|
pub struct QueryData<DB: Database> {
|
||||||
|
pub(super) query: String,
|
||||||
|
pub(super) describe: Describe<DB>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DB: Database> QueryData<DB> {
|
||||||
|
pub async fn from_db(
|
||||||
|
conn: &mut impl Executor<Database = DB>,
|
||||||
|
query: &str,
|
||||||
|
) -> crate::Result<Self> {
|
||||||
|
Ok(QueryData {
|
||||||
|
query: query.into(),
|
||||||
|
describe: conn.describe(query).await?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "offline")]
|
||||||
|
pub mod offline {
|
||||||
|
use super::QueryData;
|
||||||
|
use std::fs::File;
|
||||||
|
|
||||||
|
use std::fmt::{self, Formatter};
|
||||||
|
|
||||||
|
use crate::database::DatabaseExt;
|
||||||
|
use proc_macro2::{Span, TokenStream};
|
||||||
|
use serde::de::{Deserializer, MapAccess, Visitor};
|
||||||
|
use sqlx_core::describe::Describe;
|
||||||
|
use sqlx_core::query::query;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct DynQueryData {
|
||||||
|
#[serde(skip)]
|
||||||
|
pub db_name: String,
|
||||||
|
pub query: String,
|
||||||
|
pub describe: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DynQueryData {
|
||||||
|
/// Find and deserialize the data table for this query from a shared `sqlx-data.json`
|
||||||
|
/// file. The expected structure is a JSON map keyed by the SHA-256 hash of queries in hex.
|
||||||
|
pub fn from_data_file(path: impl AsRef<Path>, query: &str) -> crate::Result<Self> {
|
||||||
|
serde_json::Deserializer::from_reader(
|
||||||
|
File::open(path.as_ref()).map_err(|e| {
|
||||||
|
format!("failed to open path {}: {}", path.as_ref().display(), e)
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
.deserialize_map(DataFileVisitor {
|
||||||
|
query,
|
||||||
|
hash: hash_string(query),
|
||||||
|
})
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DB: DatabaseExt> QueryData<DB>
|
||||||
|
where
|
||||||
|
Describe<DB>: serde::Serialize + serde::de::DeserializeOwned,
|
||||||
|
{
|
||||||
|
pub fn from_dyn_data(dyn_data: DynQueryData) -> crate::Result<Self> {
|
||||||
|
assert!(!dyn_data.db_name.is_empty());
|
||||||
|
if DB::NAME == dyn_data.db_name {
|
||||||
|
let describe: Describe<DB> = serde_json::from_value(dyn_data.describe)?;
|
||||||
|
Ok(QueryData {
|
||||||
|
query: dyn_data.query,
|
||||||
|
describe,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"expected query data for {}, got data for {}",
|
||||||
|
DB::NAME,
|
||||||
|
dyn_data.db_name
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_in(&self, dir: impl AsRef<Path>, input_span: Span) -> crate::Result<()> {
|
||||||
|
// we save under the hash of the span representation because that should be unique
|
||||||
|
// per invocation
|
||||||
|
let path = dir.as_ref().join(format!(
|
||||||
|
"query-{}.json",
|
||||||
|
hash_string(&format!("{:?}", input_span))
|
||||||
|
));
|
||||||
|
|
||||||
|
serde_json::to_writer_pretty(
|
||||||
|
File::create(&path)
|
||||||
|
.map_err(|e| format!("failed to open path {}: {}", path.display(), e))?,
|
||||||
|
self,
|
||||||
|
)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_string(query: &str) -> String {
|
||||||
|
// picked `sha2` because it's already in the dependency tree for both MySQL and Postgres
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
hex::encode(Sha256::digest(query.as_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// lazily deserializes only the `QueryData` for the query we're looking for
|
||||||
|
struct DataFileVisitor<'a> {
|
||||||
|
query: &'a str,
|
||||||
|
hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for DataFileVisitor<'_> {
|
||||||
|
type Value = DynQueryData;
|
||||||
|
|
||||||
|
fn expecting(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
write!(f, "expected map key {:?} or \"db\"", self.hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, <A as MapAccess<'de>>::Error>
|
||||||
|
where
|
||||||
|
A: MapAccess<'de>,
|
||||||
|
{
|
||||||
|
let mut db_name: Option<String> = None;
|
||||||
|
|
||||||
|
// unfortunately we can't avoid this copy because deserializing from `io::Read`
|
||||||
|
// doesn't support deserializing borrowed values
|
||||||
|
while let Some(key) = map.next_key::<String>()? {
|
||||||
|
// lazily deserialize the query data only
|
||||||
|
if key == "db" {
|
||||||
|
db_name = Some(map.next_value::<String>()?);
|
||||||
|
} else if key == self.hash {
|
||||||
|
let db_name = db_name.ok_or_else(|| {
|
||||||
|
serde::de::Error::custom("expected \"db\" key before query hash keys")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut query_data: DynQueryData = map.next_value()?;
|
||||||
|
|
||||||
|
return if query_data.query == self.query {
|
||||||
|
query_data.db_name = db_name;
|
||||||
|
Ok(query_data)
|
||||||
|
} else {
|
||||||
|
Err(serde::de::Error::custom(format_args!(
|
||||||
|
"hash collision for stored queries:\n{:?}\n{:?}",
|
||||||
|
self.query, query_data.query
|
||||||
|
)))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(serde::de::Error::custom(format_args!(
|
||||||
|
"failed to find data for query {}",
|
||||||
|
self.hash
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,171 +1,122 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
use proc_macro2::{Ident, Span};
|
use proc_macro2::{Ident, Span};
|
||||||
use quote::{format_ident, ToTokens};
|
use quote::{format_ident, ToTokens};
|
||||||
use syn::parse::{Parse, ParseStream};
|
use syn::parse::{Parse, ParseBuffer, ParseStream};
|
||||||
use syn::punctuated::Punctuated;
|
use syn::punctuated::Punctuated;
|
||||||
use syn::spanned::Spanned;
|
use syn::spanned::Spanned;
|
||||||
use syn::token::Group;
|
use syn::token::Group;
|
||||||
use syn::{Expr, ExprLit, ExprPath, Lit};
|
use syn::{Error, Expr, ExprLit, ExprPath, Lit, LitBool, LitStr, Token};
|
||||||
use syn::{ExprGroup, Token};
|
use syn::{ExprArray, ExprGroup, Type};
|
||||||
|
|
||||||
use sqlx::connection::Connection;
|
use sqlx_core::connection::Connection;
|
||||||
use sqlx::describe::Describe;
|
use sqlx_core::describe::Describe;
|
||||||
|
|
||||||
use crate::runtime::fs;
|
|
||||||
|
|
||||||
/// Macro input shared by `query!()` and `query_file!()`
|
/// Macro input shared by `query!()` and `query_file!()`
|
||||||
pub struct QueryMacroInput {
|
pub struct QueryMacroInput {
|
||||||
pub(super) source: String,
|
pub(super) src: String,
|
||||||
pub(super) source_span: Span,
|
pub(super) src_span: Span,
|
||||||
|
|
||||||
|
pub(super) data_src: DataSrc,
|
||||||
|
|
||||||
|
pub(super) record_type: RecordType,
|
||||||
|
|
||||||
// `arg0 .. argN` for N arguments
|
// `arg0 .. argN` for N arguments
|
||||||
pub(super) arg_names: Vec<Ident>,
|
pub(super) arg_names: Vec<Ident>,
|
||||||
pub(super) arg_exprs: Vec<Expr>,
|
pub(super) arg_exprs: Vec<Expr>,
|
||||||
|
|
||||||
|
pub(super) checked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QueryMacroInput {
|
enum QuerySrc {
|
||||||
fn from_exprs(input: ParseStream, mut args: impl Iterator<Item = Expr>) -> syn::Result<Self> {
|
String(String),
|
||||||
fn lit_err<T>(span: Span, unexpected: Expr) -> syn::Result<T> {
|
File(String),
|
||||||
Err(syn::Error::new(
|
}
|
||||||
span,
|
|
||||||
format!(
|
|
||||||
"expected string literal, got {}",
|
|
||||||
unexpected.to_token_stream()
|
|
||||||
),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
let (source, source_span) = match args.next() {
|
pub enum DataSrc {
|
||||||
Some(Expr::Lit(ExprLit {
|
Env(String),
|
||||||
lit: Lit::Str(sql), ..
|
DbUrl(String),
|
||||||
})) => (sql.value(), sql.span()),
|
File,
|
||||||
Some(Expr::Group(ExprGroup {
|
}
|
||||||
expr,
|
|
||||||
group_token: Group { span },
|
|
||||||
..
|
|
||||||
})) => {
|
|
||||||
// this duplication with the above is necessary because `expr` is `Box<Expr>` here
|
|
||||||
// which we can't directly pattern-match without `box_patterns`
|
|
||||||
match *expr {
|
|
||||||
Expr::Lit(ExprLit {
|
|
||||||
lit: Lit::Str(sql), ..
|
|
||||||
}) => (sql.value(), span),
|
|
||||||
other_expr => return lit_err(span, other_expr),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(other_expr) => return lit_err(other_expr.span(), other_expr),
|
|
||||||
None => return Err(input.error("expected SQL string literal")),
|
|
||||||
};
|
|
||||||
|
|
||||||
let arg_exprs: Vec<_> = args.collect();
|
pub enum RecordType {
|
||||||
let arg_names = (0..arg_exprs.len())
|
Given(Type),
|
||||||
.map(|i| format_ident!("arg{}", i))
|
Generated,
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
source,
|
|
||||||
source_span,
|
|
||||||
arg_exprs,
|
|
||||||
arg_names,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn expand_file_src(self) -> syn::Result<Self> {
|
|
||||||
let source = read_file_src(&self.source, self.source_span).await?;
|
|
||||||
|
|
||||||
Ok(Self { source, ..self })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run a parse/describe on the query described by this input and validate that it matches the
|
|
||||||
/// passed number of args
|
|
||||||
pub async fn describe_validate<C: Connection>(
|
|
||||||
&self,
|
|
||||||
conn: &mut C,
|
|
||||||
) -> crate::Result<Describe<C::Database>> {
|
|
||||||
let describe = conn
|
|
||||||
.describe(&*self.source)
|
|
||||||
.await
|
|
||||||
.map_err(|e| syn::Error::new(self.source_span, e))?;
|
|
||||||
|
|
||||||
if self.arg_names.len() != describe.param_types.len() {
|
|
||||||
return Err(syn::Error::new(
|
|
||||||
Span::call_site(),
|
|
||||||
format!(
|
|
||||||
"expected {} parameters, got {}",
|
|
||||||
describe.param_types.len(),
|
|
||||||
self.arg_names.len()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(describe)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parse for QueryMacroInput {
|
impl Parse for QueryMacroInput {
|
||||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
let args = Punctuated::<Expr, Token![,]>::parse_terminated(input)?.into_iter();
|
let mut query_src: Option<(QuerySrc, Span)> = None;
|
||||||
|
let mut data_src = DataSrc::Env("DATABASE_URL".into());
|
||||||
|
let mut args: Option<Vec<Expr>> = None;
|
||||||
|
let mut record_type = RecordType::Generated;
|
||||||
|
let mut checked = true;
|
||||||
|
|
||||||
Self::from_exprs(input, args)
|
let mut expect_comma = false;
|
||||||
|
|
||||||
|
while !input.is_empty() {
|
||||||
|
if expect_comma {
|
||||||
|
let _ = input.parse::<syn::token::Comma>()?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Macro input shared by `query_as!()` and `query_file_as!()`
|
let key: Ident = input.parse()?;
|
||||||
pub struct QueryAsMacroInput {
|
|
||||||
pub(super) as_ty: ExprPath,
|
|
||||||
pub(super) query_input: QueryMacroInput,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl QueryAsMacroInput {
|
let _ = input.parse::<syn::token::Eq>()?;
|
||||||
pub async fn expand_file_src(self) -> syn::Result<Self> {
|
|
||||||
Ok(Self {
|
if key == "source" {
|
||||||
query_input: self.query_input.expand_file_src().await?,
|
let lit_str = input.parse::<LitStr>()?;
|
||||||
..self
|
query_src = Some((QuerySrc::String(lit_str.value()), lit_str.span()));
|
||||||
|
} else if key == "source_file" {
|
||||||
|
let lit_str = input.parse::<LitStr>()?;
|
||||||
|
query_src = Some((QuerySrc::File(lit_str.value()), lit_str.span()));
|
||||||
|
} else if key == "args" {
|
||||||
|
let exprs = input.parse::<ExprArray>()?;
|
||||||
|
args = Some(exprs.elems.into_iter().collect())
|
||||||
|
} else if key == "record" {
|
||||||
|
record_type = RecordType::Given(input.parse()?);
|
||||||
|
} else if key == "checked" {
|
||||||
|
let lit_bool = input.parse::<LitBool>()?;
|
||||||
|
checked = lit_bool.value;
|
||||||
|
} else {
|
||||||
|
let message = format!("unexpected input key: {}", key);
|
||||||
|
return Err(syn::Error::new_spanned(key, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_comma = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (src, src_span) =
|
||||||
|
query_src.ok_or_else(|| input.error("expected `source` or `source_file` key"))?;
|
||||||
|
|
||||||
|
let arg_exprs = args.unwrap_or_default();
|
||||||
|
let arg_names = (0..arg_exprs.len())
|
||||||
|
.map(|i| format_ident!("arg{}", i))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(QueryMacroInput {
|
||||||
|
src: src.resolve(src_span)?,
|
||||||
|
src_span,
|
||||||
|
data_src,
|
||||||
|
record_type,
|
||||||
|
arg_names,
|
||||||
|
arg_exprs,
|
||||||
|
checked,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parse for QueryAsMacroInput {
|
impl QuerySrc {
|
||||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
/// If the query source is a file, read it to a string. Otherwise return the query string.
|
||||||
fn path_err<T>(span: Span, unexpected: Expr) -> syn::Result<T> {
|
fn resolve(self, source_span: Span) -> syn::Result<String> {
|
||||||
Err(syn::Error::new(
|
match self {
|
||||||
span,
|
QuerySrc::String(string) => Ok(string),
|
||||||
format!(
|
QuerySrc::File(file) => read_file_src(&file, source_span),
|
||||||
"expected path to a type, got {}",
|
|
||||||
unexpected.to_token_stream()
|
|
||||||
),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut args = Punctuated::<Expr, Token![,]>::parse_terminated(input)?.into_iter();
|
|
||||||
|
|
||||||
let as_ty = match args.next() {
|
|
||||||
Some(Expr::Path(path)) => path,
|
|
||||||
Some(Expr::Group(ExprGroup {
|
|
||||||
expr,
|
|
||||||
group_token: Group { span },
|
|
||||||
..
|
|
||||||
})) => {
|
|
||||||
// this duplication with the above is necessary because `expr` is `Box<Expr>` here
|
|
||||||
// which we can't directly pattern-match without `box_patterns`
|
|
||||||
match *expr {
|
|
||||||
Expr::Path(path) => path,
|
|
||||||
other_expr => return path_err(span, other_expr),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(other_expr) => return path_err(other_expr.span(), other_expr),
|
|
||||||
None => return Err(input.error("expected path to SQL file")),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(QueryAsMacroInput {
|
|
||||||
as_ty,
|
|
||||||
query_input: QueryMacroInput::from_exprs(input, args)?,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_file_src(source: &str, source_span: Span) -> syn::Result<String> {
|
fn read_file_src(source: &str, source_span: Span) -> syn::Result<String> {
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
let path = Path::new(source);
|
let path = Path::new(source);
|
||||||
@ -201,7 +152,7 @@ async fn read_file_src(source: &str, source_span: Span) -> syn::Result<String> {
|
|||||||
|
|
||||||
let file_path = base_dir_path.join(path);
|
let file_path = base_dir_path.join(path);
|
||||||
|
|
||||||
fs::read_to_string(&file_path).await.map_err(|e| {
|
fs::read_to_string(&file_path).map_err(|e| {
|
||||||
syn::Error::new(
|
syn::Error::new(
|
||||||
source_span,
|
source_span,
|
||||||
format!(
|
format!(
|
||||||
|
@ -1,68 +1,223 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
use std::env;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::{Ident, Span, TokenStream};
|
||||||
|
use syn::Type;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub use input::QueryMacroInput;
|
||||||
use quote::{format_ident, quote};
|
use quote::{format_ident, quote};
|
||||||
|
use sqlx_core::connection::Connect;
|
||||||
pub use input::{QueryAsMacroInput, QueryMacroInput};
|
use sqlx_core::connection::Connection;
|
||||||
pub use query::expand_query;
|
use sqlx_core::database::Database;
|
||||||
|
use sqlx_core::describe::Describe;
|
||||||
|
|
||||||
use crate::database::DatabaseExt;
|
use crate::database::DatabaseExt;
|
||||||
|
use crate::query_macros::data::QueryData;
|
||||||
|
use crate::query_macros::input::RecordType;
|
||||||
|
use crate::runtime::block_on;
|
||||||
|
|
||||||
use sqlx::connection::Connection;
|
// pub use query::expand_query;
|
||||||
use sqlx::database::Database;
|
|
||||||
|
|
||||||
mod args;
|
mod args;
|
||||||
|
mod data;
|
||||||
mod input;
|
mod input;
|
||||||
mod output;
|
mod output;
|
||||||
mod query;
|
// mod query;
|
||||||
|
|
||||||
pub async fn expand_query_file<C: Connection>(
|
pub fn expand_input(input: QueryMacroInput) -> crate::Result<TokenStream> {
|
||||||
input: QueryMacroInput,
|
let manifest_dir =
|
||||||
conn: C,
|
env::var("CARGO_MANIFEST_DIR").map_err(|_| "`CARGO_MANIFEST_DIR` must be set")?;
|
||||||
checked: bool,
|
|
||||||
) -> crate::Result<TokenStream>
|
// If a .env file exists at CARGO_MANIFEST_DIR, load environment variables from this,
|
||||||
where
|
// otherwise fallback to default dotenv behaviour.
|
||||||
C::Database: DatabaseExt + Sized,
|
let env_path = std::path::Path::new(&manifest_dir).join(".env");
|
||||||
<C::Database as Database>::TypeInfo: Display,
|
if env_path.exists() {
|
||||||
{
|
dotenv::from_path(&env_path)
|
||||||
expand_query(input.expand_file_src().await?, conn, checked).await
|
.map_err(|e| format!("failed to load environment from {:?}, {}", env_path, e))?
|
||||||
|
}
|
||||||
|
|
||||||
|
// if `dotenv` wasn't initialized by the above we make sure to do it here
|
||||||
|
match dotenv::var("DATABASE_URL").ok() {
|
||||||
|
Some(db_url) => expand_from_db(input, &db_url),
|
||||||
|
#[cfg(feature = "offline")]
|
||||||
|
None => {
|
||||||
|
let data_file_path = std::path::Path::new(&manifest_dir).join("sqlx-data.json");
|
||||||
|
|
||||||
|
if data_file_path.exists() {
|
||||||
|
expand_from_file(input, data_file_path)
|
||||||
|
} else {
|
||||||
|
Err(
|
||||||
|
"`DATABASE_URL` must be set, or `cargo sqlx prepare` must have been run \
|
||||||
|
and sqlx-data.json must exist, to use query macros"
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "offline"))]
|
||||||
|
None => Err("`DATABASE_URL` must be set to use query macros".into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn expand_query_as<C: Connection>(
|
fn expand_from_db(input: QueryMacroInput, db_url: &str) -> crate::Result<TokenStream> {
|
||||||
input: QueryAsMacroInput,
|
let db_url = Url::parse(db_url)?;
|
||||||
mut conn: C,
|
match db_url.scheme() {
|
||||||
checked: bool,
|
#[cfg(feature = "postgres")]
|
||||||
|
"postgres" | "postgresql" => {
|
||||||
|
let data = block_on(async {
|
||||||
|
let mut conn = sqlx_core::postgres::PgConnection::connect(db_url).await?;
|
||||||
|
QueryData::from_db(&mut conn, &input.src).await
|
||||||
|
})?;
|
||||||
|
|
||||||
|
expand_with_data(input, data)
|
||||||
|
},
|
||||||
|
#[cfg(not(feature = "postgres"))]
|
||||||
|
"postgres" | "postgresql" => Err(format!("database URL has the scheme of a PostgreSQL database but the `postgres` feature is not enabled").into()),
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
|
"mysql" | "mariadb" => {
|
||||||
|
let data = block_on(async {
|
||||||
|
let mut conn = sqlx_core::mysql::MySqlConnection::connect(db_url).await?;
|
||||||
|
QueryData::from_db(&mut conn, &input.src).await
|
||||||
|
})?;
|
||||||
|
|
||||||
|
expand_with_data(input, data)
|
||||||
|
},
|
||||||
|
#[cfg(not(feature = "mysql"))]
|
||||||
|
"mysql" | "mariadb" => Err(format!("database URL has the scheme of a MySQL/MariaDB database but the `mysql` feature is not enabled").into()),
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
"sqlite" => {
|
||||||
|
let data = block_on(async {
|
||||||
|
let mut conn = sqlx_core::sqlite::SqliteConnection::connect(db_url).await?;
|
||||||
|
QueryData::from_db(&mut conn, &input.src).await
|
||||||
|
})?;
|
||||||
|
|
||||||
|
expand_with_data(input, data)
|
||||||
|
},
|
||||||
|
#[cfg(not(feature = "sqlite"))]
|
||||||
|
"sqlite" => Err(format!("database URL has the scheme of a SQLite database but the `sqlite` feature is not enabled").into()),
|
||||||
|
scheme => Err(format!("unknown database URL scheme {:?}", scheme).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "offline")]
|
||||||
|
pub fn expand_from_file(input: QueryMacroInput, file: PathBuf) -> crate::Result<TokenStream> {
|
||||||
|
use data::offline::DynQueryData;
|
||||||
|
|
||||||
|
let query_data = DynQueryData::from_data_file(file, &input.src)?;
|
||||||
|
assert!(!query_data.db_name.is_empty());
|
||||||
|
|
||||||
|
match &*query_data.db_name {
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
sqlx_core::postgres::Postgres::NAME => expand_with_data(
|
||||||
|
input,
|
||||||
|
QueryData::<sqlx_core::postgres::Postgres>::from_dyn_data(query_data)?,
|
||||||
|
),
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
|
sqlx_core::mysql::MySql::NAME => expand_with_data(
|
||||||
|
input,
|
||||||
|
QueryData::<sqlx_core::mysql::MySql>::from_dyn_data(query_data)?,
|
||||||
|
),
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
sqlx_core::sqlite::Sqlite::NAME => expand_with_data(
|
||||||
|
input,
|
||||||
|
QueryData::<sqlx::sqlite::Sqlite>::from_dyn_data(query_data)?,
|
||||||
|
),
|
||||||
|
_ => Err(format!(
|
||||||
|
"found query data for {} but the feature for that database was not enabled",
|
||||||
|
query_data.db_name
|
||||||
|
)
|
||||||
|
.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// marker trait for `Describe` that lets us conditionally require it to be `Serialize + Deserialize`
|
||||||
|
#[cfg(feature = "offline")]
|
||||||
|
trait DescribeExt: serde::Serialize + serde::de::DeserializeOwned {}
|
||||||
|
|
||||||
|
#[cfg(feature = "offline")]
|
||||||
|
impl<DB: Database> DescribeExt for Describe<DB> where
|
||||||
|
Describe<DB>: serde::Serialize + serde::de::DeserializeOwned
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "offline"))]
|
||||||
|
trait DescribeExt {}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "offline"))]
|
||||||
|
impl<DB: Database> DescribeExt for Describe<DB> {}
|
||||||
|
|
||||||
|
fn expand_with_data<DB: DatabaseExt>(
|
||||||
|
input: QueryMacroInput,
|
||||||
|
data: QueryData<DB>,
|
||||||
) -> crate::Result<TokenStream>
|
) -> crate::Result<TokenStream>
|
||||||
where
|
where
|
||||||
C::Database: DatabaseExt + Sized,
|
Describe<DB>: DescribeExt,
|
||||||
<C::Database as Database>::TypeInfo: Display,
|
|
||||||
{
|
{
|
||||||
let describe = input.query_input.describe_validate(&mut conn).await?;
|
// validate at the minimum that our args match the query's input parameters
|
||||||
|
if input.arg_names.len() != data.describe.param_types.len() {
|
||||||
if describe.result_columns.is_empty() {
|
|
||||||
return Err(syn::Error::new(
|
return Err(syn::Error::new(
|
||||||
input.query_input.source_span,
|
Span::call_site(),
|
||||||
"query must output at least one column",
|
format!(
|
||||||
|
"expected {} parameters, got {}",
|
||||||
|
data.describe.param_types.len(),
|
||||||
|
input.arg_names.len()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let args_tokens = args::quote_args(&input.query_input, &describe, checked)?;
|
let args_tokens = args::quote_args(&input, &data.describe)?;
|
||||||
|
|
||||||
let query_args = format_ident!("query_args");
|
let query_args = format_ident!("query_args");
|
||||||
|
|
||||||
let columns = output::columns_to_rust(&describe)?;
|
let output = if data.describe.result_columns.is_empty() {
|
||||||
let output = output::quote_query_as::<C::Database>(
|
let db_path = DB::db_path();
|
||||||
&input.query_input.source,
|
let sql = &input.src;
|
||||||
&input.as_ty.path,
|
|
||||||
&query_args,
|
quote! {
|
||||||
&columns,
|
sqlx::query::<#db_path>(#sql).bind_all(#query_args)
|
||||||
checked,
|
}
|
||||||
|
} else {
|
||||||
|
let columns = output::columns_to_rust::<DB>(&data.describe)?;
|
||||||
|
|
||||||
|
let (out_ty, mut record_tokens) = match input.record_type {
|
||||||
|
RecordType::Generated => {
|
||||||
|
let record_name: Type = syn::parse_str("Record").unwrap();
|
||||||
|
|
||||||
|
let record_fields = columns.iter().map(
|
||||||
|
|&output::RustColumn {
|
||||||
|
ref ident,
|
||||||
|
ref type_,
|
||||||
|
}| quote!(#ident: #type_,),
|
||||||
);
|
);
|
||||||
|
|
||||||
let arg_names = &input.query_input.arg_names;
|
let record_tokens = quote! {
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct #record_name {
|
||||||
|
#(#record_fields)*
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(quote! {
|
(Cow::Owned(record_name), record_tokens)
|
||||||
|
}
|
||||||
|
RecordType::Given(ref out_ty) => (Cow::Borrowed(out_ty), quote!()),
|
||||||
|
};
|
||||||
|
|
||||||
|
record_tokens.extend(output::quote_query_as::<DB>(
|
||||||
|
&input,
|
||||||
|
&out_ty,
|
||||||
|
&query_args,
|
||||||
|
&columns,
|
||||||
|
));
|
||||||
|
|
||||||
|
record_tokens
|
||||||
|
};
|
||||||
|
|
||||||
|
let arg_names = &input.arg_names;
|
||||||
|
|
||||||
|
let ret_tokens = quote! {
|
||||||
macro_rules! macro_result {
|
macro_rules! macro_result {
|
||||||
(#($#arg_names:expr),*) => {{
|
(#($#arg_names:expr),*) => {{
|
||||||
use sqlx::arguments::Arguments as _;
|
use sqlx::arguments::Arguments as _;
|
||||||
@ -72,17 +227,14 @@ where
|
|||||||
#output
|
#output
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
})
|
};
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn expand_query_file_as<C: Connection>(
|
#[cfg(feature = "offline")]
|
||||||
input: QueryAsMacroInput,
|
{
|
||||||
conn: C,
|
let save_dir = env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target/sqlx".into());
|
||||||
checked: bool,
|
std::fs::create_dir_all(&save_dir);
|
||||||
) -> crate::Result<TokenStream>
|
data.save_in(save_dir, input.src_span)?;
|
||||||
where
|
}
|
||||||
C::Database: DatabaseExt + Sized,
|
|
||||||
<C::Database as Database>::TypeInfo: Display,
|
Ok(ret_tokens)
|
||||||
{
|
|
||||||
expand_query_as(input.expand_file_src().await?, conn, checked).await
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
use proc_macro2::{Ident, Span, TokenStream};
|
use proc_macro2::{Ident, Span, TokenStream};
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::Path;
|
use syn::{Path, Type};
|
||||||
|
|
||||||
use sqlx::describe::Describe;
|
use sqlx_core::describe::Describe;
|
||||||
|
|
||||||
use crate::database::DatabaseExt;
|
use crate::database::DatabaseExt;
|
||||||
|
|
||||||
|
use crate::query_macros::QueryMacroInput;
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
pub struct RustColumn {
|
pub struct RustColumn {
|
||||||
@ -98,11 +99,10 @@ pub fn columns_to_rust<DB: DatabaseExt>(describe: &Describe<DB>) -> crate::Resul
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn quote_query_as<DB: DatabaseExt>(
|
pub fn quote_query_as<DB: DatabaseExt>(
|
||||||
sql: &str,
|
input: &QueryMacroInput,
|
||||||
out_ty: &Path,
|
out_ty: &Type,
|
||||||
bind_args: &Ident,
|
bind_args: &Ident,
|
||||||
columns: &[RustColumn],
|
columns: &[RustColumn],
|
||||||
checked: bool,
|
|
||||||
) -> TokenStream {
|
) -> TokenStream {
|
||||||
let instantiations = columns.iter().enumerate().map(
|
let instantiations = columns.iter().enumerate().map(
|
||||||
|(
|
|(
|
||||||
@ -116,7 +116,7 @@ pub fn quote_query_as<DB: DatabaseExt>(
|
|||||||
// For "checked" queries, the macro checks these at compile time and using "try_get"
|
// For "checked" queries, the macro checks these at compile time and using "try_get"
|
||||||
// would also perform pointless runtime checks
|
// would also perform pointless runtime checks
|
||||||
|
|
||||||
if checked {
|
if input.checked {
|
||||||
quote!( #ident: row.try_get_unchecked::<#type_, _>(#i).try_unwrap_optional()? )
|
quote!( #ident: row.try_get_unchecked::<#type_, _>(#i).try_unwrap_optional()? )
|
||||||
} else {
|
} else {
|
||||||
quote!( #ident: row.try_get_unchecked(#i)? )
|
quote!( #ident: row.try_get_unchecked(#i)? )
|
||||||
@ -126,6 +126,7 @@ pub fn quote_query_as<DB: DatabaseExt>(
|
|||||||
|
|
||||||
let db_path = DB::db_path();
|
let db_path = DB::db_path();
|
||||||
let row_path = DB::row_path();
|
let row_path = DB::row_path();
|
||||||
|
let sql = &input.src;
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
sqlx::query::<#db_path>(#sql).bind_all(#bind_args).try_map(|row: #row_path| {
|
sqlx::query::<#db_path>(#sql).bind_all(#bind_args).try_map(|row: #row_path| {
|
||||||
|
@ -5,7 +5,7 @@ use proc_macro2::TokenStream;
|
|||||||
use syn::{Ident, Path};
|
use syn::{Ident, Path};
|
||||||
|
|
||||||
use quote::{format_ident, quote};
|
use quote::{format_ident, quote};
|
||||||
use sqlx::{connection::Connection, database::Database};
|
use sqlx_core::{connection::Connection, database::Database};
|
||||||
|
|
||||||
use super::{args, output, QueryMacroInput};
|
use super::{args, output, QueryMacroInput};
|
||||||
use crate::database::DatabaseExt;
|
use crate::database::DatabaseExt;
|
||||||
@ -22,7 +22,7 @@ where
|
|||||||
<C::Database as Database>::TypeInfo: Display,
|
<C::Database as Database>::TypeInfo: Display,
|
||||||
{
|
{
|
||||||
let describe = input.describe_validate(&mut conn).await?;
|
let describe = input.describe_validate(&mut conn).await?;
|
||||||
let sql = &input.source;
|
let sql = &input.src;
|
||||||
|
|
||||||
let args = args::quote_args(&input, &describe, checked)?;
|
let args = args::quote_args(&input, &describe, checked)?;
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ where
|
|||||||
return Ok(quote! {
|
return Ok(quote! {
|
||||||
macro_rules! macro_result {
|
macro_rules! macro_result {
|
||||||
(#($#arg_names:expr),*) => {{
|
(#($#arg_names:expr),*) => {{
|
||||||
use sqlx::arguments::Arguments as _;
|
use sqlx_core::arguments::Arguments as _;
|
||||||
|
|
||||||
#args
|
#args
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ where
|
|||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
macro_rules! macro_result {
|
macro_rules! macro_result {
|
||||||
(#($#arg_names:expr),*) => {{
|
(#($#arg_names:expr),*) => {{
|
||||||
use sqlx::arguments::Arguments as _;
|
use sqlx_core::arguments::Arguments as _;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct #record_type {
|
struct #record_type {
|
||||||
|
@ -5,7 +5,23 @@ compile_error!("one of 'runtime-async-std' or 'runtime-tokio' features must be e
|
|||||||
compile_error!("only one of 'runtime-async-std' or 'runtime-tokio' features must be enabled");
|
compile_error!("only one of 'runtime-async-std' or 'runtime-tokio' features must be enabled");
|
||||||
|
|
||||||
#[cfg(feature = "runtime-async-std")]
|
#[cfg(feature = "runtime-async-std")]
|
||||||
pub(crate) use async_std::fs;
|
pub(crate) use async_std::task::block_on;
|
||||||
|
|
||||||
#[cfg(feature = "runtime-tokio")]
|
#[cfg(feature = "runtime-tokio")]
|
||||||
pub(crate) use tokio::fs;
|
pub fn block_on<F: std::future::Future>(future: F) -> F::Output {
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use tokio::runtime::{self, Runtime};
|
||||||
|
|
||||||
|
// lazily initialize a global runtime once for multiple invocations of the macros
|
||||||
|
static RUNTIME: Lazy<Runtime> = Lazy::new(|| {
|
||||||
|
runtime::Builder::new()
|
||||||
|
// `.basic_scheduler()` requires calling `Runtime::block_on()` which needs mutability
|
||||||
|
.threaded_scheduler()
|
||||||
|
.enable_io()
|
||||||
|
.enable_time()
|
||||||
|
.build()
|
||||||
|
.expect("failed to initialize Tokio runtime")
|
||||||
|
});
|
||||||
|
|
||||||
|
RUNTIME.enter(|| futures::executor::block_on(future))
|
||||||
|
}
|
||||||
|
@ -122,14 +122,14 @@ macro_rules! query (
|
|||||||
($query:literal) => ({
|
($query:literal) => ({
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod _macro_result {
|
mod _macro_result {
|
||||||
$crate::sqlx_macros::query!($query);
|
$crate::sqlx_macros::expand_query!(source = $query);
|
||||||
}
|
}
|
||||||
macro_result!()
|
macro_result!()
|
||||||
});
|
});
|
||||||
($query:literal, $($args:expr),*$(,)?) => ({
|
($query:literal, $($args:expr),*$(,)?) => ({
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod _macro_result {
|
mod _macro_result {
|
||||||
$crate::sqlx_macros::query!($query, $($args),*);
|
$crate::sqlx_macros::expand_query!(source = $query, args = [$($args),*]);
|
||||||
}
|
}
|
||||||
macro_result!($($args),*)
|
macro_result!($($args),*)
|
||||||
})
|
})
|
||||||
@ -140,19 +140,17 @@ macro_rules! query (
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
|
||||||
macro_rules! query_unchecked (
|
macro_rules! query_unchecked (
|
||||||
// by emitting a macro definition from our proc-macro containing the result tokens,
|
|
||||||
// we no longer have a need for `proc-macro-hack`
|
|
||||||
($query:literal) => ({
|
($query:literal) => ({
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod _macro_result {
|
mod _macro_result {
|
||||||
$crate::sqlx_macros::query_unchecked!($query);
|
$crate::sqlx_macros::expand_query!(source = $query, checked = false);
|
||||||
}
|
}
|
||||||
macro_result!()
|
macro_result!()
|
||||||
});
|
});
|
||||||
($query:literal, $($args:expr),*$(,)?) => ({
|
($query:literal, $($args:expr),*$(,)?) => ({
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod _macro_result {
|
mod _macro_result {
|
||||||
$crate::sqlx_macros::query_unchecked!($query, $($args),*);
|
$crate::sqlx_macros::expand_query!(source = $query, args = [$($args),*], checked = false);
|
||||||
}
|
}
|
||||||
macro_result!($($args),*)
|
macro_result!($($args),*)
|
||||||
})
|
})
|
||||||
@ -203,17 +201,17 @@ macro_rules! query_unchecked (
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
|
||||||
macro_rules! query_file (
|
macro_rules! query_file (
|
||||||
($query:literal) => (#[allow(dead_code)]{
|
($path:literal) => (#[allow(dead_code)]{
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod _macro_result {
|
mod _macro_result {
|
||||||
$crate::sqlx_macros::query_file!($query);
|
$crate::sqlx_macros::expand_query!(source_file = $path);
|
||||||
}
|
}
|
||||||
macro_result!()
|
macro_result!()
|
||||||
});
|
});
|
||||||
($query:literal, $($args:expr),*$(,)?) => (#[allow(dead_code)]{
|
($path:literal, $($args:expr),*$(,)?) => (#[allow(dead_code)]{
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod _macro_result {
|
mod _macro_result {
|
||||||
$crate::sqlx_macros::query_file!($query, $($args),*);
|
$crate::sqlx_macros::expand_query!(source_file = $path, args = [$($args),*]);
|
||||||
}
|
}
|
||||||
macro_result!($($args),*)
|
macro_result!($($args),*)
|
||||||
})
|
})
|
||||||
@ -224,17 +222,17 @@ macro_rules! query_file (
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
|
||||||
macro_rules! query_file_unchecked (
|
macro_rules! query_file_unchecked (
|
||||||
($query:literal) => (#[allow(dead_code)]{
|
($path:literal) => (#[allow(dead_code)]{
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod _macro_result {
|
mod _macro_result {
|
||||||
$crate::sqlx_macros::query_file_unchecked!($query);
|
$crate::sqlx_macros::query_file_unchecked!(source_file = $path, checked = false);
|
||||||
}
|
}
|
||||||
macro_result!()
|
macro_result!()
|
||||||
});
|
});
|
||||||
($query:literal, $($args:expr),*$(,)?) => (#[allow(dead_code)]{
|
($path:literal, $($args:expr),*$(,)?) => (#[allow(dead_code)]{
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod _macro_result {
|
mod _macro_result {
|
||||||
$crate::sqlx_macros::query_file_unchecked!($query, $($args),*);
|
$crate::sqlx_macros::query_file_unchecked!(source_file = $path, args = [$($args),*], checked = false);
|
||||||
}
|
}
|
||||||
macro_result!($($args),*)
|
macro_result!($($args),*)
|
||||||
})
|
})
|
||||||
@ -298,14 +296,14 @@ macro_rules! query_as (
|
|||||||
($out_struct:path, $query:literal) => (#[allow(dead_code)] {
|
($out_struct:path, $query:literal) => (#[allow(dead_code)] {
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod _macro_result {
|
mod _macro_result {
|
||||||
$crate::sqlx_macros::query_as!($out_struct, $query);
|
$crate::sqlx_macros::expand_query!(record = $out_struct, source = $query);
|
||||||
}
|
}
|
||||||
macro_result!()
|
macro_result!()
|
||||||
});
|
});
|
||||||
($out_struct:path, $query:literal, $($args:expr),*$(,)?) => (#[allow(dead_code)] {
|
($out_struct:path, $query:literal, $($args:expr),*$(,)?) => (#[allow(dead_code)] {
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod _macro_result {
|
mod _macro_result {
|
||||||
$crate::sqlx_macros::query_as!($out_struct, $query, $($args),*);
|
$crate::sqlx_macros::expand_query!(record = $out_struct, source = $query, args = [$($args),*]);
|
||||||
}
|
}
|
||||||
macro_result!($($args),*)
|
macro_result!($($args),*)
|
||||||
})
|
})
|
||||||
@ -347,17 +345,17 @@ macro_rules! query_as (
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
|
||||||
macro_rules! query_file_as (
|
macro_rules! query_file_as (
|
||||||
($out_struct:path, $query:literal) => (#[allow(dead_code)] {
|
($out_struct:path, $path:literal) => (#[allow(dead_code)] {
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod _macro_result {
|
mod _macro_result {
|
||||||
$crate::sqlx_macros::query_file_as!($out_struct, $query);
|
$crate::sqlx_macros::expand_query!(record = $out_struct, source_file = $path);
|
||||||
}
|
}
|
||||||
macro_result!()
|
macro_result!()
|
||||||
});
|
});
|
||||||
($out_struct:path, $query:literal, $($args:tt),*$(,)?) => (#[allow(dead_code)] {
|
($out_struct:path, $path:literal, $($args:tt),*$(,)?) => (#[allow(dead_code)] {
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod _macro_result {
|
mod _macro_result {
|
||||||
$crate::sqlx_macros::query_file_as!($out_struct, $query, $($args),*);
|
$crate::sqlx_macros::expand_query!(record = $out_struct, source_file = $path, args = [$($args),*]);
|
||||||
}
|
}
|
||||||
macro_result!($($args),*)
|
macro_result!($($args),*)
|
||||||
})
|
})
|
||||||
@ -371,7 +369,7 @@ macro_rules! query_as_unchecked (
|
|||||||
($out_struct:path, $query:literal) => (#[allow(dead_code)] {
|
($out_struct:path, $query:literal) => (#[allow(dead_code)] {
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod _macro_result {
|
mod _macro_result {
|
||||||
$crate::sqlx_macros::query_as_unchecked!($out_struct, $query);
|
$crate::sqlx_macros::expand_query!(record = $out_struct, source = $query, checked = false);
|
||||||
}
|
}
|
||||||
macro_result!()
|
macro_result!()
|
||||||
});
|
});
|
||||||
@ -379,7 +377,7 @@ macro_rules! query_as_unchecked (
|
|||||||
($out_struct:path, $query:literal, $($args:expr),*$(,)?) => (#[allow(dead_code)] {
|
($out_struct:path, $query:literal, $($args:expr),*$(,)?) => (#[allow(dead_code)] {
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod _macro_result {
|
mod _macro_result {
|
||||||
$crate::sqlx_macros::query_as_unchecked!($out_struct, $query, $($args),*);
|
$crate::sqlx_macros::expand_query!(record = $out_struct, source = $query, args = [$($args),*], checked = false);
|
||||||
}
|
}
|
||||||
macro_result!($($args),*)
|
macro_result!($($args),*)
|
||||||
})
|
})
|
||||||
@ -391,18 +389,18 @@ macro_rules! query_as_unchecked (
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
|
||||||
macro_rules! query_file_as_unchecked (
|
macro_rules! query_file_as_unchecked (
|
||||||
($out_struct:path, $query:literal) => (#[allow(dead_code)] {
|
($out_struct:path, $path:literal) => (#[allow(dead_code)] {
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod _macro_result {
|
mod _macro_result {
|
||||||
$crate::sqlx_macros::query_file_as_unchecked!($out_struct, $query);
|
$crate::sqlx_macros::query_file_as_unchecked!(record = $out_struct, source_file = $path, checked = false);
|
||||||
}
|
}
|
||||||
macro_result!()
|
macro_result!()
|
||||||
});
|
});
|
||||||
|
|
||||||
($out_struct:path, $query:literal, $($args:tt),*$(,)?) => (#[allow(dead_code)] {
|
($out_struct:path, $path:literal, $($args:tt),*$(,)?) => (#[allow(dead_code)] {
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod _macro_result {
|
mod _macro_result {
|
||||||
$crate::sqlx_macros::query_file_as_unchecked!($out_struct, $query, $($args),*);
|
$crate::sqlx_macros::query_file_as_unchecked!(record = $out_struct, source_file = $path, args = [$($args),*], checked = false);
|
||||||
}
|
}
|
||||||
macro_result!($($args),*)
|
macro_result!($($args),*)
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user