diff --git a/psl/builtin-connectors/src/lib.rs b/psl/builtin-connectors/src/lib.rs index dc1a33b1bc8..7c1aaec66ba 100644 --- a/psl/builtin-connectors/src/lib.rs +++ b/psl/builtin-connectors/src/lib.rs @@ -14,6 +14,7 @@ mod mongodb; mod mssql_datamodel_connector; mod mysql_datamodel_connector; mod native_type_definition; +mod planetscale; mod postgres_datamodel_connector; mod sqlite_datamodel_connector; @@ -25,5 +26,14 @@ pub const MYSQL: &'static dyn Connector = &mysql_datamodel_connector::MySqlDatam pub const SQLITE: &'static dyn Connector = &sqlite_datamodel_connector::SqliteDatamodelConnector; pub const MSSQL: &'static dyn Connector = &mssql_datamodel_connector::MsSqlDatamodelConnector; pub const MONGODB: &'static dyn Connector = &mongodb::MongoDbDatamodelConnector; +pub static PLANETSCALE_SERVERLESS: &'static dyn Connector = &planetscale::PLANETSCALE_SERVERLESS; -pub const BUILTIN_CONNECTORS: ConnectorRegistry = &[POSTGRES, MYSQL, SQLITE, MSSQL, COCKROACH, MONGODB]; +pub static BUILTIN_CONNECTORS: ConnectorRegistry = &[ + POSTGRES, + MYSQL, + SQLITE, + MSSQL, + COCKROACH, + MONGODB, + PLANETSCALE_SERVERLESS, +]; diff --git a/psl/builtin-connectors/src/mysql_datamodel_connector.rs b/psl/builtin-connectors/src/mysql_datamodel_connector.rs index 3e2ae23ba5a..2db658fdf46 100644 --- a/psl/builtin-connectors/src/mysql_datamodel_connector.rs +++ b/psl/builtin-connectors/src/mysql_datamodel_connector.rs @@ -87,7 +87,7 @@ impl Connector for MySqlDatamodelConnector { } fn is_provider(&self, name: &str) -> bool { - name == "mysql" || name == "@prisma/mysql" + name == "mysql" } fn capabilities(&self) -> ConnectorCapabilities { diff --git a/psl/builtin-connectors/src/planetscale.rs b/psl/builtin-connectors/src/planetscale.rs new file mode 100644 index 00000000000..1999f6a353f --- /dev/null +++ b/psl/builtin-connectors/src/planetscale.rs @@ -0,0 +1,15 @@ +use crate::mysql_datamodel_connector; +use psl_core::{ + datamodel_connector::RelationMode, + js_connector::{Flavor, JsConnector}, +}; + +pub(crate) static PLANETSCALE_SERVERLESS: JsConnector = JsConnector { + flavor: Flavor::MySQL, + canonical_connector: &mysql_datamodel_connector::MySqlDatamodelConnector, + + provider_name: "@prisma/planetscale", + name: "planetscale serverless", + enforced_relation_mode: Some(RelationMode::Prisma), + allowed_protocols: Some(&["mysql", "https", "mysqls"]), +}; diff --git a/psl/psl-core/src/datamodel_connector.rs b/psl/psl-core/src/datamodel_connector.rs index f26359baee9..e4822844531 100644 --- a/psl/psl-core/src/datamodel_connector.rs +++ b/psl/psl-core/src/datamodel_connector.rs @@ -24,7 +24,9 @@ pub use self::{ relation_mode::RelationMode, }; -use crate::{configuration::DatasourceConnectorData, Configuration, Datasource, PreviewFeature}; +use crate::{ + configuration::DatasourceConnectorData, js_connector::JsConnector, Configuration, Datasource, PreviewFeature, +}; use diagnostics::{DatamodelError, Diagnostics, NativeTypeErrorFactory, Span}; use enumflags2::BitFlags; use lsp_types::CompletionList; @@ -41,6 +43,11 @@ pub const EXTENSIONS_KEY: &str = "extensions"; /// The datamodel connector API. pub trait Connector: Send + Sync { + // Provides safe downcasting to a JsConnector, in case it is one. + fn as_js_connector(&self) -> Option { + None + } + /// The name of the provider, for string comparisons determining which connector we are on. fn provider_name(&self) -> &'static str; diff --git a/psl/psl-core/src/js_connector.rs b/psl/psl-core/src/js_connector.rs new file mode 100644 index 00000000000..36c872804ec --- /dev/null +++ b/psl/psl-core/src/js_connector.rs @@ -0,0 +1,126 @@ +use crate::datamodel_connector::*; +use enumflags2::BitFlags; + +/// JsConnector represents a type of connector that is implemented partially +/// in javascript and used from rust through the js-connectors crate +/// +/// Rather than a unit struct per individual connector, like we have for the rest +/// of the builtin connectors, we have a single struct which state represents the +/// features that vary in this connector with respect to a cannonical connector +/// for the flavor of SQL the particular JsConnector speaks. +/// +/// For example, the _planetscale serverless_ connector is compatible with MySQL, +/// so it reuses the builtin MySQL connector (the cannonical for the MySQL flavor) +/// for most of its features. +#[derive(Copy, Clone)] +pub struct JsConnector { + pub flavor: Flavor, + pub canonical_connector: &'static dyn Connector, + + pub provider_name: &'static str, + pub name: &'static str, + pub enforced_relation_mode: Option, + pub allowed_protocols: Option<&'static [&'static str]>, +} + +#[derive(Copy, Clone)] +pub enum Flavor { + MySQL, +} + +impl Connector for JsConnector { + fn as_js_connector(&self) -> Option { + Some(*self) + } + + fn provider_name(&self) -> &'static str { + self.provider_name + } + + fn name(&self) -> &str { + self.name + } + + fn capabilities(&self) -> ConnectorCapabilities { + self.canonical_connector.capabilities() + } + + fn max_identifier_length(&self) -> usize { + self.canonical_connector.max_identifier_length() + } + + fn referential_actions(&self) -> enumflags2::BitFlags { + self.canonical_connector.referential_actions() + } + + fn available_native_type_constructors(&self) -> &'static [NativeTypeConstructor] { + self.canonical_connector.available_native_type_constructors() + } + + fn scalar_type_for_native_type(&self, native_type: &NativeTypeInstance) -> parser_database::ScalarType { + self.canonical_connector.scalar_type_for_native_type(native_type) + } + + fn default_native_type_for_scalar_type(&self, scalar_type: &parser_database::ScalarType) -> NativeTypeInstance { + self.canonical_connector + .default_native_type_for_scalar_type(scalar_type) + } + + fn native_type_is_default_for_scalar_type( + &self, + native_type: &NativeTypeInstance, + scalar_type: &parser_database::ScalarType, + ) -> bool { + self.canonical_connector + .native_type_is_default_for_scalar_type(native_type, scalar_type) + } + + fn native_type_to_parts(&self, native_type: &NativeTypeInstance) -> (&'static str, Vec) { + self.canonical_connector.native_type_to_parts(native_type) + } + + fn parse_native_type( + &self, + name: &str, + args: &[String], + span: diagnostics::Span, + diagnostics: &mut diagnostics::Diagnostics, + ) -> Option { + self.canonical_connector + .parse_native_type(name, args, span, diagnostics) + } + + fn validate_url(&self, url: &str) -> Result<(), String> { + if let Some(allowed_protocols) = self.allowed_protocols { + let scheme = url.split(':').next().unwrap_or(""); + if allowed_protocols.contains(&scheme) { + Ok(()) + } else { + Err(format!( + "The URL scheme `{}` is not valid for the {} connector. The following schemes are allowed: {}", + scheme, + self.name, + allowed_protocols.join(", ") + )) + } + } else { + self.canonical_connector.validate_url(url) + } + } + + fn default_relation_mode(&self) -> RelationMode { + if let Some(relation_mode) = self.enforced_relation_mode { + relation_mode + } else { + self.canonical_connector.default_relation_mode() + } + } + + fn allowed_relation_mode_settings(&self) -> BitFlags { + if let Some(relation_mode) = self.enforced_relation_mode { + BitFlags::from(relation_mode) + } else { + self.canonical_connector.allowed_relation_mode_settings() + } + } +} diff --git a/psl/psl-core/src/lib.rs b/psl/psl-core/src/lib.rs index bdc234528de..29528b77cc2 100644 --- a/psl/psl-core/src/lib.rs +++ b/psl/psl-core/src/lib.rs @@ -3,6 +3,7 @@ #![allow(clippy::derive_partial_eq_without_eq)] pub mod datamodel_connector; +pub mod js_connector; /// `mcf`: Turns a collection of `configuration::Datasource` and `configuration::Generator` into a /// JSON representation. This is the `get_config()` representation. diff --git a/psl/psl/tests/validation/js_connectors/prisma_planetscale.prisma b/psl/psl/tests/validation/js_connectors/prisma_planetscale.prisma new file mode 100644 index 00000000000..42936ac0299 --- /dev/null +++ b/psl/psl/tests/validation/js_connectors/prisma_planetscale.prisma @@ -0,0 +1,8 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "@prisma/planetscale" + url = "mysql://" +} diff --git a/psl/psl/tests/validation/js_connectors/prisma_planetscale_doesnt_support_relation_mode_fk.prisma b/psl/psl/tests/validation/js_connectors/prisma_planetscale_doesnt_support_relation_mode_fk.prisma new file mode 100644 index 00000000000..489bea81f32 --- /dev/null +++ b/psl/psl/tests/validation/js_connectors/prisma_planetscale_doesnt_support_relation_mode_fk.prisma @@ -0,0 +1,15 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "@prisma/planetscale" + url = "mysql://" + relationMode = "foreignKeys" +} +// error: Error validating datasource `relationMode`: Invalid relation mode setting: "foreignKeys". Supported values: "prisma" +// --> schema.prisma:8 +//  |  +//  7 |  url = "mysql://" +//  8 |  relationMode = "foreignKeys" +//  |  diff --git a/psl/psl/tests/validation/js_connectors/prisma_planetscale_https_url.prisma b/psl/psl/tests/validation/js_connectors/prisma_planetscale_https_url.prisma new file mode 100644 index 00000000000..65e37cc69a4 --- /dev/null +++ b/psl/psl/tests/validation/js_connectors/prisma_planetscale_https_url.prisma @@ -0,0 +1,8 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "@prisma/planetscale" + url = "https://" +} diff --git a/psl/psl/tests/validation/js_connectors/prisma_planetscale_mysqls_url.prisma b/psl/psl/tests/validation/js_connectors/prisma_planetscale_mysqls_url.prisma new file mode 100644 index 00000000000..67453789be1 --- /dev/null +++ b/psl/psl/tests/validation/js_connectors/prisma_planetscale_mysqls_url.prisma @@ -0,0 +1,8 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "@prisma/planetscale" + url = "mysqls://" +} diff --git a/psl/psl/tests/validation/mysql/node_drivers/prisma_mysql.prisma b/psl/psl/tests/validation/mysql/node_drivers/prisma_mysql.prisma deleted file mode 100644 index 74ef74884d3..00000000000 --- a/psl/psl/tests/validation/mysql/node_drivers/prisma_mysql.prisma +++ /dev/null @@ -1,9 +0,0 @@ -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "@prisma/mysql" - url = "mysql://" - relationMode = "prisma" -} diff --git a/quaint/src/connector/mysql.rs b/quaint/src/connector/mysql.rs index 68f4ac28fc3..ce116dd2df6 100644 --- a/quaint/src/connector/mysql.rs +++ b/quaint/src/connector/mysql.rs @@ -22,7 +22,7 @@ use std::{ time::Duration, }; use tokio::sync::Mutex; -use url::Url; +use url::{Host, Url}; /// The underlying MySQL driver. Only available with the `expose-drivers` /// Cargo feature. @@ -98,7 +98,18 @@ impl MysqlUrl { /// The database host. If `socket` and `host` are not set, defaults to `localhost`. pub fn host(&self) -> &str { - self.url.host_str().unwrap_or("localhost") + match (self.url.host(), self.url.host_str()) { + (Some(Host::Ipv6(_)), Some(host)) => { + // The `url` crate may return an IPv6 address in brackets, which must be stripped. + if host.starts_with('[') && host.ends_with(']') { + &host[1..host.len() - 1] + } else { + host + } + } + (_, Some(host)) => host, + _ => "localhost", + } } /// If set, connected to the database through a Unix socket. @@ -604,6 +615,12 @@ mod tests { assert!(!url.query_params.ssl_opts.accept_invalid_certs()); } + #[test] + fn should_parse_ipv6_host() { + let url = MysqlUrl::new(Url::parse("mysql://[2001:db8:1234::ffff]:5432/testdb").unwrap()).unwrap(); + assert_eq!("2001:db8:1234::ffff", url.host()); + } + #[test] fn should_allow_changing_of_cache_size() { let url = MysqlUrl::new(Url::parse("mysql:///root:root@localhost:3307/foo?statement_cache_size=420").unwrap()) diff --git a/quaint/src/connector/postgres.rs b/quaint/src/connector/postgres.rs index 50ecdf09eb1..efa414dc246 100644 --- a/quaint/src/connector/postgres.rs +++ b/quaint/src/connector/postgres.rs @@ -25,7 +25,7 @@ use tokio_postgres::{ config::{ChannelBinding, SslMode}, Client, Config, Statement, }; -use url::Url; +use url::{Host, Url}; pub(crate) const DEFAULT_SCHEMA: &str = "public"; @@ -223,11 +223,19 @@ impl PostgresUrl { /// /// If none of them are set, defaults to `localhost`. pub fn host(&self) -> &str { - match (self.query_params.host.as_ref(), self.url.host_str()) { - (Some(host), _) => host.as_str(), - (None, Some("")) => "localhost", - (None, None) => "localhost", - (None, Some(host)) => host, + match (self.query_params.host.as_ref(), self.url.host_str(), self.url.host()) { + (Some(host), _, _) => host.as_str(), + (None, Some(""), _) => "localhost", + (None, None, _) => "localhost", + (None, Some(host), Some(Host::Ipv6(_))) => { + // The `url` crate may return an IPv6 address in brackets, which must be stripped. + if host.starts_with('[') && host.ends_with(']') { + &host[1..host.len() - 1] + } else { + host + } + } + (None, Some(host), _) => host, } } @@ -1142,6 +1150,12 @@ mod tests { assert_eq!("localhost", url.host()); } + #[test] + fn should_parse_ipv6_host() { + let url = PostgresUrl::new(Url::parse("postgresql://[2001:db8:1234::ffff]:5432/dbname").unwrap()).unwrap(); + assert_eq!("2001:db8:1234::ffff", url.host()); + } + #[test] fn should_handle_options_field() { let url = PostgresUrl::new(Url::parse("postgresql:///localhost:5432?options=--cluster%3Dmy_cluster").unwrap())