Skip to content
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

Merged
merged 23 commits into from
Nov 17, 2023

Conversation

jkomyno
Copy link
Contributor

@jkomyno jkomyno commented Nov 14, 2023

This PR closes https://github.com/prisma/team-orm/issues/279 and deprecates #4119.

This PR allows quaint to be conditionally compiled to wasm32-unknown-unknown.
The end goal is that query-engine-node-api keeps using quaint's native Rust Drivers + napi.rs-based Driver Adapters, whereas quaint-engine-wasm should use only quaint's definitions (without native Rust Drivers) + Wasm-based Driver Adapters (the latter are NOT part of this PR).

How it works

  • The default feature of quaint enables the SQL connector definitions into the compiled bundle, but not their drivers
  • Native Rust drivers are explicitly enabled via the native feature of quaint
  • The connector module of quaint is refactored in such a way that each SQL database's definition is now in its own folder (e.g., connector/mysql rather than connector/mysql.rs), and each such folder contains the native and wasm subfolders. Intuitively, wasm contains the Wasm-compatible definitions, data structures, and errors needed for that specific connector (e.g., mysql), and native contains extensions and additional definitions for such connector that are only used when native Rust drivers are required.
  • The 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 importing mysql/wasm as well, but not viceversa.

Compilation check

--target native

  • cargo build -p query-engine-node-api

--target wasm32-unknown-unknown

@jkomyno jkomyno added this to the 5.7.0 milestone Nov 14, 2023
@jkomyno jkomyno self-assigned this Nov 14, 2023
Copy link

codspeed-hq bot commented Nov 14, 2023

CodSpeed Performance Report

Merging #4442 will not alter performance

Comparing feat/quaint-on-wasm32-unknown-unknown (a1c2b40) with main (800a9b4)

Summary

✅ 11 untouched benchmarks

@jkomyno
Copy link
Contributor Author

jkomyno commented Nov 14, 2023

EDIT: solved by 5ab6d96.

Current blocker

sqlite's error SqliteError uses rusqlite and libsqlite3-sys under the hood, which are not compatible with wasm32-unknown-unknown no matter which feature flag is used on those crates. This error struct (along with similar ones for postgres and mysql) is used in driver_adapters::result::DriverAdapterError.

Proposed strategy:

  • copy the code definition needed from libsqlite3-sys

@jkomyno jkomyno marked this pull request as ready for review November 14, 2023 12:11
@jkomyno jkomyno requested a review from miguelff November 14, 2023 12:11
Comment on lines 4 to 8
#[derive(Debug, Error)]
enum MysqlAsyncError {
#[error("Server error: `{}'", _0)]
Server(#[source] MysqlError),
}
Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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"]
Copy link
Contributor Author

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.

Copy link
Contributor

@miguelff miguelff left a 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.
Copy link
Contributor

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?

Copy link
Contributor Author

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")]
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 19 to 301

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
}
}
Copy link
Contributor

@miguelff miguelff Nov 15, 2023

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

Comment on lines +34 to +67
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
}
}
Copy link
Contributor

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> {
Copy link
Contributor

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 {
Copy link
Contributor

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?;
Copy link
Contributor

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

Copy link
Contributor

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.

Comment on lines 4 to 8
#[derive(Debug, Error)]
enum MysqlAsyncError {
#[error("Server error: `{}'", _0)]
Server(#[source] MysqlError),
}
Copy link
Contributor

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::*`.
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure: 263bab0

Copy link
Contributor

@miguelff miguelff left a 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.

@jkomyno jkomyno requested a review from a team as a code owner November 15, 2023 12:53
@jkomyno jkomyno requested review from Weakky and removed request for a team November 15, 2023 12:53
@jkomyno
Copy link
Contributor Author

jkomyno commented Nov 15, 2023

I have flattened the modules as agreed in our call with Miguel, but I don’t agree with replacing *-native with #[cfg(not(target_arch = "wasm32"))]. Why? Let's take a look at quaint/Cargo.toml. The *-native feature flag implies additional optional dependencies or dependencies' flags.

E.g., for the mysql provider, the mysql-native feature flag toggles "mysql", "mysql_async", "tokio/time", "lru-cache", which is easily expressed via:

[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 -native flags (as we had originally discussed ~ 2 months ago), and import quaint conditionally based on:

  • whether we need it on query-engine-node-api
    [dependencies]
    quaint = { path = "../../quaint", features = ["native"] }
  • whether we need it on query-engine-wasm:
    quaint = { path = "../../quaint" }

In practice, query-engine-* crates don't import quaint directly, but you get the idea.
It's easier and clearer to have other crates importing quaint to conditionally add the native feature when not on wasm32 arch, then to apply this logic in quaint itself.

@miguelff
Copy link
Contributor

Ok, to not replace with architecture conditional flags in the light of rust-lang/cargo#1197 👍

@jkomyno
Copy link
Contributor Author

jkomyno commented Nov 15, 2023

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.

@jkomyno jkomyno requested a review from miguelff November 15, 2023 15:16
Comment on lines 306 to 346
#[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());
}
Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

@miguelff miguelff left a comment

Choose a reason for hiding this comment

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

Ship it boy! 🚀

@miguelff
Copy link
Contributor

new::regressions::prisma_15204::conversion_error::convert_to_bigint_sqlite_js

This started regressing. It might be because of any latest change in driver adapters code, could you please check and fix @aqrln ?

@jkomyno jkomyno merged commit b567896 into main Nov 17, 2023
59 of 60 checks passed
@jkomyno jkomyno deleted the feat/quaint-on-wasm32-unknown-unknown branch November 17, 2023 15:22
miguelff added a commit that referenced this pull request Nov 20, 2023
@miguelff miguelff mentioned this pull request Nov 20, 2023
miguelff added a commit that referenced this pull request Nov 20, 2023
miguelff pushed a commit that referenced this pull request Nov 20, 2023
miguelff added a commit that referenced this pull request Nov 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants