diff --git a/.github/package-filters/rs-packages-direct.yml b/.github/package-filters/rs-packages-direct.yml index 441c8137023..0ea1d150f9f 100644 --- a/.github/package-filters/rs-packages-direct.yml +++ b/.github/package-filters/rs-packages-direct.yml @@ -83,6 +83,9 @@ simple-signer: strategy-tests: - packages/strategy-tests/** +platform-query-wire: + - packages/rs-platform-query-wire/** + drive-abci: - packages/rs-drive-abci/** diff --git a/.github/package-filters/rs-packages-no-workflows.yml b/.github/package-filters/rs-packages-no-workflows.yml index 90835d0429f..a607e75df8a 100644 --- a/.github/package-filters/rs-packages-no-workflows.yml +++ b/.github/package-filters/rs-packages-no-workflows.yml @@ -86,11 +86,25 @@ strategy-tests: &strategy-tests - *simple-signer - *drive +dapi-grpc: &dapi_grpc + - packages/rs-platform-version/** + - packages/rs-dash-platform-macros/** + - packages/dapi-grpc/src/** + - packages/dapi-grpc/protos/** + - packages/dapi-grpc/build.rs + - packages/dapi-grpc/Cargo.toml + +platform-query-wire: &platform_query_wire + - packages/rs-platform-query-wire/** + - *drive + - *dapi_grpc + drive-abci: - packages/rs-drive-abci/** - *drive - *simple-signer - *strategy-tests + - *platform_query_wire dash-async: &dash_async - packages/rs-dash-async/** @@ -106,14 +120,6 @@ rs-sdk-trusted-context-provider: &sdk_trusted_context_provider - *context_provider - *dpp -dapi-grpc: &dapi_grpc - - packages/rs-platform-version/** - - packages/rs-dash-platform-macros/** - - packages/dapi-grpc/src/** - - packages/dapi-grpc/protos/** - - packages/dapi-grpc/build.rs - - packages/dapi-grpc/Cargo.toml - rs-dapi: - packages/rs-dapi/** - *dapi_grpc diff --git a/.github/package-filters/rs-packages.yml b/.github/package-filters/rs-packages.yml index 6fae2aa84ab..c973b1f7b59 100644 --- a/.github/package-filters/rs-packages.yml +++ b/.github/package-filters/rs-packages.yml @@ -102,12 +102,28 @@ strategy-tests: &strategy-tests - *simple-signer - *drive +dapi-grpc: &dapi_grpc + - .github/workflows/tests* + - packages/rs-platform-version/** + - packages/rs-dash-platform-macros/** + - packages/dapi-grpc/src/** + - packages/dapi-grpc/protos/** + - packages/dapi-grpc/build.rs + - packages/dapi-grpc/Cargo.toml + +platform-query-wire: &platform_query_wire + - .github/workflows/tests* + - packages/rs-platform-query-wire/** + - *drive + - *dapi_grpc + drive-abci: - .github/workflows/tests* - packages/rs-drive-abci/** - *drive - *simple-signer - *strategy-tests + - *platform_query_wire dash-async: &dash_async - .github/workflows/tests* @@ -126,15 +142,6 @@ rs-sdk-trusted-context-provider: &sdk_trusted_context_provider - *context_provider - *dpp -dapi-grpc: &dapi_grpc - - .github/workflows/tests* - - packages/rs-platform-version/** - - packages/rs-dash-platform-macros/** - - packages/dapi-grpc/src/** - - packages/dapi-grpc/protos/** - - packages/dapi-grpc/build.rs - - packages/dapi-grpc/Cargo.toml - rs-dapi-client: &dapi_client - .github/workflows/tests* - packages/rs-dapi-client/** diff --git a/.github/workflows/tests-rs-workspace.yml b/.github/workflows/tests-rs-workspace.yml index 6e64434f886..f5f4bb7bce5 100644 --- a/.github/workflows/tests-rs-workspace.yml +++ b/.github/workflows/tests-rs-workspace.yml @@ -203,13 +203,14 @@ jobs: cargo check -p dapi-grpc --no-default-features --features core,platform,client --locked cargo check -p drive-proof-verifier --locked cargo check -p dash-platform-queries --locked + cargo check -p platform-query-wire --locked # Native graphs: assert the networking transport stack stays out. # `tonic` itself is present (dapi-grpc's generated client types) but # without its transport feature — which is exactly what the absence # of hyper/rustls/tower proves. tokio is deliberately NOT asserted # absent: dash-context-provider depends on dash-async, which uses it # on native targets, and that edge predates the queries-crate split. - for native_package in drive-proof-verifier dash-platform-queries; do + for native_package in platform-query-wire drive-proof-verifier dash-platform-queries; do for banned in hyper rustls tower; do if cargo tree -p "$native_package" -e normal -i "$banned" 2>/dev/null | grep -q .; then echo "::error::$banned leaked into $native_package's dependency tree" @@ -344,6 +345,7 @@ jobs: --package rs-dapi-client \ --package platform-serialization \ --package dapi-grpc \ + --package platform-query-wire \ --package json-schema-compatibility-validator \ --package dashpay-contract \ --package dpns-contract \ diff --git a/Cargo.lock b/Cargo.lock index 8ad8efa2be1..908681a6868 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2256,6 +2256,7 @@ dependencies = [ "metrics-exporter-prometheus", "mockall", "nonempty", + "platform-query-wire", "platform-version", "prost 0.14.4", "rand 0.8.6", @@ -5167,6 +5168,16 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "platform-query-wire" +version = "4.2.0-dev.8" +dependencies = [ + "dapi-grpc", + "dpp", + "drive", + "thiserror 2.0.18", +] + [[package]] name = "platform-serialization" version = "4.2.0-dev.8" diff --git a/Cargo.toml b/Cargo.toml index 74e08d20530..0077561885a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "packages/rs-dash-platform-macros", "packages/rs-dpp", "packages/rs-drive", + "packages/rs-platform-query-wire", "packages/rs-platform-value", "packages/rs-platform-serialization", "packages/rs-platform-serialization-derive", diff --git a/Dockerfile b/Dockerfile index 846150203f7..6180dd691ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -387,6 +387,7 @@ COPY --parents \ packages/rs-platform-serialization-derive \ packages/rs-platform-version \ packages/rs-platform-versioning \ + packages/rs-platform-query-wire \ packages/rs-platform-value-convertible \ packages/rs-platform-wallet-ffi \ packages/rs-drive-abci \ @@ -515,6 +516,7 @@ COPY --parents \ packages/rs-platform-serialization-derive \ packages/rs-platform-version \ packages/rs-platform-versioning \ + packages/rs-platform-query-wire \ packages/rs-platform-value-convertible \ packages/rs-platform-wallet-ffi \ packages/rs-drive-abci \ @@ -681,6 +683,7 @@ COPY --parents \ packages/rs-platform-serialization-derive \ packages/rs-platform-version \ packages/rs-platform-versioning \ + packages/rs-platform-query-wire \ packages/rs-platform-value-convertible \ packages/rs-platform-wallet-ffi \ packages/rs-unified-sdk-ffi \ @@ -939,6 +942,7 @@ COPY --parents \ packages/rs-platform-serialization-derive \ packages/rs-platform-version \ packages/rs-platform-versioning \ + packages/rs-platform-query-wire \ packages/rs-platform-value-convertible \ packages/rs-platform-wallet-ffi \ packages/rs-drive-abci \ diff --git a/packages/rs-drive-abci/Cargo.toml b/packages/rs-drive-abci/Cargo.toml index 843fb9caee3..481e1673316 100644 --- a/packages/rs-drive-abci/Cargo.toml +++ b/packages/rs-drive-abci/Cargo.toml @@ -42,6 +42,9 @@ dapi-grpc = { path = "../dapi-grpc", default-features = false, features = [ "server", "platform", ] } +platform-query-wire = { path = "../rs-platform-query-wire", default-features = false, features = [ + "server", +] } tracing-subscriber = { version = "0.3.22", default-features = false, features = [ "env-filter", "ansi", diff --git a/packages/rs-drive-abci/src/error/query.rs b/packages/rs-drive-abci/src/error/query.rs index b1983823f92..8dd8f667fea 100644 --- a/packages/rs-drive-abci/src/error/query.rs +++ b/packages/rs-drive-abci/src/error/query.rs @@ -5,6 +5,7 @@ use dpp::ProtocolError; use drive::error::proof::ProofError; use drive::error::query::QuerySyntaxError as SyntaxError; use drive::error::Error as DriveError; +use platform_query_wire::proto_conversions::DecodeError as WireDecodeError; use prost::DecodeError; use tenderdash_abci::proto::abci::ResponseException; @@ -71,6 +72,21 @@ pub enum QueryError { ResourceExhausted(String), } +/// Wire-decode failures from the shared `platform-query-wire` decoders. +/// `InvalidArgument` is malformed wire input; `Unsupported` is a well-formed +/// shape the decoder deliberately refuses (e.g. `ORDER BY` on aggregate +/// keys) and surfaces as `QuerySyntaxError::Unsupported`, the same variant +/// the v1 handler's `not_yet_implemented` path uses. Both carry the message +/// string through unchanged. +impl From for QueryError { + fn from(error: WireDecodeError) -> Self { + match error { + WireDecodeError::InvalidArgument(msg) => QueryError::InvalidArgument(msg), + WireDecodeError::Unsupported(msg) => QueryError::Query(SyntaxError::Unsupported(msg)), + } + } +} + impl From for ResponseException { fn from(value: QueryError) -> Self { Self { @@ -78,3 +94,34 @@ impl From for ResponseException { } } } + +#[cfg(test)] +mod tests { + use super::*; + + /// The shared decoder's two variants map onto distinct `QueryError` + /// surfaces and the message text is preserved verbatim. A decoder that + /// later reclassifies a malformed shape as `Unsupported` (or vice versa) + /// changes what clients see, so the mapping is pinned here. + #[test] + fn wire_decode_error_mapping_preserves_variant_and_message() { + let invalid: QueryError = WireDecodeError::InvalidArgument("bad where".to_string()).into(); + assert!( + matches!(&invalid, QueryError::InvalidArgument(msg) if msg == "bad where"), + "unexpected: {invalid:?}" + ); + + let unsupported: QueryError = WireDecodeError::Unsupported( + "ORDER BY on aggregate keys is not yet implemented".to_string(), + ) + .into(); + assert!( + matches!( + &unsupported, + QueryError::Query(SyntaxError::Unsupported(msg)) + if msg == "ORDER BY on aggregate keys is not yet implemented" + ), + "unexpected: {unsupported:?}" + ); + } +} diff --git a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/chained.rs b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/chained.rs index d08c20cf4f4..f96fd2acafe 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/chained.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/chained.rs @@ -12,7 +12,6 @@ use crate::error::query::QueryError; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::platform_types::platform_state::PlatformState; -use crate::query::document_query::v1::conversions; use crate::query::response_metadata::CheckpointUsed; use crate::query::QueryValidationResult; use dapi_grpc::platform::v0::get_documents_request::get_documents_request_v1::{ @@ -36,6 +35,7 @@ use dpp::version::PlatformVersion; use drive::error::query::QuerySyntaxError; use drive::query::DriveDocumentQuery; use drive::util::grove_operations::GroveDBToUse; +use platform_query_wire::proto_conversions as conversions; impl Platform { /// Serve a chained-mode v1 request. Runs before select routing: @@ -120,11 +120,11 @@ impl Platform { let where_clauses = match conversions::where_clauses_from_proto(proto_where_clauses) { Ok(c) => c, - Err(e) => return Ok(QueryValidationResult::new_with_error(e)), + Err(e) => return Ok(QueryValidationResult::new_with_error(e.into())), }; let order_by_clauses = match conversions::order_clauses_from_proto(proto_order_by) { Ok(c) => c, - Err(e) => return Ok(QueryValidationResult::new_with_error(e)), + Err(e) => return Ok(QueryValidationResult::new_with_error(e.into())), }; let (_, contract_fetch_info) = check_validation_result_with_data!( diff --git a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/composite.rs b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/composite.rs index 9a6e3daf0d5..980d29ade75 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/dispatch/composite.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/dispatch/composite.rs @@ -13,7 +13,6 @@ use crate::error::query::QueryError; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::platform_types::platform_state::PlatformState; -use crate::query::document_query::v1::conversions; use crate::query::response_metadata::CheckpointUsed; use crate::query::QueryValidationResult; use dapi_grpc::platform::v0::get_documents_request::get_documents_request_v1::{ @@ -41,6 +40,7 @@ use drive::query::{ SubQueryResult, MAX_SUB_QUERIES, }; use drive::util::grove_operations::GroveDBToUse; +use platform_query_wire::proto_conversions as conversions; use std::sync::Arc; /// A sub-query's wire fields decoded into drive's typed forms, before @@ -152,11 +152,11 @@ impl Platform { let where_clauses = match conversions::where_clauses_from_proto(proto_where_clauses) { Ok(c) => c, - Err(e) => return Ok(QueryValidationResult::new_with_error(e)), + Err(e) => return Ok(QueryValidationResult::new_with_error(e.into())), }; let order_by_clauses = match conversions::order_clauses_from_proto(proto_order_by) { Ok(c) => c, - Err(e) => return Ok(QueryValidationResult::new_with_error(e)), + Err(e) => return Ok(QueryValidationResult::new_with_error(e.into())), }; // Every contract the composition touches, fetched once: the @@ -211,11 +211,11 @@ impl Platform { }; let where_clauses = match conversions::where_clauses_from_proto(proto.where_clauses) { Ok(c) => c, - Err(e) => return Ok(QueryValidationResult::new_with_error(e)), + Err(e) => return Ok(QueryValidationResult::new_with_error(e.into())), }; let order_by = match conversions::order_clauses_from_proto(proto.order_by) { Ok(c) => c, - Err(e) => return Ok(QueryValidationResult::new_with_error(e)), + Err(e) => return Ok(QueryValidationResult::new_with_error(e.into())), }; let binding = proto.bind.map(|bind| SubQueryBinding { source: match bind.source { diff --git a/packages/rs-drive-abci/src/query/document_query/v1/mod.rs b/packages/rs-drive-abci/src/query/document_query/v1/mod.rs index ecd1f066e36..c5efdae8cb7 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/mod.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/mod.rs @@ -27,7 +27,6 @@ //! `platform.proto` for the full supported / rejected shape table. mod compute_aggregate_mode_and_check_limit; -mod conversions; mod dispatch; mod routing; @@ -68,6 +67,7 @@ use dpp::version::PlatformVersion; use drive::drive::contract::DataContractFetchInfo; use drive::error::query::QuerySyntaxError; use drive::query::{resolve_time_range_bucket_clause, CountMode, SelectProjection}; +pub(super) use platform_query_wire::proto_conversions as conversions; use std::sync::Arc; /// Build a `QuerySyntaxError::Unsupported` carrying a stable @@ -332,7 +332,7 @@ impl Platform { let mut where_clauses = match conversions::where_clauses_from_proto(normal_proto) { Ok(c) => c, - Err(e) => return Ok(QueryValidationResult::new_with_error(e)), + Err(e) => return Ok(QueryValidationResult::new_with_error(e.into())), }; let mut resolved_time_ranges: Vec = Vec::new(); // The contract fetched for time-range resolution, handed to the @@ -375,7 +375,7 @@ impl Platform { let (field, selector, grid) = match conversions::time_range_clause_from_proto(proto_wc) { Ok(parsed) => parsed, - Err(e) => return Ok(QueryValidationResult::new_with_error(e)), + Err(e) => return Ok(QueryValidationResult::new_with_error(e.into())), }; match resolve_time_range_bucket_clause( &field, @@ -403,11 +403,11 @@ impl Platform { } let order_by_clauses = match conversions::order_clauses_from_proto(proto_order_by) { Ok(c) => c, - Err(e) => return Ok(QueryValidationResult::new_with_error(e)), + Err(e) => return Ok(QueryValidationResult::new_with_error(e.into())), }; let having_clauses = match conversions::having_clauses_from_proto(having) { Ok(c) => c, - Err(e) => return Ok(QueryValidationResult::new_with_error(e)), + Err(e) => return Ok(QueryValidationResult::new_with_error(e.into())), }; // `selects` is `repeated Select` on the wire. Empty @@ -430,7 +430,7 @@ impl Platform { let select = match proto_selects.into_iter().next() { Some(s) => match conversions::select_from_proto(s) { Ok(s) => s, - Err(e) => return Ok(QueryValidationResult::new_with_error(e)), + Err(e) => return Ok(QueryValidationResult::new_with_error(e.into())), }, None => SelectProjection::documents(), }; diff --git a/packages/rs-platform-query-wire/Cargo.toml b/packages/rs-platform-query-wire/Cargo.toml new file mode 100644 index 00000000000..3b8b37d3d94 --- /dev/null +++ b/packages/rs-platform-query-wire/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "platform-query-wire" +description = "Shared wire-request decoders mapping Dash Platform query protos onto drive query types, used by drive-abci and client-side proof verifiers" +version.workspace = true +edition = "2021" +rust-version.workspace = true +license = "MIT" + +[features] +# The `drive::query` types this crate decodes into are gated on either of +# drive's `server` / `verify` features, so exactly one must be on. `verify` +# is the default (the client-verifier cut); rs-drive-abci opts out of +# defaults and selects `server`, so the consensus binary gains no +# proof-verification code from this crate. +default = ["verify"] +server = ["drive/server"] +verify = ["drive/verify"] + +[dependencies] +# `client` is what makes dapi-grpc emit the platform message types on +# native targets; no transport is pulled in without the `transport` feature. +dapi-grpc = { path = "../dapi-grpc", default-features = false, features = [ + "platform", + "client", +] } +dpp = { path = "../rs-dpp", default-features = false } +drive = { path = "../rs-drive", default-features = false } +thiserror = "2.0.17" diff --git a/packages/rs-platform-query-wire/README.md b/packages/rs-platform-query-wire/README.md new file mode 100644 index 00000000000..006f4d7ef46 --- /dev/null +++ b/packages/rs-platform-query-wire/README.md @@ -0,0 +1,37 @@ +# platform-query-wire + +Shared wire→drive decoding for Dash Platform queries. + +This micro-crate is the single home of the decoders that map query +wire-proto types (from `dapi-grpc`) onto `drive::query` types — +currently the v1 `getDocuments` surface (WHERE / ORDER BY / HAVING / +SELECT clauses and their field values). + +## Scope + +- **Decode only.** No transport, no networking, no proof + verification, no async runtime. +- Errors surface through a neutral [`DecodeError`] (`InvalidArgument` + for malformed wire input, `Unsupported` for well-formed input naming + a capability the target cannot represent yet); each consumer maps it + onto its own error surface. + +## Why a dedicated crate + +The decode of a wire request into a rich query is an equivalence +contract at a trust boundary: a client-side proof verifier must +interpret a request exactly as the server does, or a proof could +verify against a different query than the server answered. + +- The **server** (`rs-drive-abci`) decodes every incoming v1 + `getDocuments` request through this crate. +- **Client-side verifiers** (SDK proof verification) are intended to + decode through the same functions, so server and client wire + interpretation cannot drift. + +Hosting the shared code here — rather than in a client/SDK crate — +keeps the consensus server's dependency graph free of client-flavored +dependencies. A consumer selects the `server` or `verify` feature to +match the `drive` build it already carries (`verify` is the default; +`rs-drive-abci` opts out and takes `server`), so the server gains no +proof-verification code and a client verifier gains no server code. diff --git a/packages/rs-platform-query-wire/src/lib.rs b/packages/rs-platform-query-wire/src/lib.rs new file mode 100644 index 00000000000..f40f0f8e9d5 --- /dev/null +++ b/packages/rs-platform-query-wire/src/lib.rs @@ -0,0 +1,15 @@ +//! Shared wire→drive decoding for Dash Platform queries. +//! +//! This crate is the single home of the decoders that map query +//! wire-proto types (from `dapi-grpc`) onto `drive::query` types. It +//! is consumed by the server (rs-drive-abci decodes incoming requests +//! through it) and is intended for client-side proof verifiers, so +//! the server's and a verifier's interpretation of the same request +//! bytes cannot drift. +//! +//! Scope is deliberately narrow: decode only. No transport, no proof +//! verification, no networking. Consumers select the `server` or +//! `verify` feature to match their own `drive` build (`verify` is the +//! default; rs-drive-abci opts out and takes `server`). + +pub mod proto_conversions; diff --git a/packages/rs-drive-abci/src/query/document_query/v1/conversions.rs b/packages/rs-platform-query-wire/src/proto_conversions.rs similarity index 81% rename from packages/rs-drive-abci/src/query/document_query/v1/conversions.rs rename to packages/rs-platform-query-wire/src/proto_conversions.rs index 9fe8eee949c..cf49ac5f76d 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/conversions.rs +++ b/packages/rs-platform-query-wire/src/proto_conversions.rs @@ -1,21 +1,26 @@ -//! Wire-protobuf → drive type conversions for the v1 document +//! Wire-protobuf → drive type conversions for the `getDocuments` //! query surface. //! -//! Lives next to the v1 handler because rs-drive-abci is the only -//! crate that needs the proto-decode direction (the SDK ships the -//! inverse direction in -//! `rs-sdk/src/platform/documents/document_query.rs`). Keeping the -//! two directions in their respective crates avoids forcing -//! `dapi-grpc` into rs-drive's dependency graph just to host shared -//! conversion code. +//! This is the **single** proto-decode implementation, shared by: +//! - rs-drive-abci's v1 request handler (server side — decodes the +//! incoming request before routing/execution), and +//! - client-side proof verifiers (they rebuild the rich query from the +//! wire request so a proved response can be verified against exactly +//! what was asked). +//! +//! Both directions living on one implementation is the point: the +//! bytes the server decodes and the bytes the verifier decodes must +//! agree clause-for-clause, or a proof could verify against a +//! different query than the server answered. //! //! Conversion contract: -//! - Every fallible case maps to [`QueryError::InvalidArgument`] -//! (malformed wire input, **not** future capability). The v1 -//! handler distinguishes this from -//! [`QuerySyntaxError::Unsupported`] (valid request shape, server -//! capability not yet wired) — see `v1/mod.rs`'s -//! `not_yet_implemented` helper. +//! - Every fallible case maps to [`DecodeError::InvalidArgument`] +//! (malformed wire input, **not** future capability), except the +//! aggregate `ORDER BY` target which maps to +//! [`DecodeError::Unsupported`] (valid request shape, server +//! capability not yet wired). rs-drive-abci maps these onto its +//! `QueryError::InvalidArgument` / `QuerySyntaxError::Unsupported` +//! respectively, preserving its historical error surface. //! - Conversion is schema-agnostic. `DocumentFieldValue` variants //! map 1:1 to `dpp::platform_value::Value` variants without //! consulting the document type's schema. The schema-driven @@ -26,7 +31,6 @@ //! identifier, and so on. The wire layer just names the //! primitive; the schema decides the indexed type. -use crate::error::query::QueryError; use dapi_grpc::platform::v0::get_documents_request::{ document_field_value, get_documents_request_v1::{select, Select as ProtoSelect}, @@ -43,14 +47,34 @@ use drive::query::{ OrderClause, SelectFunction, SelectProjection, TimeRangeSelector, WhereClause, WhereOperator, }; +/// Decode failure of a wire query message. +/// +/// Deliberately not a server or client error type: rs-drive-abci +/// maps it onto its `QueryError`, and client-side consumers map it +/// onto their own error surface, each preserving its own error +/// contract. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum DecodeError { + /// Malformed wire input — bad discriminant, missing oneof arm, + /// over-deep list nesting. No future protocol version would make + /// this input valid. + #[error("{0}")] + InvalidArgument(String), + /// Well-formed wire input naming a capability the decode target + /// cannot represent yet (e.g. `ORDER BY` on an aggregate key). + /// The wording signals future capability, not malformed request. + #[error("{0}")] + Unsupported(String), +} + /// Map a wire-level [`ProtoWhereOperator`] discriminant onto /// drive's [`WhereOperator`]. Unknown discriminants are wire-level /// garbage (no future protocol value would map a malformed integer /// to a valid behavior), so they surface as -/// [`QueryError::InvalidArgument`] — not `not_yet_implemented`. -pub(super) fn where_operator_from_proto(op: i32) -> Result { +/// [`DecodeError::InvalidArgument`] — not `not_yet_implemented`. +pub fn where_operator_from_proto(op: i32) -> Result { let proto_op = ProtoWhereOperator::try_from(op).map_err(|_| { - QueryError::InvalidArgument(format!( + DecodeError::InvalidArgument(format!( "unknown WhereOperator discriminant: {} (valid values: 0..=11, see \ `get_documents_request::WhereOperator`)", op @@ -73,7 +97,7 @@ pub(super) fn where_operator_from_proto(op: i32) -> Result { - return Err(QueryError::InvalidArgument( + return Err(DecodeError::InvalidArgument( "IN_TIME_RANGE where clauses are resolved from block time before \ operator conversion and must not be mixed into normal clause decoding" .to_string(), @@ -86,7 +110,7 @@ pub(super) fn where_operator_from_proto(op: i32) -> Result bool { +pub fn is_time_range_clause(clause: &ProtoWhereClause) -> bool { clause.operator == ProtoWhereOperator::InTimeRange as i32 } @@ -111,25 +135,25 @@ pub(super) fn is_time_range_clause(clause: &ProtoWhereClause) -> bool { /// a better message than the resolver's no-such-grid miss). A zero /// `phase` IS the canonical spelling of a phaseless grid, matching the /// contract grammar where `phase` is an omittable key. -pub(super) fn time_range_clause_from_proto( +pub fn time_range_clause_from_proto( clause: ProtoWhereClause, -) -> Result<(String, TimeRangeSelector, Option), QueryError> { +) -> Result<(String, TimeRangeSelector, Option), DecodeError> { let field = clause.field; if clause.value.is_some() { - return Err(QueryError::InvalidArgument(format!( + return Err(DecodeError::InvalidArgument(format!( "IN_TIME_RANGE clause on field '{}' must not set `value`: the operand is the \ typed `time_range` selection", field ))); } let selection = clause.time_range.ok_or_else(|| { - QueryError::InvalidArgument(format!( + DecodeError::InvalidArgument(format!( "IN_TIME_RANGE clause on field '{}' has no `time_range` selection set", field )) })?; let proto_selector = ProtoTimeRangeSelector::try_from(selection.selector).map_err(|_| { - QueryError::InvalidArgument(format!( + DecodeError::InvalidArgument(format!( "unknown TimeRangeSelection.Selector discriminant on field '{}': {} (valid \ values: NEWEST = 0, OLDEST = 1, BY_START = 2)", field, selection.selector @@ -142,14 +166,14 @@ pub(super) fn time_range_clause_from_proto( TimeRangeSelector::ByStart { start_ms } } (ProtoTimeRangeSelector::ByStart, None) => { - return Err(QueryError::InvalidArgument(format!( + return Err(DecodeError::InvalidArgument(format!( "IN_TIME_RANGE BY_START on field '{}' requires `start_ms` naming the \ window's start (a millisecond timestamp on the grid)", field ))) } (ProtoTimeRangeSelector::Newest | ProtoTimeRangeSelector::Oldest, Some(_)) => { - return Err(QueryError::InvalidArgument(format!( + return Err(DecodeError::InvalidArgument(format!( "IN_TIME_RANGE on field '{}': `start_ms` is only meaningful with \ BY_START; the relative selectors resolve their window from block time", field @@ -160,7 +184,7 @@ pub(super) fn time_range_clause_from_proto( .grid .map(|grid| { if grid.range == 0 || grid.step == 0 { - return Err(QueryError::InvalidArgument(format!( + return Err(DecodeError::InvalidArgument(format!( "IN_TIME_RANGE grid on field '{}' must carry the contract's declared \ `range` and `step` (non-zero seconds); a zero phase is the canonical \ spelling of a phaseless grid", @@ -190,7 +214,7 @@ pub(super) fn time_range_clause_from_proto( /// operand is always concrete; empty where-clauses are expressed /// by an empty `where_clauses` field at the request level, not by /// sending an empty `DocumentFieldValue`. -pub(super) fn value_from_proto(value: ProtoDocumentFieldValue) -> Result { +pub fn value_from_proto(value: ProtoDocumentFieldValue) -> Result { value_from_proto_at_depth(value, 0) } @@ -201,9 +225,9 @@ pub(super) fn value_from_proto(value: ProtoDocumentFieldValue) -> Result Result { +) -> Result { let variant = value.variant.ok_or_else(|| { - QueryError::InvalidArgument( + DecodeError::InvalidArgument( "DocumentFieldValue has no variant set; a where-clause operand must \ be a concrete value" .to_string(), @@ -218,7 +242,7 @@ fn value_from_proto_at_depth( document_field_value::Variant::BytesValue(b) => Value::Bytes(b), document_field_value::Variant::List(list) => { if depth >= 1 { - return Err(QueryError::InvalidArgument( + return Err(DecodeError::InvalidArgument( "nested DocumentFieldValue.list is not supported; the v1 \ query surface accepts at most one level of nesting \ (`IN` / `BETWEEN*` candidate lists of scalars)" @@ -242,23 +266,23 @@ fn value_from_proto_at_depth( /// Map a wire [`ProtoWhereClause`] onto drive's structured /// [`WhereClause`]. Errors surface as -/// [`QueryError::InvalidArgument`] for both operator-discriminant +/// [`DecodeError::InvalidArgument`] for both operator-discriminant /// and value-shape failures. -pub(super) fn where_clause_from_proto(clause: ProtoWhereClause) -> Result { +pub fn where_clause_from_proto(clause: ProtoWhereClause) -> Result { let operator = where_operator_from_proto(clause.operator)?; // `time_range` is IN_TIME_RANGE's operand and those clauses are // partitioned out before this conversion (see `is_time_range_clause`), // so on any clause reaching here a set `time_range` is a malformed mix // of the two operand kinds. if clause.time_range.is_some() { - return Err(QueryError::InvalidArgument(format!( + return Err(DecodeError::InvalidArgument(format!( "WhereClause on field '{}' sets `time_range`, which is only valid with the \ IN_TIME_RANGE operator", clause.field ))); } let value = clause.value.ok_or_else(|| { - QueryError::InvalidArgument(format!( + DecodeError::InvalidArgument(format!( "WhereClause on field '{}' has no value set; every clause must carry a \ concrete `DocumentFieldValue`", clause.field @@ -278,9 +302,9 @@ pub(super) fn where_clause_from_proto(clause: ProtoWhereClause) -> Result, -) -> Result, QueryError> { +) -> Result, DecodeError> { clauses.into_iter().map(where_clause_from_proto).collect() } @@ -291,16 +315,14 @@ pub(super) fn where_clauses_from_proto( /// (aggregate function applied to a field — wire-only, rejected /// at routing time with `Unsupported("ORDER BY on aggregate …")`). /// Unset (`None`) is rejected as malformed wire input. -pub(super) fn order_clause_from_proto(clause: ProtoOrderClause) -> Result { +pub fn order_clause_from_proto(clause: ProtoOrderClause) -> Result { let ascending = clause.ascending; match clause.target { Some(order_clause::Target::Field(field)) => Ok(OrderClause { field, ascending }), - Some(order_clause::Target::Aggregate(_)) => Err(QueryError::Query( - drive::error::query::QuerySyntaxError::Unsupported( - "ORDER BY on aggregate keys is not yet implemented".to_string(), - ), + Some(order_clause::Target::Aggregate(_)) => Err(DecodeError::Unsupported( + "ORDER BY on aggregate keys is not yet implemented".to_string(), )), - None => Err(QueryError::InvalidArgument( + None => Err(DecodeError::InvalidArgument( "OrderClause has no target set; every clause must carry either a \ `field` (plain column name) or an `aggregate` (aggregate-function \ ordering target)" @@ -312,9 +334,9 @@ pub(super) fn order_clause_from_proto(clause: ProtoOrderClause) -> Result, -) -> Result, QueryError> { +) -> Result, DecodeError> { clauses.into_iter().map(order_clause_from_proto).collect() } @@ -331,11 +353,11 @@ pub(super) fn order_clauses_from_proto( /// drive's [`HavingAggregateFunction`]. Unknown discriminants are /// wire-level garbage (no future protocol value would map a /// malformed integer to a valid behavior), so they surface as -/// [`QueryError::InvalidArgument`]. +/// [`DecodeError::InvalidArgument`]. #[allow(dead_code)] -fn having_function_from_proto(function: i32) -> Result { +fn having_function_from_proto(function: i32) -> Result { let proto = having_aggregate::Function::try_from(function).map_err(|_| { - QueryError::InvalidArgument(format!( + DecodeError::InvalidArgument(format!( "unknown HavingAggregate.Function discriminant: {} (valid values: 0..=2, see \ `get_documents_request::having_aggregate::Function`)", function @@ -352,9 +374,9 @@ fn having_function_from_proto(function: i32) -> Result Result { +fn having_operator_from_proto(operator: i32) -> Result { let proto = having_clause::Operator::try_from(operator).map_err(|_| { - QueryError::InvalidArgument(format!( + DecodeError::InvalidArgument(format!( "unknown HavingClause.Operator discriminant: {} (valid values: 0..=10, see \ `get_documents_request::having_clause::Operator`)", operator @@ -384,7 +406,7 @@ fn having_operator_from_proto(operator: i32) -> Result Result { +) -> Result { Ok(HavingAggregate { function: having_function_from_proto(aggregate.function)?, field: aggregate.field, @@ -393,7 +415,7 @@ fn having_aggregate_from_proto( /// Map a wire [`ProtoHavingClause`] onto drive's structured /// [`HavingClause`]. Errors surface as -/// [`QueryError::InvalidArgument`] for any wire-level +/// [`DecodeError::InvalidArgument`] for any wire-level /// malformation: unknown discriminant on the aggregate function or /// operator; missing aggregate; missing right operand (oneof unset /// on the wire); inner value-shape failures on the literal-value @@ -406,11 +428,9 @@ fn having_aggregate_from_proto( /// LIMIT n [OFFSET m]` — which arrives as an `OrderClause` and never /// reaches here. #[allow(dead_code)] -pub(super) fn having_clause_from_proto( - clause: ProtoHavingClause, -) -> Result { +pub fn having_clause_from_proto(clause: ProtoHavingClause) -> Result { let aggregate = clause.aggregate.ok_or_else(|| { - QueryError::InvalidArgument( + DecodeError::InvalidArgument( "HavingClause has no aggregate set; every clause must carry an \ aggregate function + field operand" .to_string(), @@ -419,7 +439,7 @@ pub(super) fn having_clause_from_proto( let aggregate = having_aggregate_from_proto(aggregate)?; let operator = having_operator_from_proto(clause.operator)?; let right = clause.right.ok_or_else(|| { - QueryError::InvalidArgument( + DecodeError::InvalidArgument( "HavingClause has no right operand set; every clause must carry a \ concrete `DocumentFieldValue` (`right.value`)" .to_string(), @@ -439,9 +459,9 @@ pub(super) fn having_clause_from_proto( /// level `repeated HavingClause` field. Returns an error on the /// first malformed clause. #[allow(dead_code)] -pub(super) fn having_clauses_from_proto( +pub fn having_clauses_from_proto( clauses: Vec, -) -> Result, QueryError> { +) -> Result, DecodeError> { clauses.into_iter().map(having_clause_from_proto).collect() } @@ -449,10 +469,10 @@ pub(super) fn having_clauses_from_proto( /// [`SelectFunction`]. Unknown discriminants are wire-level /// garbage (no future protocol value would map a malformed /// integer to a valid behavior), so they surface as -/// [`QueryError::InvalidArgument`]. -fn select_function_from_proto(function: i32) -> Result { +/// [`DecodeError::InvalidArgument`]. +fn select_function_from_proto(function: i32) -> Result { let proto = select::Function::try_from(function).map_err(|_| { - QueryError::InvalidArgument(format!( + DecodeError::InvalidArgument(format!( "unknown Select.Function discriminant: {} (valid values: 0..=5, see \ `get_documents_request::get_documents_request_v1::select::Function`)", function @@ -478,7 +498,7 @@ fn select_function_from_proto(function: i32) -> Result Result { +pub fn select_from_proto(select: ProtoSelect) -> Result { Ok(SelectProjection { function: select_function_from_proto(select.function)?, field: select.field,