feat(flow): activate sequence_range incremental mode

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
This commit is contained in:
discord9
2026-08-13 21:47:56 +08:00
parent a29a2a5ede
commit 23fe843cc5
12 changed files with 528 additions and 37 deletions
@@ -16,7 +16,7 @@ use std::sync::{Arc, Weak};
use common_catalog::consts::INFORMATION_SCHEMA_FLOW_TABLE_ID;
use common_error::ext::BoxedError;
use common_meta::ddl::create_flow::FlowType;
use common_meta::ddl::create_flow::{FlowType, INTERNAL_INCREMENTAL_MODE_KEY};
use common_meta::key::FlowId;
use common_meta::key::flow::FlowMetadataManager;
use common_meta::key::flow::flow_info::FlowInfoValue;
@@ -171,7 +171,7 @@ impl InformationSchemaFlows {
comment,
flow_options: sql::statements::OptionMap::from_filtered_string_map(
flow_info.options(),
&[FlowType::FLOW_TYPE_KEY],
&[FlowType::FLOW_TYPE_KEY, INTERNAL_INCREMENTAL_MODE_KEY],
),
query,
};
@@ -464,3 +464,57 @@ impl DfPartitionStream for InformationSchemaFlows {
))
}
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeMap, HashMap};
use chrono::Utc;
use common_meta::ddl::create_flow::{
DEFER_ON_MISSING_SOURCE_KEY, FlowType, INTERNAL_INCREMENTAL_MODE_KEY,
};
use common_meta::key::flow::flow_info::{FlowInfoValue, FlowStatus};
use table::table_name::TableName;
use super::InformationSchemaFlows;
fn test_flow_info() -> FlowInfoValue {
FlowInfoValue {
source_table_ids: vec![],
all_source_table_names: vec![],
unresolved_source_table_names: vec![],
sink_table_name: TableName::new("greptime", "public", "my_sink"),
flownode_ids: BTreeMap::new(),
catalog_name: "greptime".to_string(),
query_context: None,
flow_name: "my_flow".to_string(),
raw_sql: "SELECT number FROM numbers".to_string(),
expire_after: None,
eval_interval_secs: None,
comment: String::new(),
options: HashMap::from([
(DEFER_ON_MISSING_SOURCE_KEY.to_string(), "true".to_string()),
(FlowType::FLOW_TYPE_KEY.to_string(), "batching".to_string()),
(
INTERNAL_INCREMENTAL_MODE_KEY.to_string(),
"sequence_range".to_string(),
),
]),
status: FlowStatus::Active,
created_time: Utc::now(),
updated_time: Utc::now(),
eval_schedule: None,
}
}
#[test]
fn test_generate_show_create_flow_hides_internal_options() {
let flow_info = test_flow_info();
let sql = InformationSchemaFlows::generate_show_create_flow(&flow_info).unwrap();
// The user option survives and the reserved internal keys are hidden.
assert!(sql.contains("defer_on_missing_source = 'true'"));
assert!(!sql.contains(FlowType::FLOW_TYPE_KEY));
assert!(!sql.contains(INTERNAL_INCREMENTAL_MODE_KEY));
}
}
+5 -3
View File
@@ -473,6 +473,10 @@ pub const DEFER_ON_MISSING_SOURCE_KEY: &str = "defer_on_missing_source";
/// field in the flow create request.
pub const INTERNAL_EVAL_SCHEDULE_KEY: &str = "__greptime_internal_eval_schedule";
/// Reserved internal per-flow runtime intent (`memtable_only` | `sequence_range`),
/// injected only by internal producers and persisted with flow metadata for recovery.
pub const INTERNAL_INCREMENTAL_MODE_KEY: &str = "__greptime_internal_incremental_mode";
const FLOW_SCHEDULED_TIME_MILLIS_EXTENSION_KEY: &str = "flow.scheduled_time_millis";
fn without_scheduled_time_extension(mut query_context: QueryContext) -> QueryContext {
@@ -519,6 +523,7 @@ pub fn validate_flow_options(flow_task: &CreateFlowTask) -> Result<()> {
match key.as_str() {
DEFER_ON_MISSING_SOURCE_KEY
| FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY
| INTERNAL_INCREMENTAL_MODE_KEY
| FlowType::FLOW_TYPE_KEY => {}
unknown => {
return UnexpectedSnafu {
@@ -772,9 +777,6 @@ impl From<&CreateFlowData> for (FlowInfoValue, Vec<(FlowPartitionId, FlowRouteVa
let sql = value.task.sql.clone();
let eval_schedule = value.task.eval_schedule.clone();
// Start with a clean options map. The transient schedule payload is
// only for the meta→flownode CreateRequest boundary and must not be
// persisted in FlowInfoValue.options.
let mut options: HashMap<String, String> = value
.task
.flow_options
+28 -5
View File
@@ -25,9 +25,9 @@ use table::table_name::TableName;
use crate::ddl::DdlContext;
use crate::ddl::create_flow::{
CreateFlowData, CreateFlowProcedure, CreateFlowState, DEFER_ON_MISSING_SOURCE_KEY,
FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY, FlowType, defer_on_missing_source,
effective_eval_schedule_from_flow_info, resolve_schedule_defaults_into_task,
validate_flow_options,
FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY, FlowType, INTERNAL_INCREMENTAL_MODE_KEY,
defer_on_missing_source, effective_eval_schedule_from_flow_info,
resolve_schedule_defaults_into_task, validate_flow_options,
};
use crate::ddl::test_util::create_table::test_create_table_task;
use crate::ddl::test_util::flownode_handler::NaiveFlownodeHandler;
@@ -325,6 +325,24 @@ fn test_validate_flow_options_allows_incremental_read_option() {
validate_flow_options(&task).unwrap();
}
#[test]
fn test_validate_flow_options_accepts_internal_incremental_mode_key() {
// Meta accepts the reserved key because an internal producer may inject
// it after user option validation; value validation is the flow crate's job.
let mut task = test_create_flow_task(
"my_flow",
vec![],
TableName::new(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, "my_sink_table"),
false,
);
task.flow_options.insert(
INTERNAL_INCREMENTAL_MODE_KEY.to_string(),
"sequence_range".to_string(),
);
validate_flow_options(&task).unwrap();
}
#[test]
fn test_validate_flow_options_rejects_schedule_and_internal_keys_as_unknown() {
for key in [
@@ -346,11 +364,16 @@ fn test_validate_flow_options_rejects_schedule_and_internal_keys_as_unknown() {
.insert(key.to_string(), "value".to_string());
let err = validate_flow_options(&task).unwrap_err();
let msg = err.to_string();
assert!(
err.to_string()
.contains(&format!("Unknown flow option '{key}'")),
msg.contains(&format!("Unknown flow option '{key}'")),
"unexpected error for {key}: {err}"
);
let supported = msg
.split("supported user options: ")
.nth(1)
.unwrap_or_default();
assert!(!supported.contains(INTERNAL_INCREMENTAL_MODE_KEY), "{err}");
}
}
+48 -3
View File
@@ -26,7 +26,8 @@ use catalog::CatalogManager;
use common_base::Plugins;
use common_error::ext::BoxedError;
use common_meta::ddl::create_flow::{
FlowType, INTERNAL_EVAL_SCHEDULE_KEY, effective_eval_schedule_from_flow_info,
FlowType, INTERNAL_EVAL_SCHEDULE_KEY, INTERNAL_INCREMENTAL_MODE_KEY,
effective_eval_schedule_from_flow_info,
};
use common_meta::error::Result as MetaResult;
use common_meta::key::flow::FlowMetadataManager;
@@ -663,6 +664,24 @@ impl SrcTableToFlow {
}
}
/// The streaming engine ignores `flow_options`, so injected incremental intent
/// must surface as an internal error rather than be silently dropped.
fn validate_flow_options_for_engine(
flow_type: FlowType,
flow_options: &HashMap<String, String>,
) -> Result<(), Error> {
if flow_type == FlowType::Streaming && flow_options.contains_key(INTERNAL_INCREMENTAL_MODE_KEY)
{
return InternalSnafu {
reason: format!(
"internal flow option '{INTERNAL_INCREMENTAL_MODE_KEY}' is only valid for batching flows"
),
}
.fail();
}
Ok(())
}
impl FlowEngine for FlowDualEngine {
async fn create_flow(&self, args: CreateFlowArgs) -> Result<Option<FlowId>, Error> {
let flow_type = args
@@ -682,6 +701,8 @@ impl FlowEngine for FlowDualEngine {
}
};
validate_flow_options_for_engine(flow_type, &args.flow_options)?;
let flow_id = args.flow_id;
let src_table_ids = args.source_table_ids.clone();
@@ -1164,9 +1185,11 @@ impl StreamingEngine {
mod tests {
use std::collections::HashMap;
use common_meta::ddl::create_flow::INTERNAL_EVAL_SCHEDULE_KEY;
use common_meta::ddl::create_flow::{
FlowType, INTERNAL_EVAL_SCHEDULE_KEY, INTERNAL_INCREMENTAL_MODE_KEY,
};
use super::decode_internal_eval_schedule;
use super::{decode_internal_eval_schedule, validate_flow_options_for_engine};
use crate::error::Error;
#[test]
@@ -1185,4 +1208,26 @@ mod tests {
));
assert!(!flow_options.contains_key(INTERNAL_EVAL_SCHEDULE_KEY));
}
#[test]
fn test_internal_incremental_mode_guard_rejects_streaming_accepts_batching() {
let mut stream_options = HashMap::new();
stream_options.insert(
INTERNAL_INCREMENTAL_MODE_KEY.to_string(),
"sequence_range".to_string(),
);
let err =
validate_flow_options_for_engine(FlowType::Streaming, &stream_options).unwrap_err();
assert!(matches!(
err,
Error::Internal { reason, .. } if reason.contains(INTERNAL_INCREMENTAL_MODE_KEY)
));
let mut batch_options = HashMap::new();
batch_options.insert(
INTERNAL_INCREMENTAL_MODE_KEY.to_string(),
"sequence_range".to_string(),
);
validate_flow_options_for_engine(FlowType::Batching, &batch_options).unwrap();
}
}
+36
View File
@@ -30,6 +30,18 @@ mod task;
mod time_window;
pub(crate) mod utils;
/// Incremental read mode for a batching flow, selected only through the
/// reserved internal flow option
/// [`common_meta::ddl::create_flow::INTERNAL_INCREMENTAL_MODE_KEY`].
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum IncrementalMode {
/// Read only memtables newer than the checkpoint, skipping SSTs entirely.
#[default]
MemtableOnly,
/// Exact row-level sequence delta `(C, H]` across memtables and all SSTs.
SequenceRange,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BatchingModeOptions {
/// The default batching engine query timeout is 10 minutes
@@ -59,6 +71,11 @@ pub struct BatchingModeOptions {
///
/// When disabled, batching flows always execute full-snapshot queries.
pub experimental_enable_incremental_read: bool,
/// Internal incremental read mode for batching flows, injected only
/// through the reserved internal flow option
/// [`common_meta::ddl::create_flow::INTERNAL_INCREMENTAL_MODE_KEY`].
#[serde(skip)]
pub incremental_mode: IncrementalMode,
/// Read preference of the Frontend client.
pub read_preference: ReadPreference,
/// TLS option for client connections to frontends.
@@ -77,8 +94,27 @@ impl Default for BatchingModeOptions {
experimental_max_filter_num_per_query: 20,
experimental_time_window_merge_threshold: 3,
experimental_enable_incremental_read: false,
incremental_mode: IncrementalMode::default(),
read_preference: Default::default(),
frontend_tls: None,
}
}
}
#[cfg(test)]
mod tests {
use super::{BatchingModeOptions, IncrementalMode};
#[test]
fn test_incremental_mode_not_exposed_by_options_serialization() {
// `#[serde(skip)]`: the runtime-only field never appears in
// serialized options, and injected input is ignored.
let serialized = serde_json::to_string(&BatchingModeOptions::default()).unwrap();
assert!(!serialized.contains("incremental_mode"));
let mut json = serde_json::to_value(BatchingModeOptions::default()).unwrap();
json["incremental_mode"] = serde_json::json!("sequence_range");
let opts: BatchingModeOptions = serde_json::from_value(json).unwrap();
assert_eq!(opts.incremental_mode, IncrementalMode::default());
}
}
+51 -3
View File
@@ -21,7 +21,9 @@ use std::time::Duration;
use api::v1::flow::DirtyWindowRequests;
use catalog::CatalogManagerRef;
use common_error::ext::BoxedError;
use common_meta::ddl::create_flow::{FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY, FlowType};
use common_meta::ddl::create_flow::{
FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY, FlowType, INTERNAL_INCREMENTAL_MODE_KEY,
};
use common_meta::key::TableMetadataManagerRef;
use common_meta::key::flow::FlowMetadataManagerRef;
use common_meta::key::flow::flow_state::FlowStat;
@@ -43,17 +45,18 @@ use store_api::storage::{RegionId, TableId};
use table::table_reference::TableReference;
use tokio::sync::{RwLock, oneshot};
use crate::batching_mode::BatchingModeOptions;
use crate::batching_mode::eval_schedule::EvalSchedule;
use crate::batching_mode::frontend_client::FrontendClient;
use crate::batching_mode::state::DirtyTimeWindows;
use crate::batching_mode::task::{BatchingTask, TaskArgs};
use crate::batching_mode::time_window::{TimeWindowExpr, find_time_window_expr};
use crate::batching_mode::utils::sql_to_df_plan;
use crate::batching_mode::{BatchingModeOptions, IncrementalMode};
use crate::engine::{FlowEngine, FlowStatProvider};
use crate::error::{
CreateFlowSnafu, DatafusionSnafu, ExternalSnafu, FlowAlreadyExistSnafu, FlowNotFoundSnafu,
InvalidQuerySnafu, JoinTaskSnafu, TableNotFoundMetaSnafu, UnexpectedSnafu, UnsupportedSnafu,
InternalSnafu, InvalidQuerySnafu, JoinTaskSnafu, TableNotFoundMetaSnafu, UnexpectedSnafu,
UnsupportedSnafu,
};
use crate::metrics::METRIC_FLOW_BATCHING_ENGINE_BULK_MARK_TIME_WINDOW;
use crate::{CreateFlowArgs, Error, FlowId, TableName};
@@ -482,6 +485,24 @@ impl BatchingEngine {
})?;
}
if let Some(incremental_mode) = flow_options.get(INTERNAL_INCREMENTAL_MODE_KEY) {
let lowered = incremental_mode.trim().to_ascii_lowercase();
batch_opts.incremental_mode = match lowered.as_str() {
"memtable_only" => IncrementalMode::MemtableOnly,
"sequence_range" => IncrementalMode::SequenceRange,
_ => {
// A bad value in the reserved internal key is a broken
// internal contract, not a user input error.
return Err(InternalSnafu {
reason: format!(
"Invalid internal flow option {INTERNAL_INCREMENTAL_MODE_KEY}: {incremental_mode}"
),
}
.build());
}
};
}
Ok(Arc::new(batch_opts))
}
@@ -1099,6 +1120,33 @@ mod tests {
assert!(enabled_opts.experimental_enable_incremental_read);
}
#[tokio::test]
async fn test_flow_option_parses_incremental_mode() {
let engine = new_test_engine().await;
let default_opts = engine.batch_opts_for_flow_options(&HashMap::new()).unwrap();
assert_eq!(IncrementalMode::MemtableOnly, default_opts.incremental_mode);
let sequence_range_opts = engine
.batch_opts_for_flow_options(&HashMap::from([(
INTERNAL_INCREMENTAL_MODE_KEY.to_string(),
" sequence_range ".to_string(),
)]))
.unwrap();
assert_eq!(
IncrementalMode::SequenceRange,
sequence_range_opts.incremental_mode
);
let err = engine
.batch_opts_for_flow_options(&HashMap::from([(
INTERNAL_INCREMENTAL_MODE_KEY.to_string(),
"bogus".to_string(),
)]))
.unwrap_err();
assert!(matches!(err, Error::Internal { .. }), "{err}");
}
#[test]
fn test_table_options_enable_append_mode() {
assert!(!BatchingEngine::table_options_enable_append_mode(
+7 -5
View File
@@ -20,12 +20,13 @@ use common_telemetry::tracing::warn;
use datafusion_expr::{DmlStatement, LogicalPlan};
use query::options::{
FLOW_INCREMENTAL_AFTER_SEQS, FLOW_INCREMENTAL_MODE, FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY,
FLOW_SINK_TABLE_ID,
FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE, FLOW_SINK_TABLE_ID,
};
use snafu::ResultExt;
use table::metadata::TableId;
use crate::Error;
use crate::batching_mode::IncrementalMode;
use crate::batching_mode::state::CheckpointMode;
use crate::batching_mode::table_creator::QueryType;
use crate::batching_mode::task::BatchingTask;
@@ -221,10 +222,11 @@ impl BatchingTask {
if let Some(checkpoints_json) = incremental_checkpoints_json {
let sink_table_id = self.sink_table_id().await?;
extensions.push((FLOW_SINK_TABLE_ID, sink_table_id.to_string()));
extensions.push((
FLOW_INCREMENTAL_MODE,
FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY.to_string(),
));
let incremental_mode = match self.config.batch_opts.incremental_mode {
IncrementalMode::SequenceRange => FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE,
IncrementalMode::MemtableOnly => FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY,
};
extensions.push((FLOW_INCREMENTAL_MODE, incremental_mode.to_string()));
extensions.push((FLOW_INCREMENTAL_AFTER_SEQS, checkpoints_json));
}
+36 -2
View File
@@ -31,14 +31,15 @@ use datatypes::vectors::{
};
use pretty_assertions::assert_eq;
use query::options::{
FLOW_INCREMENTAL_AFTER_SEQS, FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY, FLOW_SCHEDULED_TIME_MILLIS,
QueryOptions,
FLOW_INCREMENTAL_AFTER_SEQS, FLOW_INCREMENTAL_MODE, FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY,
FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE, FLOW_SCHEDULED_TIME_MILLIS, QueryOptions,
};
use session::context::QueryContext;
use snafu::ResultExt;
use table::test_util::MemTable;
use super::*;
use crate::batching_mode::IncrementalMode;
use crate::batching_mode::checkpoint::{
CHECKPOINT_DECISION_ADVANCE, CHECKPOINT_DECISION_FALLBACK, CHECKPOINT_REASON_NONE,
FlowCheckpointDecision, FlowQueryFallbackReason,
@@ -1729,6 +1730,39 @@ async fn test_build_flow_query_extensions_switches_with_checkpoint_mode() {
);
}
// `sequence_range` must flow into the emitted extensions only with a
// checkpoint map present, requesting exact filtering across memtables and all
// SSTs; the default mode keeps emitting `memtable_only`.
#[tokio::test]
async fn test_build_flow_query_extensions_sequence_range_mode() {
let (task, _) = new_test_task_engine_and_plan_with_query_and_opts(
"SELECT number, ts FROM numbers_with_ts",
"numbers_with_ts",
Arc::new(BatchingModeOptions {
experimental_enable_incremental_read: true,
incremental_mode: IncrementalMode::SequenceRange,
..Default::default()
}),
)
.await
.into_task_and_plan();
task.state
.write()
.unwrap()
.advance_checkpoints(HashMap::from([(1_u64, 10_u64)]));
let extensions = task.build_flow_query_extensions(true, true).await.unwrap();
assert!(extensions.contains(&(
FLOW_INCREMENTAL_MODE,
FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE.to_string()
)));
assert!(extensions.contains(&(
FLOW_INCREMENTAL_AFTER_SEQS,
serde_json::json!({"1": 10}).to_string(),
)));
}
#[tokio::test]
async fn test_full_snapshot_scoped_plan_marks_checkpoint_advance_safe_only_after_backlog_drained() {
let TestTaskParts {
+24 -2
View File
@@ -42,6 +42,7 @@ use common_meta::cache_invalidator::CacheInvalidatorRef;
use common_meta::cache_invalidator::Context;
use common_meta::ddl::create_flow::{
DEFER_ON_MISSING_SOURCE_KEY, FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY, FlowType,
INTERNAL_INCREMENTAL_MODE_KEY,
};
#[cfg(feature = "enterprise")]
use common_meta::ddl_manager::FlowExtensionRef;
@@ -128,7 +129,12 @@ struct DdlSubmitOptions {
timeout: Duration,
}
const ALLOWED_FLOW_OPTIONS: &[&str] = &[
/// User-facing flow options accepted from SQL `WITH (...)`. The reserved
/// internal option [`INTERNAL_INCREMENTAL_MODE_KEY`] is not a user option:
/// it is rejected here and can only be injected into the underlying
/// `CreateFlowTask` by an internal producer (e.g. FlowExtension) after user
/// option validation.
const ALLOWED_FLOW_OPTIONS: [&str; 2] = [
DEFER_ON_MISSING_SOURCE_KEY,
FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY,
];
@@ -219,7 +225,7 @@ fn validate_and_normalize_flow_options(
let mut extension_options = HashMap::new();
for (key, value) in options {
if key == FlowType::FLOW_TYPE_KEY {
if key == FlowType::FLOW_TYPE_KEY || key == INTERNAL_INCREMENTAL_MODE_KEY {
return InvalidSqlSnafu {
err_msg: format!("flow option '{key}' is reserved for internal use"),
}
@@ -3398,6 +3404,22 @@ mod test {
);
}
#[test]
fn test_validate_and_normalize_flow_options_rejects_internal_incremental_mode_key() {
let err = validate_and_normalize_flow_options(
HashMap::from([(
INTERNAL_INCREMENTAL_MODE_KEY.to_string(),
"sequence_range".to_string(),
)]),
None,
)
.unwrap_err();
assert!(err.to_string().contains(&format!(
"flow option '{INTERNAL_INCREMENTAL_MODE_KEY}' is reserved for internal use"
)));
}
#[test]
fn test_validate_and_normalize_flow_options_invalid_bool() {
let err = validate_and_normalize_flow_options(
+74 -3
View File
@@ -371,6 +371,10 @@ struct FlowScanDecision {
memtable_max_sequence: Option<u64>,
/// Whether to skip SST files for memtable-only incremental source scans.
skip_sst_files: bool,
/// Explicit intent to read an exact row-level sequence delta `(min, max]`
/// across memtables and all SST files (`flow.incremental_mode=sequence_range`).
/// Historical `memtable_only` reads never set this.
exact_sequence_range: bool,
}
impl FlowScanDecision {
@@ -381,6 +385,7 @@ impl FlowScanDecision {
memtable_min_sequence: None,
memtable_max_sequence: None,
skip_sst_files: false,
exact_sequence_range: false,
}
}
}
@@ -395,6 +400,7 @@ fn decide_flow_scan(query_ctx: &QueryContext, region_id: RegionId) -> Result<Flo
memtable_min_sequence: None,
memtable_max_sequence: query_ctx.get_snapshot(region_id.as_u64()),
skip_sst_files: false,
exact_sequence_range: false,
});
};
@@ -426,9 +432,14 @@ fn decide_flow_scan(query_ctx: &QueryContext, region_id: RegionId) -> Result<Flo
// into SSTs, instead of silently bypassing the stale-fence check. If a future
// incremental delta also carries an upper bound, the lower-bound stale check
// still proves whether memtable-only is safe.
let skip_sst_files = apply_incremental
&& memtable_min_sequence.is_some()
&& flow_extensions.incremental_mode == Some(FlowIncrementalMode::MemtableOnly);
let is_memtable_only =
flow_extensions.incremental_mode == Some(FlowIncrementalMode::MemtableOnly);
// `sequence_range` is the exact row-level delta: it always includes SSTs and
// explicitly requests row-level filtering across memtables and every SST.
let is_sequence_range =
flow_extensions.incremental_mode == Some(FlowIncrementalMode::SequenceRange);
let skip_sst_files = apply_incremental && memtable_min_sequence.is_some() && is_memtable_only;
Ok(FlowScanDecision {
is_sink_scan: false,
@@ -437,6 +448,9 @@ fn decide_flow_scan(query_ctx: &QueryContext, region_id: RegionId) -> Result<Flo
memtable_min_sequence,
memtable_max_sequence,
skip_sst_files,
exact_sequence_range: apply_incremental
&& memtable_min_sequence.is_some()
&& is_sequence_range,
})
}
@@ -456,6 +470,7 @@ fn build_scan_request(
snapshot_on_scan: decision.snapshot_on_scan,
memtable_min_sequence: decision.memtable_min_sequence,
memtable_max_sequence: decision.memtable_max_sequence,
exact_sequence_range: decision.exact_sequence_range,
..Default::default()
}
}
@@ -791,6 +806,62 @@ mod tests {
assert_eq!(request.memtable_max_sequence, Some(42));
assert!(!request.snapshot_on_scan);
assert!(request.skip_sst_files);
// Historical memtable-only never requests the exact capability.
assert!(!request.exact_sequence_range);
}
#[test]
fn test_scan_request_from_query_context_sequence_range_mode_includes_ssts() {
let region_id = test_region_id();
let query_ctx = QueryContextBuilder::default()
.extensions(HashMap::from([
(
FLOW_INCREMENTAL_MODE.to_string(),
"sequence_range".to_string(),
),
(
FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
format!(r#"{{"{}":55}}"#, region_id.as_u64()),
),
]))
.snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
region_id.as_u64(),
77_u64,
)]))))
.build();
let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
assert_eq!(request.memtable_min_sequence, Some(55));
assert_eq!(request.memtable_max_sequence, Some(77));
assert!(!request.skip_sst_files);
assert!(request.exact_sequence_range);
assert!(!request.snapshot_on_scan);
}
#[test]
fn test_scan_request_from_query_context_sequence_range_binds_snapshot_on_open() {
let region_id = test_region_id();
let query_ctx = QueryContextBuilder::default()
.extensions(HashMap::from([
(
FLOW_INCREMENTAL_MODE.to_string(),
"sequence_range".to_string(),
),
(
FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
format!(r#"{{"{}":55}}"#, region_id.as_u64()),
),
]))
.build();
let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
assert_eq!(request.memtable_min_sequence, Some(55));
assert_eq!(request.memtable_max_sequence, None);
assert!(request.snapshot_on_scan);
assert!(!request.skip_sst_files);
assert!(request.exact_sequence_range);
}
#[test]
+100 -6
View File
@@ -37,6 +37,7 @@ pub const QUERY_ENABLE_REMOTE_DYNAMIC_FILTER_PUSHDOWN: &str =
"query.enable_remote_dynamic_filter_pushdown";
pub const FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY: &str = "memtable_only";
pub const FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE: &str = "sequence_range";
/// Query engine config
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -69,7 +70,15 @@ impl Default for QueryOptions {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlowIncrementalMode {
/// Historical incremental delta: read only memtables newer than the
/// checkpoint, skipping SSTs entirely. Fences are never relaxed by region
/// options; a checkpoint behind the flushed frontier stays stale.
MemtableOnly,
/// Exact row-level sequence delta `(C, H]` across memtables and all SST
/// files. Requires the source region to preserve per-row sequences; when the
/// exact capability is unavailable the engine returns a structured
/// stale/unsupported error instead of silently approximating.
SequenceRange,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
@@ -106,6 +115,9 @@ impl FlowQueryExtensions {
v if v.eq_ignore_ascii_case(FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY) => {
Ok(FlowIncrementalMode::MemtableOnly)
}
v if v.eq_ignore_ascii_case(FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE) => {
Ok(FlowIncrementalMode::SequenceRange)
}
_ => Err(invalid_query_context_extension(format!(
"Invalid value for {}: {}",
FLOW_INCREMENTAL_MODE, value
@@ -136,13 +148,25 @@ impl FlowQueryExtensions {
})
.transpose()?;
if matches!(incremental_mode, Some(FlowIncrementalMode::MemtableOnly)) {
if matches!(
incremental_mode,
Some(FlowIncrementalMode::MemtableOnly | FlowIncrementalMode::SequenceRange)
) {
let after_seqs = incremental_after_seqs.as_ref().ok_or_else(|| {
invalid_query_context_extension(format!(
"{} is required when {}={}.",
FLOW_INCREMENTAL_AFTER_SEQS,
FLOW_INCREMENTAL_MODE,
FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY
incremental_mode
.map(|m| match m {
FlowIncrementalMode::MemtableOnly => {
FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY
}
FlowIncrementalMode::SequenceRange => {
FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE
}
})
.unwrap_or_default()
))
})?;
if after_seqs.is_empty() {
@@ -150,7 +174,16 @@ impl FlowQueryExtensions {
"{} must not be empty when {}={}.",
FLOW_INCREMENTAL_AFTER_SEQS,
FLOW_INCREMENTAL_MODE,
FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY
incremental_mode
.map(|m| match m {
FlowIncrementalMode::MemtableOnly => {
FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY
}
FlowIncrementalMode::SequenceRange => {
FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE
}
})
.unwrap_or_default()
)));
}
}
@@ -170,18 +203,18 @@ impl FlowQueryExtensions {
if matches!(
self.incremental_mode,
Some(FlowIncrementalMode::MemtableOnly)
Some(FlowIncrementalMode::MemtableOnly | FlowIncrementalMode::SequenceRange)
) {
let after_seqs = self.incremental_after_seqs.as_ref().ok_or_else(|| {
invalid_query_context_extension(format!(
"{} is required when {}=memtable_only.",
"{} is required when {}=memtable_only or sequence_range.",
FLOW_INCREMENTAL_AFTER_SEQS, FLOW_INCREMENTAL_MODE
))
})?;
if !after_seqs.contains_key(&source_region_id.as_u64()) {
return Err(invalid_query_context_extension(format!(
"Missing region {} in {} when {}=memtable_only.",
"Missing region {} in {} when {}=memtable_only or sequence_range.",
source_region_id, FLOW_INCREMENTAL_AFTER_SEQS, FLOW_INCREMENTAL_MODE
)));
}
@@ -449,6 +482,67 @@ mod flow_extension_tests {
assert_eq!(parsed.sink_table_id, Some(1024));
}
#[test]
fn test_parse_flow_extensions_sequence_range_success() {
let exts = HashMap::from([
(
FLOW_INCREMENTAL_MODE.to_string(),
FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE.to_string(),
),
(
FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
r#"{"1":10,"2":20}"#.to_string(),
),
(FLOW_RETURN_REGION_SEQ.to_string(), "true".to_string()),
]);
let parsed = FlowQueryExtensions::parse_flow_extensions(&exts)
.unwrap()
.unwrap();
assert_eq!(
parsed.incremental_mode,
Some(FlowIncrementalMode::SequenceRange)
);
assert_eq!(
parsed.incremental_after_seqs.unwrap(),
HashMap::from([(1, 10), (2, 20)])
);
assert!(parsed.return_region_seq);
}
#[test]
fn test_parse_flow_extensions_sequence_range_requires_after_seqs() {
let exts = HashMap::from([(
FLOW_INCREMENTAL_MODE.to_string(),
FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE.to_string(),
)]);
let err = FlowQueryExtensions::parse_flow_extensions(&exts).unwrap_err();
assert!(format!("{err}").contains(FLOW_INCREMENTAL_AFTER_SEQS));
}
#[test]
fn test_validate_for_scan_sequence_range_missing_source_region() {
let source_region_id = RegionId::new(100, 2);
let existing_region_id = RegionId::new(100, 1);
let exts = HashMap::from([
(
FLOW_INCREMENTAL_MODE.to_string(),
FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE.to_string(),
),
(
FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
format!(r#"{{"{}":10}}"#, existing_region_id.as_u64()),
),
]);
let parsed = FlowQueryExtensions::parse_flow_extensions(&exts)
.unwrap()
.unwrap();
let err = parsed.validate_for_scan(source_region_id).unwrap_err();
assert!(format!("{err}").contains("Missing region"));
}
#[test]
fn test_parse_flow_extensions_mode_requires_after_seqs() {
let exts = HashMap::from([(
+63 -3
View File
@@ -33,7 +33,7 @@ use common_datasource::file_format::{FileFormat, Format, infer_schemas};
use common_datasource::lister::{Lister, Source};
use common_datasource::object_store::{LocalFileAccess, build_backend_with_path};
use common_meta::SchemaOptions;
use common_meta::ddl::create_flow::FlowType;
use common_meta::ddl::create_flow::{FlowType, INTERNAL_INCREMENTAL_MODE_KEY};
use common_meta::key::flow::flow_info::FlowInfoValue;
use common_query::Output;
use common_query::prelude::greptime_timestamp;
@@ -1079,7 +1079,7 @@ pub fn show_create_flow(
comment,
flow_options: OptionMap::from_filtered_string_map(
flow_val.options(),
&[FlowType::FLOW_TYPE_KEY],
&[FlowType::FLOW_TYPE_KEY, INTERNAL_INCREMENTAL_MODE_KEY],
),
query,
};
@@ -1384,8 +1384,14 @@ pub async fn show_processlist(
#[cfg(test)]
mod test {
use std::collections::BTreeMap;
use std::sync::Arc;
use chrono::Utc;
use common_meta::ddl::create_flow::{
DEFER_ON_MISSING_SOURCE_KEY, FlowType, INTERNAL_INCREMENTAL_MODE_KEY,
};
use common_meta::key::flow::flow_info::{FlowInfoValue, FlowStatus};
use common_query::{Output, OutputData};
use common_recordbatch::{RecordBatch, RecordBatches};
use common_time::Timezone;
@@ -1399,9 +1405,10 @@ mod test {
use sql::ast::{Ident, ObjectName};
use sql::statements::show::ShowVariables;
use table::TableRef;
use table::table_name::TableName;
use table::test_util::MemTable;
use super::{describe_column_type_name, show_variable};
use super::{describe_column_type_name, show_create_flow, show_variable};
use crate::error;
use crate::error::Result;
use crate::sql::{
@@ -1530,4 +1537,57 @@ mod test {
Err(e) => Err(e),
}
}
#[test]
fn test_show_create_flow_hides_internal_options() {
let flow_info = FlowInfoValue {
source_table_ids: vec![],
all_source_table_names: vec![],
unresolved_source_table_names: vec![],
sink_table_name: TableName::new("greptime", "public", "my_sink"),
flownode_ids: BTreeMap::new(),
catalog_name: "greptime".to_string(),
query_context: None,
flow_name: "my_flow".to_string(),
raw_sql: "SELECT number FROM numbers".to_string(),
expire_after: None,
eval_interval_secs: None,
comment: String::new(),
options: std::collections::HashMap::from([
(DEFER_ON_MISSING_SOURCE_KEY.to_string(), "true".to_string()),
(FlowType::FLOW_TYPE_KEY.to_string(), "batching".to_string()),
(
INTERNAL_INCREMENTAL_MODE_KEY.to_string(),
"sequence_range".to_string(),
),
]),
status: FlowStatus::Active,
created_time: Utc::now(),
updated_time: Utc::now(),
eval_schedule: None,
};
let ctx = Arc::new(QueryContextBuilder::default().build());
let output = show_create_flow(
ObjectName::from(vec![Ident::new("my_flow")]),
flow_info,
ctx,
)
.unwrap();
let sql = match output.data {
OutputData::RecordBatches(record) => record
.take()
.first()
.unwrap()
.iter_column_as_string(1)
.next()
.unwrap()
.unwrap(),
_ => unreachable!(),
};
// The user option survives and the reserved internal keys are hidden.
assert!(sql.contains("defer_on_missing_source = 'true'"));
assert!(!sql.contains(FlowType::FLOW_TYPE_KEY));
assert!(!sql.contains(INTERNAL_INCREMENTAL_MODE_KEY));
}
}