1use std::collections::HashMap;
50
51use common_telemetry::warn;
52use datafusion_common::ScalarValue;
53use datafusion_expr::type_coercion::binary::comparison_coercion;
54use datatypes::data_type::{ConcreteDataType, DataType};
55use datatypes::schema::Schema;
56use datatypes::value::{
57 Value, duration_to_scalar_value, time_to_scalar_value, timestamp_to_scalar_value,
58};
59use serde::Deserialize;
60use store_api::storage::RegionId;
61
62use crate::error::{PartitionExprIncompatibleSnafu, Result};
63use crate::rpc::router::RegionRoute;
64
65#[derive(Debug, Deserialize)]
67enum PersistedPartitionBound {
68 #[allow(dead_code)]
70 Value(Value),
71 MaxValue,
73 Expr(PersistedPartitionExpr),
74}
75
76#[derive(Debug, Deserialize)]
78struct PersistedPartitionExpr {
79 lhs: Box<PersistedOperand>,
80 op: PersistedOp,
81 rhs: Box<PersistedOperand>,
82}
83
84#[derive(Debug, Deserialize)]
86enum PersistedOperand {
87 Column(String),
88 Value(Value),
89 Expr(PersistedPartitionExpr),
90}
91
92#[derive(Debug, Deserialize)]
94enum PersistedOp {
95 Eq,
96 NotEq,
97 Lt,
98 LtEq,
99 Gt,
100 GtEq,
101 And,
102 Or,
103}
104
105pub(crate) fn changed_column_types(
110 old_schema: &Schema,
111 new_schema: &Schema,
112) -> HashMap<String, (ConcreteDataType, ConcreteDataType)> {
113 let mut changed = HashMap::new();
114 for new_column in new_schema.column_schemas() {
115 let Some(old_column) = old_schema.column_schema_by_name(&new_column.name) else {
116 continue;
117 };
118 if old_column.data_type != new_column.data_type {
119 changed.insert(
120 new_column.name.clone(),
121 (old_column.data_type.clone(), new_column.data_type.clone()),
122 );
123 }
124 }
125
126 changed
127}
128
129pub fn validate_region_partition_bounds(
136 changed_columns: &HashMap<String, (ConcreteDataType, ConcreteDataType)>,
137 region_routes: &[RegionRoute],
138) -> Result<()> {
139 if changed_columns.is_empty() {
140 return Ok(());
141 }
142
143 for route in region_routes {
144 let expr_json = route.region.partition_expr();
145 if expr_json.is_empty() {
146 continue;
147 }
148
149 let bound: PersistedPartitionBound = match serde_json::from_str(&expr_json) {
150 Ok(bound) => bound,
151 Err(err) => {
152 warn!(
155 "Skipping partition expression validation for region {}, \
156 the persisted expression is not parseable: {}, expr: {}",
157 route.region.id, err, expr_json
158 );
159 continue;
160 }
161 };
162 let PersistedPartitionBound::Expr(expr) = bound else {
163 continue;
166 };
167
168 validate_expr(&expr, changed_columns, route.region.id)?;
169 }
170
171 Ok(())
172}
173
174fn validate_expr(
175 expr: &PersistedPartitionExpr,
176 changed_columns: &HashMap<String, (ConcreteDataType, ConcreteDataType)>,
177 region_id: RegionId,
178) -> Result<()> {
179 match expr.op {
180 PersistedOp::And | PersistedOp::Or => {
181 validate_operand(&expr.lhs, changed_columns, region_id)?;
182 validate_operand(&expr.rhs, changed_columns, region_id)?;
183 }
184 PersistedOp::Eq
185 | PersistedOp::NotEq
186 | PersistedOp::Lt
187 | PersistedOp::LtEq
188 | PersistedOp::Gt
189 | PersistedOp::GtEq => {
190 validate_comparison(&expr.lhs, &expr.rhs, changed_columns, region_id)?;
192 validate_comparison(&expr.rhs, &expr.lhs, changed_columns, region_id)?;
193 }
194 }
195
196 Ok(())
197}
198
199fn validate_operand(
200 operand: &PersistedOperand,
201 changed_columns: &HashMap<String, (ConcreteDataType, ConcreteDataType)>,
202 region_id: RegionId,
203) -> Result<()> {
204 match operand {
205 PersistedOperand::Expr(expr) => validate_expr(expr, changed_columns, region_id),
206 PersistedOperand::Column(_) | PersistedOperand::Value(_) => Ok(()),
207 }
208}
209
210fn validate_comparison(
211 operand: &PersistedOperand,
212 other: &PersistedOperand,
213 changed_columns: &HashMap<String, (ConcreteDataType, ConcreteDataType)>,
214 region_id: RegionId,
215) -> Result<()> {
216 let (PersistedOperand::Column(column), PersistedOperand::Value(value)) = (operand, other)
217 else {
218 return Ok(());
219 };
220 let Some((old_type, target_type)) = changed_columns.get(column) else {
221 return Ok(());
222 };
223 let Some(scalar) = value_to_scalar_value(value) else {
226 return Ok(());
227 };
228
229 if !old_type.is_timestamp_unit_widening_to(target_type) {
235 warn!(
236 "Persisted partition bound would be compared against values of a different \
237 type after the ALTER, region_id: {}, column: '{}', target_type: {}",
238 region_id, column, target_type
239 );
240 return PartitionExprIncompatibleSnafu {
241 region_id,
242 column: column.clone(),
243 target_type: target_type.to_string(),
244 }
245 .fail();
246 }
247
248 let target_arrow_type = target_type.as_arrow_type();
255 let Some(common_type) = comparison_coercion(&target_arrow_type, &scalar.data_type()) else {
256 warn!(
257 "Persisted partition bound has no common comparison type with the altered \
258 column type, region_id: {}, column: '{}', target_type: {}",
259 region_id, column, target_type
260 );
261 return PartitionExprIncompatibleSnafu {
262 region_id,
263 column: column.clone(),
264 target_type: target_type.to_string(),
265 }
266 .fail();
267 };
268 if scalar.data_type() != common_type
269 && let Err(error) = scalar.cast_to(&common_type)
270 {
271 warn!(
272 error;
273 "Persisted partition bound is not representable with the altered column type, \
274 region_id: {}, column: '{}', target_type: {}",
275 region_id,
276 column,
277 target_type
278 );
279 return PartitionExprIncompatibleSnafu {
280 region_id,
281 column: column.clone(),
282 target_type: target_type.to_string(),
283 }
284 .fail();
285 }
286
287 Ok(())
288}
289
290fn value_to_scalar_value(value: &Value) -> Option<ScalarValue> {
294 Some(match value {
295 Value::Boolean(v) => ScalarValue::Boolean(Some(*v)),
296 Value::UInt8(v) => ScalarValue::UInt8(Some(*v)),
297 Value::UInt16(v) => ScalarValue::UInt16(Some(*v)),
298 Value::UInt32(v) => ScalarValue::UInt32(Some(*v)),
299 Value::UInt64(v) => ScalarValue::UInt64(Some(*v)),
300 Value::Int8(v) => ScalarValue::Int8(Some(*v)),
301 Value::Int16(v) => ScalarValue::Int16(Some(*v)),
302 Value::Int32(v) => ScalarValue::Int32(Some(*v)),
303 Value::Int64(v) => ScalarValue::Int64(Some(*v)),
304 Value::Float32(v) => ScalarValue::Float32(Some(v.0)),
305 Value::Float64(v) => ScalarValue::Float64(Some(v.0)),
306 Value::String(v) => ScalarValue::Utf8(Some(v.as_utf8().to_string())),
307 Value::Binary(v) => ScalarValue::Binary(Some(v.to_vec())),
308 Value::Date(v) => ScalarValue::Date32(Some(v.val())),
309 Value::Null => ScalarValue::Null,
310 Value::Timestamp(t) => timestamp_to_scalar_value(t.unit(), Some(t.value())),
311 Value::Time(t) => time_to_scalar_value(*t.unit(), Some(t.value())).ok()?,
312 Value::IntervalYearMonth(v) => ScalarValue::IntervalYearMonth(Some(v.to_i32())),
313 Value::IntervalDayTime(v) => ScalarValue::IntervalDayTime(Some((*v).into())),
314 Value::IntervalMonthDayNano(v) => ScalarValue::IntervalMonthDayNano(Some((*v).into())),
315 Value::Duration(d) => duration_to_scalar_value(d.unit(), Some(d.value())),
316 Value::Decimal128(d) => {
317 let (v, p, s) = d.to_scalar_value();
318 ScalarValue::Decimal128(v, p, s)
319 }
320 Value::List(_) | Value::Struct(_) | Value::Json(_) => return None,
321 })
322}
323
324#[cfg(test)]
325mod tests {
326 use datatypes::schema::ColumnSchema;
327 use store_api::storage::RegionId;
328
329 use super::*;
330 use crate::rpc::router::Region;
331
332 fn schema(ts_type: ConcreteDataType) -> Schema {
333 Schema::new(vec![
334 ColumnSchema::new("ts", ts_type, false),
335 ColumnSchema::new("host", ConcreteDataType::string_datatype(), true),
336 ])
337 }
338
339 fn ts_bound(millis: i64) -> String {
340 format!(
341 r#"{{"Expr":{{"lhs":{{"Column":"ts"}},"op":"Lt","rhs":{{"Value":{{"Timestamp":{{"value":{millis},"unit":"Millisecond"}}}}}}}}}}"#
342 )
343 }
344
345 fn routes(exprs: &[&str]) -> Vec<RegionRoute> {
346 exprs
347 .iter()
348 .enumerate()
349 .map(|(index, expr)| RegionRoute {
350 region: Region {
351 id: RegionId::new(1024, index as u32 + 1),
352 partition_expr: expr.to_string(),
353 ..Default::default()
354 },
355 ..Default::default()
356 })
357 .collect()
358 }
359
360 fn changed(
361 column: &str,
362 old_type: ConcreteDataType,
363 new_type: ConcreteDataType,
364 ) -> HashMap<String, (ConcreteDataType, ConcreteDataType)> {
365 HashMap::from([(column.to_string(), (old_type, new_type))])
366 }
367
368 #[test]
369 fn test_changed_column_types() {
370 let old = schema(ConcreteDataType::timestamp_millisecond_datatype());
371 let new = schema(ConcreteDataType::timestamp_nanosecond_datatype());
372 let changed = changed_column_types(&old, &new);
373 assert_eq!(changed.len(), 1);
374 assert_eq!(
375 changed["ts"],
376 (
377 ConcreteDataType::timestamp_millisecond_datatype(),
378 ConcreteDataType::timestamp_nanosecond_datatype()
379 )
380 );
381
382 let mut new_columns = old.column_schemas().to_vec();
384 new_columns.push(ColumnSchema::new(
385 "extra",
386 ConcreteDataType::int64_datatype(),
387 true,
388 ));
389 let new = Schema::new(new_columns);
390 assert!(changed_column_types(&old, &new).is_empty());
391 }
392
393 #[test]
394 fn test_overflowing_bound_is_rejected() {
395 let routes = routes(&[&ts_bound(32_503_680_000_000)]);
397 let err = validate_region_partition_bounds(
398 &changed(
399 "ts",
400 ConcreteDataType::timestamp_millisecond_datatype(),
401 ConcreteDataType::timestamp_nanosecond_datatype(),
402 ),
403 &routes,
404 )
405 .unwrap_err();
406 assert!(
407 err.to_string().contains("region 4398046511105"),
408 "unexpected error: {err}"
409 );
410 assert!(err.to_string().contains("'ts'"), "unexpected error: {err}");
411 }
412
413 #[test]
414 fn test_representable_bound_is_accepted() {
415 let routes = routes(&[&ts_bound(1_704_067_200_000)]);
417 validate_region_partition_bounds(
418 &changed(
419 "ts",
420 ConcreteDataType::timestamp_millisecond_datatype(),
421 ConcreteDataType::timestamp_nanosecond_datatype(),
422 ),
423 &routes,
424 )
425 .unwrap();
426 validate_region_partition_bounds(
427 &changed(
428 "ts",
429 ConcreteDataType::timestamp_millisecond_datatype(),
430 ConcreteDataType::timestamp_microsecond_datatype(),
431 ),
432 &routes,
433 )
434 .unwrap();
435 }
436
437 #[test]
438 fn test_later_region_rejection_names_the_region() {
439 let routes = routes(&[&ts_bound(1_704_067_200_000), &ts_bound(32_503_680_000_000)]);
440 let err = validate_region_partition_bounds(
441 &changed(
442 "ts",
443 ConcreteDataType::timestamp_millisecond_datatype(),
444 ConcreteDataType::timestamp_nanosecond_datatype(),
445 ),
446 &routes,
447 )
448 .unwrap_err();
449 assert!(
450 err.to_string().contains("region 4398046511106"),
451 "unexpected error: {err}"
452 );
453 }
454
455 #[test]
456 fn test_unrelated_columns_are_skipped() {
457 let host_bound =
458 r#"{"Expr":{"lhs":{"Column":"host"},"op":"Eq","rhs":{"Value":{"String":"a"}}}}"#;
459 validate_region_partition_bounds(
461 &changed(
462 "ts",
463 ConcreteDataType::timestamp_millisecond_datatype(),
464 ConcreteDataType::timestamp_nanosecond_datatype(),
465 ),
466 &routes(&[host_bound]),
467 )
468 .unwrap();
469 validate_region_partition_bounds(
471 &changed(
472 "host",
473 ConcreteDataType::int32_datatype(),
474 ConcreteDataType::string_datatype(),
475 ),
476 &routes(&[&ts_bound(32_503_680_000_000)]),
477 )
478 .unwrap();
479 }
480
481 #[test]
482 fn test_nested_expression_is_validated() {
483 let expr = r#"{"Expr":{"lhs":{"Expr":{"lhs":{"Column":"ts"},"op":"GtEq","rhs":{"Value":{"Timestamp":{"value":0,"unit":"Millisecond"}}}}},"op":"And","rhs":{"Expr":{"lhs":{"Column":"ts"},"op":"Lt","rhs":{"Value":{"Timestamp":{"value":32503680000000,"unit":"Millisecond"}}}}}}}"#.to_string();
484 let err = validate_region_partition_bounds(
485 &changed(
486 "ts",
487 ConcreteDataType::timestamp_millisecond_datatype(),
488 ConcreteDataType::timestamp_nanosecond_datatype(),
489 ),
490 &routes(&[&expr]),
491 )
492 .unwrap_err();
493 assert!(err.to_string().contains("'ts'"), "unexpected error: {err}");
494 }
495
496 #[test]
497 fn test_deprecated_and_empty_bounds_are_skipped() {
498 validate_region_partition_bounds(
499 &changed(
500 "ts",
501 ConcreteDataType::timestamp_millisecond_datatype(),
502 ConcreteDataType::timestamp_nanosecond_datatype(),
503 ),
504 &routes(&["", r#""MaxValue""#, r#"{"Value":{"UInt32":1}}"#, "not-json"]),
505 )
506 .unwrap();
507 }
508
509 #[test]
510 fn test_field_narrowing_is_rejected_for_persisted_bounds() {
511 let bound =
516 r#"{"Expr":{"lhs":{"Column":"v"},"op":"Lt","rhs":{"Value":{"Int64":3000000000}}}}"#;
517 let err = validate_region_partition_bounds(
518 &changed(
519 "v",
520 ConcreteDataType::int64_datatype(),
521 ConcreteDataType::int32_datatype(),
522 ),
523 &routes(&[bound]),
524 )
525 .unwrap_err();
526 assert!(err.to_string().contains("'v'"), "unexpected error: {err}");
527 }
528
529 #[test]
530 fn test_non_timestamp_widening_is_rejected_for_persisted_bounds() {
531 let bound = r#"{"Expr":{"lhs":{"Column":"v"},"op":"Lt","rhs":{"Value":{"Int32":10}}}}"#;
535 let err = validate_region_partition_bounds(
536 &changed(
537 "v",
538 ConcreteDataType::int32_datatype(),
539 ConcreteDataType::int64_datatype(),
540 ),
541 &routes(&[bound]),
542 )
543 .unwrap_err();
544 assert!(err.to_string().contains("'v'"), "unexpected error: {err}");
545 }
546
547 #[test]
548 fn test_bound_without_common_comparison_type_is_rejected() {
549 let bound =
553 r#"{"Expr":{"lhs":{"Column":"ts"},"op":"Lt","rhs":{"Value":{"Boolean":true}}}}"#;
554 let err = validate_region_partition_bounds(
555 &changed(
556 "ts",
557 ConcreteDataType::timestamp_millisecond_datatype(),
558 ConcreteDataType::timestamp_nanosecond_datatype(),
559 ),
560 &routes(&[bound]),
561 )
562 .unwrap_err();
563 assert!(err.to_string().contains("'ts'"), "unexpected error: {err}");
564 }
565}