refactor(flow): execute streaming flows with DataFusion (#8976)

* test(mito2): cover regex inverted index pruning

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* refactor(flow): execute streaming flows with DataFusion

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* refactor(flow): remove legacy streaming runtime

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(flow): avoid retrying stateless sink inserts

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(flow): align stateless writes with sink schema

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(flow): reject stale stateless source schemas

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(flow): validate stateless flow routing

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* Revert "test(mito2): cover regex inverted index pruning"

This reverts commit 79e96ac745.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(flow): preserve source timestamps in stateless flows

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(flow): address stateless review feedback

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(flow): recover stateless flows after schema changes

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(flow): serialize schema rebuilds and validate retained sources

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* test(flow): verify streaming recovery and schema changes through SQL

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(flow): cool down failed schema rebuilds

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(flow): explain legacy aggregate recreation requirements

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(flow): adapt stateless provider downcast to DataFusion 55

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

---------

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
This commit is contained in:
discord9
2026-09-17 06:44:01 +00:00
committed by GitHub
parent 604c88e7e2
commit 7c7132ea65
57 changed files with 3385 additions and 17407 deletions
Generated
+5 -143
View File
@@ -1289,7 +1289,7 @@ dependencies = [
"proc-macro2",
"quote",
"regex",
"rustc-hash 2.1.1",
"rustc-hash",
"shlex 1.3.0",
"syn 2.0.117",
]
@@ -1741,12 +1741,6 @@ dependencies = [
"shlex 2.0.1",
]
[[package]]
name = "cc-traits"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "060303ef31ef4a522737e1b1ab68c67916f2a787bb2f4f54f383279adba962b5"
[[package]]
name = "cedarwood"
version = "0.5.0"
@@ -4849,36 +4843,6 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "dfir_rs"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3b08cdfdba4b482d762906a16ac2cfd45d9949ae62e03ed3c690cfd7dae5dc"
dependencies = [
"bincode",
"byteorder",
"bytes",
"futures",
"getrandom 0.2.16",
"itertools 0.13.0",
"lattices",
"pusherator",
"ref-cast",
"regex",
"rustc-hash 1.1.0",
"sealed",
"serde",
"serde_json",
"slotmap",
"smallvec",
"tokio",
"tokio-stream",
"tokio-util",
"tracing",
"variadics",
"web-time",
]
[[package]]
name = "diff"
version = "0.1.13"
@@ -5126,18 +5090,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "enum-as-inner"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "enum_dispatch"
version = "0.3.13"
@@ -5506,7 +5458,6 @@ dependencies = [
"async-recursion",
"async-trait",
"bytes",
"cache",
"catalog",
"chrono",
"client",
@@ -5536,8 +5487,6 @@ dependencies = [
"datafusion-physical-expr",
"datafusion-substrait",
"datatypes",
"dfir_rs",
"enum-as-inner",
"enum_dispatch",
"futures",
"get-size2",
@@ -7340,7 +7289,7 @@ dependencies = [
"jieba-macros",
"phf 0.13.1",
"regex",
"rustc-hash 2.1.1",
"rustc-hash",
]
[[package]]
@@ -7753,33 +7702,6 @@ dependencies = [
"rustversion",
]
[[package]]
name = "lattices"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51222529a85db7b6e228b279b2b9bb7bc1e47d9ee7b680404bdf5440b255913c"
dependencies = [
"cc-traits",
"lattices_macro",
"ref-cast",
"sealed",
"serde",
"variadics",
"variadics_macro",
]
[[package]]
name = "lattices_macro"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eee67863077d060b8a25c754241a6e6bf234b82207a62df7b3d5387b6d523c3e"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "lazy-regex"
version = "3.4.1"
@@ -11869,16 +11791,6 @@ dependencies = [
"pulldown-cmark",
]
[[package]]
name = "pusherator"
version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3378af79ad42241f1075683daa2a0c06c6654404c3d096a493348395e620f668"
dependencies = [
"either",
"variadics",
]
[[package]]
name = "query"
version = "1.3.0-alpha.1"
@@ -12001,7 +11913,7 @@ dependencies = [
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash 2.1.1",
"rustc-hash",
"rustls",
"socket2 0.5.10",
"thiserror 2.0.17",
@@ -12023,7 +11935,7 @@ dependencies = [
"rand 0.10.1",
"rand_pcg",
"ring",
"rustc-hash 2.1.1",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
@@ -12984,12 +12896,6 @@ version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.1"
@@ -13334,18 +13240,6 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "sealed"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4a8caec23b7800fb97971a1c6ae365b6239aaeddfb934d6265f8505e795699d"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "secrecy"
version = "0.8.0"
@@ -13947,15 +13841,6 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "slotmap"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
dependencies = [
"version_check",
]
[[package]]
name = "smallvec"
version = "1.15.1"
@@ -14899,7 +14784,7 @@ dependencies = [
"rayon",
"regex",
"rust-stemmers",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
"sketches-ddsketch",
@@ -16329,29 +16214,6 @@ dependencies = [
"ryu",
]
[[package]]
name = "variadics"
version = "0.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d49933ddcb1a4ec2ebb9a2931bc526ed269a9276a456c0595ddcc7cb188896"
dependencies = [
"hashbrown 0.14.5",
"sealed",
]
[[package]]
name = "variadics_macro"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55476accb4f25086be8a1d18bed73aef9452cefb5ccd133e122e4350a1543ff3"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro2",
"quote",
"syn 2.0.117",
"variadics",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
+1 -23
View File
@@ -37,12 +37,9 @@ use common_stat::ResourceStatImpl;
use common_telemetry::info;
use common_telemetry::logging::{DEFAULT_LOGGING_DIR, TracingOptions};
use common_version::{short_version, verbose_version};
use flow::{
FlownodeBuilder, FlownodeInstance, FlownodeServiceBuilder, FrontendClient, FrontendInvoker,
};
use flow::{FlownodeBuilder, FlownodeInstance, FlownodeServiceBuilder, FrontendClient};
use meta_client::{MetaClientOptions, MetaClientType};
use plugins::flownode::context::GrpcConfigureContext;
use servers::addrs;
use servers::configurator::GrpcBuilderConfiguratorRef;
use snafu::{OptionExt, ResultExt, ensure};
use tracing_appender::non_blocking::WorkerGuard;
@@ -410,25 +407,6 @@ impl StartCommand {
.build()
.context(StartFlownodeSnafu)?;
flownode.setup_services(services);
let flownode = flownode;
let invoker = FrontendInvoker::build_from(
flownode.flow_engine().streaming_engine(),
catalog_manager.clone(),
cached_meta_backend.clone(),
layered_cache_registry.clone(),
meta_client.clone(),
client,
addrs::resolve_addr(&opts.grpc.bind_addr, Some(&opts.grpc.server_addr)),
)
.await
.context(StartFlownodeSnafu)?;
flownode
.flow_engine()
.streaming_engine()
// TODO(discord9): refactor and avoid circular reference
.set_frontend_invoker(invoker)
.await;
Ok(Instance::new(flownode, guard))
}
+1 -17
View File
@@ -57,7 +57,7 @@ use datanode::datanode::{Datanode, DatanodeBuilder};
use datanode::region_server::RegionServer;
use flow::{
FlowDualEngineRef, FlownodeBuilder, FlownodeInstance, FlownodeOptions, FrontendClient,
FrontendInvoker, GrpcQueryHandlerWithBoxedError,
GrpcQueryHandlerWithBoxedError,
};
use frontend::frontend::Frontend;
use frontend::instance::builder::FrontendBuilder;
@@ -703,22 +703,6 @@ impl StartCommand {
.set_handler(weak_grpc_handler)
.await;
// set the frontend invoker for flownode
let flow_streaming_engine = flow_engine.streaming_engine();
// flow server need to be able to use frontend to write insert requests back
let invoker = FrontendInvoker::build_from(
flow_streaming_engine.clone(),
catalog_manager.clone(),
kv_backend.clone(),
layered_cache_registry.clone(),
procedure_executor,
node_manager.clone(),
fe_instance.frontend_peer_addr().to_string(),
)
.await
.context(StartFlownodeSnafu)?;
flow_streaming_engine.set_frontend_invoker(invoker).await;
let servers = Services::new(opts, fe_instance.clone(), plugins.clone())
.build()
.context(error::StartFrontendSnafu)?;
+31 -52
View File
@@ -1,20 +1,20 @@
# flow — Agent & Contributor Guide
Navigation aid for `src/flow`. Keep it short and point to code. Paths are
relative to the repo root.
relative to the repository root.
Repo-wide rules that apply here: [`.agents/architecture-invariants.md`](../../.agents/architecture-invariants.md).
Repo-wide rules that apply to here: [`.agents/architecture-invariants.md`](../../.agents/architecture-invariants.md).
## What this crate does
Flownode is the stream-processing engine behind continuous aggregation /
materialized views. It has two execution paths:
Flownode provides two flow profiles:
- **Batching mode** (the actively developed path): splits data
into time windows and periodically runs aggregation SQL through the frontend,
writing results back to a sink table.
- **Streaming mode** (the legacy dataflow path): an incremental DFIR/dataflow
compute graph that processes row-level diffs.
- **Streaming mode**: a single stateless DataFusion query profile. Each mirror
insert is materialized as a transient input table, executed against the
retained logical plan, and written directly to the sink through
`batching_mode::frontend_client::FrontendClient`.
- **Batching mode**: splits data into time windows and periodically runs
aggregation SQL through the frontend, writing results back to a sink table.
Users cannot select a mode directly: `flow_type` is a reserved internal option.
`StatementExecutor::determine_flow_type` in `src/operator/src/statement/ddl.rs`
@@ -26,16 +26,11 @@ changing routing rules.
| Module | Path | Purpose |
| --- | --- | --- |
| `engine` | `src/flow/src/engine.rs` | `FlowEngine` trait: create/remove/flush/insert lifecycle |
| `adapter` | `src/flow/src/adapter.rs`, `src/flow/src/adapter/` | `StreamingEngine`, worker pool, dual-engine dispatch, table sources/sinks |
| `batching_mode` | `src/flow/src/batching_mode.rs`, `src/flow/src/batching_mode/` | `BatchingEngine`, task scheduling, time windows, frontend client, checkpoints |
| `compute` | `src/flow/src/compute/` | Streaming dataflow render/state |
| `expr` | `src/flow/src/expr.rs`, `src/flow/src/expr/` | Scalar/aggregate expressions and Map-Filter-Project |
| `plan` | `src/flow/src/plan.rs` | `TypedPlan` (reduce/join/MFP) |
| `transform` | `src/flow/src/transform.rs` | Substrait → flow plan |
| `df_optimizer` | `src/flow/src/df_optimizer.rs` | SQL → DataFusion logical plan → optimized plan |
| `repr` | `src/flow/src/repr.rs` | `Row`, `DiffRow`, `Batch`, `RelationDesc` |
| `server` | `src/flow/src/server.rs` | gRPC `Flow` service, `FlownodeBuilder`/`FlownodeInstance` |
| `engine` | `src/flow/src/engine.rs` | `FlowEngine` lifecycle contract |
| `adapter` | `src/flow/src/adapter.rs`, `src/flow/src/adapter/` | Stateless streaming execution, dual-engine dispatch, shared source schema/default normalization, and sink creation |
| `batching_mode` | `src/flow/src/batching_mode.rs`, `src/flow/src/batching_mode/` | `BatchingEngine`, scheduling, time windows, frontend client, and checkpoints |
| `repr` | `src/flow/src/repr.rs` | Shared row and relation schema representations |
| `server` | `src/flow/src/server.rs` | gRPC `Flow` service and flownode builders |
| `heartbeat` | `src/flow/src/heartbeat.rs` | Reports flownode state/stats to metasrv |
Flow metadata lives in `common-meta`, not here:
@@ -43,53 +38,37 @@ Flow metadata lives in `common-meta`, not here:
## Data flow
`Frontend → Flownode (gRPC)` → `FlowService` (`server.rs`) →
`FlowDualEngine` (`adapter/flownode_impl.rs`) routes by `FlowType`:
`Frontend → Flownode (gRPC)` → `FlowService` (`server.rs`) → `FlowDualEngine`
(`adapter/flownode_impl.rs`) routes by `FlowType`:
- Streaming: mirror rows are normalized against the source schema, evaluated by
the retained stateless DataFusion plan, and sent to the sink via
`FrontendClient`.
- Batching: marks dirty windows; a task later runs aggregation SQL via the
frontend client and writes the sink table (`batching_mode/task.rs`).
- Streaming: worker threads apply incremental diffs and push to the sink
(`adapter/worker.rs`, `compute/render.rs`).
frontend client and writes the sink table.
`FlowType::Streaming` and mirror routing are shared contracts; preserve both
when changing dispatch. Preserve batching behavior and the shared schema,
default, and sink-creation helpers.
## Public surface
- gRPC `Flow` service in `src/flow/src/server.rs`
(`handle_create_remove`, `handle_mirror_request`, `handle_mark_dirty_time_window`).
- Sink writes for the streaming engine go through `FrontendInvoker`
(`row_inserts`, `row_deletes`).
- gRPC `Flow` service in `src/flow/src/server.rs`.
- `FlowEngine` trait in `src/flow/src/engine.rs`.
- Started from the `cmd` crate via `FlownodeBuilder` / `FlownodeInstance`.
## When you change X, also touch Y
- **Flow definition / options**: validation in `common-meta`'s `ddl/create_flow.rs`
and the serialized `FlowInfoValue` in `common-meta`'s `key/flow/`.
- **New scalar/aggregate function** (`expr/`): also wire up evaluation in
`compute/render.rs` (streaming) and ensure batching SQL handles it.
- **Persisted flow metadata**: keep `FlowInfoValue` backward compatible
(`serde(default)` / `serde(alias)`).
- **Shared routing, metadata, or sink contracts**: check both engines. An
engine-specific implementation change does not automatically need a mirror
change in the other path.
## Testing
```bash
cargo nextest run -p flow
```
Helpers in `src/flow/src/test_utils.rs` (test context, test query engine).
Stateless streaming tests are under `src/flow/src/adapter/stateless.rs`.
## Gotchas
- Batching vs streaming differ in latency, state, and execution. Confirm the
selected mode before reasoning about a flow.
- Streaming workers are `!Send`; cross-thread interaction goes through
`WorkerHandle`, not the worker directly.
- Internal flow timestamps (`repr::Timestamp`, ms) are not necessarily the
table's time column; window functions key off the diff timestamp.
## Maintenance contract
Update this file when the dual-engine routing, the gRPC surface, or the flow
metadata contract (shared with `common-meta`) changes.
- Batching and streaming differ in latency and state. Confirm the selected mode
before reasoning about a flow.
- Streaming is finite and stateless per mirror request; it has no worker graph,
background runtime, or replay state.
- Internal flow timestamps and table time columns are separate contracts.
-3
View File
@@ -14,7 +14,6 @@ arrow-schema.workspace = true
async-recursion = "1.0"
async-trait.workspace = true
bytes.workspace = true
cache.workspace = true
catalog.workspace = true
chrono.workspace = true
client.workspace = true
@@ -43,8 +42,6 @@ datafusion-expr.workspace = true
datafusion-physical-expr.workspace = true
datafusion-substrait.workspace = true
datatypes.workspace = true
dfir_rs = { version = "0.13.0", default-features = false }
enum-as-inner = "0.6.0"
enum_dispatch = "0.3"
futures.workspace = true
get-size2 = "0.1.2"
+1042 -769
View File
File diff suppressed because it is too large Load Diff
+14 -177
View File
@@ -33,13 +33,12 @@ use common_meta::key::flow::FlowMetadataManager;
use common_meta::key::flow::flow_info::FlowScheduleConfig;
use common_meta::key::flow::flow_state::FlowStat;
use common_runtime::JoinHandle;
use common_telemetry::{error, info, trace, warn};
use datatypes::value::Value;
use common_telemetry::{error, info, warn};
use futures::TryStreamExt;
use itertools::Itertools;
use operator::utils::try_to_session_query_context;
use session::context::QueryContextBuilder;
use snafu::{IntoError, OptionExt, ResultExt, ensure};
use snafu::{OptionExt, ResultExt, ensure};
use store_api::storage::{RegionId, TableId};
use tokio::sync::{Mutex, RwLock};
@@ -48,11 +47,10 @@ use crate::batching_mode::engine::BatchingEngine;
use crate::engine::{FlowEngine, FlowStatProvider};
use crate::error::{
CreateFlowSnafu, ExternalSnafu, FlowNotFoundSnafu, FlowNotRecoveredSnafu,
IllegalCheckTaskStateSnafu, InsertIntoFlowSnafu, InternalSnafu, JoinTaskSnafu, ListFlowsSnafu,
NoAvailableFrontendSnafu, SyncCheckTaskSnafu, UnexpectedSnafu, UnsupportedSnafu,
IllegalCheckTaskStateSnafu, InternalSnafu, JoinTaskSnafu, ListFlowsSnafu,
NoAvailableFrontendSnafu, SyncCheckTaskSnafu, UnsupportedSnafu,
};
use crate::metrics::{METRIC_FLOW_ROWS, METRIC_FLOW_TASK_COUNT};
use crate::repr::{self, DiffRow};
use crate::utils::StateReportHandler;
use crate::{Error, FlowId};
@@ -165,23 +163,7 @@ impl FlowDualEngine {
}
pub async fn gen_state_report(&self) -> FlowStat {
let streaming = self.streaming_engine.flow_stat().await;
let batching = self.batching_engine.flow_stat().await;
let mut state_size = streaming.state_size;
state_size.extend(batching.state_size);
let mut last_exec_time_map = streaming.last_exec_time_map;
last_exec_time_map.extend(batching.last_exec_time_map);
let mut start_time_map = streaming.start_time_map;
start_time_map.extend(batching.start_time_map);
FlowStat {
state_size,
last_exec_time_map,
start_time_map,
}
self.batching_engine.flow_stat().await
}
/// Start state report task, which receives a sender from heartbeat task and sends report back.
@@ -702,6 +684,11 @@ impl FlowEngine for FlowDualEngine {
async fn remove_flow(&self, flow_id: FlowId) -> Result<(), Error> {
let flow_type = self.src_table2flow.read().await.get_flow_type(flow_id);
let flow_type = match flow_type {
Some(flow_type) => Some(flow_type),
None if self.streaming_engine.flow_exist(flow_id).await? => Some(FlowType::Streaming),
None => None,
};
match flow_type {
Some(FlowType::Batching) => self.batching_engine.remove_flow(flow_id).await,
@@ -742,11 +729,12 @@ impl FlowEngine for FlowDualEngine {
async fn flow_exist(&self, flow_id: FlowId) -> Result<bool, Error> {
let flow_type = self.src_table2flow.read().await.get_flow_type(flow_id);
// not using `flow_type.is_some()` to make sure the flow is actually exist in the underlying engine
// Check the underlying registry so stateless flows survive a missing
// routing-map entry during recovery.
match flow_type {
Some(FlowType::Batching) => self.batching_engine.flow_exist(flow_id).await,
Some(FlowType::Streaming) => self.streaming_engine.flow_exist(flow_id).await,
None => Ok(false),
None => self.streaming_engine.flow_exist(flow_id).await,
}
}
@@ -990,13 +978,7 @@ impl FlowEngine for StreamingEngine {
}
async fn list_flows(&self) -> Result<impl IntoIterator<Item = FlowId>, Error> {
Ok(self
.flow_err_collectors
.read()
.await
.keys()
.cloned()
.collect::<Vec<_>>())
Ok(self.stateless_flow_ids().await)
}
async fn handle_flow_inserts(
@@ -1017,151 +999,6 @@ impl FlowEngine for StreamingEngine {
}
}
/// Simple helper enum for fetching value from row with default value
#[derive(Debug, Clone)]
enum FetchFromRow {
Idx(usize),
Default(Value),
}
impl FetchFromRow {
/// Panic if idx is out of bound
fn fetch(&self, row: &repr::Row) -> Value {
match self {
FetchFromRow::Idx(idx) => row.get(*idx).unwrap().clone(),
FetchFromRow::Default(v) => v.clone(),
}
}
}
impl StreamingEngine {
async fn handle_inserts_inner(
&self,
request: InsertRequests,
) -> std::result::Result<(), Error> {
// using try_read to ensure two things:
// 1. flush wouldn't happen until inserts before it is inserted
// 2. inserts happening concurrently with flush wouldn't be block by flush
let _flush_lock = self.flush_lock.try_read();
for write_request in request.requests {
let region_id = write_request.region_id;
let table_id = RegionId::from(region_id).table_id();
let (insert_schema, rows_proto) = write_request
.rows
.map(|r| (r.schema, r.rows))
.unwrap_or_default();
// TODO(discord9): reconsider time assignment mechanism
let now = self.tick_manager.tick();
let (table_types, fetch_order) = {
let ctx = self.node_context.read().await;
// TODO(discord9): also check schema version so that altered table can be reported
let table_schema = ctx.table_source.table_from_id(&table_id).await?;
let default_vals = table_schema
.default_values
.iter()
.zip(table_schema.relation_desc.typ().column_types.iter())
.map(|(v, ty)| {
v.as_ref().and_then(|v| {
match v.create_default(ty.scalar_type(), ty.nullable()) {
Ok(v) => Some(v),
Err(err) => {
common_telemetry::error!(err; "Failed to create default value");
None
}
}
})
})
.collect_vec();
let table_types = table_schema
.relation_desc
.typ()
.column_types
.clone()
.into_iter()
.map(|t| t.scalar_type)
.collect_vec();
let table_col_names = table_schema.relation_desc.names;
let table_col_names = table_col_names
.iter().enumerate()
.map(|(idx,name)| match name {
Some(name) => Ok(name.clone()),
None => InternalSnafu {
reason: format!("Expect column {idx} of table id={table_id} to have name in table schema, found None"),
}
.fail(),
})
.collect::<Result<Vec<_>, _>>()?;
let name_to_col = HashMap::<_, _>::from_iter(
insert_schema
.iter()
.enumerate()
.map(|(i, name)| (&name.column_name, i)),
);
let fetch_order: Vec<FetchFromRow> = table_col_names
.iter()
.zip(default_vals)
.map(|(col_name, col_default_val)| {
name_to_col
.get(col_name)
.copied()
.map(FetchFromRow::Idx)
.or_else(|| col_default_val.clone().map(FetchFromRow::Default))
.with_context(|| UnexpectedSnafu {
reason: format!(
"Column not found: {}, default_value: {:?}",
col_name, col_default_val
),
})
})
.try_collect()?;
trace!("Reordering columns: {:?}", fetch_order);
(table_types, fetch_order)
};
// TODO(discord9): use column instead of row
let rows: Vec<DiffRow> = rows_proto
.into_iter()
.map(|r| {
let r = repr::Row::from(r);
let reordered = fetch_order.iter().map(|i| i.fetch(&r)).collect_vec();
repr::Row::new(reordered)
})
.map(|r| (r, now, 1))
.collect_vec();
if let Err(err) = self
.handle_write_request(region_id.into(), rows, &table_types)
.await
{
let err = BoxedError::new(err);
let flow_ids = self
.node_context
.read()
.await
.get_flow_ids(table_id)
.into_iter()
.flatten()
.cloned()
.collect_vec();
let err = InsertIntoFlowSnafu {
region_id,
flow_ids,
}
.into_error(err);
common_telemetry::error!(err; "Failed to handle write request");
return Err(err);
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
-458
View File
@@ -1,458 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Node context, prone to change with every incoming requests
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use common_recordbatch::RecordBatch;
use common_telemetry::trace;
use datatypes::prelude::ConcreteDataType;
use session::context::QueryContext;
use snafu::{OptionExt, ResultExt};
use table::metadata::TableId;
use tokio::sync::{RwLock, broadcast, mpsc};
use crate::adapter::table_source::FlowTableSource;
use crate::adapter::{FlowId, ManagedTableSource, TableName};
use crate::error::{Error, EvalSnafu, TableNotFoundSnafu};
use crate::expr::error::InternalSnafu;
use crate::expr::{Batch, GlobalId};
use crate::metrics::METRIC_FLOW_INPUT_BUF_SIZE;
use crate::plan::TypedPlan;
use crate::repr::{BATCH_SIZE, BROADCAST_CAP, DiffRow, RelationDesc, SEND_BUF_CAP};
/// A context that holds the information of the dataflow
#[derive(Debug)]
pub struct FlownodeContext {
/// mapping from source table to tasks, useful for schedule which task to run when a source table is updated
pub source_to_tasks: BTreeMap<TableId, BTreeSet<FlowId>>,
/// mapping from task to sink table, useful for sending data back to the client when a task is done running
pub flow_to_sink: BTreeMap<FlowId, TableName>,
pub flow_plans: BTreeMap<FlowId, TypedPlan>,
pub sink_to_flow: BTreeMap<TableName, FlowId>,
/// broadcast sender for source table, any incoming write request will be sent to the source table's corresponding sender
///
/// Note that we are getting insert requests with table id, so we should use table id as the key
pub source_sender: BTreeMap<TableId, SourceSender>,
/// broadcast receiver for sink table, there should only be one receiver, and it will receive all the data from the sink table
///
/// and send it back to the client, since we are mocking the sink table as a client, we should use table name as the key
/// note that the sink receiver should only have one, and we are using broadcast as mpsc channel here
pub sink_receiver:
BTreeMap<TableName, (mpsc::UnboundedSender<Batch>, mpsc::UnboundedReceiver<Batch>)>,
/// can query the schema of the table source, from metasrv with local cache
pub table_source: Box<dyn FlowTableSource>,
/// All the tables that have been registered in the worker
pub table_repr: IdToNameMap,
pub query_context: Option<Arc<QueryContext>>,
}
impl FlownodeContext {
pub fn new(table_source: Box<dyn FlowTableSource>) -> Self {
Self {
source_to_tasks: Default::default(),
flow_to_sink: Default::default(),
flow_plans: Default::default(),
sink_to_flow: Default::default(),
source_sender: Default::default(),
sink_receiver: Default::default(),
table_source,
table_repr: Default::default(),
query_context: Default::default(),
}
}
pub fn get_flow_ids(&self, table_id: TableId) -> Option<&BTreeSet<FlowId>> {
self.source_to_tasks.get(&table_id)
}
}
/// a simple broadcast sender with backpressure, bounded capacity and blocking on send when send buf is full
/// note that it wouldn't evict old data, so it's possible to block forever if the receiver is slow
///
/// receiver still use tokio broadcast channel, since only sender side need to know
/// backpressure and adjust dataflow running duration to avoid blocking
#[derive(Debug)]
pub struct SourceSender {
// TODO(discord9): make it all Vec<DiffRow>?
sender: broadcast::Sender<Batch>,
send_buf_tx: mpsc::Sender<Batch>,
send_buf_rx: RwLock<mpsc::Receiver<Batch>>,
send_buf_row_cnt: AtomicUsize,
}
impl Default for SourceSender {
fn default() -> Self {
// TODO(discord9): the capacity is arbitrary, we can adjust it later, might also want to limit the max number of rows in send buf
let (send_buf_tx, send_buf_rx) = mpsc::channel(SEND_BUF_CAP);
Self {
// TODO(discord9): found a better way then increase this to prevent lagging and hence missing input data
sender: broadcast::Sender::new(SEND_BUF_CAP),
send_buf_tx,
send_buf_rx: RwLock::new(send_buf_rx),
send_buf_row_cnt: AtomicUsize::new(0),
}
}
}
impl SourceSender {
/// max number of iterations to try flush send buf
const MAX_ITERATIONS: usize = 16;
pub fn get_receiver(&self) -> broadcast::Receiver<Batch> {
self.sender.subscribe()
}
/// send as many as possible rows from send buf
/// until send buf is empty or broadchannel is full
pub async fn try_flush(&self) -> Result<usize, Error> {
let mut row_cnt = 0;
loop {
let mut send_buf = self.send_buf_rx.write().await;
// if inner sender channel is empty or send buf is empty, there
// is nothing to do for now, just break
if self.sender.len() >= BROADCAST_CAP || send_buf.is_empty() {
break;
}
// TODO(discord9): send rows instead so it's just moving a point
if let Some(batch) = send_buf.recv().await {
let len = batch.row_count();
if let Err(prev_row_cnt) =
self.send_buf_row_cnt
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |x| x.checked_sub(len))
{
common_telemetry::error!(
"send buf row count underflow, prev = {}, len = {}",
prev_row_cnt,
len
);
}
row_cnt += len;
self.sender
.send(batch)
.map_err(|err| {
InternalSnafu {
reason: format!("Failed to send row, error = {:?}", err),
}
.build()
})
.with_context(|_| EvalSnafu)?;
}
}
if row_cnt > 0 {
trace!("Source Flushed {} rows", row_cnt);
METRIC_FLOW_INPUT_BUF_SIZE.sub(row_cnt as _);
trace!(
"Remaining Source Send buf.len() = {}",
METRIC_FLOW_INPUT_BUF_SIZE.get()
);
}
Ok(row_cnt)
}
/// return number of rows it actual send(including what's in the buffer)
pub async fn send_rows(
&self,
rows: Vec<DiffRow>,
batch_datatypes: &[ConcreteDataType],
) -> Result<usize, Error> {
METRIC_FLOW_INPUT_BUF_SIZE.add(rows.len() as _);
// important for backpressure. if send buf is full, block until it's not
while self.send_buf_row_cnt.load(Ordering::SeqCst) >= BATCH_SIZE * 4 {
tokio::task::yield_now().await;
}
// row count metrics is approx so relaxed order is ok
let batch = Batch::try_from_rows_with_types(
rows.into_iter().map(|(row, _, _)| row).collect(),
batch_datatypes,
)
.context(EvalSnafu)?;
common_telemetry::trace!("Send one batch to worker with {} rows", batch.row_count());
self.send_buf_row_cnt
.fetch_add(batch.row_count(), Ordering::SeqCst);
self.send_buf_tx.send(batch).await.map_err(|e| {
crate::error::InternalSnafu {
reason: format!("Failed to send row, error = {:?}", e),
}
.build()
})?;
Ok(0)
}
/// send record batch
pub async fn send_record_batch(&self, batch: RecordBatch) -> Result<usize, Error> {
let row_cnt = batch.num_rows();
let batch = Batch::try_from(batch)?;
self.send_buf_row_cnt.fetch_add(row_cnt, Ordering::SeqCst);
self.send_buf_tx.send(batch).await.map_err(|e| {
crate::error::InternalSnafu {
reason: format!("Failed to send batch, error = {:?}", e),
}
.build()
})?;
Ok(row_cnt)
}
}
impl FlownodeContext {
/// return number of rows it actual send(including what's in the buffer)
///
/// TODO(discord9): make this concurrent
pub async fn send(
&self,
table_id: TableId,
rows: Vec<DiffRow>,
batch_datatypes: &[ConcreteDataType],
) -> Result<usize, Error> {
let sender = self
.source_sender
.get(&table_id)
.with_context(|| TableNotFoundSnafu {
name: table_id.to_string(),
})?;
sender.send_rows(rows, batch_datatypes).await
}
pub async fn send_rb(&self, table_id: TableId, batch: RecordBatch) -> Result<usize, Error> {
let sender = self
.source_sender
.get(&table_id)
.with_context(|| TableNotFoundSnafu {
name: table_id.to_string(),
})?;
sender.send_record_batch(batch).await
}
/// flush all sender's buf
///
/// return numbers being sent
pub async fn flush_all_sender(&self) -> Result<usize, Error> {
let mut sum = 0;
for sender in self.source_sender.values() {
sender.try_flush().await.inspect(|x| sum += x)?;
}
Ok(sum)
}
}
impl FlownodeContext {
/// mapping source table to task, and sink table to task in worker context
///
/// also add their corresponding broadcast sender/receiver
pub fn register_task_src_sink(
&mut self,
task_id: FlowId,
source_table_ids: &[TableId],
sink_table_name: TableName,
) {
for source_table_id in source_table_ids {
self.add_source_sender_if_not_exist(*source_table_id);
self.source_to_tasks
.entry(*source_table_id)
.or_default()
.insert(task_id);
}
self.add_sink_receiver(sink_table_name.clone());
self.flow_to_sink.insert(task_id, sink_table_name.clone());
self.sink_to_flow.insert(sink_table_name, task_id);
}
/// add flow plan to worker context
pub fn add_flow_plan(&mut self, task_id: FlowId, plan: TypedPlan) {
self.flow_plans.insert(task_id, plan);
}
pub fn get_flow_plan(&self, task_id: &FlowId) -> Option<TypedPlan> {
self.flow_plans.get(task_id).cloned()
}
/// remove flow from worker context
pub fn remove_flow(&mut self, task_id: FlowId) {
if let Some(sink_table_name) = self.flow_to_sink.remove(&task_id) {
self.sink_to_flow.remove(&sink_table_name);
}
for (source_table_id, tasks) in self.source_to_tasks.iter_mut() {
tasks.remove(&task_id);
if tasks.is_empty() {
self.source_sender.remove(source_table_id);
}
}
self.flow_plans.remove(&task_id);
}
/// try add source sender, if already exist, do nothing
pub fn add_source_sender_if_not_exist(&mut self, table_id: TableId) {
let _sender = self.source_sender.entry(table_id).or_default();
}
pub fn add_sink_receiver(&mut self, table_name: TableName) {
self.sink_receiver
.entry(table_name)
.or_insert_with(mpsc::unbounded_channel);
}
pub fn get_source_by_global_id(&self, id: &GlobalId) -> Result<&SourceSender, Error> {
let table_id = self
.table_repr
.get_by_global_id(id)
.with_context(|| TableNotFoundSnafu {
name: format!("Global Id = {:?}", id),
})?
.1
.with_context(|| TableNotFoundSnafu {
name: format!("Table Id = {:?}", id),
})?;
self.source_sender
.get(&table_id)
.with_context(|| TableNotFoundSnafu {
name: table_id.to_string(),
})
}
pub fn get_sink_by_global_id(
&self,
id: &GlobalId,
) -> Result<mpsc::UnboundedSender<Batch>, Error> {
let table_name = self
.table_repr
.get_by_global_id(id)
.with_context(|| TableNotFoundSnafu {
name: format!("{:?}", id),
})?
.0
.with_context(|| TableNotFoundSnafu {
name: format!("Global Id = {:?}", id),
})?;
self.sink_receiver
.get(&table_name)
.map(|(s, _r)| s.clone())
.with_context(|| TableNotFoundSnafu {
name: table_name.join("."),
})
}
}
impl FlownodeContext {
/// Retrieves a GlobalId and table schema representing a table previously registered by calling the [register_table] function.
///
/// Returns an error if no table has been registered with the provided names
pub async fn table(&self, name: &TableName) -> Result<(GlobalId, RelationDesc), Error> {
let id = self
.table_repr
.get_by_name(name)
.map(|(_tid, gid)| gid)
.with_context(|| TableNotFoundSnafu {
name: name.join("."),
})?;
let schema = self.table_source.table(name).await?;
Ok((id, schema.relation_desc))
}
/// Assign a global id to a table, if already assigned, return the existing global id
///
/// require at least one of `table_name` or `table_id` to be `Some`
///
/// and will try to fetch the schema from table info manager(if table exist now)
///
/// NOTE: this will not actually render the table into collection referred as GlobalId
/// merely creating a mapping from table id to global id
pub async fn assign_global_id_to_table(
&mut self,
srv_map: &ManagedTableSource,
mut table_name: Option<TableName>,
table_id: Option<TableId>,
) -> Result<GlobalId, Error> {
// if we can find by table name/id. not assign it
if let Some(gid) = table_name
.as_ref()
.and_then(|table_name| self.table_repr.get_by_name(table_name))
.map(|(_, gid)| gid)
.or_else(|| {
table_id
.and_then(|id| self.table_repr.get_by_table_id(&id))
.map(|(_, gid)| gid)
})
{
Ok(gid)
} else {
let global_id = self.new_global_id();
// table id is Some meaning db must have created the table
if let Some(table_id) = table_id {
let known_table_name = srv_map.get_table_name(&table_id).await?;
table_name = table_name.or(Some(known_table_name));
} // if we don't have table id, it means database haven't assign one yet or we don't need it
// still update the mapping with new global id
self.table_repr.insert(table_name, table_id, global_id);
Ok(global_id)
}
}
/// Get a new global id
pub fn new_global_id(&self) -> GlobalId {
GlobalId::User(self.table_repr.global_id_to_name_id.len() as u64)
}
}
/// A tri-directional map that maps table name, table id, and global id
#[derive(Default, Debug)]
pub struct IdToNameMap {
name_to_global_id: HashMap<TableName, GlobalId>,
id_to_global_id: HashMap<TableId, GlobalId>,
global_id_to_name_id: BTreeMap<GlobalId, (Option<TableName>, Option<TableId>)>,
}
impl IdToNameMap {
pub fn new() -> Self {
Default::default()
}
pub fn insert(&mut self, name: Option<TableName>, id: Option<TableId>, global_id: GlobalId) {
name.clone()
.and_then(|name| self.name_to_global_id.insert(name.clone(), global_id));
id.and_then(|id| self.id_to_global_id.insert(id, global_id));
self.global_id_to_name_id.insert(global_id, (name, id));
}
pub fn get_by_name(&self, name: &TableName) -> Option<(Option<TableId>, GlobalId)> {
self.name_to_global_id.get(name).map(|global_id| {
let (_name, id) = self.global_id_to_name_id.get(global_id).unwrap();
(*id, *global_id)
})
}
pub fn get_by_table_id(&self, id: &TableId) -> Option<(Option<TableName>, GlobalId)> {
self.id_to_global_id.get(id).map(|global_id| {
let (name, _id) = self.global_id_to_name_id.get(global_id).unwrap();
(name.clone(), *global_id)
})
}
pub fn get_by_global_id(
&self,
global_id: &GlobalId,
) -> Option<(Option<TableName>, Option<TableId>)> {
self.global_id_to_name_id.get(global_id).cloned()
}
}
-245
View File
@@ -1,245 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! parse expr like "ts <= now() - interval '5 m'"
use nom::IResult;
use nom::branch::alt;
use nom::bytes::complete::{tag, tag_no_case};
use nom::character::complete::{alphanumeric1, digit0, multispace0};
use nom::combinator::peek;
use nom::sequence::tuple;
use crate::repr;
#[test]
fn test_parse_duration() {
let input = "1 h 5 m 42 second";
let (remain, ttl) = parse_duration(input).unwrap();
assert_eq!(remain, "");
assert_eq!(ttl, (3600 + 5 * 60 + 42) * 1000);
}
#[test]
fn test_parse_fixed() {
let input = "timestamp < now() - INTERVAL '5m 42s'";
let (remain, ttl) = parse_fixed(input).unwrap();
assert_eq!(remain, "");
assert_eq!(ttl, (5 * 60 + 42) * 1000);
}
pub fn parse_fixed(input: &str) -> IResult<&str, i64> {
let (r, _) = tuple((
multispace0,
tag_no_case("timestamp"),
multispace0,
tag("<"),
multispace0,
tag_no_case("now()"),
multispace0,
tag("-"),
multispace0,
tag_no_case("interval"),
multispace0,
))(input)?;
tuple((tag("'"), parse_duration, tag("'")))(r).map(|(r, (_, ttl, _))| (r, ttl))
}
/// parse duration and return ttl, currently only support time part of psql interval type
pub fn parse_duration(input: &str) -> IResult<&str, i64> {
let mut intervals = vec![];
let mut remain = input;
while peek(parse_quality)(remain).is_ok() {
let (r, number) = parse_quality(remain)?;
let (r, unit) = parse_time_unit(r)?;
intervals.push((number, unit));
remain = r;
}
let mut total = 0;
for (number, unit) in intervals {
let number = match unit {
TimeUnit::Second => number,
TimeUnit::Minute => number * 60,
TimeUnit::Hour => number * 60 * 60,
};
total += number;
}
total *= 1000;
Ok((remain, total))
}
enum Expr {
Col(String),
Now,
Duration(repr::Duration),
Binary {
left: Box<Expr>,
op: String,
right: Box<Expr>,
},
}
fn parse_expr(input: &str) -> IResult<&str, Expr> {
parse_expr_bp(input, 0)
}
/// a simple pratt parser
fn parse_expr_bp(input: &str, min_bp: u8) -> IResult<&str, Expr> {
let (mut input, mut lhs): (&str, Expr) = parse_item(input)?;
loop {
let (r, op) = parse_op(input)?;
let (_, (l_bp, r_bp)) = infix_binding_power(op)?;
if l_bp < min_bp {
return Ok((input, lhs));
}
let (r, rhs) = parse_expr_bp(r, r_bp)?;
input = r;
lhs = Expr::Binary {
left: Box::new(lhs),
op: op.to_string(),
right: Box::new(rhs),
};
}
}
fn parse_op(input: &str) -> IResult<&str, &str> {
alt((parse_add_sub, parse_cmp))(input)
}
fn parse_item(input: &str) -> IResult<&str, Expr> {
if let Ok((r, name)) = parse_col_name(input) {
Ok((r, Expr::Col(name.to_string())))
} else if let Ok((r, _now)) = parse_now(input) {
Ok((r, Expr::Now))
} else if let Ok((_r, _num)) = parse_quality(input) {
todo!()
} else {
todo!()
}
}
fn infix_binding_power(op: &str) -> IResult<&str, (u8, u8)> {
let ret = match op {
"<" | ">" | "<=" | ">=" => (1, 2),
"+" | "-" => (3, 4),
_ => {
return Err(nom::Err::Error(nom::error::Error::new(
op,
nom::error::ErrorKind::Fail,
)));
}
};
Ok((op, ret))
}
fn parse_col_name(input: &str) -> IResult<&str, &str> {
tuple((multispace0, alphanumeric1, multispace0))(input).map(|(r, (_, name, _))| (r, name))
}
fn parse_now(input: &str) -> IResult<&str, &str> {
tag_no_case("now()")(input)
}
fn parse_add_sub(input: &str) -> IResult<&str, &str> {
tuple((multispace0, alt((tag("+"), tag("-"))), multispace0))(input)
.map(|(r, (_, op, _))| (r, op))
}
fn parse_cmp(input: &str) -> IResult<&str, &str> {
tuple((
multispace0,
alt((tag("<="), tag(">="), tag("<"), tag(">"))),
multispace0,
))(input)
.map(|(r, (_, op, _))| (r, op))
}
/// parse a number with optional sign
fn parse_quality(input: &str) -> IResult<&str, repr::Duration> {
tuple((
multispace0,
alt((tag("+"), tag("-"), tag(""))),
digit0,
multispace0,
))(input)
.map(|(r, (_, sign, name, _))| (r, sign, name))
.and_then(|(r, sign, name)| {
let num = name.parse::<repr::Duration>().map_err(|_| {
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
})?;
let num = match sign {
"+" => num,
"-" => -num,
_ => num,
};
Ok((r, num))
})
}
#[derive(Debug, Clone)]
enum TimeUnit {
Second,
Minute,
Hour,
}
#[derive(Debug, Clone)]
enum DateUnit {
Day,
Month,
Year,
}
fn parse_time_unit(input: &str) -> IResult<&str, TimeUnit> {
fn to_second(input: &str) -> IResult<&str, TimeUnit> {
alt((
tag_no_case("second"),
tag_no_case("seconds"),
tag_no_case("S"),
))(input)
.map(move |(r, _)| (r, TimeUnit::Second))
}
fn to_minute(input: &str) -> IResult<&str, TimeUnit> {
alt((
tag_no_case("minute"),
tag_no_case("minutes"),
tag_no_case("m"),
))(input)
.map(move |(r, _)| (r, TimeUnit::Minute))
}
fn to_hour(input: &str) -> IResult<&str, TimeUnit> {
alt((tag_no_case("hour"), tag_no_case("hours"), tag_no_case("h")))(input)
.map(move |(r, _)| (r, TimeUnit::Hour))
}
tuple((
multispace0,
alt((
to_second, to_minute,
to_hour, /*
tag_no_case("day"),
tag_no_case("days"),
tag_no_case("d"),
tag_no_case("month"),
tag_no_case("months"),
tag_no_case("m"),
tag_no_case("year"),
tag_no_case("years"),
tag_no_case("y"),
*/
)),
multispace0,
))(input)
.map(|(r, (_, unit, _))| (r, unit))
}
-440
View File
@@ -1,440 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! This module contains the refill flow task, which is used to refill flow with given table id and a time range.
use std::collections::BTreeSet;
use std::sync::Arc;
use catalog::CatalogManagerRef;
use client::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
use common_error::ext::BoxedError;
use common_meta::key::flow::FlowMetadataManagerRef;
use common_recordbatch::{RecordBatch, RecordBatches, SendableRecordBatchStream};
use common_runtime::JoinHandle;
use common_telemetry::error;
use datatypes::value::Value;
use futures::StreamExt;
use query::parser::QueryLanguageParser;
use session::context::QueryContextBuilder;
use snafu::{OptionExt, ResultExt, ensure};
use table::metadata::TableId;
use crate::adapter::table_source::ManagedTableSource;
use crate::adapter::{FlowId, FlowStreamingEngineRef, StreamingEngine};
use crate::error::{FlowNotFoundSnafu, JoinTaskSnafu, UnexpectedSnafu};
use crate::expr::error::ExternalSnafu;
use crate::expr::utils::find_plan_time_window_expr_lower_bound;
use crate::repr::RelationDesc;
use crate::server::get_all_flow_ids;
use crate::{Error, FrontendInvoker};
impl StreamingEngine {
/// Create and start refill flow tasks in background
pub async fn create_and_start_refill_flow_tasks(
self: &FlowStreamingEngineRef,
flow_metadata_manager: &FlowMetadataManagerRef,
catalog_manager: &CatalogManagerRef,
) -> Result<(), Error> {
let tasks = self
.create_refill_flow_tasks(flow_metadata_manager, catalog_manager)
.await?;
self.starting_refill_flows(tasks).await?;
Ok(())
}
/// Create a series of tasks to refill flow
pub async fn create_refill_flow_tasks(
&self,
flow_metadata_manager: &FlowMetadataManagerRef,
catalog_manager: &CatalogManagerRef,
) -> Result<Vec<RefillTask>, Error> {
let nodeid = self.node_id.map(|c| c as u64);
let flow_ids = get_all_flow_ids(flow_metadata_manager, catalog_manager, nodeid).await?;
let mut refill_tasks = Vec::new();
'flow_id_loop: for flow_id in flow_ids {
let info = flow_metadata_manager
.flow_info_manager()
.get(flow_id)
.await
.map_err(BoxedError::new)
.context(ExternalSnafu)?
.context(FlowNotFoundSnafu { id: flow_id })?;
// TODO(discord9): also check flow is already running
for src_table in info.source_table_ids() {
// check if source table still exists
if !self.table_info_source.check_table_exist(src_table).await? {
error!(
"Source table id = {:?} not found while refill flow_id={}, consider re-create the flow if necessary",
src_table, flow_id
);
continue 'flow_id_loop;
}
}
let expire_after = info
.expire_after()
.map(super::expire_after_secs_to_millis)
.transpose()?;
// TODO(discord9): better way to get last point
let now = self.tick_manager.tick();
let plan = self
.node_context
.read()
.await
.get_flow_plan(&FlowId::from(flow_id))
.context(FlowNotFoundSnafu { id: flow_id })?;
let time_range = if let Some(expire_after) = expire_after {
let low_bound = common_time::Timestamp::new_millisecond(now - expire_after);
let real_low_bound = find_plan_time_window_expr_lower_bound(&plan, low_bound)?;
real_low_bound.map(|l| (l, common_time::Timestamp::new_millisecond(now)))
} else {
None
};
common_telemetry::debug!(
"Time range for refill flow_id={} is {:?}",
flow_id,
time_range
);
for src_table in info.source_table_ids() {
let time_index_col = self
.table_info_source
.get_time_index_column_from_table_id(*src_table)
.await?
.1;
let time_index_name = time_index_col.name;
let task = RefillTask::create(
flow_id as u64,
*src_table,
time_range,
&time_index_name,
&self.table_info_source,
)
.await?;
refill_tasks.push(task);
}
}
Ok(refill_tasks)
}
/// Starting to refill flows, if any error occurs, will rebuild the flow and retry
pub(crate) async fn starting_refill_flows(
self: &FlowStreamingEngineRef,
tasks: Vec<RefillTask>,
) -> Result<(), Error> {
// TODO(discord9): add a back pressure mechanism
let frontend_invoker =
self.frontend_invoker
.read()
.await
.clone()
.context(UnexpectedSnafu {
reason: "frontend invoker is not set",
})?;
for mut task in tasks {
task.start_running(self.clone(), &frontend_invoker).await?;
// TODO(discord9): save refill tasks to a map and check if it's finished when necessary
// i.e. when system table need query it's state
self.refill_tasks
.write()
.await
.insert(task.data.flow_id, task);
}
Ok(())
}
}
/// Task to refill flow with given table id and a time range
pub struct RefillTask {
data: TaskData,
state: TaskState<()>,
}
#[derive(Clone)]
struct TaskData {
flow_id: FlowId,
table_id: TableId,
table_schema: RelationDesc,
}
impl TaskData {
/// validate that incoming batch's schema is the same as table schema(by comparing types&names)
fn validate_schema(table_schema: &RelationDesc, rb: &RecordBatch) -> Result<(), Error> {
let rb_schema = &rb.schema;
ensure!(
rb_schema.column_schemas().len() == table_schema.len()?,
UnexpectedSnafu {
reason: format!(
"RecordBatch schema length does not match table schema length, {}!={}",
rb_schema.column_schemas().len(),
table_schema.len()?
)
}
);
for (i, rb_col) in rb_schema.column_schemas().iter().enumerate() {
let (rb_name, rb_ty) = (rb_col.name.as_str(), &rb_col.data_type);
let (table_name, table_ty) = (
table_schema.names[i].as_ref(),
&table_schema.typ().column_types[i].scalar_type,
);
ensure!(
Some(rb_name) == table_name.map(|c| c.as_str()),
UnexpectedSnafu {
reason: format!(
"Mismatch in column names: expected {:?}, found {}",
table_name, rb_name
)
}
);
ensure!(
rb_ty == table_ty,
UnexpectedSnafu {
reason: format!(
"Mismatch in column types for {}: expected {:?}, found {:?}",
rb_name, table_ty, rb_ty
)
}
);
}
Ok(())
}
}
/// Refill task state
enum TaskState<T> {
/// Task is not started
Prepared { sql: String },
/// Task is running
Running {
handle: JoinHandle<Result<T, Error>>,
},
/// Task is finished
Finished { res: Result<T, Error> },
}
impl<T> TaskState<T> {
fn new(sql: String) -> Self {
Self::Prepared { sql }
}
}
mod test_send {
use std::collections::BTreeMap;
use tokio::sync::RwLock;
use super::*;
fn is_send<T: Send + Sync>() {}
fn foo() {
is_send::<TaskState<()>>();
is_send::<RefillTask>();
is_send::<BTreeMap<FlowId, RefillTask>>();
is_send::<RwLock<BTreeMap<FlowId, RefillTask>>>();
}
}
impl TaskState<()> {
/// check if task is finished
async fn is_finished(&mut self) -> Result<bool, Error> {
match self {
Self::Finished { .. } => Ok(true),
Self::Running { handle } => Ok(if handle.is_finished() {
*self = Self::Finished {
res: handle.await.context(JoinTaskSnafu)?,
};
true
} else {
false
}),
_ => Ok(false),
}
}
fn start_running(
&mut self,
task_data: &TaskData,
manager: FlowStreamingEngineRef,
mut output_stream: SendableRecordBatchStream,
) -> Result<(), Error> {
let data = (*task_data).clone();
let handle: JoinHandle<Result<(), Error>> = common_runtime::spawn_global(async move {
while let Some(rb) = output_stream.next().await {
let rb = match rb {
Ok(rb) => rb,
Err(err) => Err(BoxedError::new(err)).context(ExternalSnafu)?,
};
TaskData::validate_schema(&data.table_schema, &rb)?;
// send rb into flow node
manager
.node_context
.read()
.await
.send_rb(data.table_id, rb)
.await?;
}
common_telemetry::info!(
"Refill successful for source table_id={}, flow_id={}",
data.table_id,
data.flow_id
);
Ok(())
});
*self = Self::Running { handle };
Ok(())
}
}
/// Query stream of RefillTask, simply wrap RecordBatches and RecordBatchStream and check output is not `AffectedRows`
enum QueryStream {
Batches { batches: RecordBatches },
Stream { stream: SendableRecordBatchStream },
}
impl TryFrom<common_query::Output> for QueryStream {
type Error = Error;
fn try_from(value: common_query::Output) -> Result<Self, Self::Error> {
match value.data {
common_query::OutputData::Stream(stream) => Ok(QueryStream::Stream { stream }),
common_query::OutputData::RecordBatches(batches) => {
Ok(QueryStream::Batches { batches })
}
_ => UnexpectedSnafu {
reason: format!("Unexpected output data type: {:?}", value.data),
}
.fail(),
}
}
}
impl QueryStream {
fn try_into_stream(self) -> Result<SendableRecordBatchStream, Error> {
match self {
Self::Batches { batches } => Ok(batches.as_stream()),
Self::Stream { stream } => Ok(stream),
}
}
}
impl RefillTask {
/// Query with "select * from table WHERE time >= range_start and time < range_end"
pub async fn create(
flow_id: FlowId,
table_id: TableId,
time_range: Option<(common_time::Timestamp, common_time::Timestamp)>,
time_col_name: &str,
table_src: &ManagedTableSource,
) -> Result<RefillTask, Error> {
let (table_name, table_schema) = table_src.get_table_name_schema(&table_id).await?;
let all_col_names: BTreeSet<_> = table_schema
.relation_desc
.iter_names()
.flatten()
.map(|s| s.as_str())
.collect();
if !all_col_names.contains(time_col_name) {
UnexpectedSnafu {
reason: format!(
"Can't find column {} in table {} while refill flow",
time_col_name,
table_name.join(".")
),
}
.fail()?;
}
let sql = if let Some(time_range) = time_range {
format!(
"select * from {0} where {1} >= {2} and {1} < {3}",
table_name.join("."),
time_col_name,
Value::from(time_range.0),
Value::from(time_range.1),
)
} else {
format!("select * from {0}", table_name.join("."))
};
Ok(RefillTask {
data: TaskData {
flow_id,
table_id,
table_schema: table_schema.relation_desc,
},
state: TaskState::new(sql),
})
}
/// Start running the task in background, non-blocking
pub async fn start_running(
&mut self,
manager: FlowStreamingEngineRef,
invoker: &FrontendInvoker,
) -> Result<(), Error> {
let TaskState::Prepared { sql } = &mut self.state else {
UnexpectedSnafu {
reason: "task is not prepared",
}
.fail()?
};
// we don't need information from query context in this query so a default query context is enough
let query_ctx = Arc::new(
QueryContextBuilder::default()
.current_catalog(DEFAULT_CATALOG_NAME.to_string())
.current_schema(DEFAULT_SCHEMA_NAME.to_string())
.build(),
);
let stmt_exec = invoker.statement_executor();
let stmt = QueryLanguageParser::parse_sql(sql, &query_ctx)
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
let plan = stmt_exec
.plan(&stmt, query_ctx.clone())
.await
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
let output_data = stmt_exec
.exec_plan(plan, query_ctx)
.await
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
let output_data = output_data
.map_dictionary_to_values()
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
let output_stream = QueryStream::try_from(output_data)?;
let output_stream = output_stream.try_into_stream()?;
self.state
.start_running(&self.data, manager, output_stream)?;
Ok(())
}
pub async fn is_finished(&mut self) -> Result<bool, Error> {
self.state.is_finished().await
}
}
-47
View File
@@ -1,47 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::BTreeMap;
use common_meta::key::flow::flow_state::FlowStat;
use crate::StreamingEngine;
use crate::engine::FlowStatProvider;
impl FlowStatProvider for StreamingEngine {
async fn flow_stat(&self) -> FlowStat {
let mut state_size_map = BTreeMap::new();
let mut last_exec_time_map = BTreeMap::new();
let mut start_time_map = BTreeMap::new();
for worker in self.worker_handles.iter() {
match worker.get_full_flow_stat().await {
Ok((sizes, exec_times, start_times)) => {
state_size_map.extend(sizes.into_iter().map(|(k, v)| (k as u32, v)));
last_exec_time_map.extend(exec_times.into_iter().map(|(k, v)| (k as u32, v)));
start_time_map.extend(start_times.into_iter().map(|(k, v)| (k as u32, v)));
}
Err(err) => {
common_telemetry::error!(err; "Get full flow stat error");
}
}
}
FlowStat {
state_size: state_size_map,
last_exec_time_map,
start_time_map,
}
}
}
+730
View File
@@ -0,0 +1,730 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Stateless DataFusion execution for streaming flows.
use std::collections::HashSet;
use std::sync::Arc;
use api::helper::{to_grpc_value, vectors_to_rows};
use api::v1::greptime_request::Request;
use api::v1::{RowInsertRequest, RowInsertRequests, Rows};
use common_error::ext::BoxedError;
use common_query::OutputData;
use common_recordbatch::{RecordBatch, RecordBatches, map_dictionary_to_values_data_type};
use common_time::Timestamp;
use datafusion::catalog::MemTable;
use datafusion::datasource::{TableProvider, provider_as_source, source_as_provider};
use datafusion_common::tree_node::{Transformed, TreeNode};
use datafusion_common::{Column, DFSchema, TableReference};
use datafusion_expr::logical_plan::{Distinct, Projection};
use datafusion_expr::{Expr, LogicalPlan};
use datatypes::schema::{ColumnSchema, SchemaRef};
use datatypes::value::Value;
use query::QueryEngine;
use session::context::QueryContextRef;
use snafu::{OptionExt, ResultExt, ensure};
use table::metadata::TableId;
use table::table::adapter::DfTableProviderAdapter;
use crate::TableName;
use crate::adapter::util::column_schemas_to_proto;
use crate::batching_mode::frontend_client::FrontendClient;
use crate::error::{DatafusionSnafu, Error, ExternalSnafu, InvalidQuerySnafu, UnexpectedSnafu};
use crate::repr::DiffRow;
/// The validated, immutable part of one streaming flow.
#[derive(Clone)]
pub(crate) struct StatelessFlow {
pub(crate) source_table_id: TableId,
pub(crate) source_table_name: TableName,
pub(crate) source_schema: SchemaRef,
pub(crate) source_schema_version: u32,
pub(crate) sink_table_name: TableName,
pub(crate) sink_schema: Vec<ColumnSchema>,
pub(crate) sink_primary_keys: Vec<String>,
/// The exact trailing columns resolved when the flow was created.
pub(crate) auto_columns: Vec<ColumnSchema>,
pub(crate) plan: LogicalPlan,
pub(crate) query_ctx: QueryContextRef,
pub(crate) create_args: crate::CreateFlowArgs,
}
/// Per-request input provider. It owns no catalog or storage state.
#[cfg(test)]
fn test_source_plan(table_name: TableReference, provider: Arc<dyn TableProvider>) -> LogicalPlan {
datafusion_expr::LogicalPlanBuilder::scan(table_name, provider_as_source(provider), None)
.unwrap()
.filter(datafusion_expr::col("number").gt(datafusion_expr::lit(1)))
.unwrap()
.project(vec![datafusion_expr::col("number")])
.unwrap()
.build()
.unwrap()
}
fn input_provider(batch: &RecordBatch) -> Result<Arc<dyn TableProvider>, Error> {
let arrow_batch = batch.df_record_batch().clone();
let provider = MemTable::try_new(arrow_batch.schema(), vec![vec![arrow_batch]]).context(
DatafusionSnafu {
context: "Failed to create transient flow input provider",
},
)?;
Ok(Arc::new(provider))
}
/// Adds the source timestamp to every supported plan node that has to carry it through a
/// filter or projection. The expression is appended only after the visible expressions, so the
/// sink contract remains positional.
pub(crate) fn rewrite_source_timestamp(
plan: LogicalPlan,
source_name: &TableReference,
source_timestamp_name: &str,
) -> Result<LogicalPlan, Error> {
let mut names = HashSet::new();
plan.apply(|node| {
names.extend(
node.schema()
.fields()
.iter()
.map(|field| field.name().clone()),
);
Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
})
.context(DatafusionSnafu {
context: "Failed to inspect streaming flow plan schema",
})?;
let mut hidden_name = "__flow_source_timestamp".to_string();
let mut suffix = 0;
while !names.insert(hidden_name.clone()) {
suffix += 1;
hidden_name = format!("__flow_source_timestamp_{suffix}");
}
let visible_count = plan.schema().fields().len();
let source_timestamp = Column::from_name(source_timestamp_name);
let mut plan = plan
.transform_up_with_subqueries(|node| match node {
LogicalPlan::TableScan(mut scan) => {
if scan.table_name.resolved_eq(source_name)
&& let Some(projection) = &mut scan.projection
{
let timestamp_index = scan
.source
.schema()
.index_of(source_timestamp_name)
.map_err(|_| {
datafusion::error::DataFusionError::Plan(
"Source timestamp is absent from source scan".into(),
)
})?;
if !projection.contains(&timestamp_index) {
projection.push(timestamp_index);
let schema = scan.source.schema();
scan.projected_schema = Arc::new(DFSchema::new_with_metadata(
projection
.iter()
.map(|index| {
(
Some(scan.table_name.clone()),
Arc::new(schema.field(*index).clone()),
)
})
.collect(),
schema.metadata().clone(),
)?);
}
}
Ok(Transformed::yes(LogicalPlan::TableScan(scan)))
}
LogicalPlan::Projection(mut projection) => {
let hidden_expr = if projection
.input
.schema()
.fields()
.iter()
.any(|field| field.name() == &hidden_name)
{
Expr::Column(Column::from_name(hidden_name.clone()))
} else {
Expr::Column(source_timestamp.clone())
};
projection.expr.push(hidden_expr.alias(hidden_name.clone()));
let projection = Projection::try_new(projection.expr, projection.input)?;
Ok(Transformed::yes(LogicalPlan::Projection(projection)))
}
_ => Ok(Transformed::no(node)),
})
.context(DatafusionSnafu {
context: "Failed to add source timestamp to streaming flow plan",
})?
.data;
// A plan ending at a scan or filter has no projection at which to give the carried value its
// hidden name. Add one only in that case; a projection below a filter already carries it.
if !plan
.schema()
.fields()
.iter()
.any(|field| field.name() == &hidden_name)
{
let expressions = plan
.schema()
.fields()
.iter()
.take(visible_count)
.map(|field| Expr::Column(Column::from_name(field.name())))
.chain(std::iter::once(
Expr::Column(source_timestamp).alias(hidden_name),
))
.collect::<Vec<_>>();
plan = Projection::try_new(expressions, Arc::new(plan))
.map(LogicalPlan::Projection)
.context(DatafusionSnafu {
context: "Failed to finalize source timestamp in streaming flow plan",
})?;
}
Ok(plan)
}
fn replace_source(
plan: LogicalPlan,
source_name: &TableReference,
provider: Arc<dyn TableProvider>,
) -> Result<LogicalPlan, Error> {
let mut scan_count = 0;
let mut replaced_scan_count = 0;
let plan = plan
.transform_up_with_subqueries(|node| match node {
LogicalPlan::TableScan(mut scan) => {
scan_count += 1;
if scan.table_name.resolved_eq(source_name) {
replaced_scan_count += 1;
scan.source = provider_as_source(provider.clone());
let schema = scan.source.schema();
scan.projected_schema = if let Some(projection) = &scan.projection {
Arc::new(DFSchema::new_with_metadata(
projection
.iter()
.map(|index| {
(
Some(scan.table_name.clone()),
Arc::new(schema.field(*index).clone()),
)
})
.collect(),
schema.metadata().clone(),
)?)
} else {
Arc::new(DFSchema::try_from_qualified_schema(
scan.table_name.clone(),
&schema,
)?)
};
Ok(Transformed::yes(LogicalPlan::TableScan(scan)))
} else {
Ok(Transformed::no(LogicalPlan::TableScan(scan)))
}
}
LogicalPlan::Join(_) | LogicalPlan::Aggregate(_) => {
Err(datafusion::error::DataFusionError::Plan(
"Streaming flow supports only a single projection/filter source".into(),
))
}
_ => Ok(Transformed::no(node)),
})
.context(DatafusionSnafu {
context: "Failed to substitute transient flow input provider",
})?
.data;
ensure!(
scan_count == 1 && replaced_scan_count == 1,
InvalidQuerySnafu {
reason: format!(
"Expected one source scan matching {:?}, found {scan_count} scans and {replaced_scan_count} matches",
source_name
)
}
);
Ok(plan)
}
/// Validates the deliberately small stateless streaming SQL subset.
pub(crate) fn validate_plan(plan: &LogicalPlan) -> Result<(), Error> {
let mut scans = 0;
plan.apply(|node| {
match node {
LogicalPlan::TableScan(_) => scans += 1,
LogicalPlan::Projection(_) | LogicalPlan::Filter(_) => {}
LogicalPlan::Aggregate(_) => {
return Err(datafusion::error::DataFusionError::Plan(
"Aggregation is unsupported in streaming flows. Recreate the flow to select batching mode. A source table with TTL=instant must use persisted retention first. Aggregation SQL without a time window requires EVAL INTERVAL.".into(),
));
}
// DISTINCT is evaluated against this request's transient input only. DISTINCT ON
// has ordering/selection semantics beyond the supported stateless subset.
LogicalPlan::Distinct(Distinct::All(_)) => {}
_ => {
return Err(datafusion::error::DataFusionError::Plan(
"Streaming flow supports only projection and filter over one source scan"
.into(),
));
}
}
Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
})
.context(DatafusionSnafu {
context: "Failed to validate streaming flow plan",
})?;
ensure!(
scans == 1,
InvalidQuerySnafu {
reason: format!("Expected one source scan, found {scans}")
}
);
Ok(())
}
/// Ensures the retained scan was planned against the source metadata captured for this flow.
pub(crate) fn validate_source_scan(
plan: &LogicalPlan,
source_table_id: TableId,
source_schema: &SchemaRef,
) -> Result<(), Error> {
plan.apply(|node| {
if let LogicalPlan::TableScan(scan) = node {
let provider = source_as_provider(&scan.source)?;
let provider = provider
.downcast_ref::<DfTableProviderAdapter>()
.ok_or_else(|| {
datafusion::error::DataFusionError::Plan(
"Streaming flow source scan does not use a table provider".into(),
)
})?;
let table_info = provider.table().table_info();
if table_info.ident.table_id != source_table_id
|| table_info.meta.schema.as_ref() != source_schema.as_ref()
{
return Err(datafusion::error::DataFusionError::Plan(format!(
"Streaming flow source scan does not match source table {source_table_id} schema version {}",
source_schema.version()
)));
}
}
Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
})
.context(DatafusionSnafu {
context: "Failed to validate streaming flow source scan",
})?;
Ok(())
}
/// Rejects execution when the source metadata changed after the flow plan was retained.
/// The outer adapter replans a flow when it observes a new source schema version; this guard
/// rejects a request if the version changes again before execution.
fn validate_source_schema_version(
retained_version: u32,
current_version: u32,
) -> Result<(), Error> {
ensure!(
retained_version == current_version,
InvalidQuerySnafu {
reason: format!(
"Source schema version changed from {retained_version} to {current_version}; recreate or recover the flow before writing"
)
}
);
Ok(())
}
fn synthesize_auto_values(columns: &[ColumnSchema], now: Timestamp) -> Result<Vec<Value>, Error> {
columns
.iter()
.map(|column| {
let timestamp_type = column.data_type.as_timestamp().context(InvalidQuerySnafu {
reason: format!("Auto sink column {} is not a timestamp", column.name),
})?;
let value = if column.name == crate::adapter::AUTO_CREATED_UPDATE_AT_TS_COL {
now.convert_to(timestamp_type.unit())
.context(InvalidQuerySnafu {
reason: "Current timestamp cannot be represented in sink timestamp unit",
})?
} else if column.name == crate::adapter::AUTO_CREATED_PLACEHOLDER_TS_COL {
Timestamp::new(0, timestamp_type.unit())
} else {
return InvalidQuerySnafu {
reason: format!("Unsupported auto sink column {}", column.name),
}
.fail();
};
Ok(Value::Timestamp(value))
})
.collect()
}
/// Executes one mirror write using only the supplied batch and writes its output.
pub(crate) async fn execute(
flow: &StatelessFlow,
rows: &[DiffRow],
batch_datatypes: &[datatypes::data_type::ConcreteDataType],
query_engine: &Arc<dyn QueryEngine>,
frontend_client: &Arc<FrontendClient>,
current_source_schema_version: u32,
) -> Result<usize, Error> {
validate_source_schema_version(flow.source_schema_version, current_source_schema_version)?;
let values = rows.iter().map(|(row, _, _)| row.clone()).collect();
let batch = crate::expr::Batch::try_from_rows_with_types(values, batch_datatypes)
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
let batch = RecordBatch::new(flow.source_schema.clone(), batch.batch().to_vec())
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
let provider = input_provider(&batch)?;
let source_ref = TableReference::full(
flow.source_table_name[0].clone(),
flow.source_table_name[1].clone(),
flow.source_table_name[2].clone(),
);
let plan = replace_source(flow.plan.clone(), &source_ref, provider)?;
let output = query_engine
.execute(plan, flow.query_ctx.clone())
.await
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
let batches = match output.data {
OutputData::RecordBatches(batches) => batches,
OutputData::Stream(stream) => RecordBatches::try_collect(stream)
.await
.map_err(BoxedError::new)
.context(ExternalSnafu)?,
OutputData::AffectedRows(_) => {
return UnexpectedSnafu {
reason: "Streaming flow query returned affected rows",
}
.fail();
}
};
let output_schema = batches
.schema()
.column_schemas()
.iter()
.cloned()
.map(|mut column| {
column.data_type = map_dictionary_to_values_data_type(&column.data_type);
column
})
.collect::<Vec<_>>();
crate::adapter::validate_sink_layout_with_suffix(
&output_schema,
&flow.sink_schema,
&flow.auto_columns,
)?;
let mut output_rows = Vec::new();
for batch in batches {
let vectors = datatypes::vectors::Helper::try_into_vectors(batch.columns())
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
output_rows.extend(vectors_to_rows(vectors.iter(), batch.num_rows()));
}
if output_rows.is_empty() {
return Ok(0);
}
// Auto columns are deliberately synthesized here rather than in the query plan. This keeps
// one current timestamp for the whole request and preserves the sink's timestamp precision.
let auto_values = synthesize_auto_values(&flow.auto_columns, Timestamp::current_millis())?
.into_iter()
.map(to_grpc_value)
.collect::<Vec<_>>();
for row in &mut output_rows {
row.values.extend(auto_values.iter().cloned());
}
let output_row_count = output_rows.len();
let proto_schema = column_schemas_to_proto(flow.sink_schema.clone(), &flow.sink_primary_keys)?;
let request = Request::RowInserts(RowInsertRequests {
inserts: vec![RowInsertRequest {
table_name: flow.sink_table_name[2].clone(),
rows: Some(Rows {
schema: proto_schema,
rows: output_rows,
}),
}],
});
let mut peer = None;
frontend_client
.handle_insert_once(
request,
&flow.sink_table_name[0],
&flow.sink_table_name[1],
&mut peer,
)
.await
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
Ok(output_row_count)
}
#[cfg(test)]
mod tests {
use datafusion::catalog::MemTable;
use datafusion::logical_expr::LogicalPlanBuilder;
use datatypes::data_type::ConcreteDataType;
use datatypes::schema::{ColumnSchema, Schema};
use datatypes::vectors::{Int32Vector, TimestampMillisecondVector};
use session::context::QueryContext;
use super::*;
#[test]
fn validation_accepts_distinct_over_one_source() {
let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
"number",
ConcreteDataType::int32_datatype(),
false,
)]));
let plan = LogicalPlanBuilder::scan(
TableReference::bare("source"),
provider_as_source(provider(&schema, 1)),
None,
)
.unwrap()
.project(vec![datafusion_expr::col("number")])
.unwrap()
.distinct()
.unwrap()
.build()
.unwrap();
assert!(validate_plan(&plan).is_ok());
}
#[test]
fn validation_rejects_plan_without_source_scan() {
let plan = LogicalPlan::EmptyRelation(datafusion_expr::logical_plan::EmptyRelation {
produce_one_row: false,
schema: Arc::new(DFSchema::empty()),
});
assert!(validate_plan(&plan).is_err());
}
fn provider(schema: &SchemaRef, value: i32) -> Arc<dyn TableProvider> {
let batch = RecordBatch::new(
schema.clone(),
vec![Arc::new(Int32Vector::from_slice([value])) as datatypes::prelude::VectorRef],
)
.unwrap();
let arrow = batch.df_record_batch().clone();
Arc::new(MemTable::try_new(arrow.schema(), vec![vec![arrow]]).unwrap())
}
#[test]
fn source_schema_version_must_match_retained_plan() {
assert!(validate_source_schema_version(7, 7).is_ok());
let error = validate_source_schema_version(7, 8).unwrap_err();
assert!(matches!(error, Error::InvalidQuery { reason, .. } if
reason.contains("Source schema version changed from 7 to 8")
&& reason.contains("recreate or recover")
));
}
#[test]
fn auto_values_use_the_sink_timestamp_units() {
let update_at = ColumnSchema::new(
crate::adapter::AUTO_CREATED_UPDATE_AT_TS_COL,
datatypes::data_type::ConcreteDataType::timestamp_second_datatype(),
true,
);
let placeholder = ColumnSchema::new(
crate::adapter::AUTO_CREATED_PLACEHOLDER_TS_COL,
datatypes::data_type::ConcreteDataType::timestamp_nanosecond_datatype(),
true,
);
let values = synthesize_auto_values(
&[update_at],
Timestamp::new(1_234, common_time::timestamp::TimeUnit::Millisecond),
)
.unwrap();
assert_eq!(values.len(), 1);
assert_eq!(
values[0].as_timestamp().unwrap().unit(),
common_time::timestamp::TimeUnit::Second
);
assert!(values[0].as_timestamp().unwrap().value() > 0);
let values = synthesize_auto_values(
&[
ColumnSchema::new(
crate::adapter::AUTO_CREATED_UPDATE_AT_TS_COL,
datatypes::data_type::ConcreteDataType::timestamp_microsecond_datatype(),
true,
),
placeholder,
],
Timestamp::new(1_234, common_time::timestamp::TimeUnit::Millisecond),
)
.unwrap();
assert_eq!(values[1].as_timestamp().unwrap().value(), 0);
assert_eq!(
values[1].as_timestamp().unwrap().unit(),
common_time::timestamp::TimeUnit::Nanosecond
);
}
#[test]
fn auto_values_reject_arbitrary_columns() {
let column = ColumnSchema::new(
"other",
datatypes::data_type::ConcreteDataType::timestamp_millisecond_datatype(),
true,
);
assert!(synthesize_auto_values(&[column], Timestamp::current_millis()).is_err());
}
#[test]
fn rewrite_source_timestamp_appends_collision_free_hidden_output() {
let schema = Arc::new(Schema::new(vec![
ColumnSchema::new("number", ConcreteDataType::int32_datatype(), false),
ColumnSchema::new(
"ts",
ConcreteDataType::timestamp_millisecond_datatype(),
false,
)
.with_time_index(true),
ColumnSchema::new(
"__flow_source_timestamp",
ConcreteDataType::int32_datatype(),
false,
),
]));
let batch = RecordBatch::new(
schema.clone(),
vec![
Arc::new(Int32Vector::from_slice([2])) as datatypes::prelude::VectorRef,
Arc::new(TimestampMillisecondVector::from_slice([42]))
as datatypes::prelude::VectorRef,
Arc::new(Int32Vector::from_slice([7])) as datatypes::prelude::VectorRef,
],
)
.unwrap();
let plan = datafusion_expr::LogicalPlanBuilder::scan(
TableReference::bare("source"),
provider_as_source(input_provider(&batch).unwrap()),
None,
)
.unwrap()
.filter(datafusion_expr::col("number").gt(datafusion_expr::lit(1)))
.unwrap()
.project(vec![datafusion_expr::col("number")])
.unwrap()
.build()
.unwrap();
let rewritten =
rewrite_source_timestamp(plan, &TableReference::bare("source"), "ts").unwrap();
assert_eq!(rewritten.schema().fields().len(), 2);
assert!(
rewritten
.schema()
.field(1)
.name()
.starts_with("__flow_source_timestamp")
);
}
#[tokio::test]
async fn finite_projection_filter_does_not_retain_previous_batch() {
let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
"number",
ConcreteDataType::int32_datatype(),
false,
)]));
let initial_provider = provider(&schema, 0);
let plan = test_source_plan(TableReference::bare("source"), initial_provider);
assert!(validate_plan(&plan).is_ok());
let engine = crate::test_utils::create_test_query_engine();
let run = |input: Arc<dyn TableProvider>| {
let engine = engine.clone();
let plan = plan.clone();
async move {
let plan = replace_source(plan, &TableReference::bare("source"), input).unwrap();
let output = engine.execute(plan, QueryContext::arc()).await.unwrap();
match output.data {
OutputData::Stream(stream) => RecordBatches::try_collect(stream)
.await
.unwrap()
.iter()
.map(|batch| batch.num_rows())
.sum(),
OutputData::RecordBatches(batches) => {
batches.iter().map(|batch| batch.num_rows()).sum()
}
OutputData::AffectedRows(_) => 0,
}
}
};
assert_eq!(run(provider(&schema, 1)).await, 0);
assert_eq!(run(provider(&schema, 2)).await, 1);
}
#[tokio::test]
async fn finite_distinct_collapses_duplicates_per_request() {
let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
"number",
ConcreteDataType::int32_datatype(),
false,
)]));
let input = |values: &[i32]| {
let batch = RecordBatch::new(
schema.clone(),
vec![Arc::new(Int32Vector::from_slice(values)) as datatypes::prelude::VectorRef],
)
.unwrap();
input_provider(&batch).unwrap()
};
let plan = LogicalPlanBuilder::scan(
TableReference::bare("source"),
provider_as_source(input(&[1])),
None,
)
.unwrap()
.project(vec![datafusion_expr::col("number")])
.unwrap()
.distinct()
.unwrap()
.build()
.unwrap();
assert!(validate_plan(&plan).is_ok());
let engine = crate::test_utils::create_test_query_engine();
let run = |provider: Arc<dyn TableProvider>| {
let engine = engine.clone();
let plan = plan.clone();
async move {
let plan = replace_source(plan, &TableReference::bare("source"), provider).unwrap();
let output = engine.execute(plan, QueryContext::arc()).await.unwrap();
let batches = match output.data {
OutputData::Stream(stream) => RecordBatches::try_collect(stream).await.unwrap(),
OutputData::RecordBatches(batches) => batches,
OutputData::AffectedRows(_) => panic!("unexpected affected rows"),
};
batches.iter().map(|batch| batch.num_rows()).sum::<usize>()
}
};
assert_eq!(run(input(&[1, 1, 2])).await, 2);
assert_eq!(run(input(&[1, 1])).await, 1);
}
}
File diff suppressed because it is too large Load Diff
+7 -54
View File
@@ -14,43 +14,24 @@
//! Util functions for adapter
use std::sync::Arc;
use api::helper::ColumnDataTypeWrapper;
use api::v1::column_def::options_from_column_schema;
use api::v1::{ColumnDataType, ColumnDataTypeExtension, CreateTableExpr, SemanticType};
use api::v1::{ColumnDataType, ColumnDataTypeExtension, SemanticType};
use common_error::ext::BoxedError;
use common_meta::key::table_info::TableInfoValue;
use common_meta::rpc::ddl::TriggerReason;
use datatypes::prelude::ConcreteDataType;
use datatypes::schema::{ColumnDefaultConstraint, ColumnSchema};
use itertools::Itertools;
use operator::expr_helper;
use session::context::QueryContextBuilder;
use snafu::{OptionExt, ResultExt};
use snafu::ResultExt;
use table::table_reference::TableReference;
use crate::StreamingEngine;
use crate::adapter::table_source::TableDesc;
use crate::adapter::{AUTO_CREATED_PLACEHOLDER_TS_COL, TableName, WorkerHandle};
use crate::error::{Error, ExternalSnafu, UnexpectedSnafu};
use crate::adapter::{AUTO_CREATED_PLACEHOLDER_TS_COL, TableName};
use crate::error::{Error, ExternalSnafu};
use crate::repr::{ColumnType, RelationDesc, RelationType};
impl StreamingEngine {
/// Get a worker handle for creating flow, using round robin to select a worker
pub(crate) async fn get_worker_handle_for_create_flow(&self) -> &WorkerHandle {
let use_idx = {
let mut selector = self.worker_selector.lock().await;
if *selector >= self.worker_handles.len() {
*selector = 0
};
let use_idx = *selector;
*selector += 1;
use_idx
};
// Safety: selector is always in bound
&self.worker_handles[use_idx]
}
/// Create table from given schema(will adjust to add auto column if needed), return true if table is created
pub(crate) async fn create_table_from_relation(
&self,
@@ -81,7 +62,9 @@ impl StreamingEngine {
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
self.submit_create_sink_table_ddl(create_expr).await?;
self.frontend_client
.create(create_expr, &table_name[0], &table_name[1])
.await?;
Ok(true)
}
@@ -109,36 +92,6 @@ impl StreamingEngine {
Ok(None)
}
}
/// submit a create table ddl
pub(crate) async fn submit_create_sink_table_ddl(
&self,
mut create_table: CreateTableExpr,
) -> Result<(), Error> {
let stmt_exec = {
self.frontend_invoker
.read()
.await
.as_ref()
.map(|f| f.statement_executor())
}
.context(UnexpectedSnafu {
reason: "Failed to get statement executor",
})?;
let ctx = Arc::new(
QueryContextBuilder::default()
.current_catalog(create_table.catalog_name.clone())
.current_schema(create_table.schema_name.clone())
.build(),
);
stmt_exec
.create_table_inner(&mut create_table, None, ctx, TriggerReason::AutoCreate)
.await
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
Ok(())
}
}
pub fn table_info_value_to_relation_desc(
-606
View File
@@ -1,606 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! For single-thread flow worker
use std::collections::{BTreeMap, VecDeque};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use common_telemetry::info;
use dfir_rs::scheduled::graph::Dfir;
use enum_as_inner::EnumAsInner;
use snafu::ensure;
use tokio::sync::{Mutex, broadcast, mpsc, oneshot};
use crate::adapter::FlowId;
use crate::compute::{Context, DataflowState, ErrCollector};
use crate::error::{Error, FlowAlreadyExistSnafu, InternalSnafu, UnexpectedSnafu};
use crate::expr::{Batch, GlobalId};
use crate::plan::TypedPlan;
use crate::repr::{self, DiffRow};
pub type SharedBuf = Arc<Mutex<VecDeque<DiffRow>>>;
type ReqId = usize;
/// Create both worker(`!Send`) and worker handle(`Send + Sync`)
pub fn create_worker<'a>() -> (WorkerHandle, Worker<'a>) {
let (itc_client, itc_server) = create_inter_thread_call();
let worker_handle = WorkerHandle {
itc_client,
shutdown: AtomicBool::new(false),
};
let worker = Worker {
task_states: BTreeMap::new(),
itc_server: Arc::new(Mutex::new(itc_server)),
};
(worker_handle, worker)
}
/// ActiveDataflowState is a wrapper around `Dfir` and `DataflowState`
pub(crate) struct ActiveDataflowState<'subgraph> {
df: Dfir<'subgraph>,
state: DataflowState,
err_collector: ErrCollector,
}
impl std::fmt::Debug for ActiveDataflowState<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ActiveDataflowState")
.field("df", &"<Dfir>")
.field("state", &self.state)
.field("err_collector", &self.err_collector)
.finish()
}
}
impl Default for ActiveDataflowState<'_> {
fn default() -> Self {
ActiveDataflowState {
df: Dfir::new(),
state: DataflowState::default(),
err_collector: ErrCollector::default(),
}
}
}
impl<'subgraph> ActiveDataflowState<'subgraph> {
/// Create a new render context, assigned with given global id
pub fn new_ctx<'ctx>(&'ctx mut self, global_id: GlobalId) -> Context<'ctx, 'subgraph>
where
'subgraph: 'ctx,
{
Context {
id: global_id,
df: &mut self.df,
compute_state: &mut self.state,
err_collector: self.err_collector.clone(),
input_collection: Default::default(),
local_scope: Default::default(),
input_collection_batch: Default::default(),
local_scope_batch: Default::default(),
}
}
pub fn set_current_ts(&mut self, ts: repr::Timestamp) {
self.state.set_current_ts(ts);
}
pub fn set_last_exec_time(&mut self, ts: repr::Timestamp) {
self.state.set_last_exec_time(ts);
}
/// Run all available subgraph
///
/// return true if any subgraph actually executed
pub fn run_available(&mut self) -> bool {
self.state.run_available_with_schedule(&mut self.df)
}
}
#[derive(Debug)]
pub struct WorkerHandle {
itc_client: InterThreadCallClient,
shutdown: AtomicBool,
}
impl WorkerHandle {
/// create task, return task id
pub async fn create_flow(&self, create_reqs: Request) -> Result<Option<FlowId>, Error> {
ensure!(
matches!(create_reqs, Request::Create { .. }),
InternalSnafu {
reason: format!(
"Flow Node/Worker itc failed, expect Request::Create, found {create_reqs:?}"
),
}
);
let ret = self.itc_client.call_with_resp(create_reqs).await?;
ret.into_create().map_err(|ret| {
InternalSnafu {
reason: format!(
"Flow Node/Worker itc failed, expect Response::Create, found {ret:?}"
),
}
.build()
})?
}
/// remove task, return task id
pub async fn remove_flow(&self, flow_id: FlowId) -> Result<bool, Error> {
let req = Request::Remove { flow_id };
let ret = self.itc_client.call_with_resp(req).await?;
ret.into_remove().map_err(|ret| {
InternalSnafu {
reason: format!("Flow Node/Worker failed, expect Response::Remove, found {ret:?}"),
}
.build()
})
}
/// trigger running the worker, will not block, and will run the worker parallelly
///
/// will set the current timestamp to `now` for all dataflows before running them
///
/// `blocking` indicate whether it will wait til all dataflows are finished computing if true or
/// just start computing and return immediately if false
///
/// the returned error is unrecoverable, and the worker should be shutdown/rebooted
pub async fn run_available(&self, now: repr::Timestamp, blocking: bool) -> Result<(), Error> {
common_telemetry::trace!("Running available with blocking={}", blocking);
if blocking {
let resp = self
.itc_client
.call_with_resp(Request::RunAvail { now, blocking })
.await?;
common_telemetry::trace!("Running available with response={:?}", resp);
Ok(())
} else {
self.itc_client
.call_no_resp(Request::RunAvail { now, blocking })
}
}
pub async fn contains_flow(&self, flow_id: FlowId) -> Result<bool, Error> {
let req = Request::ContainTask { flow_id };
let ret = self.itc_client.call_with_resp(req).await?;
ret.into_contain_task().map_err(|ret| {
InternalSnafu {
reason: format!(
"Flow Node/Worker itc failed, expect Response::ContainTask, found {ret:?}"
),
}
.build()
})
}
/// shutdown the worker
pub fn shutdown(&self) -> Result<(), Error> {
if !self.shutdown.fetch_or(true, Ordering::SeqCst) {
self.itc_client.call_no_resp(Request::Shutdown)
} else {
UnexpectedSnafu {
reason: "Worker already shutdown",
}
.fail()
}
}
pub async fn get_full_flow_stat(
&self,
) -> Result<
(
BTreeMap<FlowId, usize>,
BTreeMap<FlowId, i64>,
BTreeMap<FlowId, i64>,
),
Error,
> {
let ret = self
.itc_client
.call_with_resp(Request::QueryFullFlowStat)
.await?;
ret.into_query_full_flow_stat().map_err(|ret| {
InternalSnafu {
reason: format!(
"Flow Node/Worker get_full_flow_stat failed, expected Response::QueryFullFlowStat, found {ret:?}"
),
}
.build()
})
}
}
impl Drop for WorkerHandle {
fn drop(&mut self) {
if let Err(ret) = self.shutdown() {
common_telemetry::error!(
ret;
"While dropping Worker Handle, failed to shutdown worker, worker might be in inconsistent state."
);
} else {
info!("Flow Worker shutdown due to Worker Handle dropped.")
}
}
}
/// The actual worker that does the work and contain active state
#[derive(Debug)]
pub struct Worker<'subgraph> {
/// Task states
pub(crate) task_states: BTreeMap<FlowId, ActiveDataflowState<'subgraph>>,
itc_server: Arc<Mutex<InterThreadCallServer>>,
}
impl<'s> Worker<'s> {
#[allow(clippy::too_many_arguments)]
pub fn create_flow(
&mut self,
flow_id: FlowId,
plan: TypedPlan,
sink_id: GlobalId,
sink_sender: mpsc::UnboundedSender<Batch>,
source_ids: &[GlobalId],
src_recvs: Vec<broadcast::Receiver<Batch>>,
// TODO(discord9): set expire duration for all arrangement and compare to sys timestamp instead
expire_after: Option<repr::Duration>,
or_replace: bool,
create_if_not_exists: bool,
err_collector: ErrCollector,
) -> Result<Option<FlowId>, Error> {
let already_exist = self.task_states.contains_key(&flow_id);
match (create_if_not_exists, or_replace, already_exist) {
// if replace, ignore that old flow exists
(_, true, true) => {
info!("Replacing flow with id={}", flow_id);
}
(false, false, true) => FlowAlreadyExistSnafu { id: flow_id }.fail()?,
// already exists, and not replace, return None
(true, false, true) => {
info!("Flow with id={} already exists, do nothing", flow_id);
return Ok(None);
}
// continue as normal
(_, _, false) => (),
}
let mut cur_task_state = ActiveDataflowState::<'s> {
err_collector,
..Default::default()
};
cur_task_state.state.set_expire_after(expire_after);
{
let mut ctx = cur_task_state.new_ctx(sink_id);
for (source_id, src_recv) in source_ids.iter().zip(src_recvs) {
let bundle = ctx.render_source_batch(src_recv)?;
ctx.insert_global_batch(*source_id, bundle);
}
let rendered = ctx.render_plan_batch(plan)?;
ctx.render_unbounded_sink_batch(rendered, sink_sender);
}
self.task_states.insert(flow_id, cur_task_state);
Ok(Some(flow_id))
}
/// remove task, return true if a task is removed
pub fn remove_flow(&mut self, flow_id: FlowId) -> bool {
self.task_states.remove(&flow_id).is_some()
}
/// Run the worker, blocking, until shutdown signal is received
pub fn run(&mut self) {
loop {
let (req, ret_tx) = if let Some(ret) = self.itc_server.blocking_lock().blocking_recv() {
ret
} else {
common_telemetry::error!(
"Worker's itc server has been closed unexpectedly, shutting down worker now."
);
break;
};
let ret = self.handle_req(req);
match (ret, ret_tx) {
(Ok(Some(resp)), Some(ret_tx)) => {
if let Err(err) = ret_tx.send(resp) {
common_telemetry::error!(
err;
"Result receiver is dropped, can't send result"
);
};
}
(Ok(None), None) => continue,
(Ok(Some(resp)), None) => {
common_telemetry::error!(
"Expect no result for current request, but found {resp:?}"
)
}
(Ok(None), Some(_)) => {
common_telemetry::error!("Expect result for current request, but found nothing")
}
(Err(()), _) => {
break;
}
}
}
}
/// run with tick acquired from tick manager(usually means system time)
/// TODO(discord9): better tick management
pub fn run_tick(&mut self, now: repr::Timestamp) {
for (_flow_id, task_state) in self.task_states.iter_mut() {
task_state.set_current_ts(now);
task_state.set_last_exec_time(now);
task_state.run_available();
}
}
/// handle request, return response if any, Err if receive shutdown signal
///
/// return `Err(())` if receive shutdown request
fn handle_req(&mut self, req: Request) -> Result<Option<Response>, ()> {
let ret = match req {
Request::Create {
flow_id,
plan,
sink_id,
sink_sender,
source_ids,
src_recvs,
expire_after,
or_replace,
create_if_not_exists,
err_collector,
} => {
let task_create_result = self.create_flow(
flow_id,
plan,
sink_id,
sink_sender,
&source_ids,
src_recvs,
expire_after,
or_replace,
create_if_not_exists,
err_collector,
);
Some(Response::Create {
result: task_create_result,
})
}
Request::Remove { flow_id } => {
let ret = self.remove_flow(flow_id);
Some(Response::Remove { result: ret })
}
Request::RunAvail { now, blocking } => {
self.run_tick(now);
if blocking {
Some(Response::RunAvail)
} else {
None
}
}
Request::ContainTask { flow_id } => {
let ret = self.task_states.contains_key(&flow_id);
Some(Response::ContainTask { result: ret })
}
Request::Shutdown => return Err(()),
Request::QueryFullFlowStat => {
let mut state_size = BTreeMap::new();
let mut last_exec_time_map = BTreeMap::new();
let mut start_time_map = BTreeMap::new();
for (flow_id, task_state) in self.task_states.iter() {
state_size.insert(*flow_id, task_state.state.get_state_size());
if let Some(t) = task_state.state.last_exec_time() {
last_exec_time_map.insert(*flow_id, t);
}
if let Some(t) = task_state.state.start_time() {
start_time_map.insert(*flow_id, t);
}
}
Some(Response::QueryFullFlowStat {
state_size,
last_exec_time_map,
start_time_map,
})
}
};
Ok(ret)
}
}
#[derive(Debug, EnumAsInner)]
pub enum Request {
Create {
flow_id: FlowId,
plan: TypedPlan,
sink_id: GlobalId,
sink_sender: mpsc::UnboundedSender<Batch>,
source_ids: Vec<GlobalId>,
src_recvs: Vec<broadcast::Receiver<Batch>>,
expire_after: Option<repr::Duration>,
or_replace: bool,
create_if_not_exists: bool,
err_collector: ErrCollector,
},
Remove {
flow_id: FlowId,
},
/// Trigger the worker to run, useful after input buffer is full
RunAvail {
now: repr::Timestamp,
blocking: bool,
},
ContainTask {
flow_id: FlowId,
},
Shutdown,
QueryFullFlowStat,
}
#[derive(Debug, EnumAsInner)]
enum Response {
Create {
result: Result<Option<FlowId>, Error>,
// TODO(discord9): add flow err_collector
},
Remove {
result: bool,
},
ContainTask {
result: bool,
},
RunAvail,
QueryFullFlowStat {
state_size: BTreeMap<FlowId, usize>,
last_exec_time_map: BTreeMap<FlowId, i64>,
start_time_map: BTreeMap<FlowId, i64>,
},
}
fn create_inter_thread_call() -> (InterThreadCallClient, InterThreadCallServer) {
let (arg_send, arg_recv) = mpsc::unbounded_channel();
let client = InterThreadCallClient {
arg_sender: arg_send,
};
let server = InterThreadCallServer { arg_recv };
(client, server)
}
#[derive(Debug)]
struct InterThreadCallClient {
arg_sender: mpsc::UnboundedSender<(Request, Option<oneshot::Sender<Response>>)>,
}
impl InterThreadCallClient {
/// call without response
fn call_no_resp(&self, req: Request) -> Result<(), Error> {
self.arg_sender.send((req, None)).map_err(from_send_error)
}
/// call with response
async fn call_with_resp(&self, req: Request) -> Result<Response, Error> {
let (tx, rx) = oneshot::channel();
self.arg_sender
.send((req, Some(tx)))
.map_err(from_send_error)?;
rx.await.map_err(|_| {
InternalSnafu {
reason: "Sender is dropped",
}
.build()
})
}
}
#[derive(Debug)]
struct InterThreadCallServer {
pub arg_recv: mpsc::UnboundedReceiver<(Request, Option<oneshot::Sender<Response>>)>,
}
impl InterThreadCallServer {
pub async fn recv(&mut self) -> Option<(Request, Option<oneshot::Sender<Response>>)> {
self.arg_recv.recv().await
}
pub fn blocking_recv(&mut self) -> Option<(Request, Option<oneshot::Sender<Response>>)> {
self.arg_recv.blocking_recv()
}
}
fn from_send_error<T>(err: mpsc::error::SendError<T>) -> Error {
InternalSnafu {
// this `err` will simply display `channel closed`
reason: format!(
"Worker's receiver channel have been closed unexpected: {}",
err
),
}
.build()
}
#[cfg(test)]
mod test {
use tokio::sync::oneshot;
use super::*;
use crate::expr::Id;
use crate::plan::Plan;
use crate::repr::RelationType;
#[test]
fn drop_handle() {
let (tx, rx) = oneshot::channel();
let worker_thread_handle = std::thread::spawn(move || {
let (handle, mut worker) = create_worker();
tx.send(handle).unwrap();
worker.run();
});
let handle = rx.blocking_recv().unwrap();
drop(handle);
worker_thread_handle.join().unwrap();
}
#[tokio::test]
pub async fn test_simple_get_with_worker_and_handle() {
let (tx, rx) = oneshot::channel();
let worker_thread_handle = std::thread::spawn(move || {
let (handle, mut worker) = create_worker();
tx.send(handle).unwrap();
worker.run();
});
let handle = rx.await.unwrap();
let src_ids = vec![GlobalId::User(1)];
let (tx, rx) = broadcast::channel::<Batch>(1024);
let (sink_tx, mut sink_rx) = mpsc::unbounded_channel::<Batch>();
let (flow_id, plan) = (
1,
TypedPlan {
plan: Plan::Get {
id: Id::Global(GlobalId::User(1)),
},
schema: RelationType::new(vec![]).into_unnamed(),
},
);
let create_reqs = Request::Create {
flow_id,
plan,
sink_id: GlobalId::User(1),
sink_sender: sink_tx,
source_ids: src_ids,
src_recvs: vec![rx],
expire_after: None,
or_replace: false,
create_if_not_exists: true,
err_collector: ErrCollector::default(),
};
assert_eq!(
handle.create_flow(create_reqs).await.unwrap(),
Some(flow_id)
);
tx.send(Batch::empty()).unwrap();
handle.run_available(0, true).await.unwrap();
let (state_size, _, _) = handle.get_full_flow_stat().await.unwrap();
assert_eq!(state_size.len(), 1);
assert_eq!(sink_rx.recv().await.unwrap(), Batch::empty());
drop(handle);
worker_thread_handle.join().unwrap();
}
}
@@ -555,6 +555,81 @@ impl FrontendClient {
}
}
/// Handle an insert request with one attempt.
///
/// Unlike [`Self::handle`], this does not use the batching retry policy. It
/// is intended for stateless streaming sinks, where retrying an insert can
/// duplicate rows.
pub(crate) async fn handle_insert_once(
&self,
req: api::v1::greptime_request::Request,
catalog: &str,
schema: &str,
peer_desc: &mut Option<PeerDesc>,
) -> Result<u32, Error> {
match self {
FrontendClient::Distributed { .. } => {
let db = self.get_random_active_frontend(catalog, schema).await?;
*peer_desc = Some(PeerDesc::Dist {
peer: db.peer.clone(),
});
db.database
.handle(req.clone())
.await
.with_context(|_| InvalidRequestSnafu {
context: format!("Failed to handle request at {:?}: {:?}", db.peer, req),
})
}
FrontendClient::Standalone {
database_client,
query,
} => {
let ctx = QueryContextBuilder::default()
.current_catalog(catalog.to_string())
.current_schema(schema.to_string())
.extensions(HashMap::from([(
QUERY_PARALLELISM_HINT.to_string(),
query.parallelism.to_string(),
)]))
.build();
let ctx = Arc::new(ctx);
let database_client = {
database_client
.handler
.lock()
.unwrap()
.as_ref()
.context(UnexpectedSnafu {
reason: "Standalone's frontend instance is not set",
})?
.upgrade()
.context(UnexpectedSnafu {
reason: "Failed to upgrade database client",
})?
};
let resp: common_query::Output = database_client
.do_query(req, ctx)
.await
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
match resp.data {
common_query::OutputData::AffectedRows(rows) => rows.try_into().map_err(|_| {
UnexpectedSnafu {
reason: format!("Failed to convert rows to u32: {}", rows),
}
.build()
}),
_ => UnexpectedSnafu {
reason: "Unexpected output data",
}
.fail(),
}
}
}
}
/// Handle a request to frontend
pub(crate) async fn handle(
&self,
@@ -700,6 +775,7 @@ impl std::fmt::Display for PeerDesc {
#[cfg(test)]
mod tests {
use std::pin::Pin;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::{Context, Poll};
use std::time::Duration;
@@ -773,6 +849,11 @@ mod tests {
#[derive(Debug)]
struct MetricsHandler;
#[derive(Debug)]
struct InsertOnceHandler {
calls: Arc<AtomicUsize>,
}
#[derive(Debug)]
struct ExtensionAwareHandler;
@@ -808,6 +889,18 @@ mod tests {
}
}
#[async_trait::async_trait]
impl GrpcQueryHandlerWithBoxedError for InsertOnceHandler {
async fn do_query(
&self,
_query: Request,
_ctx: QueryContextRef,
) -> std::result::Result<Output, BoxedError> {
self.calls.fetch_add(1, Ordering::SeqCst);
Ok(Output::new_with_affected_rows(1))
}
}
#[async_trait::async_trait]
impl GrpcQueryHandlerWithBoxedError for MetricsHandler {
async fn do_query(
@@ -999,6 +1092,31 @@ mod tests {
);
}
#[tokio::test]
async fn test_handle_insert_once_calls_standalone_handler_once() {
let calls = Arc::new(AtomicUsize::new(0));
let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(InsertOnceHandler {
calls: calls.clone(),
});
let client =
FrontendClient::from_grpc_handler(Arc::downgrade(&handler), QueryOptions::default());
let mut peer_desc = None;
let affected_rows = client
.handle_insert_once(
Request::RowInserts(api::v1::RowInsertRequests { inserts: vec![] }),
"greptime",
"public",
&mut peer_desc,
)
.await
.unwrap();
assert_eq!(affected_rows, 1);
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert!(peer_desc.is_none());
}
#[tokio::test]
async fn test_query_with_terminal_metrics_tracks_watermark_in_standalone_mode() {
let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(MetricsHandler);
+4 -4
View File
@@ -57,7 +57,6 @@ use crate::error::{
ArrowSnafu, DatafusionSnafu, DatatypesSnafu, ExternalSnafu, PlanSnafu, TimeSnafu,
UnexpectedSnafu,
};
use crate::expr::error::DataTypeSnafu;
/// Represents a test timestamp in seconds since the Unix epoch.
const DEFAULT_TEST_TIMESTAMP: Timestamp = Timestamp::new_second(17_0000_0000);
@@ -292,9 +291,10 @@ impl TimeWindowExpr {
let mut vector = cdt.create_mutable_vector(rows.rows.len());
for row in rows.rows {
let value = pb_value_to_value_ref(&row.values[ts_col_index], None);
vector.try_push_value_ref(&value).context(DataTypeSnafu {
msg: "Failed to convert rows to columns",
})?;
vector
.try_push_value_ref(&value)
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
}
let vector = vector.to_vector();
-23
View File
@@ -1,23 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Build and Compute the dataflow
mod render;
mod state;
mod types;
pub(crate) use render::Context;
pub(crate) use state::DataflowState;
pub(crate) use types::ErrCollector;
-527
View File
@@ -1,527 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! In this file, `render` means convert a static `Plan` into a Executable Dataflow
//!
//! And the [`Context`] is the environment for the render process, it contains all the necessary information for the render process
use std::collections::BTreeMap;
use dfir_rs::scheduled::graph::Dfir;
use dfir_rs::scheduled::graph_ext::GraphExt;
use dfir_rs::scheduled::port::{PortCtx, SEND};
use itertools::Itertools;
use snafu::OptionExt;
use crate::compute::state::{DataflowState, Scheduler};
use crate::compute::types::{Collection, CollectionBundle, ErrCollector, Toff};
use crate::error::{Error, InvalidQuerySnafu, NotImplementedSnafu};
use crate::expr::{self, Batch, GlobalId, LocalId};
use crate::plan::{Plan, TypedPlan};
use crate::repr::{self, DiffRow, RelationType};
mod map;
mod reduce;
mod src_sink;
/// The Context for build a Operator with id of `GlobalId`
pub struct Context<'referred, 'df> {
pub id: GlobalId,
pub df: &'referred mut Dfir<'df>,
pub compute_state: &'referred mut DataflowState,
/// a list of all collections being used in the operator
///
/// TODO(discord9): remove extra clone by counting usage and remove it on last usage?
pub input_collection: BTreeMap<GlobalId, CollectionBundle>,
/// used by `Get`/`Let` Plan for getting/setting local variables
///
/// TODO(discord9): consider if use Vec<(LocalId, CollectionBundle)> instead
pub local_scope: Vec<BTreeMap<LocalId, CollectionBundle>>,
/// a list of all collections being used in the operator
///
/// TODO(discord9): remove extra clone by counting usage and remove it on last usage?
pub input_collection_batch: BTreeMap<GlobalId, CollectionBundle<Batch>>,
/// used by `Get`/`Let` Plan for getting/setting local variables
///
/// TODO(discord9): consider if use Vec<(LocalId, CollectionBundle)> instead
pub local_scope_batch: Vec<BTreeMap<LocalId, CollectionBundle<Batch>>>,
// Collect all errors in this operator's evaluation
pub err_collector: ErrCollector,
}
impl Drop for Context<'_, '_> {
fn drop(&mut self) {
for bundle in std::mem::take(&mut self.input_collection)
.into_values()
.chain(
std::mem::take(&mut self.local_scope)
.into_iter()
.flat_map(|v| v.into_iter())
.map(|(_k, v)| v),
)
{
bundle.collection.into_inner().drop(self.df);
drop(bundle.arranged);
}
for bundle in std::mem::take(&mut self.input_collection_batch)
.into_values()
.chain(
std::mem::take(&mut self.local_scope_batch)
.into_iter()
.flat_map(|v| v.into_iter())
.map(|(_k, v)| v),
)
{
bundle.collection.into_inner().drop(self.df);
drop(bundle.arranged);
}
// The automatically generated "drop glue" which recursively calls the destructors of all the fields (including the now empty `input_collection`)
}
}
impl Context<'_, '_> {
pub fn insert_global(&mut self, id: GlobalId, collection: CollectionBundle) {
self.input_collection.insert(id, collection);
}
pub fn insert_local(&mut self, id: LocalId, collection: CollectionBundle) {
if let Some(last) = self.local_scope.last_mut() {
last.insert(id, collection);
} else {
let first = BTreeMap::from([(id, collection)]);
self.local_scope.push(first);
}
}
pub fn insert_global_batch(&mut self, id: GlobalId, collection: CollectionBundle<Batch>) {
self.input_collection_batch.insert(id, collection);
}
pub fn insert_local_batch(&mut self, id: LocalId, collection: CollectionBundle<Batch>) {
if let Some(last) = self.local_scope_batch.last_mut() {
last.insert(id, collection);
} else {
let first = BTreeMap::from([(id, collection)]);
self.local_scope_batch.push(first);
}
}
}
impl Context<'_, '_> {
/// Like `render_plan` but in Batch Mode
pub fn render_plan_batch(&mut self, plan: TypedPlan) -> Result<CollectionBundle<Batch>, Error> {
match plan.plan {
Plan::Constant { rows } => Ok(self.render_constant_batch(rows, &plan.schema.typ)),
Plan::Get { id } => self.get_batch_by_id(id),
Plan::Let { id, value, body } => self.eval_batch_let(id, value, body),
Plan::Mfp { input, mfp } => self.render_mfp_batch(input, mfp, &plan.schema.typ),
Plan::Reduce {
input,
key_val_plan,
reduce_plan,
} => self.render_reduce_batch(input, &key_val_plan, &reduce_plan, &plan.schema.typ),
Plan::Join { .. } => NotImplementedSnafu {
reason: "Join is still WIP",
}
.fail(),
Plan::Union { .. } => NotImplementedSnafu {
reason: "Union is still WIP",
}
.fail(),
}
}
/// Interpret plan to dataflow and prepare them for execution
///
/// return the output handler of this plan
pub fn render_plan(&mut self, plan: TypedPlan) -> Result<CollectionBundle, Error> {
match plan.plan {
Plan::Constant { rows } => Ok(self.render_constant(rows)),
Plan::Get { id } => self.get_by_id(id),
Plan::Let { id, value, body } => self.eval_let(id, value, body),
Plan::Mfp { input, mfp } => self.render_mfp(input, mfp),
Plan::Reduce {
input,
key_val_plan,
reduce_plan,
} => self.render_reduce(input, key_val_plan, reduce_plan, plan.schema.typ),
Plan::Join { .. } => NotImplementedSnafu {
reason: "Join is still WIP",
}
.fail(),
Plan::Union { .. } => NotImplementedSnafu {
reason: "Union is still WIP",
}
.fail(),
}
}
/// render Constant, take all rows that have a timestamp not greater than the current time
/// This function is primarily used for testing
/// Always assume input is sorted by timestamp
pub fn render_constant_batch(
&mut self,
rows: Vec<DiffRow>,
output_type: &RelationType,
) -> CollectionBundle<Batch> {
let (send_port, recv_port) = self.df.make_edge::<_, Toff<Batch>>("constant_batch");
let mut per_time: BTreeMap<repr::Timestamp, Vec<DiffRow>> = Default::default();
for (key, group) in &rows.into_iter().chunk_by(|(_row, ts, _diff)| *ts) {
per_time.entry(key).or_default().extend(group);
}
let now = self.compute_state.current_time_ref();
// TODO(discord9): better way to schedule future run
let scheduler = self.compute_state.get_scheduler();
let scheduler_inner = scheduler.clone();
let err_collector = self.err_collector.clone();
let output_type = output_type.clone();
let subgraph_id =
self.df
.add_subgraph_source("ConstantBatch", send_port, move |_ctx, send_port| {
// find the first timestamp that is greater than now
// use filter_map
let mut after = per_time.split_off(&(*now.borrow() + 1));
// swap
std::mem::swap(&mut per_time, &mut after);
let not_great_than_now = after;
not_great_than_now.into_iter().for_each(|(_ts, rows)| {
err_collector.run(|| {
let rows = rows.into_iter().map(|(row, _ts, _diff)| row).collect();
let batch = Batch::try_from_rows_with_types(
rows,
&output_type
.column_types
.iter()
.map(|ty| ty.scalar_type().clone())
.collect_vec(),
)?;
send_port.give(vec![batch]);
Ok(())
});
});
// schedule the next run
if let Some(next_run_time) = per_time.keys().next().copied() {
scheduler_inner.schedule_at(next_run_time);
}
});
scheduler.set_cur_subgraph(subgraph_id);
CollectionBundle::from_collection(Collection::from_port(recv_port))
}
/// render Constant, take all rows that have a timestamp not greater than the current time
///
/// Always assume input is sorted by timestamp
pub fn render_constant(&mut self, rows: Vec<DiffRow>) -> CollectionBundle {
let (send_port, recv_port) = self.df.make_edge::<_, Toff>("constant");
let mut per_time: BTreeMap<repr::Timestamp, Vec<DiffRow>> = Default::default();
for (key, group) in &rows.into_iter().chunk_by(|(_row, ts, _diff)| *ts) {
per_time.entry(key).or_default().extend(group);
}
let now = self.compute_state.current_time_ref();
// TODO(discord9): better way to schedule future run
let scheduler = self.compute_state.get_scheduler();
let scheduler_inner = scheduler.clone();
let subgraph_id =
self.df
.add_subgraph_source("Constant", send_port, move |_ctx, send_port| {
// find the first timestamp that is greater than now
// use filter_map
let mut after = per_time.split_off(&(*now.borrow() + 1));
// swap
std::mem::swap(&mut per_time, &mut after);
let not_great_than_now = after;
not_great_than_now.into_iter().for_each(|(_ts, rows)| {
send_port.give(rows);
});
// schedule the next run
if let Some(next_run_time) = per_time.keys().next().copied() {
scheduler_inner.schedule_at(next_run_time);
}
});
scheduler.set_cur_subgraph(subgraph_id);
CollectionBundle::from_collection(Collection::from_port(recv_port))
}
pub fn get_batch_by_id(&mut self, id: expr::Id) -> Result<CollectionBundle<Batch>, Error> {
let ret = match id {
expr::Id::Local(local) => {
let bundle = self
.local_scope_batch
.iter()
.rev()
.find_map(|scope| scope.get(&local))
.with_context(|| InvalidQuerySnafu {
reason: format!("Local variable {:?} not found", local),
})?;
bundle.clone(self.df)
}
expr::Id::Global(id) => {
let bundle =
self.input_collection_batch
.get(&id)
.with_context(|| InvalidQuerySnafu {
reason: format!("Collection {:?} not found", id),
})?;
bundle.clone(self.df)
}
};
Ok(ret)
}
pub fn get_by_id(&mut self, id: expr::Id) -> Result<CollectionBundle, Error> {
let ret = match id {
expr::Id::Local(local) => {
let bundle = self
.local_scope
.iter()
.rev()
.find_map(|scope| scope.get(&local))
.with_context(|| InvalidQuerySnafu {
reason: format!("Local variable {:?} not found", local),
})?;
bundle.clone(self.df)
}
expr::Id::Global(id) => {
let bundle = self
.input_collection
.get(&id)
.with_context(|| InvalidQuerySnafu {
reason: format!("Collection {:?} not found", id),
})?;
bundle.clone(self.df)
}
};
Ok(ret)
}
/// Eval `Let` operator, useful for assigning a value to a local variable
pub fn eval_batch_let(
&mut self,
id: LocalId,
value: Box<TypedPlan>,
body: Box<TypedPlan>,
) -> Result<CollectionBundle<Batch>, Error> {
let value = self.render_plan_batch(*value)?;
self.local_scope_batch.push(Default::default());
self.insert_local_batch(id, value);
let ret = self.render_plan_batch(*body)?;
Ok(ret)
}
/// Eval `Let` operator, useful for assigning a value to a local variable
pub fn eval_let(
&mut self,
id: LocalId,
value: Box<TypedPlan>,
body: Box<TypedPlan>,
) -> Result<CollectionBundle, Error> {
let value = self.render_plan(*value)?;
self.local_scope.push(Default::default());
self.insert_local(id, value);
let ret = self.render_plan(*body)?;
Ok(ret)
}
}
/// The Common argument for all `Subgraph` in the render process
struct SubgraphArg<'a, T = Toff> {
now: repr::Timestamp,
err_collector: &'a ErrCollector,
scheduler: &'a Scheduler,
send: &'a PortCtx<SEND, T>,
}
#[cfg(test)]
mod test {
use std::cell::RefCell;
use std::rc::Rc;
use dfir_rs::scheduled::graph::Dfir;
use dfir_rs::scheduled::graph_ext::GraphExt;
use dfir_rs::scheduled::handoff::VecHandoff;
use pretty_assertions::assert_eq;
use super::*;
use crate::repr::Row;
pub fn run_and_check(
state: &mut DataflowState,
df: &mut Dfir,
time_range: std::ops::Range<i64>,
expected: BTreeMap<i64, Vec<DiffRow>>,
output: Rc<RefCell<Vec<DiffRow>>>,
) {
for now in time_range {
state.set_current_ts(now);
state.run_available_with_schedule(df);
if !state.get_err_collector().is_empty() {
panic!(
"Errors occur: {:?}",
state.get_err_collector().get_all_blocking()
)
}
assert!(state.get_err_collector().is_empty());
if let Some(expected) = expected.get(&now) {
assert_eq!(*output.borrow(), *expected, "at ts={}", now);
} else {
assert_eq!(*output.borrow(), vec![], "at ts={}", now);
};
output.borrow_mut().clear();
}
}
pub fn get_output_handle(
ctx: &mut Context,
mut bundle: CollectionBundle,
) -> Rc<RefCell<Vec<DiffRow>>> {
let collection = bundle.collection;
let _arranged = bundle.arranged.pop_first().unwrap().1;
let output = Rc::new(RefCell::new(vec![]));
let output_inner = output.clone();
let _subgraph = ctx.df.add_subgraph_sink(
"test_render_constant",
collection.into_inner(),
move |_ctx, recv| {
let data = recv.take_inner();
let res = data.into_iter().flat_map(|v| v.into_iter()).collect_vec();
output_inner.borrow_mut().clear();
output_inner.borrow_mut().extend(res);
},
);
output
}
pub fn harness_test_ctx<'r, 'h>(
df: &'r mut Dfir<'h>,
state: &'r mut DataflowState,
) -> Context<'r, 'h> {
let err_collector = state.get_err_collector();
Context {
id: GlobalId::User(0),
df,
compute_state: state,
input_collection: BTreeMap::new(),
local_scope: Default::default(),
input_collection_batch: BTreeMap::new(),
local_scope_batch: Default::default(),
err_collector,
}
}
/// test if constant operator works properly
/// that is it only emit once, not multiple times
#[test]
fn test_render_constant() {
let mut df = Dfir::new();
let mut state = DataflowState::default();
let mut ctx = harness_test_ctx(&mut df, &mut state);
let rows = vec![
(Row::empty(), 1, 1),
(Row::empty(), 2, 1),
(Row::empty(), 3, 1),
];
let collection = ctx.render_constant(rows);
let collection = collection.collection.clone(ctx.df);
let cnt = Rc::new(RefCell::new(0));
let cnt_inner = cnt.clone();
let res_subgraph_id = ctx.df.add_subgraph_sink(
"test_render_constant",
collection.into_inner(),
move |_ctx, recv| {
let data = recv.take_inner();
*cnt_inner.borrow_mut() += data.iter().map(|v| v.len()).sum::<usize>();
},
);
ctx.compute_state.set_current_ts(2);
ctx.compute_state.run_available_with_schedule(ctx.df);
assert_eq!(*cnt.borrow(), 2);
ctx.compute_state.set_current_ts(3);
ctx.compute_state.run_available_with_schedule(ctx.df);
// to get output
ctx.df.schedule_subgraph(res_subgraph_id);
ctx.df.run_available();
assert_eq!(*cnt.borrow(), 3);
}
/// a simple example to show how to use source and sink
#[test]
fn example_source_sink() {
let mut df = Dfir::new();
let (send_port, recv_port) = df.make_edge::<_, VecHandoff<i32>>("test_handoff");
df.add_subgraph_source("test_handoff_source", send_port, move |_ctx, send| {
for i in 0..10 {
send.give(vec![i]);
}
});
let sum = Rc::new(RefCell::new(0));
let sum_move = sum.clone();
let sink = df.add_subgraph_sink("test_handoff_sink", recv_port, move |_ctx, recv| {
let data = recv.take_inner();
*sum_move.borrow_mut() += data.iter().sum::<i32>();
});
df.run_available();
assert_eq!(sum.borrow().to_owned(), 45);
df.schedule_subgraph(sink);
df.run_available();
assert_eq!(sum.borrow().to_owned(), 45);
}
#[test]
fn test_tee_auto_schedule() {
use dfir_rs::scheduled::handoff::TeeingHandoff as Toff;
let mut df = Dfir::new();
let (send_port, recv_port) = df.make_edge::<_, Toff<i32>>("test_handoff");
let source = df.add_subgraph_source("test_handoff_source", send_port, move |_ctx, send| {
for i in 0..10 {
send.give(vec![i]);
}
});
let teed_recv_port = recv_port.tee(&mut df);
let sum = Rc::new(RefCell::new(0));
let sum_move = sum.clone();
let _sink = df.add_subgraph_sink("test_handoff_sink", teed_recv_port, move |_ctx, recv| {
let data = recv.take_inner();
*sum_move.borrow_mut() += data.iter().flat_map(|i| i.iter()).sum::<i32>();
});
drop(recv_port);
df.run_available();
assert_eq!(sum.borrow().to_owned(), 45);
df.schedule_subgraph(source);
df.run_available();
assert_eq!(sum.borrow().to_owned(), 90);
}
}
-425
View File
@@ -1,425 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::BTreeMap;
use dfir_rs::scheduled::graph_ext::GraphExt;
use dfir_rs::scheduled::port::{PortCtx, SEND};
use itertools::Itertools;
use snafu::OptionExt;
use crate::compute::render::Context;
use crate::compute::state::Scheduler;
use crate::compute::types::{Arranged, Collection, CollectionBundle, ErrCollector, Toff};
use crate::error::{Error, PlanSnafu};
use crate::expr::{Batch, EvalError, MapFilterProject, MfpPlan, ScalarExpr};
use crate::plan::TypedPlan;
use crate::repr::{self, DiffRow, KeyValDiffRow, RelationType, Row};
use crate::utils::ArrangeHandler;
impl Context<'_, '_> {
/// Like `render_mfp` but in batch mode
pub fn render_mfp_batch(
&mut self,
input: Box<TypedPlan>,
mfp: MapFilterProject,
_output_type: &RelationType,
) -> Result<CollectionBundle<Batch>, Error> {
let input = self.render_plan_batch(*input)?;
let (out_send_port, out_recv_port) = self.df.make_edge::<_, Toff<Batch>>("mfp_batch");
// This closure capture following variables:
let mfp_plan = MfpPlan::create_from(mfp)?;
let err_collector = self.err_collector.clone();
// TODO(discord9): better way to schedule future run
let scheduler = self.compute_state.get_scheduler();
let subgraph = self.df.add_subgraph_in_out(
"mfp_batch",
input.collection.into_inner(),
out_send_port,
move |_ctx, recv, send| {
// mfp only need to passively receive updates from recvs
let src_data = recv.take_inner().into_iter().flat_map(|v| v.into_iter());
let output_batches = src_data
.filter_map(|mut input_batch| {
err_collector.run(|| {
let res_batch = mfp_plan.mfp.eval_batch_into(&mut input_batch)?;
Ok(res_batch)
})
})
.collect_vec();
send.give(output_batches);
},
);
// register current subgraph in scheduler for future scheduling
scheduler.set_cur_subgraph(subgraph);
let bundle =
CollectionBundle::from_collection(Collection::<Batch>::from_port(out_recv_port));
Ok(bundle)
}
/// render MapFilterProject, will only emit the `rows` once. Assume all incoming row's sys time being `now`` and ignore the row's stated sys time
/// TODO(discord9): schedule mfp operator to run when temporal filter need
///
/// `MapFilterProject`(`mfp` for short) is scheduled to run when there is enough amount of input updates
/// ***or*** when a future update in it's output buffer(a `Arrangement`) is supposed to emit now.
// There is a false positive in using `Vec<ScalarExpr>` as key due to `Value` have `bytes` variant
#[allow(clippy::mutable_key_type)]
pub fn render_mfp(
&mut self,
input: Box<TypedPlan>,
mfp: MapFilterProject,
) -> Result<CollectionBundle, Error> {
let input = self.render_plan(*input)?;
// TODO(discord9): consider if check if contain temporal to determine if
// need arrange or not, or does this added complexity worth it
let (out_send_port, out_recv_port) = self.df.make_edge::<_, Toff>("mfp");
let output_arity = mfp.output_arity();
// default to have a arrange with only future updates, so it can be empty if no temporal filter is applied
// as stream only sends current updates and etc.
let arrange_handler = self.compute_state.new_arrange(None);
let arrange_handler_inner =
arrange_handler
.clone_future_only()
.with_context(|| PlanSnafu {
reason: "No write is expected at this point",
})?;
// This closure capture following variables:
let mfp_plan = MfpPlan::create_from(mfp)?;
let now = self.compute_state.current_time_ref();
let err_collector = self.err_collector.clone();
// TODO(discord9): better way to schedule future run
let scheduler = self.compute_state.get_scheduler();
let scheduler_inner = scheduler.clone();
let subgraph = self.df.add_subgraph_in_out(
"mfp",
input.collection.into_inner(),
out_send_port,
move |_ctx, recv, send| {
// mfp only need to passively receive updates from recvs
let data = recv.take_inner().into_iter().flat_map(|v| v.into_iter());
mfp_subgraph(
&arrange_handler_inner,
data,
&mfp_plan,
*now.borrow(),
&err_collector,
&scheduler_inner,
send,
);
},
);
// register current subgraph in scheduler for future scheduling
scheduler.set_cur_subgraph(subgraph);
let arranged = BTreeMap::from([(
(0..output_arity).map(ScalarExpr::Column).collect_vec(),
Arranged::new(arrange_handler),
)]);
let bundle = CollectionBundle {
collection: Collection::from_port(out_recv_port),
arranged,
};
Ok(bundle)
}
}
fn mfp_subgraph(
arrange: &ArrangeHandler,
input: impl IntoIterator<Item = DiffRow>,
mfp_plan: &MfpPlan,
now: repr::Timestamp,
err_collector: &ErrCollector,
scheduler: &Scheduler,
send: &PortCtx<SEND, Toff>,
) {
// all updates that should be send immediately
let mut output_now = vec![];
let run_mfp = || {
let mut all_updates = eval_mfp_core(input, mfp_plan, now, err_collector);
all_updates.retain(|(kv, ts, d)| {
if *ts > now {
true
} else {
output_now.push((kv.clone(), *ts, *d));
false
}
});
let future_updates = all_updates;
arrange.write().apply_updates(now, future_updates)?;
Ok(())
};
err_collector.run(run_mfp);
// Deal with output:
// 1. Read all updates that were emitted between the last time this arrangement had updates and the current time.
// 2. Output the updates.
// 3. Truncate all updates within that range.
let from = arrange.read().last_compaction_time();
let from = from.unwrap_or(repr::Timestamp::MIN);
let range = (
std::ops::Bound::Excluded(from),
std::ops::Bound::Included(now),
);
// find all updates that need to be send from arrangement
let output_kv = arrange.read().get_updates_in_range(range);
err_collector.run(|| {
snafu::ensure!(
mfp_plan.is_temporal() || output_kv.is_empty(),
crate::expr::error::InternalSnafu {
reason: "Output from future should be empty since temporal filter is not applied"
}
);
Ok(())
});
// the output is expected to be key -> empty val
let output = output_kv
.into_iter()
.chain(output_now) // chain previous immediately send updates
.map(|((key, _v), ts, diff)| (key, ts, diff))
.collect_vec();
// send output
send.give(output);
let run_compaction = || {
arrange.write().compact_to(now)?;
Ok(())
};
err_collector.run(run_compaction);
// schedule next time this subgraph should run
scheduler.schedule_for_arrange(&arrange.read(), now);
}
/// The core of evaluating MFP operator, given a MFP and a input, evaluate the MFP operator,
/// return the output updates **And** possibly any number of errors that occurred during the evaluation
fn eval_mfp_core(
input: impl IntoIterator<Item = DiffRow>,
mfp_plan: &MfpPlan,
now: repr::Timestamp,
err_collector: &ErrCollector,
) -> Vec<KeyValDiffRow> {
let mut all_updates = Vec::new();
for (mut row, _sys_time, diff) in input.into_iter() {
// this updates is expected to be only zero, one or two rows
let updates = mfp_plan.evaluate::<EvalError>(&mut row.inner, now, diff);
// TODO(discord9): refactor error handling
// Expect error in a single row to not interrupt the whole evaluation
let updates = updates
.filter_map(|r| match r {
Ok((key, ts, diff)) => Some(((key, Row::empty()), ts, diff)),
Err((err, _ts, _diff)) => {
err_collector.push_err(err);
None
}
})
.collect_vec();
all_updates.extend(updates);
}
all_updates
}
#[cfg(test)]
mod test {
use datatypes::data_type::ConcreteDataType;
use dfir_rs::scheduled::graph::Dfir;
use super::*;
use crate::compute::render::test::{get_output_handle, harness_test_ctx, run_and_check};
use crate::compute::state::DataflowState;
use crate::expr::{self, BinaryFunc, GlobalId};
use crate::plan::Plan;
use crate::repr::{ColumnType, RelationType};
/// test if temporal filter works properly
/// namely: if mfp operator can schedule a delete at the correct time
#[test]
fn test_render_mfp_with_temporal() {
let mut df = Dfir::new();
let mut state = DataflowState::default();
let mut ctx = harness_test_ctx(&mut df, &mut state);
let rows = vec![
(Row::new(vec![1i64.into()]), 0, 1),
(Row::new(vec![2i64.into()]), 0, 1),
(Row::new(vec![3i64.into()]), 0, 1),
];
let collection = ctx.render_constant(rows.clone());
ctx.insert_global(GlobalId::User(1), collection);
let input_plan = Plan::Get {
id: expr::Id::Global(GlobalId::User(1)),
};
let typ = RelationType::new(vec![ColumnType::new_nullable(
ConcreteDataType::int64_datatype(),
)]);
// temporal filter: now <= col(0) < now + 4
let mfp = MapFilterProject::new(1)
.filter(vec![
ScalarExpr::Column(0)
.call_unary(expr::UnaryFunc::Cast(
ConcreteDataType::timestamp_microsecond_datatype(),
))
.call_binary(
ScalarExpr::CallUnmaterializable(expr::UnmaterializableFunc::Now),
BinaryFunc::Gte,
),
ScalarExpr::Column(0)
.call_binary(
ScalarExpr::literal(4i64.into(), ConcreteDataType::int64_datatype()),
BinaryFunc::SubInt64,
)
.call_unary(expr::UnaryFunc::Cast(
ConcreteDataType::timestamp_microsecond_datatype(),
))
.call_binary(
ScalarExpr::CallUnmaterializable(expr::UnmaterializableFunc::Now),
BinaryFunc::Lt,
),
])
.unwrap();
let bundle = ctx
.render_mfp(Box::new(input_plan.with_types(typ.into_unnamed())), mfp)
.unwrap();
let output = get_output_handle(&mut ctx, bundle);
// drop ctx here to simulate actual process of compile first, run later scenario
drop(ctx);
// expected output at given time
let expected_output = BTreeMap::from([
(
0, // time
vec![
(Row::new(vec![1i64.into()]), 0, 1),
(Row::new(vec![2i64.into()]), 0, 1),
(Row::new(vec![3i64.into()]), 0, 1),
],
),
(
2, // time
vec![(Row::new(vec![1i64.into()]), 2, -1)],
),
(
3, // time
vec![(Row::new(vec![2i64.into()]), 3, -1)],
),
(
4, // time
vec![(Row::new(vec![3i64.into()]), 4, -1)],
),
]);
run_and_check(&mut state, &mut df, 0..5, expected_output, output);
}
/// test if mfp operator without temporal filter works properly
/// that is it filter the rows correctly
#[test]
fn test_render_mfp() {
let mut df = Dfir::new();
let mut state = DataflowState::default();
let mut ctx = harness_test_ctx(&mut df, &mut state);
let rows = vec![
(Row::new(vec![1.into()]), 1, 1),
(Row::new(vec![2.into()]), 2, 1),
(Row::new(vec![3.into()]), 3, 1),
];
let collection = ctx.render_constant(rows.clone());
ctx.insert_global(GlobalId::User(1), collection);
let input_plan = Plan::Get {
id: expr::Id::Global(GlobalId::User(1)),
};
let typ = RelationType::new(vec![ColumnType::new_nullable(
ConcreteDataType::int64_datatype(),
)]);
// filter: col(0)>1
let mfp = MapFilterProject::new(1)
.filter(vec![ScalarExpr::Column(0).call_binary(
ScalarExpr::literal(1.into(), ConcreteDataType::int32_datatype()),
BinaryFunc::Gt,
)])
.unwrap();
let bundle = ctx
.render_mfp(Box::new(input_plan.with_types(typ.into_unnamed())), mfp)
.unwrap();
let output = get_output_handle(&mut ctx, bundle);
drop(ctx);
let expected = BTreeMap::from([
(2, vec![(Row::new(vec![2.into()]), 2, 1)]),
(3, vec![(Row::new(vec![3.into()]), 3, 1)]),
]);
run_and_check(&mut state, &mut df, 1..5, expected, output);
}
/// test if mfp operator can run multiple times within same tick
#[test]
fn test_render_mfp_multiple_times() {
let mut df = Dfir::new();
let mut state = DataflowState::default();
let mut ctx = harness_test_ctx(&mut df, &mut state);
let (sender, recv) = tokio::sync::broadcast::channel(1000);
let collection = ctx.render_source(recv).unwrap();
ctx.insert_global(GlobalId::User(1), collection);
let input_plan = Plan::Get {
id: expr::Id::Global(GlobalId::User(1)),
};
let typ = RelationType::new(vec![ColumnType::new_nullable(
ConcreteDataType::int64_datatype(),
)]);
// filter: col(0)>1
let mfp = MapFilterProject::new(1)
.filter(vec![ScalarExpr::Column(0).call_binary(
ScalarExpr::literal(1.into(), ConcreteDataType::int32_datatype()),
BinaryFunc::Gt,
)])
.unwrap();
let bundle = ctx
.render_mfp(Box::new(input_plan.with_types(typ.into_unnamed())), mfp)
.unwrap();
let output = get_output_handle(&mut ctx, bundle);
drop(ctx);
sender.send((Row::new(vec![2.into()]), 0, 1)).unwrap();
state.run_available_with_schedule(&mut df);
assert_eq!(output.borrow().len(), 1);
output.borrow_mut().clear();
sender.send((Row::new(vec![3.into()]), 0, 1)).unwrap();
state.run_available_with_schedule(&mut df);
assert_eq!(output.borrow().len(), 1);
}
}
File diff suppressed because it is too large Load Diff
-245
View File
@@ -1,245 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Source and Sink for the dataflow
use std::collections::BTreeMap;
use common_telemetry::{debug, trace};
use dfir_rs::scheduled::graph_ext::GraphExt;
use itertools::Itertools;
use snafu::OptionExt;
use tokio::sync::broadcast::error::TryRecvError;
use tokio::sync::{broadcast, mpsc};
use crate::compute::render::Context;
use crate::compute::types::{Arranged, Collection, CollectionBundle, Toff};
use crate::error::{Error, PlanSnafu};
use crate::expr::error::InternalSnafu;
use crate::expr::{Batch, EvalError};
use crate::repr::{DiffRow, Row};
#[allow(clippy::mutable_key_type)]
impl Context<'_, '_> {
/// simply send the batch to downstream, without fancy features like buffering
pub fn render_source_batch(
&mut self,
mut src_recv: broadcast::Receiver<Batch>,
) -> Result<CollectionBundle<Batch>, Error> {
debug!("Rendering Source Batch");
let (send_port, recv_port) = self.df.make_edge::<_, Toff<Batch>>("source_batch");
let schd = self.compute_state.get_scheduler();
let inner_schd = schd.clone();
let now = self.compute_state.current_time_ref();
let err_collector = self.err_collector.clone();
let sub = self
.df
.add_subgraph_source("source_batch", send_port, move |_ctx, send| {
let mut total_batches = vec![];
let mut total_row_count = 0;
loop {
match src_recv.try_recv() {
Ok(batch) => {
total_row_count += batch.row_count();
total_batches.push(batch);
}
Err(TryRecvError::Empty) => {
break;
}
Err(TryRecvError::Lagged(lag_offset)) => {
// use `err_collector` instead of `error!` to locate which operator caused the error
err_collector.run(|| -> Result<(), EvalError> {
InternalSnafu {
reason: format!("Flow missing {} rows behind", lag_offset),
}
.fail()
});
break;
}
Err(TryRecvError::Closed) => {
err_collector.run(|| -> Result<(), EvalError> {
InternalSnafu {
reason: "Source Batch Channel is closed".to_string(),
}
.fail()
});
break;
}
}
}
trace!(
"Send {} rows in {} batches",
total_row_count,
total_batches.len()
);
send.give(total_batches);
let now = *now.borrow();
// always schedule source to run at now so we can
// repeatedly run source if needed
inner_schd.schedule_at(now);
});
schd.set_cur_subgraph(sub);
let bundle = CollectionBundle::from_collection(Collection::<Batch>::from_port(recv_port));
Ok(bundle)
}
/// Render a source which comes from brocast channel into the dataflow
/// will immediately send updates not greater than `now` and buffer the rest in arrangement
pub fn render_source(
&mut self,
mut src_recv: broadcast::Receiver<DiffRow>,
) -> Result<CollectionBundle, Error> {
debug!("Rendering Source");
let (send_port, recv_port) = self.df.make_edge::<_, Toff>("source");
let arrange_handler = self.compute_state.new_arrange(None);
let arrange_handler_inner =
arrange_handler
.clone_future_only()
.with_context(|| PlanSnafu {
reason: "No write is expected at this point",
})?;
let schd = self.compute_state.get_scheduler();
let inner_schd = schd.clone();
let now = self.compute_state.current_time_ref();
let err_collector = self.err_collector.clone();
let sub = self
.df
.add_subgraph_source("source", send_port, move |_ctx, send| {
let now = *now.borrow();
// write lock to prevent unexpected mutation
let mut arranged = arrange_handler_inner.write();
let arr = arranged.get_updates_in_range(..=now);
err_collector.run(|| arranged.compact_to(now));
let prev_avail = arr.into_iter().map(|((k, _), t, d)| (k, t, d));
let mut to_send = Vec::new();
let mut to_arrange = Vec::new();
// TODO(discord9): handling tokio broadcast error
loop {
match src_recv.try_recv() {
Ok((r, t, d)) => {
if t <= now {
to_send.push((r, t, d));
} else {
to_arrange.push(((r, Row::empty()), t, d));
}
}
Err(TryRecvError::Empty) => {
break;
}
Err(TryRecvError::Lagged(lag_offset)) => {
common_telemetry::error!("Flow missing {} rows behind", lag_offset);
break;
}
Err(err) => {
err_collector.run(|| -> Result<(), EvalError> {
InternalSnafu {
reason: format!(
"Error receiving from broadcast channel: {}",
err
),
}
.fail()
});
}
}
}
let all = prev_avail.chain(to_send).collect_vec();
if !to_arrange.is_empty() {
debug!("Source Operator buffered {} rows", to_arrange.len());
}
err_collector.run(|| arranged.apply_updates(now, to_arrange));
send.give(all);
// always schedule source to run at now so we can repeatedly run source if needed
inner_schd.schedule_at(now);
});
schd.set_cur_subgraph(sub);
let arranged = Arranged::new(arrange_handler);
arranged.writer.borrow_mut().replace(sub);
let arranged = BTreeMap::from([(vec![], arranged)]);
Ok(CollectionBundle {
collection: Collection::from_port(recv_port),
arranged,
})
}
pub fn render_unbounded_sink_batch(
&mut self,
bundle: CollectionBundle<Batch>,
sender: mpsc::UnboundedSender<Batch>,
) {
let CollectionBundle {
collection,
arranged: _,
} = bundle;
let _sink = self.df.add_subgraph_sink(
"UnboundedSinkBatch",
collection.into_inner(),
move |_ctx, recv| {
let data = recv.take_inner();
let mut row_count = 0;
let mut batch_count = 0;
for batch in data.into_iter().flat_map(|i| i.into_iter()) {
row_count += batch.row_count();
batch_count += 1;
// if the sender is closed unexpectedly, stop sending
if sender.is_closed() || sender.send(batch).is_err() {
common_telemetry::error!("UnboundedSinkBatch is closed");
break;
}
}
trace!("sink send {} rows in {} batches", row_count, batch_count);
},
);
}
pub fn render_unbounded_sink(
&mut self,
bundle: CollectionBundle,
sender: mpsc::UnboundedSender<DiffRow>,
) {
let CollectionBundle {
collection,
arranged: _,
} = bundle;
let _sink = self.df.add_subgraph_sink(
"UnboundedSink",
collection.into_inner(),
move |_ctx, recv| {
let data = recv.take_inner();
debug!(
"render_unbounded_sink: send {} rows",
data.iter().map(|i| i.len()).sum::<usize>()
);
for row in data.into_iter().flat_map(|i| i.into_iter()) {
// if the sender is closed, stop sending
if sender.is_closed() {
common_telemetry::error!("UnboundedSink is closed");
break;
}
// TODO(discord9): handling tokio error
let _ = sender.send(row);
}
},
);
}
}
-167
View File
@@ -1,167 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::cell::RefCell;
use std::collections::{BTreeMap, VecDeque};
use std::rc::Rc;
use dfir_rs::scheduled::SubgraphId;
use dfir_rs::scheduled::graph::Dfir;
use get_size2::GetSize;
use crate::compute::types::ErrCollector;
use crate::repr::{self, Timestamp};
use crate::utils::{ArrangeHandler, Arrangement};
/// input/output of a dataflow
/// One `ComputeState` manage the input/output/schedule of one `Dfir`
#[derive(Debug, Default)]
pub struct DataflowState {
/// it is important to use a deque to maintain the order of subgraph here
/// TODO(discord9): consider dedup? Also not necessary for hydroflow itself also do dedup when schedule
schedule_subgraph: Rc<RefCell<BTreeMap<Timestamp, VecDeque<SubgraphId>>>>,
/// Frontier (in sys time) before which updates should not be emitted.
///
/// We *must* apply it to sinks, to ensure correct outputs.
/// We *should* apply it to sources and imported shared state, because it improves performance.
/// Which means it's also the current time in temporal filter to get current correct result
as_of: Rc<RefCell<Timestamp>>,
/// error collector local to this `ComputeState`,
/// useful for distinguishing errors from different `Dfir`
err_collector: ErrCollector,
/// save all used arrange in this dataflow, since usually there is no delete operation
/// we can just keep track of all used arrange and schedule subgraph when they need to be updated
arrange_used: Vec<ArrangeHandler>,
/// the time arrangement need to be expired after a certain time in milliseconds
expire_after: Option<Timestamp>,
/// the last time each subgraph executed
last_exec_time: Option<Timestamp>,
/// the time the flow first executed, in unix timestamp milliseconds
start_time: Option<Timestamp>,
}
impl DataflowState {
pub fn new_arrange(&mut self, name: Option<Vec<String>>) -> ArrangeHandler {
let arrange = name.map(Arrangement::new_with_name).unwrap_or_default();
let arr = ArrangeHandler::from(arrange);
// mark this arrange as used in this dataflow
self.arrange_used.push(
arr.clone_future_only()
.expect("No write happening at this point"),
);
arr
}
/// schedule all subgraph that need to run with time <= `as_of` and run_available()
///
/// return true if any subgraph actually executed
#[allow(clippy::swap_with_temporary)]
pub fn run_available_with_schedule(&mut self, df: &mut Dfir) -> bool {
// first split keys <= as_of into another map
let mut before = self
.schedule_subgraph
.borrow_mut()
.split_off(&(*self.as_of.borrow() + 1));
std::mem::swap(&mut before, &mut self.schedule_subgraph.borrow_mut());
for (_, v) in before {
for subgraph in v {
df.schedule_subgraph(subgraph);
}
}
df.run_available()
}
pub fn get_scheduler(&self) -> Scheduler {
Scheduler {
schedule_subgraph: self.schedule_subgraph.clone(),
cur_subgraph: Rc::new(RefCell::new(None)),
}
}
/// return a handle to the current time, will update when `as_of` is updated
///
/// so it can keep track of the current time even in a closure that is called later
pub fn current_time_ref(&self) -> Rc<RefCell<Timestamp>> {
self.as_of.clone()
}
pub fn current_ts(&self) -> Timestamp {
*self.as_of.borrow()
}
pub fn set_current_ts(&mut self, ts: Timestamp) {
self.as_of.replace(ts);
}
pub fn get_err_collector(&self) -> ErrCollector {
self.err_collector.clone()
}
pub fn set_expire_after(&mut self, after: Option<repr::Duration>) {
self.expire_after = after;
}
pub fn expire_after(&self) -> Option<Timestamp> {
self.expire_after
}
pub fn get_state_size(&self) -> usize {
self.arrange_used.iter().map(|x| x.read().get_size()).sum()
}
pub fn set_last_exec_time(&mut self, time: Timestamp) {
self.last_exec_time = Some(time);
if self.start_time.is_none() {
// start_time is recorded at the completion of the first execution
// (post-execution), consistent with how last_exec_time is recorded.
self.start_time = Some(time);
}
}
pub fn last_exec_time(&self) -> Option<Timestamp> {
self.last_exec_time
}
/// Returns the time the flow first executed, in unix timestamp milliseconds.
pub fn start_time(&self) -> Option<Timestamp> {
self.start_time
}
}
#[derive(Debug, Clone)]
pub struct Scheduler {
// this scheduler is shared with `DataflowState`, so it can schedule subgraph
schedule_subgraph: Rc<RefCell<BTreeMap<Timestamp, VecDeque<SubgraphId>>>>,
cur_subgraph: Rc<RefCell<Option<SubgraphId>>>,
}
impl Scheduler {
pub fn schedule_at(&self, next_run_time: Timestamp) {
let mut schedule_subgraph = self.schedule_subgraph.borrow_mut();
let subgraph = self.cur_subgraph.borrow();
let subgraph = subgraph.as_ref().expect("Set SubgraphId before schedule");
let subgraph_queue = schedule_subgraph.entry(next_run_time).or_default();
subgraph_queue.push_back(*subgraph);
}
pub fn schedule_for_arrange(&self, arrange: &Arrangement, now: Timestamp) {
if let Some(i) = arrange.get_next_update_time(&now) {
self.schedule_at(i)
}
}
pub fn set_cur_subgraph(&self, subgraph: SubgraphId) {
self.cur_subgraph.replace(Some(subgraph));
}
}
-208
View File
@@ -1,208 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::cell::RefCell;
use std::collections::{BTreeMap, VecDeque};
use std::rc::Rc;
use std::sync::Arc;
use common_error::ext::ErrorExt;
use dfir_rs::scheduled::SubgraphId;
use dfir_rs::scheduled::graph::Dfir;
use dfir_rs::scheduled::handoff::TeeingHandoff;
use dfir_rs::scheduled::port::RecvPort;
use itertools::Itertools;
use tokio::sync::Mutex;
use crate::expr::{Batch, EvalError, ScalarExpr};
use crate::metrics::METRIC_FLOW_ERRORS;
use crate::repr::DiffRow;
use crate::utils::ArrangeHandler;
pub type Toff<T = DiffRow> = TeeingHandoff<T>;
/// A collection, represent a collections of data that is received from a handoff.
pub struct Collection<T: 'static> {
/// represent a stream of updates recv from this port
stream: RecvPort<TeeingHandoff<T>>,
}
impl<T: 'static + Clone> Collection<T> {
pub fn from_port(port: RecvPort<TeeingHandoff<T>>) -> Self {
Collection { stream: port }
}
/// clone a collection, require a mutable reference to the hydroflow instance
///
/// Note: need to be the same hydroflow instance that this collection is created from
pub fn clone(&self, df: &mut Dfir) -> Self {
Collection {
stream: self.stream.tee(df),
}
}
pub fn into_inner(self) -> RecvPort<TeeingHandoff<T>> {
self.stream
}
}
/// Arranged is a wrapper around `ArrangeHandler` that maintain a list of readers and a writer
pub struct Arranged {
pub arrangement: ArrangeHandler,
pub writer: Rc<RefCell<Option<SubgraphId>>>,
/// maintain a list of readers for the arrangement for the ease of scheduling
pub readers: Rc<RefCell<Vec<SubgraphId>>>,
}
impl Arranged {
pub fn new(arr: ArrangeHandler) -> Self {
Self {
arrangement: arr,
writer: Default::default(),
readers: Default::default(),
}
}
/// Copy it's future only updates, internally `Rc-ed` so it's cheap to copy
pub fn try_copy_future(&self) -> Option<Self> {
self.arrangement
.clone_future_only()
.map(|arrangement| Arranged {
arrangement,
readers: self.readers.clone(),
writer: self.writer.clone(),
})
}
}
/// A bundle of the various ways a collection can be represented.
///
/// This type maintains the invariant that it does contain at least one(or both) valid
/// source of data, either a collection or at least one arrangement. This is for convenience
/// of reading the data from the collection.
///
// TODO(discord9): make T default to Batch and obsolete the Row Mode
pub struct CollectionBundle<T: 'static = DiffRow> {
/// This is useful for passively reading the new updates from the collection
///
/// Invariant: the timestamp of the updates should always not greater than now, since future updates should be stored in the arrangement
pub collection: Collection<T>,
/// the key [`ScalarExpr`] indicate how the keys(also a [`Row`]) used in Arranged is extract from collection's [`Row`]
/// So it is the "index" of the arrangement
///
/// The `Arranged` is the actual data source, it can be used to read the data from the collection by
/// using the key indicated by the `Vec<ScalarExpr>`
/// There is a false positive in using `Vec<ScalarExpr>` as key due to `ScalarExpr::Literal`
/// contain a `Value` which have `bytes` variant
#[allow(clippy::mutable_key_type)]
pub arranged: BTreeMap<Vec<ScalarExpr>, Arranged>,
}
pub trait GenericBundle {
fn is_batch(&self) -> bool;
fn try_as_batch(&self) -> Option<&CollectionBundle<Batch>> {
None
}
fn try_as_row(&self) -> Option<&CollectionBundle<DiffRow>> {
None
}
}
impl GenericBundle for CollectionBundle<Batch> {
fn is_batch(&self) -> bool {
true
}
fn try_as_batch(&self) -> Option<&CollectionBundle<Batch>> {
Some(self)
}
}
impl GenericBundle for CollectionBundle<DiffRow> {
fn is_batch(&self) -> bool {
false
}
fn try_as_row(&self) -> Option<&CollectionBundle<DiffRow>> {
Some(self)
}
}
impl<T: 'static> CollectionBundle<T> {
pub fn from_collection(collection: Collection<T>) -> Self {
Self {
collection,
arranged: BTreeMap::default(),
}
}
}
impl<T: 'static + Clone> CollectionBundle<T> {
pub fn clone(&self, df: &mut Dfir) -> Self {
Self {
collection: self.collection.clone(df),
arranged: self
.arranged
.iter()
.map(|(k, v)| (k.clone(), v.try_copy_future().unwrap()))
.collect(),
}
}
}
/// A thread local error collector, used to collect errors during the evaluation of the plan
///
/// usually only the first error matters, but store all of them just in case
///
/// Using a `VecDeque` to preserve the order of errors
/// when running dataflow continuously and need errors in order
#[derive(Debug, Default, Clone)]
pub struct ErrCollector {
pub inner: Arc<Mutex<VecDeque<EvalError>>>,
}
impl ErrCollector {
pub fn get_all_blocking(&self) -> Vec<EvalError> {
self.inner.blocking_lock().drain(..).collect_vec()
}
pub async fn get_all(&self) -> Vec<EvalError> {
self.inner.lock().await.drain(..).collect_vec()
}
pub fn is_empty(&self) -> bool {
self.inner.blocking_lock().is_empty()
}
pub fn push_err(&self, err: EvalError) {
METRIC_FLOW_ERRORS
.with_label_values(&[err.status_code().as_ref()])
.inc();
self.inner.blocking_lock().push_back(err)
}
pub fn run<F, R>(&self, f: F) -> Option<R>
where
F: FnOnce() -> Result<R, EvalError>,
{
match f() {
Ok(r) => Some(r),
Err(e) => {
self.push_err(e);
None
}
}
}
}
+2 -44
View File
@@ -29,19 +29,13 @@ use datafusion::optimizer::optimize_projections::OptimizeProjections;
use datafusion::optimizer::simplify_expressions::SimplifyExpressions;
use datafusion::optimizer::{Analyzer, AnalyzerRule, Optimizer, OptimizerContext};
use datafusion_common::tree_node::{Transformed, TreeNode, TreeNodeRecursion, TreeNodeVisitor};
use query::QueryEngine;
use query::optimizer::count_wildcard::CountWildcardToTimeIndexRule;
use query::parser::QueryLanguageParser;
use query::query_engine::DefaultSerializer;
use session::context::QueryContextRef;
use snafu::ResultExt;
/// note here we are using the `substrait_proto_df` crate from the `substrait` module and
/// rename it to `substrait_proto`
use substrait::DFLogicalSubstraitConvertor;
use crate::adapter::FlownodeContext;
use crate::error::{DatafusionSnafu, Error, ExternalSnafu, UnexpectedSnafu};
use crate::plan::TypedPlan;
use crate::error::{DatafusionSnafu, Error, ExternalSnafu};
// TODO(discord9): use `Analyzer` to manage rules if more `AnalyzerRule` is needed
pub async fn apply_df_optimizer(
@@ -83,42 +77,6 @@ pub async fn apply_df_optimizer(
Ok(plan)
}
/// To reuse existing code for parse sql, the sql is first parsed into a datafusion logical plan,
/// then to a substrait plan, and finally to a flow plan.
pub async fn sql_to_flow_plan(
ctx: &mut FlownodeContext,
engine: &Arc<dyn QueryEngine>,
sql: &str,
) -> Result<TypedPlan, Error> {
let query_ctx = ctx.query_context.clone().ok_or_else(|| {
UnexpectedSnafu {
reason: "Query context is missing",
}
.build()
})?;
let stmt = QueryLanguageParser::parse_sql(sql, &query_ctx)
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
let plan = engine
.planner()
.plan(&stmt, query_ctx.clone())
.await
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
let opted_plan = apply_df_optimizer(plan, &query_ctx).await?;
// TODO(discord9): add df optimization
let sub_plan = DFLogicalSubstraitConvertor {}
.to_sub_plan(&opted_plan, DefaultSerializer)
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
let flow_plan = TypedPlan::from_substrait_plan(ctx, &sub_plan).await?;
Ok(flow_plan)
}
/// This rule check all group by exprs, and make sure they are also in select clause in a aggr query
#[derive(Debug)]
struct CheckGroupByRule {}
+3 -35
View File
@@ -25,12 +25,11 @@ use common_error::{
use common_macro::stack_trace_debug;
use common_telemetry::common_error::ext::ErrorExt;
use common_telemetry::common_error::status_code::StatusCode;
use snafu::{Location, ResultExt, Snafu};
use snafu::{Location, Snafu};
use tonic::codegen::http::HeaderValue;
use tonic::metadata::MetadataMap;
use crate::FlowId;
use crate::expr::EvalError;
/// This error is used to represent all possible errors that can occur in the flow module.
#[derive(Snafu)]
@@ -101,14 +100,6 @@ pub enum Error {
location: Location,
},
/// TODO(discord9): add detailed location of column
#[snafu(display("Failed to eval stream"))]
Eval {
source: EvalError,
#[snafu(implicit)]
location: Location,
},
#[snafu(display("Table not found: {name}"))]
TableNotFound {
name: String,
@@ -161,13 +152,6 @@ pub enum Error {
location: Location,
},
#[snafu(display("Not implement in flow: {reason}"))]
NotImplemented {
reason: String,
#[snafu(implicit)]
location: Location,
},
#[snafu(display("Flow plan error: {reason}"))]
Plan {
reason: String,
@@ -182,13 +166,6 @@ pub enum Error {
location: Location,
},
#[snafu(display("Unsupported temporal filter: {reason}"))]
UnsupportedTemporalFilter {
reason: String,
#[snafu(implicit)]
location: Location,
},
#[snafu(display("Datatypes error: {source} with extra message: {extra}"))]
Datatypes {
source: datatypes::Error,
@@ -330,8 +307,7 @@ pub type Result<T> = std::result::Result<T, Error>;
impl ErrorExt for Error {
fn status_code(&self) -> StatusCode {
match self {
Self::Eval { .. }
| Self::JoinTask { .. }
Self::JoinTask { .. }
| Self::Datafusion { .. }
| Self::InsertIntoFlow { .. }
| Self::NoAvailableFrontend { .. }
@@ -349,9 +325,7 @@ impl ErrorExt for Error {
Self::Unexpected { .. }
| Self::SyncCheckTask { .. }
| Self::IllegalCheckTaskState { .. } => StatusCode::Unexpected,
Self::NotImplemented { .. }
| Self::UnsupportedTemporalFilter { .. }
| Self::Unsupported { .. } => StatusCode::Unsupported,
Self::Unsupported { .. } => StatusCode::Unsupported,
Self::External { source, .. } => source.status_code(),
Self::Internal { .. } | Self::CacheRequired { .. } => StatusCode::Internal,
Self::StartServer { source, .. } | Self::ShutdownServer { source, .. } => {
@@ -405,9 +379,3 @@ impl ErrorExt for Error {
}
define_into_tonic_status!(Error);
impl From<EvalError> for Error {
fn from(e: EvalError) -> Self {
Err::<(), _>(e).context(EvalSnafu).unwrap_err()
}
}
+27 -322
View File
@@ -12,373 +12,78 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! for declare Expression in dataflow, including map, reduce, id and join(TODO!) etc.
//! Small row-batch conversion helper used by stateless streaming.
mod df_func;
pub(crate) mod error;
pub(crate) mod func;
mod id;
mod linear;
pub(crate) mod relation;
mod scalar;
mod signature;
pub(crate) mod utils;
use arrow::compute::FilterBuilder;
use common_recordbatch::RecordBatch;
use datatypes::prelude::{ConcreteDataType, DataType};
use datatypes::value::Value;
use datatypes::vectors::{BooleanVector, Helper, VectorRef};
pub(crate) use df_func::{DfScalarFunction, RawDfScalarFn};
pub(crate) use error::{EvalError, InvalidArgumentSnafu};
pub(crate) use func::{BinaryFunc, UnaryFunc, UnmaterializableFunc, VariadicFunc};
pub(crate) use id::{GlobalId, Id, LocalId};
use datatypes::data_type::DataType;
use datatypes::prelude::ConcreteDataType;
use datatypes::vectors::{Helper, VectorRef};
use itertools::Itertools;
pub(crate) use linear::{MapFilterProject, MfpPlan, SafeMfpPlan};
pub(crate) use relation::{Accum, Accumulator, AggregateExpr, AggregateFunc};
pub(crate) use scalar::{ScalarExpr, TypedExpr};
use snafu::{ResultExt, ensure};
use crate::Error;
use crate::error::DatatypesSnafu;
use crate::expr::error::{ArrowSnafu, DataTypeSnafu};
use crate::repr::Diff;
use crate::repr::Row;
pub const TUMBLE_START: &str = "tumble_start";
pub const TUMBLE_END: &str = "tumble_end";
/// A batch of vectors with the same length but without schema, only useful in dataflow
///
/// somewhere cheap to clone since it just contains a list of VectorRef(which is a `Arc`).
#[derive(Debug, Clone)]
pub struct Batch {
batch: Vec<VectorRef>,
row_count: usize,
/// describe if corresponding rows in batch is insert or delete, None means all rows are insert
diffs: Option<VectorRef>,
}
impl TryFrom<RecordBatch> for Batch {
type Error = Error;
fn try_from(value: RecordBatch) -> Result<Self, Self::Error> {
let columns = value.columns();
let batch = Helper::try_into_vectors(columns).context(DatatypesSnafu {
extra: "failed to convert Arrow array to vector when building Flow batch",
})?;
Ok(Self {
batch: Helper::try_into_vectors(value.columns()).context(DatatypesSnafu {
extra: "failed to convert Arrow array to vector",
})?,
row_count: value.num_rows(),
batch,
diffs: None,
})
}
}
impl PartialEq for Batch {
fn eq(&self, other: &Self) -> bool {
let mut batch_eq = true;
if self.batch.len() != other.batch.len() {
return false;
}
for (left, right) in self.batch.iter().zip(other.batch.iter()) {
batch_eq = batch_eq
&& <dyn arrow::array::Array>::eq(&left.to_arrow_array(), &right.to_arrow_array());
}
let diff_eq = match (&self.diffs, &other.diffs) {
(Some(left), Some(right)) => {
<dyn arrow::array::Array>::eq(&left.to_arrow_array(), &right.to_arrow_array())
}
(None, None) => true,
_ => false,
};
batch_eq && diff_eq && self.row_count == other.row_count
}
}
impl Eq for Batch {}
impl Default for Batch {
fn default() -> Self {
Self::empty()
}
}
impl Batch {
/// Get batch from rows, will try best to determine data type
pub fn try_from_rows_with_types(
rows: Vec<crate::repr::Row>,
batch_datatypes: &[ConcreteDataType],
) -> Result<Self, EvalError> {
rows: Vec<Row>,
types: &[ConcreteDataType],
) -> Result<Self, error::EvalError> {
if rows.is_empty() {
return Ok(Self::empty());
return Ok(Self {
batch: vec![],
row_count: 0,
});
}
let len = rows.len();
let mut builder = batch_datatypes
let mut builders = types
.iter()
.map(|ty| ty.create_mutable_vector(len))
.collect_vec();
ensure!(
rows.iter().all(|row| row.len() == builders.len()),
error::InvalidArgumentSnafu {
reason: "row length does not match schema".to_string()
}
);
for row in rows {
ensure!(
row.len() == builder.len(),
InvalidArgumentSnafu {
reason: format!(
"row length not match, expect {}, found {}",
builder.len(),
row.len()
)
}
);
for (idx, value) in row.iter().enumerate() {
builder[idx]
builders[idx]
.try_push_value_ref(&value.as_value_ref())
.context(DataTypeSnafu {
msg: "Failed to convert rows to columns",
.context(error::DataTypeSnafu {
msg: "failed to convert rows to columns",
})?;
}
}
let columns = builder.into_iter().map(|mut b| b.to_vector()).collect_vec();
let batch = Self::try_new(columns, len)?;
Ok(batch)
}
pub fn empty() -> Self {
Self {
batch: vec![],
row_count: 0,
diffs: None,
}
}
pub fn try_new(batch: Vec<VectorRef>, row_count: usize) -> Result<Self, EvalError> {
ensure!(
batch.iter().map(|v| v.len()).all_equal()
&& batch.first().map(|v| v.len() == row_count).unwrap_or(true),
InvalidArgumentSnafu {
reason: "All columns should have same length".to_string()
}
);
Ok(Self {
batch,
row_count,
diffs: None,
batch: builders.into_iter().map(|mut b| b.to_vector()).collect(),
row_count: len,
})
}
pub fn new_unchecked(batch: Vec<VectorRef>, row_count: usize) -> Self {
Self {
batch,
row_count,
diffs: None,
}
}
pub fn batch(&self) -> &[VectorRef] {
&self.batch
}
pub fn batch_mut(&mut self) -> &mut Vec<VectorRef> {
&mut self.batch
}
pub fn row_count(&self) -> usize {
self.row_count
}
pub fn set_row_count(&mut self, row_count: usize) {
self.row_count = row_count;
}
pub fn column_count(&self) -> usize {
self.batch.len()
}
pub fn get_row(&self, idx: usize) -> Result<Vec<Value>, EvalError> {
ensure!(
idx < self.row_count,
InvalidArgumentSnafu {
reason: format!(
"Expect row index to be less than {}, found {}",
self.row_count, idx
)
}
);
let mut ret = Vec::with_capacity(self.column_count());
ret.extend(self.batch.iter().map(|v| v.get(idx)));
Ok(ret)
}
/// Slices the `Batch`, returning a new `Batch`.
pub fn slice(&self, offset: usize, length: usize) -> Result<Batch, EvalError> {
let batch = self
.batch()
.iter()
.map(|v| v.slice(offset, length))
.collect_vec();
Batch::try_new(batch, length)
}
/// append another batch to self
///
/// NOTE: This is expensive since it will create new vectors for each column
pub fn append_batch(&mut self, other: Batch) -> Result<(), EvalError> {
ensure!(
self.batch.len() == other.batch.len()
|| self.batch.is_empty()
|| other.batch.is_empty(),
InvalidArgumentSnafu {
reason: format!(
"Expect two batch to have same numbers of column, found {} and {} columns",
self.batch.len(),
other.batch.len()
)
}
);
if self.batch.is_empty() {
self.batch = other.batch;
self.row_count = other.row_count;
return Ok(());
} else if other.batch.is_empty() {
return Ok(());
}
let dts = {
let max_len = self.batch.len().max(other.batch.len());
let mut dts = Vec::with_capacity(max_len);
for i in 0..max_len {
if let Some(v) = self.batch().get(i)
&& !v.data_type().is_null()
{
dts.push(v.data_type())
} else if let Some(v) = other.batch().get(i)
&& !v.data_type().is_null()
{
dts.push(v.data_type())
} else {
// both are null, so we will push null type
dts.push(datatypes::prelude::ConcreteDataType::null_datatype())
}
}
dts
};
let batch_builders = dts
.iter()
.map(|dt| dt.create_mutable_vector(self.row_count() + other.row_count()))
.collect_vec();
let mut result = vec![];
let self_row_count = self.row_count();
let other_row_count = other.row_count();
for (idx, mut builder) in batch_builders.into_iter().enumerate() {
builder
.extend_slice_of(self.batch()[idx].as_ref(), 0, self_row_count)
.context(DataTypeSnafu {
msg: "Failed to extend vector",
})?;
builder
.extend_slice_of(other.batch()[idx].as_ref(), 0, other_row_count)
.context(DataTypeSnafu {
msg: "Failed to extend vector",
})?;
result.push(builder.to_vector());
}
self.batch = result;
self.row_count = self_row_count + other_row_count;
Ok(())
}
/// filter the batch with given predicate
pub fn filter(&self, predicate: &BooleanVector) -> Result<Self, EvalError> {
let len = predicate.as_boolean_array().true_count();
let filter_builder = FilterBuilder::new(predicate.as_boolean_array()).optimize();
let filter_pred = filter_builder.build();
let filtered = self
.batch()
.iter()
.map(|col| filter_pred.filter(col.to_arrow_array().as_ref()))
.try_collect::<_, Vec<_>, _>()
.context(ArrowSnafu {
context: "Failed to filter val batches",
})?;
let res_vector = Helper::try_into_vectors(&filtered).context(DataTypeSnafu {
msg: "can't convert arrow array to vector",
})?;
Self::try_new(res_vector, len)
}
}
/// Vector with diff to note the insert and delete
pub(crate) struct VectorDiff {
vector: VectorRef,
diff: Option<VectorRef>,
}
impl From<VectorRef> for VectorDiff {
fn from(vector: VectorRef) -> Self {
Self { vector, diff: None }
}
}
impl VectorDiff {
fn len(&self) -> usize {
self.vector.len()
}
fn try_new(vector: VectorRef, diff: Option<VectorRef>) -> Result<Self, EvalError> {
ensure!(
diff.as_ref().is_none_or(|diff| diff.len() == vector.len()),
InvalidArgumentSnafu {
reason: "Length of vector and diff should be the same"
}
);
Ok(Self { vector, diff })
}
}
impl IntoIterator for VectorDiff {
type Item = (Value, Diff);
type IntoIter = VectorDiffIter;
fn into_iter(self) -> Self::IntoIter {
VectorDiffIter {
vector: self.vector,
diff: self.diff,
idx: 0,
}
}
}
/// iterator for VectorDiff
pub(crate) struct VectorDiffIter {
vector: VectorRef,
diff: Option<VectorRef>,
idx: usize,
}
impl std::iter::Iterator for VectorDiffIter {
type Item = (Value, Diff);
fn next(&mut self) -> Option<Self::Item> {
if self.idx >= self.vector.len() {
return None;
}
let value = self.vector.get(self.idx);
// +1 means insert, -1 means delete, and default to +1 insert when diff is not provided
let diff = if let Some(diff) = self.diff.as_ref() {
if let Ok(diff_at) = diff.get(self.idx).try_into() {
diff_at
} else {
common_telemetry::warn!("Invalid diff value at index {}", self.idx);
return None;
}
} else {
1
};
self.idx += 1;
Some((value, diff))
}
}
-300
View File
@@ -1,300 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Porting Datafusion scalar function to our scalar function to be used in dataflow
use std::sync::Arc;
use arrow::array::RecordBatchOptions;
use bytes::BytesMut;
use common_error::ext::BoxedError;
use common_recordbatch::DfRecordBatch;
use common_telemetry::debug;
use datafusion_physical_expr::PhysicalExpr;
use datatypes::data_type::DataType;
use datatypes::value::Value;
use datatypes::vectors::VectorRef;
use prost::Message;
use snafu::{IntoError, ResultExt};
use substrait::error::{DecodeRelSnafu, EncodeRelSnafu};
use substrait::substrait_proto_df::proto::expression::ScalarFunction;
use crate::error::Error;
use crate::expr::error::{
ArrowSnafu, DatafusionSnafu as EvalDatafusionSnafu, EvalError, ExternalSnafu,
InvalidArgumentSnafu,
};
use crate::expr::{Batch, ScalarExpr};
use crate::repr::RelationDesc;
use crate::transform::{FunctionExtensions, from_scalar_fn_to_df_fn_impl};
/// A way to represent a scalar function that is implemented in Datafusion
#[derive(Debug, Clone)]
pub struct DfScalarFunction {
/// The raw bytes encoded datafusion scalar function
pub(crate) raw_fn: RawDfScalarFn,
// TODO(discord9): directly from datafusion expr
/// The implementation of the function
pub(crate) fn_impl: Arc<dyn PhysicalExpr>,
/// The input schema of the function
pub(crate) df_schema: Arc<datafusion_common::DFSchema>,
}
impl DfScalarFunction {
pub fn new(raw_fn: RawDfScalarFn, fn_impl: Arc<dyn PhysicalExpr>) -> Result<Self, Error> {
Ok(Self {
df_schema: Arc::new(raw_fn.input_schema.to_df_schema()?),
raw_fn,
fn_impl,
})
}
pub async fn try_from_raw_fn(raw_fn: RawDfScalarFn) -> Result<Self, Error> {
Ok(Self {
fn_impl: raw_fn.get_fn_impl().await?,
df_schema: Arc::new(raw_fn.input_schema.to_df_schema()?),
raw_fn,
})
}
/// Evaluate a batch of expressions using input values
pub fn eval_batch(&self, batch: &Batch, exprs: &[ScalarExpr]) -> Result<VectorRef, EvalError> {
let row_count = batch.row_count();
let batch: Vec<_> = exprs
.iter()
.map(|expr| expr.eval_batch(batch))
.collect::<Result<_, _>>()?;
let schema = self.df_schema.inner().clone();
let arrays = batch
.iter()
.map(|array| array.to_arrow_array())
.collect::<Vec<_>>();
let rb = DfRecordBatch::try_new_with_options(schema, arrays, &RecordBatchOptions::new().with_row_count(Some(row_count))).map_err(|err| {
ArrowSnafu {
context:
"Failed to create RecordBatch from values when eval_batch datafusion scalar function",
}
.into_error(err)
})?;
let len = rb.num_rows();
let res = self.fn_impl.evaluate(&rb).context(EvalDatafusionSnafu {
context: "Failed to evaluate datafusion scalar function",
})?;
let res = common_query::columnar_value::ColumnarValue::try_from(&res)
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
let res_vec = res
.try_into_vector(len)
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
Ok(res_vec)
}
/// eval a list of expressions using input values
fn eval_args(values: &[Value], exprs: &[ScalarExpr]) -> Result<Vec<Value>, EvalError> {
exprs
.iter()
.map(|expr| expr.eval(values))
.collect::<Result<_, _>>()
}
// TODO(discord9): add RecordBatch support
pub fn eval(&self, values: &[Value], exprs: &[ScalarExpr]) -> Result<Value, EvalError> {
// first eval exprs to construct values to feed to datafusion
let values: Vec<_> = Self::eval_args(values, exprs)?;
if values.is_empty() {
return InvalidArgumentSnafu {
reason: "values is empty".to_string(),
}
.fail();
}
// TODO(discord9): make cols all array length of one
let mut cols = vec![];
for (idx, typ) in self
.raw_fn
.input_schema
.typ()
.column_types
.iter()
.enumerate()
{
let typ = typ.scalar_type();
let mut array = typ.create_mutable_vector(1);
array.push_value_ref(&values[idx].as_value_ref());
cols.push(array.to_vector().to_arrow_array());
}
let schema = self.df_schema.inner().clone();
let rb = DfRecordBatch::try_new_with_options(
schema,
cols,
&RecordBatchOptions::new().with_row_count(Some(1)),
)
.map_err(|err| {
ArrowSnafu {
context:
"Failed to create RecordBatch from values when eval datafusion scalar function",
}
.into_error(err)
})?;
let res = self.fn_impl.evaluate(&rb).context(EvalDatafusionSnafu {
context: "Failed to evaluate datafusion scalar function",
})?;
let res = common_query::columnar_value::ColumnarValue::try_from(&res)
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
let res_vec = res
.try_into_vector(1)
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
let res_val = res_vec
.try_get(0)
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
Ok(res_val)
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RawDfScalarFn {
/// The raw bytes encoded datafusion scalar function,
/// due to substrait have too many layers of nested struct and `ScalarFunction` 's derive is different
/// for simplicity's sake
/// so we store bytes instead of `ScalarFunction` here
/// but in unit test we will still compare decoded struct(using `f_decoded` field in Debug impl)
pub(crate) f: bytes::BytesMut,
/// The input schema of the function
pub(crate) input_schema: RelationDesc,
/// Extension contains mapping from function reference to function name
pub(crate) extensions: FunctionExtensions,
}
impl std::fmt::Debug for RawDfScalarFn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RawDfScalarFn")
.field("f", &self.f)
.field("f_decoded", &ScalarFunction::decode(&mut self.f.as_ref()))
.field("df_schema", &self.input_schema)
.field("extensions", &self.extensions)
.finish()
}
}
impl RawDfScalarFn {
pub fn from_proto(
f: &substrait::substrait_proto_df::proto::expression::ScalarFunction,
input_schema: RelationDesc,
extensions: FunctionExtensions,
) -> Result<Self, Error> {
let mut buf = BytesMut::new();
f.encode(&mut buf)
.context(EncodeRelSnafu)
.map_err(BoxedError::new)
.context(crate::error::ExternalSnafu)?;
Ok(Self {
f: buf,
input_schema,
extensions,
})
}
async fn get_fn_impl(&self) -> Result<Arc<dyn PhysicalExpr>, Error> {
let f = ScalarFunction::decode(&mut self.f.as_ref())
.context(DecodeRelSnafu)
.map_err(BoxedError::new)
.context(crate::error::ExternalSnafu)?;
debug!("Decoded scalar function: {:?}", f);
let input_schema = &self.input_schema;
let extensions = &self.extensions;
from_scalar_fn_to_df_fn_impl(&f, input_schema, extensions).await
}
}
impl std::cmp::PartialEq for DfScalarFunction {
fn eq(&self, other: &Self) -> bool {
self.raw_fn.eq(&other.raw_fn)
}
}
// can't derive Eq because of Arc<dyn PhysicalExpr> not eq, so implement it manually
impl std::cmp::Eq for DfScalarFunction {}
impl std::cmp::PartialOrd for DfScalarFunction {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl std::cmp::Ord for DfScalarFunction {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.raw_fn.cmp(&other.raw_fn)
}
}
impl std::hash::Hash for DfScalarFunction {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.raw_fn.hash(state);
}
}
#[cfg(test)]
mod test {
use datatypes::prelude::ConcreteDataType;
use substrait::substrait_proto_df::proto::expression::literal::LiteralType;
use substrait::substrait_proto_df::proto::expression::{Literal, RexType};
use substrait::substrait_proto_df::proto::function_argument::ArgType;
use substrait::substrait_proto_df::proto::{Expression, FunctionArgument};
use super::*;
use crate::repr::{ColumnType, RelationType};
#[tokio::test]
async fn test_df_scalar_function() {
let raw_scalar_func = ScalarFunction {
function_reference: 0,
arguments: vec![FunctionArgument {
arg_type: Some(ArgType::Value(Expression {
rex_type: Some(RexType::Literal(Literal {
nullable: false,
type_variation_reference: 0,
literal_type: Some(LiteralType::I64(-1)),
})),
})),
}],
output_type: None,
..Default::default()
};
let input_schema = RelationDesc::try_new(
RelationType::new(vec![ColumnType::new_nullable(
ConcreteDataType::null_datatype(),
)]),
vec!["null_column".to_string()],
)
.unwrap();
let extensions = FunctionExtensions::from_iter(vec![(0, "abs")]);
let raw_fn = RawDfScalarFn::from_proto(&raw_scalar_func, input_schema, extensions).unwrap();
let df_func = DfScalarFunction::try_from_raw_fn(raw_fn).await.unwrap();
assert_eq!(
df_func
.eval(&[Value::Null], &[ScalarExpr::Column(0)])
.unwrap(),
Value::Int64(1)
);
}
}
+4 -28
View File
@@ -31,12 +31,6 @@ use snafu::{Location, Snafu};
#[snafu(visibility(pub))]
#[stack_trace_debug]
pub enum EvalError {
#[snafu(display("Division by zero"))]
DivisionByZero {
#[snafu(implicit)]
location: Location,
},
#[snafu(display("Type mismatch: expected {expected}, actual {actual}"))]
TypeMismatch {
expected: ConcreteDataType,
@@ -84,26 +78,12 @@ pub enum EvalError {
location: Location,
},
#[snafu(display("Optimize error: {reason}"))]
Optimize {
reason: String,
#[snafu(implicit)]
location: Location,
},
#[snafu(display("Overflowed during evaluation"))]
Overflow {
#[snafu(implicit)]
location: Location,
},
#[snafu(display("Incoming data already expired by {} ms", expired_by))]
DataAlreadyExpired {
expired_by: i64,
#[snafu(implicit)]
location: Location,
},
#[snafu(display("Arrow error: {error:?}, context: {context}"))]
Arrow {
#[snafu(source)]
@@ -134,20 +114,16 @@ impl ErrorExt for EvalError {
fn status_code(&self) -> StatusCode {
use EvalError::*;
match self {
DivisionByZero { .. }
| TypeMismatch { .. }
TypeMismatch { .. }
| TryFromValue { .. }
| DataAlreadyExpired { .. }
| InvalidArgument { .. }
| Overflow { .. } => StatusCode::InvalidArguments,
CastValue { source, .. } | DataType { source, .. } => source.status_code(),
Internal { .. }
| Optimize { .. }
| Arrow { .. }
| Datafusion { .. }
| External { .. } => StatusCode::Internal,
Internal { .. } | Arrow { .. } | Datafusion { .. } | External { .. } => {
StatusCode::Internal
}
}
}
File diff suppressed because it is too large Load Diff
-43
View File
@@ -1,43 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! `Id` is used to identify a dataflow component in plan like `Plan::Get{id: Id}`, this could be a source of data for an arrangement.
use serde::{Deserialize, Serialize};
/// Global id's scope is in Current Flow node, and is cross-dataflow
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub enum GlobalId {
/// System namespace.
System(u64),
/// User namespace.
User(u64),
/// Transient namespace.
Transient(u64),
/// Dummy id for query being explained
Explain,
}
/// Local id is used in local scope created by `Plan::Let{id: LocalId, value, body}`
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub struct LocalId(pub(crate) u64);
/// Id is used to identify a dataflow component in plan like `Plan::Get{id: Id}`
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub enum Id {
/// An identifier that refers to a local component of a dataflow.
Local(LocalId),
/// An identifier that refers to a global dataflow.
Global(GlobalId),
}
File diff suppressed because it is too large Load Diff
-36
View File
@@ -1,36 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Describes an aggregation function and it's input expression.
pub(crate) use accum::{Accum, Accumulator};
pub(crate) use func::AggregateFunc;
use crate::expr::ScalarExpr;
mod accum;
mod func;
/// Describes an aggregation expression.
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct AggregateExpr {
/// Names the aggregation function.
pub func: AggregateFunc,
/// An expression which extracts from each row the input to `func`.
/// TODO(discord9): currently unused in render phase(because AccumulablePlan remember each Aggr Expr's input/output column),
/// so it only used in generate KeyValPlan from AggregateExpr
pub expr: ScalarExpr,
/// Should the aggregation be applied only to distinct results in each group.
pub distinct: bool,
}
File diff suppressed because it is too large Load Diff
-303
View File
@@ -1,303 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::HashMap;
use std::sync::OnceLock;
use datatypes::prelude::ConcreteDataType;
use datatypes::value::Value;
use datatypes::vectors::VectorRef;
use serde::{Deserialize, Serialize};
use smallvec::smallvec;
use snafu::OptionExt;
use strum::{EnumIter, IntoEnumIterator};
use crate::error::{Error, InvalidQuerySnafu};
use crate::expr::VectorDiff;
use crate::expr::error::EvalError;
use crate::expr::relation::accum::{Accum, Accumulator};
use crate::expr::signature::{GenericFn, Signature};
use crate::repr::Diff;
/// Aggregate functions that can be applied to a group of rows.
///
/// `Mean` function is deliberately not included as it can be computed from `Sum` and `Count`, whose state can be better managed.
///
/// type of the input and output of the aggregate function:
///
/// `sum(i*)->i64, sum(u*)->u64`
///
/// `count()->i64`
///
/// `min/max(T)->T`
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Hash, EnumIter)]
pub enum AggregateFunc {
MaxInt16,
MaxInt32,
MaxInt64,
MaxUInt16,
MaxUInt32,
MaxUInt64,
MaxFloat32,
MaxFloat64,
MaxBool,
MaxString,
MaxDate,
MaxDateTime,
MaxTimestamp,
MaxTime,
MaxDuration,
MaxInterval,
MinInt16,
MinInt32,
MinInt64,
MinUInt16,
MinUInt32,
MinUInt64,
MinFloat32,
MinFloat64,
MinBool,
MinString,
MinDate,
MinDateTime,
MinTimestamp,
MinTime,
MinDuration,
MinInterval,
SumInt16,
SumInt32,
SumInt64,
SumUInt16,
SumUInt32,
SumUInt64,
SumFloat32,
SumFloat64,
Count,
Any,
All,
}
impl AggregateFunc {
/// if this function is a `max`
pub fn is_max(&self) -> bool {
self.signature().generic_fn == GenericFn::Max
}
/// if this function is a `min`
pub fn is_min(&self) -> bool {
self.signature().generic_fn == GenericFn::Min
}
/// Eval value, diff with accumulator
///
/// Expect self to be accumulable aggregate function, i.e. sum/count
///
/// TODO(discord9): deal with overflow&better accumulator
pub fn eval_diff_accumulable<A, I>(
&self,
accum: A,
value_diffs: I,
) -> Result<(Value, Vec<Value>), EvalError>
where
A: IntoIterator<Item = Value>,
I: IntoIterator<Item = (Value, Diff)>,
{
let mut accum = accum.into_iter().peekable();
let mut accum = if accum.peek().is_none() {
Accum::new_accum(self)?
} else {
Accum::try_from_iter(self, &mut accum)?
};
accum.update_batch(self, value_diffs)?;
let res = accum.eval(self)?;
Ok((res, accum.into_state()))
}
/// return output value and new accumulator state
pub fn eval_batch<A>(
&self,
accum: A,
vector: VectorRef,
diff: Option<VectorRef>,
) -> Result<(Value, Vec<Value>), EvalError>
where
A: IntoIterator<Item = Value>,
{
let mut accum = accum.into_iter().peekable();
let mut accum = if accum.peek().is_none() {
Accum::new_accum(self)?
} else {
Accum::try_from_iter(self, &mut accum)?
};
let vector_diff = VectorDiff::try_new(vector, diff)?;
accum.update_batch(self, vector_diff)?;
let res = accum.eval(self)?;
Ok((res, accum.into_state()))
}
}
/// Generate signature for each aggregate function
macro_rules! generate_signature {
($value:ident,
{ $($user_arm:tt)* },
[ $(
$auto_arm:ident=>($($arg:ident),*)
),*
]
) => {
match $value {
$($user_arm)*,
$(
Self::$auto_arm => gen_one_siginature!($($arg),*),
)*
}
};
}
/// Generate one match arm with optional arguments
macro_rules! gen_one_siginature {
(
$con_type:ident, $generic:ident
) => {
Signature {
input: smallvec![ConcreteDataType::$con_type(), ConcreteDataType::$con_type(),],
output: ConcreteDataType::$con_type(),
generic_fn: GenericFn::$generic,
}
};
(
$in_type:ident, $out_type:ident, $generic:ident
) => {
Signature {
input: smallvec![ConcreteDataType::$in_type()],
output: ConcreteDataType::$out_type(),
generic_fn: GenericFn::$generic,
}
};
}
static SPECIALIZATION: OnceLock<HashMap<(GenericFn, ConcreteDataType), AggregateFunc>> =
OnceLock::new();
impl AggregateFunc {
/// Create a `AggregateFunc` from a string of the function name and given argument type(optional)
/// given an None type will be treated as null type,
/// which in turn for AggregateFunc like `Count` will be treated as any type
pub fn from_str_and_type(
name: &str,
arg_type: Option<ConcreteDataType>,
) -> Result<Self, Error> {
let rule = SPECIALIZATION.get_or_init(|| {
let mut spec = HashMap::new();
for func in Self::iter() {
let sig = func.signature();
spec.insert((sig.generic_fn, sig.input[0].clone()), func);
}
spec
});
let generic_fn = match name {
"max" => GenericFn::Max,
"min" => GenericFn::Min,
"sum" => GenericFn::Sum,
"count" => GenericFn::Count,
"bool_or" => GenericFn::Any,
"bool_and" => GenericFn::All,
_ => {
return InvalidQuerySnafu {
reason: format!("Unknown aggregate function: {}", name),
}
.fail();
}
};
let input_type = if matches!(generic_fn, GenericFn::Count) {
ConcreteDataType::null_datatype()
} else {
arg_type.unwrap_or_else(ConcreteDataType::null_datatype)
};
rule.get(&(generic_fn, input_type.clone()))
.cloned()
.with_context(|| InvalidQuerySnafu {
reason: format!(
"No specialization found for aggregate function {:?} with input type {:?}",
generic_fn, input_type
),
})
}
/// all concrete datatypes with precision types will be returned with largest possible variant
/// as a exception, count have a signature of `null -> i64`, but it's actually `anytype -> i64`
///
/// TODO(discorcd9): fix signature for sum unsign -> u64 sum signed -> i64
pub fn signature(&self) -> Signature {
generate_signature!(self, {
AggregateFunc::Count => Signature {
input: smallvec![ConcreteDataType::null_datatype()],
output: ConcreteDataType::int64_datatype(),
generic_fn: GenericFn::Count,
}
},[
MaxInt16 => (int16_datatype, Max),
MaxInt32 => (int32_datatype, Max),
MaxInt64 => (int64_datatype, Max),
MaxUInt16 => (uint16_datatype, Max),
MaxUInt32 => (uint32_datatype, Max),
MaxUInt64 => (uint64_datatype, Max),
MaxFloat32 => (float32_datatype, Max),
MaxFloat64 => (float64_datatype, Max),
MaxBool => (boolean_datatype, Max),
MaxString => (string_datatype, Max),
MaxDate => (date_datatype, Max),
MaxDateTime => (timestamp_microsecond_datatype, Max),
MaxTimestamp => (timestamp_second_datatype, Max),
MaxTime => (time_second_datatype, Max),
MaxDuration => (duration_second_datatype, Max),
MaxInterval => (interval_year_month_datatype, Max),
MinInt16 => (int16_datatype, Min),
MinInt32 => (int32_datatype, Min),
MinInt64 => (int64_datatype, Min),
MinUInt16 => (uint16_datatype, Min),
MinUInt32 => (uint32_datatype, Min),
MinUInt64 => (uint64_datatype, Min),
MinFloat32 => (float32_datatype, Min),
MinFloat64 => (float64_datatype, Min),
MinBool => (boolean_datatype, Min),
MinString => (string_datatype, Min),
MinDate => (date_datatype, Min),
MinDateTime => (timestamp_microsecond_datatype, Min),
MinTimestamp => (timestamp_second_datatype, Min),
MinTime => (time_second_datatype, Min),
MinDuration => (duration_second_datatype, Min),
MinInterval => (interval_year_month_datatype, Min),
SumInt16 => (int16_datatype, int64_datatype, Sum),
SumInt32 => (int32_datatype, int64_datatype, Sum),
SumInt64 => (int64_datatype, int64_datatype, Sum),
SumUInt16 => (uint16_datatype, uint64_datatype, Sum),
SumUInt32 => (uint32_datatype, uint64_datatype, Sum),
SumUInt64 => (uint64_datatype, uint64_datatype, Sum),
SumFloat32 => (float32_datatype, Sum),
SumFloat64 => (float64_datatype, Sum),
Any => (boolean_datatype, Any),
All => (boolean_datatype, All)
])
}
}
-877
View File
@@ -1,877 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Scalar expressions.
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use arrow::array::{ArrayData, ArrayRef, BooleanArray, make_array};
use arrow::buffer::BooleanBuffer;
use arrow::compute::or_kleene;
use common_error::ext::BoxedError;
use datafusion::physical_expr_common::datum::compare_with_eq;
use datafusion_common::DataFusionError;
use datatypes::prelude::{ConcreteDataType, DataType};
use datatypes::value::Value;
use datatypes::vectors::{BooleanVector, Helper, VectorRef};
use dfir_rs::lattices::cc_traits::Iter;
use itertools::Itertools;
use snafu::{OptionExt, ResultExt, ensure};
use crate::error::{
DatafusionSnafu, Error, InvalidQuerySnafu, UnexpectedSnafu, UnsupportedTemporalFilterSnafu,
};
use crate::expr::error::{
ArrowSnafu, DataTypeSnafu, EvalError, InvalidArgumentSnafu, OptimizeSnafu, TypeMismatchSnafu,
};
use crate::expr::func::{BinaryFunc, UnaryFunc, UnmaterializableFunc, VariadicFunc};
use crate::expr::{Batch, DfScalarFunction};
use crate::repr::ColumnType;
/// A scalar expression with a known type.
#[derive(Ord, PartialOrd, Clone, Debug, Eq, PartialEq, Hash)]
pub struct TypedExpr {
/// The expression.
pub expr: ScalarExpr,
/// The type of the expression.
pub typ: ColumnType,
}
impl TypedExpr {
pub fn new(expr: ScalarExpr, typ: ColumnType) -> Self {
Self { expr, typ }
}
}
/// A scalar expression, which can be evaluated to a value.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ScalarExpr {
/// A column of the input row
Column(usize),
/// A literal value.
/// Extra type info to know original type even when it is null
Literal(Value, ConcreteDataType),
/// A call to an unmaterializable function.
///
/// These functions cannot be evaluated by `ScalarExpr::eval`. They must
/// be transformed away by a higher layer.
CallUnmaterializable(UnmaterializableFunc),
CallUnary {
func: UnaryFunc,
expr: Box<ScalarExpr>,
},
CallBinary {
func: BinaryFunc,
expr1: Box<ScalarExpr>,
expr2: Box<ScalarExpr>,
},
CallVariadic {
func: VariadicFunc,
exprs: Vec<ScalarExpr>,
},
CallDf {
/// invariant: the input args set inside this [`DfScalarFunction`] is
/// always col(0) to col(n-1) where n is the length of `expr`
df_scalar_fn: DfScalarFunction,
exprs: Vec<ScalarExpr>,
},
/// Conditionally evaluated expressions.
///
/// It is important that `then` and `els` only be evaluated if
/// `cond` is true or not, respectively. This is the only way
/// users can guard execution (other logical operator do not
/// short-circuit) and we need to preserve that.
If {
cond: Box<ScalarExpr>,
then: Box<ScalarExpr>,
els: Box<ScalarExpr>,
},
InList {
expr: Box<ScalarExpr>,
list: Vec<ScalarExpr>,
},
}
impl ScalarExpr {
pub fn with_type(self, typ: ColumnType) -> TypedExpr {
TypedExpr::new(self, typ)
}
/// try to determine the type of the expression
pub fn typ(&self, context: &[ColumnType]) -> Result<ColumnType, Error> {
match self {
ScalarExpr::Column(i) => context.get(*i).cloned().ok_or_else(|| {
UnexpectedSnafu {
reason: format!("column index {} out of range of len={}", i, context.len()),
}
.build()
}),
ScalarExpr::Literal(_, typ) => Ok(ColumnType::new_nullable(typ.clone())),
ScalarExpr::CallUnmaterializable(func) => {
Ok(ColumnType::new_nullable(func.signature().output))
}
ScalarExpr::CallUnary { func, .. } => {
Ok(ColumnType::new_nullable(func.signature().output))
}
ScalarExpr::CallBinary { func, .. } => {
Ok(ColumnType::new_nullable(func.signature().output))
}
ScalarExpr::CallVariadic { func, .. } => {
Ok(ColumnType::new_nullable(func.signature().output))
}
ScalarExpr::If { then, .. } => then.typ(context),
ScalarExpr::CallDf { df_scalar_fn, .. } => {
let arrow_typ = df_scalar_fn
.fn_impl
// TODO(discord9): get scheme from args instead?
.data_type(df_scalar_fn.df_schema.as_arrow())
.context({
DatafusionSnafu {
context: "Failed to get data type from datafusion scalar function",
}
})?;
let typ = ConcreteDataType::try_from(&arrow_typ)
.map_err(BoxedError::new)
.context(crate::error::ExternalSnafu)?;
Ok(ColumnType::new_nullable(typ))
}
ScalarExpr::InList { expr, .. } => expr.typ(context),
}
}
}
impl ScalarExpr {
pub fn cast(self, typ: ConcreteDataType) -> Self {
ScalarExpr::CallUnary {
func: UnaryFunc::Cast(typ),
expr: Box::new(self),
}
}
/// apply optimization to the expression, like flatten variadic function
pub fn optimize(&mut self) {
self.flatten_variadic_fn();
}
/// Because Substrait's `And`/`Or` function is binary, but FlowPlan's
/// `And`/`Or` function is variadic, we need to flatten the `And` function if multiple `And`/`Or` functions are nested.
fn flatten_variadic_fn(&mut self) {
if let ScalarExpr::CallVariadic { func, exprs } = self {
let mut new_exprs = vec![];
for expr in std::mem::take(exprs) {
if let ScalarExpr::CallVariadic {
func: inner_func,
exprs: mut inner_exprs,
} = expr
{
if *func == inner_func {
for inner_expr in inner_exprs.iter_mut() {
inner_expr.flatten_variadic_fn();
}
new_exprs.extend(inner_exprs);
}
} else {
new_exprs.push(expr);
}
}
*exprs = new_exprs;
}
}
}
impl ScalarExpr {
/// Call a unary function on this expression.
pub fn call_unary(self, func: UnaryFunc) -> Self {
ScalarExpr::CallUnary {
func,
expr: Box::new(self),
}
}
/// Call a binary function on this expression and another.
pub fn call_binary(self, other: Self, func: BinaryFunc) -> Self {
ScalarExpr::CallBinary {
func,
expr1: Box::new(self),
expr2: Box::new(other),
}
}
pub fn eval_batch(&self, batch: &Batch) -> Result<VectorRef, EvalError> {
match self {
ScalarExpr::Column(i) => Ok(batch.batch()[*i].clone()),
ScalarExpr::Literal(val, dt) => Ok(Helper::try_from_scalar_value(
val.try_to_scalar_value(dt).context(DataTypeSnafu {
msg: "Failed to convert literal to scalar value",
})?,
batch.row_count(),
None,
)
.context(DataTypeSnafu {
msg: "Failed to convert scalar value to vector ref when parsing literal",
})?),
ScalarExpr::CallUnmaterializable(_) => OptimizeSnafu {
reason: "Can't eval unmaterializable function",
}
.fail()?,
ScalarExpr::CallUnary { func, expr } => func.eval_batch(batch, expr),
ScalarExpr::CallBinary { func, expr1, expr2 } => func.eval_batch(batch, expr1, expr2),
ScalarExpr::CallVariadic { func, exprs } => func.eval_batch(batch, exprs),
ScalarExpr::CallDf {
df_scalar_fn,
exprs,
} => df_scalar_fn.eval_batch(batch, exprs),
ScalarExpr::If { cond, then, els } => Self::eval_if_then(batch, cond, then, els),
ScalarExpr::InList { expr, list } => Self::eval_in_list(batch, expr, list),
}
}
fn eval_in_list(
batch: &Batch,
expr: &ScalarExpr,
list: &[ScalarExpr],
) -> Result<VectorRef, EvalError> {
let eval_list = list
.iter()
.map(|e| e.eval_batch(batch))
.collect::<Result<Vec<_>, _>>()?;
let eval_expr = expr.eval_batch(batch)?;
ensure!(
eval_list
.iter()
.all(|v| v.data_type() == eval_expr.data_type()),
TypeMismatchSnafu {
expected: eval_expr.data_type(),
actual: eval_list
.iter()
.find(|v| v.data_type() != eval_expr.data_type())
.map(|v| v.data_type())
.unwrap(),
}
);
let lhs = eval_expr.to_arrow_array();
let found = eval_list
.iter()
.map(|v| v.to_arrow_array())
.try_fold(
BooleanArray::new(BooleanBuffer::new_unset(batch.row_count()), None),
|result, in_list_elem| -> Result<BooleanArray, DataFusionError> {
let rhs = compare_with_eq(&lhs, &in_list_elem, false)?;
Ok(or_kleene(&result, &rhs)?)
},
)
.with_context(|_| crate::expr::error::DatafusionSnafu {
context: "Failed to compare eval_expr with eval_list",
})?;
let res = BooleanVector::from(found);
Ok(Arc::new(res))
}
/// NOTE: this if then eval impl assume all given expr are pure, and will not change the state of the world
/// since it will evaluate both then and else branch and filter the result
fn eval_if_then(
batch: &Batch,
cond: &ScalarExpr,
then: &ScalarExpr,
els: &ScalarExpr,
) -> Result<VectorRef, EvalError> {
let conds = cond.eval_batch(batch)?;
let bool_conds = conds
.as_any()
.downcast_ref::<BooleanVector>()
.context({
TypeMismatchSnafu {
expected: ConcreteDataType::boolean_datatype(),
actual: conds.data_type(),
}
})?
.as_boolean_array();
let indices = bool_conds
.into_iter()
.enumerate()
.map(|(idx, b)| {
(
match b {
Some(true) => 0, // then branch vector
Some(false) => 1, // else branch vector
None => 2, // null vector
},
idx,
)
})
.collect_vec();
let then_input_vec = then.eval_batch(batch)?;
let else_input_vec = els.eval_batch(batch)?;
ensure!(
then_input_vec.data_type() == else_input_vec.data_type(),
TypeMismatchSnafu {
expected: then_input_vec.data_type(),
actual: else_input_vec.data_type(),
}
);
ensure!(
then_input_vec.len() == else_input_vec.len()
&& then_input_vec.len() == batch.row_count(),
InvalidArgumentSnafu {
reason: format!(
"then and else branch must have the same length(found {} and {}) which equals input batch's row count(which is {})",
then_input_vec.len(),
else_input_vec.len(),
batch.row_count()
)
}
);
fn new_nulls(dt: &arrow_schema::DataType, len: usize) -> ArrayRef {
let data = ArrayData::new_null(dt, len);
make_array(data)
}
let null_input_vec = new_nulls(
&then_input_vec.data_type().as_arrow_type(),
batch.row_count(),
);
let interleave_values = vec![
then_input_vec.to_arrow_array(),
else_input_vec.to_arrow_array(),
null_input_vec,
];
let int_ref: Vec<_> = interleave_values.iter().map(|x| x.as_ref()).collect();
let interleave_res_arr =
arrow::compute::interleave(&int_ref, &indices).context(ArrowSnafu {
context: "Failed to interleave output arrays",
})?;
let res_vec = Helper::try_into_vector(interleave_res_arr).context(DataTypeSnafu {
msg: "Failed to convert arrow array to vector",
})?;
Ok(res_vec)
}
/// Eval this expression with the given values.
///
/// TODO(discord9): add tests to make sure `eval_batch` is the same as `eval` in
/// most cases
pub fn eval(&self, values: &[Value]) -> Result<Value, EvalError> {
match self {
ScalarExpr::Column(index) => Ok(values[*index].clone()),
ScalarExpr::Literal(row_res, _ty) => Ok(row_res.clone()),
ScalarExpr::CallUnmaterializable(_) => OptimizeSnafu {
reason: "Can't eval unmaterializable function".to_string(),
}
.fail(),
ScalarExpr::CallUnary { func, expr } => func.eval(values, expr),
ScalarExpr::CallBinary { func, expr1, expr2 } => func.eval(values, expr1, expr2),
ScalarExpr::CallVariadic { func, exprs } => func.eval(values, exprs),
ScalarExpr::If { cond, then, els } => match cond.eval(values) {
Ok(Value::Boolean(true)) => then.eval(values),
Ok(Value::Boolean(false)) => els.eval(values),
_ => InvalidArgumentSnafu {
reason: "if condition must be boolean".to_string(),
}
.fail(),
},
ScalarExpr::CallDf {
df_scalar_fn,
exprs,
} => df_scalar_fn.eval(values, exprs),
ScalarExpr::InList { expr, list } => {
let eval_expr = expr.eval(values)?;
let eval_list = list
.iter()
.map(|v| v.eval(values))
.collect::<Result<Vec<_>, _>>()?;
let found = eval_list.iter().any(|item| *item == eval_expr);
Ok(Value::Boolean(found))
}
}
}
/// Rewrites column indices with their value in `permutation`.
///
/// This method is applicable even when `permutation` is not a
/// strict permutation, and it only needs to have entries for
/// each column referenced in `self`.
pub fn permute(&mut self, permutation: &[usize]) -> Result<(), Error> {
// check first so that we don't end up with a partially permuted expression
ensure!(
self.get_all_ref_columns()
.into_iter()
.all(|i| i < permutation.len()),
InvalidQuerySnafu {
reason: format!(
"permutation {:?} is not a valid permutation for expression {:?}",
permutation, self
),
}
);
self.visit_mut_post_nolimit(&mut |e| {
if let ScalarExpr::Column(old_i) = e {
*old_i = permutation[*old_i];
}
Ok(())
})?;
Ok(())
}
/// Rewrites column indices with their value in `permutation`.
///
/// This method is applicable even when `permutation` is not a
/// strict permutation, and it only needs to have entries for
/// each column referenced in `self`.
pub fn permute_map(&mut self, permutation: &BTreeMap<usize, usize>) -> Result<(), Error> {
// check first so that we don't end up with a partially permuted expression
ensure!(
self.get_all_ref_columns()
.is_subset(&permutation.keys().cloned().collect()),
InvalidQuerySnafu {
reason: format!(
"permutation {:?} is not a valid permutation for expression {:?}",
permutation, self
),
}
);
self.visit_mut_post_nolimit(&mut |e| {
if let ScalarExpr::Column(old_i) = e {
*old_i = permutation[old_i];
}
Ok(())
})
}
/// Returns the set of columns that are referenced by `self`.
pub fn get_all_ref_columns(&self) -> BTreeSet<usize> {
let mut support = BTreeSet::new();
self.visit_post_nolimit(&mut |e| {
if let ScalarExpr::Column(i) = e {
support.insert(*i);
}
Ok(())
})
.unwrap();
support
}
/// Return true if the expression is a column reference.
pub fn is_column(&self) -> bool {
matches!(self, ScalarExpr::Column(_))
}
/// Cast the expression to a column reference if it is one.
pub fn as_column(&self) -> Option<usize> {
if let ScalarExpr::Column(i) = self {
Some(*i)
} else {
None
}
}
/// Cast the expression to a literal if it is one.
pub fn as_literal(&self) -> Option<Value> {
if let ScalarExpr::Literal(lit, _column_type) = self {
Some(lit.clone())
} else {
None
}
}
/// Return true if the expression is a literal.
pub fn is_literal(&self) -> bool {
matches!(self, ScalarExpr::Literal(..))
}
/// Return true if the expression is a literal true.
pub fn is_literal_true(&self) -> bool {
Some(Value::Boolean(true)) == self.as_literal()
}
/// Return true if the expression is a literal false.
pub fn is_literal_false(&self) -> bool {
Some(Value::Boolean(false)) == self.as_literal()
}
/// Return true if the expression is a literal null.
pub fn is_literal_null(&self) -> bool {
Some(Value::Null) == self.as_literal()
}
/// Build a literal null
pub fn literal_null() -> Self {
ScalarExpr::Literal(Value::Null, ConcreteDataType::null_datatype())
}
/// Build a literal from value and type
pub fn literal(res: Value, typ: ConcreteDataType) -> Self {
ScalarExpr::Literal(res, typ)
}
/// Build a literal false
pub fn literal_false() -> Self {
ScalarExpr::Literal(Value::Boolean(false), ConcreteDataType::boolean_datatype())
}
/// Build a literal true
pub fn literal_true() -> Self {
ScalarExpr::Literal(Value::Boolean(true), ConcreteDataType::boolean_datatype())
}
}
impl ScalarExpr {
/// visit post-order without stack call limit, but may cause stack overflow
fn visit_post_nolimit<F>(&self, f: &mut F) -> Result<(), EvalError>
where
F: FnMut(&Self) -> Result<(), EvalError>,
{
self.visit_children(|e| e.visit_post_nolimit(f))?;
f(self)
}
fn visit_children<F>(&self, mut f: F) -> Result<(), EvalError>
where
F: FnMut(&Self) -> Result<(), EvalError>,
{
match self {
ScalarExpr::Column(_)
| ScalarExpr::Literal(_, _)
| ScalarExpr::CallUnmaterializable(_) => Ok(()),
ScalarExpr::CallUnary { expr, .. } => f(expr),
ScalarExpr::CallBinary { expr1, expr2, .. } => {
f(expr1)?;
f(expr2)
}
ScalarExpr::CallVariadic { exprs, .. } => {
for expr in exprs {
f(expr)?;
}
Ok(())
}
ScalarExpr::If { cond, then, els } => {
f(cond)?;
f(then)?;
f(els)
}
ScalarExpr::CallDf {
df_scalar_fn: _,
exprs,
} => {
for expr in exprs {
f(expr)?;
}
Ok(())
}
ScalarExpr::InList { expr, list } => {
f(expr)?;
for item in list {
f(item)?;
}
Ok(())
}
}
}
fn visit_mut_post_nolimit<F>(&mut self, f: &mut F) -> Result<(), Error>
where
F: FnMut(&mut Self) -> Result<(), Error>,
{
self.visit_mut_children(|e: &mut Self| e.visit_mut_post_nolimit(f))?;
f(self)
}
fn visit_mut_children<F>(&mut self, mut f: F) -> Result<(), Error>
where
F: FnMut(&mut Self) -> Result<(), Error>,
{
match self {
ScalarExpr::Column(_)
| ScalarExpr::Literal(_, _)
| ScalarExpr::CallUnmaterializable(_) => Ok(()),
ScalarExpr::CallUnary { expr, .. } => f(expr),
ScalarExpr::CallBinary { expr1, expr2, .. } => {
f(expr1)?;
f(expr2)
}
ScalarExpr::CallVariadic { exprs, .. } => {
for expr in exprs {
f(expr)?;
}
Ok(())
}
ScalarExpr::If { cond, then, els } => {
f(cond)?;
f(then)?;
f(els)
}
ScalarExpr::CallDf {
df_scalar_fn: _,
exprs,
} => {
for expr in exprs {
f(expr)?;
}
Ok(())
}
ScalarExpr::InList { expr, list } => {
f(expr)?;
for item in list {
f(item)?;
}
Ok(())
}
}
}
}
impl ScalarExpr {
/// if expr contains function `Now`
pub fn contains_temporal(&self) -> bool {
let mut contains = false;
self.visit_post_nolimit(&mut |e| {
if let ScalarExpr::CallUnmaterializable(UnmaterializableFunc::Now) = e {
contains = true;
}
Ok(())
})
.unwrap();
contains
}
/// extract lower or upper bound of `Now` for expr, where `lower bound <= expr < upper bound`
///
/// returned bool indicates whether the bound is upper bound:
///
/// false for lower bound, true for upper bound
/// TODO(discord9): allow simple transform like `now() + a < b` to `now() < b - a`
pub fn extract_bound(&self) -> Result<(Option<Self>, Option<Self>), Error> {
let unsupported_err = |msg: &str| {
UnsupportedTemporalFilterSnafu {
reason: msg.to_string(),
}
.fail()
};
let Self::CallBinary {
mut func,
mut expr1,
mut expr2,
} = self.clone()
else {
return unsupported_err("Not a binary expression");
};
// TODO(discord9): support simple transform like `now() + a < b` to `now() < b - a`
let expr1_is_now = *expr1 == ScalarExpr::CallUnmaterializable(UnmaterializableFunc::Now);
let expr2_is_now = *expr2 == ScalarExpr::CallUnmaterializable(UnmaterializableFunc::Now);
if !(expr1_is_now ^ expr2_is_now) {
return unsupported_err("None of the sides of the comparison is `now()`");
}
if expr2_is_now {
std::mem::swap(&mut expr1, &mut expr2);
func = BinaryFunc::reverse_compare(&func)?;
}
let step = |expr: ScalarExpr| expr.call_unary(UnaryFunc::StepTimestamp);
match func {
// now == expr2 -> now <= expr2 && now < expr2 + 1
BinaryFunc::Eq => Ok((Some(*expr2.clone()), Some(step(*expr2)))),
// now < expr2 -> now < expr2
BinaryFunc::Lt => Ok((None, Some(*expr2))),
// now <= expr2 -> now < expr2 + 1
BinaryFunc::Lte => Ok((None, Some(step(*expr2)))),
// now > expr2 -> now >= expr2 + 1
BinaryFunc::Gt => Ok((Some(step(*expr2)), None)),
// now >= expr2 -> now >= expr2
BinaryFunc::Gte => Ok((Some(*expr2), None)),
_ => unreachable!("Already checked"),
}
}
}
#[cfg(test)]
mod test {
use datatypes::vectors::{Int32Vector, Vector};
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_extract_bound() {
let test_list: [(ScalarExpr, Result<_, EvalError>); 5] = [
// col(0) == now
(
ScalarExpr::CallBinary {
func: BinaryFunc::Eq,
expr1: Box::new(ScalarExpr::CallUnmaterializable(UnmaterializableFunc::Now)),
expr2: Box::new(ScalarExpr::Column(0)),
},
Ok((
Some(ScalarExpr::Column(0)),
Some(ScalarExpr::CallUnary {
func: UnaryFunc::StepTimestamp,
expr: Box::new(ScalarExpr::Column(0)),
}),
)),
),
// now < col(0)
(
ScalarExpr::CallBinary {
func: BinaryFunc::Lt,
expr1: Box::new(ScalarExpr::CallUnmaterializable(UnmaterializableFunc::Now)),
expr2: Box::new(ScalarExpr::Column(0)),
},
Ok((None, Some(ScalarExpr::Column(0)))),
),
// now <= col(0)
(
ScalarExpr::CallBinary {
func: BinaryFunc::Lte,
expr1: Box::new(ScalarExpr::CallUnmaterializable(UnmaterializableFunc::Now)),
expr2: Box::new(ScalarExpr::Column(0)),
},
Ok((
None,
Some(ScalarExpr::CallUnary {
func: UnaryFunc::StepTimestamp,
expr: Box::new(ScalarExpr::Column(0)),
}),
)),
),
// now > col(0) -> now >= col(0) + 1
(
ScalarExpr::CallBinary {
func: BinaryFunc::Gt,
expr1: Box::new(ScalarExpr::CallUnmaterializable(UnmaterializableFunc::Now)),
expr2: Box::new(ScalarExpr::Column(0)),
},
Ok((
Some(ScalarExpr::CallUnary {
func: UnaryFunc::StepTimestamp,
expr: Box::new(ScalarExpr::Column(0)),
}),
None,
)),
),
// now >= col(0)
(
ScalarExpr::CallBinary {
func: BinaryFunc::Gte,
expr1: Box::new(ScalarExpr::CallUnmaterializable(UnmaterializableFunc::Now)),
expr2: Box::new(ScalarExpr::Column(0)),
},
Ok((Some(ScalarExpr::Column(0)), None)),
),
];
for (expr, expected) in test_list.into_iter() {
let actual = expr.extract_bound();
// EvalError is not Eq, so we need to compare the error message
match (actual, expected) {
(Ok(l), Ok(r)) => assert_eq!(l, r),
(l, r) => panic!("expected: {:?}, actual: {:?}", r, l),
}
}
}
#[test]
fn test_bad_permute() {
let mut expr = ScalarExpr::Column(4);
let permutation = vec![1, 2, 3];
let res = expr.permute(&permutation);
assert!(matches!(res, Err(Error::InvalidQuery { .. })));
let mut expr = ScalarExpr::Column(0);
let permute_map = BTreeMap::from([(1, 2), (3, 4)]);
let res = expr.permute_map(&permute_map);
assert!(matches!(res, Err(Error::InvalidQuery { .. })));
}
#[test]
fn test_eval_batch_if_then() {
// TODO(discord9): add more tests
{
let expr = ScalarExpr::If {
cond: Box::new(ScalarExpr::Column(0).call_binary(
ScalarExpr::literal(Value::from(0), ConcreteDataType::int32_datatype()),
BinaryFunc::Eq,
)),
then: Box::new(ScalarExpr::literal(
Value::from(42),
ConcreteDataType::int32_datatype(),
)),
els: Box::new(ScalarExpr::literal(
Value::from(37),
ConcreteDataType::int32_datatype(),
)),
};
let raw = vec![
None,
Some(0),
Some(1),
None,
None,
Some(0),
Some(0),
Some(1),
Some(1),
];
let raw_len = raw.len();
let vectors = vec![Int32Vector::from(raw).slice(0, raw_len)];
let batch = Batch::try_new(vectors, raw_len).unwrap();
let expected = Int32Vector::from(vec![
None,
Some(42),
Some(37),
None,
None,
Some(42),
Some(42),
Some(37),
Some(37),
])
.slice(0, raw_len);
assert_eq!(expr.eval_batch(&batch).unwrap(), expected);
let raw = vec![Some(0)];
let raw_len = raw.len();
let vectors = vec![Int32Vector::from(raw).slice(0, raw_len)];
let batch = Batch::try_new(vectors, raw_len).unwrap();
let expected = Int32Vector::from(vec![Some(42)]).slice(0, raw_len);
assert_eq!(expr.eval_batch(&batch).unwrap(), expected);
let raw: Vec<Option<i32>> = vec![];
let raw_len = raw.len();
let vectors = vec![Int32Vector::from(raw).slice(0, raw_len)];
let batch = Batch::try_new(vectors, raw_len).unwrap();
let expected = Int32Vector::from(vec![]).slice(0, raw_len);
assert_eq!(expr.eval_batch(&batch).unwrap(), expected);
}
}
}
-70
View File
@@ -1,70 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Function signature, useful for type checking and function resolution.
use datatypes::data_type::ConcreteDataType;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
/// Function signature
///
/// TODO(discord9): use `common_query::signature::Signature` crate
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, Hash)]
pub struct Signature {
/// the input types, usually not great than two input arg
pub input: SmallVec<[ConcreteDataType; 2]>,
/// Output type
pub output: ConcreteDataType,
/// Generic function
pub generic_fn: GenericFn,
}
/// Generic function category
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, Hash)]
pub enum GenericFn {
// aggregate func
Max,
Min,
Sum,
Count,
Any,
All,
// unary func
Not,
IsNull,
IsTrue,
IsFalse,
StepTimestamp,
Cast,
// binary func
Eq,
NotEq,
Lt,
Lte,
Gt,
Gte,
Add,
Sub,
Mul,
Div,
Mod,
// variadic func
And,
Or,
// unmaterized func
Now,
CurrentSchema,
TumbleWindow,
}
-348
View File
@@ -1,348 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! This module contains utility functions for expressions.
use std::cmp::Ordering;
use std::collections::BTreeMap;
use datatypes::value::Value;
use snafu::{OptionExt, ensure};
use crate::Result;
use crate::error::UnexpectedSnafu;
use crate::expr::ScalarExpr;
use crate::plan::TypedPlan;
/// Find lower bound for time `current` in given `plan` for the time window expr.
///
/// i.e. for time window expr being `date_bin(INTERVAL '5 minutes', ts) as time_window` and `current="2021-07-01 00:01:01.000"`,
/// return `Some("2021-07-01 00:00:00.000")`
///
/// if `plan` doesn't contain a `TIME INDEX` column, return `None`
pub fn find_plan_time_window_expr_lower_bound(
plan: &TypedPlan,
current: common_time::Timestamp,
) -> Result<Option<common_time::Timestamp>> {
let typ = plan.schema.typ();
let Some(mut time_index) = typ.time_index else {
return Ok(None);
};
let mut cur_plan = plan;
let mut expr_time_index;
loop {
// follow upward and find deepest time index expr that is not a column ref
expr_time_index = Some(cur_plan.plan.get_nth_expr(time_index).context(
UnexpectedSnafu {
reason: "Failed to find time index expr",
},
)?);
if let Some(ScalarExpr::Column(i)) = expr_time_index {
time_index = i;
} else {
break;
}
if let Some(input) = cur_plan.plan.get_first_input_plan() {
cur_plan = input;
} else {
break;
}
}
let expr_time_index = expr_time_index.context(UnexpectedSnafu {
reason: "Failed to find time index expr",
})?;
let ts_col = expr_time_index
.get_all_ref_columns()
.first()
.cloned()
.context(UnexpectedSnafu {
reason: "Failed to find time index column",
})?;
find_time_window_lower_bound(&expr_time_index, ts_col, current)
}
/// Find the lower bound of time window in given `expr` and `current` timestamp.
///
/// i.e. for `current="2021-07-01 00:01:01.000"` and `expr=date_bin(INTERVAL '5 minutes', ts) as time_window` and `ts_col=ts`,
/// return `Some("2021-07-01 00:00:00.000")` since it's the lower bound
/// of current time window given the current timestamp
///
/// if return None, meaning this time window have no lower bound
pub fn find_time_window_lower_bound(
expr: &ScalarExpr,
ts_col_idx: usize,
current: common_time::Timestamp,
) -> Result<Option<common_time::Timestamp>> {
let all_ref_columns = expr.get_all_ref_columns();
ensure!(
all_ref_columns.contains(&ts_col_idx),
UnexpectedSnafu {
reason: format!(
"Expected column {} to be referenced in expression {expr:?}",
ts_col_idx
),
}
);
ensure!(
all_ref_columns.len() == 1,
UnexpectedSnafu {
reason: format!(
"Expect only one column to be referenced in expression {expr:?}, found {all_ref_columns:?}"
),
}
);
let permute_map = BTreeMap::from([(ts_col_idx, 0usize)]);
let mut rewrote_expr = expr.clone();
rewrote_expr.permute_map(&permute_map)?;
fn eval_to_timestamp(expr: &ScalarExpr, values: &[Value]) -> Result<common_time::Timestamp> {
let val = expr.eval(values)?;
if let Value::Timestamp(ts) = val {
Ok(ts)
} else {
UnexpectedSnafu {
reason: format!("Expected timestamp in expression {expr:?} but got {val:?}"),
}
.fail()?
}
}
let cur_time_window = eval_to_timestamp(&rewrote_expr, &[current.into()])?;
// search to find the lower bound
let mut offset: i64 = 1;
let lower_bound;
let mut upper_bound = Some(current);
// first expontial probe to found a range for binary search
loop {
let Some(next_val) = current.value().checked_sub(offset) else {
// no lower bound
return Ok(None);
};
let prev_time_probe = common_time::Timestamp::new(next_val, current.unit());
let prev_time_window = eval_to_timestamp(&rewrote_expr, &[prev_time_probe.into()])?;
match prev_time_window.cmp(&cur_time_window) {
Ordering::Less => {
lower_bound = Some(prev_time_probe);
break;
}
Ordering::Equal => {
upper_bound = Some(prev_time_probe);
}
Ordering::Greater => {
UnexpectedSnafu {
reason: format!(
"Unsupported time window expression {rewrote_expr:?}, expect monotonic increasing for time window expression {expr:?}"
),
}
.fail()?
}
}
let Some(new_offset) = offset.checked_mul(2) else {
// no lower bound
return Ok(None);
};
offset = new_offset;
}
// binary search for the lower bound
ensure!(
lower_bound.map(|v| v.unit()) == upper_bound.map(|v| v.unit()),
UnexpectedSnafu {
reason: format!(
" unit mismatch for time window expression {expr:?}, found {lower_bound:?} and {upper_bound:?}"
),
}
);
let output_unit = lower_bound.expect("should have lower bound").unit();
let mut low = lower_bound.expect("should have lower bound").value();
let mut high = upper_bound.expect("should have upper bound").value();
while low < high {
let mid = (low + high) / 2;
let mid_probe = common_time::Timestamp::new(mid, output_unit);
let mid_time_window = eval_to_timestamp(&rewrote_expr, &[mid_probe.into()])?;
match mid_time_window.cmp(&cur_time_window) {
Ordering::Less => low = mid + 1,
Ordering::Equal => high = mid,
Ordering::Greater => UnexpectedSnafu {
reason: format!("Binary search failed for time window expression {expr:?}"),
}
.fail()?,
}
}
let final_lower_bound_for_time_window = common_time::Timestamp::new(low, output_unit);
Ok(Some(final_lower_bound_for_time_window))
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
use crate::plan::{Plan, TypedPlan};
use crate::test_utils::{create_test_ctx, create_test_query_engine, sql_to_substrait};
#[tokio::test]
async fn test_plan_time_window_lower_bound() {
let testcases = [
// no time index
(
"SELECT date_bin('5 minutes', ts) FROM numbers_with_ts;",
"2021-07-01 00:01:01.000",
None,
),
// time index
(
"SELECT date_bin('5 minutes', ts) as time_window FROM numbers_with_ts GROUP BY time_window;",
"2021-07-01 00:01:01.000",
Some("2021-07-01 00:00:00.000"),
),
// time index with other fields
(
"SELECT sum(number) as sum_up, date_bin('5 minutes', ts) as time_window FROM numbers_with_ts GROUP BY time_window;",
"2021-07-01 00:01:01.000",
Some("2021-07-01 00:00:00.000"),
),
// time index with other pks
(
"SELECT number, date_bin('5 minutes', ts) as time_window FROM numbers_with_ts GROUP BY time_window, number;",
"2021-07-01 00:01:01.000",
Some("2021-07-01 00:00:00.000"),
),
];
let engine = create_test_query_engine();
for (sql, current, expected) in &testcases {
let plan = sql_to_substrait(engine.clone(), sql).await;
let mut ctx = create_test_ctx();
let flow_plan = TypedPlan::from_substrait_plan(&mut ctx, &plan)
.await
.unwrap();
let current = common_time::Timestamp::from_str(current, None).unwrap();
let expected =
expected.map(|expected| common_time::Timestamp::from_str(expected, None).unwrap());
assert_eq!(
find_plan_time_window_expr_lower_bound(&flow_plan, current).unwrap(),
expected
);
}
}
#[tokio::test]
async fn test_timewindow_lower_bound() {
let testcases = [
(
("'5 minutes'", "ts", Some("2021-07-01 00:00:00.000")),
"2021-07-01 00:01:01.000",
"2021-07-01 00:00:00.000",
),
(
("'5 minutes'", "ts", None),
"2021-07-01 00:01:01.000",
"2021-07-01 00:00:00.000",
),
(
("'5 minutes'", "ts", None),
"2021-07-01 00:00:00.000",
"2021-07-01 00:00:00.000",
),
// test edge cases
(
("'5 minutes'", "ts", None),
"2021-07-01 00:05:00.000",
"2021-07-01 00:05:00.000",
),
(
("'5 minutes'", "ts", None),
"2021-07-01 00:04:59.999",
"2021-07-01 00:00:00.000",
),
(
("'5 minutes'", "ts", None),
"2021-07-01 00:04:59.999999999",
"2021-07-01 00:00:00.000",
),
(
("'5 minutes'", "ts", None),
"2021-07-01 00:04:59.999999999999",
"2021-07-01 00:00:00.000",
),
(
("'5 minutes'", "ts", None),
"2021-07-01 00:04:59.999999999999999",
"2021-07-01 00:00:00.000",
),
];
let engine = create_test_query_engine();
for (args, current, expected) in testcases {
let sql = if let Some(origin) = args.2 {
format!(
"SELECT date_bin({}, {}, '{origin}') FROM numbers_with_ts;",
args.0, args.1
)
} else {
format!(
"SELECT date_bin({}, {}) FROM numbers_with_ts;",
args.0, args.1
)
};
let plan = sql_to_substrait(engine.clone(), &sql).await;
let mut ctx = create_test_ctx();
let flow_plan = TypedPlan::from_substrait_plan(&mut ctx, &plan)
.await
.unwrap();
let expr = {
let mfp = flow_plan.plan;
let Plan::Mfp { mfp, .. } = mfp else {
unreachable!()
};
mfp.expressions[0].clone()
};
let current = common_time::Timestamp::from_str(current, None).unwrap();
let res = find_time_window_lower_bound(&expr, 1, current).unwrap();
let expected = Some(common_time::Timestamp::from_str(expected, None).unwrap());
assert_eq!(res, expected);
}
}
}
+2 -9
View File
@@ -12,9 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! This crate manage dataflow in Greptime, including adapter, expr, plan, repr and utils.
//! It can transform substrait plan into it's own plan and execute it.
//! It also contains definition of expression, adapter and plan, and internal state management.
//! Flow execution and flownode services.
#![allow(dead_code)]
#![warn(clippy::too_many_lines)]
@@ -25,17 +23,14 @@
// allow unused for now because it should be use later
mod adapter;
pub(crate) mod batching_mode;
mod compute;
mod df_optimizer;
pub(crate) mod engine;
pub mod error;
mod expr;
pub mod heartbeat;
mod metrics;
mod plan;
mod repr;
mod server;
mod transform;
mod utils;
#[cfg(test)]
@@ -46,8 +41,6 @@ pub use adapter::{FlowConfig, FlowStreamingEngineRef, StreamingEngine};
pub use batching_mode::frontend_client::{FrontendClient, GrpcQueryHandlerWithBoxedError};
pub(crate) use engine::{CreateFlowArgs, FlowId, TableName};
pub use error::{Error, Result};
pub use server::{
FlownodeBuilder, FlownodeInstance, FlownodeServer, FlownodeServiceBuilder, FrontendInvoker,
};
pub use server::{FlownodeBuilder, FlownodeInstance, FlownodeServer, FlownodeServiceBuilder};
pub use crate::adapter::FlownodeOptions;
-270
View File
@@ -1,270 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! This module contain basic definition for dataflow's plan
//! that can be translate to hydro dataflow
mod join;
mod reduce;
use std::collections::BTreeSet;
use crate::error::Error;
use crate::expr::{GlobalId, Id, LocalId, MapFilterProject, SafeMfpPlan, ScalarExpr, TypedExpr};
use crate::plan::join::JoinPlan;
pub(crate) use crate::plan::reduce::{AccumulablePlan, AggrWithIndex, KeyValPlan, ReducePlan};
use crate::repr::{DiffRow, RelationDesc};
/// A plan for a dataflow component. But with type to indicate the output type of the relation.
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct TypedPlan {
/// output type of the relation
pub schema: RelationDesc,
/// The untyped plan.
pub plan: Plan,
}
impl TypedPlan {
/// directly apply a mfp to the plan
pub fn mfp(self, mfp: SafeMfpPlan) -> Result<Self, Error> {
let new_type = self.schema.apply_mfp(&mfp)?;
let mfp = mfp.mfp;
let plan = match self.plan {
Plan::Mfp {
input,
mfp: old_mfp,
} => Plan::Mfp {
input,
mfp: MapFilterProject::compose(old_mfp, mfp)?,
},
_ => Plan::Mfp {
input: Box::new(self),
mfp,
},
};
Ok(TypedPlan {
schema: new_type,
plan,
})
}
/// project the plan to the given expressions
pub fn projection(self, exprs: Vec<TypedExpr>) -> Result<Self, Error> {
let input_arity = self.schema.typ.column_types.len();
let output_arity = exprs.len();
let (exprs, _expr_typs): (Vec<_>, Vec<_>) = exprs
.into_iter()
.map(|TypedExpr { expr, typ }| (expr, typ))
.unzip();
let mfp = MapFilterProject::new(input_arity)
.map(exprs)?
.project(input_arity..input_arity + output_arity)?
.into_safe();
let out_typ = self.schema.apply_mfp(&mfp)?;
let mfp = mfp.mfp;
// special case for mfp to compose when the plan is already mfp
let plan = match self.plan {
Plan::Mfp {
input,
mfp: old_mfp,
} => Plan::Mfp {
input,
mfp: MapFilterProject::compose(old_mfp, mfp)?,
},
_ => Plan::Mfp {
input: Box::new(self),
mfp,
},
};
Ok(TypedPlan {
schema: out_typ,
plan,
})
}
/// Add a new filter to the plan, will filter out the records that do not satisfy the filter
pub fn filter(self, filter: TypedExpr) -> Result<Self, Error> {
let typ = self.schema.clone();
let plan = match self.plan {
Plan::Mfp {
input,
mfp: old_mfp,
} => Plan::Mfp {
input,
mfp: old_mfp.filter(vec![filter.expr])?,
},
_ => Plan::Mfp {
input: Box::new(self),
mfp: MapFilterProject::new(typ.typ.column_types.len()).filter(vec![filter.expr])?,
},
};
Ok(TypedPlan { schema: typ, plan })
}
}
/// TODO(discord9): support `TableFunc`(by define FlatMap that map 1 to n)
/// Plan describe how to transform data in dataflow
///
/// This can be considered as a physical plan in dataflow, which describe how to transform data in a streaming manner.
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum Plan {
/// A constant collection of rows.
Constant { rows: Vec<DiffRow> },
/// Get CDC data from an source, be it external reference to an existing source or an internal
/// reference to a `Let` identifier
Get { id: Id },
/// Create a temporary collection from given `value`, and make this bind only available
/// in scope of `body`
///
/// Similar to this rust code snippet:
/// ```rust, ignore
/// {
/// let id = value;
/// body
/// }
Let {
id: LocalId,
value: Box<TypedPlan>,
body: Box<TypedPlan>,
},
/// Map, Filter, and Project operators. Chained together.
Mfp {
/// The input collection.
input: Box<TypedPlan>,
/// Linear operator to apply to each record.
mfp: MapFilterProject,
},
/// Reduce operator, aggregation by key assembled from KeyValPlan
Reduce {
/// The input collection.
input: Box<TypedPlan>,
/// A plan for changing input records into key, value pairs.
key_val_plan: KeyValPlan,
/// A plan for performing the reduce.
///
/// The implementation of reduction has several different strategies based
/// on the properties of the reduction, and the input itself.
reduce_plan: ReducePlan,
},
/// A multiway relational equijoin, with fused map, filter, and projection.
///
/// This stage performs a multiway join among `inputs`, using the equality
/// constraints expressed in `plan`. The plan also describes the implementation
/// strategy we will use, and any pushed down per-record work.
Join {
/// An ordered list of inputs that will be joined.
inputs: Vec<TypedPlan>,
/// Detailed information about the implementation of the join.
///
/// This includes information about the implementation strategy, but also
/// any map, filter, project work that we might follow the join with, but
/// potentially pushed down into the implementation of the join.
plan: JoinPlan,
},
/// Adds the contents of the input collections.
///
/// Importantly, this is *multiset* union, so the multiplicities of records will
/// add. This is in contrast to *set* union, where the multiplicities would be
/// capped at one. A set union can be formed with `Union` followed by `Reduce`
/// implementing the "distinct" operator.
Union {
/// The input collections
inputs: Vec<TypedPlan>,
/// Whether to consolidate the output, e.g., cancel negated records.
consolidate_output: bool,
},
}
impl Plan {
pub fn with_types(self, schema: RelationDesc) -> TypedPlan {
TypedPlan { schema, plan: self }
}
}
impl Plan {
/// Get nth expr using column ref
pub fn get_nth_expr(&self, n: usize) -> Option<ScalarExpr> {
match self {
Self::Mfp { mfp, .. } => mfp.get_nth_expr(n),
Self::Reduce { key_val_plan, .. } => key_val_plan.get_nth_expr(n),
_ => None,
}
}
/// Get the first input plan if exists
pub fn get_first_input_plan(&self) -> Option<&TypedPlan> {
match self {
Plan::Let { value, .. } => Some(value),
Plan::Mfp { input, .. } => Some(input),
Plan::Reduce { input, .. } => Some(input),
Plan::Join { inputs, .. } => inputs.first(),
Plan::Union { inputs, .. } => inputs.first(),
_ => None,
}
}
/// Get mutable ref to the first input plan if exists
pub fn get_mut_first_input_plan(&mut self) -> Option<&mut TypedPlan> {
match self {
Plan::Let { value, .. } => Some(value),
Plan::Mfp { input, .. } => Some(input),
Plan::Reduce { input, .. } => Some(input),
Plan::Join { inputs, .. } => inputs.first_mut(),
Plan::Union { inputs, .. } => inputs.first_mut(),
_ => None,
}
}
/// Find all the used collection in the plan
pub fn find_used_collection(&self) -> BTreeSet<GlobalId> {
fn recur_find_use(plan: &Plan, used: &mut BTreeSet<GlobalId>) {
match plan {
Plan::Get { id } => {
match id {
Id::Local(_) => (),
Id::Global(g) => {
used.insert(*g);
}
};
}
Plan::Let { value, body, .. } => {
recur_find_use(&value.plan, used);
recur_find_use(&body.plan, used);
}
Plan::Mfp { input, .. } => {
recur_find_use(&input.plan, used);
}
Plan::Reduce { input, .. } => {
recur_find_use(&input.plan, used);
}
Plan::Join { inputs, .. } => {
for input in inputs {
recur_find_use(&input.plan, used);
}
}
Plan::Union { inputs, .. } => {
for input in inputs {
recur_find_use(&input.plan, used);
}
}
_ => {}
}
}
let mut ret = Default::default();
recur_find_use(self, &mut ret);
ret
}
}
-76
View File
@@ -1,76 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::expr::ScalarExpr;
use crate::plan::SafeMfpPlan;
/// TODO(discord9): consider impl more join strategies
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum JoinPlan {
Linear(LinearJoinPlan),
}
/// Determine if a given row should stay in the output. And apply a map filter project before output the row
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct JoinFilter {
/// each element in the outer vector will check if each expr in itself can be eval to same value
/// if not, the row will be filtered out. Useful for equi-join(join based on equality of some columns)
pub ready_equivalences: Vec<Vec<ScalarExpr>>,
/// Apply a map filter project before output the row
pub before: SafeMfpPlan,
}
/// A plan for the execution of a linear join.
///
/// A linear join is a sequence of stages, each of which introduces
/// a new collection. Each stage is represented by a [LinearStagePlan].
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct LinearJoinPlan {
/// The source relation from which we start the join.
pub source_relation: usize,
/// The arrangement to use for the source relation, if any
pub source_key: Option<Vec<ScalarExpr>>,
/// An initial closure to apply before any stages.
///
/// Values of `None` indicate the identity closure.
pub initial_closure: Option<JoinFilter>,
/// A *sequence* of stages to apply one after the other.
pub stage_plans: Vec<LinearStagePlan>,
/// A concluding filter to apply after the last stage.
///
/// Values of `None` indicate the identity closure.
pub final_closure: Option<JoinFilter>,
}
/// A plan for the execution of one stage of a linear join.
///
/// Each stage is a binary join between the current accumulated
/// join results, and a new collection. The former is referred to
/// as the "stream" and the latter the "lookup".
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct LinearStagePlan {
/// The index of the relation into which we will look up.
pub lookup_relation: usize,
/// The key expressions to use for the stream relation.
pub stream_key: Vec<ScalarExpr>,
/// Columns to retain from the stream relation.
/// These columns are those that are not redundant with `stream_key`,
/// and cannot be read out of the key component of an arrangement.
pub stream_thinning: Vec<usize>,
/// The key expressions to use for the lookup relation.
pub lookup_key: Vec<ScalarExpr>,
/// The closure to apply to the concatenation of the key columns,
/// the stream value columns, and the lookup value columns.
pub closure: JoinFilter,
}
-87
View File
@@ -1,87 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::expr::{AggregateExpr, SafeMfpPlan, ScalarExpr};
/// Describe how to extract key-value pair from a `Row`
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct KeyValPlan {
/// Extract key from row
pub key_plan: SafeMfpPlan,
/// Extract value from row
pub val_plan: SafeMfpPlan,
}
impl KeyValPlan {
/// Get nth expr using column ref
pub fn get_nth_expr(&self, n: usize) -> Option<ScalarExpr> {
self.key_plan.get_nth_expr(n).or_else(|| {
self.val_plan
.get_nth_expr(n - self.key_plan.projection.len())
})
}
}
/// TODO(discord9): def&impl of Hierarchical aggregates(for min/max with support to deletion) and
/// basic aggregates(for other aggregate functions) and mixed aggregate
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum ReducePlan {
/// Plan for not computing any aggregations, just determining the set of
/// distinct keys.
Distinct,
/// Plan for computing only accumulable aggregations.
/// Including simple functions like `sum`, `count`, `min/max`(without deletion)
Accumulable(AccumulablePlan),
}
/// Accumulable plan for the execution of a reduction.
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct AccumulablePlan {
/// All of the aggregations we were asked to compute, stored
/// in order.
pub full_aggrs: Vec<AggregateExpr>,
/// All of the non-distinct accumulable aggregates.
/// Each element represents:
/// (index of aggr output, index of value among inputs, aggr expr)
/// These will all be rendered together in one dataflow fragment.
///
/// Invariant: the output index is the index of the aggregation in `full_aggrs`
/// which means output index is always smaller than the length of `full_aggrs`
pub simple_aggrs: Vec<AggrWithIndex>,
/// Same as `simple_aggrs` but for all of the `DISTINCT` accumulable aggregations.
pub distinct_aggrs: Vec<AggrWithIndex>,
}
/// Invariant: the output index is the index of the aggregation in `full_aggrs`
/// which means output index is always smaller than the length of `full_aggrs`
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct AggrWithIndex {
/// aggregation expression
pub expr: AggregateExpr,
/// index of aggr input among input row
pub input_idx: usize,
/// index of aggr output among output row
pub output_idx: usize,
}
impl AggrWithIndex {
/// Create a new `AggrWithIndex`
pub fn new(expr: AggregateExpr, input_idx: usize, output_idx: usize) -> Self {
Self {
expr,
input_idx,
output_idx,
}
}
}
+1 -1
View File
@@ -57,7 +57,7 @@ pub const BROADCAST_CAP: usize = 1024;
/// The maximum capacity of the send buffer, to prevent the buffer from growing too large
pub const SEND_BUF_CAP: usize = BROADCAST_CAP * 2;
/// Flow worker will try to at least accumulate this many rows before processing them(if one second haven't passed)
/// Batch size used by the batching execution path.
pub const BATCH_SIZE: usize = 32 * 16384;
/// Convert a value that is or can be converted to Datetime to internal timestamp
+2 -95
View File
@@ -15,12 +15,10 @@
use datafusion_common::DFSchema;
use datatypes::data_type::DataType;
use datatypes::prelude::ConcreteDataType;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use snafu::{OptionExt, ResultExt, ensure};
use snafu::{ResultExt, ensure};
use crate::error::{DatafusionSnafu, InternalSnafu, InvalidQuerySnafu, Result, UnexpectedSnafu};
use crate::expr::{SafeMfpPlan, ScalarExpr};
use crate::error::{DatafusionSnafu, InternalSnafu, InvalidQuerySnafu, Result};
/// a set of column indices that are "keys" for the collection.
#[derive(Default, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Hash)]
@@ -96,73 +94,6 @@ impl RelationType {
self
}
/// Trying to apply a mpf on current types, will return a new RelationType
/// with the new types, will also try to preserve keys&time index information
/// if the old key&time index columns are preserve in given mfp
///
/// i.e. old column of size 3, with a mfp's
///
/// project = `[2, 1]`,
///
/// the old key = `[1]`, old time index = `[2]`,
///
/// then new key=`[1]`, new time index=`[0]`
///
/// note that this function will remove empty keys like key=`[]` will be removed
pub fn apply_mfp(&self, mfp: &SafeMfpPlan) -> Result<Self> {
let mfp = &mfp.mfp;
let mut all_types = self.column_types.clone();
for expr in &mfp.expressions {
let expr_typ = expr.typ(&self.column_types)?;
all_types.push(expr_typ);
}
let all_types = all_types;
let mfp_out_types = mfp
.projection
.iter()
.map(|i| {
all_types.get(*i).cloned().with_context(|| UnexpectedSnafu {
reason: format!(
"MFP index out of bound, len is {}, but the index is {}",
all_types.len(),
*i
),
})
})
.try_collect()?;
let old_to_new_col = mfp.get_old_to_new_mapping();
// since it's just a mfp, we also try to preserve keys&time index information, if they survive mfp transform
let keys = self
.keys
.iter()
.filter_map(|key| {
key.column_indices
.iter()
.map(|old| old_to_new_col.get(old).cloned())
.collect::<Option<Vec<_>>>()
// remove empty keys
.and_then(|v| if v.is_empty() { None } else { Some(v) })
.map(Key::from)
})
.collect_vec();
let time_index = self
.time_index
.and_then(|old| old_to_new_col.get(&old).cloned());
let auto_columns = self
.auto_columns
.iter()
.filter_map(|old| old_to_new_col.get(old).cloned())
.collect_vec();
Ok(Self {
column_types: mfp_out_types,
keys,
time_index,
auto_columns,
})
}
/// Constructs a `RelationType` representing the relation with no columns and
/// no keys.
pub fn empty() -> Self {
@@ -372,30 +303,6 @@ impl RelationDesc {
context: format!("Error when converting to DFSchema: {:?}", arrow_schema),
})
}
/// apply mfp, and also project col names for the projected columns
pub fn apply_mfp(&self, mfp: &SafeMfpPlan) -> Result<Self> {
// TODO(discord9): find a way to deduce name at best effect
let names = {
let mfp = &mfp.mfp;
let mut names = self.names.clone();
for expr in &mfp.expressions {
if let ScalarExpr::Column(i) = expr {
names.push(self.names.get(*i).cloned().flatten());
} else {
names.push(None);
}
}
mfp.projection
.iter()
.map(|i| names.get(*i).cloned().flatten())
.collect_vec()
};
Ok(Self {
typ: self.typ.apply_mfp(mfp)?,
names,
})
}
}
impl RelationDesc {
+59 -363
View File
@@ -18,52 +18,34 @@ use std::net::SocketAddr;
use std::sync::Arc;
use api::v1::flow::DirtyWindowRequests;
use api::v1::{RowDeleteRequests, RowInsertRequests};
use cache::{PARTITION_INFO_CACHE_NAME, TABLE_FLOWNODE_SET_CACHE_NAME, TABLE_ROUTE_CACHE_NAME};
use catalog::CatalogManagerRef;
use common_base::Plugins;
use common_datasource::object_store::LocalFileAccess;
use common_error::ext::BoxedError;
use common_meta::cache::{LayeredCacheRegistryRef, TableFlownodeSetCacheRef, TableRouteCacheRef};
use common_meta::key::TableMetadataManagerRef;
use common_meta::key::flow::FlowMetadataManagerRef;
use common_meta::kv_backend::KvBackendRef;
use common_meta::node_manager::{Flownode, NodeManagerRef};
use common_meta::procedure_executor::ProcedureExecutorRef;
use common_query::Output;
use common_runtime::JoinHandle;
use common_telemetry::tracing::info;
use common_meta::node_manager::Flownode;
use futures::TryStreamExt;
use greptime_proto::v1::flow::{FlowRequest, FlowResponse, InsertRequests, flow_server};
use itertools::Itertools;
use operator::delete::Deleter;
use operator::insert::Inserter;
use operator::statement::StatementExecutor;
use partition::cache::PartitionInfoCacheRef;
use partition::manager::PartitionRuleManager;
use query::{QueryEngine, QueryEngineFactory};
use query::QueryEngineFactory;
use servers::add_service;
use servers::grpc::builder::GrpcServerBuilder;
use servers::grpc::{GrpcServer, GrpcServerConfig};
use servers::http::HttpServerBuilder;
use servers::metrics_handler::MetricsHandler;
use servers::server::{ServerHandler, ServerHandlers};
use session::context::QueryContextRef;
use snafu::{OptionExt, ResultExt};
use tokio::sync::{Mutex, broadcast, oneshot};
use snafu::ResultExt;
use tonic::codec::CompressionEncoding;
use tonic::{Request, Response, Status};
use crate::adapter::flownode_impl::{FlowDualEngine, FlowDualEngineRef};
use crate::adapter::{FlowStreamingEngineRef, create_worker};
use crate::batching_mode::engine::BatchingEngine;
use crate::error::{
CacheRequiredSnafu, DatafusionSnafu, ExternalSnafu, ListFlowsSnafu, ParseAddrSnafu,
ShutdownServerSnafu, StartServerSnafu, UnexpectedSnafu, to_status_with_last_err,
DatafusionSnafu, ExternalSnafu, ListFlowsSnafu, ParseAddrSnafu, ShutdownServerSnafu,
StartServerSnafu, to_status_with_last_err,
};
use crate::heartbeat::HeartbeatTask;
use crate::metrics::{METRIC_FLOW_PROCESSING_TIME, METRIC_FLOW_ROWS};
use crate::transform::register_function_to_query_engine;
use crate::utils::{SizeReportSender, StateReportHandler};
use crate::{Error, FlownodeOptions, FrontendClient, StreamingEngine};
@@ -163,100 +145,15 @@ pub struct FlownodeServer {
/// this struct mostly useful for construct/start and stop the
/// flow node server
struct FlownodeServerInner {
/// worker shutdown signal, not to be confused with server_shutdown_tx
worker_shutdown_tx: Mutex<broadcast::Sender<()>>,
/// server shutdown signal for shutdown grpc server
server_shutdown_tx: Mutex<broadcast::Sender<()>>,
/// streaming task handler
streaming_task_handler: Mutex<Option<JoinHandle<()>>>,
/// state report task handler
state_report_task_handler: Mutex<Option<JoinHandle<()>>>,
flow_service: FlowService,
}
impl FlownodeServer {
pub fn new(flow_service: FlowService) -> Self {
let (tx, _rx) = broadcast::channel::<()>(1);
let (server_tx, _server_rx) = broadcast::channel::<()>(1);
Self {
inner: Arc::new(FlownodeServerInner {
flow_service,
worker_shutdown_tx: Mutex::new(tx),
server_shutdown_tx: Mutex::new(server_tx),
streaming_task_handler: Mutex::new(None),
state_report_task_handler: Mutex::new(None),
}),
inner: Arc::new(FlownodeServerInner { flow_service }),
}
}
/// Start the background task for streaming computation.
///
/// Should be called only after heartbeat is establish, hence can get cluster info
async fn start_workers(&self) -> Result<(), Error> {
let manager_ref = self.inner.flow_service.dual_engine.clone();
let mut state_report_task_handler = self.inner.state_report_task_handler.lock().await;
let started_state_report_task = state_report_task_handler.is_none();
if state_report_task_handler.is_none() {
*state_report_task_handler = manager_ref.clone().start_state_report_task().await;
}
drop(state_report_task_handler);
let handle = manager_ref
.streaming_engine()
.run_background(Some(self.inner.worker_shutdown_tx.lock().await.subscribe()));
self.inner
.streaming_task_handler
.lock()
.await
.replace(handle);
if let Err(err) = self
.inner
.flow_service
.dual_engine
.start_flow_consistent_check_task()
.await
{
self.rollback_started_workers(started_state_report_task)
.await;
return Err(err);
}
Ok(())
}
async fn rollback_started_workers(&self, abort_state_report_task: bool) {
let tx = self.inner.worker_shutdown_tx.lock().await;
if tx.send(()).is_err() {
info!("Receiver dropped, the flow node server has already shutdown");
}
drop(tx);
if let Some(handle) = self.inner.streaming_task_handler.lock().await.take() {
handle.abort();
}
if abort_state_report_task
&& let Some(handle) = self.inner.state_report_task_handler.lock().await.take()
{
handle.abort();
}
}
/// Stop the background task for streaming computation.
async fn stop_workers(&self) -> Result<(), Error> {
let tx = self.inner.worker_shutdown_tx.lock().await;
if tx.send(()).is_err() {
info!("Receiver dropped, the flow node server has already shutdown");
}
// Keep state_report_task_handler alive across worker restarts.
// Dropping it here would permanently lose the report channel receiver.
self.inner
.flow_service
.dual_engine
.stop_flow_consistent_check_task()
.await?;
Ok(())
}
}
impl FlownodeServer {
@@ -274,6 +171,8 @@ pub struct FlownodeInstance {
flownode_server: FlownodeServer,
services: ServerHandlers,
heartbeat_task: Option<HeartbeatTask>,
state_report_task: Option<common_runtime::JoinHandle<()>>,
consistent_check_task_started: bool,
}
impl FlownodeInstance {
@@ -282,25 +181,57 @@ impl FlownodeInstance {
task.start().await?;
}
self.flownode_server.start_workers().await?;
let engine = self.flow_engine();
// The state-report task owns the only report receiver, so keep it alive
// across an ordinary stop/start cycle of this instance.
if self.state_report_task.is_none() {
self.state_report_task = engine.clone().start_state_report_task().await;
}
if let Err(err) = engine.start_flow_consistent_check_task().await {
self.rollback_background_tasks().await;
return Err(err);
}
self.consistent_check_task_started = true;
self.services.start_all().await.context(StartServerSnafu)?;
if let Err(err) = self.services.start_all().await.context(StartServerSnafu) {
self.rollback_background_tasks().await;
return Err(err);
}
Ok(())
}
pub async fn shutdown(&mut self) -> Result<(), Error> {
self.services
let services_result = self
.services
.shutdown_all()
.await
.context(ShutdownServerSnafu)?;
.context(ShutdownServerSnafu);
let tasks_result = self.stop_background_tasks().await;
self.flownode_server.stop_workers().await?;
services_result?;
tasks_result?;
Ok(())
}
async fn stop_background_tasks(&mut self) -> Result<(), Error> {
let check_result = if self.consistent_check_task_started {
self.consistent_check_task_started = false;
self.flow_engine().stop_flow_consistent_check_task().await
} else {
Ok(())
};
if let Some(task) = &self.heartbeat_task {
task.shutdown();
}
Ok(())
check_result
}
async fn rollback_background_tasks(&mut self) {
if let Err(err) = self.stop_background_tasks().await {
common_telemetry::error!(err; "Failed to roll back flownode background tasks");
}
}
pub fn flownode_server(&self) -> &FlownodeServer {
@@ -401,10 +332,7 @@ impl FlownodeBuilder {
.context(DatafusionSnafu {
context: "Failed to build query engine",
})?;
let manager = Arc::new(
self.build_manager(query_engine_factory.query_engine())
.await?,
);
let manager = Arc::new(self.build_manager(query_engine_factory.query_engine()));
let batching = Arc::new(BatchingEngine::new(
self.frontend_client.clone(),
query_engine_factory.query_engine(),
@@ -432,46 +360,19 @@ impl FlownodeBuilder {
flownode_server: server,
services: ServerHandlers::default(),
heartbeat_task,
state_report_task: None,
consistent_check_task_started: false,
};
Ok(instance)
}
/// build [`FlowWorkerManager`], note this doesn't take ownership of `self`,
/// nor does it actually start running the worker.
async fn build_manager(
&mut self,
query_engine: Arc<dyn QueryEngine>,
) -> Result<StreamingEngine, Error> {
let table_meta = self.table_meta.clone();
register_function_to_query_engine(&query_engine);
let num_workers = self.opts.flow.num_workers;
let node_id = self.opts.node_id.map(|id| id as u32);
let mut man = StreamingEngine::new(node_id, query_engine, table_meta);
for worker_id in 0..num_workers {
let (tx, rx) = oneshot::channel();
let _handle = std::thread::Builder::new()
.name(format!("flow-worker-{}", worker_id))
.spawn(move || {
let (handle, mut worker) = create_worker();
let _ = tx.send(handle);
info!("Flow Worker started in new thread");
worker.run();
});
let worker_handle = rx.await.map_err(|e| {
UnexpectedSnafu {
reason: format!("Failed to receive worker handle: {}", e),
}
.build()
})?;
man.add_worker_handle(worker_handle);
}
info!("Flow Node Manager started");
Ok(man)
fn build_manager(&self, query_engine: Arc<dyn query::QueryEngine>) -> StreamingEngine {
StreamingEngine::new(
self.opts.node_id.map(|id| id as u32),
query_engine,
self.table_meta.clone(),
self.frontend_client.clone(),
)
}
}
@@ -552,134 +453,6 @@ impl<'a> FlownodeServiceBuilder<'a> {
}
}
/// Basically a tiny frontend that communicates with datanode, different from [`FrontendClient`] which
/// connect to a real frontend instead, this is used for flow's streaming engine. And is for simple query.
///
/// For heavy query use [`FrontendClient`] which offload computation to frontend, lifting the load from flownode
#[derive(Clone)]
pub struct FrontendInvoker {
inserter: Arc<Inserter>,
deleter: Arc<Deleter>,
statement_executor: Arc<StatementExecutor>,
}
impl FrontendInvoker {
pub fn new(
inserter: Arc<Inserter>,
deleter: Arc<Deleter>,
statement_executor: Arc<StatementExecutor>,
) -> Self {
Self {
inserter,
deleter,
statement_executor,
}
}
pub async fn build_from(
flow_streaming_engine: FlowStreamingEngineRef,
catalog_manager: CatalogManagerRef,
kv_backend: KvBackendRef,
layered_cache_registry: LayeredCacheRegistryRef,
procedure_executor: ProcedureExecutorRef,
node_manager: NodeManagerRef,
origin_frontend_addr: String,
) -> Result<FrontendInvoker, Error> {
let table_route_cache: TableRouteCacheRef =
layered_cache_registry.get().context(CacheRequiredSnafu {
name: TABLE_ROUTE_CACHE_NAME,
})?;
let partition_info_cache: PartitionInfoCacheRef =
layered_cache_registry.get().context(CacheRequiredSnafu {
name: PARTITION_INFO_CACHE_NAME,
})?;
let partition_manager = Arc::new(PartitionRuleManager::new(
kv_backend.clone(),
table_route_cache.clone(),
partition_info_cache.clone(),
));
let table_flownode_cache: TableFlownodeSetCacheRef =
layered_cache_registry.get().context(CacheRequiredSnafu {
name: TABLE_FLOWNODE_SET_CACHE_NAME,
})?;
// TODO(auto_create_table): flow sink tables are created through a controlled
// `CREATE FLOW` path, not client writes, so they are intentionally exempt from
// the frontend's global auto-create switch. Revisit if flow should honor it.
let inserter = Arc::new(Inserter::new(
catalog_manager.clone(),
partition_manager.clone(),
node_manager.clone(),
table_flownode_cache,
true,
));
let deleter = Arc::new(Deleter::new(
catalog_manager.clone(),
partition_manager.clone(),
node_manager.clone(),
));
let query_engine = flow_streaming_engine.query_engine.clone();
let statement_executor = Arc::new(StatementExecutor::new(
catalog_manager.clone(),
query_engine.clone(),
procedure_executor.clone(),
kv_backend.clone(),
layered_cache_registry.clone(),
inserter.clone(),
partition_manager,
None,
origin_frontend_addr,
LocalFileAccess::Disabled,
));
let invoker = FrontendInvoker::new(inserter, deleter, statement_executor);
Ok(invoker)
}
}
impl FrontendInvoker {
pub async fn row_inserts(
&self,
requests: RowInsertRequests,
ctx: QueryContextRef,
) -> common_frontend::error::Result<Output> {
let _timer = METRIC_FLOW_PROCESSING_TIME
.with_label_values(&["output_insert"])
.start_timer();
self.inserter
.handle_row_inserts(requests, ctx, &self.statement_executor, false, false)
.await
.map_err(BoxedError::new)
.context(common_frontend::error::ExternalSnafu)
}
pub async fn row_deletes(
&self,
requests: RowDeleteRequests,
ctx: QueryContextRef,
) -> common_frontend::error::Result<Output> {
let _timer = METRIC_FLOW_PROCESSING_TIME
.with_label_values(&["output_delete"])
.start_timer();
self.deleter
.handle_row_deletes(requests, ctx)
.await
.map_err(BoxedError::new)
.context(common_frontend::error::ExternalSnafu)
}
pub fn statement_executor(&self) -> Arc<StatementExecutor> {
self.statement_executor.clone()
}
}
/// get all flow ids in this flownode
pub(crate) async fn get_all_flow_ids(
flow_metadata_manager: &FlowMetadataManagerRef,
@@ -722,25 +495,16 @@ pub(crate) async fn get_all_flow_ids(
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::time::Duration;
use api::v1::HealthCheckRequest;
use api::v1::health_check_client::HealthCheckClient;
use api::v1::meta::Role;
use catalog::memory::new_memory_catalog_manager;
use common_base::Plugins;
use common_meta::key::TableMetadataManager;
use common_meta::key::flow::FlowMetadataManager;
use common_meta::kv_backend::memory::MemoryKvBackend;
use meta_client::client::MetaClient;
use query::options::QueryOptions;
use servers::grpc::GRPC_SERVER;
use super::*;
use crate::adapter::flownode_impl::FlowDualEngine;
use crate::batching_mode::BatchingModeOptions;
use crate::batching_mode::engine::BatchingEngine;
use crate::utils::SizeReportSender;
async fn new_test_flownode_server() -> (FlownodeServer, SizeReportSender) {
let (frontend_client, _handler) =
@@ -766,13 +530,15 @@ mod tests {
let catalog_manager = new_memory_catalog_manager().unwrap();
let query_engine = crate::test_utils::create_test_query_engine();
let frontend_client = Arc::new(frontend_client);
let streaming_engine = Arc::new(StreamingEngine::new(
node_id,
query_engine.clone(),
table_meta.clone(),
frontend_client.clone(),
));
let batching_engine = Arc::new(BatchingEngine::new(
Arc::new(frontend_client),
frontend_client,
query_engine,
flow_meta.clone(),
table_meta,
@@ -793,74 +559,4 @@ mod tests {
let server = FlownodeServer::new(FlowService::new(dual_engine));
(server, report_sender)
}
#[tokio::test]
async fn test_state_report_handler_survives_worker_restart() {
let (server, report_sender) = new_test_flownode_server().await;
server.start_workers().await.unwrap();
report_sender.query(Duration::from_secs(3)).await.unwrap();
server.stop_workers().await.unwrap();
report_sender.query(Duration::from_secs(3)).await.unwrap();
server.start_workers().await.unwrap();
report_sender.query(Duration::from_secs(3)).await.unwrap();
server.stop_workers().await.unwrap();
}
#[tokio::test]
async fn test_start_workers_rolls_back_on_check_task_start_failure() {
let batching_opts = BatchingModeOptions {
experimental_frontend_scan_timeout: Duration::from_millis(1),
..Default::default()
};
let frontend_client = FrontendClient::from_meta_client(
Arc::new(MetaClient::new(0, Role::Frontend)),
QueryOptions::default(),
batching_opts.clone(),
)
.unwrap();
let (server, _report_sender) =
new_test_flownode_server_with_frontend_client(frontend_client, batching_opts, Some(1))
.await;
server.start_workers().await.unwrap_err();
assert!(server.inner.streaming_task_handler.lock().await.is_none());
assert!(
server
.inner
.state_report_task_handler
.lock()
.await
.is_none()
);
}
#[tokio::test]
async fn test_service_builder_registers_reachable_health_check() {
// Arrange: compose the production gRPC service with an ephemeral local listener.
let (flownode_server, _report_sender) = new_test_flownode_server().await;
let mut opts = FlownodeOptions::default();
opts.grpc.bind_addr = "127.0.0.1:0".to_string();
let mut services = FlownodeServiceBuilder::new(&opts)
.with_default_grpc_server(&flownode_server)
.build()
.unwrap();
services.start_all().await.unwrap();
let addr = services.addr(GRPC_SERVER).unwrap();
// Act: call the shared health handler through the registered production server.
let mut client = HealthCheckClient::connect(format!("http://{addr}"))
.await
.unwrap();
let result = client.health_check(HealthCheckRequest {}).await;
services.shutdown_all().await.unwrap();
// Assert: the service composition exposes a healthy endpoint.
assert!(result.is_ok());
}
}
-66
View File
@@ -22,58 +22,13 @@ use datatypes::schema::Schema;
use datatypes::timestamp::TimestampMillisecond;
use datatypes::vectors::{TimestampMillisecondVectorBuilder, VectorRef};
use itertools::Itertools;
use prost::Message;
use query::QueryEngine;
use query::options::QueryOptions;
use query::parser::QueryLanguageParser;
use query::query_engine::DefaultSerializer;
use session::context::QueryContext;
/// note here we are using the `substrait_proto_df` crate from the `substrait` module and
/// rename it to `substrait_proto`
use substrait::substrait_proto_df as substrait_proto;
use substrait::{DFLogicalSubstraitConvertor, SubstraitPlan};
use substrait_proto::proto;
use table::table::numbers::{NUMBERS_TABLE_NAME, NumbersTable};
use table::test_util::MemTable;
use crate::adapter::FlownodeContext;
use crate::adapter::node_context::IdToNameMap;
use crate::adapter::table_source::test::FlowDummyTableSource;
use crate::df_optimizer::apply_df_optimizer;
use crate::expr::GlobalId;
use crate::transform::register_function_to_query_engine;
pub fn create_test_ctx() -> FlownodeContext {
let mut tri_map = IdToNameMap::new();
{
let gid = GlobalId::User(0);
let name = [
"greptime".to_string(),
"public".to_string(),
"numbers".to_string(),
];
tri_map.insert(Some(name.clone()), Some(1024), gid);
}
{
let gid = GlobalId::User(1);
let name = [
"greptime".to_string(),
"public".to_string(),
"numbers_with_ts".to_string(),
];
tri_map.insert(Some(name.clone()), Some(1025), gid);
}
let dummy_source = FlowDummyTableSource::default();
let mut ctx = FlownodeContext::new(Box::new(dummy_source));
ctx.table_repr = tri_map;
ctx.query_context = Some(Arc::new(QueryContext::with("greptime", "public")));
ctx
}
pub fn create_test_query_engine() -> Arc<dyn QueryEngine> {
let catalog_list = catalog::memory::new_memory_catalog_manager().unwrap();
let req = RegisterTableRequest {
@@ -158,28 +113,7 @@ pub fn create_test_query_engine() -> Arc<dyn QueryEngine> {
);
let engine = factory.query_engine();
register_function_to_query_engine(&engine);
assert_eq!("datafusion", engine.name());
engine
}
pub async fn sql_to_substrait(engine: Arc<dyn QueryEngine>, sql: &str) -> proto::Plan {
// let engine = create_test_query_engine();
let stmt = QueryLanguageParser::parse_sql(sql, &QueryContext::arc()).unwrap();
let plan = engine
.planner()
.plan(&stmt, QueryContext::arc())
.await
.unwrap();
let plan = apply_df_optimizer(plan, &QueryContext::arc())
.await
.unwrap();
// encode then decode so to rely on the impl of conversion from logical plan to substrait plan
let bytes = DFLogicalSubstraitConvertor {}
.encode(&plan, DefaultSerializer)
.unwrap();
proto::Plan::decode(bytes).unwrap()
}
-318
View File
@@ -1,318 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Transform Substrait into execution plan
use std::collections::BTreeMap;
use std::sync::Arc;
use common_function::function::FunctionRef;
use datafusion::arrow::datatypes::{DataType, TimeUnit};
use datafusion::logical_expr::ColumnarValue;
use datafusion_expr::{ScalarFunctionArgs, Signature, Volatility};
use datafusion_substrait::extensions::Extensions;
use query::QueryEngine;
use serde::{Deserialize, Serialize};
/// note here we are using the `substrait_proto_df` crate from the `substrait` module and
/// rename it to `substrait_proto`
use substrait::substrait_proto_df as substrait_proto;
use substrait_proto::proto::extensions::SimpleExtensionDeclaration;
use substrait_proto::proto::extensions::simple_extension_declaration::MappingType;
use crate::adapter::FlownodeContext;
use crate::error::{Error, NotImplementedSnafu};
use crate::expr::{TUMBLE_END, TUMBLE_START};
/// a simple macro to generate a not implemented error
macro_rules! not_impl_err {
($($arg:tt)*) => {
NotImplementedSnafu {
reason: format!($($arg)*),
}.fail()
};
}
/// generate a plan error
macro_rules! plan_err {
($($arg:tt)*) => {
PlanSnafu {
reason: format!($($arg)*),
}.fail()
};
}
mod aggr;
mod expr;
mod literal;
mod plan;
pub(crate) use expr::from_scalar_fn_to_df_fn_impl;
/// In Substrait, a function can be define by an u32 anchor, and the anchor can be mapped to a name
///
/// So in substrait plan, a ref to a function can be a single u32 anchor instead of a full name in string
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FunctionExtensions {
anchor_to_name: BTreeMap<u32, String>,
}
impl FunctionExtensions {
pub fn from_iter(inner: impl IntoIterator<Item = (u32, impl ToString)>) -> Self {
Self {
anchor_to_name: inner.into_iter().map(|(k, s)| (k, s.to_string())).collect(),
}
}
/// Create a new FunctionExtensions from a list of SimpleExtensionDeclaration
pub fn try_from_proto(extensions: &[SimpleExtensionDeclaration]) -> Result<Self, Error> {
let mut anchor_to_name = BTreeMap::new();
for e in extensions {
match &e.mapping_type {
Some(ext) => match ext {
MappingType::ExtensionFunction(ext_f) => {
anchor_to_name.insert(ext_f.function_anchor, ext_f.name.clone());
}
_ => not_impl_err!("Extension type not supported: {ext:?}")?,
},
None => not_impl_err!("Cannot parse empty extension")?,
}
}
Ok(Self { anchor_to_name })
}
/// Get the name of a function by it's anchor
pub fn get(&self, anchor: &u32) -> Option<&String> {
self.anchor_to_name.get(anchor)
}
pub fn to_extensions(&self) -> Extensions {
Extensions {
functions: self
.anchor_to_name
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect(),
..Default::default()
}
}
}
/// register flow-specific functions to the query engine
pub fn register_function_to_query_engine(engine: &Arc<dyn QueryEngine>) {
let tumble_fn = Arc::new(TumbleFunction::new("tumble")) as FunctionRef;
let tumble_start_fn = Arc::new(TumbleFunction::new(TUMBLE_START)) as FunctionRef;
let tumble_end_fn = Arc::new(TumbleFunction::new(TUMBLE_END)) as FunctionRef;
engine.register_scalar_function(tumble_fn.into());
engine.register_scalar_function(tumble_start_fn.into());
engine.register_scalar_function(tumble_end_fn.into());
}
#[derive(Debug)]
pub struct TumbleFunction {
name: String,
signature: Signature,
}
impl TumbleFunction {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
signature: Signature::variadic_any(Volatility::Immutable),
}
}
}
impl std::fmt::Display for TumbleFunction {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.name.to_ascii_uppercase())
}
}
impl common_function::function::Function for TumbleFunction {
fn name(&self) -> &str {
&self.name
}
fn return_type(&self, _: &[DataType]) -> datafusion_common::Result<DataType> {
Ok(DataType::Timestamp(TimeUnit::Millisecond, None))
}
fn signature(&self) -> &Signature {
&self.signature
}
fn invoke_with_args(&self, _: ScalarFunctionArgs) -> datafusion_common::Result<ColumnarValue> {
datafusion_common::not_impl_err!("{}", self.name())
}
}
#[cfg(test)]
mod test {
use std::sync::Arc;
use catalog::RegisterTableRequest;
use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, NUMBERS_TABLE_ID};
use datatypes::data_type::ConcreteDataType as CDT;
use datatypes::prelude::*;
use datatypes::schema::Schema;
use datatypes::timestamp::TimestampMillisecond;
use datatypes::vectors::{TimestampMillisecondVectorBuilder, VectorRef};
use itertools::Itertools;
use prost::Message;
use query::QueryEngine;
use query::options::QueryOptions;
use query::parser::QueryLanguageParser;
use query::query_engine::DefaultSerializer;
use session::context::QueryContext;
use substrait::{DFLogicalSubstraitConvertor, SubstraitPlan};
use substrait_proto::proto;
use table::table::numbers::{NUMBERS_TABLE_NAME, NumbersTable};
use table::test_util::MemTable;
use super::*;
use crate::adapter::node_context::IdToNameMap;
use crate::adapter::table_source::test::FlowDummyTableSource;
use crate::df_optimizer::apply_df_optimizer;
use crate::expr::GlobalId;
pub fn create_test_ctx() -> FlownodeContext {
let mut tri_map = IdToNameMap::new();
// FIXME(discord9): deprecated, use `numbers_with_ts` instead since this table has no timestamp column
{
let gid = GlobalId::User(0);
let name = [
"greptime".to_string(),
"public".to_string(),
"numbers".to_string(),
];
tri_map.insert(Some(name.clone()), Some(1024), gid);
}
{
let gid = GlobalId::User(1);
let name = [
"greptime".to_string(),
"public".to_string(),
"numbers_with_ts".to_string(),
];
tri_map.insert(Some(name.clone()), Some(1025), gid);
}
let dummy_source = FlowDummyTableSource::default();
let mut ctx = FlownodeContext::new(Box::new(dummy_source));
ctx.table_repr = tri_map;
ctx.query_context = Some(Arc::new(QueryContext::with("greptime", "public")));
ctx
}
pub fn create_test_query_engine() -> Arc<dyn QueryEngine> {
let catalog_list = catalog::memory::new_memory_catalog_manager().unwrap();
let req = RegisterTableRequest {
catalog: DEFAULT_CATALOG_NAME.to_string(),
schema: DEFAULT_SCHEMA_NAME.to_string(),
table_name: NUMBERS_TABLE_NAME.to_string(),
table_id: NUMBERS_TABLE_ID,
table: NumbersTable::table(NUMBERS_TABLE_ID),
};
catalog_list.register_table_sync(req).unwrap();
let schema = vec![
datatypes::schema::ColumnSchema::new("number", CDT::uint32_datatype(), false),
datatypes::schema::ColumnSchema::new(
"ts",
CDT::timestamp_millisecond_datatype(),
false,
),
];
let mut columns = vec![];
let numbers = (1..=10).collect_vec();
let column: VectorRef = Arc::new(<u32 as Scalar>::VectorType::from_vec(numbers));
columns.push(column);
let ts = (1..=10).collect_vec();
let mut builder = TimestampMillisecondVectorBuilder::with_capacity(10);
ts.into_iter()
.map(|v| builder.push(Some(TimestampMillisecond::new(v))))
.count();
let column: VectorRef = builder.to_vector_cloned();
columns.push(column);
let schema = Arc::new(Schema::new(schema));
let recordbatch = common_recordbatch::RecordBatch::new(schema, columns).unwrap();
let table = MemTable::table("numbers_with_ts", recordbatch);
let req_with_ts = RegisterTableRequest {
catalog: DEFAULT_CATALOG_NAME.to_string(),
schema: DEFAULT_SCHEMA_NAME.to_string(),
table_name: "numbers_with_ts".to_string(),
table_id: 1024,
table,
};
catalog_list.register_table_sync(req_with_ts).unwrap();
let factory = query::QueryEngineFactory::new(
catalog_list,
None,
None,
None,
None,
false,
QueryOptions::default(),
);
let engine = factory.query_engine();
register_function_to_query_engine(&engine);
assert_eq!("datafusion", engine.name());
engine
}
pub async fn sql_to_substrait(engine: Arc<dyn QueryEngine>, sql: &str) -> proto::Plan {
// let engine = create_test_query_engine();
let stmt = QueryLanguageParser::parse_sql(sql, &QueryContext::arc()).unwrap();
let plan = engine
.planner()
.plan(&stmt, QueryContext::arc())
.await
.unwrap();
let plan = apply_df_optimizer(plan, &QueryContext::arc())
.await
.unwrap();
// encode then decode so to rely on the impl of conversion from logical plan to substrait plan
let bytes = DFLogicalSubstraitConvertor {}
.encode(&plan, DefaultSerializer)
.unwrap();
proto::Plan::decode(bytes).unwrap()
}
/// TODO(discord9): add more illegal sql tests
#[tokio::test]
async fn test_missing_key_check() {
let engine = create_test_query_engine();
let sql = "SELECT avg(number) FROM numbers_with_ts GROUP BY tumble(ts, '1 hour'), number";
let stmt = QueryLanguageParser::parse_sql(sql, &QueryContext::arc()).unwrap();
let plan = engine
.planner()
.plan(&stmt, QueryContext::arc())
.await
.unwrap();
let plan = apply_df_optimizer(plan, &QueryContext::arc()).await;
assert!(plan.is_err());
}
}
-803
View File
@@ -1,803 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use itertools::Itertools;
use snafu::OptionExt;
use substrait_proto::proto;
use substrait_proto::proto::aggregate_function::AggregationInvocation;
use substrait_proto::proto::aggregate_rel::{Grouping, Measure};
use substrait_proto::proto::function_argument::ArgType;
use crate::error::{Error, NotImplementedSnafu, PlanSnafu};
use crate::expr::{
AggregateExpr, AggregateFunc, MapFilterProject, ScalarExpr, TypedExpr, UnaryFunc,
};
use crate::plan::{AccumulablePlan, AggrWithIndex, KeyValPlan, Plan, ReducePlan, TypedPlan};
use crate::repr::{ColumnType, RelationDesc, RelationType};
use crate::transform::{FlownodeContext, FunctionExtensions, substrait_proto};
impl TypedExpr {
/// Allow `deprecated` due to the usage of deprecated grouping_expressions on datafusion to substrait side
#[allow(deprecated)]
async fn from_substrait_agg_grouping(
ctx: &mut FlownodeContext,
grouping_expressions: &[proto::Expression],
groupings: &[Grouping],
typ: &RelationDesc,
extensions: &FunctionExtensions,
) -> Result<Vec<TypedExpr>, Error> {
let _ = ctx;
let mut group_expr = vec![];
match groupings.len() {
1 => {
// handle case when deprecated grouping_expressions is referenced by index is empty
let expressions: Box<dyn Iterator<Item = &proto::Expression> + Send> = if groupings
[0]
.expression_references
.is_empty()
{
Box::new(groupings[0].grouping_expressions.iter())
} else {
if groupings[0]
.expression_references
.iter()
.any(|idx| *idx as usize >= grouping_expressions.len())
{
return PlanSnafu {
reason: format!("Invalid grouping expression reference: {:?} for grouping expr: {:?}",
groupings[0].expression_references,
grouping_expressions
),
}.fail()?;
}
Box::new(
groupings[0]
.expression_references
.iter()
.map(|idx| &grouping_expressions[*idx as usize]),
)
};
for e in expressions {
let x = TypedExpr::from_substrait_rex(e, typ, extensions).await?;
group_expr.push(x);
}
}
_ => {
return not_impl_err!(
"Grouping sets not support yet, use union all with group by instead."
);
}
};
Ok(group_expr)
}
}
impl AggregateExpr {
/// Convert list of `Measure` into Flow's AggregateExpr
///
/// Return both the AggregateExpr and a MapFilterProject that is the final output of the aggregate function
async fn from_substrait_agg_measures(
ctx: &mut FlownodeContext,
measures: &[Measure],
typ: &RelationDesc,
extensions: &FunctionExtensions,
) -> Result<Vec<AggregateExpr>, Error> {
let _ = ctx;
let mut all_aggr_exprs = vec![];
for m in measures {
let filter = match m
.filter
.as_ref()
.map(|fil| TypedExpr::from_substrait_rex(fil, typ, extensions))
{
Some(fut) => Some(fut.await),
None => None,
}
.transpose()?;
let aggr_expr = match &m.measure {
Some(f) => {
let distinct = match f.invocation {
_ if f.invocation == AggregationInvocation::Distinct as i32 => true,
_ if f.invocation == AggregationInvocation::All as i32 => false,
_ => false,
};
AggregateExpr::from_substrait_agg_func(
f, typ, extensions, &filter, // TODO(discord9): impl order_by
&None, distinct,
)
.await?
}
None => {
return not_impl_err!("Aggregate without aggregate function is not supported");
}
};
all_aggr_exprs.extend(aggr_expr);
}
Ok(all_aggr_exprs)
}
/// Convert AggregateFunction into Flow's AggregateExpr
///
/// the returned value is a tuple of AggregateExpr and a optional ScalarExpr that if exist is the final output of the aggregate function
/// since aggr functions like `avg` need to be transform to `sum(x)/cast(count(x) as x_type)`
pub async fn from_substrait_agg_func(
f: &proto::AggregateFunction,
input_schema: &RelationDesc,
extensions: &FunctionExtensions,
filter: &Option<TypedExpr>,
order_by: &Option<Vec<TypedExpr>>,
distinct: bool,
) -> Result<Vec<AggregateExpr>, Error> {
// TODO(discord9): impl filter
let _ = filter;
let _ = order_by;
let mut args = vec![];
for arg in &f.arguments {
let arg_expr = match &arg.arg_type {
Some(ArgType::Value(e)) => {
TypedExpr::from_substrait_rex(e, input_schema, extensions).await
}
_ => not_impl_err!("Aggregated function argument non-Value type not supported"),
}?;
args.push(arg_expr);
}
if args.len() != 1 {
let fn_name = extensions.get(&f.function_reference).cloned();
return not_impl_err!(
"Aggregated function (name={:?}) with multiple arguments is not supported",
fn_name
);
}
let arg = if let Some(first) = args.first() {
first
} else {
return not_impl_err!("Aggregated function without arguments is not supported");
};
let fn_name = extensions
.get(&f.function_reference)
.cloned()
.map(|s| s.to_lowercase());
match fn_name.as_ref().map(|s| s.as_ref()) {
Some(function_name) => {
let func = AggregateFunc::from_str_and_type(
function_name,
Some(arg.typ.scalar_type.clone()),
)?;
let exprs = vec![AggregateExpr {
func,
expr: arg.expr.clone(),
distinct,
}];
Ok(exprs)
}
None => not_impl_err!(
"Aggregated function not found: function anchor = {:?}",
f.function_reference
),
}
}
}
impl KeyValPlan {
/// Generate KeyValPlan from AggregateExpr and group_exprs
///
/// will also change aggregate expr to use column ref if necessary
fn from_substrait_gen_key_val_plan(
aggr_exprs: &mut [AggregateExpr],
group_exprs: &[TypedExpr],
input_arity: usize,
) -> Result<KeyValPlan, Error> {
let group_expr_val = group_exprs
.iter()
.map(|expr| expr.expr.clone())
.collect_vec();
let output_arity = group_expr_val.len();
let key_plan = MapFilterProject::new(input_arity)
.map(group_expr_val)?
.project(input_arity..input_arity + output_arity)?;
// val_plan is extracted from aggr_exprs to give aggr function it's necessary input
// and since aggr func need inputs that is column ref, we just add a prefix mfp to transform any expr that is not into a column ref
let val_plan = {
let need_mfp = aggr_exprs.iter().any(|agg| agg.expr.as_column().is_none());
if need_mfp {
// create mfp from aggr_expr, and modify aggr_expr to use the output column of mfp
let input_exprs = aggr_exprs
.iter_mut()
.enumerate()
.map(|(idx, aggr)| {
let ret = aggr.expr.clone();
aggr.expr = ScalarExpr::Column(idx);
ret
})
.collect_vec();
let aggr_arity = aggr_exprs.len();
MapFilterProject::new(input_arity)
.map(input_exprs)?
.project(input_arity..input_arity + aggr_arity)?
} else {
// simply take all inputs as value
MapFilterProject::new(input_arity)
}
};
Ok(KeyValPlan {
key_plan: key_plan.into_safe(),
val_plan: val_plan.into_safe(),
})
}
}
/// find out the column that should be time index in group exprs(which is all columns that should be keys)
/// TODO(discord9): better ways to assign time index
/// for now, it will found the first column that is timestamp or has a tumble window floor function
fn find_time_index_in_group_exprs(group_exprs: &[TypedExpr]) -> Option<usize> {
group_exprs.iter().position(|expr| {
matches!(
&expr.expr,
ScalarExpr::CallUnary {
func: UnaryFunc::TumbleWindowFloor { .. },
expr: _
}
) || expr.typ.scalar_type.is_timestamp()
})
}
impl TypedPlan {
/// Convert AggregateRel into Flow's TypedPlan
///
/// The output of aggr plan is:
///
/// <group_exprs>..<aggr_exprs>
#[async_recursion::async_recursion]
pub async fn from_substrait_agg_rel(
ctx: &mut FlownodeContext,
agg: &proto::AggregateRel,
extensions: &FunctionExtensions,
) -> Result<TypedPlan, Error> {
let input = if let Some(input) = agg.input.as_ref() {
TypedPlan::from_substrait_rel(ctx, input, extensions).await?
} else {
return not_impl_err!("Aggregate without an input is not supported");
};
let group_exprs = TypedExpr::from_substrait_agg_grouping(
ctx,
&agg.grouping_expressions,
&agg.groupings,
&input.schema,
extensions,
)
.await?;
let time_index = find_time_index_in_group_exprs(&group_exprs);
let mut aggr_exprs = AggregateExpr::from_substrait_agg_measures(
ctx,
&agg.measures,
&input.schema,
extensions,
)
.await?;
let key_val_plan = KeyValPlan::from_substrait_gen_key_val_plan(
&mut aggr_exprs,
&group_exprs,
input.schema.typ.column_types.len(),
)?;
// output type is group_exprs + aggr_exprs
let output_type = {
let mut output_types = Vec::new();
// give best effort to get column name
let mut output_names = Vec::new();
// first append group_expr as key, then aggr_expr as value
for expr in group_exprs.iter() {
output_types.push(expr.typ.clone());
let col_name = match &expr.expr {
ScalarExpr::Column(col) => input.schema.get_name(*col).clone(),
// TODO(discord9): impl& use ScalarExpr.display_name, which recursively build expr's name
_ => None,
};
output_names.push(col_name)
}
for aggr in &aggr_exprs {
output_types.push(ColumnType::new_nullable(
aggr.func.signature().output.clone(),
));
// TODO(discord9): find a clever way to name them?
output_names.push(None);
}
// TODO(discord9): try best to get time
if group_exprs.is_empty() {
RelationType::new(output_types)
} else {
RelationType::new(output_types).with_key((0..group_exprs.len()).collect_vec())
}
.with_time_index(time_index)
.into_named(output_names)
};
// copy aggr_exprs to full_aggrs, and split them into simple_aggrs and distinct_aggrs
// also set them input/output column
let full_aggrs = aggr_exprs;
let mut simple_aggrs = Vec::new();
let mut distinct_aggrs = Vec::new();
for (output_column, aggr_expr) in full_aggrs.iter().enumerate() {
let input_column = aggr_expr.expr.as_column().with_context(|| PlanSnafu {
reason: "Expect aggregate argument to be transformed into a column at this point",
})?;
if aggr_expr.distinct {
distinct_aggrs.push(AggrWithIndex::new(
aggr_expr.clone(),
input_column,
output_column,
));
} else {
simple_aggrs.push(AggrWithIndex::new(
aggr_expr.clone(),
input_column,
output_column,
));
}
}
let accum_plan = AccumulablePlan {
full_aggrs,
simple_aggrs,
distinct_aggrs,
};
let plan = Plan::Reduce {
input: Box::new(input),
key_val_plan,
reduce_plan: ReducePlan::Accumulable(accum_plan),
};
// FIX(discord9): deal with key first
return Ok(TypedPlan {
schema: output_type,
plan,
});
}
}
#[cfg(test)]
mod test {
use bytes::BytesMut;
use common_time::IntervalMonthDayNano;
use datatypes::data_type::ConcreteDataType as CDT;
use datatypes::prelude::ConcreteDataType;
use datatypes::value::Value;
use pretty_assertions::assert_eq;
use super::*;
use crate::expr::{BinaryFunc, DfScalarFunction, GlobalId, RawDfScalarFn};
use crate::plan::{Plan, TypedPlan};
use crate::repr::{ColumnType, RelationType};
use crate::transform::test::{create_test_ctx, create_test_query_engine, sql_to_substrait};
#[tokio::test]
async fn test_sum() {
let engine = create_test_query_engine();
let sql = "SELECT sum(number) FROM numbers";
let plan = sql_to_substrait(engine.clone(), sql).await;
let mut ctx = create_test_ctx();
let flow_plan = TypedPlan::from_substrait_plan(&mut ctx, &plan).await;
let aggr_expr = AggregateExpr {
func: AggregateFunc::SumUInt64,
expr: ScalarExpr::Column(0),
distinct: false,
};
let expected = TypedPlan {
schema: RelationType::new(vec![ColumnType::new(CDT::uint64_datatype(), true)])
.into_named(vec![Some("sum(numbers.number)".to_string())]),
plan: Plan::Reduce {
input: Box::new(
Plan::Get {
id: crate::expr::Id::Global(GlobalId::User(0)),
}
.with_types(
RelationType::new(vec![ColumnType::new(
ConcreteDataType::uint32_datatype(),
false,
)])
.into_named(vec![Some("number".to_string())]),
)
.mfp(MapFilterProject::new(1).into_safe())
.unwrap(),
),
key_val_plan: KeyValPlan {
key_plan: MapFilterProject::new(1)
.project(vec![])
.unwrap()
.into_safe(),
val_plan: MapFilterProject::new(1)
.map(vec![
ScalarExpr::Column(0)
.call_unary(UnaryFunc::Cast(CDT::uint64_datatype())),
])
.unwrap()
.project(vec![1])
.unwrap()
.into_safe(),
},
reduce_plan: ReducePlan::Accumulable(AccumulablePlan {
full_aggrs: vec![aggr_expr.clone()],
simple_aggrs: vec![AggrWithIndex::new(aggr_expr.clone(), 0, 0)],
distinct_aggrs: vec![],
}),
},
};
assert_eq!(flow_plan.unwrap(), expected);
}
#[tokio::test]
async fn test_distinct_number() {
let engine = create_test_query_engine();
let sql = "SELECT DISTINCT number FROM numbers";
let plan = sql_to_substrait(engine.clone(), sql).await;
let mut ctx = create_test_ctx();
let flow_plan = TypedPlan::from_substrait_plan(&mut ctx, &plan)
.await
.unwrap();
let expected = TypedPlan {
schema: RelationType::new(vec![
ColumnType::new(CDT::uint32_datatype(), false), // col number
])
.with_key(vec![0])
.into_named(vec![Some("number".to_string())]),
plan: Plan::Reduce {
input: Box::new(
Plan::Get {
id: crate::expr::Id::Global(GlobalId::User(0)),
}
.with_types(
RelationType::new(vec![ColumnType::new(
ConcreteDataType::uint32_datatype(),
false,
)])
.into_named(vec![Some("number".to_string())]),
)
.mfp(MapFilterProject::new(1).into_safe())
.unwrap(),
),
key_val_plan: KeyValPlan {
key_plan: MapFilterProject::new(1)
.map(vec![ScalarExpr::Column(0)])
.unwrap()
.project(vec![1])
.unwrap()
.into_safe(),
val_plan: MapFilterProject::new(1)
.project(vec![0])
.unwrap()
.into_safe(),
},
reduce_plan: ReducePlan::Accumulable(AccumulablePlan {
full_aggrs: vec![],
simple_aggrs: vec![],
distinct_aggrs: vec![],
}),
},
};
assert_eq!(flow_plan, expected);
}
#[tokio::test]
async fn test_sum_group_by() {
let engine = create_test_query_engine();
let sql = "SELECT sum(number), number FROM numbers GROUP BY number";
let plan = sql_to_substrait(engine.clone(), sql).await;
let mut ctx = create_test_ctx();
let flow_plan = TypedPlan::from_substrait_plan(&mut ctx, &plan)
.await
.unwrap();
let aggr_expr = AggregateExpr {
func: AggregateFunc::SumUInt64,
expr: ScalarExpr::Column(0),
distinct: false,
};
let expected = TypedPlan {
schema: RelationType::new(vec![
ColumnType::new(CDT::uint64_datatype(), true), // col sum(number)
ColumnType::new(CDT::uint32_datatype(), false), // col number
])
.with_key(vec![1])
.into_named(vec![
Some("sum(numbers.number)".to_string()),
Some("number".to_string()),
]),
plan: Plan::Mfp {
input: Box::new(
Plan::Reduce {
input: Box::new(
Plan::Get {
id: crate::expr::Id::Global(GlobalId::User(0)),
}
.with_types(
RelationType::new(vec![ColumnType::new(
ConcreteDataType::uint32_datatype(),
false,
)])
.into_named(vec![Some("number".to_string())]),
)
.mfp(MapFilterProject::new(1).into_safe())
.unwrap(),
),
key_val_plan: KeyValPlan {
key_plan: MapFilterProject::new(1)
.map(vec![ScalarExpr::Column(0)])
.unwrap()
.project(vec![1])
.unwrap()
.into_safe(),
val_plan: MapFilterProject::new(1)
.map(vec![
ScalarExpr::Column(0)
.call_unary(UnaryFunc::Cast(CDT::uint64_datatype())),
])
.unwrap()
.project(vec![1])
.unwrap()
.into_safe(),
},
reduce_plan: ReducePlan::Accumulable(AccumulablePlan {
full_aggrs: vec![aggr_expr.clone()],
simple_aggrs: vec![AggrWithIndex::new(aggr_expr.clone(), 0, 0)],
distinct_aggrs: vec![],
}),
}
.with_types(
RelationType::new(vec![
ColumnType::new(CDT::uint32_datatype(), false), // col number
ColumnType::new(CDT::uint64_datatype(), true), // col sum(number)
])
.with_key(vec![0])
.into_named(vec![Some("number".to_string()), None]),
),
),
mfp: MapFilterProject::new(2)
.map(vec![ScalarExpr::Column(1), ScalarExpr::Column(0)])
.unwrap()
.project(vec![2, 3])
.unwrap(),
},
};
assert_eq!(flow_plan, expected);
}
#[tokio::test]
async fn test_sum_add() {
let engine = create_test_query_engine();
let sql = "SELECT sum(number+number) FROM numbers";
let plan = sql_to_substrait(engine.clone(), sql).await;
let mut ctx = create_test_ctx();
let flow_plan = TypedPlan::from_substrait_plan(&mut ctx, &plan).await;
let aggr_expr = AggregateExpr {
func: AggregateFunc::SumUInt64,
expr: ScalarExpr::Column(0),
distinct: false,
};
let expected = TypedPlan {
schema: RelationType::new(vec![ColumnType::new(CDT::uint64_datatype(), true)])
.into_named(vec![Some(
"sum(numbers.number + numbers.number)".to_string(),
)]),
plan: Plan::Reduce {
input: Box::new(
Plan::Mfp {
input: Box::new(
Plan::Get {
id: crate::expr::Id::Global(GlobalId::User(0)),
}
.with_types(
RelationType::new(vec![ColumnType::new(
ConcreteDataType::uint32_datatype(),
false,
)])
.into_named(vec![Some("number".to_string())]),
),
),
mfp: MapFilterProject::new(1),
}
.with_types(
RelationType::new(vec![ColumnType::new(
ConcreteDataType::uint32_datatype(),
false,
)])
.into_named(vec![Some("number".to_string())]),
),
),
key_val_plan: KeyValPlan {
key_plan: MapFilterProject::new(1)
.project(vec![])
.unwrap()
.into_safe(),
val_plan: MapFilterProject::new(1)
.map(vec![
ScalarExpr::Column(0)
.call_binary(ScalarExpr::Column(0), BinaryFunc::AddUInt32)
.call_unary(UnaryFunc::Cast(CDT::uint64_datatype())),
])
.unwrap()
.project(vec![1])
.unwrap()
.into_safe(),
},
reduce_plan: ReducePlan::Accumulable(AccumulablePlan {
full_aggrs: vec![aggr_expr.clone()],
simple_aggrs: vec![AggrWithIndex::new(aggr_expr.clone(), 0, 0)],
distinct_aggrs: vec![],
}),
},
};
assert_eq!(flow_plan.unwrap(), expected);
}
#[tokio::test]
async fn test_cast_max_min() {
let engine = create_test_query_engine();
let sql = "SELECT (max(number) - min(number))/30.0, date_bin(INTERVAL '30 second', CAST(ts AS TimestampMillisecond)) as time_window from numbers_with_ts GROUP BY time_window";
let plan = sql_to_substrait(engine.clone(), sql).await;
let mut ctx = create_test_ctx();
let flow_plan = TypedPlan::from_substrait_plan(&mut ctx, &plan).await;
let aggr_exprs = vec![
AggregateExpr {
func: AggregateFunc::MaxUInt32,
expr: ScalarExpr::Column(0),
distinct: false,
},
AggregateExpr {
func: AggregateFunc::MinUInt32,
expr: ScalarExpr::Column(0),
distinct: false,
},
];
let expected = TypedPlan {
schema: RelationType::new(vec![
ColumnType::new(CDT::float64_datatype(), true),
ColumnType::new(CDT::timestamp_millisecond_datatype(), true),
])
.with_time_index(Some(1))
.into_named(vec![
Some(
"max(numbers_with_ts.number) - min(numbers_with_ts.number) / Float64(30)"
.to_string(),
),
Some("time_window".to_string()),
]),
plan: Plan::Mfp {
input: Box::new(
Plan::Reduce {
input: Box::new(
Plan::Get {
id: crate::expr::Id::Global(GlobalId::User(1)),
}
.with_types(
RelationType::new(vec![
ColumnType::new(ConcreteDataType::uint32_datatype(), false),
ColumnType::new(ConcreteDataType::timestamp_millisecond_datatype(), false),
])
.into_named(vec![
Some("number".to_string()),
Some("ts".to_string()),
]),
)
.mfp(MapFilterProject::new(2).into_safe())
.unwrap(),
),
key_val_plan: KeyValPlan {
key_plan: MapFilterProject::new(2)
.map(vec![ScalarExpr::CallDf {
df_scalar_fn: DfScalarFunction::try_from_raw_fn(
RawDfScalarFn {
f: BytesMut::from(
b"\x08\x02\x1a\x07\x8a\x02\x04\x08\x03\x18\x01\"\x0f\x1a\r\n\x0b\xa2\x02\x08\n\0\x12\x04\x10\x1e \t\"\n\x1a\x08\x12\x06\n\x04\x12\x02\x08\x01".as_ref(),
),
input_schema: RelationType::new(vec![ColumnType::new(
ConcreteDataType::interval_month_day_nano_datatype(),
true,
),ColumnType::new(
ConcreteDataType::timestamp_millisecond_datatype(),
false,
)])
.into_unnamed(),
extensions: FunctionExtensions::from_iter([
(0, "subtract".to_string()),
(1, "divide".to_string()),
(2, "date_bin".to_string()),
(3, "max".to_string()),
(4, "min".to_string()),
]),
},
)
.await
.unwrap(),
exprs: vec![
ScalarExpr::Literal(
Value::IntervalMonthDayNano(IntervalMonthDayNano::new(0, 0, 30000000000)),
CDT::interval_month_day_nano_datatype()
),
ScalarExpr::Column(1)
],
}])
.unwrap()
.project(vec![2])
.unwrap()
.into_safe(),
val_plan: MapFilterProject::new(2)
.into_safe(),
},
reduce_plan: ReducePlan::Accumulable(AccumulablePlan {
full_aggrs: aggr_exprs.clone(),
simple_aggrs: vec![AggrWithIndex::new(aggr_exprs[0].clone(), 0, 0),
AggrWithIndex::new(aggr_exprs[1].clone(), 0, 1)],
distinct_aggrs: vec![],
}),
}
.with_types(
RelationType::new(vec![
ColumnType::new(
ConcreteDataType::timestamp_millisecond_datatype(),
true,
), // time_window
ColumnType::new(ConcreteDataType::uint32_datatype(), true), // max
ColumnType::new(ConcreteDataType::uint32_datatype(), true), // min
])
.with_time_index(Some(0))
.into_unnamed(),
),
),
mfp: MapFilterProject::new(3)
.map(vec![
ScalarExpr::Column(1)
.call_binary(ScalarExpr::Column(2), BinaryFunc::SubUInt32)
.cast(CDT::float64_datatype())
.call_binary(
ScalarExpr::Literal(Value::from(30.0f64), CDT::float64_datatype()),
BinaryFunc::DivFloat64,
),
ScalarExpr::Column(0),
])
.unwrap()
.project(vec![3, 4])
.unwrap(),
},
};
assert_eq!(flow_plan.unwrap(), expected);
}
}
-839
View File
@@ -1,839 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#![warn(unused_imports)]
use std::sync::Arc;
use common_error::ext::BoxedError;
use common_telemetry::debug;
use datafusion::execution::SessionStateBuilder;
use datafusion::functions::all_default_functions;
use datafusion_expr::physical_planning_context::PhysicalPlanningContext;
use datafusion_physical_expr::PhysicalExpr;
use datafusion_substrait::logical_plan::consumer::DefaultSubstraitConsumer;
use datatypes::data_type::ConcreteDataType as CDT;
use snafu::{OptionExt, ResultExt, ensure};
use substrait_proto::proto::Expression;
use substrait_proto::proto::expression::field_reference::ReferenceType::DirectReference;
use substrait_proto::proto::expression::reference_segment::ReferenceType::StructField;
use substrait_proto::proto::expression::{IfThen, RexType, ScalarFunction};
use substrait_proto::proto::function_argument::ArgType;
use crate::error::{
DatafusionSnafu, DatatypesSnafu, Error, EvalSnafu, ExternalSnafu, InvalidQuerySnafu,
NotImplementedSnafu, PlanSnafu, UnexpectedSnafu,
};
use crate::expr::{
BinaryFunc, DfScalarFunction, RawDfScalarFn, ScalarExpr, TUMBLE_END, TUMBLE_START, TypedExpr,
UnaryFunc, UnmaterializableFunc, VariadicFunc,
};
use crate::repr::{ColumnType, RelationDesc, RelationType};
use crate::transform::literal::{
from_substrait_literal, from_substrait_type, to_substrait_literal,
};
use crate::transform::{FunctionExtensions, substrait_proto};
// TODO(discord9): refactor plan to substrait convert of `arrow_cast` function thus remove this function
/// ref to `arrow_schema::datatype` for type name
fn typename_to_cdt(name: &str) -> Result<CDT, Error> {
let ret = match name {
"Int8" => CDT::int8_datatype(),
"Int16" => CDT::int16_datatype(),
"Int32" => CDT::int32_datatype(),
"Int64" => CDT::int64_datatype(),
"UInt8" => CDT::uint8_datatype(),
"UInt16" => CDT::uint16_datatype(),
"UInt32" => CDT::uint32_datatype(),
"UInt64" => CDT::uint64_datatype(),
"Float32" => CDT::float32_datatype(),
"Float64" => CDT::float64_datatype(),
"Boolean" => CDT::boolean_datatype(),
"String" => CDT::string_datatype(),
"Date" | "Date32" | "Date64" => CDT::date_datatype(),
"Timestamp" => CDT::timestamp_second_datatype(),
"Timestamp(Second, None)" => CDT::timestamp_second_datatype(),
"Timestamp(Millisecond, None)" => CDT::timestamp_millisecond_datatype(),
"Timestamp(Microsecond, None)" => CDT::timestamp_microsecond_datatype(),
"Timestamp(Nanosecond, None)" => CDT::timestamp_nanosecond_datatype(),
"Time32(Second)" | "Time64(Second)" => CDT::time_second_datatype(),
"Time32(Millisecond)" | "Time64(Millisecond)" => CDT::time_millisecond_datatype(),
"Time32(Microsecond)" | "Time64(Microsecond)" => CDT::time_microsecond_datatype(),
"Time32(Nanosecond)" | "Time64(Nanosecond)" => CDT::time_nanosecond_datatype(),
_ => NotImplementedSnafu {
reason: format!("Unrecognized typename: {}", name),
}
.fail()?,
};
Ok(ret)
}
/// Convert [`ScalarFunction`] to corresponding Datafusion's [`PhysicalExpr`]
pub(crate) async fn from_scalar_fn_to_df_fn_impl(
f: &ScalarFunction,
input_schema: &RelationDesc,
extensions: &FunctionExtensions,
) -> Result<Arc<dyn PhysicalExpr>, Error> {
let e = Expression {
rex_type: Some(RexType::ScalarFunction(f.clone())),
};
let schema = input_schema.to_df_schema()?;
let extensions = extensions.to_extensions();
let session_state = SessionStateBuilder::new()
.with_scalar_functions(all_default_functions())
.build();
let consumer = DefaultSubstraitConsumer::new(&extensions, &session_state);
let df_expr =
substrait::df_logical_plan::consumer::from_substrait_rex(&consumer, &e, &schema).await;
let expr = df_expr.context({
DatafusionSnafu {
context: "Failed to convert substrait scalar function to datafusion scalar function",
}
})?;
let phy_expr = datafusion::physical_expr::create_physical_expr(
&expr,
&schema,
&Default::default(),
&PhysicalPlanningContext::default(),
)
.context(DatafusionSnafu {
context: "Failed to create physical expression from logical expression",
})?;
Ok(phy_expr)
}
/// Return an [`Expression`](wrapped in a [`FunctionArgument`]) that references the i-th column of the input relation
pub(crate) fn proto_col(i: usize) -> substrait_proto::proto::FunctionArgument {
use substrait_proto::proto::expression;
let expr = Expression {
rex_type: Some(expression::RexType::Selection(Box::new(
expression::FieldReference {
reference_type: Some(expression::field_reference::ReferenceType::DirectReference(
expression::ReferenceSegment {
reference_type: Some(
expression::reference_segment::ReferenceType::StructField(Box::new(
expression::reference_segment::StructField {
field: i as i32,
child: None,
},
)),
),
},
)),
root_type: None,
},
))),
};
substrait_proto::proto::FunctionArgument {
arg_type: Some(substrait_proto::proto::function_argument::ArgType::Value(
expr,
)),
}
}
fn is_proto_literal(arg: &substrait_proto::proto::FunctionArgument) -> bool {
use substrait_proto::proto::expression;
matches!(
arg.arg_type.as_ref().unwrap(),
ArgType::Value(Expression {
rex_type: Some(expression::RexType::Literal(_)),
})
)
}
fn build_proto_lit(
lit: substrait_proto::proto::expression::Literal,
) -> substrait_proto::proto::FunctionArgument {
use substrait_proto::proto;
proto::FunctionArgument {
arg_type: Some(ArgType::Value(Expression {
rex_type: Some(proto::expression::RexType::Literal(lit)),
})),
}
}
/// rewrite ScalarFunction's arguments to Columns 0..n so nested exprs are still handled by us instead of datafusion
///
/// specially, if a argument is a literal, the replacement will not happen
fn rewrite_scalar_function(
f: &ScalarFunction,
arg_typed_exprs: &[TypedExpr],
) -> Result<ScalarFunction, Error> {
let mut f_rewrite = f.clone();
ensure!(
f_rewrite.arguments.len() == arg_typed_exprs.len(),
crate::error::InternalSnafu {
reason: format!(
"Expect `f_rewrite` and `arg_typed_expr` to be same length, found {} and {}",
f_rewrite.arguments.len(),
arg_typed_exprs.len()
)
}
);
for (idx, raw_expr) in f_rewrite.arguments.iter_mut().enumerate() {
// only replace it with col(idx) if it is not literal
// will try best to determine if it is literal, i.e. for function like `cast(<literal>)` will try
// in both world to understand if it results in a literal
match (
is_proto_literal(raw_expr),
arg_typed_exprs[idx].expr.is_literal(),
) {
(false, false) => *raw_expr = proto_col(idx),
(true, _) => (),
(false, true) => {
if let ScalarExpr::Literal(val, ty) = &arg_typed_exprs[idx].expr {
let df_val = val
.try_to_scalar_value(ty)
.map_err(BoxedError::new)
.context(ExternalSnafu)?;
let lit_sub = to_substrait_literal(&df_val)?;
// put const-folded literal back to df to simplify stuff
*raw_expr = build_proto_lit(lit_sub);
} else {
UnexpectedSnafu {
reason: format!(
"Expect value to be literal, but found {:?}",
arg_typed_exprs[idx].expr
),
}
.fail()?
}
}
}
}
Ok(f_rewrite)
}
impl TypedExpr {
pub async fn from_substrait_to_datafusion_scalar_func(
f: &ScalarFunction,
arg_typed_exprs: Vec<TypedExpr>,
extensions: &FunctionExtensions,
) -> Result<TypedExpr, Error> {
let (arg_exprs, arg_types): (Vec<_>, Vec<_>) = arg_typed_exprs
.clone()
.into_iter()
.map(|e| (e.expr, e.typ))
.unzip();
debug!("Before rewrite: {:?}", f);
let f_rewrite = rewrite_scalar_function(f, &arg_typed_exprs)?;
debug!("After rewrite: {:?}", f_rewrite);
let input_schema = RelationType::new(arg_types).into_unnamed();
let raw_fn =
RawDfScalarFn::from_proto(&f_rewrite, input_schema.clone(), extensions.clone())?;
let df_func = DfScalarFunction::try_from_raw_fn(raw_fn).await?;
let expr = ScalarExpr::CallDf {
df_scalar_fn: df_func,
exprs: arg_exprs,
};
// df already know it's own schema, so not providing here
let ret_type = expr.typ(&[])?;
Ok(TypedExpr::new(expr, ret_type))
}
/// Convert ScalarFunction into Flow's ScalarExpr
pub async fn from_substrait_scalar_func(
f: &ScalarFunction,
input_schema: &RelationDesc,
extensions: &FunctionExtensions,
) -> Result<TypedExpr, Error> {
let fn_name =
extensions
.get(&f.function_reference)
.with_context(|| NotImplementedSnafu {
reason: format!(
"Aggregated function not found: function reference = {:?}",
f.function_reference
),
})?;
let arg_len = f.arguments.len();
let arg_typed_exprs: Vec<TypedExpr> = {
let mut rets = Vec::new();
for arg in f.arguments.iter() {
let ret = match &arg.arg_type {
Some(ArgType::Value(e)) => {
TypedExpr::from_substrait_rex(e, input_schema, extensions).await
}
_ => not_impl_err!("Aggregated function argument non-Value type not supported"),
}?;
rets.push(ret);
}
rets
};
// literal's type is determined by the function and type of other args
let (arg_exprs, arg_types): (Vec<_>, Vec<_>) = arg_typed_exprs
.clone()
.into_iter()
.map(
|TypedExpr {
expr: arg_val,
typ: arg_type,
}| {
if arg_val.is_literal() {
(arg_val, None)
} else {
(arg_val, Some(arg_type.scalar_type))
}
},
)
.unzip();
match arg_len {
1 if UnaryFunc::is_valid_func_name(fn_name) => {
let func = UnaryFunc::from_str_and_type(fn_name, None)?;
let arg = arg_exprs[0].clone();
let ret_type = ColumnType::new_nullable(func.signature().output.clone());
Ok(TypedExpr::new(arg.call_unary(func), ret_type))
}
2 if fn_name == "arrow_cast" => {
let cast_to = arg_exprs[1]
.clone()
.as_literal()
.and_then(|lit| lit.as_string())
.with_context(|| InvalidQuerySnafu {
reason: "array_cast's second argument must be a literal string",
})?;
let cast_to = typename_to_cdt(&cast_to)?;
let func = UnaryFunc::Cast(cast_to.clone());
let arg = arg_exprs[0].clone();
// constant folding here since some datafusion function require it for constant arg(i.e. `DATE_BIN`)
if arg.is_literal() {
let res = func.eval(&[], &arg).context(EvalSnafu)?;
Ok(TypedExpr::new(
ScalarExpr::Literal(res, cast_to.clone()),
ColumnType::new_nullable(cast_to),
))
} else {
let ret_type = ColumnType::new_nullable(func.signature().output.clone());
Ok(TypedExpr::new(arg.call_unary(func), ret_type))
}
}
2 if BinaryFunc::is_valid_func_name(fn_name) => {
let (func, signature) =
BinaryFunc::from_str_expr_and_type(fn_name, &arg_exprs, &arg_types[0..2])?;
// constant folding here
let is_all_literal = arg_exprs.iter().all(|arg| arg.is_literal());
if is_all_literal {
let res = func
.eval(&[], &arg_exprs[0], &arg_exprs[1])
.context(EvalSnafu)?;
// if output type is null, it should be inferred from the input types
let con_typ = signature.output.clone();
let typ = ColumnType::new_nullable(con_typ.clone());
return Ok(TypedExpr::new(ScalarExpr::Literal(res, con_typ), typ));
}
let mut arg_exprs = arg_exprs;
for (idx, arg_expr) in arg_exprs.iter_mut().enumerate() {
if let ScalarExpr::Literal(val, typ) = arg_expr {
let dest_type = signature.input[idx].clone();
// cast val to target_type
let dest_val = if !dest_type.is_null() {
datatypes::types::cast(val.clone(), &dest_type)
.with_context(|_|
DatatypesSnafu{
extra: format!("Failed to implicitly cast literal {val:?} to type {dest_type:?}")
})?
} else {
val.clone()
};
*val = dest_val;
*typ = dest_type;
}
}
let ret_type = ColumnType::new_nullable(func.signature().output.clone());
let ret_expr = arg_exprs[0].clone().call_binary(arg_exprs[1].clone(), func);
Ok(TypedExpr::new(ret_expr, ret_type))
}
_var => {
if fn_name == TUMBLE_START || fn_name == TUMBLE_END {
let (func, arg) = UnaryFunc::from_tumble_func(fn_name, &arg_typed_exprs)?;
let ret_type = ColumnType::new_nullable(func.signature().output.clone());
Ok(TypedExpr::new(arg.expr.call_unary(func), ret_type))
} else if VariadicFunc::is_valid_func_name(fn_name) {
let func = VariadicFunc::from_str_and_types(fn_name, &arg_types)?;
let ret_type = ColumnType::new_nullable(func.signature().output.clone());
let mut expr = ScalarExpr::CallVariadic {
func,
exprs: arg_exprs,
};
expr.optimize();
Ok(TypedExpr::new(expr, ret_type))
} else if UnmaterializableFunc::is_valid_func_name(fn_name) {
let func = UnmaterializableFunc::from_str_args(fn_name, arg_typed_exprs)?;
let ret_type = ColumnType::new_nullable(func.signature().output.clone());
Ok(TypedExpr::new(
ScalarExpr::CallUnmaterializable(func),
ret_type,
))
} else {
let try_as_df = Self::from_substrait_to_datafusion_scalar_func(
f,
arg_typed_exprs,
extensions,
)
.await?;
Ok(try_as_df)
}
}
}
}
/// Convert IfThen into Flow's ScalarExpr
pub async fn from_substrait_ifthen_rex(
if_then: &IfThen,
input_schema: &RelationDesc,
extensions: &FunctionExtensions,
) -> Result<TypedExpr, Error> {
let ifs: Vec<_> = {
let mut ifs = Vec::new();
for if_clause in if_then.ifs.iter() {
let proto_if = if_clause.r#if.as_ref().with_context(|| InvalidQuerySnafu {
reason: "IfThen clause without if",
})?;
let proto_then = if_clause.then.as_ref().with_context(|| InvalidQuerySnafu {
reason: "IfThen clause without then",
})?;
let cond =
TypedExpr::from_substrait_rex(proto_if, input_schema, extensions).await?;
let then =
TypedExpr::from_substrait_rex(proto_then, input_schema, extensions).await?;
ifs.push((cond, then));
}
ifs
};
// if no else is presented
let els = match if_then
.r#else
.as_ref()
.map(|e| TypedExpr::from_substrait_rex(e, input_schema, extensions))
{
Some(fut) => Some(fut.await),
None => None,
}
.transpose()?
.unwrap_or_else(|| {
TypedExpr::new(
ScalarExpr::literal_null(),
ColumnType::new_nullable(CDT::null_datatype()),
)
});
fn build_if_then_recur(
mut next_if_then: impl Iterator<Item = (TypedExpr, TypedExpr)>,
els: TypedExpr,
) -> TypedExpr {
if let Some((cond, then)) = next_if_then.next() {
// always assume the type of `if`` expr is the same with the `then`` expr
TypedExpr::new(
ScalarExpr::If {
cond: Box::new(cond.expr),
then: Box::new(then.expr),
els: Box::new(build_if_then_recur(next_if_then, els).expr),
},
then.typ,
)
} else {
els
}
}
let expr_if = build_if_then_recur(ifs.into_iter(), els);
Ok(expr_if)
}
/// Convert Substrait Rex into Flow's ScalarExpr
#[async_recursion::async_recursion]
pub async fn from_substrait_rex(
e: &Expression,
input_schema: &RelationDesc,
extensions: &FunctionExtensions,
) -> Result<TypedExpr, Error> {
match &e.rex_type {
Some(RexType::Literal(lit)) => {
let lit = from_substrait_literal(lit)?;
Ok(TypedExpr::new(
ScalarExpr::Literal(lit.0, lit.1.clone()),
ColumnType::new_nullable(lit.1),
))
}
Some(RexType::SingularOrList(s)) => {
let substrait_expr = s.value.as_ref().with_context(|| InvalidQuerySnafu {
reason: "SingularOrList expression without value",
})?;
let typed_expr =
TypedExpr::from_substrait_rex(substrait_expr, input_schema, extensions).await?;
// Note that we didn't impl support to in list expr
if !s.options.is_empty() {
let mut list = Vec::with_capacity(s.options.len());
for opt in s.options.iter() {
let opt_expr =
TypedExpr::from_substrait_rex(opt, input_schema, extensions).await?;
list.push(opt_expr.expr);
}
let in_list_expr = ScalarExpr::InList {
expr: Box::new(typed_expr.expr),
list,
};
Ok(TypedExpr::new(
in_list_expr,
ColumnType::new_nullable(CDT::boolean_datatype()),
))
} else {
Ok(typed_expr)
}
}
Some(RexType::Selection(field_ref)) => match &field_ref.reference_type {
Some(DirectReference(direct)) => match &direct.reference_type.as_ref() {
Some(StructField(x)) => match &x.child.as_ref() {
Some(_) => {
not_impl_err!(
"Direct reference StructField with child is not supported"
)
}
None => {
let column = x.field as usize;
let column_type = input_schema.typ().column_types[column].clone();
Ok(TypedExpr::new(ScalarExpr::Column(column), column_type))
}
},
_ => not_impl_err!(
"Direct reference with types other than StructField is not supported"
),
},
_ => not_impl_err!("unsupported field ref type"),
},
Some(RexType::ScalarFunction(f)) => {
TypedExpr::from_substrait_scalar_func(f, input_schema, extensions).await
}
Some(RexType::IfThen(if_then)) => {
TypedExpr::from_substrait_ifthen_rex(if_then, input_schema, extensions).await
}
Some(RexType::Cast(cast)) => {
let input = cast.input.as_ref().with_context(|| InvalidQuerySnafu {
reason: "Cast expression without input",
})?;
let input = TypedExpr::from_substrait_rex(input, input_schema, extensions).await?;
let cast_type = from_substrait_type(cast.r#type.as_ref().with_context(|| {
InvalidQuerySnafu {
reason: "Cast expression without type",
}
})?)?;
let func = UnaryFunc::from_str_and_type("cast", Some(cast_type.clone()))?;
Ok(TypedExpr::new(
input.expr.call_unary(func),
ColumnType::new_nullable(cast_type),
))
}
Some(RexType::WindowFunction(_)) => PlanSnafu {
reason:
"Window function is not supported yet. Please use aggregation function instead."
.to_string(),
}
.fail(),
_ => not_impl_err!("unsupported rex_type"),
}
}
}
#[cfg(test)]
mod test {
use datatypes::prelude::ConcreteDataType;
use datatypes::value::Value;
use pretty_assertions::assert_eq;
use super::*;
use crate::expr::{GlobalId, MapFilterProject};
use crate::plan::{Plan, TypedPlan};
use crate::repr::{self, ColumnType, RelationType};
use crate::transform::test::{create_test_ctx, create_test_query_engine, sql_to_substrait};
/// test if `WHERE` condition can be converted to Flow's ScalarExpr in mfp's filter
#[tokio::test]
async fn test_where_and() {
let engine = create_test_query_engine();
let sql =
"SELECT number FROM numbers_with_ts WHERE number >= 1 AND number <= 3 AND number!=2";
let plan = sql_to_substrait(engine.clone(), sql).await;
let mut ctx = create_test_ctx();
let flow_plan = TypedPlan::from_substrait_plan(&mut ctx, &plan).await;
// optimize binary and to variadic and
let filter = ScalarExpr::CallVariadic {
func: VariadicFunc::And,
exprs: vec![
ScalarExpr::Column(2).call_binary(
ScalarExpr::Literal(Value::from(1i64), CDT::int64_datatype()),
BinaryFunc::Gte,
),
ScalarExpr::Column(2).call_binary(
ScalarExpr::Literal(Value::from(3i64), CDT::int64_datatype()),
BinaryFunc::Lte,
),
ScalarExpr::Column(2).call_binary(
ScalarExpr::Literal(Value::from(2i64), CDT::int64_datatype()),
BinaryFunc::NotEq,
),
],
};
let expected = TypedPlan {
schema: RelationType::new(vec![ColumnType::new(CDT::uint32_datatype(), false)])
.into_named(vec![Some("number".to_string())]),
plan: Plan::Mfp {
input: Box::new(
Plan::Get {
id: crate::expr::Id::Global(GlobalId::User(1)),
}
.with_types(
RelationType::new(vec![
ColumnType::new(ConcreteDataType::uint32_datatype(), false),
ColumnType::new(
ConcreteDataType::timestamp_millisecond_datatype(),
false,
),
])
.into_named(vec![Some("number".to_string()), Some("ts".to_string())]),
),
),
mfp: MapFilterProject::new(2)
.map(vec![
ScalarExpr::CallUnary {
func: UnaryFunc::Cast(CDT::int64_datatype()),
expr: Box::new(ScalarExpr::Column(0)),
},
ScalarExpr::Column(0),
ScalarExpr::Column(3),
])
.unwrap()
.filter(vec![filter])
.unwrap()
.project(vec![4])
.unwrap(),
},
};
assert_eq!(flow_plan.unwrap(), expected);
}
/// case: binary functions&constant folding can happen in converting substrait plan
#[tokio::test]
async fn test_binary_func_and_constant_folding() {
let engine = create_test_query_engine();
let sql = "SELECT 1+1*2-1/1+1%2==3 FROM numbers";
let plan = sql_to_substrait(engine.clone(), sql).await;
let mut ctx = create_test_ctx();
let flow_plan = TypedPlan::from_substrait_plan(&mut ctx, &plan).await;
let expected = TypedPlan {
schema: RelationType::new(vec![ColumnType::new(CDT::boolean_datatype(), true)])
.into_named(vec![Some("Int64(1) + Int64(1) * Int64(2) - Int64(1) / Int64(1) + Int64(1) % Int64(2) = Int64(3)".to_string())]),
plan: Plan::Constant {
rows: vec![(
repr::Row::new(vec![Value::from(true)]),
repr::Timestamp::MIN,
1,
)],
},
};
assert_eq!(flow_plan.unwrap(), expected);
}
/// test if the type of the literal is correctly inferred, i.e. in here literal is decoded to be int64, but need to be uint32,
#[tokio::test]
async fn test_implicitly_cast() {
let engine = create_test_query_engine();
let sql = "SELECT number+1 FROM numbers";
let plan = sql_to_substrait(engine.clone(), sql).await;
let mut ctx = create_test_ctx();
let flow_plan = TypedPlan::from_substrait_plan(&mut ctx, &plan).await;
let expected = TypedPlan {
schema: RelationType::new(vec![ColumnType::new(CDT::int64_datatype(), true)])
.into_named(vec![Some("numbers.number + Int64(1)".to_string())]),
plan: Plan::Mfp {
input: Box::new(
Plan::Get {
id: crate::expr::Id::Global(GlobalId::User(0)),
}
.with_types(
RelationType::new(vec![ColumnType::new(
ConcreteDataType::uint32_datatype(),
false,
)])
.into_named(vec![Some("number".to_string())]),
),
),
mfp: MapFilterProject::new(1)
.map(vec![
ScalarExpr::Column(0)
.call_unary(UnaryFunc::Cast(CDT::int64_datatype()))
.call_binary(
ScalarExpr::Literal(Value::from(1i64), CDT::int64_datatype()),
BinaryFunc::AddInt64,
),
])
.unwrap()
.project(vec![1])
.unwrap(),
},
};
assert_eq!(flow_plan.unwrap(), expected);
}
#[tokio::test]
async fn test_cast() {
let engine = create_test_query_engine();
let sql = "SELECT CAST(1 AS INT16) FROM numbers";
let plan = sql_to_substrait(engine.clone(), sql).await;
let mut ctx = create_test_ctx();
let flow_plan = TypedPlan::from_substrait_plan(&mut ctx, &plan).await;
let expected = TypedPlan {
schema: RelationType::new(vec![ColumnType::new(CDT::int16_datatype(), true)])
.into_named(vec![Some(
"arrow_cast(Int64(1),Utf8(\"Int16\"))".to_string(),
)]),
plan: Plan::Constant {
// cast of literal is constant folded
rows: vec![(repr::Row::new(vec![Value::from(1i16)]), i64::MIN, 1)],
},
};
assert_eq!(flow_plan.unwrap(), expected);
}
#[tokio::test]
async fn test_select_add() {
let engine = create_test_query_engine();
let sql = "SELECT number+number FROM numbers";
let plan = sql_to_substrait(engine.clone(), sql).await;
let mut ctx = create_test_ctx();
let flow_plan = TypedPlan::from_substrait_plan(&mut ctx, &plan).await;
let expected = TypedPlan {
schema: RelationType::new(vec![ColumnType::new(CDT::uint32_datatype(), true)])
.into_named(vec![Some("numbers.number + numbers.number".to_string())]),
plan: Plan::Mfp {
input: Box::new(
Plan::Get {
id: crate::expr::Id::Global(GlobalId::User(0)),
}
.with_types(
RelationType::new(vec![ColumnType::new(
ConcreteDataType::uint32_datatype(),
false,
)])
.into_named(vec![Some("number".to_string())]),
),
),
mfp: MapFilterProject::new(1)
.map(vec![
ScalarExpr::Column(0)
.call_binary(ScalarExpr::Column(0), BinaryFunc::AddUInt32),
])
.unwrap()
.project(vec![1])
.unwrap(),
},
};
assert_eq!(flow_plan.unwrap(), expected);
}
#[tokio::test]
async fn test_func_sig() {
fn lit(v: impl ToString) -> substrait_proto::proto::FunctionArgument {
use substrait_proto::proto::expression;
let expr = Expression {
rex_type: Some(expression::RexType::Literal(expression::Literal {
nullable: false,
type_variation_reference: 0,
literal_type: Some(expression::literal::LiteralType::String(v.to_string())),
})),
};
substrait_proto::proto::FunctionArgument {
arg_type: Some(substrait_proto::proto::function_argument::ArgType::Value(
expr,
)),
}
}
let f = substrait_proto::proto::expression::ScalarFunction {
function_reference: 0,
arguments: vec![proto_col(0)],
options: vec![],
output_type: None,
..Default::default()
};
let input_schema =
RelationType::new(vec![ColumnType::new(CDT::uint32_datatype(), false)]).into_unnamed();
let extensions = FunctionExtensions::from_iter([(0, "is_null".to_string())]);
let res = TypedExpr::from_substrait_scalar_func(&f, &input_schema, &extensions)
.await
.unwrap();
assert_eq!(
res,
TypedExpr {
expr: ScalarExpr::Column(0).call_unary(UnaryFunc::IsNull),
typ: ColumnType {
scalar_type: CDT::boolean_datatype(),
nullable: true,
},
}
);
let f = substrait_proto::proto::expression::ScalarFunction {
function_reference: 0,
arguments: vec![proto_col(0), proto_col(1)],
options: vec![],
output_type: None,
..Default::default()
};
let input_schema = RelationType::new(vec![
ColumnType::new(CDT::uint32_datatype(), false),
ColumnType::new(CDT::uint32_datatype(), false),
])
.into_unnamed();
let extensions = FunctionExtensions::from_iter([(0, "add".to_string())]);
let res = TypedExpr::from_substrait_scalar_func(&f, &input_schema, &extensions)
.await
.unwrap();
assert_eq!(
res,
TypedExpr {
expr: ScalarExpr::Column(0)
.call_binary(ScalarExpr::Column(1), BinaryFunc::AddUInt32,),
typ: ColumnType {
scalar_type: CDT::uint32_datatype(),
nullable: true,
},
}
);
}
}
-426
View File
@@ -1,426 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::array::TryFromSliceError;
use bytes::Bytes;
use common_decimal::Decimal128;
use common_time::timestamp::TimeUnit;
use common_time::{Date, IntervalMonthDayNano, Timestamp};
use datafusion_common::ScalarValue;
use datatypes::data_type::ConcreteDataType as CDT;
use datatypes::value::Value;
use num_traits::FromBytes;
use snafu::OptionExt;
use substrait::variation_const::{
DATE_32_TYPE_VARIATION_REF, DATE_64_TYPE_VARIATION_REF, DEFAULT_TYPE_VARIATION_REF,
UNSIGNED_INTEGER_TYPE_VARIATION_REF,
};
use substrait_proto::proto;
use substrait_proto::proto::expression::Literal;
use substrait_proto::proto::expression::literal::{LiteralType, PrecisionTimestamp};
use substrait_proto::proto::r#type::Kind;
use crate::error::{Error, NotImplementedSnafu, PlanSnafu, UnexpectedSnafu};
use crate::transform::substrait_proto;
#[derive(Debug)]
enum TimestampPrecision {
Second = 0,
Millisecond = 3,
Microsecond = 6,
Nanosecond = 9,
}
impl TryFrom<i32> for TimestampPrecision {
type Error = Error;
fn try_from(prec: i32) -> Result<Self, Self::Error> {
match prec {
0 => Ok(Self::Second),
3 => Ok(Self::Millisecond),
6 => Ok(Self::Microsecond),
9 => Ok(Self::Nanosecond),
_ => not_impl_err!("Unsupported precision: {prec}"),
}
}
}
impl TimestampPrecision {
fn to_time_unit(&self) -> TimeUnit {
match self {
Self::Second => TimeUnit::Second,
Self::Millisecond => TimeUnit::Millisecond,
Self::Microsecond => TimeUnit::Microsecond,
Self::Nanosecond => TimeUnit::Nanosecond,
}
}
fn to_cdt(&self) -> CDT {
match self {
Self::Second => CDT::timestamp_second_datatype(),
Self::Millisecond => CDT::timestamp_millisecond_datatype(),
Self::Microsecond => CDT::timestamp_microsecond_datatype(),
Self::Nanosecond => CDT::timestamp_nanosecond_datatype(),
}
}
}
/// TODO(discord9): this is copy from datafusion-substrait since the original function is not public, will be replace once is exported
pub(crate) fn to_substrait_literal(value: &ScalarValue) -> Result<Literal, Error> {
if value.is_null() {
return not_impl_err!("Unsupported literal: {value:?}");
}
let (literal_type, type_variation_reference) = match value {
ScalarValue::Boolean(Some(b)) => (LiteralType::Boolean(*b), DEFAULT_TYPE_VARIATION_REF),
ScalarValue::Int8(Some(n)) => (LiteralType::I8(*n as i32), DEFAULT_TYPE_VARIATION_REF),
ScalarValue::UInt8(Some(n)) => (
LiteralType::I8(*n as i32),
UNSIGNED_INTEGER_TYPE_VARIATION_REF,
),
ScalarValue::Int16(Some(n)) => (LiteralType::I16(*n as i32), DEFAULT_TYPE_VARIATION_REF),
ScalarValue::UInt16(Some(n)) => (
LiteralType::I16(*n as i32),
UNSIGNED_INTEGER_TYPE_VARIATION_REF,
),
ScalarValue::Int32(Some(n)) => (LiteralType::I32(*n), DEFAULT_TYPE_VARIATION_REF),
ScalarValue::UInt32(Some(n)) => (
LiteralType::I32(*n as i32),
UNSIGNED_INTEGER_TYPE_VARIATION_REF,
),
ScalarValue::Int64(Some(n)) => (LiteralType::I64(*n), DEFAULT_TYPE_VARIATION_REF),
ScalarValue::UInt64(Some(n)) => (
LiteralType::I64(*n as i64),
UNSIGNED_INTEGER_TYPE_VARIATION_REF,
),
ScalarValue::Float32(Some(f)) => (LiteralType::Fp32(*f), DEFAULT_TYPE_VARIATION_REF),
ScalarValue::Float64(Some(f)) => (LiteralType::Fp64(*f), DEFAULT_TYPE_VARIATION_REF),
// TODO(discord9): deal with timezone
ScalarValue::TimestampSecond(Some(t), _) => (
LiteralType::PrecisionTimestamp(PrecisionTimestamp {
value: *t,
precision: TimestampPrecision::Second as i32,
}),
DEFAULT_TYPE_VARIATION_REF,
),
ScalarValue::TimestampMillisecond(Some(t), _) => (
LiteralType::PrecisionTimestamp(PrecisionTimestamp {
value: *t,
precision: TimestampPrecision::Millisecond as i32,
}),
DEFAULT_TYPE_VARIATION_REF,
),
ScalarValue::TimestampMicrosecond(Some(t), _) => (
LiteralType::PrecisionTimestamp(PrecisionTimestamp {
value: *t,
precision: TimestampPrecision::Microsecond as i32,
}),
DEFAULT_TYPE_VARIATION_REF,
),
ScalarValue::TimestampNanosecond(Some(t), _) => (
LiteralType::PrecisionTimestamp(PrecisionTimestamp {
value: *t,
precision: TimestampPrecision::Nanosecond as i32,
}),
DEFAULT_TYPE_VARIATION_REF,
),
ScalarValue::Date32(Some(d)) => (LiteralType::Date(*d), DATE_32_TYPE_VARIATION_REF),
_ => (
not_impl_err!("Unsupported literal: {value:?}")?,
DEFAULT_TYPE_VARIATION_REF,
),
};
Ok(Literal {
nullable: false,
type_variation_reference,
literal_type: Some(literal_type),
})
}
/// Convert a Substrait literal into a Value and its ConcreteDataType (So that we can know type even if the value is null)
pub(crate) fn from_substrait_literal(lit: &Literal) -> Result<(Value, CDT), Error> {
let scalar_value = match &lit.literal_type {
Some(LiteralType::Boolean(b)) => (Value::from(*b), CDT::boolean_datatype()),
Some(LiteralType::I8(n)) => match lit.type_variation_reference {
DEFAULT_TYPE_VARIATION_REF => (Value::from(*n as i8), CDT::int8_datatype()),
UNSIGNED_INTEGER_TYPE_VARIATION_REF => (Value::from(*n as u8), CDT::uint8_datatype()),
others => not_impl_err!("Unknown type variation reference {others}",)?,
},
Some(LiteralType::I16(n)) => match lit.type_variation_reference {
DEFAULT_TYPE_VARIATION_REF => (Value::from(*n as i16), CDT::int16_datatype()),
UNSIGNED_INTEGER_TYPE_VARIATION_REF => (Value::from(*n as u16), CDT::uint16_datatype()),
others => not_impl_err!("Unknown type variation reference {others}",)?,
},
Some(LiteralType::I32(n)) => match lit.type_variation_reference {
DEFAULT_TYPE_VARIATION_REF => (Value::from(*n), CDT::int32_datatype()),
UNSIGNED_INTEGER_TYPE_VARIATION_REF => (Value::from(*n as u32), CDT::uint32_datatype()),
others => not_impl_err!("Unknown type variation reference {others}",)?,
},
Some(LiteralType::I64(n)) => match lit.type_variation_reference {
DEFAULT_TYPE_VARIATION_REF => (Value::from(*n), CDT::int64_datatype()),
UNSIGNED_INTEGER_TYPE_VARIATION_REF => (Value::from(*n as u64), CDT::uint64_datatype()),
others => not_impl_err!("Unknown type variation reference {others}",)?,
},
Some(LiteralType::Fp32(f)) => (Value::from(*f), CDT::float32_datatype()),
Some(LiteralType::Fp64(f)) => (Value::from(*f), CDT::float64_datatype()),
Some(LiteralType::Timestamp(t)) => (
Value::from(Timestamp::new_microsecond(*t)),
CDT::timestamp_microsecond_datatype(),
),
Some(LiteralType::PrecisionTimestamp(prec_ts)) => {
let (prec, val) = (prec_ts.precision, prec_ts.value);
let prec = TimestampPrecision::try_from(prec)?;
let unit = prec.to_time_unit();
let typ = prec.to_cdt();
(Value::from(Timestamp::new(val, unit)), typ)
}
Some(LiteralType::Date(d)) => (Value::from(Date::new(*d)), CDT::date_datatype()),
Some(LiteralType::String(s)) => (Value::from(s.clone()), CDT::string_datatype()),
Some(LiteralType::Binary(b)) | Some(LiteralType::FixedBinary(b)) => {
(Value::from(b.clone()), CDT::binary_datatype())
}
Some(LiteralType::Decimal(d)) => {
let value: [u8; 16] = d.value.clone().try_into().map_err(|e| {
PlanSnafu {
reason: format!("Failed to parse decimal value from {e:?}"),
}
.build()
})?;
let p: u8 = d.precision.try_into().map_err(|e| {
PlanSnafu {
reason: format!("Failed to parse decimal precision: {e}"),
}
.build()
})?;
let s: i8 = d.scale.try_into().map_err(|e| {
PlanSnafu {
reason: format!("Failed to parse decimal scale: {e}"),
}
.build()
})?;
let value = i128::from_le_bytes(value);
(
Value::from(Decimal128::new(value, p, s)),
CDT::decimal128_datatype(p, s),
)
}
Some(LiteralType::Null(ntype)) => (Value::Null, from_substrait_type(ntype)?),
Some(LiteralType::IntervalDayToSecond(interval)) => from_interval_day_sec(interval)?,
Some(LiteralType::IntervalYearToMonth(interval)) => from_interval_year_month(interval)?,
Some(LiteralType::IntervalCompound(interval_compound)) => {
let interval_day_time = &interval_compound
.interval_day_to_second
.map(|i| from_interval_day_sec(&i))
.transpose()?;
let interval_year_month = &interval_compound
.interval_year_to_month
.map(|i| from_interval_year_month(&i))
.transpose()?;
let mut compound = IntervalMonthDayNano::new(0, 0, 0);
if let Some(day_sec) = interval_day_time {
let Value::IntervalDayTime(day_time) = day_sec.0 else {
UnexpectedSnafu {
reason: format!("Expect IntervalDayTime, found {:?}", day_sec),
}
.fail()?
};
// 1 day in milliseconds = 24 * 60 * 60 * 1000 = 8.64e7 ms = 8.64e13 ns << 2^63
// so overflow is unexpected
compound.nanoseconds = compound
.nanoseconds
.checked_add(day_time.milliseconds as i64 * 1_000_000)
.with_context(|| UnexpectedSnafu {
reason: format!(
"Overflow when converting interval: {:?}",
interval_compound
),
})?;
compound.days += day_time.days;
}
if let Some(year_month) = interval_year_month {
let Value::IntervalYearMonth(year_month) = year_month.0 else {
UnexpectedSnafu {
reason: format!("Expect IntervalYearMonth, found {:?}", year_month),
}
.fail()?
};
compound.months += year_month.months;
}
(
Value::IntervalMonthDayNano(compound),
CDT::interval_month_day_nano_datatype(),
)
}
_ => not_impl_err!("unsupported literal_type: {:?}", &lit.literal_type)?,
};
Ok(scalar_value)
}
fn from_interval_day_sec(
interval: &proto::expression::literal::IntervalDayToSecond,
) -> Result<(Value, CDT), Error> {
let (days, seconds, subseconds) = (interval.days, interval.seconds, interval.subseconds);
let millis = if let Some(prec) = interval.precision_mode {
use substrait_proto::proto::expression::literal::interval_day_to_second::PrecisionMode;
match prec {
PrecisionMode::Precision(e) => {
if e >= 3 {
subseconds
/ 10_i64
.checked_pow((e - 3) as _)
.with_context(|| UnexpectedSnafu {
reason: format!(
"Overflow when converting interval: {:?}",
interval
),
})?
} else {
subseconds
* 10_i64
.checked_pow((3 - e) as _)
.with_context(|| UnexpectedSnafu {
reason: format!(
"Overflow when converting interval: {:?}",
interval
),
})?
}
}
PrecisionMode::Microseconds(_) => subseconds / 1000,
}
} else if subseconds == 0 {
0
} else {
not_impl_err!("unsupported subseconds without precision_mode: {subseconds}")?
};
let value_interval = common_time::IntervalDayTime::new(days, seconds * 1000 + millis as i32);
Ok((
Value::IntervalDayTime(value_interval),
CDT::interval_day_time_datatype(),
))
}
fn from_interval_year_month(
interval: &proto::expression::literal::IntervalYearToMonth,
) -> Result<(Value, CDT), Error> {
let value_interval = common_time::IntervalYearMonth::new(interval.years * 12 + interval.months);
Ok((
Value::IntervalYearMonth(value_interval),
CDT::interval_year_month_datatype(),
))
}
fn from_bytes<T: FromBytes>(i: &Bytes) -> Result<T, Error>
where
for<'a> &'a <T as num_traits::FromBytes>::Bytes:
std::convert::TryFrom<&'a [u8], Error = TryFromSliceError>,
{
let (int_bytes, _rest) = i.split_at(std::mem::size_of::<T>());
let i = T::from_le_bytes(int_bytes.try_into().map_err(|e| {
UnexpectedSnafu {
reason: format!(
"Expect slice to be {} bytes, found {} bytes, error={:?}",
std::mem::size_of::<T>(),
int_bytes.len(),
e
),
}
.build()
})?);
Ok(i)
}
/// convert a Substrait type into a ConcreteDataType
pub fn from_substrait_type(null_type: &substrait_proto::proto::Type) -> Result<CDT, Error> {
if let Some(kind) = &null_type.kind {
match kind {
Kind::Bool(_) => Ok(CDT::boolean_datatype()),
Kind::I8(integer) => match integer.type_variation_reference {
DEFAULT_TYPE_VARIATION_REF => Ok(CDT::int8_datatype()),
UNSIGNED_INTEGER_TYPE_VARIATION_REF => Ok(CDT::uint8_datatype()),
v => not_impl_err!("Unsupported Substrait type variation {v} of type {kind:?}"),
},
Kind::I16(integer) => match integer.type_variation_reference {
DEFAULT_TYPE_VARIATION_REF => Ok(CDT::int16_datatype()),
UNSIGNED_INTEGER_TYPE_VARIATION_REF => Ok(CDT::uint16_datatype()),
v => not_impl_err!("Unsupported Substrait type variation {v} of type {kind:?}"),
},
Kind::I32(integer) => match integer.type_variation_reference {
DEFAULT_TYPE_VARIATION_REF => Ok(CDT::int32_datatype()),
UNSIGNED_INTEGER_TYPE_VARIATION_REF => Ok(CDT::uint32_datatype()),
v => not_impl_err!("Unsupported Substrait type variation {v} of type {kind:?}"),
},
Kind::I64(integer) => match integer.type_variation_reference {
DEFAULT_TYPE_VARIATION_REF => Ok(CDT::int64_datatype()),
UNSIGNED_INTEGER_TYPE_VARIATION_REF => Ok(CDT::uint64_datatype()),
v => not_impl_err!("Unsupported Substrait type variation {v} of type {kind:?}"),
},
Kind::Fp32(_) => Ok(CDT::float32_datatype()),
Kind::Fp64(_) => Ok(CDT::float64_datatype()),
Kind::PrecisionTimestamp(ts) => {
Ok(TimestampPrecision::try_from(ts.precision)?.to_cdt())
}
Kind::Date(date) => match date.type_variation_reference {
DATE_32_TYPE_VARIATION_REF | DATE_64_TYPE_VARIATION_REF => Ok(CDT::date_datatype()),
v => not_impl_err!("Unsupported Substrait type variation {v} of type {kind:?}"),
},
Kind::Binary(_) => Ok(CDT::binary_datatype()),
Kind::String(_) => Ok(CDT::string_datatype()),
Kind::Decimal(d) => Ok(CDT::decimal128_datatype(d.precision as u8, d.scale as i8)),
_ => not_impl_err!("Unsupported Substrait type: {kind:?}"),
}
} else {
not_impl_err!("Null type without kind is not supported")
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
use crate::plan::{Plan, TypedPlan};
use crate::repr::{self, ColumnType, RelationType};
use crate::transform::test::{create_test_ctx, create_test_query_engine, sql_to_substrait};
/// test if literal in substrait plan can be correctly converted to flow plan
#[tokio::test]
async fn test_literal() {
let engine = create_test_query_engine();
let sql = "SELECT 1 FROM numbers";
let plan = sql_to_substrait(engine.clone(), sql).await;
let mut ctx = create_test_ctx();
let flow_plan = TypedPlan::from_substrait_plan(&mut ctx, &plan).await;
let expected = TypedPlan {
schema: RelationType::new(vec![ColumnType::new(CDT::int64_datatype(), true)])
.into_named(vec![Some("Int64(1)".to_string())]),
plan: Plan::Constant {
rows: vec![(
repr::Row::new(vec![Value::Int64(1)]),
repr::Timestamp::MIN,
1,
)],
},
};
assert_eq!(flow_plan.unwrap(), expected);
}
}
-276
View File
@@ -1,276 +0,0 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::HashSet;
use itertools::Itertools;
use snafu::OptionExt;
use substrait::substrait_proto_df::proto::{FilterRel, ReadRel};
use substrait_proto::proto::expression::MaskExpression;
use substrait_proto::proto::read_rel::ReadType;
use substrait_proto::proto::rel::RelType;
use substrait_proto::proto::{Plan as SubPlan, ProjectRel, Rel, plan_rel};
use crate::error::{Error, InvalidQuerySnafu, NotImplementedSnafu, PlanSnafu, UnexpectedSnafu};
use crate::expr::{MapFilterProject, TypedExpr};
use crate::plan::{Plan, TypedPlan};
use crate::repr::{self, RelationType};
use crate::transform::{FlownodeContext, FunctionExtensions, substrait_proto};
impl TypedPlan {
/// Convert Substrait Plan into Flow's TypedPlan
pub async fn from_substrait_plan(
ctx: &mut FlownodeContext,
plan: &SubPlan,
) -> Result<TypedPlan, Error> {
// Register function extension
let function_extension = FunctionExtensions::try_from_proto(&plan.extensions)?;
// Parse relations
match plan.relations.len() {
1 => match plan.relations[0].rel_type.as_ref() {
Some(rt) => match rt {
plan_rel::RelType::Rel(rel) => {
Ok(TypedPlan::from_substrait_rel(ctx, rel, &function_extension).await?)
}
plan_rel::RelType::Root(root) => {
let input = root.input.as_ref().with_context(|| InvalidQuerySnafu {
reason: "Root relation without input",
})?;
let mut ret =
TypedPlan::from_substrait_rel(ctx, input, &function_extension).await?;
if !root.names.is_empty() {
ret.schema = ret.schema.clone().try_with_names(root.names.clone())?;
}
Ok(ret)
}
},
None => plan_err!("Cannot parse plan relation: None"),
},
_ => not_impl_err!(
"Substrait plan with more than 1 relation trees not supported. Number of relation trees: {:?}",
plan.relations.len()
),
}
}
#[async_recursion::async_recursion]
pub async fn from_substrait_project(
ctx: &mut FlownodeContext,
p: &ProjectRel,
extensions: &FunctionExtensions,
) -> Result<TypedPlan, Error> {
let input = if let Some(input) = p.input.as_ref() {
TypedPlan::from_substrait_rel(ctx, input, extensions).await?
} else {
return not_impl_err!("Projection without an input is not supported");
};
// because this `input.schema` is incorrect for pre-expand substrait plan, so we have to use schema before expand multi-value
// function to correctly transform it, and late rewrite it
// TODO(discord9): this logic is obsoleted since now expand happens in datafusion optimizer
let schema_before_expand = {
let input_schema = input.schema.clone();
let auto_columns: HashSet<usize> =
HashSet::from_iter(input_schema.typ().auto_columns.clone());
let not_auto_added_columns = (0..input_schema.len()?)
.filter(|i| !auto_columns.contains(i))
.collect_vec();
let mfp = MapFilterProject::new(input_schema.len()?)
.project(not_auto_added_columns)?
.into_safe();
input_schema.apply_mfp(&mfp)?
};
let mut exprs: Vec<TypedExpr> = Vec::with_capacity(p.expressions.len());
for e in &p.expressions {
let expr = TypedExpr::from_substrait_rex(e, &schema_before_expand, extensions).await?;
exprs.push(expr);
}
let is_literal = exprs.iter().all(|expr| expr.expr.is_literal());
if is_literal {
let (literals, lit_types): (Vec<_>, Vec<_>) = exprs
.into_iter()
.map(|TypedExpr { expr, typ }| (expr, typ))
.unzip();
let typ = RelationType::new(lit_types);
let row = literals
.into_iter()
.map(|lit| lit.as_literal().expect("A literal"))
.collect_vec();
let row = repr::Row::new(row);
let plan = Plan::Constant {
rows: vec![(row, repr::Timestamp::MIN, 1)],
};
Ok(TypedPlan {
schema: typ.into_unnamed(),
plan,
})
} else {
input.projection(exprs)
}
}
#[async_recursion::async_recursion]
pub async fn from_substrait_filter(
ctx: &mut FlownodeContext,
filter: &FilterRel,
extensions: &FunctionExtensions,
) -> Result<TypedPlan, Error> {
let input = if let Some(input) = filter.input.as_ref() {
TypedPlan::from_substrait_rel(ctx, input, extensions).await?
} else {
return not_impl_err!("Filter without an input is not supported");
};
let expr = if let Some(condition) = filter.condition.as_ref() {
TypedExpr::from_substrait_rex(condition, &input.schema, extensions).await?
} else {
return not_impl_err!("Filter without an condition is not valid");
};
input.filter(expr)
}
pub async fn from_substrait_read(
ctx: &mut FlownodeContext,
read: &ReadRel,
_extensions: &FunctionExtensions,
) -> Result<TypedPlan, Error> {
if let Some(ReadType::NamedTable(nt)) = &read.read_type {
let query_ctx = ctx.query_context.clone().context(UnexpectedSnafu {
reason: "Query context not found",
})?;
let table_reference = match nt.names.len() {
1 => [
query_ctx.current_catalog().to_string(),
query_ctx.current_schema().clone(),
nt.names[0].clone(),
],
2 => [
query_ctx.current_catalog().to_string(),
nt.names[0].clone(),
nt.names[1].clone(),
],
3 => [
nt.names[0].clone(),
nt.names[1].clone(),
nt.names[2].clone(),
],
_ => InvalidQuerySnafu {
reason: "Expect table to have name",
}
.fail()?,
};
let table = ctx.table(&table_reference).await?;
let get_table = Plan::Get {
id: crate::expr::Id::Global(table.0),
};
let get_table = TypedPlan {
schema: table.1,
plan: get_table,
};
if let Some(MaskExpression {
select: Some(projection),
..
}) = &read.projection
{
let column_indices: Vec<usize> = projection
.struct_items
.iter()
.map(|item| item.field as usize)
.collect();
let input_arity = get_table.schema.typ().column_types.len();
let mfp = MapFilterProject::new(input_arity).project(column_indices.clone())?;
get_table.mfp(mfp.into_safe())
} else {
Ok(get_table)
}
} else {
not_impl_err!("Only NamedTable reads are supported")
}
}
/// Convert Substrait Rel into Flow's TypedPlan
/// TODO(discord9): SELECT DISTINCT(does it get compile with something else?)
pub async fn from_substrait_rel(
ctx: &mut FlownodeContext,
rel: &Rel,
extensions: &FunctionExtensions,
) -> Result<TypedPlan, Error> {
match &rel.rel_type {
Some(RelType::Project(p)) => {
Self::from_substrait_project(ctx, p.as_ref(), extensions).await
}
Some(RelType::Filter(filter)) => {
Self::from_substrait_filter(ctx, filter, extensions).await
}
Some(RelType::Read(read)) => Self::from_substrait_read(ctx, read, extensions).await,
Some(RelType::Aggregate(agg)) => {
Self::from_substrait_agg_rel(ctx, agg, extensions).await
}
_ => not_impl_err!("Unsupported relation type: {:?}", rel.rel_type),
}
}
}
#[cfg(test)]
mod test {
use datatypes::data_type::ConcreteDataType as CDT;
use datatypes::prelude::ConcreteDataType;
use pretty_assertions::assert_eq;
use super::*;
use crate::expr::GlobalId;
use crate::plan::{Plan, TypedPlan};
use crate::repr::{ColumnType, RelationType};
use crate::transform::test::{create_test_ctx, create_test_query_engine, sql_to_substrait};
#[tokio::test]
async fn test_select() {
let engine = create_test_query_engine();
let sql = "SELECT number FROM numbers";
let plan = sql_to_substrait(engine.clone(), sql).await;
let mut ctx = create_test_ctx();
let flow_plan = TypedPlan::from_substrait_plan(&mut ctx, &plan).await;
let expected = TypedPlan {
schema: RelationType::new(vec![ColumnType::new(CDT::uint32_datatype(), false)])
.into_named(vec![Some("number".to_string())]),
plan: Plan::Mfp {
input: Box::new(
Plan::Get {
id: crate::expr::Id::Global(GlobalId::User(0)),
}
.with_types(
RelationType::new(vec![ColumnType::new(
ConcreteDataType::uint32_datatype(),
false,
)])
.into_named(vec![Some("number".to_string())]),
),
),
mfp: MapFilterProject::new(1),
},
};
assert_eq!(flow_plan.unwrap(), expected);
}
}
+12 -990
View File
File diff suppressed because it is too large Load Diff
+174 -25
View File
@@ -65,6 +65,7 @@ use common_telemetry::{debug, info, tracing, warn};
use common_time::{Timestamp, Timezone};
use datafusion_common::tree_node::TreeNodeVisitor;
use datafusion_expr::LogicalPlan;
use datafusion_expr::logical_plan::Distinct;
use datatypes::prelude::ConcreteDataType;
use datatypes::schema::{ColumnSchema, Schema};
use datatypes::value::Value;
@@ -301,13 +302,33 @@ fn determine_flow_type_for_source_state(
return Ok(Some(FlowType::Batching));
}
if has_instant_ttl_source_table {
return Ok(Some(FlowType::Streaming));
}
Ok(None)
}
/// The stateless streaming runtime accepts only a single source scan wrapped by
/// projections and filters. Keep this check local to the operator: the flow
/// validator is intentionally private to the flow crate.
fn is_stateless_flow_plan(plan: &LogicalPlan) -> bool {
let mut scans = 0;
let mut supported_nodes = true;
let result = plan.apply_with_subqueries(|node| {
match node {
LogicalPlan::TableScan(_) => scans += 1,
LogicalPlan::Projection(_) | LogicalPlan::Filter(_) => {}
LogicalPlan::Distinct(Distinct::All(_)) => {}
_ => supported_nodes = false,
}
Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
});
result.is_ok() && supported_nodes && scans == 1
}
const INSTANT_TTL_FLOW_QUERY_ERROR: &str = "instant-TTL flow sources support only stateless projection/filter queries over one source; non-stateless queries require a persisted source";
const STATELESS_FLOW_QUERY_ERROR: &str =
"flow streaming supports only stateless projection/filter queries over one source";
impl StatementExecutor {
pub fn catalog_manager(&self) -> CatalogManagerRef {
self.catalog_manager.clone()
@@ -858,6 +879,9 @@ impl StatementExecutor {
/// Determines the flow type from source-table state, schedule requirements,
/// and SQL shape.
///
/// Aggregates and persisted-source DISTINCT use batching. Plain DISTINCT is request-local only
/// for instant-TTL sources, where it retains the legacy streaming compatibility behavior.
async fn determine_flow_type(
&self,
expr: &CreateFlowExpr,
@@ -941,17 +965,31 @@ impl StatementExecutor {
);
let stmt = &stmts[0];
if is_tql(query_ctx.sql_dialect(), &expr.sql)
let is_tql_query = is_tql(query_ctx.sql_dialect(), &expr.sql)
.map_err(BoxedError::new)
.context(ExternalSnafu)?
{
.context(ExternalSnafu)?;
if is_tql_query {
if has_instant_ttl_source_table {
return InvalidSqlSnafu {
err_msg: INSTANT_TTL_FLOW_QUERY_ERROR.to_string(),
}
.fail();
}
return Ok(FlowType::Batching);
}
// support tql parse too
// Plan before selecting a mode. In particular, instant-TTL sources
// must not bypass validation of the stateless streaming subset.
let plan = match stmt {
// prom ql is only supported in batching mode
Statement::Tql(_) => return Ok(FlowType::Batching),
Statement::Tql(_) => {
if has_instant_ttl_source_table {
return InvalidSqlSnafu {
err_msg: INSTANT_TTL_FLOW_QUERY_ERROR.to_string(),
}
.fail();
}
return Ok(FlowType::Batching);
}
_ => engine
.planner()
.plan(&QueryStatement::Sql(stmt.clone()), query_ctx)
@@ -960,12 +998,15 @@ impl StatementExecutor {
.context(ExternalSnafu)?,
};
/// Visitor to find aggregation or distinct
struct FindAggr {
/// Visitor to classify aggregation and DISTINCT separately. Plain DISTINCT is request
/// local only for instant-TTL sources; persisted-source DISTINCT keeps batching semantics.
struct FindQueryShape {
is_aggr: bool,
is_distinct: bool,
has_distinct_on: bool,
}
impl TreeNodeVisitor<'_> for FindAggr {
impl TreeNodeVisitor<'_> for FindQueryShape {
type Node = LogicalPlan;
fn f_down(
&mut self,
@@ -973,25 +1014,58 @@ impl StatementExecutor {
) -> datafusion_common::Result<datafusion_common::tree_node::TreeNodeRecursion>
{
match node {
LogicalPlan::Aggregate(_) | LogicalPlan::Distinct(_) => {
self.is_aggr = true;
return Ok(datafusion_common::tree_node::TreeNodeRecursion::Stop);
}
LogicalPlan::Aggregate(_) => self.is_aggr = true,
LogicalPlan::Distinct(Distinct::All(_)) => self.is_distinct = true,
LogicalPlan::Distinct(Distinct::On(_)) => self.has_distinct_on = true,
_ => (),
}
Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
}
}
let mut find_aggr = FindAggr { is_aggr: false };
let mut query_shape = FindQueryShape {
is_aggr: false,
is_distinct: false,
has_distinct_on: false,
};
plan.visit_with_subqueries(&mut find_aggr)
plan.visit_with_subqueries(&mut query_shape)
.context(BuildDfLogicalPlanSnafu)?;
if find_aggr.is_aggr {
Ok(FlowType::Batching)
} else {
Ok(FlowType::Streaming)
if query_shape.has_distinct_on {
return InvalidSqlSnafu {
err_msg: if has_instant_ttl_source_table {
INSTANT_TTL_FLOW_QUERY_ERROR.to_string()
} else {
STATELESS_FLOW_QUERY_ERROR.to_string()
},
}
.fail();
}
if query_shape.is_aggr {
return if has_instant_ttl_source_table {
InvalidSqlSnafu {
err_msg: INSTANT_TTL_FLOW_QUERY_ERROR.to_string(),
}
.fail()
} else {
Ok(FlowType::Batching)
};
}
if query_shape.is_distinct && !has_instant_ttl_source_table {
return Ok(FlowType::Batching);
}
ensure!(
is_stateless_flow_plan(&plan),
InvalidSqlSnafu {
err_msg: if has_instant_ttl_source_table {
INSTANT_TTL_FLOW_QUERY_ERROR.to_string()
} else {
STATELESS_FLOW_QUERY_ERROR.to_string()
},
}
);
Ok(FlowType::Streaming)
}
#[tracing::instrument(skip_all)]
@@ -2868,6 +2942,7 @@ mod test {
#[cfg(feature = "enterprise")]
use api::v1::meta::{ProcedureDetailResponse, ReconcileRequest, ReconcileResponse};
use arrow::datatypes::{DataType as ArrowDataType, Field as ArrowField};
#[cfg(feature = "enterprise")]
use common_meta::cache_invalidator::{CacheInvalidator, CacheInvalidatorRef};
#[cfg(feature = "enterprise")]
@@ -2890,6 +2965,9 @@ mod test {
use common_meta::rpc::procedure::{
MigrateRegionRequest, MigrateRegionResponse, ProcedureStateResponse,
};
use datafusion::functions_aggregate::expr_fn::count;
use datafusion::logical_expr::builder::LogicalTableSource;
use datafusion::logical_expr::{LogicalPlanBuilder, col};
use session::context::{QueryContext, QueryContextBuilder};
use sql::dialect::GreptimeDbDialect;
use sql::parser::{ParseOptions, ParserContext};
@@ -3281,6 +3359,77 @@ SELECT max(c1), min(c2) FROM schema_2.table_2;";
);
}
fn stateless_test_scan(name: &str) -> LogicalPlan {
let schema = arrow::datatypes::Schema::new(vec![ArrowField::new(
"value",
ArrowDataType::Int32,
true,
)]);
LogicalPlanBuilder::scan(
name,
Arc::new(LogicalTableSource::new(Arc::new(schema))),
None,
)
.unwrap()
.build()
.unwrap()
}
#[test]
fn test_is_stateless_flow_plan_accepts_scan_projection_and_filter() {
let scan = stateless_test_scan("source");
assert!(is_stateless_flow_plan(&scan));
let projection = LogicalPlanBuilder::from(scan.clone())
.project(vec![col("value")])
.unwrap()
.build()
.unwrap();
assert!(is_stateless_flow_plan(&projection));
let filter = LogicalPlanBuilder::from(projection)
.filter(col("value").gt(datafusion_expr::lit(0)))
.unwrap()
.build()
.unwrap();
assert!(is_stateless_flow_plan(&filter));
}
#[test]
fn test_is_stateless_flow_plan_rejects_aggregate_and_multiple_scans() {
let scan = stateless_test_scan("source");
let aggregate = LogicalPlanBuilder::from(scan.clone())
.aggregate(
Vec::<datafusion_expr::Expr>::new(),
vec![count(col("value"))],
)
.unwrap()
.build()
.unwrap();
assert!(!is_stateless_flow_plan(&aggregate));
let distinct = LogicalPlanBuilder::from(scan.clone())
.distinct()
.unwrap()
.build()
.unwrap();
assert!(is_stateless_flow_plan(&distinct));
let distinct_on_aggregate = LogicalPlanBuilder::from(aggregate)
.distinct()
.unwrap()
.build()
.unwrap();
assert!(!is_stateless_flow_plan(&distinct_on_aggregate));
let multiple_scans = LogicalPlanBuilder::from(stateless_test_scan("left"))
.cross_join(stateless_test_scan("right"))
.unwrap()
.build()
.unwrap();
assert!(!is_stateless_flow_plan(&multiple_scans));
}
// --- Schedule option tests ---
#[test]
@@ -3370,11 +3519,11 @@ SELECT max(c1), min(c2) FROM schema_2.table_2;";
}
#[test]
fn test_determine_flow_type_for_source_state_instant_ttl_without_missing_sources() {
fn test_determine_flow_type_for_source_state_existing_sources_require_plan() {
assert_eq!(
determine_flow_type_for_source_state("my_flow", &HashMap::new(), false, true, false)
.unwrap(),
Some(FlowType::Streaming)
None
);
}
-19
View File
@@ -22,7 +22,6 @@ use cache::{
use catalog::information_schema::NoopInformationExtension;
use catalog::kvbackend::KvBackendCatalogManagerBuilder;
use catalog::process_manager::ProcessManager;
use cmd::error::StartFlownodeSnafu;
use common_base::Plugins;
use common_catalog::consts::{MIN_USER_FLOW_ID, MIN_USER_TABLE_ID};
use common_config::KvBackendConfig;
@@ -55,7 +54,6 @@ use frontend::instance::builder::FrontendBuilder;
use frontend::server::Services;
use meta_srv::metasrv::{FLOW_ID_SEQ, TABLE_ID_SEQ};
use servers::grpc::GrpcOptions;
use snafu::ResultExt;
use standalone::options::StandaloneOptions;
use standalone::{StandaloneDatanodeManager, StandaloneRepartitionProcedureFactory};
@@ -312,23 +310,6 @@ impl GreptimeDbStandaloneBuilder {
frontend_instance_handler
.set_handler(weak_grpc_handler)
.await;
let flow_streaming_engine = flownode.flow_engine().streaming_engine();
let invoker = flow::FrontendInvoker::build_from(
flow_streaming_engine.clone(),
catalog_manager.clone(),
kv_backend.clone(),
cache_registry.clone(),
procedure_executor.clone(),
node_manager.clone(),
instance.frontend_peer_addr().to_string(),
)
.await
.context(StartFlownodeSnafu)
.unwrap();
flow_streaming_engine.set_frontend_invoker(invoker).await;
procedure_manager.start().await.unwrap();
wal_provider.start().await.unwrap();
@@ -8,7 +8,7 @@ CREATE TABLE distinct_basic (
Affected Rows: 0
-- should fallback to streaming mode when there is no EVAL INTERVAL
-- request-local DISTINCT is supported in streaming mode
-- SQLNESS REPLACE id=\d+ id=REDACTED
CREATE FLOW test_distinct_basic SINK TO out_distinct_basic AS
SELECT
@@ -18,8 +18,18 @@ FROM
Affected Rows: 0
-- instant-TTL sources reject non-stateless LIMIT plans
CREATE FLOW test_limit_instant_rejected SINK TO out_limit_instant_rejected AS
SELECT
number
FROM
distinct_basic
LIMIT 1;
Error: 1004(InvalidArguments), Invalid SQL, error: instant-TTL flow sources support only stateless projection/filter queries over one source; non-stateless queries require a persisted source
-- flow_options should have a flow_type:streaming
-- since source table's ttl=instant
-- since source table's ttl=instant and DISTINCT is request-local
SELECT flow_name, options FROM INFORMATION_SCHEMA.FLOWS;
+---------------------+---------------------------+
@@ -113,9 +123,25 @@ ADMIN FLUSH_TABLE('distinct_basic');
| 0 |
+-------------------------------------+
-- Recover the persisted streaming DISTINCT flow, then replan its first write
-- against an extended source schema without recreating the flow.
-- SQLNESS ARG restart=true
SELECT 1;
+----------+
| Int64(1) |
+----------+
| 1 |
+----------+
ALTER TABLE distinct_basic ADD COLUMN extra INT NULL;
Affected Rows: 0
INSERT INTO
distinct_basic
distinct_basic (number, ts)
VALUES
(23, "2021-07-01 00:00:01.600"),
(23, "2021-07-01 00:00:01.600");
Affected Rows: 0
@@ -194,7 +220,7 @@ DROP TABLE distinct_basic;
Affected Rows: 0
-- test ttl = 5s
-- test ttl = 5s (DISTINCT remains batching for persisted sources)
CREATE TABLE distinct_basic (
"number" INT,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@@ -204,6 +230,22 @@ CREATE TABLE distinct_basic (
Affected Rows: 0
-- Without a schedule, persisted DISTINCT must reach batching validation,
-- not silently become request-local streaming.
CREATE FLOW test_distinct_persisted_unscheduled
SINK TO out_distinct_persisted_unscheduled AS
SELECT DISTINCT number AS dis FROM distinct_basic;
Error: 3001(EngineExecuteQuery), Invalid query: SQL batching flow without a time-window expression must specify EVAL INTERVAL to run as an explicit full-query flow
DROP FLOW IF EXISTS test_distinct_persisted_unscheduled;
Affected Rows: 0
DROP TABLE IF EXISTS out_distinct_persisted_unscheduled;
Affected Rows: 0
CREATE FLOW test_distinct_basic SINK TO out_distinct_basic EVAL INTERVAL '1m' AS
SELECT
DISTINCT number as dis
@@ -213,7 +255,7 @@ FROM
Affected Rows: 0
-- flow_options should have a flow_type:batching
-- since source table's ttl=instant
-- persisted-source DISTINCT retains batching semantics
SELECT flow_name, options FROM INFORMATION_SCHEMA.FLOWS;
+---------------------+--------------------------+
@@ -6,7 +6,7 @@ CREATE TABLE distinct_basic (
TIME INDEX(ts)
)WITH ('ttl' = 'instant');
-- should fallback to streaming mode when there is no EVAL INTERVAL
-- request-local DISTINCT is supported in streaming mode
-- SQLNESS REPLACE id=\d+ id=REDACTED
CREATE FLOW test_distinct_basic SINK TO out_distinct_basic AS
SELECT
@@ -14,8 +14,16 @@ SELECT
FROM
distinct_basic;
-- instant-TTL sources reject non-stateless LIMIT plans
CREATE FLOW test_limit_instant_rejected SINK TO out_limit_instant_rejected AS
SELECT
number
FROM
distinct_basic
LIMIT 1;
-- flow_options should have a flow_type:streaming
-- since source table's ttl=instant
-- since source table's ttl=instant and DISTINCT is request-local
SELECT flow_name, options FROM INFORMATION_SCHEMA.FLOWS;
SHOW CREATE TABLE distinct_basic;
@@ -44,9 +52,17 @@ SELECT number FROM distinct_basic;
-- SQLNESS SLEEP 6s
ADMIN FLUSH_TABLE('distinct_basic');
-- Recover the persisted streaming DISTINCT flow, then replan its first write
-- against an extended source schema without recreating the flow.
-- SQLNESS ARG restart=true
SELECT 1;
ALTER TABLE distinct_basic ADD COLUMN extra INT NULL;
INSERT INTO
distinct_basic
distinct_basic (number, ts)
VALUES
(23, "2021-07-01 00:00:01.600"),
(23, "2021-07-01 00:00:01.600");
-- SQLNESS REPLACE (ADMIN\sFLUSH_FLOW\('\w+'\)\s+\|\n\+-+\+\n\|\s+)[0-9]+\s+\| $1 FLOW_FLUSHED |
@@ -84,7 +100,7 @@ SELECT count(*) FROM INFORMATION_SCHEMA.FLOWS WHERE flow_name = 'test_distinct_b
DROP TABLE distinct_basic;
-- test ttl = 5s
-- test ttl = 5s (DISTINCT remains batching for persisted sources)
CREATE TABLE distinct_basic (
"number" INT,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@@ -92,6 +108,15 @@ CREATE TABLE distinct_basic (
TIME INDEX(ts)
)WITH ('ttl' = '5s');
-- Without a schedule, persisted DISTINCT must reach batching validation,
-- not silently become request-local streaming.
CREATE FLOW test_distinct_persisted_unscheduled
SINK TO out_distinct_persisted_unscheduled AS
SELECT DISTINCT number AS dis FROM distinct_basic;
DROP FLOW IF EXISTS test_distinct_persisted_unscheduled;
DROP TABLE IF EXISTS out_distinct_persisted_unscheduled;
CREATE FLOW test_distinct_basic SINK TO out_distinct_basic EVAL INTERVAL '1m' AS
SELECT
DISTINCT number as dis
@@ -99,7 +124,7 @@ FROM
distinct_basic;
-- flow_options should have a flow_type:batching
-- since source table's ttl=instant
-- persisted-source DISTINCT retains batching semantics
SELECT flow_name, options FROM INFORMATION_SCHEMA.FLOWS;
-- SQLNESS ARG restart=true
@@ -405,12 +405,12 @@ SELECT number FROM out_num_cnt_show;
-- should mismatch, hence the old flow remains
CREATE OR REPLACE FLOW filter_numbers_show SINK TO out_num_cnt_show AS SELECT number AS n1, number AS n2 FROM numbers_input_show where number > 15;
Error: 3001(EngineExecuteQuery), Invalid query: Column 1(name is 'ts', flow inferred name is 'n2')'s data type mismatch, expect Timestamp(Millisecond(TimestampMillisecondType)) got Int32(Int32Type)
Error: 3001(EngineExecuteQuery), Invalid query: Flow output column 1 has type Int32(Int32Type), but sink column ts has type Timestamp(Millisecond(TimestampMillisecondType))
-- should mismatch, hence the old flow remains
CREATE OR REPLACE FLOW filter_numbers_show SINK TO out_num_cnt_show AS SELECT number AS n1, number AS n2, number AS n3 FROM numbers_input_show where number > 15;
Error: 3001(EngineExecuteQuery), Invalid query: Column 1(name is 'ts', flow inferred name is 'n2')'s data type mismatch, expect Timestamp(Millisecond(TimestampMillisecondType)) got Int32(Int32Type)
Error: 3001(EngineExecuteQuery), Invalid query: Flow output has 3 columns, but sink has 2 columns; only zero, one, or two trailing auto columns are supported
SELECT flow_definition, source_table_names FROM INFORMATION_SCHEMA.FLOWS WHERE flow_name='filter_numbers_show';