1use std::hash::Hasher;
16use std::sync::Arc;
17
18use datatypes::arrow::array::{Array, ArrayRef, BinaryBuilder, UInt64Array};
19use datatypes::arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
20use datatypes::arrow::record_batch::RecordBatch;
21use datatypes::arrow_array::{is_string_null_at, string_array_value_at_index};
22use fxhash::FxHasher;
23use mito_codec::row_converter::SparsePrimaryKeyCodec;
24use snafu::ResultExt;
25use store_api::storage::ColumnId;
26use store_api::storage::consts::PRIMARY_KEY_COLUMN_NAME;
27
28use crate::error::{EncodePrimaryKeySnafu, Result, UnexpectedRequestSnafu};
29
30#[allow(dead_code)]
32pub struct TagColumnInfo {
33 pub name: String,
35 pub index: usize,
37 pub column_id: ColumnId,
39}
40
41pub fn compute_tsid_array(
51 batch: &RecordBatch,
52 sorted_tag_columns: &[TagColumnInfo],
53 tag_arrays: &[&ArrayRef],
54) -> UInt64Array {
55 let num_rows = batch.num_rows();
56
57 let label_name_hash = {
58 let mut hasher = FxHasher::default();
59 for tag_col in sorted_tag_columns {
60 hasher.write(tag_col.name.as_bytes());
61 hasher.write_u8(0xff);
62 }
63 hasher.finish()
64 };
65
66 let mut tsid_values = Vec::with_capacity(num_rows);
67 for row in 0..num_rows {
68 let has_null = tag_arrays.iter().any(|arr| is_string_null_at(arr, row));
69
70 let tsid = if !has_null {
71 let mut hasher = FxHasher::default();
72 hasher.write_u64(label_name_hash);
73 for arr in tag_arrays {
74 if let Some(value) = string_array_value_at_index(arr, row) {
75 hasher.write(value.as_bytes());
76 hasher.write_u8(0xff);
77 }
78 }
79 hasher.finish()
80 } else {
81 let mut name_hasher = FxHasher::default();
82 for (tc, arr) in sorted_tag_columns.iter().zip(tag_arrays.iter()) {
83 if !is_string_null_at(arr, row) {
84 name_hasher.write(tc.name.as_bytes());
85 name_hasher.write_u8(0xff);
86 }
87 }
88 let row_label_hash = name_hasher.finish();
89
90 let mut val_hasher = FxHasher::default();
91 val_hasher.write_u64(row_label_hash);
92 for arr in tag_arrays {
93 if let Some(value) = string_array_value_at_index(arr, row) {
94 val_hasher.write(value.as_bytes());
95 val_hasher.write_u8(0xff);
96 }
97 }
98 val_hasher.finish()
99 };
100
101 tsid_values.push(tsid);
102 }
103
104 UInt64Array::from(tsid_values)
105}
106
107fn build_tag_arrays<'a>(
108 batch: &'a RecordBatch,
109 sorted_tag_columns: &[TagColumnInfo],
110) -> Result<Vec<&'a ArrayRef>> {
111 sorted_tag_columns
112 .iter()
113 .map(|tc| {
114 let array = batch.column(tc.index);
115 match array.data_type() {
116 DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View => Ok(array),
117 DataType::Dictionary(key_type, value_type)
118 if key_type.is_integer() && value_type.is_string() =>
119 {
120 Ok(array)
121 }
122 data_type => UnexpectedRequestSnafu {
123 reason: format!(
124 "Tag column '{}' must be a string, given: {data_type}",
125 tc.name
126 ),
127 }
128 .fail(),
129 }
130 })
131 .collect()
132}
133
134pub fn modify_batch_sparse(
154 batch: RecordBatch,
155 table_id: u32,
156 sorted_tag_columns: &[TagColumnInfo],
157 extra_column_indices: &[usize],
158) -> Result<RecordBatch> {
159 let num_rows = batch.num_rows();
160 let codec = SparsePrimaryKeyCodec::schemaless();
161 let tag_arrays = build_tag_arrays(&batch, sorted_tag_columns)?;
162 let tsid_array = compute_tsid_array(&batch, sorted_tag_columns, &tag_arrays);
163
164 let mut pk_builder = BinaryBuilder::with_capacity(num_rows, 0);
165 let mut buffer = Vec::new();
166 for row in 0..num_rows {
167 buffer.clear();
168 codec
169 .encode_internal(table_id, tsid_array.value(row), &mut buffer)
170 .context(EncodePrimaryKeySnafu)?;
171
172 let tags = sorted_tag_columns
173 .iter()
174 .zip(tag_arrays.iter())
175 .filter_map(|(tc, arr)| {
176 string_array_value_at_index(arr, row).map(|value| (tc.column_id, value.as_bytes()))
177 });
178 codec
179 .encode_raw_tag_value(tags, &mut buffer)
180 .context(EncodePrimaryKeySnafu)?;
181
182 pk_builder.append_value(&buffer);
183 }
184
185 let pk_array = pk_builder.finish();
186
187 let mut fields = vec![Arc::new(Field::new(
188 PRIMARY_KEY_COLUMN_NAME,
189 DataType::Binary,
190 false,
191 ))];
192 let mut columns: Vec<Arc<dyn Array>> = vec![Arc::new(pk_array)];
193
194 for &idx in extra_column_indices {
195 fields.push(batch.schema().fields()[idx].clone());
196 columns.push(batch.column(idx).clone());
197 }
198
199 let new_schema = Arc::new(ArrowSchema::new(fields));
200 RecordBatch::try_new(new_schema, columns).map_err(|e| {
201 UnexpectedRequestSnafu {
202 reason: format!("Failed to build modified sparse RecordBatch: {e}"),
203 }
204 .build()
205 })
206}
207
208#[cfg(test)]
209mod tests {
210 use std::collections::HashMap;
211 use std::sync::Arc;
212
213 use api::v1::value::ValueData;
214 use api::v1::{ColumnDataType, ColumnSchema, Row, Rows, SemanticType, Value};
215 use datatypes::arrow::array::{BinaryArray, Int64Array, StringArray};
216 use datatypes::arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
217 use datatypes::arrow::record_batch::RecordBatch;
218 use store_api::codec::PrimaryKeyEncoding;
219 use store_api::metadata::ColumnMetadata;
220 use store_api::storage::consts::PRIMARY_KEY_COLUMN_NAME;
221
222 use super::*;
223 use crate::row_modifier::{RowModifier, RowsIter, TableIdInput};
224
225 fn build_sparse_test_batch() -> RecordBatch {
226 let schema = Arc::new(ArrowSchema::new(vec![
227 Field::new("greptime_timestamp", DataType::Int64, false),
228 Field::new("greptime_value", DataType::Float64, true),
229 Field::new("namespace", DataType::Utf8, true),
230 Field::new("host", DataType::Utf8, true),
231 ]));
232 RecordBatch::try_new(
233 schema,
234 vec![
235 Arc::new(Int64Array::from(vec![1000])),
236 Arc::new(datatypes::arrow::array::Float64Array::from(vec![42.0])),
237 Arc::new(StringArray::from(vec!["greptimedb"])),
238 Arc::new(StringArray::from(vec!["127.0.0.1"])),
239 ],
240 )
241 .unwrap()
242 }
243
244 fn sparse_tag_columns() -> Vec<TagColumnInfo> {
245 vec![
246 TagColumnInfo {
247 name: "host".to_string(),
248 index: 3,
249 column_id: 3,
250 },
251 TagColumnInfo {
252 name: "namespace".to_string(),
253 index: 2,
254 column_id: 2,
255 },
256 ]
257 }
258
259 #[test]
260 fn test_compute_tsid_basic() {
261 let schema = Arc::new(ArrowSchema::new(vec![
262 Field::new("namespace", DataType::Utf8, true),
263 Field::new("host", DataType::Utf8, true),
264 ]));
265 let batch = RecordBatch::try_new(
266 schema,
267 vec![
268 Arc::new(StringArray::from(vec!["greptimedb"])),
269 Arc::new(StringArray::from(vec!["127.0.0.1"])),
270 ],
271 )
272 .unwrap();
273
274 let tag_columns: Vec<TagColumnInfo> = vec![
275 TagColumnInfo {
276 name: "host".to_string(),
277 index: 1,
278 column_id: 2,
279 },
280 TagColumnInfo {
281 name: "namespace".to_string(),
282 index: 0,
283 column_id: 1,
284 },
285 ];
286 let tag_arrays = build_tag_arrays(&batch, &tag_columns).unwrap();
287 let tsid_array = compute_tsid_array(&batch, &tag_columns, &tag_arrays);
288
289 assert_eq!(tsid_array.value(0), 2721566936019240841);
290 }
291
292 #[test]
293 fn test_compute_tsid_with_nulls() {
294 let schema = Arc::new(ArrowSchema::new(vec![
295 Field::new("a", DataType::Utf8, true),
296 Field::new("b", DataType::Utf8, true),
297 ]));
298 let batch_no_null = RecordBatch::try_new(
299 schema.clone(),
300 vec![
301 Arc::new(StringArray::from(vec!["A"])),
302 Arc::new(StringArray::from(vec!["B"])),
303 ],
304 )
305 .unwrap();
306 let tag_cols_2: Vec<TagColumnInfo> = vec![
307 TagColumnInfo {
308 name: "a".to_string(),
309 index: 0,
310 column_id: 1,
311 },
312 TagColumnInfo {
313 name: "b".to_string(),
314 index: 1,
315 column_id: 2,
316 },
317 ];
318 let tag_arrays_2 = build_tag_arrays(&batch_no_null, &tag_cols_2).unwrap();
319 let tsid_no_null = compute_tsid_array(&batch_no_null, &tag_cols_2, &tag_arrays_2);
320
321 let schema3 = Arc::new(ArrowSchema::new(vec![
322 Field::new("a", DataType::Utf8, true),
323 Field::new("b", DataType::Utf8, true),
324 Field::new("c", DataType::Utf8, true),
325 ]));
326 let batch_with_null = RecordBatch::try_new(
327 schema3,
328 vec![
329 Arc::new(StringArray::from(vec!["A"])),
330 Arc::new(StringArray::from(vec!["B"])),
331 Arc::new(StringArray::from(vec![None as Option<&str>])),
332 ],
333 )
334 .unwrap();
335 let tag_cols_3: Vec<TagColumnInfo> = vec![
336 TagColumnInfo {
337 name: "a".to_string(),
338 index: 0,
339 column_id: 1,
340 },
341 TagColumnInfo {
342 name: "b".to_string(),
343 index: 1,
344 column_id: 2,
345 },
346 TagColumnInfo {
347 name: "c".to_string(),
348 index: 2,
349 column_id: 3,
350 },
351 ];
352 let tag_arrays_3 = build_tag_arrays(&batch_with_null, &tag_cols_3).unwrap();
353 let tsid_with_null = compute_tsid_array(&batch_with_null, &tag_cols_3, &tag_arrays_3);
354
355 assert_eq!(tsid_no_null.value(0), tsid_with_null.value(0));
356 }
357
358 #[test]
359 fn test_modify_batch_sparse() {
360 let batch = build_sparse_test_batch();
361 let tag_columns = sparse_tag_columns();
362 let non_tag_indices = vec![0, 1];
363 let table_id: u32 = 1025;
364
365 let modified =
366 modify_batch_sparse(batch, table_id, &tag_columns, &non_tag_indices).unwrap();
367
368 assert_eq!(modified.num_columns(), 3);
369 assert_eq!(modified.schema().field(0).name(), PRIMARY_KEY_COLUMN_NAME);
370 assert_eq!(modified.schema().field(1).name(), "greptime_timestamp");
371 assert_eq!(modified.schema().field(2).name(), "greptime_value");
372 }
373
374 #[test]
375 fn test_modify_batch_sparse_matches_row_modifier() {
376 let batch = build_sparse_test_batch();
377 let tag_columns = sparse_tag_columns();
378 let non_tag_indices = vec![0, 1];
379 let table_id: u32 = 1025;
380 let modified =
381 modify_batch_sparse(batch, table_id, &tag_columns, &non_tag_indices).unwrap();
382
383 let make_info = |name: &str, column_id: ColumnId| ColumnMetadata {
384 column_schema: datatypes::schema::ColumnSchema::new(
385 name.to_string(),
386 datatypes::prelude::ConcreteDataType::string_datatype(),
387 false,
388 ),
389 semantic_type: SemanticType::Tag,
390 column_id,
391 };
392 let name_to_column_id: HashMap<String, ColumnMetadata> = [
393 (
394 "greptime_timestamp".to_string(),
395 make_info("greptime_timestamp", 0),
396 ),
397 ("greptime_value".to_string(), make_info("greptime_value", 1)),
398 ("namespace".to_string(), make_info("namespace", 2)),
399 ("host".to_string(), make_info("host", 3)),
400 ]
401 .into_iter()
402 .collect();
403
404 let rows = Rows {
405 schema: vec![
406 ColumnSchema {
407 column_name: "greptime_timestamp".to_string(),
408 datatype: ColumnDataType::TimestampMillisecond as i32,
409 semantic_type: SemanticType::Timestamp as i32,
410 ..Default::default()
411 },
412 ColumnSchema {
413 column_name: "greptime_value".to_string(),
414 datatype: ColumnDataType::Float64 as i32,
415 semantic_type: SemanticType::Field as i32,
416 ..Default::default()
417 },
418 ColumnSchema {
419 column_name: "namespace".to_string(),
420 datatype: ColumnDataType::String as i32,
421 semantic_type: SemanticType::Tag as i32,
422 ..Default::default()
423 },
424 ColumnSchema {
425 column_name: "host".to_string(),
426 datatype: ColumnDataType::String as i32,
427 semantic_type: SemanticType::Tag as i32,
428 ..Default::default()
429 },
430 ],
431 rows: vec![Row {
432 values: vec![
433 Value {
434 value_data: Some(ValueData::TimestampMillisecondValue(1000)),
435 },
436 Value {
437 value_data: Some(ValueData::F64Value(42.0)),
438 },
439 Value {
440 value_data: Some(ValueData::StringValue("greptimedb".to_string())),
441 },
442 Value {
443 value_data: Some(ValueData::StringValue("127.0.0.1".to_string())),
444 },
445 ],
446 }],
447 };
448
449 let row_iter = RowsIter::new(rows, &name_to_column_id);
450 let rows = RowModifier::default()
451 .modify_rows(
452 row_iter,
453 TableIdInput::Single(table_id),
454 PrimaryKeyEncoding::Sparse,
455 )
456 .unwrap();
457 let ValueData::BinaryValue(expected_pk) =
458 rows.rows[0].values[0].value_data.clone().unwrap()
459 else {
460 panic!("expected binary primary key");
461 };
462
463 let actual_array = modified
464 .column(0)
465 .as_any()
466 .downcast_ref::<BinaryArray>()
467 .unwrap();
468 assert_eq!(actual_array.value(0), expected_pk.as_slice());
469 }
470
471 #[test]
472 fn label_replace_with_utf8view_labels_does_not_panic() {
473 let tag_arrays: Vec<ArrayRef> = vec![
477 Arc::new(StringArray::from(vec!["greptimedb"])),
478 Arc::new(datatypes::arrow::array::LargeStringArray::from(vec![
479 "greptimedb",
480 ])),
481 Arc::new(datatypes::arrow::array::StringViewArray::from(vec![
482 "greptimedb",
483 ])),
484 Arc::new(datatypes::arrow::array::DictionaryArray::<
485 datatypes::arrow::datatypes::UInt32Type,
486 >::new(
487 datatypes::arrow::array::UInt32Array::from(vec![0]),
488 Arc::new(StringArray::from(vec!["greptimedb"])),
489 )),
490 ];
491 let tag_columns = vec![TagColumnInfo {
492 name: "namespace".to_string(),
493 index: 2,
494 column_id: 2,
495 }];
496 let non_tag_indices = vec![0, 1];
497
498 let primary_keys = tag_arrays
499 .into_iter()
500 .map(|tag_array| {
501 let schema = Arc::new(ArrowSchema::new(vec![
502 Field::new("greptime_timestamp", DataType::Int64, false),
503 Field::new("greptime_value", DataType::Float64, true),
504 Field::new("namespace", tag_array.data_type().clone(), true),
505 ]));
506 let batch = RecordBatch::try_new(
507 schema,
508 vec![
509 Arc::new(Int64Array::from(vec![1000])),
510 Arc::new(datatypes::arrow::array::Float64Array::from(vec![42.0])),
511 tag_array,
512 ],
513 )
514 .unwrap();
515 let modified =
516 modify_batch_sparse(batch, 1025, &tag_columns, &non_tag_indices).unwrap();
517 modified
518 .column(0)
519 .as_any()
520 .downcast_ref::<BinaryArray>()
521 .unwrap()
522 .value(0)
523 .to_vec()
524 })
525 .collect::<Vec<_>>();
526
527 assert!(primary_keys.windows(2).all(|keys| keys[0] == keys[1]));
528 }
529
530 #[test]
531 fn test_compute_tsid_utf8view_with_nulls_matches_utf8() {
532 let namespace_values: Vec<Option<&str>> =
537 vec![Some("ns-a"), Some("ns-b"), None, Some("ns-c")];
538 let host_values: Vec<Option<&str>> = vec![Some("host-1"), None, Some("host-2"), None];
539
540 let build_batch = |data_type: DataType| {
541 let (namespace, host): (ArrayRef, ArrayRef) = match &data_type {
542 DataType::Utf8 => (
543 Arc::new(StringArray::from(namespace_values.clone())),
544 Arc::new(StringArray::from(host_values.clone())),
545 ),
546 DataType::Utf8View => (
547 Arc::new(datatypes::arrow::array::StringViewArray::from(
548 namespace_values.clone(),
549 )),
550 Arc::new(datatypes::arrow::array::StringViewArray::from(
551 host_values.clone(),
552 )),
553 ),
554 _ => unreachable!(),
555 };
556 let schema = Arc::new(ArrowSchema::new(vec![
557 Field::new("greptime_timestamp", DataType::Int64, false),
558 Field::new("greptime_value", DataType::Float64, true),
559 Field::new("namespace", data_type.clone(), true),
560 Field::new("host", data_type, true),
561 ]));
562 RecordBatch::try_new(
563 schema,
564 vec![
565 Arc::new(Int64Array::from(vec![1000, 1001, 1002, 1003])),
566 Arc::new(datatypes::arrow::array::Float64Array::from(vec![
567 1.0, 2.0, 3.0, 4.0,
568 ])),
569 namespace,
570 host,
571 ],
572 )
573 .unwrap()
574 };
575
576 let tag_columns = vec![
577 TagColumnInfo {
578 name: "host".to_string(),
579 index: 3,
580 column_id: 3,
581 },
582 TagColumnInfo {
583 name: "namespace".to_string(),
584 index: 2,
585 column_id: 2,
586 },
587 ];
588
589 let utf8_batch = build_batch(DataType::Utf8);
590 let utf8view_batch = build_batch(DataType::Utf8View);
591
592 let utf8_tag_arrays = build_tag_arrays(&utf8_batch, &tag_columns).unwrap();
594 let utf8view_tag_arrays = build_tag_arrays(&utf8view_batch, &tag_columns).unwrap();
595
596 let utf8_tsids = compute_tsid_array(&utf8_batch, &tag_columns, &utf8_tag_arrays);
598 let utf8view_tsids =
599 compute_tsid_array(&utf8view_batch, &tag_columns, &utf8view_tag_arrays);
600 assert_eq!(utf8_tsids, utf8view_tsids);
601
602 let non_tag_indices = vec![0, 1];
604 let modified_utf8 =
605 modify_batch_sparse(utf8_batch, 1025, &tag_columns, &non_tag_indices).unwrap();
606 let modified_utf8view =
607 modify_batch_sparse(utf8view_batch, 1025, &tag_columns, &non_tag_indices).unwrap();
608 let utf8_pks = modified_utf8
609 .column(0)
610 .as_any()
611 .downcast_ref::<BinaryArray>()
612 .unwrap();
613 let utf8view_pks = modified_utf8view
614 .column(0)
615 .as_any()
616 .downcast_ref::<BinaryArray>()
617 .unwrap();
618 for row in 0..utf8_pks.len() {
619 assert_eq!(utf8_pks.value(row), utf8view_pks.value(row));
620 }
621 }
622}