From 816142b8110c62b5c55498a871f6de825fae5267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Houl=C3=A9?= Date: Wed, 26 Feb 2020 16:59:40 +0100 Subject: [PATCH 01/15] Test mapping of mysql native types --- Cargo.lock | 2 + introspection-engine/core/src/lib.rs | 7 + introspection-engine/core/src/rpc.rs | 12 +- query-engine/prisma/Cargo.toml | 2 + query-engine/prisma/src/tests.rs | 1 + query-engine/prisma/src/tests/test_api.rs | 4 + .../prisma/src/tests/type_mappings.rs | 2 + .../src/tests/type_mappings/mysql_types.rs | 236 ++++++++++++++++++ .../src/tests/type_mappings/test_api.rs | 136 ++++++++++ 9 files changed, 396 insertions(+), 6 deletions(-) create mode 100644 introspection-engine/core/src/lib.rs create mode 100644 query-engine/prisma/src/tests/type_mappings.rs create mode 100644 query-engine/prisma/src/tests/type_mappings/mysql_types.rs create mode 100644 query-engine/prisma/src/tests/type_mappings/test_api.rs diff --git a/Cargo.lock b/Cargo.lock index 96876295449d..036557771deb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1950,11 +1950,13 @@ dependencies = [ "hyper", "indexmap", "indoc", + "introspection-core", "itertools", "log", "migration-connector", "migration-core", "once_cell", + "pretty_assertions", "prisma-inflector", "prisma-models", "quaint", diff --git a/introspection-engine/core/src/lib.rs b/introspection-engine/core/src/lib.rs new file mode 100644 index 000000000000..4b1a69169d4b --- /dev/null +++ b/introspection-engine/core/src/lib.rs @@ -0,0 +1,7 @@ +mod command_error; +mod error; +mod error_rendering; +mod rpc; + +pub use error::Error; +pub use rpc::RpcImpl; diff --git a/introspection-engine/core/src/rpc.rs b/introspection-engine/core/src/rpc.rs index f5189c1b0fc0..390aea6c3d98 100644 --- a/introspection-engine/core/src/rpc.rs +++ b/introspection-engine/core/src/rpc.rs @@ -26,7 +26,7 @@ pub trait Rpc { fn introspect(&self, input: IntrospectionInput) -> RpcFutureResult; } -pub(crate) struct RpcImpl; +pub struct RpcImpl; impl Rpc for RpcImpl { fn list_databases(&self, input: IntrospectionInput) -> RpcFutureResult> { @@ -47,7 +47,7 @@ impl Rpc for RpcImpl { } impl RpcImpl { - pub(crate) fn new() -> Self { + pub fn new() -> Self { RpcImpl } @@ -63,7 +63,7 @@ impl RpcImpl { Ok(Box::new(SqlIntrospectionConnector::new(&url).await?)) } - pub(crate) async fn introspect_internal(schema: String) -> RpcResult { + pub async fn introspect_internal(schema: String) -> RpcResult { let config = datamodel::parse_configuration(&schema).map_err(Error::from)?; let url = config .datasources @@ -85,17 +85,17 @@ impl RpcImpl { } } - pub(crate) async fn list_databases_internal(schema: String) -> RpcResult> { + pub async fn list_databases_internal(schema: String) -> RpcResult> { let connector = RpcImpl::load_connector(&schema).await?; Ok(connector.list_databases().await.map_err(Error::from)?) } - pub(crate) async fn get_database_description(schema: String) -> RpcResult { + pub async fn get_database_description(schema: String) -> RpcResult { let connector = RpcImpl::load_connector(&schema).await?; Ok(connector.get_database_description().await.map_err(Error::from)?) } - pub(crate) async fn get_database_metadata_internal(schema: String) -> RpcResult { + pub async fn get_database_metadata_internal(schema: String) -> RpcResult { let connector = RpcImpl::load_connector(&schema).await?; Ok(connector.get_metadata().await.map_err(Error::from)?) } diff --git a/query-engine/prisma/Cargo.toml b/query-engine/prisma/Cargo.toml index 93e823e7bd36..7337257727a7 100644 --- a/query-engine/prisma/Cargo.toml +++ b/query-engine/prisma/Cargo.toml @@ -42,6 +42,7 @@ tracing-attributes = "0.1" log = "0.4" user-facing-errors = { path = "../../libs/user-facing-errors" } +pretty_assertions = "0.6.1" [build-dependencies] rustc_version = "0.2.3" @@ -52,6 +53,7 @@ test-setup = { path = "../../libs/test-setup" } quaint = { git = "https://github.com/prisma/quaint", features = ["full"] } migration-connector = { path = "../../migration-engine/connectors/migration-connector" } migration-core = { path = "../../migration-engine/core" } +introspection-core = { path = "../../introspection-engine/core" } sql-migration-connector = { path = "../../migration-engine/connectors/sql-migration-connector" } indoc = "0.3" anyhow = "1" diff --git a/query-engine/prisma/src/tests.rs b/query-engine/prisma/src/tests.rs index c1d78226bb79..474cf570ede3 100644 --- a/query-engine/prisma/src/tests.rs +++ b/query-engine/prisma/src/tests.rs @@ -1,3 +1,4 @@ mod dmmf; mod execute_raw; mod test_api; +mod type_mappings; diff --git a/query-engine/prisma/src/tests/test_api.rs b/query-engine/prisma/src/tests/test_api.rs index b37e8a66af20..466db70985a8 100644 --- a/query-engine/prisma/src/tests/test_api.rs +++ b/query-engine/prisma/src/tests/test_api.rs @@ -22,6 +22,10 @@ pub struct QueryEngine { } impl QueryEngine { + pub fn new(ctx: PrismaContext) -> Self { + QueryEngine { context: Arc::new(ctx) } + } + pub async fn request(&self, body: impl Into) -> serde_json::Value { let request = PrismaRequest { body: GraphQlBody::Single(body.into()), diff --git a/query-engine/prisma/src/tests/type_mappings.rs b/query-engine/prisma/src/tests/type_mappings.rs new file mode 100644 index 000000000000..0ca997c65c94 --- /dev/null +++ b/query-engine/prisma/src/tests/type_mappings.rs @@ -0,0 +1,2 @@ +mod mysql_types; +mod test_api; diff --git a/query-engine/prisma/src/tests/type_mappings/mysql_types.rs b/query-engine/prisma/src/tests/type_mappings/mysql_types.rs new file mode 100644 index 000000000000..ae72f8f3885d --- /dev/null +++ b/query-engine/prisma/src/tests/type_mappings/mysql_types.rs @@ -0,0 +1,236 @@ +use super::test_api::*; +use datamodel::dml::ScalarType; +use indoc::indoc; +use pretty_assertions::assert_eq; +use serde_json::json; +use test_macros::*; + +const CREATE_TYPES_TABLE: &str = indoc! { + r##" + CREATE TABLE `types` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `numeric_integer_tinyint` tinyint(4), + `numeric_integer_smallint` smallint(6), + `numeric_integer_int` int(11), + `numeric_integer_bigint` bigint(20), + `numeric_floating_decimal` decimal(10,2), + `numeric_floating_float` float, + `numeric_fixed_double` double, + `numeric_fixed_real` double, + /* `numeric_bit` bit(64), tries to convert to string, doesn't do what we want */ + `numeric_boolean` tinyint(1), + `date_date` date, + `date_datetime` datetime, + `date_timestamp` timestamp null DEFAULT null, + `date_time` time, + /* `date_year` year(4), should be introspected as int, but we get datetime */ + `string_char` char(255), + `string_varchar` varchar(255), + `string_text_tinytext` tinytext, + `string_text_text` text, + `string_text_mediumtext` mediumtext, + `string_text_longtext` longtext, + `string_binary_binary` binary(20), + `string_binary_varbinary` varbinary(255), + `string_blob_tinyblob` tinyblob, + `string_blob_mediumblob` mediumblob, + `string_blob_blob` blob, + `string_blob_longblob` longblob, + /* `string_enum` enum('0','1','2'), */ + `string_set` set('a','b','c'), + `spatial_geometry` geometry, + `spatial_point` point, + `spatial_linestring` linestring, + `spatial_polygon` polygon, + `spatial_multipoint` multipoint, + `spatial_multilinestring` multilinestring, + `spatial_multipolygon` multipolygon, + `spatial_geometrycollection` geometrycollection, + `json` json, + + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=latin1; + "## +}; + +#[test_each_connector(tags("mysql"))] +async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { + api.execute(CREATE_TYPES_TABLE).await?; + + let (datamodel, engine) = api.create_engine().await?; + + datamodel.assert_model("types", |model| { + model + .assert_field_type("numeric_integer_tinyint", ScalarType::Int)? + .assert_field_type("numeric_integer_smallint", ScalarType::Int)? + .assert_field_type("numeric_integer_int", ScalarType::Int)? + .assert_field_type("numeric_integer_bigint", ScalarType::Int)? + .assert_field_type("numeric_floating_decimal", ScalarType::Float)? + .assert_field_type("numeric_floating_float", ScalarType::Float)? + .assert_field_type("numeric_fixed_double", ScalarType::Float)? + .assert_field_type("numeric_fixed_real", ScalarType::Float)? + // .assert_field_type("numeric_bit", ScalarType::String)? + .assert_field_type("numeric_boolean", ScalarType::Boolean)? + .assert_field_type("date_date", ScalarType::DateTime)? + .assert_field_type("date_datetime", ScalarType::DateTime)? + .assert_field_type("date_timestamp", ScalarType::DateTime)? + .assert_field_type("date_time", ScalarType::DateTime)? + // .assert_field_type("date_year", ScalarType::Int) + .assert_field_type("string_char", ScalarType::String)? + .assert_field_type("string_varchar", ScalarType::String)? + .assert_field_type("string_text_tinytext", ScalarType::String)? + .assert_field_type("string_text_text", ScalarType::String)? + .assert_field_type("string_text_mediumtext", ScalarType::String)? + .assert_field_type("string_text_longtext", ScalarType::String)? + .assert_field_type("string_binary_binary", ScalarType::String)? + .assert_field_type("string_blob_tinyblob", ScalarType::String)? + .assert_field_type("string_blob_mediumblob", ScalarType::String)? + .assert_field_type("string_blob_blob", ScalarType::String)? + .assert_field_type("string_blob_longblob", ScalarType::String)? + .assert_field_type("string_set", ScalarType::String)? + .assert_field_type("spatial_geometry", ScalarType::String)? + .assert_field_type("spatial_point", ScalarType::String)? + .assert_field_type("spatial_linestring", ScalarType::String)? + .assert_field_type("spatial_polygon", ScalarType::String)? + .assert_field_type("spatial_multipoint", ScalarType::String)? + .assert_field_type("spatial_multilinestring", ScalarType::String)? + .assert_field_type("spatial_multipolygon", ScalarType::String)? + .assert_field_type("spatial_geometrycollection", ScalarType::String)? + .assert_field_type("json", ScalarType::String) + })?; + + // Write the values. + { + let write = indoc! { + " + mutation { + createOnetypes( + data: { + numeric_integer_tinyint: 12, + numeric_integer_smallint: 350, + numeric_integer_int: 9002, + numeric_integer_bigint: 30000, + numeric_floating_decimal: 3.14 + numeric_floating_float: -32.0 + numeric_fixed_double: 0.14 + numeric_fixed_real: 12.12 + # numeric_bit: \"01111\" + numeric_boolean: true + date_date: \"2020-02-27T00:00:00Z\" + date_datetime: \"2020-02-27T19:10:22Z\" + date_timestamp: \"2020-02-27T19:11:22Z\" + # date_time: \"2020-02-20T12:50:01Z\" + # date_year: 2012 + string_char: \"make dolphins easy\" + string_varchar: \"dolphins of varying characters\" + string_text_tinytext: \"tiny dolphins\" + string_text_text: \"dolphins\" + string_text_mediumtext: \"medium dolphins\" + string_text_longtext: \"long dolphins\" + string_binary_binary: \"hello 2020\" + string_blob_tinyblob: \"smol blob\" + string_blob_mediumblob: \"average blob\" + string_blob_blob: \"very average blob\" + string_blob_longblob: \"loong looooong bloooooooob\" + json: \"{ \\\"name\\\": null }\", + } + ) { id } + } + " + }; + + let write_response = engine.request(write).await; + + let expected_write_response = json!({ + "data": { + "createOnetypes": { + "id": 1, + } + } + }); + + assert_eq!(write_response, expected_write_response); + } + + // Read the values back. + { + let read = indoc! { + " + query { + findManytypes { + numeric_integer_tinyint + numeric_integer_smallint + numeric_integer_int + numeric_integer_bigint + numeric_floating_decimal + numeric_floating_float + numeric_fixed_double + numeric_fixed_real + # numeric_bit + numeric_boolean + date_date + date_datetime + date_timestamp + # date_time + # date_year + string_char + string_varchar + string_text_tinytext + string_text_text + string_text_mediumtext + string_text_longtext + string_binary_binary + string_blob_tinyblob + string_blob_mediumblob + string_blob_blob + string_blob_longblob + # omitting spatial/geometry types + json + } + } + " + }; + + let read_response = engine.request(read).await; + + let expected_read_response = json!({ + "data": { + "findManytypes": [ + { + "numeric_integer_tinyint": 12, + "numeric_integer_smallint": 350, + "numeric_integer_int": 9002, + "numeric_integer_bigint": 30000, + "numeric_floating_decimal": 3.14, + "numeric_floating_float": -32.0, + "numeric_fixed_double": 0.14, + "numeric_fixed_real": 12.12, + // "numeric_bit": "1110", + "numeric_boolean": true, + "date_date": "2020-02-27T00:00:00.000Z", + "date_datetime": "2020-02-27T19:10:22.000Z", + "date_timestamp": "2020-02-27T19:11:22.000Z", + // "date_time": "2020-02-27T19:11:22.000Z", + // "date_year": 2012, + "string_char": "make dolphins easy", + "string_varchar": "dolphins of varying characters", + "string_text_tinytext": "tiny dolphins", + "string_text_text": "dolphins", + "string_text_mediumtext": "medium dolphins", + "string_text_longtext": "long dolphins", + "string_binary_binary": "hello 2020\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}", + "string_blob_tinyblob": "smol blob", + "string_blob_mediumblob": "average blob", + "string_blob_blob": "very average blob", + "string_blob_longblob": "loong looooong bloooooooob", + "json": "{ \"name\": null }", + }, + ] + }, + }); + + assert_eq!(read_response, expected_read_response); + } + + Ok(()) +} diff --git a/query-engine/prisma/src/tests/type_mappings/test_api.rs b/query-engine/prisma/src/tests/type_mappings/test_api.rs new file mode 100644 index 000000000000..a7c73785f260 --- /dev/null +++ b/query-engine/prisma/src/tests/type_mappings/test_api.rs @@ -0,0 +1,136 @@ +use super::super::test_api::QueryEngine; +use crate::context::PrismaContext; +use quaint::prelude::Queryable; + +pub type TestResult = anyhow::Result<()>; + +pub struct TestApi { + provider: &'static str, + database_string: String, + is_pgbouncer: bool, +} + +impl TestApi { + fn datasource(&self) -> String { + format!( + r#" + datasource my_db {{ + provider = "{provider}" + url = "{url}" + }} + "#, + provider = self.provider, + url = self.database_string, + ) + } + + pub async fn execute(&self, sql: &str) -> anyhow::Result<()> { + let conn = quaint::single::Quaint::new(&self.database_string).await?; + + conn.execute_raw(sql, &[]).await?; + + Ok(()) + } + + pub async fn create_engine(&self) -> anyhow::Result<(DatamodelAssertions, QueryEngine)> { + let datasource = self.datasource(); + + let schema = introspection_core::RpcImpl::introspect_internal(datasource) + .await + .map_err(|err| anyhow::anyhow!("{:?}", err.data))?; + + let context = PrismaContext::builder() + .enable_raw_queries(true) + .datamodel(schema.clone()) + .force_transactions(self.is_pgbouncer) + .build() + .await + .unwrap(); + + eprintln!("{}", schema); + let schema = datamodel::parse_datamodel(&schema).unwrap(); + + Ok((DatamodelAssertions(schema), QueryEngine::new(context))) + } +} + +pub struct DatamodelAssertions(datamodel::Datamodel); + +impl DatamodelAssertions { + pub fn assert_model(self, name: &str, assert_fn: F) -> anyhow::Result + where + F: for<'a> FnOnce(ModelAssertions<'a>) -> anyhow::Result>, + { + let model = self + .0 + .find_model(name) + .ok_or_else(|| anyhow::anyhow!("Assertion error: could not find model {}", name))?; + + assert_fn(ModelAssertions(model))?; + + Ok(self) + } +} + +pub struct ModelAssertions<'a>(&'a datamodel::dml::Model); + +impl<'a> ModelAssertions<'a> { + pub fn assert_field_type(self, name: &str, r#type: datamodel::dml::ScalarType) -> anyhow::Result { + let field = self + .0 + .find_field(name) + .ok_or_else(|| anyhow::anyhow!("Assertion error: could not find field {}", name))?; + + anyhow::ensure!( + field.field_type == datamodel::dml::FieldType::Base(r#type), + "Assertion error: expected the field {} to have type {:?}, but found {:?}", + field.name, + r#type, + &field.field_type, + ); + + Ok(self) + } +} + +pub async fn mysql_8_test_api(db_name: &str) -> TestApi { + let mysql_url = test_setup::mysql_8_url(db_name); + + test_setup::create_mysql_database(&mysql_url.parse().unwrap()) + .await + .unwrap(); + + TestApi { + database_string: mysql_url, + provider: "mysql", + is_pgbouncer: false, + } +} + +pub async fn mysql_test_api(db_name: &str) -> TestApi { + let mysql_url = test_setup::mysql_url(db_name); + + test_setup::create_mysql_database(&mysql_url.parse().unwrap()) + .await + .unwrap(); + + TestApi { + database_string: mysql_url, + provider: "mysql", + is_pgbouncer: false, + } +} + +pub async fn mysql_mariadb_test_api(db_name: &str) -> TestApi { + let mysql_url = test_setup::mariadb_url(db_name); + + test_setup::create_mysql_database(&mysql_url.parse().unwrap()) + .await + .unwrap(); + + TestApi { + database_string: mysql_url, + provider: "mysql", + is_pgbouncer: false, + } +} From d69b5681371685a281ed906a47199e47a805110e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Houl=C3=A9?= Date: Thu, 27 Feb 2020 14:50:51 +0100 Subject: [PATCH 02/15] [mysql] Introspect year columns to integer --- libs/sql-schema-describer/src/mysql.rs | 2 +- .../tests/mysql_introspection_tests.rs | 2 +- .../prisma/src/tests/type_mappings/mysql_types.rs | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libs/sql-schema-describer/src/mysql.rs b/libs/sql-schema-describer/src/mysql.rs index fd30c7c581cc..3e9238a77f36 100644 --- a/libs/sql-schema-describer/src/mysql.rs +++ b/libs/sql-schema-describer/src/mysql.rs @@ -460,7 +460,7 @@ fn get_column_type_and_enum( ("time", _) => ColumnTypeFamily::DateTime, ("datetime", _) => ColumnTypeFamily::DateTime, ("timestamp", _) => ColumnTypeFamily::DateTime, - ("year", _) => ColumnTypeFamily::DateTime, + ("year", _) => ColumnTypeFamily::Int, ("char", _) => ColumnTypeFamily::String, ("varchar", _) => ColumnTypeFamily::String, ("text", _) => ColumnTypeFamily::String, diff --git a/libs/sql-schema-describer/tests/mysql_introspection_tests.rs b/libs/sql-schema-describer/tests/mysql_introspection_tests.rs index 7e8e6e6d0224..bd4ff4889873 100644 --- a/libs/sql-schema-describer/tests/mysql_introspection_tests.rs +++ b/libs/sql-schema-describer/tests/mysql_introspection_tests.rs @@ -228,7 +228,7 @@ async fn all_mysql_column_types_must_work() { name: "year_col".to_string(), tpe: ColumnType { raw: "year".to_string(), - family: ColumnTypeFamily::DateTime, + family: ColumnTypeFamily::Int, arity: ColumnArity::Required, }, diff --git a/query-engine/prisma/src/tests/type_mappings/mysql_types.rs b/query-engine/prisma/src/tests/type_mappings/mysql_types.rs index ae72f8f3885d..200ff44848f8 100644 --- a/query-engine/prisma/src/tests/type_mappings/mysql_types.rs +++ b/query-engine/prisma/src/tests/type_mappings/mysql_types.rs @@ -23,7 +23,7 @@ const CREATE_TYPES_TABLE: &str = indoc! { `date_datetime` datetime, `date_timestamp` timestamp null DEFAULT null, `date_time` time, - /* `date_year` year(4), should be introspected as int, but we get datetime */ + `date_year` year(4), `string_char` char(255), `string_varchar` varchar(255), `string_text_tinytext` tinytext, @@ -75,7 +75,7 @@ async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { .assert_field_type("date_datetime", ScalarType::DateTime)? .assert_field_type("date_timestamp", ScalarType::DateTime)? .assert_field_type("date_time", ScalarType::DateTime)? - // .assert_field_type("date_year", ScalarType::Int) + .assert_field_type("date_year", ScalarType::Int)? .assert_field_type("string_char", ScalarType::String)? .assert_field_type("string_varchar", ScalarType::String)? .assert_field_type("string_text_tinytext", ScalarType::String)? @@ -120,7 +120,7 @@ async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { date_datetime: \"2020-02-27T19:10:22Z\" date_timestamp: \"2020-02-27T19:11:22Z\" # date_time: \"2020-02-20T12:50:01Z\" - # date_year: 2012 + date_year: 2012 string_char: \"make dolphins easy\" string_varchar: \"dolphins of varying characters\" string_text_tinytext: \"tiny dolphins\" @@ -132,7 +132,7 @@ async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { string_blob_mediumblob: \"average blob\" string_blob_blob: \"very average blob\" string_blob_longblob: \"loong looooong bloooooooob\" - json: \"{ \\\"name\\\": null }\", + json: \"{\\\"name\\\": null}\", } ) { id } } @@ -172,7 +172,7 @@ async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { date_datetime date_timestamp # date_time - # date_year + date_year string_char string_varchar string_text_tinytext @@ -211,7 +211,7 @@ async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { "date_datetime": "2020-02-27T19:10:22.000Z", "date_timestamp": "2020-02-27T19:11:22.000Z", // "date_time": "2020-02-27T19:11:22.000Z", - // "date_year": 2012, + "date_year": 2012, "string_char": "make dolphins easy", "string_varchar": "dolphins of varying characters", "string_text_tinytext": "tiny dolphins", @@ -223,7 +223,7 @@ async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { "string_blob_mediumblob": "average blob", "string_blob_blob": "very average blob", "string_blob_longblob": "loong looooong bloooooooob", - "json": "{ \"name\": null }", + "json": "{\"name\": null}", }, ] }, From 6880d77263e7be56d1d8c1825abb2e1bea524194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Houl=C3=A9?= Date: Thu, 27 Feb 2020 15:07:44 +0100 Subject: [PATCH 03/15] [mysql] Test roundtripping of enums --- .../src/tests/type_mappings/mysql_types.rs | 8 ++++++-- .../prisma/src/tests/type_mappings/test_api.rs | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/query-engine/prisma/src/tests/type_mappings/mysql_types.rs b/query-engine/prisma/src/tests/type_mappings/mysql_types.rs index 200ff44848f8..e2694381d566 100644 --- a/query-engine/prisma/src/tests/type_mappings/mysql_types.rs +++ b/query-engine/prisma/src/tests/type_mappings/mysql_types.rs @@ -36,7 +36,7 @@ const CREATE_TYPES_TABLE: &str = indoc! { `string_blob_mediumblob` mediumblob, `string_blob_blob` blob, `string_blob_longblob` longblob, - /* `string_enum` enum('0','1','2'), */ + `string_enum` enum('pollicle_dogs','jellicle_cats'), `string_set` set('a','b','c'), `spatial_geometry` geometry, `spatial_point` point, @@ -87,6 +87,7 @@ async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { .assert_field_type("string_blob_mediumblob", ScalarType::String)? .assert_field_type("string_blob_blob", ScalarType::String)? .assert_field_type("string_blob_longblob", ScalarType::String)? + .assert_field_enum_type("string_enum", "types_string_enum")? .assert_field_type("string_set", ScalarType::String)? .assert_field_type("spatial_geometry", ScalarType::String)? .assert_field_type("spatial_point", ScalarType::String)? @@ -132,7 +133,8 @@ async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { string_blob_mediumblob: \"average blob\" string_blob_blob: \"very average blob\" string_blob_longblob: \"loong looooong bloooooooob\" - json: \"{\\\"name\\\": null}\", + string_enum: \"jellicle_cats\" + json: \"{\\\"name\\\": null}\" } ) { id } } @@ -184,6 +186,7 @@ async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { string_blob_mediumblob string_blob_blob string_blob_longblob + string_enum # omitting spatial/geometry types json } @@ -223,6 +226,7 @@ async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { "string_blob_mediumblob": "average blob", "string_blob_blob": "very average blob", "string_blob_longblob": "loong looooong bloooooooob", + "string_enum": "jellicle_cats", "json": "{\"name\": null}", }, ] diff --git a/query-engine/prisma/src/tests/type_mappings/test_api.rs b/query-engine/prisma/src/tests/type_mappings/test_api.rs index a7c73785f260..9ad6705522c5 100644 --- a/query-engine/prisma/src/tests/type_mappings/test_api.rs +++ b/query-engine/prisma/src/tests/type_mappings/test_api.rs @@ -91,6 +91,23 @@ impl<'a> ModelAssertions<'a> { Ok(self) } + + pub fn assert_field_enum_type(self, name: &str, enum_name: &str) -> anyhow::Result { + let field = self + .0 + .find_field(name) + .ok_or_else(|| anyhow::anyhow!("Assertion error: could not find field {}", name))?; + + anyhow::ensure!( + field.field_type == datamodel::dml::FieldType::Enum(enum_name.into()), + "Assertion error: expected the field {} to have enum type {:?}, but found {:?}", + field.name, + enum_name, + &field.field_type, + ); + + Ok(self) + } } pub async fn mysql_8_test_api(db_name: &str) -> TestApi { From dba75e4cd3af2506c7d071ca9f195bd7a7e2fae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Houl=C3=A9?= Date: Thu, 27 Feb 2020 16:57:22 +0100 Subject: [PATCH 04/15] Add support for mysql `bit` columns It maps them to an integer, assuming they were populated with an integer value, but this could not be the case. --- Cargo.toml | 3 + libs/prisma-value/src/sql_ext.rs | 1 + libs/sql-schema-describer/src/mysql.rs | 1 + .../connectors/sql-query-connector/src/row.rs | 100 +++++++++++++++++- .../src/tests/type_mappings/mysql_types.rs | 44 +++++++- 5 files changed, 141 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 79ff413288a1..39480b97a232 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,6 @@ members = [ "libs/prisma-models", "libs/prisma-value", ] + +[patch.'https://github.com/prisma/quaint'] +quaint = { path = "../quaint" } \ No newline at end of file diff --git a/libs/prisma-value/src/sql_ext.rs b/libs/prisma-value/src/sql_ext.rs index ce9291f6a7d7..d1da95da2941 100644 --- a/libs/prisma-value/src/sql_ext.rs +++ b/libs/prisma-value/src/sql_ext.rs @@ -15,6 +15,7 @@ impl<'a> From> for PrismaValue { ParameterizedValue::Uuid(uuid) => PrismaValue::Uuid(uuid), ParameterizedValue::DateTime(dt) => PrismaValue::DateTime(dt), ParameterizedValue::Char(c) => PrismaValue::String(c.to_string()), + ParameterizedValue::Bytes(bytes) => todo!("PrismaValue::Bytes"), } } } diff --git a/libs/sql-schema-describer/src/mysql.rs b/libs/sql-schema-describer/src/mysql.rs index 3e9238a77f36..565402f00dba 100644 --- a/libs/sql-schema-describer/src/mysql.rs +++ b/libs/sql-schema-describer/src/mysql.rs @@ -456,6 +456,7 @@ fn get_column_type_and_enum( ("numeric", _) => ColumnTypeFamily::Float, ("float", _) => ColumnTypeFamily::Float, ("double", _) => ColumnTypeFamily::Float, + ("bit", _) => ColumnTypeFamily::Int, ("date", _) => ColumnTypeFamily::DateTime, ("time", _) => ColumnTypeFamily::DateTime, ("datetime", _) => ColumnTypeFamily::DateTime, diff --git a/query-engine/connectors/sql-query-connector/src/row.rs b/query-engine/connectors/sql-query-connector/src/row.rs index 6732a25fca1e..02e6679561e0 100644 --- a/query-engine/connectors/sql-query-connector/src/row.rs +++ b/query-engine/connectors/sql-query-connector/src/row.rs @@ -7,7 +7,7 @@ use quaint::{ connector::ResultRow, }; use rust_decimal::{prelude::FromPrimitive, Decimal}; -use std::{borrow::Borrow, io}; +use std::{borrow::Borrow, io, str::FromStr}; use uuid::Uuid; /// An allocated representation of a `Row` returned from the database. @@ -136,7 +136,9 @@ pub fn row_value_to_prisma_value( ParameterizedValue::Integer(i) => { PrismaValue::Float(Decimal::from_f64(i as f64).expect("f64 was not a Decimal.")) } - ParameterizedValue::Text(s) => PrismaValue::Float(s.parse().unwrap()), + ParameterizedValue::Text(_) | ParameterizedValue::Bytes(_) => { + PrismaValue::Float(p_value.as_str().unwrap().parse().unwrap()) + } _ => { let error = io::Error::new( io::ErrorKind::InvalidData, @@ -145,7 +147,18 @@ pub fn row_value_to_prisma_value( return Err(SqlError::ConversionError(error.into())); } }, - _ => PrismaValue::from(p_value), + TypeIdentifier::Int => match p_value { + ParameterizedValue::Integer(i) => PrismaValue::Int(i), + ParameterizedValue::Bytes(bytes) => PrismaValue::Int(interpret_bytes_as_i64(&bytes)), + ParameterizedValue::Text(txt) => PrismaValue::Int( + i64::from_str(dbg!(txt.trim_start_matches('\0'))) + .map_err(|err| SqlError::ConversionError(err.into()))?, + ), + other => PrismaValue::from(other), + }, + TypeIdentifier::String => p_value.into_string().map(PrismaValue::String).ok_or_else(|| { + SqlError::ConversionError(failure::format_err!("Could not extract text value from result set")) + })?, }) } @@ -171,3 +184,84 @@ impl From<&SqlId> for DatabaseValue<'static> { id.clone().into() } } + +// We assume the bytes are stored as a big endian signed integer, because that is what +// mysql does if you enter a numeric value for a bits column. +fn interpret_bytes_as_i64(bytes: &[u8]) -> i64 { + match bytes.len() { + 8 => i64::from_be_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]), + len if len < 8 => { + let sign_bit_mask: u8 = 0b10000000; + // The first byte will only contain the sign bit. + let most_significant_bit_byte = bytes[0] & sign_bit_mask; + let padding = if most_significant_bit_byte == 0 { 0 } else { 0b11111111 }; + let mut i64_bytes = [padding; 8]; + + for (target_byte, source_byte) in i64_bytes.iter_mut().rev().zip(bytes.iter().rev()) { + *target_byte = *source_byte; + } + + i64::from_be_bytes(i64_bytes) + } + 0 => 0, + _ => panic!("Attempted to interpret more than 8 bytes as an integer."), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn quaint_bytes_to_integer_conversion_works() { + // Negative i64 + { + let i: i64 = -123456789123; + let bytes = i.to_be_bytes(); + let roundtripped = interpret_bytes_as_i64(&bytes); + assert_eq!(roundtripped, i); + } + + // Positive i64 + { + let i: i64 = 123456789123; + let bytes = i.to_be_bytes(); + let roundtripped = interpret_bytes_as_i64(&bytes); + assert_eq!(roundtripped, i); + } + + // Positive i32 + { + let i: i32 = 123456789; + let bytes = i.to_be_bytes(); + let roundtripped = interpret_bytes_as_i64(&bytes); + assert_eq!(roundtripped, i as i64); + } + + // Negative i32 + { + let i: i32 = -123456789; + let bytes = i.to_be_bytes(); + let roundtripped = interpret_bytes_as_i64(&bytes); + assert_eq!(roundtripped, i as i64); + } + + // Positive i16 + { + let i: i16 = 12345; + let bytes = i.to_be_bytes(); + let roundtripped = interpret_bytes_as_i64(&bytes); + assert_eq!(roundtripped, i as i64); + } + + // Negative i16 + { + let i: i16 = -12345; + let bytes = i.to_be_bytes(); + let roundtripped = interpret_bytes_as_i64(&bytes); + assert_eq!(roundtripped, i as i64); + } + } +} diff --git a/query-engine/prisma/src/tests/type_mappings/mysql_types.rs b/query-engine/prisma/src/tests/type_mappings/mysql_types.rs index e2694381d566..fe4592375b0d 100644 --- a/query-engine/prisma/src/tests/type_mappings/mysql_types.rs +++ b/query-engine/prisma/src/tests/type_mappings/mysql_types.rs @@ -17,7 +17,7 @@ const CREATE_TYPES_TABLE: &str = indoc! { `numeric_floating_float` float, `numeric_fixed_double` double, `numeric_fixed_real` double, - /* `numeric_bit` bit(64), tries to convert to string, doesn't do what we want */ + `numeric_bit` bit(64), `numeric_boolean` tinyint(1), `date_date` date, `date_datetime` datetime, @@ -69,7 +69,7 @@ async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { .assert_field_type("numeric_floating_float", ScalarType::Float)? .assert_field_type("numeric_fixed_double", ScalarType::Float)? .assert_field_type("numeric_fixed_real", ScalarType::Float)? - // .assert_field_type("numeric_bit", ScalarType::String)? + .assert_field_type("numeric_bit", ScalarType::Int)? .assert_field_type("numeric_boolean", ScalarType::Boolean)? .assert_field_type("date_date", ScalarType::DateTime)? .assert_field_type("date_datetime", ScalarType::DateTime)? @@ -115,7 +115,7 @@ async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { numeric_floating_float: -32.0 numeric_fixed_double: 0.14 numeric_fixed_real: 12.12 - # numeric_bit: \"01111\" + numeric_bit: 4 numeric_boolean: true date_date: \"2020-02-27T00:00:00Z\" date_datetime: \"2020-02-27T19:10:22Z\" @@ -168,7 +168,7 @@ async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { numeric_floating_float numeric_fixed_double numeric_fixed_real - # numeric_bit + numeric_bit numeric_boolean date_date date_datetime @@ -208,7 +208,7 @@ async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { "numeric_floating_float": -32.0, "numeric_fixed_double": 0.14, "numeric_fixed_real": 12.12, - // "numeric_bit": "1110", + "numeric_bit": 4, "numeric_boolean": true, "date_date": "2020-02-27T00:00:00.000Z", "date_datetime": "2020-02-27T19:10:22.000Z", @@ -238,3 +238,37 @@ async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { Ok(()) } + +#[test_each_connector(tags("mysql"))] +async fn mysql_bit_columns_are_properly_mapped_to_signed_integers(api: &TestApi) -> TestResult { + api.execute(CREATE_TYPES_TABLE).await?; + + let (_datamodel, engine) = api.create_engine().await?; + + let write = indoc! { + " + mutation { + createOnetypes( + data: { + numeric_bit: -12 + } + ) { id numeric_bit } + } + " + }; + + let write_response = engine.request(write).await; + + let expected_write_response = json!({ + "data": { + "createOnetypes": { + "id": 1, + "numeric_bit": -12, + } + } + }); + + assert_eq!(write_response, expected_write_response); + + Ok(()) +} From 31a33575c30324f5a338dbd8350fe5e312c41c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Houl=C3=A9?= Date: Mon, 2 Mar 2020 11:10:38 +0100 Subject: [PATCH 05/15] Test PostgreSQL type mappings --- Cargo.lock | 4 +- Cargo.toml | 3 - libs/prisma-value/src/sql_ext.rs | 2 +- libs/sql-schema-describer/src/postgres.rs | 4 +- libs/test-setup/src/lib.rs | 8 +- .../connectors/sql-query-connector/src/row.rs | 9 +- query-engine/prisma/Cargo.toml | 1 + .../prisma/src/tests/type_mappings.rs | 1 + .../src/tests/type_mappings/postgres_types.rs | 231 ++++++++++++++++++ .../src/tests/type_mappings/test_api.rs | 58 +++++ 10 files changed, 311 insertions(+), 10 deletions(-) create mode 100644 query-engine/prisma/src/tests/type_mappings/postgres_types.rs diff --git a/Cargo.lock b/Cargo.lock index 036557771deb..2ee3e22f1959 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1976,6 +1976,7 @@ dependencies = [ "tokio", "tracing", "tracing-attributes", + "tracing-futures", "tracing-log", "tracing-subscriber", "url 2.1.1", @@ -2109,9 +2110,10 @@ dependencies = [ [[package]] name = "quaint" version = "0.2.0-alpha.9" -source = "git+https://github.com/prisma/quaint#f3c27482fd49bde532339716cce514d764fa9548" +source = "git+https://github.com/prisma/quaint#e971ded73ad13a391a40f168b4c443bc969d9fc4" dependencies = [ "async-trait", + "base64 0.11.0", "bytes", "chrono", "futures 0.3.4", diff --git a/Cargo.toml b/Cargo.toml index 39480b97a232..79ff413288a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,3 @@ members = [ "libs/prisma-models", "libs/prisma-value", ] - -[patch.'https://github.com/prisma/quaint'] -quaint = { path = "../quaint" } \ No newline at end of file diff --git a/libs/prisma-value/src/sql_ext.rs b/libs/prisma-value/src/sql_ext.rs index d1da95da2941..66a86a74c22b 100644 --- a/libs/prisma-value/src/sql_ext.rs +++ b/libs/prisma-value/src/sql_ext.rs @@ -15,7 +15,7 @@ impl<'a> From> for PrismaValue { ParameterizedValue::Uuid(uuid) => PrismaValue::Uuid(uuid), ParameterizedValue::DateTime(dt) => PrismaValue::DateTime(dt), ParameterizedValue::Char(c) => PrismaValue::String(c.to_string()), - ParameterizedValue::Bytes(bytes) => todo!("PrismaValue::Bytes"), + ParameterizedValue::Bytes(_bytes) => unreachable!("PrismaValue::Bytes"), } } } diff --git a/libs/sql-schema-describer/src/postgres.rs b/libs/sql-schema-describer/src/postgres.rs index 883954aca725..6cd2be06bbfe 100644 --- a/libs/sql-schema-describer/src/postgres.rs +++ b/libs/sql-schema-describer/src/postgres.rs @@ -573,6 +573,7 @@ fn get_column_type<'a>(data_type: &str, full_data_type: &'a str, arity: ColumnAr "int2" | "_int2" => Int, "int4" | "_int4" => Int, "int8" | "_int8" => Int, + "oid" | "_oid" => Int, "float4" | "_float4" => Float, "float8" | "_float8" => Float, "bool" | "_bool" => Boolean, @@ -592,8 +593,9 @@ fn get_column_type<'a>(data_type: &str, full_data_type: &'a str, arity: ColumnAr "path" | "_path" => Geometric, "polygon" | "_polygon" => Geometric, "bpchar" | "_bpchar" => String, - "interval" | "_interval" => DateTime, + "interval" | "_interval" => String, "numeric" | "_numeric" => Float, + "money" | "_money" => Float, "pg_lsn" | "_pg_lsn" => LogSequenceNumber, "time" | "_time" => DateTime, "timetz" | "_timetz" => DateTime, diff --git a/libs/test-setup/src/lib.rs b/libs/test-setup/src/lib.rs index 1683536bd8f4..ac18d64ee572 100644 --- a/libs/test-setup/src/lib.rs +++ b/libs/test-setup/src/lib.rs @@ -335,11 +335,17 @@ pub async fn create_postgres_database(original_url: &Url) -> Result PrismaValue::from(other), }, - TypeIdentifier::String => p_value.into_string().map(PrismaValue::String).ok_or_else(|| { - SqlError::ConversionError(failure::format_err!("Could not extract text value from result set")) - })?, + TypeIdentifier::String => match p_value { + ParameterizedValue::Uuid(uuid) => PrismaValue::String(uuid.to_string()), + ParameterizedValue::Json(json_value) => PrismaValue::String(serde_json::to_string(&json_value).expect("JSON value to string")), + _ => p_value.into_string().map(PrismaValue::String).ok_or_else(|| { + SqlError::ConversionError(failure::format_err!("Could not extract text value from result set")) })?, + } }) } diff --git a/query-engine/prisma/Cargo.toml b/query-engine/prisma/Cargo.toml index 7337257727a7..7577a2552598 100644 --- a/query-engine/prisma/Cargo.toml +++ b/query-engine/prisma/Cargo.toml @@ -43,6 +43,7 @@ log = "0.4" user-facing-errors = { path = "../../libs/user-facing-errors" } pretty_assertions = "0.6.1" +tracing-futures = "0.2.3" [build-dependencies] rustc_version = "0.2.3" diff --git a/query-engine/prisma/src/tests/type_mappings.rs b/query-engine/prisma/src/tests/type_mappings.rs index 0ca997c65c94..58281837a938 100644 --- a/query-engine/prisma/src/tests/type_mappings.rs +++ b/query-engine/prisma/src/tests/type_mappings.rs @@ -1,2 +1,3 @@ mod mysql_types; +mod postgres_types; mod test_api; diff --git a/query-engine/prisma/src/tests/type_mappings/postgres_types.rs b/query-engine/prisma/src/tests/type_mappings/postgres_types.rs new file mode 100644 index 000000000000..1fc2d2ce4b2c --- /dev/null +++ b/query-engine/prisma/src/tests/type_mappings/postgres_types.rs @@ -0,0 +1,231 @@ +use super::test_api::*; +use datamodel::ScalarType; +use indoc::indoc; +use pretty_assertions::assert_eq; +use serde_json::json; +use test_macros::test_each_connector; + +const CREATE_TYPES_TABLE: &str = indoc! { + r##" + CREATE TABLE "prisma-tests"."types" ( + id SERIAL PRIMARY KEY, + numeric_int2 int2, + numeric_int4 int4, + numeric_int8 int8, + + numeric_decimal decimal(8, 4), + numeric_float4 float4, + numeric_float8 float8, + + numeric_serial2 serial2, + numeric_serial4 serial4, + numeric_serial8 serial8, + + numeric_money money, + numeric_oid oid, + + string_char char(8), + string_varchar varchar(20), + string_text text, + + binary_bytea bytea, + binary_bits bit(80), + binary_bits_varying bit varying(80), + binary_uuid uuid, + + time_timestamp timestamp, + time_timestamptz timestamptz, + time_date date, + time_time time, + time_timetz timetz, + time_interval interval, + + boolean_boolean boolean, + + network_cidr cidr, + network_inet inet, + network_mac macaddr, + + search_tsvector tsvector, + search_tsquery tsquery, + + json_json json, + json_jsonb jsonb, + + range_int4range int4range, + range_int8range int8range, + range_numrange numrange, + range_tsrange tsrange, + range_tstzrange tstzrange, + range_daterange daterange + ); + "## +}; + +#[test_each_connector(tags("postgres"), log = "debug")] +async fn postgres_types_roundtrip(api: &TestApi) -> TestResult { + api.execute(CREATE_TYPES_TABLE).await?; + + let (datamodel, engine) = api.create_engine().await?; + + datamodel.assert_model("types", |model| { + model + .assert_field_type("numeric_int2", ScalarType::Int)? + .assert_field_type("numeric_int4", ScalarType::Int)? + .assert_field_type("numeric_int8", ScalarType::Int)? + .assert_field_type("numeric_decimal", ScalarType::Float)? + .assert_field_type("numeric_float4", ScalarType::Float)? + .assert_field_type("numeric_float8", ScalarType::Float)? + .assert_field_type("numeric_serial2", ScalarType::Int)? + .assert_field_type("numeric_serial4", ScalarType::Int)? + .assert_field_type("numeric_serial8", ScalarType::Int)? + .assert_field_type("numeric_money", ScalarType::Float)? + .assert_field_type("numeric_oid", ScalarType::Int)? + .assert_field_type("string_char", ScalarType::String)? + .assert_field_type("string_varchar", ScalarType::String)? + .assert_field_type("string_text", ScalarType::String)? + .assert_field_type("binary_bytea", ScalarType::String)? + .assert_field_type("binary_bits", ScalarType::String)? + .assert_field_type("binary_bits_varying", ScalarType::String)? + .assert_field_type("binary_uuid", ScalarType::String)? + .assert_field_type("time_timestamp", ScalarType::DateTime)? + .assert_field_type("time_timestamptz", ScalarType::DateTime)? + .assert_field_type("time_date", ScalarType::DateTime)? + .assert_field_type("time_time", ScalarType::DateTime)? + .assert_field_type("time_timetz", ScalarType::DateTime)? + .assert_field_type("time_interval", ScalarType::String)? + .assert_field_type("boolean_boolean", ScalarType::Boolean)? + .assert_field_type("network_cidr", ScalarType::String)? + .assert_field_type("network_inet", ScalarType::String)? + .assert_field_type("network_mac", ScalarType::String)? + .assert_field_type("search_tsvector", ScalarType::String)? + .assert_field_type("search_tsquery", ScalarType::String)? + .assert_field_type("json_json", ScalarType::String)? + .assert_field_type("json_jsonb", ScalarType::String)? + .assert_field_type("range_int4range", ScalarType::String)? + .assert_field_type("range_int8range", ScalarType::String)? + .assert_field_type("range_numrange", ScalarType::String)? + .assert_field_type("range_tsrange", ScalarType::String)? + .assert_field_type("range_tstzrange", ScalarType::String)? + .assert_field_type("range_daterange", ScalarType::String) + })?; + + let query = indoc! { + r##" + mutation { + createOnetypes( + data: { + numeric_int2: 12 + numeric_int4: 9002 + numeric_int8: 100000000 + numeric_decimal: 49.3444 + numeric_float4: 12.12 + numeric_float8: 3.139428 + numeric_serial2: 8, + numeric_serial4: 80, + numeric_serial8: 80000, + numeric_money: 3.50 + numeric_oid: 2000 + string_char: "yeet" + string_varchar: "yeet variable" + string_text: "to yeet or not to yeet" + binary_uuid: "111142ec-880b-4062-913d-8eac479ab957" + time_timestamp: "2020-03-02T08:00:00.000" + time_timestamptz: "2020-03-02T08:00:00.000" + time_date: "2020-03-05T00:00:00.000" + time_time: "2020-03-05T08:00:00.000" + time_timetz: "2020-03-05T08:00:00.000" + # time_interval: "3 hours" + boolean_boolean: true + # network_cidr: "192.168.100.14/24" + network_inet: "192.168.100.14" + # network_mac: "12:33:ed:44:49:36" + # search_tsvector: "''a'' ''dump'' ''dumps'' ''fox'' ''in'' ''the''" + # search_tsquery: "''foxy cat''" + json_json: "{ \"isJson\": true }" + json_jsonb: "{ \"isJSONB\": true }" + # range_int4range: "[-4, 8)" + # range_int8range: "[4000, 9000)" + # range_numrange: "[11.1, 22.2)" + # range_tsrange: "[2010-01-01 14:30, 2010-01-01 15:30)" + # range_tstzrange: "[2010-01-01 14:30, 2010-01-01 15:30)" + # range_daterange: "[2020-03-02, 2020-03-22)" + } + ) { + numeric_int2 + numeric_int4 + numeric_int8 + numeric_decimal + numeric_float4 + numeric_float8 + numeric_serial2 + numeric_serial4 + numeric_serial8 + numeric_money + numeric_oid + string_char + string_varchar + string_text + binary_uuid + time_timestamp + time_timestamptz + time_date + time_time + time_timetz + # time_interval + boolean_boolean + # network_cidr + network_inet + # network_mac + # search_tsvector + # search_tsquery + json_json + json_jsonb + # range_int4range + # range_int8range + # range_numrange + # range_tsrange + # range_tstzrange + # range_daterange + } + } + "## + }; + + let response = engine.request(query).await; + + let expected_response = json!({ + "data": { + "createOnetypes": { + "numeric_int2": 12, + "numeric_int4": 9002, + "numeric_int8": 100000000, + "numeric_serial2": 8, + "numeric_serial4": 80, + "numeric_serial8": 80000, + "numeric_decimal": 49.3444, + "numeric_float4": 12.12, + "numeric_float8": 3.139428, + "numeric_money": 3.5, + "numeric_oid": 2000, + "string_char": "yeet ", + "string_varchar": "yeet variable", + "string_text": "to yeet or not to yeet", + "binary_uuid": "111142ec-880b-4062-913d-8eac479ab957", + "time_timestamp": "2020-03-02T08:00:00.000Z", + "time_timestamptz": "2020-03-02T08:00:00.000Z", + "time_date": "2020-03-05T00:00:00.000Z", + "time_time": "1970-01-01T08:00:00.000Z", + "time_timetz": "1970-01-01T08:00:00.000Z", + "boolean_boolean": true, + "network_inet": "192.168.100.14", + "json_json": "{\"isJson\":true}", + "json_jsonb": "{\"isJSONB\":true}", + } + } + }); + + assert_eq!(response, expected_response); + + Ok(()) +} diff --git a/query-engine/prisma/src/tests/type_mappings/test_api.rs b/query-engine/prisma/src/tests/type_mappings/test_api.rs index 9ad6705522c5..6df638ab0072 100644 --- a/query-engine/prisma/src/tests/type_mappings/test_api.rs +++ b/query-engine/prisma/src/tests/type_mappings/test_api.rs @@ -35,6 +35,8 @@ impl TestApi { pub async fn create_engine(&self) -> anyhow::Result<(DatamodelAssertions, QueryEngine)> { let datasource = self.datasource(); + dbg!(&datasource); + let schema = introspection_core::RpcImpl::introspect_internal(datasource) .await .map_err(|err| anyhow::anyhow!("{:?}", err.data))?; @@ -151,3 +153,59 @@ pub async fn mysql_mariadb_test_api(db_name: &str) -> TestApi { is_pgbouncer: false, } } + +pub async fn postgres_test_api(db_name: &str) -> TestApi { + let postgres_url = test_setup::postgres_10_url(db_name); + + test_setup::create_postgres_database(&postgres_url.parse().unwrap()) + .await + .unwrap(); + + TestApi { + database_string: postgres_url, + provider: "postgres", + is_pgbouncer: false, + } +} + +pub async fn postgres9_test_api(db_name: &str) -> TestApi { + let postgres_url = test_setup::postgres_9_url(db_name); + + test_setup::create_postgres_database(&postgres_url.parse().unwrap()) + .await + .unwrap(); + + TestApi { + database_string: postgres_url, + provider: "postgres", + is_pgbouncer: false, + } +} + +pub async fn postgres11_test_api(db_name: &str) -> TestApi { + let postgres_url = test_setup::postgres_11_url(db_name); + + test_setup::create_postgres_database(&postgres_url.parse().unwrap()) + .await + .unwrap(); + + TestApi { + database_string: postgres_url, + provider: "postgres", + is_pgbouncer: false, + } +} + +pub async fn postgres12_test_api(db_name: &str) -> TestApi { + let postgres_url = test_setup::postgres_12_url(db_name); + + test_setup::create_postgres_database(&postgres_url.parse().unwrap()) + .await + .unwrap(); + + TestApi { + database_string: postgres_url, + provider: "postgres", + is_pgbouncer: false, + } +} From 12895527365c33c6570c9b6bf982361af2f1a2d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Houl=C3=A9?= Date: Fri, 6 Mar 2020 17:38:48 +0100 Subject: [PATCH 06/15] Fix precision loss issue on decimals --- .../datamodel-connector/src/scalars.rs | 2 +- .../core/src/common/value_validator.rs | 6 +- .../src/tests/type_mappings/postgres_types.rs | 61 +++++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/libs/datamodel/connectors/datamodel-connector/src/scalars.rs b/libs/datamodel/connectors/datamodel-connector/src/scalars.rs index 6b556a236fd7..8f761e254374 100644 --- a/libs/datamodel/connectors/datamodel-connector/src/scalars.rs +++ b/libs/datamodel/connectors/datamodel-connector/src/scalars.rs @@ -44,7 +44,7 @@ impl ToString for ScalarType { pub enum ScalarValue { Int(i32), Float(f32), - Decimal(f32), + Decimal(f64), Boolean(bool), String(String), DateTime(DateTime), diff --git a/libs/datamodel/core/src/common/value_validator.rs b/libs/datamodel/core/src/common/value_validator.rs index f96e0f77341e..e4733244e73a 100644 --- a/libs/datamodel/core/src/common/value_validator.rs +++ b/libs/datamodel/core/src/common/value_validator.rs @@ -125,10 +125,10 @@ impl ValueValidator { // TODO: Ask which decimal type to take. /// Tries to convert the wrapped value to a Prisma Decimal. - pub fn as_decimal(&self) -> Result { + pub fn as_decimal(&self) -> Result { match &self.value { - ast::Expression::NumericValue(value, _) => self.wrap_error_from_result(value.parse::(), "numeric"), - ast::Expression::Any(value, _) => self.wrap_error_from_result(value.parse::(), "numeric"), + ast::Expression::NumericValue(value, _) => self.wrap_error_from_result(value.parse::(), "numeric"), + ast::Expression::Any(value, _) => self.wrap_error_from_result(value.parse::(), "numeric"), _ => Err(self.construct_type_mismatch_error("numeric")), } } diff --git a/query-engine/prisma/src/tests/type_mappings/postgres_types.rs b/query-engine/prisma/src/tests/type_mappings/postgres_types.rs index 1fc2d2ce4b2c..ac15be4a1c87 100644 --- a/query-engine/prisma/src/tests/type_mappings/postgres_types.rs +++ b/query-engine/prisma/src/tests/type_mappings/postgres_types.rs @@ -229,3 +229,64 @@ async fn postgres_types_roundtrip(api: &TestApi) -> TestResult { Ok(()) } + +#[test_each_connector(tags("postgres"))] +async fn small_float_values_must_work(api: &TestApi) -> TestResult { + let schema = indoc! { + r#" + CREATE TABLE floatilla ( + id SERIAL PRIMARY KEY, + f32 float4, + f64 float8, + decimal_column decimal + ); + "# + }; + + api.execute(schema).await?; + + let (datamodel, engine) = api.create_engine().await?; + + datamodel.assert_model("floatilla", |model| { + model + .assert_field_type("f32", ScalarType::Float)? + .assert_field_type("f64", ScalarType::Float)? + .assert_field_type("decimal_column", ScalarType::Float) + })?; + + let query = indoc! { + r##" + mutation { + createOnefloatilla( + data: { + f32: 0.00006927, + f64: 0.00006927, + decimal_column: 0.00006927 + } + ) { + id + f32 + f64 + decimal_column + } + } + "## + }; + + let response = engine.request(query).await; + + let expected_response = json!({ + "data": { + "createOnefloatilla": { + "id": 1, + "f32": 0.00006927, + "f64": 0.00006927, + "decimal_column": 0.00006927 + } + } + }); + + assert_eq!(response, expected_response); + + Ok(()) +} From 90bebbc963b096bb2dce4fe26bacbe5fad3e4882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Houl=C3=A9?= Date: Fri, 6 Mar 2020 18:06:48 +0100 Subject: [PATCH 07/15] Test array types roundtrip on postgres --- Cargo.lock | 2 +- .../src/tests/type_mappings/postgres_types.rs | 166 ++++++++++++++++++ 2 files changed, 167 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 2ee3e22f1959..a8491494ac21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2110,7 +2110,7 @@ dependencies = [ [[package]] name = "quaint" version = "0.2.0-alpha.9" -source = "git+https://github.com/prisma/quaint#e971ded73ad13a391a40f168b4c443bc969d9fc4" +source = "git+https://github.com/prisma/quaint#fe5b8d7d8d11a8282307cccd48bdc883d15575f4" dependencies = [ "async-trait", "base64 0.11.0", diff --git a/query-engine/prisma/src/tests/type_mappings/postgres_types.rs b/query-engine/prisma/src/tests/type_mappings/postgres_types.rs index ac15be4a1c87..559e9943fcb6 100644 --- a/query-engine/prisma/src/tests/type_mappings/postgres_types.rs +++ b/query-engine/prisma/src/tests/type_mappings/postgres_types.rs @@ -129,6 +129,7 @@ async fn postgres_types_roundtrip(api: &TestApi) -> TestResult { string_char: "yeet" string_varchar: "yeet variable" string_text: "to yeet or not to yeet" + # binary_bytea: "test" binary_uuid: "111142ec-880b-4062-913d-8eac479ab957" time_timestamp: "2020-03-02T08:00:00.000" time_timestamptz: "2020-03-02T08:00:00.000" @@ -166,6 +167,7 @@ async fn postgres_types_roundtrip(api: &TestApi) -> TestResult { string_char string_varchar string_text + # binary_bytea binary_uuid time_timestamp time_timestamptz @@ -290,3 +292,167 @@ async fn small_float_values_must_work(api: &TestApi) -> TestResult { Ok(()) } + +const CREATE_ARRAY_TYPES_TABLE: &str = indoc! { + r##" + CREATE TABLE "prisma-tests"."arraytypes" ( + id SERIAL PRIMARY KEY, + numeric_int2 int2[], + numeric_int4 int4[], + numeric_int8 int8[], + + numeric_decimal decimal(8, 4)[], + numeric_float4 float4[], + numeric_float8 float8[], + + numeric_money money[], + numeric_oid oid[], + + string_char char(8)[], + string_varchar varchar(20)[], + string_text text[], + + binary_bytea bytea[], + binary_bits bit(80)[], + binary_bits_varying bit varying(80)[], + binary_uuid uuid[], + + time_timestamp timestamp[], + time_timestamptz timestamptz[], + time_date date[], + time_time time[], + time_timetz timetz[], + + boolean_boolean boolean[], + + network_cidr cidr[], + network_inet inet[], + + json_json json[], + json_jsonb jsonb[] + ); + "## +}; + +#[test_each_connector(tags("postgres"), log = "debug")] +async fn postgres_array_types_roundtrip(api: &TestApi) -> TestResult { + api.execute(CREATE_ARRAY_TYPES_TABLE).await?; + + let (datamodel, engine) = api.create_engine().await?; + + datamodel.assert_model("arraytypes", |model| { + model + .assert_field_type("numeric_int2", ScalarType::Int)? + .assert_field_type("numeric_int4", ScalarType::Int)? + .assert_field_type("numeric_int8", ScalarType::Int)? + .assert_field_type("numeric_decimal", ScalarType::Float)? + .assert_field_type("numeric_float4", ScalarType::Float)? + .assert_field_type("numeric_float8", ScalarType::Float)? + .assert_field_type("numeric_money", ScalarType::Float)? + .assert_field_type("numeric_oid", ScalarType::Int)? + .assert_field_type("string_char", ScalarType::String)? + .assert_field_type("string_varchar", ScalarType::String)? + .assert_field_type("string_text", ScalarType::String)? + .assert_field_type("binary_bytea", ScalarType::String)? + .assert_field_type("binary_bits", ScalarType::String)? + .assert_field_type("binary_bits_varying", ScalarType::String)? + .assert_field_type("binary_uuid", ScalarType::String)? + .assert_field_type("time_timestamp", ScalarType::DateTime)? + .assert_field_type("time_timestamptz", ScalarType::DateTime)? + .assert_field_type("time_date", ScalarType::DateTime)? + .assert_field_type("time_time", ScalarType::DateTime)? + .assert_field_type("time_timetz", ScalarType::DateTime)? + .assert_field_type("boolean_boolean", ScalarType::Boolean)? + .assert_field_type("network_inet", ScalarType::String)? + .assert_field_type("json_json", ScalarType::String)? + .assert_field_type("json_jsonb", ScalarType::String) + })?; + + let query = indoc! { + r##" + mutation { + createOnearraytypes( + data: { + numeric_int2: { set: [12] } + numeric_int4: { set: [9002] } + numeric_int8: { set: [100000000] } + numeric_decimal: { set: [49.3444] } + numeric_float4: { set: [12.12] } + numeric_float8: { set: [3.139428] } + numeric_money: { set: [3.50] } + numeric_oid: { set: [2000] } + string_char: { set: ["yeet"] } + string_varchar: { set: ["yeet variable"] } + string_text: { set: ["to yeet or not to yeet"] } + binary_uuid: { set: ["111142ec-880b-4062-913d-8eac479ab957"] } + time_timestamp: { set: ["2020-03-02T08:00:00.000"] } + time_timestamptz: { set: ["2020-03-02T08:00:00.000"] } + time_date: { set: ["2020-03-05T00:00:00.000"] } + time_time: { set: ["2020-03-05T08:00:00.000"] } + time_timetz: { set: ["2020-03-05T08:00:00.000"] } + boolean_boolean: { set: [true, true, false, true] } + network_inet: { set: ["192.168.100.14"] } + json_json: { set: ["{ \"isJson\": true }"] } + json_jsonb: { set: ["{ \"isJSONB\": true }"] } + } + ) { + numeric_int2 + numeric_int4 + numeric_int8 + numeric_decimal + numeric_float4 + numeric_float8 + numeric_money + numeric_oid + string_char + string_varchar + string_text + binary_uuid + time_timestamp + time_timestamptz + time_date + time_time + time_timetz + boolean_boolean + network_inet + json_json + json_jsonb + } + } + "## + }; + + let response = engine.request(query).await; + + let expected_response = json!({ + "data": { + "createOnearraytypes": { + "numeric_int2": [12], + "numeric_int4": [9002], + "numeric_int8": [100000000], + "numeric_decimal": [49.3444], + "numeric_float4": [12.12], + "numeric_float8": [3.139428], + "numeric_money": [3.5], + "numeric_oid": [2000], + "string_char": ["yeet "], + "string_varchar": ["yeet variable"], + "string_text": ["to yeet or not to yeet"], + "binary_uuid": ["111142ec-880b-4062-913d-8eac479ab957"], + "time_timestamp": ["2020-03-02T08:00:00.000Z"], + "time_timestamptz": ["2020-03-02T08:00:00.000Z"], + "time_date": ["2020-03-05T00:00:00.000Z"], + "time_time": ["1970-01-01T08:00:00.000Z"], + "time_timetz": ["1970-01-01T08:00:00.000Z"], + "boolean_boolean": [true, true, false, true], + "network_inet": ["192.168.100.14"], + "json_json": ["{\"isJson\":true}"], + "json_jsonb": ["{\"isJSONB\":true}"], + } + } + }); + + assert_eq!(response, expected_response); + + Ok(()) +} From 4844732acd786b44caf1ccbfe855d12c79f4289c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Houl=C3=A9?= Date: Mon, 9 Mar 2020 18:25:00 +0100 Subject: [PATCH 08/15] Make ScalarValue::Float double precision --- .../sql-introspection-connector/src/misc_helpers.rs | 4 ++-- .../connectors/datamodel-connector/src/scalars.rs | 2 +- libs/datamodel/core/src/common/value_validator.rs | 6 +++--- libs/prisma-value/src/lib.rs | 10 ---------- .../prisma/src/tests/type_mappings/postgres_types.rs | 2 +- 5 files changed, 7 insertions(+), 17 deletions(-) diff --git a/introspection-engine/connectors/sql-introspection-connector/src/misc_helpers.rs b/introspection-engine/connectors/sql-introspection-connector/src/misc_helpers.rs index 2a137cb9efa1..7dd71724c187 100644 --- a/introspection-engine/connectors/sql-introspection-connector/src/misc_helpers.rs +++ b/introspection-engine/connectors/sql-introspection-connector/src/misc_helpers.rs @@ -463,7 +463,7 @@ fn parse_bool(value: &str) -> Option { static RE_FLOAT: Lazy = Lazy::new(|| Regex::new(r"^'?([^']+)'?$").expect("compile regex")); -fn parse_float(value: &str) -> Option { +fn parse_float(value: &str) -> Option { debug!("Parsing float '{}'", value); let rslt = RE_FLOAT.captures(value); if rslt.is_none() { @@ -473,7 +473,7 @@ fn parse_float(value: &str) -> Option { let captures = rslt.expect("get captures"); let num_str = captures.get(1).expect("get capture").as_str(); - let num_rslt = num_str.parse::(); + let num_rslt = num_str.parse::(); match num_rslt { Ok(num) => Some(num), Err(_) => { diff --git a/libs/datamodel/connectors/datamodel-connector/src/scalars.rs b/libs/datamodel/connectors/datamodel-connector/src/scalars.rs index 8f761e254374..124e540a845d 100644 --- a/libs/datamodel/connectors/datamodel-connector/src/scalars.rs +++ b/libs/datamodel/connectors/datamodel-connector/src/scalars.rs @@ -43,7 +43,7 @@ impl ToString for ScalarType { #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub enum ScalarValue { Int(i32), - Float(f32), + Float(f64), Decimal(f64), Boolean(bool), String(String), diff --git a/libs/datamodel/core/src/common/value_validator.rs b/libs/datamodel/core/src/common/value_validator.rs index e4733244e73a..cec388dd8ef8 100644 --- a/libs/datamodel/core/src/common/value_validator.rs +++ b/libs/datamodel/core/src/common/value_validator.rs @@ -115,10 +115,10 @@ impl ValueValidator { } /// Tries to convert the wrapped value to a Prisma Float. - pub fn as_float(&self) -> Result { + pub fn as_float(&self) -> Result { match &self.value { - ast::Expression::NumericValue(value, _) => self.wrap_error_from_result(value.parse::(), "numeric"), - ast::Expression::Any(value, _) => self.wrap_error_from_result(value.parse::(), "numeric"), + ast::Expression::NumericValue(value, _) => self.wrap_error_from_result(value.parse::(), "numeric"), + ast::Expression::Any(value, _) => self.wrap_error_from_result(value.parse::(), "numeric"), _ => Err(self.construct_type_mismatch_error("numeric")), } } diff --git a/libs/prisma-value/src/lib.rs b/libs/prisma-value/src/lib.rs index e759153bfd82..ace486df9a0b 100644 --- a/libs/prisma-value/src/lib.rs +++ b/libs/prisma-value/src/lib.rs @@ -140,16 +140,6 @@ impl TryFrom for PrismaValue { fn try_from(f: f64) -> PrismaValueResult { Decimal::from_f64(f) - .map(|d| PrismaValue::Float(d)) - .ok_or(ConversionFailure::new("f32", "Decimal")) - } -} - -impl TryFrom for PrismaValue { - type Error = ConversionFailure; - - fn try_from(f: f32) -> PrismaValueResult { - Decimal::from_f32(f) .map(|d| PrismaValue::Float(d)) .ok_or(ConversionFailure::new("f64", "Decimal")) } diff --git a/query-engine/prisma/src/tests/type_mappings/postgres_types.rs b/query-engine/prisma/src/tests/type_mappings/postgres_types.rs index 559e9943fcb6..e1cbc5019d3f 100644 --- a/query-engine/prisma/src/tests/type_mappings/postgres_types.rs +++ b/query-engine/prisma/src/tests/type_mappings/postgres_types.rs @@ -232,7 +232,7 @@ async fn postgres_types_roundtrip(api: &TestApi) -> TestResult { Ok(()) } -#[test_each_connector(tags("postgres"))] +#[test_each_connector(tags("postgres"), log = "debug")] async fn small_float_values_must_work(api: &TestApi) -> TestResult { let schema = indoc! { r#" From 4304b88ce3e01d7b7bd1ecf425d7c62dbdeceee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Houl=C3=A9?= Date: Tue, 10 Mar 2020 13:40:11 +0100 Subject: [PATCH 09/15] Replace f64->Decimal conversion with f64->String->Decimal It appears to be a bug in rust_decimal. Issue link: https://github.com/paupino/rust-decimal/issues/228 --- .../tests/postgres_introspection_tests.rs | 2 +- query-engine/connectors/sql-query-connector/src/row.rs | 10 +++++++--- .../src/request_handlers/graphql/protocol_adapter.rs | 6 +++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/libs/sql-schema-describer/tests/postgres_introspection_tests.rs b/libs/sql-schema-describer/tests/postgres_introspection_tests.rs index 75ba4a4b461c..fc72e053443a 100644 --- a/libs/sql-schema-describer/tests/postgres_introspection_tests.rs +++ b/libs/sql-schema-describer/tests/postgres_introspection_tests.rs @@ -327,7 +327,7 @@ async fn all_postgres_column_types_must_work() { name: "interval_col".into(), tpe: ColumnType { raw: "interval".into(), - family: ColumnTypeFamily::DateTime, + family: ColumnTypeFamily::String, arity: ColumnArity::Required, }, diff --git a/query-engine/connectors/sql-query-connector/src/row.rs b/query-engine/connectors/sql-query-connector/src/row.rs index 333dced41dc3..6017bc06a321 100644 --- a/query-engine/connectors/sql-query-connector/src/row.rs +++ b/query-engine/connectors/sql-query-connector/src/row.rs @@ -158,10 +158,14 @@ pub fn row_value_to_prisma_value( }, TypeIdentifier::String => match p_value { ParameterizedValue::Uuid(uuid) => PrismaValue::String(uuid.to_string()), - ParameterizedValue::Json(json_value) => PrismaValue::String(serde_json::to_string(&json_value).expect("JSON value to string")), + ParameterizedValue::Json(json_value) => { + PrismaValue::String(serde_json::to_string(&json_value).expect("JSON value to string")) + } + ParameterizedValue::Null => PrismaValue::Null, _ => p_value.into_string().map(PrismaValue::String).ok_or_else(|| { - SqlError::ConversionError(failure::format_err!("Could not extract text value from result set")) })?, - } + SqlError::ConversionError(failure::format_err!("Could not extract text value from result set")) + })?, + }, }) } diff --git a/query-engine/prisma/src/request_handlers/graphql/protocol_adapter.rs b/query-engine/prisma/src/request_handlers/graphql/protocol_adapter.rs index 1c69770bbcc4..978b82da6abf 100644 --- a/query-engine/prisma/src/request_handlers/graphql/protocol_adapter.rs +++ b/query-engine/prisma/src/request_handlers/graphql/protocol_adapter.rs @@ -3,8 +3,8 @@ use graphql_parser::query::{ Definition, Document, OperationDefinition, Selection as GqlSelection, SelectionSet, Value, }; use query_core::query_document::*; -use rust_decimal::{prelude::FromPrimitive, Decimal}; -use std::collections::BTreeMap; +use rust_decimal::Decimal; +use std::{collections::BTreeMap, str::FromStr}; /// Protocol adapter for GraphQL -> Query Document. /// @@ -145,7 +145,7 @@ impl GraphQLProtocolAdapter { i ))), }, - Value::Float(f) => match Decimal::from_f64(f) { + Value::Float(f) => match Decimal::from_str(&f.to_string()).ok() { Some(dec) => Ok(QueryValue::Float(dec)), None => Err(PrismaError::QueryConversionError(format!( "invalid 64-bit float: {:?}", From 9d4c7f9673ed024ee8f71e47e9b7c5d3f25c2d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Houl=C3=A9?= Date: Tue, 10 Mar 2020 16:52:26 +0100 Subject: [PATCH 10/15] Update quaint --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index a8491494ac21..4ae6b067cb73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2110,7 +2110,7 @@ dependencies = [ [[package]] name = "quaint" version = "0.2.0-alpha.9" -source = "git+https://github.com/prisma/quaint#fe5b8d7d8d11a8282307cccd48bdc883d15575f4" +source = "git+https://github.com/prisma/quaint#5c85aed15976ebd8c9004251302424a587c8840b" dependencies = [ "async-trait", "base64 0.11.0", From 2b50140d5255b1e71c639610cb2018df2d95fbb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Houl=C3=A9?= Date: Tue, 10 Mar 2020 18:35:31 +0100 Subject: [PATCH 11/15] Never fail PrismaValue conversions on postgres --- query-engine/connectors/sql-query-connector/src/row.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/query-engine/connectors/sql-query-connector/src/row.rs b/query-engine/connectors/sql-query-connector/src/row.rs index 6017bc06a321..3ef7a49ee480 100644 --- a/query-engine/connectors/sql-query-connector/src/row.rs +++ b/query-engine/connectors/sql-query-connector/src/row.rs @@ -162,9 +162,7 @@ pub fn row_value_to_prisma_value( PrismaValue::String(serde_json::to_string(&json_value).expect("JSON value to string")) } ParameterizedValue::Null => PrismaValue::Null, - _ => p_value.into_string().map(PrismaValue::String).ok_or_else(|| { - SqlError::ConversionError(failure::format_err!("Could not extract text value from result set")) - })?, + other => PrismaValue::from(other), }, }) } From e241cf411251e120c548178942b7731aafd7fb68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Houl=C3=A9?= Date: Wed, 11 Mar 2020 09:15:07 +0100 Subject: [PATCH 12/15] Try to convert mysql byte values to string --- libs/prisma-value/src/sql_ext.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/prisma-value/src/sql_ext.rs b/libs/prisma-value/src/sql_ext.rs index 66a86a74c22b..692d13778d75 100644 --- a/libs/prisma-value/src/sql_ext.rs +++ b/libs/prisma-value/src/sql_ext.rs @@ -15,7 +15,9 @@ impl<'a> From> for PrismaValue { ParameterizedValue::Uuid(uuid) => PrismaValue::Uuid(uuid), ParameterizedValue::DateTime(dt) => PrismaValue::DateTime(dt), ParameterizedValue::Char(c) => PrismaValue::String(c.to_string()), - ParameterizedValue::Bytes(_bytes) => unreachable!("PrismaValue::Bytes"), + ParameterizedValue::Bytes(bytes) => PrismaValue::String( + String::from_utf8(bytes.into_owned()).expect("PrismaValue::String from ParameterizedValue::Bytes"), + ), } } } From b04fb096dcde5db9f91975851abaaff604e1f5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Houl=C3=A9?= Date: Wed, 11 Mar 2020 11:04:05 +0100 Subject: [PATCH 13/15] Bring minor improvements to row.rs --- .../connectors/sql-query-connector/src/row.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/query-engine/connectors/sql-query-connector/src/row.rs b/query-engine/connectors/sql-query-connector/src/row.rs index 3ef7a49ee480..e254273262b7 100644 --- a/query-engine/connectors/sql-query-connector/src/row.rs +++ b/query-engine/connectors/sql-query-connector/src/row.rs @@ -33,6 +33,7 @@ impl ToSqlRow for ResultRow { fn to_sql_row<'b>(self, idents: &[(TypeIdentifier, FieldArity)]) -> crate::Result { let mut row = SqlRow::default(); let row_width = idents.len(); + row.values.reserve(row_width); for (i, p_value) in self.into_iter().enumerate().take(row_width) { let pv = match &idents[i] { (type_identifier, FieldArity::List) => match p_value { @@ -118,7 +119,10 @@ pub fn row_value_to_prisma_value( ParameterizedValue::Text(dt_string) => { let dt = DateTime::parse_from_rfc3339(dt_string.borrow()) .or_else(|_| DateTime::parse_from_rfc2822(dt_string.borrow())) - .expect(&format!("Could not parse stored DateTime string: {}", dt_string)); + .map_err(|err| { + failure::format_err!("Could not parse stored DateTime string: {} ({})", dt_string, err) + }) + .unwrap(); PrismaValue::DateTime(dt.with_timezone(&Utc)) } @@ -136,9 +140,13 @@ pub fn row_value_to_prisma_value( ParameterizedValue::Integer(i) => { PrismaValue::Float(Decimal::from_f64(i as f64).expect("f64 was not a Decimal.")) } - ParameterizedValue::Text(_) | ParameterizedValue::Bytes(_) => { - PrismaValue::Float(p_value.as_str().unwrap().parse().unwrap()) - } + ParameterizedValue::Text(_) | ParameterizedValue::Bytes(_) => PrismaValue::Float( + p_value + .as_str() + .expect("text/bytes as str") + .parse() + .map_err(|err: rust_decimal::Error| SqlError::ColumnReadFailure(err.into()))?, + ), _ => { let error = io::Error::new( io::ErrorKind::InvalidData, From 63956d1d09c3013c0dd3e138e8b1e5996ac5dca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Houl=C3=A9?= Date: Wed, 11 Mar 2020 11:22:24 +0100 Subject: [PATCH 14/15] Add a comment about the rust_decimal bug on ingestion --- .../prisma/src/request_handlers/graphql/protocol_adapter.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/query-engine/prisma/src/request_handlers/graphql/protocol_adapter.rs b/query-engine/prisma/src/request_handlers/graphql/protocol_adapter.rs index 978b82da6abf..739a227c8341 100644 --- a/query-engine/prisma/src/request_handlers/graphql/protocol_adapter.rs +++ b/query-engine/prisma/src/request_handlers/graphql/protocol_adapter.rs @@ -145,6 +145,8 @@ impl GraphQLProtocolAdapter { i ))), }, + // We can't use Decimal::from_f64 here due to a bug in rust_decimal. + // Issue: https://github.com/paupino/rust-decimal/issues/228 Value::Float(f) => match Decimal::from_str(&f.to_string()).ok() { Some(dec) => Ok(QueryValue::Float(dec)), None => Err(PrismaError::QueryConversionError(format!( From 8d8ffad4ca31bfebccb4affe764e974fcb86a378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Houl=C3=A9?= Date: Wed, 11 Mar 2020 11:33:26 +0100 Subject: [PATCH 15/15] Change some naming in the IE+QE test api --- .../prisma/src/tests/type_mappings/mysql_types.rs | 8 ++++---- .../prisma/src/tests/type_mappings/postgres_types.rs | 12 ++++++------ .../prisma/src/tests/type_mappings/test_api.rs | 6 ++---- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/query-engine/prisma/src/tests/type_mappings/mysql_types.rs b/query-engine/prisma/src/tests/type_mappings/mysql_types.rs index fe4592375b0d..802fd9b7ef59 100644 --- a/query-engine/prisma/src/tests/type_mappings/mysql_types.rs +++ b/query-engine/prisma/src/tests/type_mappings/mysql_types.rs @@ -55,9 +55,9 @@ const CREATE_TYPES_TABLE: &str = indoc! { #[test_each_connector(tags("mysql"))] async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { - api.execute(CREATE_TYPES_TABLE).await?; + api.execute_sql(CREATE_TYPES_TABLE).await?; - let (datamodel, engine) = api.create_engine().await?; + let (datamodel, engine) = api.introspect_and_start_query_engine().await?; datamodel.assert_model("types", |model| { model @@ -241,9 +241,9 @@ async fn mysql_types_roundtrip(api: &TestApi) -> TestResult { #[test_each_connector(tags("mysql"))] async fn mysql_bit_columns_are_properly_mapped_to_signed_integers(api: &TestApi) -> TestResult { - api.execute(CREATE_TYPES_TABLE).await?; + api.execute_sql(CREATE_TYPES_TABLE).await?; - let (_datamodel, engine) = api.create_engine().await?; + let (_datamodel, engine) = api.introspect_and_start_query_engine().await?; let write = indoc! { " diff --git a/query-engine/prisma/src/tests/type_mappings/postgres_types.rs b/query-engine/prisma/src/tests/type_mappings/postgres_types.rs index e1cbc5019d3f..fff12ef73890 100644 --- a/query-engine/prisma/src/tests/type_mappings/postgres_types.rs +++ b/query-engine/prisma/src/tests/type_mappings/postgres_types.rs @@ -64,9 +64,9 @@ const CREATE_TYPES_TABLE: &str = indoc! { #[test_each_connector(tags("postgres"), log = "debug")] async fn postgres_types_roundtrip(api: &TestApi) -> TestResult { - api.execute(CREATE_TYPES_TABLE).await?; + api.execute_sql(CREATE_TYPES_TABLE).await?; - let (datamodel, engine) = api.create_engine().await?; + let (datamodel, engine) = api.introspect_and_start_query_engine().await?; datamodel.assert_model("types", |model| { model @@ -245,9 +245,9 @@ async fn small_float_values_must_work(api: &TestApi) -> TestResult { "# }; - api.execute(schema).await?; + api.execute_sql(schema).await?; - let (datamodel, engine) = api.create_engine().await?; + let (datamodel, engine) = api.introspect_and_start_query_engine().await?; datamodel.assert_model("floatilla", |model| { model @@ -336,9 +336,9 @@ const CREATE_ARRAY_TYPES_TABLE: &str = indoc! { #[test_each_connector(tags("postgres"), log = "debug")] async fn postgres_array_types_roundtrip(api: &TestApi) -> TestResult { - api.execute(CREATE_ARRAY_TYPES_TABLE).await?; + api.execute_sql(CREATE_ARRAY_TYPES_TABLE).await?; - let (datamodel, engine) = api.create_engine().await?; + let (datamodel, engine) = api.introspect_and_start_query_engine().await?; datamodel.assert_model("arraytypes", |model| { model diff --git a/query-engine/prisma/src/tests/type_mappings/test_api.rs b/query-engine/prisma/src/tests/type_mappings/test_api.rs index 6df638ab0072..836c816ebbdf 100644 --- a/query-engine/prisma/src/tests/type_mappings/test_api.rs +++ b/query-engine/prisma/src/tests/type_mappings/test_api.rs @@ -24,7 +24,7 @@ impl TestApi { ) } - pub async fn execute(&self, sql: &str) -> anyhow::Result<()> { + pub async fn execute_sql(&self, sql: &str) -> anyhow::Result<()> { let conn = quaint::single::Quaint::new(&self.database_string).await?; conn.execute_raw(sql, &[]).await?; @@ -32,11 +32,9 @@ impl TestApi { Ok(()) } - pub async fn create_engine(&self) -> anyhow::Result<(DatamodelAssertions, QueryEngine)> { + pub async fn introspect_and_start_query_engine(&self) -> anyhow::Result<(DatamodelAssertions, QueryEngine)> { let datasource = self.datasource(); - dbg!(&datasource); - let schema = introspection_core::RpcImpl::introspect_internal(datasource) .await .map_err(|err| anyhow::anyhow!("{:?}", err.data))?;