1mod conventions;
29mod relationships;
30
31use std::sync::{Arc, LazyLock};
32
33use api::v1::column_data_type_extension::TypeExt;
34use api::v1::{
35 ColumnDataType, ColumnDataTypeExtension, ColumnDef, CreateTableExpr, JsonTypeExtension,
36 SemanticType,
37};
38use common_catalog::consts::{
39 CONFIDENCE_COLUMN, DEFAULT_CATALOG_NAME, DEFAULT_PRIVATE_SCHEMA_NAME, DST_ID_COLUMN,
40 DST_TYPE_COLUMN, DURATION_COUNT_COLUMN, DURATION_SUM_COLUMN, EDGE_ATTRIBUTES_COLUMN,
41 ENTITY_DESCRIPTIVE_COLUMN, ENTITY_ID_ATTRS_COLUMN, ENTITY_ID_COLUMN, ENTITY_SCOPE_COLUMN,
42 ENTITY_TYPE_COLUMN, ERROR_COUNT_COLUMN, FRESH_UNTIL_COLUMN, GENERATION_ID_COLUMN,
43 OBSERVED_AT_COLUMN, PROVENANCE_COLUMN, REL_TYPE_COLUMN, REQUEST_COUNT_COLUMN,
44 SEMANTIC_RELATIONSHIPS_DECLARED_TABLE_NAME, SOURCE_TABLES_COLUMN, SRC_ID_COLUMN,
45 SRC_TYPE_COLUMN, VALID_FROM_COLUMN, VALID_UNTIL_COLUMN, WINDOW_END_COLUMN, WINDOW_START_COLUMN,
46};
47use common_function::function::FunctionContext;
48use common_function::function_registry::FUNCTION_REGISTRY;
49pub use conventions::{
50 Conventions, ENTITY_TYPE_GEN_AI_AGENT, ENTITY_TYPE_GEN_AI_MODEL, ENTITY_TYPE_GEN_AI_TOOL,
51 ENTITY_TYPE_HOST, ENTITY_TYPE_K8S_CONTAINER, ENTITY_TYPE_K8S_NODE, ENTITY_TYPE_K8S_POD,
52 ENTITY_TYPE_K8S_WORKLOAD, ENTITY_TYPE_PROCESS, ENTITY_TYPE_SERVICE,
53 ENTITY_TYPE_SERVICE_INSTANCE, ImplicitEntity, PROVENANCE_AGENT, PROVENANCE_ATTRIBUTE,
54 PROVENANCE_DECLARED, PROVENANCE_TRACE, REL_TYPE_CALLS, REL_TYPE_CONTAINS, REL_TYPE_DEPENDS_ON,
55 REL_TYPE_INVOKES, REL_TYPE_OWNS, REL_TYPE_PART_OF, REL_TYPE_RUNS_ON, REL_TYPE_USES,
56 conventions,
57};
58use datafusion::arrow::datatypes::{DataType, TimeUnit};
59use datafusion::dataframe::DataFrame;
60use datafusion::functions::{core as core_fns, datetime as datetime_fns, string as string_fns};
61use datafusion::functions_nested::expr_fn::make_array;
62use datafusion_common::{Column, Result as DfResult, ScalarValue};
63use datafusion_expr::{Expr, LogicalPlan, ScalarUDF, cast, ident, lit};
64pub use relationships::{
65 CallsSource, CoDeclaredSource, DeclaredSource, RelationshipSources, build_relationships_plan,
66};
67use store_api::mito_engine_options::{APPEND_MODE_KEY, MERGE_MODE_KEY, TTL_KEY};
68
69pub fn declared_relationships_schema_matches(table_info: &table::metadata::TableInfo) -> bool {
75 let meta = &table_info.meta;
76 if meta.engine != common_catalog::consts::MITO_ENGINE {
77 return false;
78 }
79 let schema = &meta.schema;
80 if schema
81 .timestamp_column()
82 .is_none_or(|column| column.name != OBSERVED_AT_COLUMN)
83 {
84 return false;
85 }
86 let primary_keys = meta
87 .primary_key_indices
88 .iter()
89 .filter_map(|idx| schema.column_schemas().get(*idx))
90 .map(|column| column.name.as_str());
91 if !primary_keys.eq(DECLARED_PRIMARY_KEY_COLUMNS) {
92 return false;
93 }
94 let options = &meta.options.extra_options;
97 if options.get(APPEND_MODE_KEY).map(String::as_str) == Some("true")
98 || options
99 .get(MERGE_MODE_KEY)
100 .is_some_and(|mode| mode != "last_row")
101 {
102 return false;
103 }
104
105 let canonical = build_declared_relationships_expr(DEFAULT_CATALOG_NAME);
106 if schema.column_schemas().len() != canonical.column_defs.len() {
107 return false;
108 }
109 canonical
110 .column_defs
111 .iter()
112 .zip(schema.column_schemas())
113 .all(|(def, column)| {
114 let Ok(wrapper) = api::helper::ColumnDataTypeWrapper::try_new(
115 def.data_type,
116 def.datatype_extension.clone(),
117 ) else {
118 return false;
119 };
120 def.name == column.name
121 && datatypes::prelude::ConcreteDataType::from(wrapper) == column.data_type
122 })
123}
124
125const BIN_NANOS: i64 = 60 * 1_000_000_000;
128
129const DEFAULT_DECLARED_RELATIONSHIPS_TTL: &str = "90d";
131const DECLARED_RELATIONSHIPS_TTL_ENV: &str = "GREPTIMEDB_DECLARED_RELATIONSHIPS_TTL";
135
136fn declared_relationships_ttl() -> String {
137 std::env::var(DECLARED_RELATIONSHIPS_TTL_ENV)
138 .ok()
139 .map(|ttl| ttl.trim().to_string())
140 .filter(|ttl| !ttl.is_empty())
141 .unwrap_or_else(|| DEFAULT_DECLARED_RELATIONSHIPS_TTL.to_string())
142}
143
144pub const DECLARED_PRIMARY_KEY_COLUMNS: [&str; 8] = [
149 SRC_TYPE_COLUMN,
150 SRC_ID_COLUMN,
151 REL_TYPE_COLUMN,
152 DST_TYPE_COLUMN,
153 DST_ID_COLUMN,
154 PROVENANCE_COLUMN,
155 ENTITY_SCOPE_COLUMN,
156 GENERATION_ID_COLUMN,
157];
158
159const DECLARED_EDGE_IDENTITY_COLUMNS: [&str; 6] = [
164 SRC_TYPE_COLUMN,
165 SRC_ID_COLUMN,
166 REL_TYPE_COLUMN,
167 DST_TYPE_COLUMN,
168 DST_ID_COLUMN,
169 PROVENANCE_COLUMN,
170];
171
172fn column(
173 name: &str,
174 data_type: ColumnDataType,
175 semantic_type: SemanticType,
176 nullable: bool,
177) -> ColumnDef {
178 ColumnDef {
179 name: name.to_string(),
180 data_type: data_type as i32,
181 is_nullable: nullable,
182 default_constraint: vec![],
183 semantic_type: semantic_type as i32,
184 comment: String::new(),
185 datatype_extension: None,
186 options: None,
187 }
188}
189
190fn tag(name: &str) -> ColumnDef {
191 column(name, ColumnDataType::String, SemanticType::Tag, false)
192}
193
194fn field(name: &str, data_type: ColumnDataType) -> ColumnDef {
195 column(name, data_type, SemanticType::Field, true)
196}
197
198fn json_field(name: &str) -> ColumnDef {
199 let mut def = field(name, ColumnDataType::Binary);
200 def.datatype_extension = Some(ColumnDataTypeExtension {
201 type_ext: Some(TypeExt::JsonType(JsonTypeExtension::JsonBinary.into())),
202 });
203 def
204}
205
206pub fn build_declared_relationships_expr(catalog: &str) -> CreateTableExpr {
217 let column_defs = vec![
218 column(
221 OBSERVED_AT_COLUMN,
222 ColumnDataType::TimestampMillisecond,
223 SemanticType::Timestamp,
224 false,
225 ),
226 field(WINDOW_START_COLUMN, ColumnDataType::TimestampMillisecond),
227 field(WINDOW_END_COLUMN, ColumnDataType::TimestampMillisecond),
228 field(FRESH_UNTIL_COLUMN, ColumnDataType::TimestampMillisecond),
229 field(VALID_FROM_COLUMN, ColumnDataType::TimestampMillisecond),
233 field(VALID_UNTIL_COLUMN, ColumnDataType::TimestampMillisecond),
234 tag(SRC_TYPE_COLUMN),
236 tag(SRC_ID_COLUMN),
237 tag(REL_TYPE_COLUMN),
238 tag(DST_TYPE_COLUMN),
239 tag(DST_ID_COLUMN),
240 tag(PROVENANCE_COLUMN),
241 tag(ENTITY_SCOPE_COLUMN),
242 tag(GENERATION_ID_COLUMN),
243 field(CONFIDENCE_COLUMN, ColumnDataType::Float64),
245 field(REQUEST_COUNT_COLUMN, ColumnDataType::Int64),
246 field(ERROR_COUNT_COLUMN, ColumnDataType::Int64),
247 field(DURATION_SUM_COLUMN, ColumnDataType::Float64),
248 field(DURATION_COUNT_COLUMN, ColumnDataType::Int64),
249 json_field(EDGE_ATTRIBUTES_COLUMN),
252 ];
253
254 let table_options = [(TTL_KEY.to_string(), declared_relationships_ttl())]
255 .into_iter()
256 .collect();
257
258 CreateTableExpr {
259 catalog_name: catalog.to_string(),
260 schema_name: DEFAULT_PRIVATE_SCHEMA_NAME.to_string(),
261 table_name: SEMANTIC_RELATIONSHIPS_DECLARED_TABLE_NAME.to_string(),
262 desc: "Hand-declared edges of the entity-relationship graph".to_string(),
263 column_defs,
264 time_index: OBSERVED_AT_COLUMN.to_string(),
265 primary_keys: DECLARED_PRIMARY_KEY_COLUMNS
266 .iter()
267 .map(|c| c.to_string())
268 .collect(),
269 create_if_not_exists: true,
270 table_options,
271 table_id: None,
272 engine: common_catalog::consts::MITO_ENGINE.to_string(),
273 }
274}
275
276#[derive(Debug, Clone)]
279pub struct EntityDeclaration {
280 pub schema: String,
282 pub table: String,
284 pub time_index: String,
286 pub entity_type: String,
287 pub id_columns: Vec<String>,
289 pub descriptive_columns: Vec<String>,
291 pub scope_columns: Vec<String>,
294}
295
296#[derive(Debug, Clone)]
313pub struct GraphQueryWindow {
314 observed_start_ms: i64,
315 observed_end_ms: i64,
316 source_start_ms: i64,
317 source_end_ms: i64,
318}
319
320impl GraphQueryWindow {
321 pub fn from_observed(start_ms: i64, end_ms: i64) -> Self {
323 const BIN_MS: i64 = BIN_NANOS / 1_000_000;
324 let ceil_to_bin = |ms: i64| {
326 ms.div_euclid(BIN_MS) * BIN_MS
327 + if ms.rem_euclid(BIN_MS) == 0 {
328 0
329 } else {
330 BIN_MS
331 }
332 };
333 Self {
337 observed_start_ms: start_ms,
338 observed_end_ms: end_ms,
339 source_start_ms: ceil_to_bin(start_ms),
340 source_end_ms: ceil_to_bin(end_ms),
341 }
342 }
343
344 pub fn default_last_hour() -> Self {
348 let end_ms = common_time::util::current_time_millis();
349 Self::from_observed(end_ms - 60 * 60 * 1000, end_ms)
350 }
351
352 pub fn observed_start(&self) -> Expr {
354 ts_ms_lit(self.observed_start_ms)
355 }
356
357 pub fn observed_end(&self) -> Expr {
359 ts_ms_lit(self.observed_end_ms)
360 }
361
362 pub fn source_start(&self) -> Expr {
364 ts_ms_lit(self.source_start_ms)
365 }
366
367 pub fn source_end(&self) -> Expr {
369 ts_ms_lit(self.source_end_ms)
370 }
371}
372
373fn ts_ms_lit(ms: i64) -> Expr {
374 lit(ScalarValue::TimestampMillisecond(Some(ms), None))
375}
376
377fn interval(nanos: i64) -> Expr {
379 lit(ScalarValue::new_interval_mdn(0, 0, nanos))
380}
381
382fn bin_interval() -> Expr {
383 interval(BIN_NANOS)
384}
385
386fn bin_ms(ts: Expr) -> Expr {
390 cast(
391 datetime_fns::date_bin().call(vec![bin_interval(), ts]),
392 DataType::Timestamp(TimeUnit::Millisecond, None),
393 )
394}
395
396fn union_all(acc: Option<DataFrame>, branch: DataFrame) -> DfResult<Option<DataFrame>> {
398 Ok(Some(match acc {
399 Some(acc) => acc.union(branch)?,
400 None => branch,
401 }))
402}
403
404fn qcol(relation: &str, name: &str) -> Expr {
407 Expr::Column(Column::new(Some(relation), name))
408}
409
410fn concat_expr(parts: Vec<Expr>) -> Expr {
411 string_fns::concat().call(parts)
412}
413
414fn cast_string_or_empty(column: &str) -> Expr {
417 core_fns::coalesce().call(vec![cast(ident(column), DataType::Utf8), lit("")])
418}
419
420pub(crate) fn identifies(column: &str) -> Expr {
429 ident(column)
430 .is_not_null()
431 .and(cast(ident(column), DataType::Utf8).not_eq(lit("")))
432}
433
434fn entity_id_expr(id_columns: &[String], col: &dyn Fn(&str) -> Expr) -> Expr {
435 if let [id] = id_columns {
436 cast(col(id), DataType::Utf8)
437 } else {
438 let mut cols = id_columns.to_vec();
439 cols.sort();
440 sorted_kv_expr_with(&cols, false, col)
441 }
442}
443
444static PARSE_JSON_UDF: LazyLock<Arc<ScalarUDF>> = LazyLock::new(|| {
447 Arc::new(
448 FUNCTION_REGISTRY
449 .get_function("parse_json")
450 .expect("parse_json must be registered")
451 .provide(FunctionContext::default()),
452 )
453});
454
455fn parse_json_expr(json_text: Expr) -> Expr {
459 cast(PARSE_JSON_UDF.call(vec![json_text]), DataType::Binary)
460}
461
462fn null_json() -> Expr {
465 lit(ScalarValue::Binary(None))
466}
467
468fn json_quote(value: &str) -> String {
470 serde_json::Value::from(value).to_string()
471}
472
473static JSON_OBJECT_UDF: LazyLock<Arc<ScalarUDF>> = LazyLock::new(|| {
477 Arc::new(
478 FUNCTION_REGISTRY
479 .get_function("json_object")
480 .expect("json_object must be registered")
481 .provide(FunctionContext::default()),
482 )
483});
484
485fn json_object_expr(columns: &[String]) -> Expr {
490 let mut args = Vec::with_capacity(columns.len() * 2);
491 for column in columns {
492 args.push(lit(column.as_str()));
493 args.push(cast_string_or_empty(column));
494 }
495 cast(JSON_OBJECT_UDF.call(args), DataType::Binary)
496}
497
498fn sorted_kv_expr_with(sorted_cols: &[String], nullable: bool, col: &dyn Fn(&str) -> Expr) -> Expr {
502 let mut parts = Vec::with_capacity(sorted_cols.len() * 3);
503 for (i, column) in sorted_cols.iter().enumerate() {
504 if i > 0 {
505 parts.push(lit(","));
506 }
507 parts.push(lit(format!("{column}=")));
508 parts.push(if nullable {
509 core_fns::coalesce().call(vec![cast(col(column), DataType::Utf8), lit("")])
510 } else {
511 cast(col(column), DataType::Utf8)
512 });
513 }
514 concat_expr(parts)
515}
516
517const REGISTRY_COLUMNS: [&str; 10] = [
518 OBSERVED_AT_COLUMN,
519 WINDOW_START_COLUMN,
520 WINDOW_END_COLUMN,
521 FRESH_UNTIL_COLUMN,
522 ENTITY_TYPE_COLUMN,
523 ENTITY_ID_COLUMN,
524 ENTITY_ID_ATTRS_COLUMN,
525 ENTITY_SCOPE_COLUMN,
526 ENTITY_DESCRIPTIVE_COLUMN,
527 SOURCE_TABLES_COLUMN,
528];
529
530const REGISTRY_VALID_COLUMN: &str = "__entity_valid";
531
532fn unnest_rows(
538 df: DataFrame,
539 window_predicate: Expr,
540 valid_column: &'static str,
541 columns: &[&'static str],
542 rows: Vec<Vec<Expr>>,
543) -> DfResult<DataFrame> {
544 let mut arrays = vec![Vec::with_capacity(rows.len()); columns.len() + 1];
545 for row in rows {
546 debug_assert_eq!(row.len(), arrays.len());
547 for (array, value) in arrays.iter_mut().zip(row) {
548 array.push(value);
549 }
550 }
551 let array_names = std::iter::once(valid_column)
552 .chain(columns.iter().copied())
553 .collect::<Vec<_>>();
554 let array_projection = array_names
555 .iter()
556 .zip(arrays)
557 .map(|(name, values)| make_array(values).alias(*name))
558 .collect::<Vec<_>>();
559
560 df.filter(window_predicate)?
561 .select(array_projection)?
562 .unnest_columns(&array_names)?
563 .filter(ident(valid_column))?
564 .select(columns.iter().map(|c| ident(*c)).collect::<Vec<_>>())?
565 .distinct()
566}
567
568fn registry_source(
571 first: &EntityDeclaration,
572 rest: &[EntityDeclaration],
573 df: DataFrame,
574 window: &GraphQueryWindow,
575) -> DfResult<DataFrame> {
576 let ts = ident(&first.time_index);
577 let bin = bin_ms(ts.clone());
578 let window_predicate = ts
579 .clone()
580 .gt_eq(window.source_start())
581 .and(ts.lt(window.source_end()));
582
583 let mut rows = Vec::with_capacity(1 + rest.len());
584 for decl in std::iter::once(first).chain(rest) {
585 let entity_id = entity_id_expr(&decl.id_columns, &|c| ident(c));
590 let entity_id_attrs = if decl.id_columns.len() == 1 {
591 null_json()
592 } else {
593 json_object_expr(&decl.id_columns)
594 };
595
596 let scope = match decl.scope_columns.as_slice() {
597 [] => lit(""),
598 [single] => cast_string_or_empty(single),
600 _ => {
601 let mut cols = decl.scope_columns.clone();
602 cols.sort();
603 sorted_kv_expr_with(&cols, true, &|c| ident(c))
604 }
605 };
606
607 let descriptive = if decl.descriptive_columns.is_empty() {
608 null_json()
609 } else {
610 json_object_expr(&decl.descriptive_columns)
611 };
612
613 let source_tables = parse_json_expr(lit(format!(
614 "[{}]",
615 json_quote(&format!("{}.{}", decl.schema, decl.table))
616 )));
617
618 let valid = decl
621 .id_columns
622 .iter()
623 .fold(lit(true), |predicate, id| predicate.and(identifies(id)));
624
625 rows.push(vec![
626 valid,
627 bin.clone(),
628 bin.clone(),
629 bin.clone() + bin_interval(),
630 bin.clone() + bin_interval(),
631 lit(decl.entity_type.as_str()),
632 entity_id,
633 entity_id_attrs,
634 scope,
635 descriptive,
636 source_tables,
637 ]);
638 }
639
640 unnest_rows(
641 df,
642 window_predicate,
643 REGISTRY_VALID_COLUMN,
644 ®ISTRY_COLUMNS,
645 rows,
646 )
647}
648
649pub struct RegistrySource {
652 pub declarations: Vec<EntityDeclaration>,
653 pub scan: DataFrame,
654}
655
656pub fn build_registry_plan(
661 sources: Vec<RegistrySource>,
662 window: &GraphQueryWindow,
663) -> DfResult<Option<LogicalPlan>> {
664 let mut union_df: Option<DataFrame> = None;
665 for source in sources {
666 let Some((first, rest)) = source.declarations.split_first() else {
667 continue;
668 };
669 union_df = union_all(union_df, registry_source(first, rest, source.scan, window)?)?;
670 }
671 Ok(union_df.map(DataFrame::into_unoptimized_plan))
672}
673
674#[cfg(test)]
677pub(crate) mod test_util {
678 use datafusion::arrow::array::{Array, BinaryArray, StringArray, TimestampMillisecondArray};
679 use datafusion::arrow::record_batch::RecordBatch;
680 use datafusion::prelude::SessionContext;
681 use datafusion_expr::LogicalPlan;
682
683 pub(crate) async fn collect(ctx: &SessionContext, plan: LogicalPlan) -> Vec<RecordBatch> {
684 ctx.execute_logical_plan(plan)
685 .await
686 .unwrap()
687 .collect()
688 .await
689 .unwrap()
690 }
691
692 pub(crate) fn json_texts(batch: &RecordBatch, column: usize) -> Vec<Option<String>> {
693 let array = batch
694 .column(column)
695 .as_any()
696 .downcast_ref::<BinaryArray>()
697 .unwrap();
698 (0..array.len())
699 .map(|i| {
700 array
701 .is_valid(i)
702 .then(|| jsonb::from_slice(array.value(i)).unwrap().to_string())
703 })
704 .collect()
705 }
706
707 pub(crate) fn strings(batch: &RecordBatch, column: usize) -> Vec<String> {
708 let array = batch
709 .column(column)
710 .as_any()
711 .downcast_ref::<StringArray>()
712 .unwrap();
713 (0..array.len())
714 .map(|i| array.value(i).to_string())
715 .collect()
716 }
717
718 pub(crate) fn ts_values(batch: &RecordBatch, column: usize) -> Vec<i64> {
719 let array = batch
720 .column(column)
721 .as_any()
722 .downcast_ref::<TimestampMillisecondArray>()
723 .unwrap();
724 (0..array.len()).map(|i| array.value(i)).collect()
725 }
726}
727
728#[cfg(test)]
729mod tests {
730 use std::sync::Arc;
731
732 use datafusion::arrow::array::{ArrayRef, Int64Array, StringArray, TimestampMillisecondArray};
733 use datafusion::arrow::datatypes::{Field, Schema};
734 use datafusion::arrow::record_batch::RecordBatch;
735 use datafusion::datasource::MemTable;
736 use datafusion::prelude::SessionContext;
737
738 use super::test_util::{collect, json_texts, strings};
739 use super::*;
740
741 fn test_window() -> GraphQueryWindow {
742 GraphQueryWindow::from_observed(0, 10 * 60 * 1000)
743 }
744
745 #[test]
746 fn window_source_scan_covers_whole_buckets() {
747 let window = GraphQueryWindow::from_observed(630_000, 690_000);
750 assert_eq!(
751 (window.source_start_ms, window.source_end_ms),
752 (660_000, 720_000)
753 );
754
755 let window = GraphQueryWindow::from_observed(600_000, 720_000);
757 assert_eq!(
758 (window.source_start_ms, window.source_end_ms),
759 (600_000, 720_000)
760 );
761
762 let window = GraphQueryWindow::from_observed(610_000, 650_000);
764 assert!(window.source_start_ms >= window.source_end_ms);
765
766 let window = GraphQueryWindow::from_observed(-90_000, -30_000);
768 assert_eq!((window.source_start_ms, window.source_end_ms), (-60_000, 0));
769 }
770
771 fn metric_table_ctx() -> SessionContext {
774 let schema = Arc::new(Schema::new(vec![
775 Field::new(
776 "ts",
777 DataType::Timestamp(TimeUnit::Millisecond, None),
778 false,
779 ),
780 Field::new("service_name", DataType::Utf8, false),
781 Field::new("pid", DataType::Int64, false),
782 Field::new("host", DataType::Utf8, true),
783 ]));
784 let batch = RecordBatch::try_new(
785 schema.clone(),
786 vec![
787 Arc::new(TimestampMillisecondArray::from(vec![1_000, 2_000, 61_000])) as ArrayRef,
788 Arc::new(StringArray::from(vec!["cart", "cart", "cart"])),
789 Arc::new(Int64Array::from(vec![42, 42, 42])),
790 Arc::new(StringArray::from(vec![
791 Some("we\"ird\\\nhost"),
792 None,
793 Some("h2"),
794 ])),
795 ],
796 )
797 .unwrap();
798 let ctx = SessionContext::new();
799 ctx.register_table(
800 "app_latency",
801 Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap()),
802 )
803 .unwrap();
804 ctx
805 }
806
807 fn decl(entity_type: &str, id_columns: &[&str]) -> EntityDeclaration {
808 EntityDeclaration {
809 schema: "public".to_string(),
810 table: "app_latency".to_string(),
811 time_index: "ts".to_string(),
812 entity_type: entity_type.to_string(),
813 id_columns: id_columns.iter().map(|s| s.to_string()).collect(),
814 descriptive_columns: vec![],
815 scope_columns: vec![],
816 }
817 }
818
819 #[tokio::test]
820 async fn registry_single_column_identity() {
821 let ctx = metric_table_ctx();
822 let df = ctx.table("app_latency").await.unwrap();
823 let plan = build_registry_plan(
824 vec![RegistrySource {
825 declarations: vec![decl("service", &["service_name"])],
826 scan: df,
827 }],
828 &test_window(),
829 )
830 .unwrap()
831 .unwrap();
832
833 let names = plan
834 .schema()
835 .fields()
836 .iter()
837 .map(|f| f.name().as_str())
838 .collect::<Vec<_>>();
839 assert_eq!(
840 names,
841 [
842 "observed_at",
843 "window_start",
844 "window_end",
845 "fresh_until",
846 "entity_type",
847 "entity_id",
848 "entity_id_attrs",
849 "scope",
850 "descriptive",
851 "source_tables",
852 ]
853 );
854
855 let batches = collect(&ctx, plan).await;
856 let total: usize = batches.iter().map(|b| b.num_rows()).sum();
857 assert_eq!(total, 2);
859 let batch = &batches[0];
860 assert_eq!(strings(batch, 4), vec!["service"; batch.num_rows()]);
861 assert_eq!(strings(batch, 5), vec!["cart"; batch.num_rows()]);
862 assert!(json_texts(batch, 6).iter().all(Option::is_none));
864 assert!(json_texts(batch, 8).iter().all(Option::is_none));
865 assert_eq!(
866 json_texts(batch, 9),
867 vec![Some(r#"["public.app_latency"]"#.to_string()); batch.num_rows()]
868 );
869 }
870
871 #[tokio::test]
872 async fn registry_composite_identity_and_descriptive_escaping() {
873 let ctx = metric_table_ctx();
874 let df = ctx.table("app_latency").await.unwrap();
875 let mut declaration = decl("process", &["service_name", "pid"]);
876 declaration.descriptive_columns = vec!["host".to_string()];
877 let plan = build_registry_plan(
878 vec![RegistrySource {
879 declarations: vec![declaration],
880 scan: df,
881 }],
882 &test_window(),
883 )
884 .unwrap()
885 .unwrap();
886
887 let batches = collect(&ctx, plan).await;
888 let mut rows: Vec<(String, Option<String>, Option<String>)> = batches
889 .iter()
890 .flat_map(|batch| {
891 let ids = strings(batch, 5);
892 let id_attrs = json_texts(batch, 6);
893 let descriptives = json_texts(batch, 8);
894 ids.into_iter()
895 .zip(id_attrs)
896 .zip(descriptives)
897 .map(|((id, attrs), descriptive)| (id, attrs, descriptive))
898 .collect::<Vec<_>>()
899 })
900 .collect();
901 rows.sort();
902
903 assert_eq!(
907 rows,
908 vec![
909 (
910 "pid=42,service_name=cart".to_string(),
911 Some(r#"{"pid":"42","service_name":"cart"}"#.to_string()),
912 Some(r#"{"host":""}"#.to_string()),
913 ),
914 (
915 "pid=42,service_name=cart".to_string(),
916 Some(r#"{"pid":"42","service_name":"cart"}"#.to_string()),
917 Some(r#"{"host":"h2"}"#.to_string()),
918 ),
919 (
920 "pid=42,service_name=cart".to_string(),
921 Some(r#"{"pid":"42","service_name":"cart"}"#.to_string()),
922 Some(r#"{"host":"we\"ird\\\nhost"}"#.to_string()),
923 ),
924 ]
925 );
926 }
927
928 #[tokio::test]
929 async fn registry_scope_variants() {
930 let ctx = metric_table_ctx();
931
932 let mut single = decl("service", &["service_name"]);
934 single.scope_columns = vec!["host".to_string()];
935 let df = ctx.table("app_latency").await.unwrap();
936 let plan = build_registry_plan(
937 vec![RegistrySource {
938 declarations: vec![single],
939 scan: df,
940 }],
941 &test_window(),
942 )
943 .unwrap()
944 .unwrap();
945 let batches = collect(&ctx, plan).await;
946 let mut scopes: Vec<String> = batches.iter().flat_map(|b| strings(b, 7)).collect();
947 scopes.sort();
948 assert_eq!(scopes, vec!["", "h2", "we\"ird\\\nhost"]);
949
950 let mut multi = decl("service", &["service_name"]);
952 multi.scope_columns = vec!["pid".to_string(), "host".to_string()];
953 let df = ctx.table("app_latency").await.unwrap();
954 let plan = build_registry_plan(
955 vec![RegistrySource {
956 declarations: vec![multi],
957 scan: df,
958 }],
959 &test_window(),
960 )
961 .unwrap()
962 .unwrap();
963 let batches = collect(&ctx, plan).await;
964 let mut scopes: Vec<String> = batches.iter().flat_map(|b| strings(b, 7)).collect();
965 scopes.sort();
966 assert_eq!(
967 scopes,
968 vec![
969 "host=,pid=42",
970 "host=h2,pid=42",
971 "host=we\"ird\\\nhost,pid=42"
972 ]
973 );
974 }
975
976 #[tokio::test]
977 async fn registry_skips_absent_identity_rows() {
978 let schema = Arc::new(Schema::new(vec![
981 Field::new(
982 "ts",
983 DataType::Timestamp(TimeUnit::Millisecond, None),
984 false,
985 ),
986 Field::new("host", DataType::Utf8, true),
987 ]));
988 let batch = RecordBatch::try_new(
989 schema.clone(),
990 vec![
991 Arc::new(TimestampMillisecondArray::from(vec![1_000, 2_000, 3_000])) as ArrayRef,
992 Arc::new(StringArray::from(vec![Some("h2"), None, Some("")])),
993 ],
994 )
995 .unwrap();
996 let ctx = SessionContext::new();
997 ctx.register_table(
998 "app_latency",
999 Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap()),
1000 )
1001 .unwrap();
1002 let df = ctx.table("app_latency").await.unwrap();
1003 let plan = build_registry_plan(
1004 vec![RegistrySource {
1005 declarations: vec![decl("host", &["host"])],
1006 scan: df,
1007 }],
1008 &test_window(),
1009 )
1010 .unwrap()
1011 .unwrap();
1012 let batches = collect(&ctx, plan).await;
1013 let ids: Vec<String> = batches.iter().flat_map(|b| strings(b, 5)).collect();
1014 assert_eq!(ids, vec!["h2"]);
1015 }
1016
1017 #[tokio::test]
1018 async fn registry_expands_declarations_with_one_source_scan() {
1019 let ctx = metric_table_ctx();
1020 let df = ctx.table("app_latency").await.unwrap();
1021 let plan = build_registry_plan(
1022 vec![RegistrySource {
1023 declarations: vec![decl("service", &["service_name"]), decl("host", &["pid"])],
1024 scan: df,
1025 }],
1026 &test_window(),
1027 )
1028 .unwrap()
1029 .unwrap();
1030
1031 assert_eq!(
1032 plan.display_indent()
1033 .to_string()
1034 .matches("TableScan: app_latency")
1035 .count(),
1036 1
1037 );
1038 let batches = collect(&ctx, plan).await;
1039 let mut types: Vec<String> = batches.iter().flat_map(|b| strings(b, 4)).collect();
1040 types.sort();
1041 assert_eq!(types, vec!["host", "host", "service", "service"]);
1042
1043 assert!(
1045 build_registry_plan(vec![], &test_window())
1046 .unwrap()
1047 .is_none()
1048 );
1049 }
1050
1051 fn declared_table_info(
1054 mutate_columns: impl FnOnce(&mut Vec<datatypes::schema::ColumnSchema>),
1055 engine: &str,
1056 primary_key_indices: Vec<usize>,
1057 extra_options: &[(&str, &str)],
1058 ) -> table::metadata::TableInfo {
1059 let canonical = build_declared_relationships_expr("greptime");
1060 let mut columns: Vec<datatypes::schema::ColumnSchema> = canonical
1061 .column_defs
1062 .iter()
1063 .map(|def| {
1064 let wrapper = api::helper::ColumnDataTypeWrapper::try_new(
1065 def.data_type,
1066 def.datatype_extension.clone(),
1067 )
1068 .unwrap();
1069 datatypes::schema::ColumnSchema::new(
1070 &def.name,
1071 datatypes::prelude::ConcreteDataType::from(wrapper),
1072 def.is_nullable,
1073 )
1074 .with_time_index(def.name == OBSERVED_AT_COLUMN)
1075 })
1076 .collect();
1077 mutate_columns(&mut columns);
1078 let schema = Arc::new(
1079 datatypes::schema::SchemaBuilder::try_from_columns(columns)
1080 .unwrap()
1081 .build()
1082 .unwrap(),
1083 );
1084 let options = table::requests::TableOptions {
1085 extra_options: extra_options
1086 .iter()
1087 .map(|(k, v)| (k.to_string(), v.to_string()))
1088 .collect(),
1089 ..Default::default()
1090 };
1091 let meta = table::metadata::TableMeta {
1092 schema,
1093 primary_key_indices,
1094 value_indices: vec![],
1095 engine: engine.to_string(),
1096 next_column_id: 1,
1097 options,
1098 created_on: Default::default(),
1099 updated_on: Default::default(),
1100 partition_key_indices: vec![],
1101 column_ids: vec![],
1102 };
1103 table::metadata::TableInfoBuilder::default()
1104 .table_id(1)
1105 .name(SEMANTIC_RELATIONSHIPS_DECLARED_TABLE_NAME)
1106 .catalog_name("greptime")
1107 .schema_name(DEFAULT_PRIVATE_SCHEMA_NAME)
1108 .table_version(0)
1109 .table_type(table::metadata::TableType::Base)
1110 .meta(meta)
1111 .build()
1112 .unwrap()
1113 }
1114
1115 #[test]
1116 fn declared_schema_match_checks_more_than_columns() {
1117 let mito = common_catalog::consts::MITO_ENGINE;
1118 let canonical_pk = || (6..14).collect::<Vec<_>>();
1119
1120 let ok = declared_table_info(|_| {}, mito, canonical_pk(), &[(TTL_KEY, "180d")]);
1121 assert!(declared_relationships_schema_matches(&ok));
1122 assert_eq!(
1125 ok.meta
1126 .schema
1127 .column_schema_by_name("attributes")
1128 .map(|c| c.data_type.clone()),
1129 Some(datatypes::prelude::ConcreteDataType::json_datatype())
1130 );
1131 let ok = declared_table_info(
1132 |_| {},
1133 mito,
1134 canonical_pk(),
1135 &[(MERGE_MODE_KEY, "last_row")],
1136 );
1137 assert!(declared_relationships_schema_matches(&ok));
1138
1139 let wrong_engine = declared_table_info(|_| {}, "metric", canonical_pk(), &[]);
1140 assert!(!declared_relationships_schema_matches(&wrong_engine));
1141
1142 let pk_missing_generation_id = declared_table_info(|_| {}, mito, (6..13).collect(), &[]);
1143 assert!(!declared_relationships_schema_matches(
1144 &pk_missing_generation_id
1145 ));
1146
1147 let wrong_time_index = declared_table_info(
1148 |columns| {
1149 columns[0] = columns[0].clone().with_time_index(false);
1150 columns[4] = datatypes::schema::ColumnSchema::new(
1151 "valid_from",
1152 datatypes::prelude::ConcreteDataType::timestamp_millisecond_datatype(),
1153 false,
1154 )
1155 .with_time_index(true);
1156 },
1157 mito,
1158 canonical_pk(),
1159 &[],
1160 );
1161 assert!(!declared_relationships_schema_matches(&wrong_time_index));
1162
1163 let append_mode =
1164 declared_table_info(|_| {}, mito, canonical_pk(), &[(APPEND_MODE_KEY, "true")]);
1165 assert!(!declared_relationships_schema_matches(&append_mode));
1166
1167 let wrong_column_type = declared_table_info(
1168 |columns| {
1169 columns[14] = datatypes::schema::ColumnSchema::new(
1170 "confidence",
1171 datatypes::prelude::ConcreteDataType::int64_datatype(),
1172 true,
1173 );
1174 },
1175 mito,
1176 canonical_pk(),
1177 &[],
1178 );
1179 assert!(!declared_relationships_schema_matches(&wrong_column_type));
1180 }
1181}