From 2347cb13044d04d30f40b74b1def849690a3f604 Mon Sep 17 00:00:00 2001 From: jkomyno Date: Wed, 22 Nov 2023 02:46:55 +0100 Subject: [PATCH] feat(driver-adapters): remove "queryable" into its own module --- query-engine/driver-adapters/Cargo.toml | 2 +- query-engine/driver-adapters/src/lib.rs | 2 + .../driver-adapters/src/napi/conversion.rs | 2 +- query-engine/driver-adapters/src/napi/mod.rs | 6 +- .../driver-adapters/src/napi/transaction.rs | 6 +- .../driver-adapters/src/queryable/mod.rs | 310 ++++++++++++++++++ .../driver-adapters/src/queryable/napi.rs | 32 ++ .../driver-adapters/src/queryable/wasm.rs | 33 ++ .../src/{wasm => }/send_future.rs | 0 query-engine/driver-adapters/src/wasm/mod.rs | 6 +- .../driver-adapters/src/wasm/proxy.rs | 7 +- .../driver-adapters/src/wasm/transaction.rs | 8 +- 12 files changed, 392 insertions(+), 22 deletions(-) create mode 100644 query-engine/driver-adapters/src/queryable/mod.rs create mode 100644 query-engine/driver-adapters/src/queryable/napi.rs create mode 100644 query-engine/driver-adapters/src/queryable/wasm.rs rename query-engine/driver-adapters/src/{wasm => }/send_future.rs (100%) diff --git a/query-engine/driver-adapters/Cargo.toml b/query-engine/driver-adapters/Cargo.toml index 8fa27edb5aa0..ec77df85e142 100644 --- a/query-engine/driver-adapters/Cargo.toml +++ b/query-engine/driver-adapters/Cargo.toml @@ -13,6 +13,7 @@ tracing = "0.1" tracing-core = "0.1" metrics = "0.18" uuid = { version = "1", features = ["v4"] } +pin-project = "1" # Note: these deps are temporarily specified here to avoid importing them from tiberius (the SQL server driver). # They will be imported from quaint-core instead in a future PR. @@ -37,4 +38,3 @@ serde-wasm-bindgen.workspace = true wasm-bindgen.workspace = true wasm-bindgen-futures.workspace = true tsify.workspace = true -pin-project = "1" diff --git a/query-engine/driver-adapters/src/lib.rs b/query-engine/driver-adapters/src/lib.rs index ca8aa4541bd1..0e40b814c43f 100644 --- a/query-engine/driver-adapters/src/lib.rs +++ b/query-engine/driver-adapters/src/lib.rs @@ -9,6 +9,8 @@ pub(crate) mod conversion; pub(crate) mod error; +pub(crate) mod queryable; +pub(crate) mod send_future; pub(crate) mod types; #[cfg(not(target_arch = "wasm32"))] diff --git a/query-engine/driver-adapters/src/napi/conversion.rs b/query-engine/driver-adapters/src/napi/conversion.rs index 5ab630998d27..ac2dda60a279 100644 --- a/query-engine/driver-adapters/src/napi/conversion.rs +++ b/query-engine/driver-adapters/src/napi/conversion.rs @@ -1,4 +1,4 @@ -pub(crate) use crate::conversion::{mysql, postgres, sqlite, JSArg}; +pub(crate) use crate::conversion::JSArg; use napi::bindgen_prelude::{FromNapiValue, ToNapiValue}; use napi::NapiValue; diff --git a/query-engine/driver-adapters/src/napi/mod.rs b/query-engine/driver-adapters/src/napi/mod.rs index 05267dec453b..4612cb550553 100644 --- a/query-engine/driver-adapters/src/napi/mod.rs +++ b/query-engine/driver-adapters/src/napi/mod.rs @@ -3,8 +3,8 @@ mod async_js_function; mod conversion; mod error; -mod proxy; -mod queryable; +pub(crate) mod proxy; mod result; mod transaction; -pub use queryable::{from_napi, JsQueryable}; + +pub use crate::queryable::{from_napi, JsQueryable}; diff --git a/query-engine/driver-adapters/src/napi/transaction.rs b/query-engine/driver-adapters/src/napi/transaction.rs index 16ecbb435ce9..69219d06ef1e 100644 --- a/query-engine/driver-adapters/src/napi/transaction.rs +++ b/query-engine/driver-adapters/src/napi/transaction.rs @@ -7,10 +7,8 @@ use quaint::{ Value, }; -use super::{ - proxy::{CommonProxy, TransactionOptions, TransactionProxy}, - queryable::JsBaseQueryable, -}; +use super::proxy::{CommonProxy, TransactionOptions, TransactionProxy}; +use crate::queryable::JsBaseQueryable; // Wrapper around JS transaction objects that implements Queryable // and quaint::Transaction. Can be used in place of quaint transaction, diff --git a/query-engine/driver-adapters/src/queryable/mod.rs b/query-engine/driver-adapters/src/queryable/mod.rs new file mode 100644 index 000000000000..ac252bbb011b --- /dev/null +++ b/query-engine/driver-adapters/src/queryable/mod.rs @@ -0,0 +1,310 @@ +#[cfg(not(target_arch = "wasm32"))] +pub(crate) mod napi; + +#[cfg(not(target_arch = "wasm32"))] +pub use napi::from_napi; + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) use napi::JsBaseQueryable; + +#[cfg(target_arch = "wasm32")] +pub(crate) mod wasm; + +#[cfg(target_arch = "wasm32")] +pub use wasm::from_wasm; + +#[cfg(target_arch = "wasm32")] +pub(crate) use wasm::JsBaseQueryable; + +use super::{ + conversion, + proxy::{CommonProxy, DriverProxy, Query}, +}; +use crate::send_future::SendFuture; +use async_trait::async_trait; +use futures::Future; +use psl::datamodel_connector::Flavour; +use quaint::{ + connector::{metrics, IsolationLevel, Transaction}, + error::{Error, ErrorKind}, + prelude::{Query as QuaintQuery, Queryable as QuaintQueryable, ResultSet, TransactionCapable}, + visitor::{self, Visitor}, +}; +use tracing::{info_span, Instrument}; + +impl JsBaseQueryable { + pub(crate) fn new(proxy: CommonProxy) -> Self { + let flavour: Flavour = proxy.flavour.parse().unwrap(); + Self { proxy, flavour } + } + + /// visit a quaint query AST according to the flavour of the JS connector + fn visit_quaint_query<'a>(&self, q: QuaintQuery<'a>) -> quaint::Result<(String, Vec>)> { + match self.flavour { + Flavour::Mysql => visitor::Mysql::build(q), + Flavour::Postgres => visitor::Postgres::build(q), + Flavour::Sqlite => visitor::Sqlite::build(q), + _ => unimplemented!("Unsupported flavour for JS connector {:?}", self.flavour), + } + } + + async fn build_query(&self, sql: &str, values: &[quaint::Value<'_>]) -> quaint::Result { + let sql: String = sql.to_string(); + + let converter = match self.flavour { + Flavour::Postgres => conversion::postgres::value_to_js_arg, + Flavour::Sqlite => conversion::sqlite::value_to_js_arg, + Flavour::Mysql => conversion::mysql::value_to_js_arg, + _ => unreachable!("Unsupported flavour for JS connector {:?}", self.flavour), + }; + + let args = values + .iter() + .map(converter) + .collect::>>()?; + + Ok(Query { sql, args }) + } +} + +#[async_trait] +impl QuaintQueryable for JsBaseQueryable { + async fn query(&self, q: QuaintQuery<'_>) -> quaint::Result { + let (sql, params) = self.visit_quaint_query(q)?; + self.query_raw(&sql, ¶ms).await + } + + async fn query_raw(&self, sql: &str, params: &[quaint::Value<'_>]) -> quaint::Result { + metrics::query("js.query_raw", sql, params, move || async move { + self.do_query_raw(sql, params).await + }) + .await + } + + async fn query_raw_typed(&self, sql: &str, params: &[quaint::Value<'_>]) -> quaint::Result { + self.query_raw(sql, params).await + } + + async fn execute(&self, q: QuaintQuery<'_>) -> quaint::Result { + let (sql, params) = self.visit_quaint_query(q)?; + self.execute_raw(&sql, ¶ms).await + } + + async fn execute_raw(&self, sql: &str, params: &[quaint::Value<'_>]) -> quaint::Result { + metrics::query("js.execute_raw", sql, params, move || async move { + self.do_execute_raw(sql, params).await + }) + .await + } + + async fn execute_raw_typed(&self, sql: &str, params: &[quaint::Value<'_>]) -> quaint::Result { + self.execute_raw(sql, params).await + } + + async fn raw_cmd(&self, cmd: &str) -> quaint::Result<()> { + let params = &[]; + metrics::query("js.raw_cmd", cmd, params, move || async move { + self.do_execute_raw(cmd, params).await?; + Ok(()) + }) + .await + } + + async fn version(&self) -> quaint::Result> { + // Note: JS Connectors don't use this method. + Ok(None) + } + + fn is_healthy(&self) -> bool { + // Note: JS Connectors don't use this method. + true + } + + /// Sets the transaction isolation level to given value. + /// Implementers have to make sure that the passed isolation level is valid for the underlying database. + async fn set_tx_isolation_level(&self, isolation_level: IsolationLevel) -> quaint::Result<()> { + if matches!(isolation_level, IsolationLevel::Snapshot) { + return Err(Error::builder(ErrorKind::invalid_isolation_level(&isolation_level)).build()); + } + + if self.flavour == Flavour::Sqlite { + return match isolation_level { + IsolationLevel::Serializable => Ok(()), + _ => Err(Error::builder(ErrorKind::invalid_isolation_level(&isolation_level)).build()), + }; + } + + self.raw_cmd(&format!("SET TRANSACTION ISOLATION LEVEL {isolation_level}")) + .await + } + + fn requires_isolation_first(&self) -> bool { + match self.flavour { + Flavour::Mysql => true, + Flavour::Postgres | Flavour::Sqlite => false, + _ => unreachable!(), + } + } +} + +impl JsBaseQueryable { + pub fn phantom_query_message(stmt: &str) -> String { + format!(r#"-- Implicit "{}" query via underlying driver"#, stmt) + } + + async fn do_query_raw_inner(&self, sql: &str, params: &[quaint::Value<'_>]) -> quaint::Result { + let len = params.len(); + let serialization_span = info_span!("js:query:args", user_facing = true, "length" = %len); + let query = self.build_query(sql, params).instrument(serialization_span).await?; + + let sql_span = info_span!("js:query:sql", user_facing = true, "db.statement" = %sql); + let result_set = self.proxy.query_raw(query).instrument(sql_span).await?; + + let len = result_set.len(); + let _deserialization_span = info_span!("js:query:result", user_facing = true, "length" = %len).entered(); + + result_set.try_into() + } + + fn do_query_raw<'a>( + &'a self, + sql: &'a str, + params: &'a [quaint::Value<'a>], + ) -> SendFuture> + 'a> { + SendFuture(self.do_query_raw_inner(sql, params)) + } + + async fn do_execute_raw_inner(&self, sql: &str, params: &[quaint::Value<'_>]) -> quaint::Result { + let len = params.len(); + let serialization_span = info_span!("js:query:args", user_facing = true, "length" = %len); + let query = self.build_query(sql, params).instrument(serialization_span).await?; + + let sql_span = info_span!("js:query:sql", user_facing = true, "db.statement" = %sql); + let affected_rows = self.proxy.execute_raw(query).instrument(sql_span).await?; + + Ok(affected_rows as u64) + } + + fn do_execute_raw<'a>( + &'a self, + sql: &'a str, + params: &'a [quaint::Value<'a>], + ) -> SendFuture> + 'a> { + SendFuture(self.do_execute_raw_inner(sql, params)) + } +} + +/// A JsQueryable adapts a Proxy to implement quaint's Queryable interface. It has the +/// responsibility of transforming inputs and outputs of `query` and `execute` methods from quaint +/// types to types that can be translated into javascript and viceversa. This is to let the rest of +/// the query engine work as if it was using quaint itself. The aforementioned transformations are: +/// +/// Transforming a `quaint::ast::Query` into SQL by visiting it for the specific flavour of SQL +/// expected by the client connector. (eg. using the mysql visitor for the Planetscale client +/// connector) +/// +/// Transforming a `JSResultSet` (what client connectors implemented in javascript provide) +/// into a `quaint::connector::result_set::ResultSet`. A quaint `ResultSet` is basically a vector +/// of `quaint::Value` but said type is a tagged enum, with non-unit variants that cannot be converted to javascript as is. +/// +pub struct JsQueryable { + inner: JsBaseQueryable, + driver_proxy: DriverProxy, +} + +impl std::fmt::Display for JsQueryable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "JSQueryable(driver)") + } +} + +impl std::fmt::Debug for JsQueryable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "JSQueryable(driver)") + } +} + +#[async_trait] +impl QuaintQueryable for JsQueryable { + async fn query(&self, q: QuaintQuery<'_>) -> quaint::Result { + self.inner.query(q).await + } + + async fn query_raw(&self, sql: &str, params: &[quaint::Value<'_>]) -> quaint::Result { + self.inner.query_raw(sql, params).await + } + + async fn query_raw_typed(&self, sql: &str, params: &[quaint::Value<'_>]) -> quaint::Result { + self.inner.query_raw_typed(sql, params).await + } + + async fn execute(&self, q: QuaintQuery<'_>) -> quaint::Result { + self.inner.execute(q).await + } + + async fn execute_raw(&self, sql: &str, params: &[quaint::Value<'_>]) -> quaint::Result { + self.inner.execute_raw(sql, params).await + } + + async fn execute_raw_typed(&self, sql: &str, params: &[quaint::Value<'_>]) -> quaint::Result { + self.inner.execute_raw_typed(sql, params).await + } + + async fn raw_cmd(&self, cmd: &str) -> quaint::Result<()> { + self.inner.raw_cmd(cmd).await + } + + async fn version(&self) -> quaint::Result> { + self.inner.version().await + } + + fn is_healthy(&self) -> bool { + self.inner.is_healthy() + } + + async fn set_tx_isolation_level(&self, isolation_level: IsolationLevel) -> quaint::Result<()> { + self.inner.set_tx_isolation_level(isolation_level).await + } + + fn requires_isolation_first(&self) -> bool { + self.inner.requires_isolation_first() + } +} + +#[async_trait] +impl TransactionCapable for JsQueryable { + async fn start_transaction<'a>( + &'a self, + isolation: Option, + ) -> quaint::Result> { + let tx = self.driver_proxy.start_transaction().await?; + + let isolation_first = tx.requires_isolation_first(); + + if isolation_first { + if let Some(isolation) = isolation { + tx.set_tx_isolation_level(isolation).await?; + } + } + + let begin_stmt = tx.begin_statement(); + + let tx_opts = tx.options(); + if tx_opts.use_phantom_query { + let begin_stmt = JsBaseQueryable::phantom_query_message(begin_stmt); + tx.raw_phantom_cmd(begin_stmt.as_str()).await?; + } else { + tx.raw_cmd(begin_stmt).await?; + } + + if !isolation_first { + if let Some(isolation) = isolation { + tx.set_tx_isolation_level(isolation).await?; + } + } + + self.server_reset_query(tx.as_ref()).await?; + + Ok(tx) + } +} diff --git a/query-engine/driver-adapters/src/queryable/napi.rs b/query-engine/driver-adapters/src/queryable/napi.rs new file mode 100644 index 000000000000..2245802908c2 --- /dev/null +++ b/query-engine/driver-adapters/src/queryable/napi.rs @@ -0,0 +1,32 @@ +use crate::napi::proxy::{CommonProxy, DriverProxy}; +use crate::JsQueryable; +use napi::JsObject; +use psl::datamodel_connector::Flavour; + +/// A JsQueryable adapts a Proxy to implement quaint's Queryable interface. It has the +/// responsibility of transforming inputs and outputs of `query` and `execute` methods from quaint +/// types to types that can be translated into javascript and viceversa. This is to let the rest of +/// the query engine work as if it was using quaint itself. The aforementioned transformations are: +/// +/// Transforming a `quaint::ast::Query` into SQL by visiting it for the specific flavour of SQL +/// expected by the client connector. (eg. using the mysql visitor for the Planetscale client +/// connector) +/// +/// Transforming a `JSResultSet` (what client connectors implemented in javascript provide) +/// into a `quaint::connector::result_set::ResultSet`. A quaint `ResultSet` is basically a vector +/// of `quaint::Value` but said type is a tagged enum, with non-unit variants that cannot be converted to javascript as is. +/// +pub(crate) struct JsBaseQueryable { + pub(crate) proxy: CommonProxy, + pub flavour: Flavour, +} + +pub fn from_napi(driver: JsObject) -> JsQueryable { + let common = CommonProxy::new(&driver).unwrap(); + let driver_proxy = DriverProxy::new(&driver).unwrap(); + + JsQueryable { + inner: JsBaseQueryable::new(common), + driver_proxy, + } +} diff --git a/query-engine/driver-adapters/src/queryable/wasm.rs b/query-engine/driver-adapters/src/queryable/wasm.rs new file mode 100644 index 000000000000..867d1fb5081a --- /dev/null +++ b/query-engine/driver-adapters/src/queryable/wasm.rs @@ -0,0 +1,33 @@ +use crate::wasm::proxy::{CommonProxy, DriverProxy}; +use crate::{JsObjectExtern, JsQueryable}; +use psl::datamodel_connector::Flavour; +use wasm_bindgen::prelude::wasm_bindgen; + +/// A JsQueryable adapts a Proxy to implement quaint's Queryable interface. It has the +/// responsibility of transforming inputs and outputs of `query` and `execute` methods from quaint +/// types to types that can be translated into javascript and viceversa. This is to let the rest of +/// the query engine work as if it was using quaint itself. The aforementioned transformations are: +/// +/// Transforming a `quaint::ast::Query` into SQL by visiting it for the specific flavour of SQL +/// expected by the client connector. (eg. using the mysql visitor for the Planetscale client +/// connector) +/// +/// Transforming a `JSResultSet` (what client connectors implemented in javascript provide) +/// into a `quaint::connector::result_set::ResultSet`. A quaint `ResultSet` is basically a vector +/// of `quaint::Value` but said type is a tagged enum, with non-unit variants that cannot be converted to javascript as is. +#[wasm_bindgen(getter_with_clone)] +#[derive(Default)] +pub(crate) struct JsBaseQueryable { + pub(crate) proxy: CommonProxy, + pub flavour: Flavour, +} + +pub fn from_wasm(driver: JsObjectExtern) -> JsQueryable { + let common = CommonProxy::new(&driver).unwrap(); + let driver_proxy = DriverProxy::new(&driver).unwrap(); + + JsQueryable { + inner: JsBaseQueryable::new(common), + driver_proxy, + } +} diff --git a/query-engine/driver-adapters/src/wasm/send_future.rs b/query-engine/driver-adapters/src/send_future.rs similarity index 100% rename from query-engine/driver-adapters/src/wasm/send_future.rs rename to query-engine/driver-adapters/src/send_future.rs diff --git a/query-engine/driver-adapters/src/wasm/mod.rs b/query-engine/driver-adapters/src/wasm/mod.rs index 5f817569c31c..416b9db4a787 100644 --- a/query-engine/driver-adapters/src/wasm/mod.rs +++ b/query-engine/driver-adapters/src/wasm/mod.rs @@ -4,10 +4,8 @@ mod async_js_function; mod conversion; mod error; mod js_object_extern; -mod proxy; -mod queryable; -mod send_future; +pub(crate) mod proxy; mod transaction; +pub use crate::queryable::{from_wasm, JsQueryable}; pub use js_object_extern::JsObjectExtern; -pub use queryable::{from_wasm, JsQueryable}; diff --git a/query-engine/driver-adapters/src/wasm/proxy.rs b/query-engine/driver-adapters/src/wasm/proxy.rs index 75bc8f6347e2..7ab578830f42 100644 --- a/query-engine/driver-adapters/src/wasm/proxy.rs +++ b/query-engine/driver-adapters/src/wasm/proxy.rs @@ -1,12 +1,13 @@ use futures::Future; -use js_sys::{Function as JsFunction, JsString, Object as JsObject}; +use js_sys::{Function as JsFunction, JsString}; use tsify::Tsify; -use super::{async_js_function::AsyncJsFunction, send_future::SendFuture, transaction::JsTransaction}; +use super::{async_js_function::AsyncJsFunction, transaction::JsTransaction}; +use crate::send_future::SendFuture; pub use crate::types::{ColumnType, JSResultSet, Query, TransactionOptions}; use crate::JsObjectExtern; use metrics::increment_gauge; -use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; +use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; type JsResult = core::result::Result; diff --git a/query-engine/driver-adapters/src/wasm/transaction.rs b/query-engine/driver-adapters/src/wasm/transaction.rs index e7aba9f418c4..43925b488101 100644 --- a/query-engine/driver-adapters/src/wasm/transaction.rs +++ b/query-engine/driver-adapters/src/wasm/transaction.rs @@ -6,13 +6,9 @@ use quaint::{ Value, }; use serde::Deserialize; -use wasm_bindgen::prelude::wasm_bindgen; -use super::{ - proxy::{CommonProxy, TransactionOptions, TransactionProxy}, - queryable::JsBaseQueryable, - send_future::SendFuture, -}; +use super::proxy::{TransactionOptions, TransactionProxy}; +use crate::{queryable::JsBaseQueryable, send_future::SendFuture}; // Wrapper around JS transaction objects that implements Queryable // and quaint::Transaction. Can be used in place of quaint transaction,