Skip to main content

operator/req_convert/common/
partitioner.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashMap;
16use std::sync::Arc;
17
18use api::v1::region::{
19    BulkInsertRequest, DeleteRequest, DeleteRequests, InsertRequest, InsertRequests, RegionRequest,
20    bulk_insert_request, region_request::Body,
21};
22use api::v1::{ArrowIpc, PartitionExprVersion, Rows};
23use common_error::ext::BoxedError;
24use common_grpc::flight::{self, FlightDecoder};
25use datatypes::arrow::array::{Array, BinaryArray, StringBuilder};
26use datatypes::arrow::compute;
27use datatypes::arrow::datatypes::{DataType, Field, Schema};
28use datatypes::arrow::record_batch::RecordBatch;
29use mito_codec::row_converter::sparse::{SparseOffsetsCache, SparsePrimaryKeyView};
30use partition::PartitionRuleRef;
31use partition::manager::PartitionRuleManager;
32use partition::partition::RegionMask;
33use partition::splitter::RowSplitter;
34use snafu::{OptionExt, ResultExt, ensure};
35use store_api::storage::consts;
36use store_api::storage::{RegionId, RegionNumber, TableId};
37use table::metadata::TableInfo;
38
39use crate::error::{self, Result, SplitDeleteSnafu, SplitInsertSnafu};
40use crate::write_dispatcher::{WriteAttribution, write_row_count};
41
42/// Loads table routing rules and converts rows to region write requests.
43pub struct Partitioner<'a> {
44    partition_manager: &'a PartitionRuleManager,
45}
46
47/// A single snapshot of a table's partition rule and expression versions.
48pub(crate) struct TablePartitioner {
49    table_id: TableId,
50    rule: PartitionRuleRef,
51    versions: HashMap<RegionNumber, Option<PartitionExprVersion>>,
52}
53
54impl<'a> Partitioner<'a> {
55    /// Creates a partitioner backed by the shared routing caches.
56    pub fn new(partition_manager: &'a PartitionRuleManager) -> Self {
57        Self { partition_manager }
58    }
59
60    /// Routes insert rows using the current table partitions.
61    pub async fn partition_insert_requests(
62        &self,
63        table_info: &TableInfo,
64        rows: Rows,
65        skip_wal: bool,
66    ) -> Result<Vec<InsertRequest>> {
67        TablePartitioner::try_new(self.partition_manager, table_info)
68            .await
69            .context(SplitInsertSnafu)?
70            .partition_insert_requests(rows, skip_wal)
71    }
72
73    /// Routes delete rows using the current table partitions.
74    pub async fn partition_delete_requests(
75        &self,
76        table_info: &TableInfo,
77        rows: Rows,
78    ) -> Result<Vec<DeleteRequest>> {
79        TablePartitioner::try_new(self.partition_manager, table_info)
80            .await
81            .context(SplitDeleteSnafu)?
82            .partition_delete_requests(rows)
83    }
84}
85
86impl TablePartitioner {
87    pub(crate) fn rebuild_attributed_write(
88        &self,
89        request: RegionRequest,
90        table_info: &TableInfo,
91        attribution: WriteAttribution,
92    ) -> Result<Vec<(RegionRequest, WriteAttribution)>> {
93        if !attribution.is_tracked() {
94            return Ok(self
95                .rebuild_write_requests(request, table_info)?
96                .into_iter()
97                .map(|request| (request, WriteAttribution::default()))
98                .collect());
99        }
100        let RegionRequest { header, body } = request;
101        if let Some(Body::BulkInsert(bulk)) = body {
102            return Ok(self
103                .rebuild_bulk_insert_request(bulk, table_info, attribution)?
104                .into_iter()
105                .map(|(body, attribution)| {
106                    (
107                        RegionRequest {
108                            header: header.clone(),
109                            body: Some(body),
110                        },
111                        attribution,
112                    )
113                })
114                .collect());
115        }
116        self.rebuild_write_requests(RegionRequest { header, body }, table_info)?
117            .into_iter()
118            .map(|request| {
119                let attribution = attribution
120                    .for_rows(write_row_count(&request))
121                    .map_err(BoxedError::new)
122                    .context(error::ExternalSnafu)?;
123                Ok((request, attribution))
124            })
125            .collect()
126    }
127
128    pub fn partition_attributed_bulk_requests(
129        &self,
130        batch: RecordBatch,
131        skip_wal: bool,
132        attribution: WriteAttribution,
133    ) -> Result<Vec<(BulkInsertRequest, WriteAttribution)>> {
134        if !attribution.is_tracked() {
135            return Ok(self
136                .partition_bulk_insert_requests(batch, None, skip_wal)?
137                .into_iter()
138                .map(|request| (request, WriteAttribution::default()))
139                .collect());
140        }
141        let masks = self
142            .rule
143            .split_record_batch(&batch)
144            .context(SplitInsertSnafu)?;
145        self.build_attributed_bulk_requests(batch, masks, None, skip_wal, attribution)
146    }
147
148    fn build_attributed_bulk_requests(
149        &self,
150        batch: RecordBatch,
151        masks: HashMap<RegionNumber, RegionMask>,
152        original_ipc: Option<ArrowIpc>,
153        skip_wal: bool,
154        attribution: WriteAttribution,
155    ) -> Result<Vec<(BulkInsertRequest, WriteAttribution)>> {
156        let mut by_region = masks
157            .iter()
158            .filter(|(_, mask)| !mask.select_none())
159            .map(|(region, mask)| {
160                Ok((
161                    *region,
162                    attribution
163                        .filter(mask)
164                        .map_err(BoxedError::new)
165                        .context(error::ExternalSnafu)?,
166                ))
167            })
168            .collect::<Result<HashMap<_, _>>>()?;
169        self.build_bulk_insert_requests(batch, masks, original_ipc, skip_wal)?
170            .into_iter()
171            .map(|request| {
172                let attribution = by_region
173                    .remove(&RegionId::from_u64(request.region_id).region_number())
174                    .context(error::UnexpectedSnafu {
175                        violated: "Missing write ownership after partitioning",
176                    })?;
177                Ok((request, attribution))
178            })
179            .collect()
180    }
181
182    /// Keeps process-local row ownership aligned when a failed write is repartitioned.
183    pub(crate) fn rebuild_tracked_write_requests(
184        &self,
185        request: RegionRequest,
186        table_info: &TableInfo,
187        owners: Option<Vec<usize>>,
188    ) -> Result<Vec<(RegionRequest, Option<Vec<usize>>)>> {
189        let Some(owners) = owners else {
190            return Ok(self
191                .rebuild_write_requests(request, table_info)?
192                .into_iter()
193                .map(|request| (request, None))
194                .collect());
195        };
196        let RegionRequest { header, body } = request;
197        if let Some(Body::BulkInsert(bulk)) = body {
198            let region_id = RegionId::from_u64(bulk.region_id);
199            if self.is_partition_expr_unchanged(region_id, bulk.partition_expr_version) {
200                return Ok(vec![(
201                    RegionRequest {
202                        header,
203                        body: Some(Body::BulkInsert(bulk)),
204                    },
205                    Some(owners),
206                )]);
207            }
208            let bulk_insert_request::Body::ArrowIpc(ipc) =
209                bulk.body.context(error::UnexpectedSnafu {
210                    violated: "Cannot rebuild a bulk insert without Arrow IPC data",
211                })?;
212            let batch = FlightDecoder::try_from_schema_bytes(&ipc.schema)
213                .and_then(|mut decoder| {
214                    decoder.try_decode_record_batch(&ipc.data_header, &ipc.payload)
215                })
216                .map_err(BoxedError::new)
217                .context(error::ExternalSnafu)?;
218            let routing = self.restore_partition_tags(&batch, table_info)?;
219            let masks = self
220                .rule
221                .split_record_batch(routing.as_ref().unwrap_or(&batch))
222                .context(SplitInsertSnafu)?;
223            return self
224                .build_tracked_bulk_insert_requests(batch, masks, owners, Some(ipc), bulk.skip_wal)
225                .map(|requests| {
226                    requests
227                        .into_iter()
228                        .map(|(request, owners)| {
229                            (
230                                RegionRequest {
231                                    header: header.clone(),
232                                    body: Some(Body::BulkInsert(request)),
233                                },
234                                Some(owners),
235                            )
236                        })
237                        .collect()
238                });
239        }
240        let owner = owners.first().copied().context(error::UnexpectedSnafu {
241            violated: "Missing row ownership",
242        })?;
243        ensure!(
244            owners.iter().all(|value| *value == owner),
245            error::UnexpectedSnafu {
246                violated: "Mixed row ownership requires Arrow batching"
247            }
248        );
249        self.rebuild_write_requests(RegionRequest { header, body }, table_info)?
250            .into_iter()
251            .map(|request| {
252                let count = match &request.body {
253                    Some(Body::Inserts(inserts)) => inserts
254                        .requests
255                        .iter()
256                        .filter_map(|r| r.rows.as_ref())
257                        .map(|r| r.rows.len())
258                        .sum(),
259                    _ => {
260                        return error::UnexpectedSnafu {
261                            violated: "Unsupported tracked write",
262                        }
263                        .fail();
264                    }
265                };
266                Ok((request, Some(vec![owner; count])))
267            })
268            .collect()
269    }
270
271    pub(crate) fn partition_tracked_bulk_insert_requests(
272        &self,
273        batch: RecordBatch,
274        owners: Vec<usize>,
275        skip_wal: bool,
276    ) -> Result<Vec<(BulkInsertRequest, Vec<usize>)>> {
277        let masks = self
278            .rule
279            .split_record_batch(&batch)
280            .context(SplitInsertSnafu)?;
281        self.build_tracked_bulk_insert_requests(batch, masks, owners, None, skip_wal)
282    }
283
284    fn build_tracked_bulk_insert_requests(
285        &self,
286        batch: RecordBatch,
287        masks: HashMap<RegionNumber, RegionMask>,
288        owners: Vec<usize>,
289        original_ipc: Option<ArrowIpc>,
290        skip_wal: bool,
291    ) -> Result<Vec<(BulkInsertRequest, Vec<usize>)>> {
292        ensure!(
293            owners.len() == batch.num_rows(),
294            error::UnexpectedSnafu {
295                violated: "Write ownership does not match row count"
296            }
297        );
298        let mut selected = HashMap::new();
299        for (region, mask) in &masks {
300            let mut selected_owners = Vec::with_capacity(mask.selected_rows());
301            selected_owners.extend(
302                mask.array()
303                    .values()
304                    .set_indices()
305                    .map(|index| owners[index]),
306            );
307            selected.insert(
308                RegionId::new(self.table_id, *region).as_u64(),
309                selected_owners,
310            );
311        }
312        self.build_bulk_insert_requests(batch, masks, original_ipc, skip_wal)?
313            .into_iter()
314            .map(|request| {
315                let owners =
316                    selected
317                        .remove(&request.region_id)
318                        .context(error::UnexpectedSnafu {
319                            violated: "Missing partition ownership",
320                        })?;
321                Ok((request, owners))
322            })
323            .collect()
324    }
325
326    /// Reads a table's partition rule and expression versions into one routing snapshot.
327    pub async fn try_new(
328        partition_manager: &PartitionRuleManager,
329        table_info: &TableInfo,
330    ) -> partition::error::Result<Self> {
331        let (rule, versions) = partition_manager
332            .find_table_partition_rule(table_info)
333            .await?;
334        Ok(Self {
335            table_id: table_info.table_id(),
336            rule,
337            versions: versions
338                .into_iter()
339                .map(|(region, version)| {
340                    (region, version.map(|value| PartitionExprVersion { value }))
341                })
342                .collect(),
343        })
344    }
345
346    /// Rebuilds write requests using this partition snapshot after a route refresh.
347    ///
348    /// Takes ownership so row payloads can move into their new regions. Requests whose
349    /// region and partition version still match retain their original payloads.
350    /// The table metadata comes from the same refresh that created this partitioner.
351    pub(crate) fn rebuild_write_requests(
352        &self,
353        request: RegionRequest,
354        table_info: &TableInfo,
355    ) -> Result<Vec<RegionRequest>> {
356        let RegionRequest { header, body } = request;
357        let bodies = match body.context(error::UnexpectedSnafu {
358            violated: "Cannot rebuild a request without a body",
359        })? {
360            Body::Inserts(inserts) => self.rebuild_insert_requests(inserts)?,
361            Body::Deletes(deletes) => self.rebuild_delete_requests(deletes)?,
362            Body::BulkInsert(bulk) => self
363                .rebuild_bulk_insert_request(bulk, table_info, WriteAttribution::default())?
364                .into_iter()
365                .map(|(body, _)| body)
366                .collect(),
367            _ => {
368                return error::UnexpectedSnafu {
369                    violated: "Only writes can be rebuilt",
370                }
371                .fail();
372            }
373        };
374        Ok(bodies
375            .into_iter()
376            .map(|body| RegionRequest {
377                header: header.clone(),
378                body: Some(body),
379            })
380            .collect())
381    }
382
383    /// Moves insert rows into requests for this snapshot's destination regions.
384    pub fn partition_insert_requests(
385        &self,
386        rows: Rows,
387        skip_wal: bool,
388    ) -> Result<Vec<InsertRequest>> {
389        Ok(RowSplitter::new(self.rule.clone())
390            .split(rows)
391            .context(SplitInsertSnafu)?
392            .into_iter()
393            .map(|(region, rows)| InsertRequest {
394                region_id: RegionId::new(self.table_id, region).as_u64(),
395                rows: Some(rows),
396                partition_expr_version: self.version(region),
397                skip_wal,
398            })
399            .collect())
400    }
401
402    /// Moves delete rows into requests for this snapshot's destination regions.
403    pub fn partition_delete_requests(&self, rows: Rows) -> Result<Vec<DeleteRequest>> {
404        Ok(RowSplitter::new(self.rule.clone())
405            .split(rows)
406            .context(SplitDeleteSnafu)?
407            .into_iter()
408            .map(|(region, rows)| DeleteRequest {
409                region_id: RegionId::new(self.table_id, region).as_u64(),
410                rows: Some(rows),
411                partition_expr_version: self.version(region),
412            })
413            .collect())
414    }
415
416    /// Splits an Arrow batch, reusing the original IPC when one region selects all rows.
417    pub fn partition_bulk_insert_requests(
418        &self,
419        batch: RecordBatch,
420        original_ipc: Option<ArrowIpc>,
421        skip_wal: bool,
422    ) -> Result<Vec<BulkInsertRequest>> {
423        let masks = self
424            .rule
425            .split_record_batch(&batch)
426            .context(SplitInsertSnafu)?;
427        self.build_bulk_insert_requests(batch, masks, original_ipc, skip_wal)
428    }
429
430    fn build_bulk_insert_requests(
431        &self,
432        batch: RecordBatch,
433        masks: HashMap<RegionNumber, RegionMask>,
434        mut original_ipc: Option<ArrowIpc>,
435        skip_wal: bool,
436    ) -> Result<Vec<BulkInsertRequest>> {
437        let mut requests = Vec::with_capacity(masks.len());
438        for (region, mask) in masks {
439            if mask.select_none() {
440                continue;
441            }
442            let original = if mask.select_all() {
443                original_ipc.take()
444            } else {
445                None
446            };
447            let ipc = if let Some(ipc) = original {
448                ipc
449            } else {
450                let selected = if mask.select_all() {
451                    batch.clone()
452                } else {
453                    compute::filter_record_batch(&batch, mask.array())
454                        .context(error::ComputeArrowSnafu)?
455                };
456                let (schema, data_header, payload) = flight::record_batch_to_ipc(selected)
457                    .map_err(BoxedError::new)
458                    .context(error::ExternalSnafu)?;
459                ArrowIpc {
460                    schema,
461                    data_header,
462                    payload,
463                }
464            };
465            requests.push(BulkInsertRequest {
466                region_id: RegionId::new(self.table_id, region).as_u64(),
467                partition_expr_version: self.version(region),
468                // The destination datanode validates against its current schema.
469                aligned_schema_version: None,
470                skip_wal,
471                body: Some(bulk_insert_request::Body::ArrowIpc(ipc)),
472            });
473        }
474        Ok(requests)
475    }
476
477    fn rebuild_insert_requests(&self, inserts: InsertRequests) -> Result<Vec<Body>> {
478        let mut bodies = Vec::new();
479        for insert in inserts.requests {
480            let region_id = RegionId::from_u64(insert.region_id);
481            if self.is_partition_expr_unchanged(region_id, insert.partition_expr_version) {
482                bodies.push(Body::Inserts(InsertRequests {
483                    requests: vec![insert],
484                }));
485                continue;
486            }
487            let rows = insert.rows.context(error::UnexpectedSnafu {
488                violated: "Cannot rebuild an insert without rows",
489            })?;
490            for request in self.partition_insert_requests(rows, insert.skip_wal)? {
491                bodies.push(Body::Inserts(InsertRequests {
492                    requests: vec![request],
493                }));
494            }
495        }
496        Ok(bodies)
497    }
498
499    fn rebuild_delete_requests(&self, deletes: DeleteRequests) -> Result<Vec<Body>> {
500        let mut bodies = Vec::new();
501        for delete in deletes.requests {
502            let region_id = RegionId::from_u64(delete.region_id);
503            if self.is_partition_expr_unchanged(region_id, delete.partition_expr_version) {
504                bodies.push(Body::Deletes(DeleteRequests {
505                    requests: vec![delete],
506                }));
507                continue;
508            }
509            let rows = delete.rows.context(error::UnexpectedSnafu {
510                violated: "Cannot rebuild a delete without rows",
511            })?;
512            for request in self.partition_delete_requests(rows)? {
513                bodies.push(Body::Deletes(DeleteRequests {
514                    requests: vec![request],
515                }));
516            }
517        }
518        Ok(bodies)
519    }
520
521    fn rebuild_bulk_insert_request(
522        &self,
523        bulk: BulkInsertRequest,
524        table_info: &TableInfo,
525        attribution: WriteAttribution,
526    ) -> Result<Vec<(Body, WriteAttribution)>> {
527        let region_id = RegionId::from_u64(bulk.region_id);
528        if self.is_partition_expr_unchanged(region_id, bulk.partition_expr_version) {
529            return Ok(vec![(Body::BulkInsert(bulk), attribution)]);
530        }
531        let bulk_insert_request::Body::ArrowIpc(ipc) =
532            bulk.body.context(error::UnexpectedSnafu {
533                violated: "Cannot rebuild a bulk insert without Arrow IPC data",
534            })?;
535        let batch = FlightDecoder::try_from_schema_bytes(&ipc.schema)
536            .and_then(|mut decoder| decoder.try_decode_record_batch(&ipc.data_header, &ipc.payload))
537            .map_err(BoxedError::new)
538            .context(error::ExternalSnafu)?;
539        let routing_batch = self.restore_partition_tags(&batch, table_info)?;
540        let masks = self
541            .rule
542            .split_record_batch(routing_batch.as_ref().unwrap_or(&batch))
543            .context(SplitInsertSnafu)?;
544        let writes = if attribution.is_tracked() {
545            self.build_attributed_bulk_requests(
546                batch,
547                masks,
548                Some(ipc),
549                bulk.skip_wal,
550                attribution,
551            )?
552        } else {
553            self.build_bulk_insert_requests(batch, masks, Some(ipc), bulk.skip_wal)?
554                .into_iter()
555                .map(|request| (request, WriteAttribution::default()))
556                .collect()
557        };
558        Ok(writes
559            .into_iter()
560            .map(|(request, attribution)| (Body::BulkInsert(request), attribution))
561            .collect())
562    }
563
564    /// Restores omitted physical-table tags for routing without changing the write payload.
565    fn restore_partition_tags(
566        &self,
567        batch: &RecordBatch,
568        table_info: &TableInfo,
569    ) -> Result<Option<RecordBatch>> {
570        let meta = &table_info.meta;
571        if !table_info.is_physical_table() || meta.column_ids.len() != meta.schema.num_columns() {
572            return Ok(None);
573        }
574        let missing = self
575            .rule
576            .partition_columns()
577            .iter()
578            .filter(|name| batch.column_by_name(name).is_none())
579            .filter_map(|name| {
580                let index = meta.schema.column_index_by_name(name)?;
581                (meta.primary_key_indices.contains(&index)
582                    && meta.schema.column_schemas()[index].data_type.is_string())
583                .then_some((name, meta.column_ids[index]))
584            })
585            .collect::<Vec<_>>();
586        if missing.is_empty() {
587            return Ok(None);
588        }
589        let Some(primary_key) = batch.column_by_name(consts::PRIMARY_KEY_COLUMN_NAME) else {
590            return Ok(None);
591        };
592        let primary_key =
593            primary_key
594                .as_any()
595                .downcast_ref::<BinaryArray>()
596                .context(error::UnexpectedSnafu {
597                    violated: "Sparse primary key column must be Binary",
598                })?;
599        let mut builders = missing
600            .iter()
601            .map(|_| StringBuilder::new())
602            .collect::<Vec<_>>();
603        let mut cache = SparseOffsetsCache::new();
604        let mut buffer = Vec::new();
605        for row in 0..batch.num_rows() {
606            ensure!(
607                !primary_key.is_null(row),
608                error::UnexpectedSnafu {
609                    violated: "Sparse primary key must not be null",
610                }
611            );
612            let mut key = SparsePrimaryKeyView::new(primary_key.value(row), &mut cache)
613                .map_err(BoxedError::new)
614                .context(error::ExternalSnafu)?;
615            for ((_, column_id), builder) in missing.iter().zip(&mut builders) {
616                let label = key
617                    .label(*column_id, &mut buffer)
618                    .map_err(BoxedError::new)
619                    .context(error::ExternalSnafu)?;
620                builder.append_option(label);
621            }
622        }
623        let schema = batch.schema();
624        let mut fields = schema.fields().to_vec();
625        let mut columns = batch.columns().to_vec();
626        for ((name, _), mut builder) in missing.into_iter().zip(builders) {
627            fields.push(Arc::new(Field::new(name, DataType::Utf8, true)));
628            columns.push(Arc::new(builder.finish()));
629        }
630        RecordBatch::try_new(
631            Arc::new(Schema::new_with_metadata(fields, schema.metadata().clone())),
632            columns,
633        )
634        .map(Some)
635        .context(error::ComputeArrowSnafu)
636    }
637
638    /// Whether the request's region still exists with the same partition expression version.
639    ///
640    /// A leader change leaves the partition unchanged, so the original payload can be
641    /// sent to the new peer. A removed region or changed partition expression version
642    /// requires rebuilding the request using this snapshot's partition rule.
643    fn is_partition_expr_unchanged(
644        &self,
645        region_id: RegionId,
646        version: Option<PartitionExprVersion>,
647    ) -> bool {
648        region_id.table_id() == self.table_id
649            && self.versions.get(&region_id.region_number()) == Some(&version)
650    }
651
652    fn version(&self, region: RegionNumber) -> Option<PartitionExprVersion> {
653        self.versions.get(&region).copied().flatten()
654    }
655}
656
657#[cfg(test)]
658mod tests {
659    use std::sync::Arc;
660
661    use api::v1::region::{
662        AlignedSchemaVersion, BulkInsertRequest, DeleteRequest, InsertRequest, RegionRequestHeader,
663    };
664    use api::v1::value::ValueData;
665    use api::v1::{ArrowIpc, PartitionExprVersion};
666    use api::v1::{ColumnDataType, ColumnSchema, Row, Rows, Value};
667    use common_error::ext::{ErrorExt, RetryHint};
668    use common_error::status_code::StatusCode;
669    use common_grpc::flight;
670    use common_meta::cache::{self, TableInfoCache};
671    use common_meta::key::TableMetadataManager;
672    use common_meta::key::table_route::TableRouteValue;
673    use common_meta::kv_backend::memory::MemoryKvBackend;
674    use common_meta::kv_backend::test_util::MockKvBackendBuilder;
675    use common_meta::rpc::router::{Region, RegionRoute};
676    use common_meta::rpc::store::RangeResponse;
677    use datatypes::arrow::array::{BooleanArray, Int32Array, StringArray};
678    use datatypes::arrow::datatypes::{DataType, Field, Schema};
679    use datatypes::arrow::record_batch::RecordBatch;
680    use datatypes::data_type::ConcreteDataType;
681    use datatypes::schema;
682    use datatypes::value::{Value as DataValue, ValueRef};
683    use mito_codec::row_converter::sparse;
684    use mito_codec::row_converter::{PrimaryKeyCodec, SparsePrimaryKeyCodec};
685    use moka::future::Cache;
686    use partition::expr;
687    use partition::manager::{PartitionRuleManager, PartitionRuleManagerRef};
688    use partition::multi_dim::MultiDimPartitionRule;
689    use store_api::metric_engine_consts;
690
691    use super::*;
692    use crate::error::Error;
693    use crate::test_util;
694    use crate::test_util::{test_new_partition_info_cache, test_new_table_route_cache};
695
696    fn build_sparse_partitioner() -> TablePartitioner {
697        TablePartitioner {
698            table_id: 1,
699            rule: Arc::new(
700                MultiDimPartitionRule::try_new(
701                    vec!["zone".to_string()],
702                    vec![1, 2],
703                    vec![
704                        expr::col("zone").lt(DataValue::String("m".into())),
705                        expr::col("zone").gt_eq(DataValue::String("m".into())),
706                    ],
707                    true,
708                )
709                .unwrap(),
710            ),
711            versions: [
712                (1, Some(PartitionExprVersion { value: 2 })),
713                (2, Some(PartitionExprVersion { value: 2 })),
714            ]
715            .into_iter()
716            .collect(),
717        }
718    }
719
720    fn build_sparse_table_info() -> TableInfo {
721        let mut table_info = test_util::new_test_table_info(1, "physical_tags", [1, 2].into_iter());
722        table_info.meta.schema = Arc::new(schema::Schema::new(vec![
723            schema::ColumnSchema::new("host", ConcreteDataType::string_datatype(), true),
724            schema::ColumnSchema::new("zone", ConcreteDataType::string_datatype(), true),
725        ]));
726        table_info.meta.primary_key_indices = vec![0, 1];
727        table_info.meta.partition_key_indices = vec![1];
728        table_info.meta.column_ids = vec![10, 20];
729        table_info.meta.options.extra_options.insert(
730            metric_engine_consts::PHYSICAL_TABLE_METADATA_KEY.to_string(),
731            String::new(),
732        );
733        table_info
734    }
735
736    fn build_sparse_batch() -> RecordBatch {
737        let codec = SparsePrimaryKeyCodec::schemaless();
738        let keys = [Some("east"), Some("west"), Some(""), None, None]
739            .into_iter()
740            .enumerate()
741            .map(|(index, zone)| {
742                let mut key = Vec::new();
743                let values = [
744                    (
745                        sparse::RESERVED_COLUMN_ID_TABLE_ID,
746                        ValueRef::UInt32(42 + index as u32),
747                    ),
748                    (
749                        sparse::RESERVED_COLUMN_ID_TSID,
750                        ValueRef::UInt64(index as u64),
751                    ),
752                    (10, ValueRef::String("node")),
753                ];
754                codec.encode_value_refs(&values, &mut key).unwrap();
755                if let Some(zone) = zone {
756                    codec
757                        .encode_raw_tag_value([(20, zone.as_bytes())].into_iter(), &mut key)
758                        .unwrap();
759                }
760                if index == 4 {
761                    key.extend_from_slice(&20_u32.to_be_bytes());
762                    key.push(0);
763                }
764                key
765            })
766            .collect::<Vec<_>>();
767        RecordBatch::try_new(
768            Arc::new(Schema::new(vec![
769                Field::new("host", DataType::Utf8, false),
770                Field::new(consts::PRIMARY_KEY_COLUMN_NAME, DataType::Binary, false),
771            ])),
772            vec![
773                Arc::new(StringArray::from(vec!["node"; keys.len()])),
774                Arc::new(BinaryArray::from_iter_values(keys.iter())),
775            ],
776        )
777        .unwrap()
778    }
779
780    #[tokio::test]
781    async fn test_partition_tags_use_physical_table_metadata() {
782        let backend = Arc::new(MemoryKvBackend::new());
783        let table_route_cache = test_new_table_route_cache(backend.clone());
784        let partition_info_cache = test_new_partition_info_cache(table_route_cache.clone());
785        let manager =
786            PartitionRuleManager::new(backend.clone(), table_route_cache, partition_info_cache);
787        let table_info = build_sparse_table_info();
788        let routes = [
789            expr::col("zone").lt(DataValue::String("m".into())),
790            expr::col("zone").gt_eq(DataValue::String("m".into())),
791        ]
792        .into_iter()
793        .enumerate()
794        .map(|(index, expr)| RegionRoute {
795            region: Region {
796                id: RegionId::new(1, index as u32 + 1),
797                partition_expr: expr.as_json_str().unwrap(),
798                ..Default::default()
799            },
800            ..Default::default()
801        })
802        .collect();
803        TableMetadataManager::new(backend)
804            .create_table_metadata(
805                table_info.clone(),
806                TableRouteValue::physical(routes),
807                Default::default(),
808            )
809            .await
810            .unwrap();
811        let batch = build_sparse_batch();
812        let partitioner = TablePartitioner::try_new(&manager, &table_info)
813            .await
814            .unwrap();
815        let restored = partitioner
816            .restore_partition_tags(&batch, &table_info)
817            .unwrap()
818            .unwrap();
819        let zones = restored
820            .column_by_name("zone")
821            .unwrap()
822            .as_any()
823            .downcast_ref::<StringArray>()
824            .unwrap();
825        assert_eq!(
826            zones.iter().collect::<Vec<_>>(),
827            vec![Some("east"), Some("west"), Some(""), None, None]
828        );
829
830        for (physical, tag, valid_ids) in [
831            (false, true, true),
832            (true, false, true),
833            (true, true, false),
834        ] {
835            let mut info = table_info.clone();
836            if !physical {
837                info.meta
838                    .options
839                    .extra_options
840                    .remove(metric_engine_consts::PHYSICAL_TABLE_METADATA_KEY);
841            }
842            if !tag {
843                info.meta.primary_key_indices = vec![0];
844            }
845            if !valid_ids {
846                info.meta.column_ids.clear();
847            }
848            let partitioner = TablePartitioner::try_new(&manager, &info).await.unwrap();
849            assert!(
850                partitioner
851                    .restore_partition_tags(&batch, &info)
852                    .unwrap()
853                    .is_none()
854            );
855            assert!(
856                partitioner
857                    .partition_bulk_insert_requests(batch.clone(), None, false)
858                    .is_err()
859            );
860        }
861    }
862
863    #[test]
864    fn test_rebuild_sparse_bulk_with_changed_partition_column() {
865        let partitioner = build_sparse_partitioner();
866        let table_info = build_sparse_table_info();
867        let batch = build_sparse_batch();
868        let restored = partitioner
869            .restore_partition_tags(&batch, &table_info)
870            .unwrap()
871            .unwrap();
872        let zones = restored
873            .column_by_name("zone")
874            .unwrap()
875            .as_any()
876            .downcast_ref::<StringArray>()
877            .unwrap();
878        assert_eq!(
879            zones.iter().collect::<Vec<_>>(),
880            vec![Some("east"), Some("west"), Some(""), None, None]
881        );
882        let (schema, data_header, payload) = flight::record_batch_to_ipc(batch.clone()).unwrap();
883        let rebuilt = partitioner
884            .rebuild_write_requests(
885                build_request(Body::BulkInsert(BulkInsertRequest {
886                    region_id: RegionId::new(1, 1).as_u64(),
887                    partition_expr_version: Some(PartitionExprVersion { value: 1 }),
888                    skip_wal: true,
889                    body: Some(bulk_insert_request::Body::ArrowIpc(ArrowIpc {
890                        schema,
891                        data_header,
892                        payload,
893                    })),
894                    ..Default::default()
895                })),
896                &table_info,
897            )
898            .unwrap();
899        assert_eq!(rebuilt.len(), 2);
900        for request in rebuilt {
901            let Some(Body::BulkInsert(bulk)) = request.body else {
902                unreachable!()
903            };
904            assert!(bulk.skip_wal);
905            assert_eq!(
906                bulk.partition_expr_version,
907                Some(PartitionExprVersion { value: 2 })
908            );
909            let Some(bulk_insert_request::Body::ArrowIpc(ipc)) = bulk.body else {
910                unreachable!()
911            };
912            let actual = FlightDecoder::try_from_schema_bytes(&ipc.schema)
913                .unwrap()
914                .try_decode_record_batch(&ipc.data_header, &ipc.payload)
915                .unwrap();
916            let mask = if RegionId::from_u64(bulk.region_id).region_number() == 1 {
917                vec![true, false, true, true, true]
918            } else {
919                vec![false, true, false, false, false]
920            };
921            let expected = compute::filter_record_batch(&batch, &mask.into()).unwrap();
922            assert_eq!(actual, expected);
923        }
924    }
925
926    #[test]
927    fn test_restore_partition_tags_checks_only_missing_tags() {
928        let partitioner = build_sparse_partitioner();
929        let table_info = build_sparse_table_info();
930        let batch = build_sparse_batch();
931        let invalid = RecordBatch::try_new(
932            batch.schema(),
933            vec![
934                batch.column(0).clone(),
935                Arc::new(BinaryArray::from_iter_values([b"invalid"; 5])),
936            ],
937        )
938        .unwrap();
939        assert!(
940            partitioner
941                .restore_partition_tags(&invalid, &table_info)
942                .is_err()
943        );
944        let mut fields = invalid.schema().fields().to_vec();
945        fields.push(Arc::new(Field::new("zone", DataType::Utf8, true)));
946        let mut columns = invalid.columns().to_vec();
947        columns.push(Arc::new(StringArray::from(vec![Some("east"); 5])));
948        let complete = RecordBatch::try_new(Arc::new(Schema::new(fields)), columns).unwrap();
949        assert!(
950            partitioner
951                .restore_partition_tags(&complete, &table_info)
952                .unwrap()
953                .is_none()
954        );
955        let (schema, data_header, payload) = flight::record_batch_to_ipc(invalid).unwrap();
956        let request = build_request(Body::BulkInsert(BulkInsertRequest {
957            region_id: RegionId::new(1, 1).as_u64(),
958            partition_expr_version: Some(PartitionExprVersion { value: 2 }),
959            body: Some(bulk_insert_request::Body::ArrowIpc(ArrowIpc {
960                schema,
961                data_header,
962                payload,
963            })),
964            ..Default::default()
965        }));
966        let expected = request.clone();
967        assert_eq!(
968            partitioner
969                .rebuild_write_requests(request, &table_info)
970                .unwrap(),
971            vec![expected]
972        );
973    }
974
975    #[tokio::test]
976    async fn test_partition_requests_preserve_routing_errors() {
977        let table_info = test_util::new_test_table_info(1, "table_1", [1].into_iter());
978        for (backend_unavailable, expected_status, expected_hint) in [
979            (false, StatusCode::TableNotFound, RetryHint::NonRetryable),
980            (true, StatusCode::Internal, RetryHint::Retryable),
981        ] {
982            let backend = Arc::new(
983                MockKvBackendBuilder::default()
984                    .range_fn(Arc::new(move |_| {
985                        if backend_unavailable {
986                            common_meta::error::NoLeaderSnafu { table_id: 1u32 }.fail()
987                        } else {
988                            Ok(RangeResponse {
989                                kvs: Vec::new(),
990                                more: false,
991                            })
992                        }
993                    }))
994                    .build()
995                    .unwrap(),
996            );
997            let routes = Arc::new(cache::new_table_route_cache(
998                "partitioner_test_routes".to_string(),
999                Cache::new(16),
1000                backend.clone(),
1001            ));
1002            let partitions = Arc::new(partition::cache::new_partition_info_cache(
1003                "partitioner_test_partitions".to_string(),
1004                Cache::new(16),
1005                routes.clone(),
1006            ));
1007            let manager = PartitionRuleManager::new(backend, routes, partitions);
1008            let partitioner = Partitioner::new(&manager);
1009            let insert_error = partitioner
1010                .partition_insert_requests(&table_info, Rows::default(), false)
1011                .await
1012                .unwrap_err();
1013            let delete_error = partitioner
1014                .partition_delete_requests(&table_info, Rows::default())
1015                .await
1016                .unwrap_err();
1017
1018            assert!(matches!(insert_error, Error::SplitInsert { .. }));
1019            assert!(matches!(delete_error, Error::SplitDelete { .. }));
1020            for error in [insert_error, delete_error] {
1021                assert_eq!(error.status_code(), expected_status);
1022                assert_eq!(error.retry_hint(), expected_hint);
1023            }
1024        }
1025    }
1026
1027    async fn create_fixture() -> (PartitionRuleManagerRef, TableInfoCache) {
1028        let backend = Arc::new(MemoryKvBackend::new());
1029        let manager = test_util::create_partition_rule_manager(backend.clone()).await;
1030        TableMetadataManager::new(backend.clone())
1031            .create_table_metadata(
1032                test_util::new_test_table_info(42, "logical_reroute", [1, 2, 3].into_iter()),
1033                TableRouteValue::logical(1),
1034                Default::default(),
1035            )
1036            .await
1037            .unwrap();
1038        let cache = cache::new_table_info_cache(
1039            "reroute_test_table_info".to_string(),
1040            Cache::new(16),
1041            backend,
1042        );
1043        (manager, cache)
1044    }
1045
1046    fn build_request(body: Body) -> RegionRequest {
1047        RegionRequest {
1048            header: Some(RegionRequestHeader {
1049                dbname: "greptime.public".to_string(),
1050                ..Default::default()
1051            }),
1052            body: Some(body),
1053        }
1054    }
1055
1056    fn build_rows(values: &[i32]) -> Rows {
1057        Rows {
1058            schema: vec![ColumnSchema {
1059                column_name: "a".to_string(),
1060                datatype: ColumnDataType::Int32 as i32,
1061                ..Default::default()
1062            }],
1063            rows: values
1064                .iter()
1065                .map(|value| Row {
1066                    values: vec![Value {
1067                        value_data: Some(ValueData::I32Value(*value)),
1068                    }],
1069                })
1070                .collect(),
1071        }
1072    }
1073
1074    fn build_row_request(
1075        rows: Rows,
1076        insert: bool,
1077        region: u32,
1078        version: Option<u64>,
1079    ) -> RegionRequest {
1080        let region_id = RegionId::new(1, region).as_u64();
1081        let partition_expr_version = version.map(|value| PartitionExprVersion { value });
1082        let body = if insert {
1083            Body::Inserts(InsertRequests {
1084                requests: vec![InsertRequest {
1085                    region_id,
1086                    partition_expr_version,
1087                    rows: Some(rows),
1088                    skip_wal: true,
1089                }],
1090            })
1091        } else {
1092            Body::Deletes(DeleteRequests {
1093                requests: vec![DeleteRequest {
1094                    region_id,
1095                    partition_expr_version,
1096                    rows: Some(rows),
1097                }],
1098            })
1099        };
1100        build_request(body)
1101    }
1102
1103    fn row_request_parts(request: &RegionRequest) -> (u64, Option<u64>, &Rows) {
1104        match request.body.as_ref().unwrap() {
1105            Body::Inserts(inserts) => {
1106                assert_eq!(inserts.requests.len(), 1);
1107                let insert = &inserts.requests[0];
1108                assert!(insert.skip_wal);
1109                (
1110                    insert.region_id,
1111                    insert.partition_expr_version.map(|v| v.value),
1112                    insert.rows.as_ref().unwrap(),
1113                )
1114            }
1115            Body::Deletes(deletes) => {
1116                assert_eq!(deletes.requests.len(), 1);
1117                let delete = &deletes.requests[0];
1118                (
1119                    delete.region_id,
1120                    delete.partition_expr_version.map(|v| v.value),
1121                    delete.rows.as_ref().unwrap(),
1122                )
1123            }
1124            _ => panic!("expected row write"),
1125        }
1126    }
1127
1128    #[tokio::test]
1129    async fn test_rebuild_rows_updates_partitions_without_losing_rows() {
1130        let (manager, cache) = create_fixture().await;
1131        let table_info = cache.get(1).await.unwrap().unwrap();
1132        let (_, versions) = manager
1133            .find_table_partition_rule(&table_info)
1134            .await
1135            .unwrap();
1136        for insert in [true, false] {
1137            let request = build_row_request(build_rows(&[1, 20, 70]), insert, 99, Some(0));
1138            let header = request.header.clone();
1139            let rerouted = rebuild_with_cached_table(&manager, &cache, request)
1140                .await
1141                .unwrap();
1142            assert_eq!(rerouted.len(), 3);
1143            let mut actual = Vec::new();
1144            for request in rerouted {
1145                assert_eq!(request.header, header);
1146                let (id, version, rows) = row_request_parts(&request);
1147                let region_id = RegionId::from_u64(id);
1148                assert_eq!(region_id.table_id(), 1);
1149                assert_eq!(version, versions[&region_id.region_number()]);
1150                assert_eq!(rows.schema, build_rows(&[]).schema);
1151                assert_eq!(rows.rows.len(), 1);
1152                actual.push((region_id.region_number(), rows.rows[0].clone()));
1153            }
1154            actual.sort_by_key(|(region, _)| *region);
1155            assert_eq!(
1156                actual,
1157                vec![
1158                    (1, build_rows(&[70]).rows.remove(0)),
1159                    (2, build_rows(&[20]).rows.remove(0)),
1160                    (3, build_rows(&[1]).rows.remove(0))
1161                ]
1162            );
1163        }
1164    }
1165
1166    #[tokio::test]
1167    async fn test_unchanged_row_routes_preserve_allocations() {
1168        let (manager, cache) = create_fixture().await;
1169        let table_info = cache.get(1).await.unwrap().unwrap();
1170        let (_, versions) = manager
1171            .find_table_partition_rule(&table_info)
1172            .await
1173            .unwrap();
1174        for insert in [true, false] {
1175            let request = build_row_request(build_rows(&[1]), insert, 3, versions[&3]);
1176            let original_rows = row_request_parts(&request).2;
1177            let row_ptr = original_rows.rows.as_ptr();
1178            let value_ptr = original_rows.rows[0].values.as_ptr();
1179            let rerouted = rebuild_with_cached_table(&manager, &cache, request)
1180                .await
1181                .unwrap();
1182            assert_eq!(rerouted.len(), 1);
1183            let rows = row_request_parts(&rerouted[0]).2;
1184            assert_eq!(rows.rows.as_ptr(), row_ptr);
1185            assert_eq!(rows.rows[0].values.as_ptr(), value_ptr);
1186            assert_eq!(rows, &build_rows(&[1]));
1187        }
1188    }
1189
1190    fn build_bulk_request(region: u32, version: Option<u64>, values: Vec<i32>) -> RegionRequest {
1191        let batch = RecordBatch::try_new(
1192            Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)])),
1193            vec![Arc::new(Int32Array::from(values))],
1194        )
1195        .unwrap();
1196        let (schema, data_header, payload) = flight::record_batch_to_ipc(batch).unwrap();
1197        build_request(Body::BulkInsert(BulkInsertRequest {
1198            region_id: RegionId::new(1, region).as_u64(),
1199            partition_expr_version: version.map(|value| PartitionExprVersion { value }),
1200            aligned_schema_version: Some(AlignedSchemaVersion {
1201                schema_version: 123,
1202            }),
1203            skip_wal: true,
1204            body: Some(bulk_insert_request::Body::ArrowIpc(ArrowIpc {
1205                schema,
1206                data_header,
1207                payload,
1208            })),
1209        }))
1210    }
1211
1212    #[tokio::test]
1213    async fn test_rebuild_bulk_reencodes_only_changed_partitions() {
1214        let (manager, cache) = create_fixture().await;
1215        let table_info = cache.get(1).await.unwrap().unwrap();
1216        let (_, versions) = manager
1217            .find_table_partition_rule(&table_info)
1218            .await
1219            .unwrap();
1220        let partitioner = TablePartitioner::try_new(&manager, &table_info)
1221            .await
1222            .unwrap();
1223        for tracked in [false, true] {
1224            let request = build_bulk_request(99, Some(0), vec![1, 20, 70]);
1225            let header = request.header.clone();
1226            let rerouted = partitioner
1227                .rebuild_tracked_write_requests(
1228                    request,
1229                    &table_info,
1230                    tracked.then(|| vec![2, 0, 2]),
1231                )
1232                .unwrap();
1233            assert_eq!(rerouted.len(), 3);
1234            let mut actual = Vec::new();
1235            for (request, owners) in rerouted {
1236                assert_eq!(request.header, header);
1237                let Some(Body::BulkInsert(bulk)) = request.body else {
1238                    panic!("expected bulk insert")
1239                };
1240                let region = RegionId::from_u64(bulk.region_id);
1241                assert_eq!(region.table_id(), 1);
1242                assert!(bulk.skip_wal);
1243                assert_eq!(bulk.aligned_schema_version, None);
1244                assert_eq!(
1245                    bulk.partition_expr_version.map(|v| v.value),
1246                    versions[&region.region_number()]
1247                );
1248                assert_eq!(
1249                    owners,
1250                    tracked.then(|| vec![if region.region_number() == 2 { 0 } else { 2 }])
1251                );
1252                let Some(bulk_insert_request::Body::ArrowIpc(ipc)) = bulk.body else {
1253                    panic!("expected Arrow IPC")
1254                };
1255                let batch = FlightDecoder::try_from_schema_bytes(&ipc.schema)
1256                    .unwrap()
1257                    .try_decode_record_batch(&ipc.data_header, &ipc.payload)
1258                    .unwrap();
1259                assert_eq!(batch.num_rows(), 1);
1260                let values = batch
1261                    .column(0)
1262                    .as_any()
1263                    .downcast_ref::<Int32Array>()
1264                    .unwrap();
1265                actual.push((region.region_number(), values.value(0)));
1266            }
1267            actual.sort_unstable();
1268            assert_eq!(actual, [(1, 70), (2, 20), (3, 1)]);
1269
1270            // Both unchanged routes and reroutes selecting all rows reuse the original IPC.
1271            for (region, version, unchanged) in [(99, Some(0), false), (3, versions[&3], true)] {
1272                let request = build_bulk_request(region, version, vec![1, 2, 3]);
1273                let header = request.header.clone();
1274                let Some(Body::BulkInsert(bulk)) = request.body.as_ref() else {
1275                    unreachable!()
1276                };
1277                let Some(bulk_insert_request::Body::ArrowIpc(ipc)) = &bulk.body else {
1278                    unreachable!()
1279                };
1280                let pointers = (
1281                    ipc.schema.as_ptr(),
1282                    ipc.data_header.as_ptr(),
1283                    ipc.payload.as_ptr(),
1284                );
1285                let rerouted = partitioner
1286                    .rebuild_tracked_write_requests(
1287                        request,
1288                        &table_info,
1289                        tracked.then(|| vec![2, 0, 2]),
1290                    )
1291                    .unwrap();
1292                assert_eq!(rerouted.len(), 1);
1293                let (request, owners) = &rerouted[0];
1294                assert_eq!(request.header, header);
1295                assert_eq!(owners, &tracked.then(|| vec![2, 0, 2]));
1296                let Some(Body::BulkInsert(bulk)) = request.body.as_ref() else {
1297                    unreachable!()
1298                };
1299                assert_eq!(bulk.region_id, RegionId::new(1, 3).as_u64());
1300                assert!(bulk.skip_wal);
1301                assert_eq!(bulk.partition_expr_version.map(|v| v.value), versions[&3]);
1302                assert_eq!(
1303                    bulk.aligned_schema_version,
1304                    unchanged.then_some(AlignedSchemaVersion {
1305                        schema_version: 123
1306                    })
1307                );
1308                let Some(bulk_insert_request::Body::ArrowIpc(ipc)) = &bulk.body else {
1309                    unreachable!()
1310                };
1311                assert_eq!(
1312                    (
1313                        ipc.schema.as_ptr(),
1314                        ipc.data_header.as_ptr(),
1315                        ipc.payload.as_ptr()
1316                    ),
1317                    pointers,
1318                    "tracked={tracked}, unchanged={unchanged}"
1319                );
1320            }
1321        }
1322    }
1323
1324    #[tokio::test]
1325    async fn test_rebuild_logical_table_preserves_logical_region_ids() {
1326        let (manager, cache) = create_fixture().await;
1327        for insert in [true, false] {
1328            let mut request = build_row_request(build_rows(&[1, 20, 70]), insert, 99, Some(0));
1329            match request.body.as_mut().unwrap() {
1330                Body::Inserts(inserts) => {
1331                    inserts.requests[0].region_id = RegionId::new(42, 99).as_u64()
1332                }
1333                Body::Deletes(deletes) => {
1334                    deletes.requests[0].region_id = RegionId::new(42, 99).as_u64()
1335                }
1336                _ => unreachable!(),
1337            }
1338            let rerouted = rebuild_with_cached_table(&manager, &cache, request)
1339                .await
1340                .unwrap();
1341            assert_eq!(rerouted.len(), 3);
1342            let mut regions = rerouted
1343                .iter()
1344                .map(|request| {
1345                    let region = RegionId::from_u64(row_request_parts(request).0);
1346                    assert_eq!(region.table_id(), 42);
1347                    region.region_number()
1348                })
1349                .collect::<Vec<_>>();
1350            regions.sort_unstable();
1351            assert_eq!(regions, [1, 2, 3]);
1352        }
1353    }
1354
1355    async fn rebuild_with_cached_table(
1356        manager: &PartitionRuleManager,
1357        cache: &TableInfoCache,
1358        request: RegionRequest,
1359    ) -> Result<Vec<RegionRequest>> {
1360        let region_id = match request.body.as_ref().unwrap() {
1361            Body::Inserts(inserts) => inserts.requests[0].region_id,
1362            Body::Deletes(deletes) => deletes.requests[0].region_id,
1363            Body::BulkInsert(bulk) => bulk.region_id,
1364            _ => unreachable!(),
1365        };
1366        let table_id = RegionId::from_u64(region_id).table_id();
1367        let table_info = cache.get(table_id).await.unwrap().unwrap();
1368        let partitioner = TablePartitioner::try_new(manager, &table_info)
1369            .await
1370            .context(error::InvalidPartitionSnafu)?;
1371        partitioner.rebuild_write_requests(request, &table_info)
1372    }
1373
1374    #[tokio::test]
1375    async fn test_tracked_bulk_owners_preserve_payload_alignment() {
1376        let (manager, cache) = create_fixture().await;
1377        let table_info = cache.get(1).await.unwrap().unwrap();
1378        let partitioner = TablePartitioner::try_new(&manager, &table_info)
1379            .await
1380            .unwrap();
1381        for len in [0, 1, 7, 64, 65, 257] {
1382            let batch = RecordBatch::try_new(
1383                Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)])),
1384                vec![Arc::new(Int32Array::from_iter_values(0..len as i32))],
1385            )
1386            .unwrap();
1387            let owners = (0..len)
1388                .map(|i| [2, 2, 0, 1, 0, 0][i % 6])
1389                .collect::<Vec<_>>();
1390            for offset in [0, 1, 7, 65] {
1391                for pattern in 0..3 {
1392                    let values = (0..offset + len)
1393                        .map(|i| match pattern {
1394                            0 => false,
1395                            1 => true,
1396                            _ => i % 3 != 0,
1397                        })
1398                        .collect::<Vec<_>>();
1399                    let expected = (0..len).filter(|i| values[offset + i]).collect::<Vec<_>>();
1400                    let mask = BooleanArray::from(values).slice(offset, len);
1401                    let requests = partitioner
1402                        .build_tracked_bulk_insert_requests(
1403                            batch.clone(),
1404                            [(1, RegionMask::from(mask))].into_iter().collect(),
1405                            owners.clone(),
1406                            None,
1407                            false,
1408                        )
1409                        .unwrap();
1410                    if expected.is_empty() {
1411                        assert!(requests.is_empty());
1412                        continue;
1413                    }
1414                    assert_eq!(requests.len(), 1);
1415                    let (request, selected_owners) = &requests[0];
1416                    let Some(bulk_insert_request::Body::ArrowIpc(ipc)) = &request.body else {
1417                        panic!("expected Arrow IPC")
1418                    };
1419                    let payload = FlightDecoder::try_from_schema_bytes(&ipc.schema)
1420                        .unwrap()
1421                        .try_decode_record_batch(&ipc.data_header, &ipc.payload)
1422                        .unwrap();
1423                    let actual_rows = payload
1424                        .column(0)
1425                        .as_any()
1426                        .downcast_ref::<Int32Array>()
1427                        .unwrap();
1428                    assert_eq!(
1429                        actual_rows
1430                            .values()
1431                            .iter()
1432                            .map(|x| *x as usize)
1433                            .collect::<Vec<_>>(),
1434                        expected
1435                    );
1436                    assert_eq!(
1437                        *selected_owners,
1438                        expected.iter().map(|i| owners[*i]).collect::<Vec<_>>()
1439                    );
1440                }
1441            }
1442            let wrong = partitioner.build_tracked_bulk_insert_requests(
1443                batch,
1444                HashMap::new(),
1445                vec![0; len + 1],
1446                None,
1447                false,
1448            );
1449            assert!(wrong.is_err());
1450        }
1451    }
1452}