这是indexloc提供的服务,不要输入任何密码
Skip to content

add uuid scalar type #148

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,46 @@ This changelog documents the changes between release versions.

## [Unreleased]

### Added

- Add uuid scalar type ([#148](https://github.com/hasura/ndc-mongodb/pull/148))

### Fixed

- Update dependencies to get fixes for reported security vulnerabilities ([#149](https://github.com/hasura/ndc-mongodb/pull/149))

#### UUID scalar type

Previously UUID values would show up in GraphQL as `BinData`. BinData is a generalized BSON type for binary data. It
doesn't provide a great interface for working with UUIDs because binary data must be given as a JSON object with binary
data in base64-encoding (while UUIDs are usually given in a specific hex-encoded string format), and there is also
a mandatory "subtype" field. For example a BinData value representing a UUID fetched via GraphQL looks like this:

```json
{ "base64": "QKaT0MAKQl2vXFNeN/3+nA==", "subType":"04" }
```

With this change UUID fields can use the new `uuid` type instead of `binData`. Values of type `uuid` are represented in
JSON as strings. The same value in a field with type `uuid` looks like this:

```json
"40a693d0-c00a-425d-af5c-535e37fdfe9c"
```

This means that you can now, for example, filter using string representations for UUIDs:

```gql
query {
posts(where: {id: {_eq: "40a693d0-c00a-425d-af5c-535e37fdfe9c"}}) {
title
}
}
```

Introspection has been updated so that database fields containing UUIDs will use the `uuid` type when setting up new
collections, or when re-introspecting after deleting the existing schema configuration. For migrating you may delete and
re-introspect, or edit schema files to change occurrences of `binData` to `uuid`.

#### Security Fixes

Rust dependencies have been updated to get fixes for these advisories:
Expand Down
10 changes: 8 additions & 2 deletions crates/cli/src/introspection/sampling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use configuration::{
Schema, WithName,
};
use futures_util::TryStreamExt;
use mongodb::bson::{doc, Bson, Document};
use mongodb::bson::{doc, spec::BinarySubtype, Binary, Bson, Document};
use mongodb_agent_common::mongodb::{CollectionTrait as _, DatabaseTrait};
use mongodb_support::{
aggregate::{Pipeline, Stage},
Expand Down Expand Up @@ -220,7 +220,13 @@ fn make_field_type(
Bson::Int32(_) => scalar(Int),
Bson::Int64(_) => scalar(Long),
Bson::Timestamp(_) => scalar(Timestamp),
Bson::Binary(_) => scalar(BinData),
Bson::Binary(Binary { subtype, .. }) => {
if *subtype == BinarySubtype::Uuid {
scalar(UUID)
} else {
scalar(BinData)
}
}
Bson::ObjectId(_) => scalar(ObjectId),
Bson::DateTime(_) => scalar(Date),
Bson::Symbol(_) => scalar(Symbol),
Expand Down
24 changes: 3 additions & 21 deletions crates/cli/src/introspection/type_unification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,9 @@ pub fn unify_type(type_a: Type, type_b: Type) -> Type {
// Scalar types unify if they are the same type, or if one is a superset of the other.
// If they are diffferent then the union is ExtendedJSON.
(Type::Scalar(scalar_a), Type::Scalar(scalar_b)) => {
if scalar_a == scalar_b || is_supertype(&scalar_a, &scalar_b) {
Type::Scalar(scalar_a)
} else if is_supertype(&scalar_b, &scalar_a) {
Type::Scalar(scalar_b)
} else {
Type::ExtendedJSON
}
BsonScalarType::common_supertype(scalar_a, scalar_b)
.map(Type::Scalar)
.unwrap_or(Type::ExtendedJSON)
}

// Object types unify if they have the same name.
Expand Down Expand Up @@ -192,20 +188,6 @@ pub fn unify_object_types(
merged_type_map.into_values().collect()
}

/// True iff we consider a to be a supertype of b.
///
/// Note that if you add more supertypes here then it is important to also update the custom
/// equality check in our tests in mongodb_agent_common::query::serialization::tests. Equality
/// needs to be transitive over supertypes, so for example if we have,
///
/// (Double, Int), (Decimal, Double)
///
/// then in addition to comparing ints to doubles, and doubles to decimals, we also need to compare
/// decimals to ints.
pub fn is_supertype(a: &BsonScalarType, b: &BsonScalarType) -> bool {
matches!((a, b), (Double, Int))
}
Comment on lines -205 to -207
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this to a method on BsonScalarType


#[cfg(test)]
mod tests {
use std::collections::{HashMap, HashSet};
Expand Down
18 changes: 5 additions & 13 deletions crates/cli/src/native_query/type_solver/simplify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ use mongodb_support::BsonScalarType;
use ndc_models::{FieldName, ObjectTypeName};
use nonempty::NonEmpty;

use crate::introspection::type_unification::is_supertype;

use crate::native_query::helpers::get_object_field_type;
use crate::native_query::type_constraint::Variance;
use crate::native_query::{
Expand Down Expand Up @@ -290,19 +288,13 @@ fn solve_scalar(
b: BsonScalarType,
) -> Result<TypeConstraint, Error> {
let solution = match variance {
Variance::Covariant => {
if a == b || is_supertype(&a, &b) {
Some(C::Scalar(a))
} else if is_supertype(&b, &a) {
Some(C::Scalar(b))
} else {
Some(C::Union([C::Scalar(a), C::Scalar(b)].into()))
}
}
Variance::Covariant => BsonScalarType::common_supertype(a, b)
.map(C::Scalar)
.or_else(|| Some(C::Union([C::Scalar(a), C::Scalar(b)].into()))),
Variance::Contravariant => {
if a == b || is_supertype(&a, &b) {
if a == b || BsonScalarType::is_supertype(a, b) {
Some(C::Scalar(b))
} else if is_supertype(&b, &a) {
} else if BsonScalarType::is_supertype(b, a) {
Some(C::Scalar(a))
} else {
None
Expand Down
22 changes: 21 additions & 1 deletion crates/integration-tests/src/tests/filtering.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use insta::assert_yaml_snapshot;
use ndc_test_helpers::{binop, field, query, query_request, target, variable};
use ndc_test_helpers::{binop, field, query, query_request, target, value, variable};

use crate::{connector::Connector, graphql_query, run_connector_query};

Expand Down Expand Up @@ -85,3 +85,23 @@ async fn filters_by_comparisons_on_elements_of_array_of_scalars_against_variable
);
Ok(())
}

#[tokio::test]
async fn filters_by_uuid() -> anyhow::Result<()> {
assert_yaml_snapshot!(
run_connector_query(
Connector::TestCases,
query_request().collection("uuids").query(
query()
.predicate(binop(
"_eq",
target!("uuid"),
value!("40a693d0-c00a-425d-af5c-535e37fdfe9c")
))
.fields([field!("name"), field!("uuid"), field!("uuid_as_string")]),
)
)
.await?
);
Ok(())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
source: crates/integration-tests/src/tests/filtering.rs
expression: "run_connector_query(Connector::TestCases,\nquery_request().collection(\"uuids\").query(query().predicate(binop(\"_eq\",\ntarget!(\"uuid\"),\nvalue!(\"40a693d0-c00a-425d-af5c-535e37fdfe9c\"))).fields([field!(\"name\"),\nfield!(\"uuid\"), field!(\"uuid_as_string\")]),)).await?"
---
- rows:
- name: peristeria elata
uuid: 40a693d0-c00a-425d-af5c-535e37fdfe9c
uuid_as_string: 40a693d0-c00a-425d-af5c-535e37fdfe9c
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ cc 21360610045c5a616b371fb8d5492eb0c22065d62e54d9c8a8761872e2e192f3 # shrinks to
cc 8842e7f78af24e19847be5d8ee3d47c547ef6c1bb54801d360a131f41a87f4fa
cc 2a192b415e5669716701331fe4141383a12ceda9acc9f32e4284cbc2ed6f2d8a # shrinks to bson = Document({"A": Document({"¡": JavaScriptCodeWithScope { code: "", scope: Document({"\0": Int32(-1)}) }})}), mode = Relaxed
cc 4c37daee6ab1e1bcc75b4089786253f29271d116a1785180560ca431d2b4a651 # shrinks to bson = Document({"0": Document({"A": Array([Int32(0), Decimal128(...)])})})
cc ad219d6630a8e9a386e734b6ba440577162cca8435c7685e32b574e9b1aa390e
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ pub enum BsonToJsonError {
#[error("error converting 64-bit floating point number from BSON to JSON: {0}")]
DoubleConversion(f64),

#[error("error converting UUID from BSON to JSON: {0}")]
UuidConversion(#[from] bson::uuid::Error),

#[error("input object of type {0:?} is missing a field, \"{1}\"")]
MissingObjectField(Type, String),

Expand Down Expand Up @@ -85,6 +88,7 @@ fn bson_scalar_to_json(
(BsonScalarType::Timestamp, Bson::Timestamp(v)) => {
Ok(to_value::<json_formats::Timestamp>(v.into())?)
}
(BsonScalarType::UUID, Bson::Binary(b)) => Ok(serde_json::to_value(b.to_uuid()?)?),
(BsonScalarType::BinData, Bson::Binary(b)) => {
Ok(to_value::<json_formats::BinData>(b.into())?)
}
Expand Down
61 changes: 35 additions & 26 deletions crates/mongodb-agent-common/src/query/serialization/json_to_bson.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,12 @@ pub fn json_to_bson(expected_type: &Type, value: Value) -> Result<Bson> {

/// Works like json_to_bson, but only converts BSON scalar types.
pub fn json_to_bson_scalar(expected_type: BsonScalarType, value: Value) -> Result<Bson> {
use BsonScalarType as S;
let result = match expected_type {
BsonScalarType::Double => Bson::Double(deserialize(expected_type, value)?),
BsonScalarType::Int => Bson::Int32(deserialize(expected_type, value)?),
BsonScalarType::Long => convert_long(&from_string(expected_type, value)?)?,
BsonScalarType::Decimal => Bson::Decimal128(
S::Double => Bson::Double(deserialize(expected_type, value)?),
S::Int => Bson::Int32(deserialize(expected_type, value)?),
S::Long => convert_long(&from_string(expected_type, value)?)?,
S::Decimal => Bson::Decimal128(
Decimal128::from_str(&from_string(expected_type, value.clone())?).map_err(|err| {
JsonToBsonError::ConversionErrorWithContext(
Type::Scalar(MongoScalarType::Bson(expected_type)),
Expand All @@ -84,37 +85,34 @@ pub fn json_to_bson_scalar(expected_type: BsonScalarType, value: Value) -> Resul
)
})?,
),
BsonScalarType::String => Bson::String(deserialize(expected_type, value)?),
BsonScalarType::Date => convert_date(&from_string(expected_type, value)?)?,
BsonScalarType::Timestamp => {
deserialize::<json_formats::Timestamp>(expected_type, value)?.into()
}
BsonScalarType::BinData => {
deserialize::<json_formats::BinData>(expected_type, value)?.into()
}
BsonScalarType::ObjectId => Bson::ObjectId(deserialize(expected_type, value)?),
BsonScalarType::Bool => match value {
S::String => Bson::String(deserialize(expected_type, value)?),
S::Date => convert_date(&from_string(expected_type, value)?)?,
S::Timestamp => deserialize::<json_formats::Timestamp>(expected_type, value)?.into(),
S::BinData => deserialize::<json_formats::BinData>(expected_type, value)?.into(),
S::UUID => convert_uuid(&from_string(expected_type, value)?)?,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry about the churn in this function. This is the only added line. The logic in other lines is unchanged.

S::ObjectId => Bson::ObjectId(deserialize(expected_type, value)?),
S::Bool => match value {
Value::Bool(b) => Bson::Boolean(b),
_ => incompatible_scalar_type(BsonScalarType::Bool, value)?,
_ => incompatible_scalar_type(S::Bool, value)?,
},
BsonScalarType::Null => match value {
S::Null => match value {
Value::Null => Bson::Null,
_ => incompatible_scalar_type(BsonScalarType::Null, value)?,
_ => incompatible_scalar_type(S::Null, value)?,
},
BsonScalarType::Undefined => match value {
S::Undefined => match value {
Value::Null => Bson::Undefined,
_ => incompatible_scalar_type(BsonScalarType::Undefined, value)?,
_ => incompatible_scalar_type(S::Undefined, value)?,
},
BsonScalarType::Regex => deserialize::<json_formats::Regex>(expected_type, value)?.into(),
BsonScalarType::Javascript => Bson::JavaScriptCode(deserialize(expected_type, value)?),
BsonScalarType::JavascriptWithScope => {
S::Regex => deserialize::<json_formats::Regex>(expected_type, value)?.into(),
S::Javascript => Bson::JavaScriptCode(deserialize(expected_type, value)?),
S::JavascriptWithScope => {
deserialize::<json_formats::JavaScriptCodeWithScope>(expected_type, value)?.into()
}
BsonScalarType::MinKey => Bson::MinKey,
BsonScalarType::MaxKey => Bson::MaxKey,
BsonScalarType::Symbol => Bson::Symbol(deserialize(expected_type, value)?),
S::MinKey => Bson::MinKey,
S::MaxKey => Bson::MaxKey,
S::Symbol => Bson::Symbol(deserialize(expected_type, value)?),
// dbPointer is deprecated
BsonScalarType::DbPointer => Err(JsonToBsonError::NotImplemented(expected_type))?,
S::DbPointer => Err(JsonToBsonError::NotImplemented(expected_type))?,
};
Ok(result)
}
Expand Down Expand Up @@ -191,6 +189,17 @@ fn convert_long(value: &str) -> Result<Bson> {
Ok(Bson::Int64(n))
}

fn convert_uuid(value: &str) -> Result<Bson> {
let uuid = bson::Uuid::parse_str(value).map_err(|err| {
JsonToBsonError::ConversionErrorWithContext(
Type::Scalar(MongoScalarType::Bson(BsonScalarType::UUID)),
value.into(),
err.into(),
)
})?;
Ok(bson::binary::Binary::from_uuid(uuid).into())
}

fn deserialize<T>(expected_type: BsonScalarType, value: Value) -> Result<T>
where
T: DeserializeOwned,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ fn bson_scalar_type_representation(bson_scalar_type: BsonScalarType) -> Option<T
BsonScalarType::Date => Some(TypeRepresentation::Timestamp), // Mongo Date is milliseconds since unix epoch
BsonScalarType::Timestamp => None, // Internal Mongo timestamp type
BsonScalarType::BinData => None,
BsonScalarType::UUID => Some(TypeRepresentation::String),
BsonScalarType::ObjectId => Some(TypeRepresentation::String), // Mongo ObjectId is usually expressed as a 24 char hex string (12 byte number)
BsonScalarType::Bool => Some(TypeRepresentation::Boolean),
BsonScalarType::Null => None,
Expand Down
15 changes: 14 additions & 1 deletion crates/mongodb-agent-common/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ pub enum Property {
},
#[serde(untagged)]
Scalar {
#[serde(rename = "bsonType", default = "default_bson_scalar_type")]
#[serde(
rename = "bsonType",
deserialize_with = "deserialize_scalar_bson_type",
default = "default_bson_scalar_type"
)]
bson_type: BsonScalarType,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
Expand All @@ -60,6 +64,15 @@ pub fn get_property_description(p: &Property) -> Option<String> {
}
}

fn deserialize_scalar_bson_type<'de, D>(deserializer: D) -> Result<BsonScalarType, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let value = BsonType::deserialize(deserializer)?;
value.try_into().map_err(D::Error::custom)
}

fn default_bson_scalar_type() -> BsonScalarType {
BsonScalarType::Undefined
}
Expand Down
Loading