-
Notifications
You must be signed in to change notification settings - Fork 245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(driver-adapters): enable Wasm on quaint
and related crates
#4442
Conversation
CodSpeed Performance ReportMerging #4442 will not alter performanceComparing Summary
|
EDIT: solved by 5ab6d96. Current blocker
Proposed strategy:
|
#[derive(Debug, Error)] | ||
enum MysqlAsyncError { | ||
#[error("Server error: `{}'", _0)] | ||
Server(#[source] MysqlError), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a partial copy of the mysql_async::Error
enum.
Importing mysql_async
here would break the Wasm compilation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Could you please add a comment in the source code explaining this comment, and pointing to the source code of the original Enum, where you grab this variant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure: bacb635
@@ -102,7 +127,7 @@ branch = "vendored-openssl" | |||
|
|||
[dependencies.rusqlite] | |||
version = "0.29" | |||
features = ["chrono", "bundled", "column_decltype"] | |||
features = ["chrono", "column_decltype"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The bundled
feature for rusqlite
is enabled when the sqlite-native
feature of quaint
(implied by native
and all
) is turned on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alberto and I reviewed this code together. We discussed some of the decisions made, and some potential improvements that are commented below, but are not blocking at all.
One piece of actionable feedback that we talked about is that the wasm folders for each provider should include code that only will be linked when compiling to wasm-unknown-unknown
, and native modules should not depend on anything within this folder.
As such we are going to make a small refactoring to move from this set of dependencies:
provider::native -> provider::wasm::common
provider::wasm -> provider::wasm::common
to
provider::native -> provider::common
provider::wasm -> provider::common
Other than that the code looks good enough and further refactorings that have to do with legacy aspects of the code, will be tackled independently (@jkomyno will create them)
@@ -0,0 +1,256 @@ | |||
//! Definitions for the MSSQL connector. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the answer is no, but to double check with you, files within the native directories, don't contain any new definitions, but are only part of the diff, because a slice of it was moved from the previous files at the root of any provider folder. Am I right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, Miguel.
use tokio_util::compat::{Compat, TokioAsyncWriteCompatExt}; | ||
|
||
/// The underlying SQL Server driver. Only available with the `expose-drivers` Cargo feature. | ||
#[cfg(feature = "expose-drivers")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we remove this feature, given that we don't publish quaint anymore? This is not blocking at all, but if we are not using it, and it's not very difficult to remove it, let's get rid of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
connect_timeout = match as_int { | ||
0 => None, | ||
_ => Some(Duration::from_secs(as_int)), | ||
}; | ||
} | ||
"pool_timeout" => { | ||
let as_int = v | ||
.parse::<u64>() | ||
.map_err(|_| Error::builder(ErrorKind::InvalidConnectionArguments).build())?; | ||
|
||
pool_timeout = match as_int { | ||
0 => None, | ||
_ => Some(Duration::from_secs(as_int)), | ||
}; | ||
} | ||
"sslaccept" => { | ||
use_ssl = true; | ||
match v.as_ref() { | ||
"strict" => { | ||
#[cfg(feature = "mysql-native")] | ||
{ | ||
ssl_opts = ssl_opts.with_danger_accept_invalid_certs(false); | ||
} | ||
} | ||
"accept_invalid_certs" => {} | ||
_ => { | ||
tracing::debug!( | ||
message = "Unsupported SSL accept mode, defaulting to `accept_invalid_certs`", | ||
mode = &*v | ||
); | ||
} | ||
}; | ||
} | ||
"max_connection_lifetime" => { | ||
let as_int = v | ||
.parse() | ||
.map_err(|_| Error::builder(ErrorKind::InvalidConnectionArguments).build())?; | ||
|
||
if as_int == 0 { | ||
max_connection_lifetime = None; | ||
} else { | ||
max_connection_lifetime = Some(Duration::from_secs(as_int)); | ||
} | ||
} | ||
"max_idle_connection_lifetime" => { | ||
let as_int = v | ||
.parse() | ||
.map_err(|_| Error::builder(ErrorKind::InvalidConnectionArguments).build())?; | ||
|
||
if as_int == 0 { | ||
max_idle_connection_lifetime = None; | ||
} else { | ||
max_idle_connection_lifetime = Some(Duration::from_secs(as_int)); | ||
} | ||
} | ||
_ => { | ||
tracing::trace!(message = "Discarding connection string param", param = &*k); | ||
} | ||
}; | ||
} | ||
|
||
// Wrapping this in a block, as attributes on expressions are still experimental | ||
// See: https://github.com/rust-lang/rust/issues/15701 | ||
#[cfg(feature = "mysql-native")] | ||
{ | ||
ssl_opts = match identity { | ||
Some((Some(path), Some(pw))) => { | ||
let identity = mysql_async::ClientIdentity::new(path).with_password(pw); | ||
ssl_opts.with_client_identity(Some(identity)) | ||
} | ||
Some((Some(path), None)) => { | ||
let identity = mysql_async::ClientIdentity::new(path); | ||
ssl_opts.with_client_identity(Some(identity)) | ||
} | ||
_ => ssl_opts, | ||
}; | ||
} | ||
|
||
Ok(MysqlUrlQueryParams { | ||
#[cfg(feature = "mysql-native")] | ||
ssl_opts, | ||
connection_limit, | ||
use_ssl, | ||
socket, | ||
socket_timeout, | ||
connect_timeout, | ||
pool_timeout, | ||
max_connection_lifetime, | ||
max_idle_connection_lifetime, | ||
prefer_socket, | ||
statement_cache_size, | ||
}) | ||
} | ||
|
||
#[cfg(feature = "pooled")] | ||
pub(crate) fn connection_limit(&self) -> Option<usize> { | ||
self.query_params.connection_limit | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For other reviewer's interest. Now some implementations of a struct contract are segregated into common and native modules (as it should be) This is the common part of MysqlUrl
impl MysqlUrl { | ||
pub(crate) fn cache(&self) -> LruCache<String, my::Statement> { | ||
LruCache::new(self.query_params.statement_cache_size) | ||
} | ||
|
||
pub(crate) fn to_opts_builder(&self) -> my::OptsBuilder { | ||
let mut config = my::OptsBuilder::default() | ||
.stmt_cache_size(Some(0)) | ||
.user(Some(self.username())) | ||
.pass(self.password()) | ||
.db_name(Some(self.dbname())); | ||
|
||
match self.socket() { | ||
Some(ref socket) => { | ||
config = config.socket(Some(socket)); | ||
} | ||
None => { | ||
config = config.ip_or_hostname(self.host()).tcp_port(self.port()); | ||
} | ||
} | ||
|
||
config = config.conn_ttl(Some(Duration::from_secs(5))); | ||
|
||
if self.query_params.use_ssl { | ||
config = config.ssl_opts(Some(self.query_params.ssl_opts.clone())); | ||
} | ||
|
||
if self.query_params.prefer_socket.is_some() { | ||
config = config.prefer_socket(self.query_params.prefer_socket); | ||
} | ||
|
||
config | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And this is the second part of the interface that was segregated as mentioned in https://github.com/prisma/prisma-engines/pull/4442/files#diff-c21c4fed44e88a7b5e9434cc899df264f55d52bb9b2bb654f15c7ecff7540060R301
In here, there are symbols that either are IO (part of the native subject) related. But also others that are only used in the native code paths. Like the cache
function.
pub use mysql_async; | ||
|
||
impl MysqlUrl { | ||
pub(crate) fn cache(&self) -> LruCache<String, my::Statement> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider inlining in the MySQL constructor -if possible- as this is an implementation detail of the statement cache, that is exposed in the interface.
LruCache::new(self.query_params.statement_cache_size) | ||
} | ||
|
||
pub(crate) fn to_opts_builder(&self) -> my::OptsBuilder { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder why a URL has to depend on socket at all. 🤷🏾
impl Mysql { | ||
/// Create a new MySQL connection using `OptsBuilder` from the `mysql` crate. | ||
pub async fn new(url: MysqlUrl) -> crate::Result<Self> { | ||
let conn = timeout::connect(url.connect_timeout(), my::Conn::new(url.to_opts_builder())).await?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wondering why we couldn't just make the MySQL url, 'common' rather than native. @jkomyno pointed me to this code. The reason is that a URL is an object that belongs to the driver-specific crate mysql_async
, and it's used at least in here, as an argument for timeout::connect
, so the URL cannot return a different object that can polyfill mysql_async::OptsBuilder
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is of course, existing porting code, nothing newly added.
#[derive(Debug, Error)] | ||
enum MysqlAsyncError { | ||
#[error("Server error: `{}'", _0)] | ||
Server(#[source] MysqlError), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Could you please add a comment in the source code explaining this comment, and pointing to the source code of the original Enum, where you grab this variant?
@@ -0,0 +1,7 @@ | |||
//! This is a partial copy of `rusqlite::ffi::*`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A sightly more verbose comment, in the lines of:
"These are only the subset of constants being used by the query-engine to parse the errors into some higher-level, more meaningful error up the stack"
Maybe a "See errors.rs" in the WASM folder -> to be probably moved to a common folder
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure: 263bab0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the record, we paired on flattening the structure of each of the providers directories, to get rid off wasm specific things.
It's still pending to replace the additional family of feature (-native) which is only dependant on the target architecture with conditional compilation based on whether we are on the wasm32
target or not, which @jkomyno will do for both mysql, and the rest of the providers. After that, this should be ready to be merged.
I have flattened the modules as agreed in our call with Miguel, but I don’t agree with replacing E.g., for the [features]
native = [
"mysql-native",
# ...
]
mysql-native = ["mysql", "mysql_async", "tokio/time", "lru-cache"] There is no equivalently valid Cargo.toml syntax for achieving the same result with such clarity (see rust-lang/cargo#1197). For this reason, I argue we should keep the
In practice, |
Ok, to not replace with architecture conditional flags in the light of rust-lang/cargo#1197 👍 |
Great, thanks. This PR is now ready for a final review round. |
#[test] | ||
fn should_parse_socket_url() { | ||
let url = MysqlUrl::new(Url::parse("mysql://root@localhost/dbname?socket=(/tmp/mysql.sock)").unwrap()).unwrap(); | ||
assert_eq!("dbname", url.dbname()); | ||
assert_eq!(&Some(String::from("/tmp/mysql.sock")), url.socket()); | ||
} | ||
|
||
#[test] | ||
fn should_parse_prefer_socket() { | ||
let url = | ||
MysqlUrl::new(Url::parse("mysql://root:root@localhost:3307/testdb?prefer_socket=false").unwrap()).unwrap(); | ||
assert!(!url.prefer_socket().unwrap()); | ||
} | ||
|
||
#[test] | ||
fn should_parse_sslaccept() { | ||
let url = | ||
MysqlUrl::new(Url::parse("mysql://root:root@localhost:3307/testdb?sslaccept=strict").unwrap()).unwrap(); | ||
assert!(url.query_params.use_ssl); | ||
assert!(!url.query_params.ssl_opts.skip_domain_validation()); | ||
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()) | ||
.unwrap(); | ||
assert_eq!(420, url.cache().capacity()); | ||
} | ||
|
||
#[test] | ||
fn should_have_default_cache_size() { | ||
let url = MysqlUrl::new(Url::parse("mysql:///root:root@localhost:3307/foo").unwrap()).unwrap(); | ||
assert_eq!(100, url.cache().capacity()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests should most likely be part of the url.rs crate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be the case for other connectors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I've addressed this in 95a4e28.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ship it boy! 🚀
This started regressing. It might be because of any latest change in driver adapters code, could you please check and fix @aqrln ? |
This PR closes https://github.com/prisma/team-orm/issues/279 and deprecates #4119.
This PR allows
quaint
to be conditionally compiled towasm32-unknown-unknown
.The end goal is that
query-engine-node-api
keeps usingquaint
's native Rust Drivers + napi.rs-based Driver Adapters, whereasquaint-engine-wasm
should use onlyquaint
's definitions (without native Rust Drivers) + Wasm-based Driver Adapters (the latter are NOT part of this PR).How it works
default
feature ofquaint
enables the SQL connector definitions into the compiled bundle, but not their driversnative
feature ofquaint
connector
module ofquaint
is refactored in such a way that each SQL database's definition is now in its own folder (e.g.,connector/mysql
rather thanconnector/mysql.rs
), and each such folder contains thenative
andwasm
subfolders. Intuitively,wasm
contains the Wasm-compatible definitions, data structures, and errors needed for that specific connector (e.g.,mysql
), andnative
contains extensions and additional definitions for such connector that are only used when native Rust drivers are required.connector.rs
, by default, imports the Wasm-compatible connector definitions (e.g., via#[cfg(feature = "mysql")]
), and optionally imports the native bindings on top of it (e.g., via#[cfg(feature = "mysql-native")]
). In other words, in practice, importing e.g.mysql/native
implies importingmysql/wasm
as well, but not viceversa.Compilation check
--target native
cargo build -p query-engine-node-api
--target wasm32-unknown-unknown
cargo build -p quaint --target wasm32-unknown-unknown
cargo build -p prisma-models --target wasm32-unknown-unknown
cargo build -p user-facing-errors --target wasm32-unknown-unknown
cargo build -p query-core --target wasm32-unknown-unknown
(https://github.com/prisma/team-orm/issues/280)cargo build -p request-handlers --target wasm32-unknown-unknown
(https://github.com/prisma/team-orm/issues/281)cargo build -p query-connector --target wasm32-unknown-unknown
cargo build -p sql-query-connector --target wasm32-unknown-unknown
(https://github.com/prisma/team-orm/issues/283)