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_grpc::error::Error as GrpcError;
26use common_grpc::flight;
27use common_telemetry::tracing_context::TracingContext;
28use operator::write_dispatcher::WriteDispatcherRef;
29use partition::manager::PartitionRuleManagerRef;
30use partition::partition::PartitionRuleRef;
31use snafu::ResultExt;
32use store_api::storage::RegionId;
33use table::metadata::{TableId, TableInfo, TableInfoRef};
34
35use crate::error;
36use crate::error::{Error, Result};
37use crate::metrics;
38
39#[derive(Clone)]
40pub struct PhysicalTableMetadata {
41 pub table_info: TableInfoRef,
42 pub col_name_to_ids: Option<HashMap<String, u32>>,
44}
45
46#[async_trait]
47pub trait PhysicalFlushCatalogProvider: Send + Sync {
48 async fn physical_table(
49 &self,
50 catalog: &str,
51 schema: &str,
52 table_name: &str,
53 query_ctx: &session::context::QueryContext,
54 ) -> catalog::error::Result<Option<PhysicalTableMetadata>>;
55}
56
57#[async_trait]
59pub trait PhysicalFlushPartitionProvider: Send + Sync {
60 async fn find_table_partition_rule(
62 &self,
63 table_info: &TableInfo,
64 ) -> partition::error::Result<(PartitionRuleRef, HashMap<u32, Option<u64>>)>;
65}
66
67#[async_trait]
69pub trait PhysicalFlushWriteDispatcher: Send + Sync {
70 async fn dispatch(&self, requests: Vec<RegionRequest>) -> Result<usize>;
72}
73
74#[derive(Clone)]
75pub(in crate::batcher::logical_table) struct CatalogManagerPhysicalFlushAdapter {
76 pub(in crate::batcher::logical_table) catalog_manager: CatalogManagerRef,
77}
78
79#[async_trait]
80impl PhysicalFlushCatalogProvider for CatalogManagerPhysicalFlushAdapter {
81 async fn physical_table(
82 &self,
83 catalog: &str,
84 schema: &str,
85 table_name: &str,
86 query_ctx: &session::context::QueryContext,
87 ) -> catalog::error::Result<Option<PhysicalTableMetadata>> {
88 self.catalog_manager
89 .table(catalog, schema, table_name, Some(query_ctx))
90 .await
91 .map(|table| {
92 table.map(|table| {
93 let table_info = table.table_info();
94 let name_to_ids = table_info.name_to_ids();
95 PhysicalTableMetadata {
96 table_info,
97 col_name_to_ids: name_to_ids,
98 }
99 })
100 })
101 }
102}
103
104#[derive(Clone)]
105pub(in crate::batcher::logical_table) struct PartitionManagerPhysicalFlushAdapter {
106 pub(in crate::batcher::logical_table) partition_manager: PartitionRuleManagerRef,
107}
108
109#[async_trait]
110impl PhysicalFlushPartitionProvider for PartitionManagerPhysicalFlushAdapter {
111 async fn find_table_partition_rule(
112 &self,
113 table_info: &TableInfo,
114 ) -> partition::error::Result<(PartitionRuleRef, HashMap<u32, Option<u64>>)> {
115 self.partition_manager
116 .find_table_partition_rule(table_info)
117 .await
118 }
119}
120
121#[derive(Clone)]
123pub(crate) struct WriteDispatcherPhysicalFlushAdapter {
124 pub(crate) write_dispatcher: WriteDispatcherRef,
125}
126
127#[async_trait]
128impl PhysicalFlushWriteDispatcher for WriteDispatcherPhysicalFlushAdapter {
129 async fn dispatch(&self, requests: Vec<RegionRequest>) -> Result<usize> {
130 self.write_dispatcher
131 .dispatch_with_metrics(
132 requests,
133 metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
134 .with_label_values(&["flush_write_region"]),
135 )
136 .await
137 .context(error::CommonMetaSnafu)
138 .map(|response| response.affected_rows)
139 }
140}
141
142pub(crate) struct PlannedRegionBatch {
144 pub(crate) region_id: RegionId,
145 pub(crate) batch: RecordBatch,
146}
147
148#[cfg(test)]
149impl PlannedRegionBatch {
150 pub(crate) fn num_rows(&self) -> usize {
151 self.batch.num_rows()
152 }
153}
154
155pub(in crate::batcher::logical_table) fn split_combined_batch_by_region(
156 combined_batch: &RecordBatch,
157 partition_rule: &dyn partition::partition::PartitionRule,
158) -> Result<HashMap<u32, partition::partition::RegionMask>> {
159 let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
160 .with_label_values(&["flush_physical_split_record_batch"])
161 .start_timer();
162 let map = partition_rule.split_record_batch(combined_batch)?;
163 Ok(map)
164}
165
166pub(crate) fn plan_region_batch(
168 combined_batch: &RecordBatch,
169 physical_table_id: TableId,
170 region_number: u32,
171 mask: &partition::partition::RegionMask,
172) -> Result<Option<PlannedRegionBatch>> {
173 if mask.select_none() {
174 return Ok(None);
175 }
176
177 let region_batch = if mask.select_all() {
178 combined_batch.clone()
179 } else {
180 let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
181 .with_label_values(&["flush_physical_filter_record_batch"])
182 .start_timer();
183 compute::filter_record_batch(combined_batch, mask.array()).context(error::ArrowSnafu)?
184 };
185
186 let row_count = region_batch.num_rows();
187 if row_count == 0 {
188 return Ok(None);
189 }
190
191 Ok(Some(PlannedRegionBatch {
192 region_id: RegionId::new(physical_table_id, region_number),
193 batch: region_batch,
194 }))
195}
196
197pub(crate) fn plan_region_batches(
199 combined_batch: RecordBatch,
200 physical_table_id: TableId,
201 partition_rule: &dyn partition::partition::PartitionRule,
202) -> Result<Vec<PlannedRegionBatch>> {
203 let region_masks = split_combined_batch_by_region(&combined_batch, partition_rule)?;
204
205 let mut planned_batches = Vec::new();
208 for (region_number, mask) in region_masks {
209 if let Some(planned_batch) =
210 plan_region_batch(&combined_batch, physical_table_id, region_number, &mask)?
211 {
212 planned_batches.push(planned_batch);
213 }
214 }
215
216 Ok(planned_batches)
217}
218
219pub(crate) fn encode_region_write_requests(
221 planned_batches: Vec<PlannedRegionBatch>,
222 skip_wal: bool,
223 partition_versions: &HashMap<u32, Option<u64>>,
224) -> Result<Vec<RegionRequest>> {
225 let mut region_writes = Vec::with_capacity(planned_batches.len());
226 for planned in planned_batches {
227 let region_id = planned.region_id;
228 let (schema_bytes, data_header, payload) = {
229 let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
230 .with_label_values(&["flush_physical_encode_ipc"])
231 .start_timer();
232 flight::record_batch_to_ipc(planned.batch).map_err(map_ipc_error)?
233 };
234
235 let request = RegionRequest {
236 header: Some(RegionRequestHeader {
237 tracing_context: TracingContext::from_current_span().to_w3c(),
238 ..Default::default()
239 }),
240 body: Some(region_request::Body::BulkInsert(BulkInsertRequest {
241 skip_wal,
242 region_id: region_id.as_u64(),
243 partition_expr_version: partition_versions
244 .get(®ion_id.region_number())
245 .copied()
246 .flatten()
247 .map(|value| PartitionExprVersion { value }),
248 aligned_schema_version: None,
251 body: Some(bulk_insert_request::Body::ArrowIpc(ArrowIpc {
252 schema: schema_bytes,
253 data_header,
254 payload,
255 })),
256 })),
257 };
258
259 region_writes.push(request);
260 }
261
262 Ok(region_writes)
263}
264
265pub(in crate::batcher::logical_table) fn map_ipc_error(error: GrpcError) -> Error {
266 match error {
267 GrpcError::NotSupported { feat } => Error::NotSupported { feat },
268 GrpcError::InvalidFlightData { reason, .. } => Error::Internal { err_msg: reason },
269 error => Error::Internal {
270 err_msg: error.to_string(),
271 },
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use std::collections::{HashMap, HashSet};
278 use std::sync::Arc;
279
280 use api::v1::PartitionExprVersion;
281 use api::v1::region::{bulk_insert_request, region_request};
282 use arrow::array::{Array, BinaryArray, BooleanArray, StringArray, TimestampMillisecondArray};
283 use arrow::datatypes::{DataType as ArrowDataType, Field, Schema as ArrowSchema};
284 use arrow::record_batch::RecordBatch;
285 use common_grpc::flight::FlightDecoder;
286 use partition::partition::{PartitionRule, RegionMask};
287 use store_api::storage::RegionId;
288
289 use crate::batcher::logical_table::batch_convert::{self, RecordBatchWithTsIdx, TableBatch};
290 use crate::batcher::logical_table::region_write::{
291 PlannedRegionBatch, encode_region_write_requests, plan_region_batches,
292 };
293
294 #[test]
295 fn test_plan_region_batches_preserves_partition_columns_through_encoding() {
296 let logical_batch = RecordBatch::try_new(
297 Arc::new(ArrowSchema::new(vec![
298 Field::new(
299 "greptime_timestamp",
300 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
301 false,
302 ),
303 Field::new("greptime_value", ArrowDataType::Float64, true),
304 Field::new("host", ArrowDataType::Utf8, true),
305 Field::new("zone", ArrowDataType::Utf8, true),
306 ])),
307 vec![
308 Arc::new(TimestampMillisecondArray::from(vec![1000_i64, 2000_i64])),
309 Arc::new(arrow::array::Float64Array::from(vec![1.0_f64, 2.0_f64])),
310 Arc::new(StringArray::from(vec!["node-1", "node-2"])),
311 Arc::new(StringArray::from(vec!["east", "west"])),
312 ],
313 )
314 .unwrap();
315 let table_batches = [TableBatch {
316 table_name: "metric".to_string(),
317 table_id: 2048,
318 batches: vec![RecordBatchWithTsIdx::try_new(logical_batch, 0).unwrap()],
319 row_count: 2,
320 }];
321 let modified = batch_convert::transform_logical_batches_to_physical(
322 &table_batches,
323 &HashMap::from([("host".to_string(), 1), ("zone".to_string(), 2)]),
324 &HashSet::from(["host"]),
325 )
326 .unwrap();
327 let combined_batch = batch_convert::concat_modified_batches(&modified).unwrap();
328 assert!(combined_batch.column_by_name("host").is_some());
329 assert!(combined_batch.column_by_name("zone").is_none());
330 let expected = combined_batch.clone();
331 let mut planned_batches = plan_region_batches(
332 combined_batch,
333 1024,
334 &TwoRegionPartitionRule {
335 partition_columns: vec!["host".to_string()],
336 },
337 )
338 .unwrap();
339 planned_batches.sort_by_key(|planned| planned.region_id.region_number());
340
341 assert_eq!(2, planned_batches.len());
342 assert_eq!(RegionId::new(1024, 1), planned_batches[0].region_id);
343 assert_eq!(1, planned_batches[0].num_rows());
344 assert_eq!(4, planned_batches[0].batch.num_columns());
345 assert_eq!(RegionId::new(1024, 2), planned_batches[1].region_id);
346 assert_eq!(1, planned_batches[1].num_rows());
347 assert_eq!(4, planned_batches[1].batch.num_columns());
348
349 let writes = encode_region_write_requests(
350 planned_batches,
351 false,
352 &HashMap::from([(1, Some(7)), (2, Some(8))]),
353 )
354 .unwrap();
355 for (index, write) in writes.into_iter().enumerate() {
356 let Some(region_request::Body::BulkInsert(request)) = write.body else {
357 panic!("expected bulk")
358 };
359 assert_eq!(
360 request.partition_expr_version.unwrap().value,
361 7 + index as u64
362 );
363 let Some(bulk_insert_request::Body::ArrowIpc(ipc)) = request.body else {
364 panic!("expected IPC")
365 };
366 let batch = FlightDecoder::try_from_schema_bytes(&ipc.schema)
367 .unwrap()
368 .try_decode_record_batch(&ipc.data_header, &ipc.payload)
369 .unwrap();
370 assert!(batch.column_by_name("zone").is_none());
373 assert_eq!(batch, expected.slice(index, 1));
374 let tags = batch
375 .column_by_name("host")
376 .unwrap()
377 .as_any()
378 .downcast_ref::<StringArray>()
379 .unwrap();
380 assert_eq!(tags.len(), 1);
381 assert_eq!(tags.value(0), if index == 0 { "node-1" } else { "node-2" });
382 }
383 }
384
385 #[test]
386 fn test_encode_region_write_requests_builds_bulk_insert_requests() {
387 check_encode_region_write_requests(false);
388 check_encode_region_write_requests(true);
389 }
390
391 struct TwoRegionPartitionRule {
392 partition_columns: Vec<String>,
393 }
394
395 impl PartitionRule for TwoRegionPartitionRule {
396 fn as_any(&self) -> &dyn std::any::Any {
397 self
398 }
399
400 fn partition_columns(&self) -> &[String] {
401 &self.partition_columns
402 }
403
404 fn find_region(
405 &self,
406 _values: &[datatypes::prelude::Value],
407 ) -> partition::error::Result<store_api::storage::RegionNumber> {
408 unimplemented!()
409 }
410
411 fn split_record_batch(
412 &self,
413 _record_batch: &RecordBatch,
414 ) -> partition::error::Result<HashMap<store_api::storage::RegionNumber, RegionMask>>
415 {
416 Ok(HashMap::from([
417 (1, RegionMask::new(BooleanArray::from(vec![true, false]), 1)),
418 (2, RegionMask::new(BooleanArray::from(vec![false, true]), 1)),
419 (
420 3,
421 RegionMask::new(BooleanArray::from(vec![false, false]), 0),
422 ),
423 ]))
424 }
425 }
426
427 fn check_encode_region_write_requests(skip_wal: bool) {
428 let planned_batch = PlannedRegionBatch {
429 region_id: RegionId::new(1024, 1),
430 batch: RecordBatch::try_new(
431 Arc::new(ArrowSchema::new(vec![
432 Field::new("__primary_key", ArrowDataType::Binary, false),
433 Field::new(
434 "greptime_timestamp",
435 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
436 false,
437 ),
438 Field::new("greptime_value", ArrowDataType::Float64, true),
439 ])),
440 vec![
441 Arc::new(BinaryArray::from(vec![b"k1".as_slice()])),
442 Arc::new(TimestampMillisecondArray::from(vec![1000_i64])),
443 Arc::new(arrow::array::Float64Array::from(vec![1.0_f64])),
444 ],
445 )
446 .unwrap(),
447 };
448 let writes = encode_region_write_requests(
449 vec![planned_batch],
450 skip_wal,
451 &HashMap::from([(1, Some(7))]),
452 )
453 .unwrap();
454
455 assert_eq!(1, writes.len());
456 let Some(region_request::Body::BulkInsert(request)) = &writes[0].body else {
457 panic!("expected bulk insert request");
458 };
459 assert_eq!(RegionId::new(1024, 1).as_u64(), request.region_id);
460 assert_eq!(skip_wal, request.skip_wal);
461 assert_eq!(
462 Some(PartitionExprVersion { value: 7 }),
463 request.partition_expr_version
464 );
465 }
466}