Skip to content

Commit

Permalink
qe: Fix nested objects with $type key in JSON protocol
Browse files Browse the repository at this point in the history
Introduce another special value to JSON protocol, `"$type": "Raw"`.
When encountered, no other nested `$type` keys would be interpreted as
special and will be written to DB as is. Main usecase is JSON column
values with user-provided `$type` keys.

This is an alternative to #4668, that might look cleaner on the engine
side.

Part of the fix for prisma/prisma#21454, will require client adjustments
as well.
  • Loading branch information
Sergey Tatarintsev committed Jan 24, 2024
1 parent c1c3774 commit 454abb7
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod prisma_18517;
mod prisma_20799;
mod prisma_21182;
mod prisma_21369;
mod prisma_21454;
mod prisma_21901;
mod prisma_22298;
mod prisma_5952;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use query_engine_tests::*;

#[test_suite(schema(schema), capabilities(Json))]
mod prisma_21454 {

fn schema() -> String {
let schema = indoc! {
r#"
model Model {
#id(id, String, @id)
json Json
}
"#
};

schema.to_owned()
}

#[connector_test]
async fn dollar_type_in_json(runner: Runner) -> TestResult<()> {
let res = runner
.query_json(
r#"{
"modelName": "Model",
"action": "createOne",
"query": {
"selection": { "json": true },
"arguments": {
"data": {
"id": "123",
"json": { "$type": "Raw", "value": {"$type": "Something" } }
}
}
}
}"#,
)
.await?;

res.assert_success();

insta::assert_snapshot!(res.to_string(), @r###"{"data":{"createOneModel":{"json":{"$type":"Json","value":"{\"$type\":\"Something\"}"}}}}"###);

Ok(())
}

#[connector_test]
async fn nested_dollar_type_in_json(runner: Runner) -> TestResult<()> {
let res = runner
.query_json(
r#"{
"modelName": "Model",
"action": "createOne",
"query": {
"selection": { "json": true },
"arguments": {
"data": {
"id": "123",
"json": {
"something": { "$type": "Raw", "value": {"$type": "Something" } }
}
}
}
}
}"#,
)
.await?;

res.assert_success();

insta::assert_snapshot!(res.to_string(), @r###"{"data":{"createOneModel":{"json":{"$type":"Json","value":"{\"something\":{\"$type\":\"Something\"}}"}}}}"###);

Ok(())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ mod json {

insta::assert_snapshot!(
res,
@r###"{"data":{"findManyTestModel":[{"json":null},{"json":"null"}]}}"###
@r###"{"data":{"findManyTestModel":[{"json":null},{"json":null}]}}"###
);
}
query_engine_tests::EngineProtocol::Json => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ impl<'a, 'b> FieldTypeInferrer<'a, 'b> {
None => InferredType::Unknown,
}
}
ArgumentValue::FieldRef(_) => unreachable!(),
ArgumentValue::FieldRef(_) | ArgumentValue::Raw(_) => unreachable!(),
}
}

Expand Down
1 change: 1 addition & 0 deletions query-engine/core/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod custom_types {
pub const JSON: &str = "Json";
pub const ENUM: &str = "Enum";
pub const FIELD_REF: &str = "FieldRef";
pub const RAW: &str = "Raw";

pub fn make_object(typ: &str, value: PrismaValue) -> PrismaValue {
PrismaValue::Object(vec![make_type_pair(typ), make_value_pair(value)])
Expand Down
6 changes: 6 additions & 0 deletions query-engine/core/src/query_document/argument_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub enum ArgumentValue {
Scalar(PrismaValue),
Object(ArgumentValueObject),
List(Vec<ArgumentValue>),
Raw(serde_json::Value),
FieldRef(ArgumentValueObject),
}

Expand Down Expand Up @@ -46,6 +47,10 @@ impl ArgumentValue {
Self::Scalar(PrismaValue::Json(str))
}

pub fn raw(value: serde_json::Value) -> Self {
Self::Raw(value)
}

pub fn bytes(bytes: Vec<u8>) -> Self {
Self::Scalar(PrismaValue::Bytes(bytes))
}
Expand Down Expand Up @@ -76,6 +81,7 @@ impl ArgumentValue {
pub(crate) fn should_be_parsed_as_json(&self) -> bool {
match self {
ArgumentValue::Object(_) => true,
ArgumentValue::Raw(_) => true,
ArgumentValue::List(l) => l.iter().all(|v| v.should_be_parsed_as_json()),
ArgumentValue::Scalar(pv) => !matches!(pv, PrismaValue::Enum(_) | PrismaValue::Json(_)),
ArgumentValue::FieldRef(_) => false,
Expand Down
1 change: 1 addition & 0 deletions query-engine/core/src/query_document/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,7 @@ pub(crate) mod conversions {
format!("({})", itertools::join(v.iter().map(argument_value_to_type_name), ", "))
}
ArgumentValue::FieldRef(_) => "FieldRef".to_string(),
ArgumentValue::Raw(_) => "JSON".to_string(),
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ impl<'a> JsonProtocolAdapter<'a> {
.map(ArgumentValue::float)
.map_err(|_| build_err())
}

Some(custom_types::RAW) => {
let value = obj.get(custom_types::VALUE).ok_or_else(build_err)?;
Ok(ArgumentValue::raw(value.clone()))
}
Some(custom_types::BYTES) => {
let value = obj
.get(custom_types::VALUE)
Expand Down Expand Up @@ -1288,10 +1293,10 @@ mod tests {
assert_debug_snapshot!(operation.arguments()[0].1, @r###"
Object(
{
"x": Scalar(
Json(
"{ \"$type\": \"foo\", \"value\": \"bar\" }",
),
"x": Json(
RawJson {
value: "{ \"$type\": \"foo\", \"value\": \"bar\" }",
},
),
},
)
Expand Down

0 comments on commit 454abb7

Please sign in to comment.