1use std::collections::HashMap;
16
17use api::v1::region::{
18 BulkInsertRequest, RegionRequest, RegionRequestHeader, bulk_insert_request, region_request,
19};
20use api::v1::{ArrowIpc, PartitionExprVersion};
21use arrow::compute;
22use arrow::record_batch::RecordBatch;
23use async_trait::async_trait;
24use catalog::CatalogManagerRef;
25use common_error::ext::BoxedError;
26use common_grpc::error::Error as GrpcError;
27use common_grpc::flight;
28use common_query::WriteProgress;
29use common_telemetry::tracing_context::TracingContext;
30use operator::write_dispatcher::TrackedWrite;
31use operator::write_dispatcher::WriteDispatcherRef;
32use partition::manager::PartitionRuleManagerRef;
33use partition::partition::PartitionRuleRef;
34use snafu::ResultExt;
35use store_api::storage::RegionId;
36use table::metadata::{TableId, TableInfo, TableInfoRef};
37
38use crate::error;
39use crate::error::{Error, Result};
40use crate::metrics;
41
42#[derive(Clone)]
43pub struct PhysicalTableMetadata {
44 pub table_info: TableInfoRef,
45 pub col_name_to_ids: Option<HashMap<String, u32>>,
47}
48
49#[async_trait]
50pub trait PhysicalFlushCatalogProvider: Send + Sync {
51 async fn physical_table(
52 &self,
53 catalog: &str,
54 schema: &str,
55 table_name: &str,
56 query_ctx: &session::context::QueryContext,
57 ) -> catalog::error::Result<Option<PhysicalTableMetadata>>;
58}
59
60#[async_trait]
62pub trait PhysicalFlushPartitionProvider: Send + Sync {
63 async fn find_table_partition_rule(
65 &self,
66 table_info: &TableInfo,
67 ) -> partition::error::Result<(PartitionRuleRef, HashMap<u32, Option<u64>>)>;
68}
69
70#[async_trait]
72pub trait PhysicalFlushWriteDispatcher: Send + Sync {
73 async fn dispatch(&self, requests: Vec<RegionRequest>) -> Result<usize>;
75
76 async fn dispatch_tracked(
77 &self,
78 requests: Vec<TrackedWrite>,
79 inputs: usize,
80 ) -> Vec<WriteProgress> {
81 let mut progress = vec![WriteProgress::default(); inputs];
82 let total = requests.iter().map(|r| r.owners.len()).sum::<usize>();
83 let owners = requests
84 .iter()
85 .flat_map(|r| r.owners.iter().copied())
86 .collect::<Vec<_>>();
87 match self
88 .dispatch(requests.into_iter().map(|r| r.request).collect())
89 .await
90 {
91 Ok(rows) if rows == total => {
92 for owner in owners {
93 progress[owner].affected_rows += 1;
94 }
95 }
96 Ok(_) => {
97 return vec![
98 WriteProgress::failed(BoxedError::new(
99 error::InternalSnafu {
100 err_msg: "Unattributable physical write acknowledgement"
101 }
102 .build()
103 ));
104 inputs
105 ];
106 }
107 Err(error) => return vec![WriteProgress::failed(BoxedError::new(error)); inputs],
108 }
109 progress
110 }
111}
112
113#[derive(Clone)]
114pub(in crate::batcher::logical_table) struct CatalogManagerPhysicalFlushAdapter {
115 pub(in crate::batcher::logical_table) catalog_manager: CatalogManagerRef,
116}
117
118#[async_trait]
119impl PhysicalFlushCatalogProvider for CatalogManagerPhysicalFlushAdapter {
120 async fn physical_table(
121 &self,
122 catalog: &str,
123 schema: &str,
124 table_name: &str,
125 query_ctx: &session::context::QueryContext,
126 ) -> catalog::error::Result<Option<PhysicalTableMetadata>> {
127 self.catalog_manager
128 .table(catalog, schema, table_name, Some(query_ctx))
129 .await
130 .map(|table| {
131 table.map(|table| {
132 let table_info = table.table_info();
133 let name_to_ids = table_info.name_to_ids();
134 PhysicalTableMetadata {
135 table_info,
136 col_name_to_ids: name_to_ids,
137 }
138 })
139 })
140 }
141}
142
143#[derive(Clone)]
144pub(in crate::batcher::logical_table) struct PartitionManagerPhysicalFlushAdapter {
145 pub(in crate::batcher::logical_table) partition_manager: PartitionRuleManagerRef,
146}
147
148#[async_trait]
149impl PhysicalFlushPartitionProvider for PartitionManagerPhysicalFlushAdapter {
150 async fn find_table_partition_rule(
151 &self,
152 table_info: &TableInfo,
153 ) -> partition::error::Result<(PartitionRuleRef, HashMap<u32, Option<u64>>)> {
154 self.partition_manager
155 .find_table_partition_rule(table_info)
156 .await
157 }
158}
159
160#[derive(Clone)]
162pub(crate) struct WriteDispatcherPhysicalFlushAdapter {
163 pub(crate) write_dispatcher: WriteDispatcherRef,
164}
165
166#[async_trait]
167impl PhysicalFlushWriteDispatcher for WriteDispatcherPhysicalFlushAdapter {
168 async fn dispatch_tracked(
169 &self,
170 requests: Vec<TrackedWrite>,
171 inputs: usize,
172 ) -> Vec<WriteProgress> {
173 self.write_dispatcher
174 .dispatch_tracked(
175 requests,
176 inputs,
177 Some(
178 &metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
179 .with_label_values(&["flush_write_region"]),
180 ),
181 )
182 .await
183 }
184 async fn dispatch(&self, requests: Vec<RegionRequest>) -> Result<usize> {
185 self.write_dispatcher
186 .dispatch_with_metrics(
187 requests,
188 metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
189 .with_label_values(&["flush_write_region"]),
190 )
191 .await
192 .context(error::CommonMetaSnafu)
193 .map(|response| response.affected_rows)
194 }
195}
196
197pub(crate) struct PlannedRegionBatch {
199 pub(crate) region_id: RegionId,
200 pub(crate) batch: RecordBatch,
201}
202
203#[cfg(test)]
204impl PlannedRegionBatch {
205 pub(crate) fn num_rows(&self) -> usize {
206 self.batch.num_rows()
207 }
208}
209
210pub(in crate::batcher::logical_table) fn split_combined_batch_by_region(
211 combined_batch: &RecordBatch,
212 partition_rule: &dyn partition::partition::PartitionRule,
213) -> Result<HashMap<u32, partition::partition::RegionMask>> {
214 let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
215 .with_label_values(&["flush_physical_split_record_batch"])
216 .start_timer();
217 let map = partition_rule.split_record_batch(combined_batch)?;
218 Ok(map)
219}
220
221pub(crate) fn plan_region_batch(
223 combined_batch: &RecordBatch,
224 physical_table_id: TableId,
225 region_number: u32,
226 mask: &partition::partition::RegionMask,
227) -> Result<Option<PlannedRegionBatch>> {
228 if mask.select_none() {
229 return Ok(None);
230 }
231
232 let region_batch = if mask.select_all() {
233 combined_batch.clone()
234 } else {
235 let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
236 .with_label_values(&["flush_physical_filter_record_batch"])
237 .start_timer();
238 compute::filter_record_batch(combined_batch, mask.array()).context(error::ArrowSnafu)?
239 };
240
241 let row_count = region_batch.num_rows();
242 if row_count == 0 {
243 return Ok(None);
244 }
245
246 Ok(Some(PlannedRegionBatch {
247 region_id: RegionId::new(physical_table_id, region_number),
248 batch: region_batch,
249 }))
250}
251
252#[cfg(test)]
254pub(crate) fn plan_region_batches(
255 combined_batch: RecordBatch,
256 physical_table_id: TableId,
257 partition_rule: &dyn partition::partition::PartitionRule,
258) -> Result<Vec<PlannedRegionBatch>> {
259 let region_masks = split_combined_batch_by_region(&combined_batch, partition_rule)?;
260
261 let mut planned_batches = Vec::new();
264 for (region_number, mask) in region_masks {
265 if let Some(planned_batch) =
266 plan_region_batch(&combined_batch, physical_table_id, region_number, &mask)?
267 {
268 planned_batches.push(planned_batch);
269 }
270 }
271
272 Ok(planned_batches)
273}
274
275pub(crate) fn encode_region_write_request(
277 planned: PlannedRegionBatch,
278 skip_wal: bool,
279 partition_versions: &HashMap<u32, Option<u64>>,
280) -> Result<RegionRequest> {
281 let region_id = planned.region_id;
282 let (schema_bytes, data_header, payload) = {
283 let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
284 .with_label_values(&["flush_physical_encode_ipc"])
285 .start_timer();
286 flight::record_batch_to_ipc(planned.batch).map_err(map_ipc_error)?
287 };
288
289 Ok(RegionRequest {
290 header: Some(RegionRequestHeader {
291 tracing_context: TracingContext::from_current_span().to_w3c(),
292 ..Default::default()
293 }),
294 body: Some(region_request::Body::BulkInsert(BulkInsertRequest {
295 skip_wal,
296 region_id: region_id.as_u64(),
297 partition_expr_version: partition_versions
298 .get(®ion_id.region_number())
299 .copied()
300 .flatten()
301 .map(|value| PartitionExprVersion { value }),
302 aligned_schema_version: None,
305 body: Some(bulk_insert_request::Body::ArrowIpc(ArrowIpc {
306 schema: schema_bytes,
307 data_header,
308 payload,
309 })),
310 })),
311 })
312}
313
314pub(in crate::batcher::logical_table) fn map_ipc_error(error: GrpcError) -> Error {
315 match error {
316 GrpcError::NotSupported { feat } => Error::NotSupported { feat },
317 GrpcError::InvalidFlightData { reason, .. } => Error::Internal { err_msg: reason },
318 error => Error::Internal {
319 err_msg: error.to_string(),
320 },
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use std::collections::{HashMap, HashSet};
327 use std::sync::Arc;
328
329 use api::region::RegionResponse;
330 use api::v1::PartitionExprVersion;
331 use api::v1::region::{
332 InsertRequest, InsertRequests, RegionRequest, bulk_insert_request, region_request,
333 };
334 use arrow::array::{Array, BinaryArray, BooleanArray, StringArray, TimestampMillisecondArray};
335 use arrow::datatypes::{DataType as ArrowDataType, Field, Schema as ArrowSchema};
336 use arrow::record_batch::RecordBatch;
337 use common_grpc::flight::FlightDecoder;
338 use common_meta::ddl::test_util::datanode_handler::DatanodeWatcher;
339 use common_meta::test_util::MockDatanodeManager;
340 use operator::test_util::{create_partition_rule_manager, prepare_mocked_backend};
341 use operator::write_dispatcher::{TrackedWrite, WriteDispatcher};
342 use partition::partition::{PartitionRule, RegionMask};
343 use store_api::storage::RegionId;
344
345 use crate::batcher::logical_table::batch_convert::{self, RecordBatchWithTsIdx, TableBatch};
346 use crate::batcher::logical_table::region_write::{
347 PhysicalFlushWriteDispatcher, PlannedRegionBatch, WriteDispatcherPhysicalFlushAdapter,
348 encode_region_write_request, plan_region_batches,
349 };
350 use crate::metrics;
351
352 #[tokio::test]
353 async fn test_tracked_dispatch_records_flush_write_region_metric() {
354 let partitions = create_partition_rule_manager(prepare_mocked_backend().await).await;
355 let (sender, _receiver) = tokio::sync::mpsc::channel(1);
356 let handler = DatanodeWatcher::new(sender).with_handler(|_, _| Ok(RegionResponse::new(1)));
357 let dispatcher = WriteDispatcherPhysicalFlushAdapter {
358 write_dispatcher: Arc::new(WriteDispatcher::new(
359 Arc::new(MockDatanodeManager::new(handler)),
360 partitions,
361 )),
362 };
363 let histogram = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
364 .with_label_values(&["flush_write_region"]);
365 let before = histogram.get_sample_count();
366 let progress = dispatcher
367 .dispatch_tracked(
368 vec![TrackedWrite {
369 request: RegionRequest {
370 body: Some(region_request::Body::Inserts(InsertRequests {
371 requests: vec![InsertRequest {
372 region_id: RegionId::new(1, 1).as_u64(),
373 ..Default::default()
374 }],
375 })),
376 ..Default::default()
377 },
378 owners: vec![0],
379 }],
380 1,
381 )
382 .await;
383
384 assert_eq!(progress[0].affected_rows, 1);
385 assert!(progress[0].error.is_none());
386 assert_eq!(histogram.get_sample_count(), before + 1);
387 }
388
389 #[test]
390 fn test_plan_region_batches_preserves_partition_columns_through_encoding() {
391 let logical_batch = RecordBatch::try_new(
392 Arc::new(ArrowSchema::new(vec![
393 Field::new(
394 "greptime_timestamp",
395 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
396 false,
397 ),
398 Field::new("greptime_value", ArrowDataType::Float64, true),
399 Field::new("host", ArrowDataType::Utf8, true),
400 Field::new("zone", ArrowDataType::Utf8, true),
401 ])),
402 vec![
403 Arc::new(TimestampMillisecondArray::from(vec![1000_i64, 2000_i64])),
404 Arc::new(arrow::array::Float64Array::from(vec![1.0_f64, 2.0_f64])),
405 Arc::new(StringArray::from(vec!["node-1", "node-2"])),
406 Arc::new(StringArray::from(vec!["east", "west"])),
407 ],
408 )
409 .unwrap();
410 let table_batches = [TableBatch {
411 table_name: "metric".to_string(),
412 table_id: 2048,
413 batches: vec![RecordBatchWithTsIdx::try_new(logical_batch, 0).unwrap()],
414 row_count: 2,
415 }];
416 let modified = batch_convert::transform_logical_batches_to_physical(
417 &table_batches,
418 &HashMap::from([("host".to_string(), 1), ("zone".to_string(), 2)]),
419 &HashSet::from(["host"]),
420 )
421 .unwrap();
422 let combined_batch = batch_convert::concat_modified_batches(&modified).unwrap();
423 assert!(combined_batch.column_by_name("host").is_some());
424 assert!(combined_batch.column_by_name("zone").is_none());
425 let expected = combined_batch.clone();
426 let mut planned_batches = plan_region_batches(
427 combined_batch,
428 1024,
429 &TwoRegionPartitionRule {
430 partition_columns: vec!["host".to_string()],
431 },
432 )
433 .unwrap();
434 planned_batches.sort_by_key(|planned| planned.region_id.region_number());
435
436 assert_eq!(2, planned_batches.len());
437 assert_eq!(RegionId::new(1024, 1), planned_batches[0].region_id);
438 assert_eq!(1, planned_batches[0].num_rows());
439 assert_eq!(4, planned_batches[0].batch.num_columns());
440 assert_eq!(RegionId::new(1024, 2), planned_batches[1].region_id);
441 assert_eq!(1, planned_batches[1].num_rows());
442 assert_eq!(4, planned_batches[1].batch.num_columns());
443
444 let partition_versions = HashMap::from([(1, Some(7)), (2, Some(8))]);
445 for (index, planned) in planned_batches.into_iter().enumerate() {
446 let write = encode_region_write_request(planned, false, &partition_versions).unwrap();
447 let Some(region_request::Body::BulkInsert(request)) = write.body else {
448 panic!("expected bulk")
449 };
450 assert_eq!(
451 request.partition_expr_version.unwrap().value,
452 7 + index as u64
453 );
454 let Some(bulk_insert_request::Body::ArrowIpc(ipc)) = request.body else {
455 panic!("expected IPC")
456 };
457 let batch = FlightDecoder::try_from_schema_bytes(&ipc.schema)
458 .unwrap()
459 .try_decode_record_batch(&ipc.data_header, &ipc.payload)
460 .unwrap();
461 assert!(batch.column_by_name("zone").is_none());
464 assert_eq!(batch, expected.slice(index, 1));
465 let tags = batch
466 .column_by_name("host")
467 .unwrap()
468 .as_any()
469 .downcast_ref::<StringArray>()
470 .unwrap();
471 assert_eq!(tags.len(), 1);
472 assert_eq!(tags.value(0), if index == 0 { "node-1" } else { "node-2" });
473 }
474 }
475
476 #[test]
477 fn test_encode_region_write_request_builds_bulk_insert_request() {
478 check_encode_region_write_request(false);
479 check_encode_region_write_request(true);
480 }
481
482 struct TwoRegionPartitionRule {
483 partition_columns: Vec<String>,
484 }
485
486 impl PartitionRule for TwoRegionPartitionRule {
487 fn as_any(&self) -> &dyn std::any::Any {
488 self
489 }
490
491 fn partition_columns(&self) -> &[String] {
492 &self.partition_columns
493 }
494
495 fn find_region(
496 &self,
497 _values: &[datatypes::prelude::Value],
498 ) -> partition::error::Result<store_api::storage::RegionNumber> {
499 unimplemented!()
500 }
501
502 fn split_record_batch(
503 &self,
504 _record_batch: &RecordBatch,
505 ) -> partition::error::Result<HashMap<store_api::storage::RegionNumber, RegionMask>>
506 {
507 Ok(HashMap::from([
508 (1, RegionMask::new(BooleanArray::from(vec![true, false]), 1)),
509 (2, RegionMask::new(BooleanArray::from(vec![false, true]), 1)),
510 (
511 3,
512 RegionMask::new(BooleanArray::from(vec![false, false]), 0),
513 ),
514 ]))
515 }
516 }
517
518 fn check_encode_region_write_request(skip_wal: bool) {
519 let planned_batch = PlannedRegionBatch {
520 region_id: RegionId::new(1024, 1),
521 batch: RecordBatch::try_new(
522 Arc::new(ArrowSchema::new(vec![
523 Field::new("__primary_key", ArrowDataType::Binary, false),
524 Field::new(
525 "greptime_timestamp",
526 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
527 false,
528 ),
529 Field::new("greptime_value", ArrowDataType::Float64, true),
530 ])),
531 vec![
532 Arc::new(BinaryArray::from(vec![b"k1".as_slice()])),
533 Arc::new(TimestampMillisecondArray::from(vec![1000_i64])),
534 Arc::new(arrow::array::Float64Array::from(vec![1.0_f64])),
535 ],
536 )
537 .unwrap(),
538 };
539 let write =
540 encode_region_write_request(planned_batch, skip_wal, &HashMap::from([(1, Some(7))]))
541 .unwrap();
542
543 let Some(region_request::Body::BulkInsert(request)) = &write.body else {
544 panic!("expected bulk insert request");
545 };
546 assert_eq!(RegionId::new(1024, 1).as_u64(), request.region_id);
547 assert_eq!(skip_wal, request.skip_wal);
548 assert_eq!(
549 Some(PartitionExprVersion { value: 7 }),
550 request.partition_expr_version
551 );
552 }
553}