Skip to content

Commit

Permalink
feat(qe): Allow quaint::Value to include metainformation about the co…
Browse files Browse the repository at this point in the history
…rresponding native database type (#4311)
  • Loading branch information
Miguel Fernández authored Oct 5, 2023
1 parent c815422 commit 6e26301
Show file tree
Hide file tree
Showing 57 changed files with 1,943 additions and 1,329 deletions.
8 changes: 0 additions & 8 deletions .github/workflows/query-engine-driver-adapters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,6 @@ jobs:
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: "Setup pnpm cache"
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: "Login to Docker Hub"
uses: docker/login-action@v2
continue-on-error: true
Expand Down
140 changes: 140 additions & 0 deletions quaint/quaint-test-macros/src/test_each_connector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use darling::FromMeta;
use once_cell::sync::Lazy;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quaint_test_setup::{ConnectorDefinition, Tags, CONNECTORS};
use quote::quote;
use std::str::FromStr;
use syn::{parse_macro_input, spanned::Spanned, AttributeArgs, Ident, ItemFn};

static TAGS_FILTER: Lazy<Tags> = Lazy::new(|| {
let tags_str = std::env::var("TEST_EACH_CONNECTOR_TAGS").ok();
let mut tags = Tags::empty();

if let Some(tags_str) = tags_str {
for tag_str in tags_str.split(',') {
let tag = Tags::from_str(tag_str).unwrap();
tags |= tag;
}
}

tags
});

#[derive(Debug, FromMeta)]
struct TestEachConnectorArgs {
/// If present, run only the tests for the connectors with any of the passed
/// in tags.
#[darling(default)]
tags: TagsWrapper,

/// Optional list of tags to ignore.
#[darling(default)]
ignore: TagsWrapper,
}

impl TestEachConnectorArgs {
fn connectors_to_test(&self) -> impl Iterator<Item = &ConnectorDefinition> {
CONNECTORS
.all()
.filter(move |connector| TAGS_FILTER.is_empty() || connector.tags.contains(*TAGS_FILTER))
.filter(move |connector| self.tags.0.is_empty() || connector.tags.intersects(self.tags.0))
.filter(move |connector| !connector.tags.intersects(self.ignore.0))
}
}

#[derive(Debug)]
struct TagsWrapper(Tags);

impl Default for TagsWrapper {
fn default() -> Self {
TagsWrapper(Tags::empty())
}
}

impl darling::FromMeta for TagsWrapper {
fn from_list(items: &[syn::NestedMeta]) -> Result<Self, darling::Error> {
let mut tags = Tags::empty();

for item in items {
match item {
syn::NestedMeta::Lit(syn::Lit::Str(s)) => {
let s = s.value();
let tag = Tags::from_str(&s)
.map_err(|err| darling::Error::unknown_value(&err.to_string()).with_span(&item.span()))?;
tags.insert(tag);
}
syn::NestedMeta::Lit(other) => {
return Err(darling::Error::unexpected_lit_type(other).with_span(&other.span()))
}
syn::NestedMeta::Meta(meta) => {
return Err(darling::Error::unsupported_shape("Expected string literal").with_span(&meta.span()))
}
}
}

Ok(TagsWrapper(tags))
}
}

#[allow(clippy::needless_borrow)]
pub fn test_each_connector_impl(attr: TokenStream, input: TokenStream) -> TokenStream {
let attributes_meta: syn::AttributeArgs = parse_macro_input!(attr as AttributeArgs);
let args = TestEachConnectorArgs::from_list(&attributes_meta);

let mut test_function = parse_macro_input!(input as ItemFn);
super::strip_test_attribute(&mut test_function);

let tests = match args {
Ok(args) => test_each_connector_async_wrapper_functions(&args, &test_function),
Err(err) => return err.write_errors().into(),
};

let output = quote! {
#(#tests)*

#test_function
};

output.into()
}

#[allow(clippy::needless_borrow)]
fn test_each_connector_async_wrapper_functions(
args: &TestEachConnectorArgs,
test_function: &ItemFn,
) -> Vec<proc_macro2::TokenStream> {
let test_fn_name = &test_function.sig.ident;
let mut tests = Vec::with_capacity(CONNECTORS.len());

let optional_unwrap = if super::function_returns_result(&test_function) {
Some(quote!(.unwrap()))
} else {
None
};

for connector in args.connectors_to_test() {
let connector_name = connector.name();
let feature_name = connector.feature_name();
let connector_test_fn_name = Ident::new(&format!("{}_on_{}", test_fn_name, connector_name), Span::call_site());

let conn_api_factory = Ident::new(connector.test_api(), Span::call_site());

let test = quote! {
#[test]
#[cfg(feature = #feature_name)]
fn #connector_test_fn_name() {
let fut = async {
let mut api = #conn_api_factory().await#optional_unwrap;
#test_fn_name(&mut api).await#optional_unwrap
};

quaint_test_setup::run_with_tokio(fut)
}
};

tests.push(test);
}

tests
}
2 changes: 1 addition & 1 deletion quaint/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,4 @@ pub use table::*;
pub use union::Union;
pub use update::*;
pub(crate) use values::Params;
pub use values::{IntoRaw, Raw, Value, Values};
pub use values::{IntoRaw, Raw, Value, ValueType, Values};
2 changes: 1 addition & 1 deletion quaint/src/ast/column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ impl<'a> Column<'a> {
/// Sets whether the column is selected.
///
/// On Postgres, this defines whether an enum column should be casted to `TEXT` when rendered.
///
///
/// Since enums are user-defined custom types, `tokio-postgres` fires an additional query
/// when selecting columns of type enum to know which custom type the column refers to.
/// Casting the enum column to `TEXT` avoid this roundtrip since `TEXT` is a builtin type.
Expand Down
7 changes: 5 additions & 2 deletions quaint/src/ast/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ impl<'a> EnumVariant<'a> {
}

pub fn into_text(self) -> Value<'a> {
Value::Text(Some(self.0))
Value::text(self.0)
}

