Skip to content

Commit

Permalink
feat(query-engine-common): reduce duplicated code between `query-engi…
Browse files Browse the repository at this point in the history
…ne-node-api` and `query-engine-wasm` (#4521)

* feat(quaint): allow wasm32-unknown-unknown compilation; currently fails on native

* feat(quaint): split postgres connector into native and wasm submodules

* feat(quaint): split mysql connector into native and wasm submodules

* feat(quaint): recover wasm error for mysql

* feat(quaint): split mssql connector into native and wasm submodules

* feat(quaint): split sqlite connector into native and wasm submodules

* chore(quaint): fix clippy when compiling natively

* chore(quaint): fix clippy when compiling to wasm32-unknown-unknown

* chore(quaint): update README

* chore(quaint): rename "*-connector" feature flag to "*-native"

* feat(quaint): enable pure Wasm SqliteError

* feat(query-connect): allow wasm32-unknown-unknown compilation

* feat(sql-query-connector): allow wasm32-unknown-unknown compilation

* chore(query-engine-wasm): add currently unused local crates to test wasm32-unknown-unknown compilation

* chore: update Cargo.lock

* chore: remove leftover comments

* feat(query-core): allow wasm32-unknown-unknown compilation

* chore(sql-query-connector): fix clipppy on wasm32

* chore: remove leftover comment

* chore: remove leftover comment

* feat(driver-adapters): enable Wasm on request-handlers

* WIP: refactor mysql module to flatten its structure

* feat(quaint): flatten mssql connector module

* feat(quaint): flatten postgres connector module

* feat(quaint): flatten sqlite connector module

* chore(quaint): export all public definitions in connector "url" modules

* chore(quaint): refactor tests for connectors, addressing feedback

* chore: add comment on MysqlAsyncError

* chore: add comment on ffi.rs for sqlite

* chore: replace awkward "super::super::" with "crate::..."

* chore: add comments around "query_core::executor::task"

* chore: add comments around "query_core::executor::task"

* chore: add "request-handlers" to "query-engine-wasm"

* chore: add wasm dependencies to Cargo workspace

* feat(driver-adapters): move napi-specific code into "napi" module, prepare empty "wasm" module

* feat(driver-adapters): extracted platform-agnostic "DriverAdapterError" into "driver_adapters::error"

* chore(driver-adapters): add "driver-adapters" to "query-engine-wasm"

* feat(driver-adapters): add Wasm-specific "async_js_function"

* feat(driver-adapters): extracted common types to "driver_adapters::types"

* feat(driver-adapters): extracted "TryFrom<JSResultSet> for QuaintResultSet" to "driver_adapters::conversion::js_to_quaint"

* feat(driver-adapters): allow feature-complete Wasm compilation of "driver-adapters"

* feat(driver-adapters): plug "driver-adapters" to "query-engine-wasm"

* chore: remove .cargo, add it to .gitignore

* chore: move "task" module into its own file

* fix(driver-adapters): ci for "request-handlers"

* fix(driver-adapters): ci for "request-handlers"

* fix(driver-adapters): clippy compile error on "query-engine-wasm"

* chore(driver-adapters): fix conflicting library name warning on "cargo build"

* chore: fix conflicts

* chore: fixed some clippy warnings

* chore: add .cargo to .gitignore

* feat(query-engine-wasm): ported some logic from query-engine-node-api in a wasm32-compatible fashion

* Add connect/disconnect

* fix: remove tokio-induced panic in "connect"

* feat: remove ducktor

* feat(driver-adapters): remove "queryable" into its own module

* Couple of fixes

1. Build schema and connect sequentually
2. Print full stacktrace for WASM error
3. Expand example to attempt a query

* Fix `Instant::now` usage

Replace `Instant::now` with a custom library, that will use appriate
time/date functions for the platforms. For native, it is `std::time`,
for WASM in should probably be `performance::now()` but it is a no-op
stub for now.

* fix(driver-adapters): understand "flavour" and adjust casing in "JSResultSet"

* feat(driver-adapters): add some Into traits for "JsResult"

* Fix JSResult parsing

* Reorganize example

Switch to sqlite so cloud services are not required
Save schema to external file

* Fix some of the warnings

* Remove unused file

* Fix WASM transaction binding

Correctly gets to a point of starting the tranasction and executing the
query, fails on parsing the results like normal queries do.

* Cleanup

* feat(driver-adapters): fix enum parsing, add "wasm-rs-dbg" crate for dev development

* chore(driver-adapters): remove unused "src/wasm/queryable.rs"

* chore(driver-adapters): add "createOne" and "driverAdapters" preview feature to example

* chore(driver-adapters): add wasm-bindgen-test example

* chore(driver-adapters): update Cargo.lock

* Handle non-promise return values

* Run tests on WASM

* feat(core): allow Drop'ing futures running in loop in wasm32-* via controlled spawns

* feat(chore): add Arc<DriverAdapter> comment

* feat(chore): re-enable previous "disconnect()"

* feat(chore): update example.js

* fix(query-engine-node-api): fix compilation errors

* Update cuid

* Remove one more isntant usage

* chore(driver-adapters): simplify async_js_function API

* chore(driver-adapters): unify napi/wasm errors into "crate::JsResult"

* chore(driver-adapters): continue unifying napi/wasm functions

* chore(driver-adapters): unify napi/wasm logic for proxy

* chore(driver-adapters): unify napi/wasm logic queryable.rs and proxy.rs

* chore(driver-adapters): unify napi/wasm logic for transaction.rs

* chore(driver-adapters): clippy fixes

* chore(driver-adapters): cli
ppy fixes

* chore: remove dbg! output

* chore: remove dbg! output

* Fix itx panic

* Remove unused import

* Fix hanging itx

* fix insta tests

Strictly speaking, not related to wasm engine at all - we bumped `insta`
at some point and that required adding `allow_duplicates` macro around
the loop.

Close prisma/team-orm#651

* feat(query-engine-wasm): enable tracing and bits of telemetry

* chore: clippy

* Fix panic kills running engine in query-engine-tests (#4499)

* I didn't have wasm-pack installed, let's fix that

* Update wasm-bindgen-futures to 0.4.39

This includes rustwasm/wasm-bindgen#3203 that use queueMicrotask
to transalate spawn_local rust code.

This has fixed rustwasm/wasm-bindgen#2392 which was an issue about not being able to catch async wasm traps. This might (or not) have an effect on the issue we are trying to solve in here.

* Revert "Update wasm-bindgen-futures to 0.4.39"

This reverts commit 9a494dc.

* Restart executor when it dies

* Document Restartable

* Remove async_panic_to_js_error in WASM query engine

* Rename p -> process

* Use tokio::sync::RwLock rather than futures::lock::Mutex

* Better error messaging

* Fixing clippy

* Exclude unit tests for wasm32 when compiling the binary for other architectures

* Fix duplicate snapshots in json_filters test

* Size low hanging fruits

Removes following functionality from WASM engine:
- GraphQL protocol
- DMMF
- SDL Schema

Neither of the features are used by the client runtimes and thrid party
clients don't and can not use WASM engine, so it is safe to remove.

* feat(driver-adapters): serialize empty values as "null" rather than "undefined"

* chore: fixed query-engine-node-api build

* chore: bumped wasm-bindgen version to 0.2.89

* chore: clean up quaint transitive dependencies

* chore: removed wasm.rs test

* chore: removed temporary wasm machinery

* chore: fix clippy

* chore: remove unwrap from "sleep"

* chore: revert unnecessary psl change

* chore(driver-adapters): fix unit tests

* chore(driver-adapters): add clippy check for wasm32

* chore(driver-adapters): add clippy check for wasm32

* chore: fix clippy

* Revert "chore: removed wasm.rs test"

This reverts commit 60b0d8c.

* test(driver-adapters): add byte tests for conversion

* Revert "Revert "chore: removed wasm.rs test""

This reverts commit 481ba65.

* [skip ci] chore: fix build CI check logic

* Revert "[skip ci] chore: fix build CI check logic"

This reverts commit 28c6ff9.

* Stop using removed method

* Fix broken JS

* chore(review): rename threadsafe_fn to fn_

* chore(review): add comment related to js_sys::Reflect in JsObjectExtern

* chore(review): rename SendFuture to UnsafeFuture, improving comments

* chore(review): use fully-specialized types for wasm/napi-specific logic, when possible; apply clippy fixes

* chore(review): rename JsResult into AdapterResult, reduce duplication, improve comments

* chore(review): remove redudant full type qualifier

* chore(review): revert changes to psl-core

* chore(review): improve comments on js.rs

* Test fixes for NAPI tests (#4515)

* chore(review): comment on spawn_controlled actors, add error tracing to unexpected case

* chore(review): remove unused dependency

* chore(review): move wasm/napi-specific task JoinHandle stuff to crosstarget-utils

* feat(query-engine-common): reduce duplicated code between "query-engine-wasm" and "query-engine-node-api"

* qe-wasm: Partially fix tests (#4517)

* qe-wasm: Partially fix the test suite

1. Bash script for build did not abort on error, hence sed error on
   linux went unnoticed.
2. We don't actually need sed trickery sincce `wasm-pack` has a flag for
   changing binary name.
3. `tracing` feature does not actually work on WASM even partially: it panics on
   `Instant` invokation as soon as first span is created. Since we are
   running tests with all preview features enabled, that means that
   practically any test panics now. Disabled it again.

A lot of tests are still failing on ThreadRng invocation and stacktrace
is not really helpful, but I still think it's better if we get it
working.

* Update query-engine/query-engine-wasm/build.sh

* qe-wasm: Fix RNG on Node 18 in a test runner (#4526)

* feat(query-engine-common): move more stuff to "query-engine-common"

* feat: limit the number of conditional flags for each native-only struct field by introducing *Native structs conditionally

* feat: remove unused code (supersedes #4524)

---------

Co-authored-by: Miguel Fernandez <[email protected]>
Co-authored-by: Sergey Tatarintsev <[email protected]>
Co-authored-by: Jan Piotrowski <[email protected]>
Co-authored-by: Miguel Fernández <[email protected]>
  • Loading branch information
5 people authored Dec 18, 2023
1 parent 61928b0 commit ae99f51
Show file tree
Hide file tree
Showing 20 changed files with 495 additions and 668 deletions.
27 changes: 27 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions libs/query-engine-common/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "query-engine-common"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
thiserror = "1"
url = "2"
query-connector = { path = "../../query-engine/connectors/query-connector" }
query-core = { path = "../../query-engine/core" }
user-facing-errors = { path = "../user-facing-errors" }
serde_json.workspace = true
serde.workspace = true
connection-string.workspace = true
psl.workspace = true
async-trait = "0.1"
tracing = "0.1"
tracing-subscriber = { version = "0.3" }
tracing-futures = "0.2"
tracing-opentelemetry = "0.17.3"
opentelemetry = { version = "0.17"}

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
napi.workspace = true
query-engine-metrics = { path = "../../query-engine/metrics" }

[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen.workspace = true
tsify.workspace = true
158 changes: 158 additions & 0 deletions libs/query-engine-common/src/engine.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#![allow(unused_imports)]

use crate::error::ApiError;
use query_core::{protocol::EngineProtocol, schema::QuerySchema, QueryExecutor};
use serde::Deserialize;
use std::{
collections::{BTreeMap, HashMap},
path::PathBuf,
sync::Arc,
};

#[cfg(target_arch = "wasm32")]
use tsify::Tsify;

/// The state of the engine.
pub enum Inner {
/// Not connected, holding all data to form a connection.
Builder(EngineBuilder),
/// A connected engine, holding all data to disconnect and form a new
/// connection. Allows querying when on this state.
Connected(ConnectedEngine),
}

impl Inner {
/// Returns a builder if the engine is not connected
pub fn as_builder(&self) -> crate::Result<&EngineBuilder> {
match self {
Inner::Builder(ref builder) => Ok(builder),
Inner::Connected(_) => Err(ApiError::AlreadyConnected),
}
}

/// Returns the engine if connected
pub fn as_engine(&self) -> crate::Result<&ConnectedEngine> {
match self {
Inner::Builder(_) => Err(ApiError::NotConnected),
Inner::Connected(ref engine) => Ok(engine),
}
}
}

#[cfg(not(target_arch = "wasm32"))]
pub struct EngineBuilderNative {
pub config_dir: PathBuf,
pub env: HashMap<String, String>,
}

/// Everything needed to connect to the database and have the core running.
pub struct EngineBuilder {
pub schema: Arc<psl::ValidatedSchema>,
pub engine_protocol: EngineProtocol,

#[cfg(not(target_arch = "wasm32"))]
pub native: EngineBuilderNative,
}

#[cfg(not(target_arch = "wasm32"))]
pub struct ConnectedEngineNative {
pub config_dir: PathBuf,
pub env: HashMap<String, String>,
pub metrics: Option<query_engine_metrics::MetricRegistry>,
}

/// Internal structure for querying and reconnecting with the engine.
pub struct ConnectedEngine {
pub schema: Arc<psl::ValidatedSchema>,
pub query_schema: Arc<QuerySchema>,
pub executor: crate::Executor,
pub engine_protocol: EngineProtocol,

#[cfg(not(target_arch = "wasm32"))]
pub native: ConnectedEngineNative,
}

impl ConnectedEngine {
/// The schema AST for Query Engine core.
pub fn query_schema(&self) -> &Arc<QuerySchema> {
&self.query_schema
}

/// The query executor.
pub fn executor(&self) -> &(dyn QueryExecutor + Send + Sync) {
self.executor.as_ref()
}

pub fn engine_protocol(&self) -> EngineProtocol {
self.engine_protocol
}
}

#[cfg(not(target_arch = "wasm32"))]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConstructorOptionsNative {
#[serde(default)]
pub datasource_overrides: BTreeMap<String, String>,
pub config_dir: PathBuf,
#[serde(default)]
pub env: serde_json::Value,
#[serde(default)]
pub ignore_env_var_errors: bool,
}

/// Parameters defining the construction of an engine.
#[derive(Debug, Deserialize)]
#[cfg_attr(target_arch = "wasm32", derive(Tsify))]
#[cfg_attr(target_arch = "wasm32", tsify(from_wasm_abi))]
#[serde(rename_all = "camelCase")]
pub struct ConstructorOptions {
pub datamodel: String,
pub log_level: String,
#[serde(default)]
pub log_queries: bool,
#[serde(default)]
pub engine_protocol: Option<EngineProtocol>,

#[cfg(not(target_arch = "wasm32"))]
#[serde(flatten)]
pub native: ConstructorOptionsNative,
}

pub fn map_known_error(err: query_core::CoreError) -> crate::Result<String> {
let user_error: user_facing_errors::Error = err.into();
let value = serde_json::to_string(&user_error)?;

Ok(value)
}

pub fn stringify_env_values(origin: serde_json::Value) -> crate::Result<HashMap<String, String>> {
use serde_json::Value;

let msg = match origin {
Value::Object(map) => {
let mut result: HashMap<String, String> = HashMap::new();

for (key, val) in map.into_iter() {
match val {
Value::Null => continue,
Value::String(val) => {
result.insert(key, val);
}
val => {
result.insert(key, val.to_string());
}
}
}

return Ok(result);
}
Value::Null => return Ok(Default::default()),
Value::Bool(_) => "Expected an object for the env constructor parameter, got a boolean.",
Value::Number(_) => "Expected an object for the env constructor parameter, got a number.",
Value::String(_) => "Expected an object for the env constructor parameter, got a string.",
Value::Array(_) => "Expected an object for the env constructor parameter, got an array.",
};

Err(ApiError::JsonDecode(msg.to_string()))
}
104 changes: 104 additions & 0 deletions libs/query-engine-common/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use psl::diagnostics::Diagnostics;
use query_connector::error::ConnectorError;
use query_core::CoreError;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ApiError {
#[error("{:?}", _0)]
Conversion(Diagnostics, String),

#[error("{}", _0)]
Configuration(String),

#[error("{}", _0)]
Core(CoreError),

#[error("{}", _0)]
Connector(ConnectorError),

#[error("Can't modify an already connected engine.")]
AlreadyConnected,

#[error("Engine is not yet connected.")]
NotConnected,

#[error("{}", _0)]
JsonDecode(String),
}

impl From<ApiError> for user_facing_errors::Error {
fn from(err: ApiError) -> Self {
use std::fmt::Write as _;

match err {
ApiError::Connector(ConnectorError {
user_facing_error: Some(err),
..
}) => err.into(),
ApiError::Conversion(errors, dml_string) => {
let mut full_error = errors.to_pretty_string("schema.prisma", &dml_string);
write!(full_error, "\nValidation Error Count: {}", errors.errors().len()).unwrap();

user_facing_errors::Error::from(user_facing_errors::KnownError::new(
user_facing_errors::common::SchemaParserError { full_error },
))
}
ApiError::Core(error) => user_facing_errors::Error::from(error),
other => user_facing_errors::Error::new_non_panic_with_current_backtrace(other.to_string()),
}
}
}

impl ApiError {
pub fn conversion(diagnostics: Diagnostics, dml: impl ToString) -> Self {
Self::Conversion(diagnostics, dml.to_string())
}

pub fn configuration(msg: impl ToString) -> Self {
Self::Configuration(msg.to_string())
}
}

impl From<CoreError> for ApiError {
fn from(e: CoreError) -> Self {
match e {
CoreError::ConfigurationError(message) => Self::Configuration(message),
core_error => Self::Core(core_error),
}
}
}

impl From<ConnectorError> for ApiError {
fn from(e: ConnectorError) -> Self {
Self::Connector(e)
}
}

impl From<url::ParseError> for ApiError {
fn from(e: url::ParseError) -> Self {
Self::configuration(format!("Error parsing connection string: {e}"))
}
}

impl From<connection_string::Error> for ApiError {
fn from(e: connection_string::Error) -> Self {
Self::configuration(format!("Error parsing connection string: {e}"))
}
}

impl From<serde_json::Error> for ApiError {
fn from(e: serde_json::Error) -> Self {
Self::JsonDecode(format!("{e}"))
}
}

#[cfg(not(target_arch = "wasm32"))]
impl From<ApiError> for napi::Error {
fn from(e: ApiError) -> Self {
let user_facing = user_facing_errors::Error::from(e);
let message = serde_json::to_string(&user_facing).unwrap();

napi::Error::from_reason(message)
}
}
9 changes: 9 additions & 0 deletions libs/query-engine-common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//! Common definitions and functions for the Query Engine library.

pub mod engine;
pub mod error;
pub mod logger;
pub mod tracer;

pub type Result<T> = std::result::Result<T, error::ApiError>;
pub type Executor = Box<dyn query_core::QueryExecutor + Send + Sync>;
3 changes: 3 additions & 0 deletions libs/query-engine-common/src/logger.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub trait StringCallback {
fn call(&self, message: String) -> Result<(), String>;
}
Loading

0 comments on commit ae99f51

Please sign in to comment.