pub fn into_enum(self, name: Option<EnumName<'a>>) -> Value<'a> {
Value::Enum(Some(self), name)
match name {
Some(name) => Value::enum_variant_with_name(self.0, name),
None => Value::enum_variant(self.0),
}
}
}

Expand Down
20 changes: 16 additions & 4 deletions quaint/src/ast/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ impl<'a> Expression<'a> {

pub(crate) fn is_json_expr(&self) -> bool {
match &self.kind {
ExpressionKind::Parameterized(Value::Json(_)) => true,
ExpressionKind::Parameterized(Value {
typed: ValueType::Json(_),
..
}) => true,

ExpressionKind::Value(expr) => expr.is_json_value(),

Expand All @@ -58,7 +61,10 @@ impl<'a> Expression<'a> {

pub(crate) fn is_json_value(&self) -> bool {
match &self.kind {
ExpressionKind::Parameterized(Value::Json(_)) => true,
ExpressionKind::Parameterized(Value {
typed: ValueType::Json(_),
..
}) => true,

ExpressionKind::Value(expr) => expr.is_json_value(),
_ => false,
Expand All @@ -69,7 +75,10 @@ impl<'a> Expression<'a> {

pub(crate) fn into_json_value(self) -> Option<serde_json::Value> {
match self.kind {
ExpressionKind::Parameterized(Value::Json(json_val)) => json_val,
ExpressionKind::Parameterized(Value {
typed: ValueType::Json(json_val),
..
}) => json_val,

ExpressionKind::Value(expr) => expr.into_json_value(),
_ => None,
Expand Down Expand Up @@ -217,7 +226,10 @@ pub enum ExpressionKind<'a> {
impl<'a> ExpressionKind<'a> {
pub(crate) fn is_xml_value(&self) -> bool {
match self {
Self::Parameterized(Value::Xml(_)) => true,
Self::Parameterized(Value {
typed: ValueType::Xml(_),
..
}) => true,
Self::Value(expr) => expr.is_xml_value(),
_ => false,
}
Expand Down
4 changes: 2 additions & 2 deletions quaint/src/ast/function/row_to_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ pub struct RowToJson<'a> {
/// let result = conn.select(select).await?;
///
/// assert_eq!(
/// Value::Json(Some(serde_json::json!({
/// Value::json(serde_json::json!({
/// "toto": "hello_world"
/// }))),
/// })),
/// result.into_single().unwrap()[0]
/// );
/// # Ok(())
Expand Down
Loading

0 comments on commit 6e26301

Please sign in to comment.