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_GRAPH_WINDOW_NANOS, SEMANTIC_RELATIONSHIPS_DECLARED_TABLE_NAME, SOURCE_TABLES_COLUMN,
45 SRC_ID_COLUMN, SRC_TYPE_COLUMN, VALID_FROM_COLUMN, VALID_UNTIL_COLUMN, WINDOW_END_COLUMN,
46 WINDOW_START_COLUMN,
47};
48use common_function::function::FunctionContext;
49use common_function::function_registry::FUNCTION_REGISTRY;
50pub use conventions::{
51 Conventions, ENTITY_TYPE_GEN_AI_AGENT, ENTITY_TYPE_GEN_AI_MODEL, ENTITY_TYPE_GEN_AI_TOOL,
52 ENTITY_TYPE_HOST, ENTITY_TYPE_K8S_CONTAINER, ENTITY_TYPE_K8S_NODE, ENTITY_TYPE_K8S_POD,
53 ENTITY_TYPE_K8S_WORKLOAD, ENTITY_TYPE_PROCESS, ENTITY_TYPE_SERVICE,
54 ENTITY_TYPE_SERVICE_INSTANCE, ImplicitEntity, PROVENANCE_AGENT, PROVENANCE_ATTRIBUTE,
55 PROVENANCE_DECLARED, PROVENANCE_TRACE, REL_TYPE_CALLS, REL_TYPE_CONTAINS, REL_TYPE_DEPENDS_ON,
56 REL_TYPE_INVOKES, REL_TYPE_OWNS, REL_TYPE_PART_OF, REL_TYPE_RUNS_ON, REL_TYPE_USES,
57 conventions,
58};
59use datafusion::arrow::datatypes::{DataType, TimeUnit};
60use datafusion::dataframe::DataFrame;
61use datafusion::functions::{core as core_fns, datetime as datetime_fns, string as string_fns};
62use datafusion::functions_nested::expr_fn::make_array;
63use datafusion_common::{Column, Result as DfResult, ScalarValue};
64use datafusion_expr::{Case, Expr, LogicalPlan, ScalarUDF, cast, ident, lit, not};
65pub use relationships::{
66 CallsSource, CoDeclaredSource, DeclaredSource, RelationshipSources, build_relationships_plan,
67};
68use store_api::mito_engine_options::{APPEND_MODE_KEY, MERGE_MODE_KEY, TTL_KEY};
69
70pub fn declared_relationships_schema_matches(table_info: &table::metadata::TableInfo) -> bool {
76 let meta = &table_info.meta;
77 if meta.engine != common_catalog::consts::MITO_ENGINE {
78 return false;
79 }
80 let schema = &meta.schema;
81 if schema
82 .timestamp_column()
83 .is_none_or(|column| column.name != OBSERVED_AT_COLUMN)
84 {
85 return false;
86 }
87 let primary_keys = meta
88 .primary_key_indices
89 .iter()
90 .filter_map(|idx| schema.column_schemas().get(*idx))
91 .map(|column| column.name.as_str());
92 if !primary_keys.eq(DECLARED_PRIMARY_KEY_COLUMNS) {
93 return false;
94 }
95 let options = &meta.options.extra_options;
98 if options.get(APPEND_MODE_KEY).map(String::as_str) == Some("true")
99 || options
100 .get(MERGE_MODE_KEY)
101 .is_some_and(|mode| mode != "last_row")
102 {
103 return false;
104 }
105
106 let canonical = build_declared_relationships_expr(DEFAULT_CATALOG_NAME);
107 if schema.column_schemas().len() != canonical.column_defs.len() {
108 return false;
109 }
110 canonical
111 .column_defs
112 .iter()
113 .zip(schema.column_schemas())
114 .all(|(def, column)| {
115 let Ok(wrapper) = api::helper::ColumnDataTypeWrapper::try_new(
116 def.data_type,
117 def.datatype_extension.clone(),
118 ) else {
119 return false;
120 };
121 def.name == column.name
122 && datatypes::prelude::ConcreteDataType::from(wrapper) == column.data_type
123 })
124}
125
126const BIN_NANOS: i64 = SEMANTIC_GRAPH_WINDOW_NANOS;
130
131const DEFAULT_DECLARED_RELATIONSHIPS_TTL: &str = "90d";
133const DECLARED_RELATIONSHIPS_TTL_ENV: &str = "GREPTIMEDB_DECLARED_RELATIONSHIPS_TTL";
137
138fn declared_relationships_ttl() -> String {
139 std::env::var(DECLARED_RELATIONSHIPS_TTL_ENV)
140 .ok()
141 .map(|ttl| ttl.trim().to_string())
142 .filter(|ttl| !ttl.is_empty())
143 .unwrap_or_else(|| DEFAULT_DECLARED_RELATIONSHIPS_TTL.to_string())
144}
145
146pub const DECLARED_PRIMARY_KEY_COLUMNS: [&str; 8] = [
151 SRC_TYPE_COLUMN,
152 SRC_ID_COLUMN,
153 REL_TYPE_COLUMN,
154 DST_TYPE_COLUMN,
155 DST_ID_COLUMN,
156 PROVENANCE_COLUMN,
157 ENTITY_SCOPE_COLUMN,
158 GENERATION_ID_COLUMN,
159];
160
161const DECLARED_EDGE_IDENTITY_COLUMNS: [&str; 6] = [
166 SRC_TYPE_COLUMN,
167 SRC_ID_COLUMN,
168 REL_TYPE_COLUMN,
169 DST_TYPE_COLUMN,
170 DST_ID_COLUMN,
171 PROVENANCE_COLUMN,
172];
173
174fn column(
175 name: &str,
176 data_type: ColumnDataType,
177 semantic_type: SemanticType,
178 nullable: bool,
179) -> ColumnDef {
180 ColumnDef {
181 name: name.to_string(),
182 data_type: data_type as i32,
183 is_nullable: nullable,
184 default_constraint: vec![],
185 semantic_type: semantic_type as i32,
186 comment: String::new(),
187 datatype_extension: None,
188 options: None,
189 }
190}
191
192fn tag(name: &str) -> ColumnDef {
193 column(name, ColumnDataType::String, SemanticType::Tag, false)
194}
195
196fn field(name: &str, data_type: ColumnDataType) -> ColumnDef {
197 column(name, data_type, SemanticType::Field, true)
198}
199
200fn json_field(name: &str) -> ColumnDef {
201 let mut def = field(name, ColumnDataType::Binary);
202 def.datatype_extension = Some(ColumnDataTypeExtension {
203 type_ext: Some(TypeExt::JsonType(JsonTypeExtension::JsonBinary.into())),
204 });
205 def
206}
207
208pub fn build_declared_relationships_expr(catalog: &str) -> CreateTableExpr {
219 let column_defs = vec![
220 column(
223 OBSERVED_AT_COLUMN,
224 ColumnDataType::TimestampMillisecond,
225 SemanticType::Timestamp,
226 false,
227 ),
228 field(WINDOW_START_COLUMN, ColumnDataType::TimestampMillisecond),
229 field(WINDOW_END_COLUMN, ColumnDataType::TimestampMillisecond),
230 field(FRESH_UNTIL_COLUMN, ColumnDataType::TimestampMillisecond),
231 field(VALID_FROM_COLUMN, ColumnDataType::TimestampMillisecond),
235 field(VALID_UNTIL_COLUMN, ColumnDataType::TimestampMillisecond),
236 tag(SRC_TYPE_COLUMN),
238 tag(SRC_ID_COLUMN),
239 tag(REL_TYPE_COLUMN),
240 tag(DST_TYPE_COLUMN),
241 tag(DST_ID_COLUMN),
242 tag(PROVENANCE_COLUMN),
243 tag(ENTITY_SCOPE_COLUMN),
244 tag(GENERATION_ID_COLUMN),
245 field(CONFIDENCE_COLUMN, ColumnDataType::Float64),
247 field(REQUEST_COUNT_COLUMN, ColumnDataType::Int64),
248 field(ERROR_COUNT_COLUMN, ColumnDataType::Int64),
249 field(DURATION_SUM_COLUMN, ColumnDataType::Float64),
250 field(DURATION_COUNT_COLUMN, ColumnDataType::Int64),
251 json_field(EDGE_ATTRIBUTES_COLUMN),
254 ];
255
256 let table_options = [(TTL_KEY.to_string(), declared_relationships_ttl())]
257 .into_iter()
258 .collect();
259
260 CreateTableExpr {
261 catalog_name: catalog.to_string(),
262 schema_name: DEFAULT_PRIVATE_SCHEMA_NAME.to_string(),
263 table_name: SEMANTIC_RELATIONSHIPS_DECLARED_TABLE_NAME.to_string(),
264 desc: "Hand-declared edges of the entity-relationship graph".to_string(),
265 column_defs,
266 time_index: OBSERVED_AT_COLUMN.to_string(),
267 primary_keys: DECLARED_PRIMARY_KEY_COLUMNS
268 .iter()
269 .map(|c| c.to_string())
270 .collect(),
271 create_if_not_exists: true,
272 table_options,
273 table_id: None,
274 engine: common_catalog::consts::MITO_ENGINE.to_string(),
275 }
276}
277
278#[derive(Debug, Clone)]
281pub struct EntityDeclaration {
282 pub schema: String,
284 pub table: String,
286 pub time_index: String,
288 pub entity_type: String,
289 pub id_columns: Vec<String>,
291 pub id_qualifier: Option<String>,
295 pub superseded_by_columns: Vec<String>,
300 pub descriptive_columns: Vec<String>,
302 pub scope_columns: Vec<String>,
305}
306
307#[derive(Debug, Clone)]
324pub struct GraphQueryWindow {
325 observed_start_ms: i64,
326 observed_end_ms: i64,
327 source_start_ms: i64,
328 source_end_ms: i64,
329}
330
331impl GraphQueryWindow {
332 pub fn from_observed(start_ms: i64, end_ms: i64) -> Self {
334 const BIN_MS: i64 = BIN_NANOS / 1_000_000;
335 let ceil_to_bin = |ms: i64| {
337 ms.div_euclid(BIN_MS) * BIN_MS
338 + if ms.rem_euclid(BIN_MS) == 0 {
339 0
340 } else {
341 BIN_MS
342 }
343 };
344 Self {
348 observed_start_ms: start_ms,
349 observed_end_ms: end_ms,
350 source_start_ms: ceil_to_bin(start_ms),
351 source_end_ms: ceil_to_bin(end_ms),
352 }
353 }
354
355 pub fn default_last_hour() -> Self {
359 let end_ms = common_time::util::current_time_millis();
360 Self::from_observed(end_ms - 60 * 60 * 1000, end_ms)
361 }
362
363 pub fn observed_start(&self) -> Expr {
365 ts_ms_lit(self.observed_start_ms)
366 }
367
368 pub fn observed_end(&self) -> Expr {
370 ts_ms_lit(self.observed_end_ms)
371 }
372
373 pub fn source_start(&self) -> Expr {
375 ts_ms_lit(self.source_start_ms)
376 }
377
378 pub fn source_end(&self) -> Expr {
380 ts_ms_lit(self.source_end_ms)
381 }
382}
383
384fn ts_ms_lit(ms: i64) -> Expr {
385 lit(ScalarValue::TimestampMillisecond(Some(ms), None))
386}
387
388fn interval(nanos: i64) -> Expr {
390 lit(ScalarValue::new_interval_mdn(0, 0, nanos))
391}
392
393fn bin_interval() -> Expr {
394 interval(BIN_NANOS)
395}
396
397fn bin_ms(ts: Expr) -> Expr {
401 cast(
402 datetime_fns::date_bin().call(vec![bin_interval(), ts]),
403 DataType::Timestamp(TimeUnit::Millisecond, None),
404 )
405}
406
407fn union_all(acc: Option<DataFrame>, branch: DataFrame) -> DfResult<Option<DataFrame>> {
409 Ok(Some(match acc {
410 Some(acc) => acc.union(branch)?,
411 None => branch,
412 }))
413}
414
415fn qcol(relation: &str, name: &str) -> Expr {
418 Expr::Column(Column::new(Some(relation), name))
419}
420
421fn concat_expr(parts: Vec<Expr>) -> Expr {
422 string_fns::concat().call(parts)
423}
424
425fn cast_string_or_empty(column: &str) -> Expr {
428 core_fns::coalesce().call(vec![cast(ident(column), DataType::Utf8), lit("")])
429}
430
431fn identifies(column: &str) -> Expr {
437 ident(column)
438 .is_not_null()
439 .and(cast(ident(column), DataType::Utf8).not_eq(lit("")))
440}
441
442pub(crate) fn declaration_predicate(declaration: &EntityDeclaration) -> Expr {
446 let mut predicate = lit(true);
447 for column in &declaration.id_columns {
448 predicate = predicate.and(identifies(column));
449 }
450 if let Some(superseding) = declaration
451 .superseded_by_columns
452 .iter()
453 .map(|column| identifies(column))
454 .reduce(Expr::and)
455 {
456 predicate = predicate.and(not(superseding));
457 }
458 predicate
459}
460
461const ID_SEPARATOR: &str = ",";
462const ID_ESCAPE: &str = "\\";
463const ID_QUALIFIER_SEPARATOR: &str = "/";
465
466fn escaped_id_value(value: Expr) -> Expr {
470 let escape_escape = string_fns::replace().call(vec![
471 value,
472 lit(ID_ESCAPE),
473 lit(format!("{ID_ESCAPE}{ID_ESCAPE}")),
474 ]);
475 string_fns::replace().call(vec![
476 escape_escape,
477 lit(ID_SEPARATOR),
478 lit(format!("{ID_ESCAPE}{ID_SEPARATOR}")),
479 ])
480}
481
482fn entity_id_expr(
494 id_columns: &[String],
495 qualifier: Option<&str>,
496 col: &dyn Fn(&str) -> Expr,
497) -> Expr {
498 let mut parts = Vec::with_capacity(id_columns.len() * 2);
499 for (index, column) in id_columns.iter().enumerate() {
500 if index > 0 {
501 parts.push(lit(ID_SEPARATOR));
502 }
503 let value = escaped_id_value(cast(col(column), DataType::Utf8));
504 parts.push(match qualifier {
505 Some(qualifier) if index == 0 => qualified_id_expr(qualifier, value, col),
506 _ => value,
507 });
508 }
509 if let [single] = parts.as_slice() {
510 single.clone()
511 } else {
512 concat_expr(parts)
513 }
514}
515
516fn qualified_id_expr(qualifier: &str, value: Expr, col: &dyn Fn(&str) -> Expr) -> Expr {
520 let qualifier = escaped_id_value(
521 core_fns::coalesce().call(vec![cast(col(qualifier), DataType::Utf8), lit("")]),
522 );
523 Expr::Case(Case::new(
524 None,
525 vec![(
526 Box::new(qualifier.clone().eq(lit(""))),
527 Box::new(value.clone()),
528 )],
529 Some(Box::new(concat_expr(vec![
530 qualifier,
531 lit(ID_QUALIFIER_SEPARATOR),
532 value,
533 ]))),
534 ))
535}
536
537static PARSE_JSON_UDF: LazyLock<Arc<ScalarUDF>> = LazyLock::new(|| {
540 Arc::new(
541 FUNCTION_REGISTRY
542 .get_function("parse_json")
543 .expect("parse_json must be registered")
544 .provide(FunctionContext::default()),
545 )
546});
547
548fn parse_json_expr(json_text: Expr) -> Expr {
552 cast(PARSE_JSON_UDF.call(vec![json_text]), DataType::Binary)
553}
554
555fn null_json() -> Expr {
558 lit(ScalarValue::Binary(None))
559}
560
561fn json_quote(value: &str) -> String {
563 serde_json::Value::from(value).to_string()
564}
565
566static JSON_OBJECT_UDF: LazyLock<Arc<ScalarUDF>> = LazyLock::new(|| {
570 Arc::new(
571 FUNCTION_REGISTRY
572 .get_function("json_object")
573 .expect("json_object must be registered")
574 .provide(FunctionContext::default()),
575 )
576});
577
578fn json_object_expr(columns: &[String]) -> Expr {
583 let mut args = Vec::with_capacity(columns.len() * 2);
584 for column in columns {
585 args.push(lit(column.as_str()));
586 args.push(cast_string_or_empty(column));
587 }
588 cast(JSON_OBJECT_UDF.call(args), DataType::Binary)
589}
590
591fn sorted_kv_expr_with(sorted_cols: &[String], nullable: bool, col: &dyn Fn(&str) -> Expr) -> Expr {
595 let mut parts = Vec::with_capacity(sorted_cols.len() * 3);
596 for (i, column) in sorted_cols.iter().enumerate() {
597 if i > 0 {
598 parts.push(lit(","));
599 }
600 parts.push(lit(format!("{column}=")));
601 parts.push(if nullable {
602 core_fns::coalesce().call(vec![cast(col(column), DataType::Utf8), lit("")])
603 } else {
604 cast(col(column), DataType::Utf8)
605 });
606 }
607 concat_expr(parts)
608}
609
610const REGISTRY_COLUMNS: [&str; 10] = [
611 OBSERVED_AT_COLUMN,
612 WINDOW_START_COLUMN,
613 WINDOW_END_COLUMN,
614 FRESH_UNTIL_COLUMN,
615 ENTITY_TYPE_COLUMN,
616 ENTITY_ID_COLUMN,
617 ENTITY_ID_ATTRS_COLUMN,
618 ENTITY_SCOPE_COLUMN,
619 ENTITY_DESCRIPTIVE_COLUMN,
620 SOURCE_TABLES_COLUMN,
621];
622
623const REGISTRY_VALID_COLUMN: &str = "__entity_valid";
624
625fn unnest_rows(
631 df: DataFrame,
632 window_predicate: Expr,
633 valid_column: &'static str,
634 columns: &[&'static str],
635 rows: Vec<Vec<Expr>>,
636) -> DfResult<DataFrame> {
637 let mut arrays = vec![Vec::with_capacity(rows.len()); columns.len() + 1];
638 for row in rows {
639 debug_assert_eq!(row.len(), arrays.len());
640 for (array, value) in arrays.iter_mut().zip(row) {
641 array.push(value);
642 }
643 }
644 let array_names = std::iter::once(valid_column)
645 .chain(columns.iter().copied())
646 .collect::<Vec<_>>();
647 let array_projection = array_names
648 .iter()
649 .zip(arrays)
650 .map(|(name, values)| make_array(values).alias(*name))
651 .collect::<Vec<_>>();
652
653 df.filter(window_predicate)?
654 .select(array_projection)?
655 .unnest_columns(&array_names)?
656 .filter(ident(valid_column))?
657 .select(columns.iter().map(|c| ident(*c)).collect::<Vec<_>>())?
658 .distinct()
659}
660
661fn registry_source(
664 first: &EntityDeclaration,
665 rest: &[EntityDeclaration],
666 df: DataFrame,
667 window: &GraphQueryWindow,
668) -> DfResult<DataFrame> {
669 let ts = ident(&first.time_index);
670 let bin = bin_ms(ts.clone());
671 let window_predicate = ts
672 .clone()
673 .gt_eq(window.source_start())
674 .and(ts.lt(window.source_end()));
675
676 let mut rows = Vec::with_capacity(1 + rest.len());
677 for decl in std::iter::once(first).chain(rest) {
678 let entity_id = entity_id_expr(&decl.id_columns, decl.id_qualifier.as_deref(), &|c| {
681 ident(c)
682 });
683 let id_parts = decl
684 .id_qualifier
685 .iter()
686 .chain(&decl.id_columns)
687 .cloned()
688 .collect::<Vec<_>>();
689 let entity_id_attrs = json_object_expr(&id_parts);
692
693 let scope = match decl.scope_columns.as_slice() {
694 [] => lit(""),
695 [single] => cast_string_or_empty(single),
697 _ => {
698 let mut cols = decl.scope_columns.clone();
699 cols.sort();
700 sorted_kv_expr_with(&cols, true, &|c| ident(c))
701 }
702 };
703
704 let descriptive = if decl.descriptive_columns.is_empty() {
705 null_json()
706 } else {
707 json_object_expr(&decl.descriptive_columns)
708 };
709
710 let source_tables = parse_json_expr(lit(format!(
711 "[{}]",
712 json_quote(&format!("{}.{}", decl.schema, decl.table))
713 )));
714
715 let valid = declaration_predicate(decl);
718
719 rows.push(vec![
720 valid,
721 bin.clone(),
722 bin.clone(),
723 bin.clone() + bin_interval(),
724 bin.clone() + bin_interval(),
725 lit(decl.entity_type.as_str()),
726 entity_id,
727 entity_id_attrs,
728 scope,
729 descriptive,
730 source_tables,
731 ]);
732 }
733
734 unnest_rows(
735 df,
736 window_predicate,
737 REGISTRY_VALID_COLUMN,
738 ®ISTRY_COLUMNS,
739 rows,
740 )
741}
742
743pub struct RegistrySource {
746 pub declarations: Vec<EntityDeclaration>,
747 pub scan: DataFrame,
748}
749
750pub fn build_registry_plan(
755 sources: Vec<RegistrySource>,
756 window: &GraphQueryWindow,
757) -> DfResult<Option<LogicalPlan>> {
758 let mut union_df: Option<DataFrame> = None;
759 for source in sources {
760 let Some((first, rest)) = source.declarations.split_first() else {
761 continue;
762 };
763 union_df = union_all(union_df, registry_source(first, rest, source.scan, window)?)?;
764 }
765 Ok(union_df.map(DataFrame::into_unoptimized_plan))
766}
767
768#[cfg(test)]
771pub(crate) mod test_util {
772 use datafusion::arrow::array::{Array, BinaryArray, StringArray, TimestampMillisecondArray};
773 use datafusion::arrow::record_batch::RecordBatch;
774 use datafusion::prelude::SessionContext;
775 use datafusion_expr::LogicalPlan;
776
777 pub(crate) async fn collect(ctx: &SessionContext, plan: LogicalPlan) -> Vec<RecordBatch> {
778 ctx.execute_logical_plan(plan)
779 .await
780 .unwrap()
781 .collect()
782 .await
783 .unwrap()
784 }
785
786 pub(crate) fn json_texts(batch: &RecordBatch, column: usize) -> Vec<Option<String>> {
787 let array = batch
788 .column(column)
789 .as_any()
790 .downcast_ref::<BinaryArray>()
791 .unwrap();
792 (0..array.len())
793 .map(|i| {
794 array
795 .is_valid(i)
796 .then(|| jsonb::from_slice(array.value(i)).unwrap().to_string())
797 })
798 .collect()
799 }
800
801 pub(crate) fn strings(batch: &RecordBatch, column: usize) -> Vec<String> {
802 let array = batch
803 .column(column)
804 .as_any()
805 .downcast_ref::<StringArray>()
806 .unwrap();
807 (0..array.len())
808 .map(|i| array.value(i).to_string())
809 .collect()
810 }
811
812 pub(crate) fn ts_values(batch: &RecordBatch, column: usize) -> Vec<i64> {
813 let array = batch
814 .column(column)
815 .as_any()
816 .downcast_ref::<TimestampMillisecondArray>()
817 .unwrap();
818 (0..array.len()).map(|i| array.value(i)).collect()
819 }
820}
821
822#[cfg(test)]
823mod tests {
824 use std::sync::Arc;
825
826 use datafusion::arrow::array::{ArrayRef, Int64Array, StringArray, TimestampMillisecondArray};
827 use datafusion::arrow::datatypes::{Field, Schema};
828 use datafusion::arrow::record_batch::RecordBatch;
829 use datafusion::datasource::MemTable;
830 use datafusion::prelude::SessionContext;
831
832 use super::test_util::{collect, json_texts, strings};
833 use super::*;
834
835 fn test_window() -> GraphQueryWindow {
836 GraphQueryWindow::from_observed(0, 10 * 60 * 1000)
837 }
838
839 #[test]
840 fn window_source_scan_covers_whole_buckets() {
841 let window = GraphQueryWindow::from_observed(630_000, 690_000);
844 assert_eq!(
845 (window.source_start_ms, window.source_end_ms),
846 (660_000, 720_000)
847 );
848
849 let window = GraphQueryWindow::from_observed(600_000, 720_000);
851 assert_eq!(
852 (window.source_start_ms, window.source_end_ms),
853 (600_000, 720_000)
854 );
855
856 let window = GraphQueryWindow::from_observed(610_000, 650_000);
858 assert!(window.source_start_ms >= window.source_end_ms);
859
860 let window = GraphQueryWindow::from_observed(-90_000, -30_000);
862 assert_eq!((window.source_start_ms, window.source_end_ms), (-60_000, 0));
863 }
864
865 fn metric_table_ctx() -> SessionContext {
868 let schema = Arc::new(Schema::new(vec![
869 Field::new(
870 "ts",
871 DataType::Timestamp(TimeUnit::Millisecond, None),
872 false,
873 ),
874 Field::new("service_name", DataType::Utf8, false),
875 Field::new("pid", DataType::Int64, false),
876 Field::new("host", DataType::Utf8, true),
877 ]));
878 let batch = RecordBatch::try_new(
879 schema.clone(),
880 vec![
881 Arc::new(TimestampMillisecondArray::from(vec![1_000, 2_000, 61_000])) as ArrayRef,
882 Arc::new(StringArray::from(vec!["cart", "cart", "cart"])),
883 Arc::new(Int64Array::from(vec![42, 42, 42])),
884 Arc::new(StringArray::from(vec![
885 Some("we\"ird\\\nhost"),
886 None,
887 Some("h2"),
888 ])),
889 ],
890 )
891 .unwrap();
892 let ctx = SessionContext::new();
893 ctx.register_table(
894 "app_latency",
895 Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap()),
896 )
897 .unwrap();
898 ctx
899 }
900
901 fn decl(entity_type: &str, id_columns: &[&str]) -> EntityDeclaration {
902 EntityDeclaration {
903 schema: "public".to_string(),
904 table: "app_latency".to_string(),
905 time_index: "ts".to_string(),
906 entity_type: entity_type.to_string(),
907 id_columns: id_columns.iter().map(|s| s.to_string()).collect(),
908 id_qualifier: None,
909 superseded_by_columns: vec![],
910 descriptive_columns: vec![],
911 scope_columns: vec![],
912 }
913 }
914
915 #[tokio::test]
918 async fn registry_id_escaping_and_qualifier() {
919 let schema = Arc::new(Schema::new(vec![
920 Field::new(
921 "ts",
922 DataType::Timestamp(TimeUnit::Millisecond, None),
923 false,
924 ),
925 Field::new("service_name", DataType::Utf8, false),
926 Field::new("instance", DataType::Utf8, false),
927 Field::new("namespace", DataType::Utf8, true),
928 ]));
929 let batch = RecordBatch::try_new(
930 schema.clone(),
931 vec![
932 Arc::new(TimestampMillisecondArray::from(vec![1_000, 2_000, 3_000])) as ArrayRef,
933 Arc::new(StringArray::from(vec!["cart", "a,b", "we\\ird"])),
934 Arc::new(StringArray::from(vec!["i-1", "c", "i-3"])),
935 Arc::new(StringArray::from(vec![Some("shop"), None, Some("")])),
936 ],
937 )
938 .unwrap();
939 let ctx = SessionContext::new();
940 ctx.register_table(
941 "svc",
942 Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap()),
943 )
944 .unwrap();
945
946 let mut declaration = decl("service.instance", &["service_name", "instance"]);
947 declaration.id_qualifier = Some("namespace".to_string());
948 declaration.time_index = "ts".to_string();
949 let plan = build_registry_plan(
950 vec![RegistrySource {
951 declarations: vec![declaration],
952 scan: ctx.table("svc").await.unwrap(),
953 }],
954 &test_window(),
955 )
956 .unwrap()
957 .unwrap();
958
959 let mut ids: Vec<String> = collect(&ctx, plan)
960 .await
961 .iter()
962 .flat_map(|batch| strings(batch, 5))
963 .collect();
964 ids.sort();
965 assert_eq!(
966 ids,
967 vec![
968 "a\\,b,c".to_string(),
971 "shop/cart,i-1".to_string(),
972 "we\\\\ird,i-3".to_string(),
973 ]
974 );
975 }
976
977 #[tokio::test]
978 async fn registry_single_column_identity() {
979 let ctx = metric_table_ctx();
980 let df = ctx.table("app_latency").await.unwrap();
981 let plan = build_registry_plan(
982 vec![RegistrySource {
983 declarations: vec![decl("service", &["service_name"])],
984 scan: df,
985 }],
986 &test_window(),
987 )
988 .unwrap()
989 .unwrap();
990
991 let names = plan
992 .schema()
993 .fields()
994 .iter()
995 .map(|f| f.name().as_str())
996 .collect::<Vec<_>>();
997 assert_eq!(
998 names,
999 [
1000 "observed_at",
1001 "window_start",
1002 "window_end",
1003 "fresh_until",
1004 "entity_type",
1005 "entity_id",
1006 "entity_id_attrs",
1007 "scope",
1008 "descriptive",
1009 "source_tables",
1010 ]
1011 );
1012
1013 let batches = collect(&ctx, plan).await;
1014 let total: usize = batches.iter().map(|b| b.num_rows()).sum();
1015 assert_eq!(total, 2);
1017 let batch = &batches[0];
1018 assert_eq!(strings(batch, 4), vec!["service"; batch.num_rows()]);
1019 assert_eq!(strings(batch, 5), vec!["cart"; batch.num_rows()]);
1020 assert_eq!(
1022 json_texts(batch, 6),
1023 vec![Some(r#"{"service_name":"cart"}"#.to_string()); batch.num_rows()]
1024 );
1025 assert!(json_texts(batch, 8).iter().all(Option::is_none));
1026 assert_eq!(
1027 json_texts(batch, 9),
1028 vec![Some(r#"["public.app_latency"]"#.to_string()); batch.num_rows()]
1029 );
1030 }
1031
1032 #[tokio::test]
1033 async fn registry_composite_identity_and_descriptive_escaping() {
1034 let ctx = metric_table_ctx();
1035 let df = ctx.table("app_latency").await.unwrap();
1036 let mut declaration = decl("process", &["service_name", "pid"]);
1037 declaration.descriptive_columns = vec!["host".to_string()];
1038 let plan = build_registry_plan(
1039 vec![RegistrySource {
1040 declarations: vec![declaration],
1041 scan: df,
1042 }],
1043 &test_window(),
1044 )
1045 .unwrap()
1046 .unwrap();
1047
1048 let batches = collect(&ctx, plan).await;
1049 let mut rows: Vec<(String, Option<String>, Option<String>)> = batches
1050 .iter()
1051 .flat_map(|batch| {
1052 let ids = strings(batch, 5);
1053 let id_attrs = json_texts(batch, 6);
1054 let descriptives = json_texts(batch, 8);
1055 ids.into_iter()
1056 .zip(id_attrs)
1057 .zip(descriptives)
1058 .map(|((id, attrs), descriptive)| (id, attrs, descriptive))
1059 .collect::<Vec<_>>()
1060 })
1061 .collect();
1062 rows.sort();
1063
1064 assert_eq!(
1068 rows,
1069 vec![
1070 (
1071 "cart,42".to_string(),
1072 Some(r#"{"pid":"42","service_name":"cart"}"#.to_string()),
1073 Some(r#"{"host":""}"#.to_string()),
1074 ),
1075 (
1076 "cart,42".to_string(),
1077 Some(r#"{"pid":"42","service_name":"cart"}"#.to_string()),
1078 Some(r#"{"host":"h2"}"#.to_string()),
1079 ),
1080 (
1081 "cart,42".to_string(),
1082 Some(r#"{"pid":"42","service_name":"cart"}"#.to_string()),
1083 Some(r#"{"host":"we\"ird\\\nhost"}"#.to_string()),
1084 ),
1085 ]
1086 );
1087 }
1088
1089 #[tokio::test]
1090 async fn registry_scope_variants() {
1091 let ctx = metric_table_ctx();
1092
1093 let mut single = decl("service", &["service_name"]);
1095 single.scope_columns = vec!["host".to_string()];
1096 let df = ctx.table("app_latency").await.unwrap();
1097 let plan = build_registry_plan(
1098 vec![RegistrySource {
1099 declarations: vec![single],
1100 scan: df,
1101 }],
1102 &test_window(),
1103 )
1104 .unwrap()
1105 .unwrap();
1106 let batches = collect(&ctx, plan).await;
1107 let mut scopes: Vec<String> = batches.iter().flat_map(|b| strings(b, 7)).collect();
1108 scopes.sort();
1109 assert_eq!(scopes, vec!["", "h2", "we\"ird\\\nhost"]);
1110
1111 let mut multi = decl("service", &["service_name"]);
1113 multi.scope_columns = vec!["pid".to_string(), "host".to_string()];
1114 let df = ctx.table("app_latency").await.unwrap();
1115 let plan = build_registry_plan(
1116 vec![RegistrySource {
1117 declarations: vec![multi],
1118 scan: df,
1119 }],
1120 &test_window(),
1121 )
1122 .unwrap()
1123 .unwrap();
1124 let batches = collect(&ctx, plan).await;
1125 let mut scopes: Vec<String> = batches.iter().flat_map(|b| strings(b, 7)).collect();
1126 scopes.sort();
1127 assert_eq!(
1128 scopes,
1129 vec![
1130 "host=,pid=42",
1131 "host=h2,pid=42",
1132 "host=we\"ird\\\nhost,pid=42"
1133 ]
1134 );
1135 }
1136
1137 #[tokio::test]
1138 async fn registry_skips_absent_identity_rows() {
1139 let schema = Arc::new(Schema::new(vec![
1142 Field::new(
1143 "ts",
1144 DataType::Timestamp(TimeUnit::Millisecond, None),
1145 false,
1146 ),
1147 Field::new("host", DataType::Utf8, true),
1148 ]));
1149 let batch = RecordBatch::try_new(
1150 schema.clone(),
1151 vec![
1152 Arc::new(TimestampMillisecondArray::from(vec![1_000, 2_000, 3_000])) as ArrayRef,
1153 Arc::new(StringArray::from(vec![Some("h2"), None, Some("")])),
1154 ],
1155 )
1156 .unwrap();
1157 let ctx = SessionContext::new();
1158 ctx.register_table(
1159 "app_latency",
1160 Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap()),
1161 )
1162 .unwrap();
1163 let df = ctx.table("app_latency").await.unwrap();
1164 let plan = build_registry_plan(
1165 vec![RegistrySource {
1166 declarations: vec![decl("host", &["host"])],
1167 scan: df,
1168 }],
1169 &test_window(),
1170 )
1171 .unwrap()
1172 .unwrap();
1173 let batches = collect(&ctx, plan).await;
1174 let ids: Vec<String> = batches.iter().flat_map(|b| strings(b, 5)).collect();
1175 assert_eq!(ids, vec!["h2"]);
1176 }
1177
1178 #[tokio::test]
1179 async fn registry_superseding_is_per_row_and_never_drops_an_entity() {
1180 let schema = Arc::new(Schema::new(vec![
1184 Field::new(
1185 "ts",
1186 DataType::Timestamp(TimeUnit::Millisecond, None),
1187 false,
1188 ),
1189 Field::new("container_id", DataType::Utf8, false),
1190 Field::new("pod_uid", DataType::Utf8, true),
1191 Field::new("container_name", DataType::Utf8, true),
1192 ]));
1193 let batch = RecordBatch::try_new(
1194 schema.clone(),
1195 vec![
1196 Arc::new(TimestampMillisecondArray::from(vec![
1197 1_000, 2_000, 3_000, 4_000,
1198 ])) as ArrayRef,
1199 Arc::new(StringArray::from(vec![
1200 "c-pod",
1201 "c-docker",
1202 "c-empty",
1203 "c-partial",
1204 ])),
1205 Arc::new(StringArray::from(vec![
1206 Some("uid-1"),
1207 None,
1208 Some(""),
1209 Some("uid-2"),
1210 ])),
1211 Arc::new(StringArray::from(vec![
1212 Some("api"),
1213 Some("api"),
1214 Some("api"),
1215 None,
1216 ])),
1217 ],
1218 )
1219 .unwrap();
1220 let ctx = SessionContext::new();
1221 ctx.register_table(
1222 "descriptors",
1223 Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap()),
1224 )
1225 .unwrap();
1226
1227 let mut generic = decl("container", &["container_id"]);
1228 generic.table = "descriptors".to_string();
1229 generic.superseded_by_columns = vec!["pod_uid".to_string(), "container_name".to_string()];
1230 let mut specific = decl("k8s.container", &["pod_uid", "container_name"]);
1231 specific.table = "descriptors".to_string();
1232 let plan = build_registry_plan(
1233 vec![RegistrySource {
1234 declarations: vec![generic, specific],
1235 scan: ctx.table("descriptors").await.unwrap(),
1236 }],
1237 &test_window(),
1238 )
1239 .unwrap()
1240 .unwrap();
1241
1242 let mut rows: Vec<(String, String)> = collect(&ctx, plan)
1243 .await
1244 .iter()
1245 .flat_map(|b| {
1246 strings(b, 4)
1247 .into_iter()
1248 .zip(strings(b, 5))
1249 .collect::<Vec<_>>()
1250 })
1251 .collect();
1252 rows.sort();
1253 assert_eq!(
1254 rows,
1255 vec![
1256 ("container".to_string(), "c-docker".to_string()),
1258 ("container".to_string(), "c-empty".to_string()),
1259 ("container".to_string(), "c-partial".to_string()),
1262 ("k8s.container".to_string(), "uid-1,api".to_string()),
1263 ]
1264 );
1265 }
1266
1267 #[tokio::test]
1268 async fn registry_expands_declarations_with_one_source_scan() {
1269 let ctx = metric_table_ctx();
1270 let df = ctx.table("app_latency").await.unwrap();
1271 let plan = build_registry_plan(
1272 vec![RegistrySource {
1273 declarations: vec![decl("service", &["service_name"]), decl("host", &["pid"])],
1274 scan: df,
1275 }],
1276 &test_window(),
1277 )
1278 .unwrap()
1279 .unwrap();
1280
1281 assert_eq!(
1282 plan.display_indent()
1283 .to_string()
1284 .matches("TableScan: app_latency")
1285 .count(),
1286 1
1287 );
1288 let batches = collect(&ctx, plan).await;
1289 let mut types: Vec<String> = batches.iter().flat_map(|b| strings(b, 4)).collect();
1290 types.sort();
1291 assert_eq!(types, vec!["host", "host", "service", "service"]);
1292
1293 assert!(
1295 build_registry_plan(vec![], &test_window())
1296 .unwrap()
1297 .is_none()
1298 );
1299 }
1300
1301 fn declared_table_info(
1304 mutate_columns: impl FnOnce(&mut Vec<datatypes::schema::ColumnSchema>),
1305 engine: &str,
1306 primary_key_indices: Vec<usize>,
1307 extra_options: &[(&str, &str)],
1308 ) -> table::metadata::TableInfo {
1309 let canonical = build_declared_relationships_expr("greptime");
1310 let mut columns: Vec<datatypes::schema::ColumnSchema> = canonical
1311 .column_defs
1312 .iter()
1313 .map(|def| {
1314 let wrapper = api::helper::ColumnDataTypeWrapper::try_new(
1315 def.data_type,
1316 def.datatype_extension.clone(),
1317 )
1318 .unwrap();
1319 datatypes::schema::ColumnSchema::new(
1320 &def.name,
1321 datatypes::prelude::ConcreteDataType::from(wrapper),
1322 def.is_nullable,
1323 )
1324 .with_time_index(def.name == OBSERVED_AT_COLUMN)
1325 })
1326 .collect();
1327 mutate_columns(&mut columns);
1328 let schema = Arc::new(
1329 datatypes::schema::SchemaBuilder::try_from_columns(columns)
1330 .unwrap()
1331 .build()
1332 .unwrap(),
1333 );
1334 let options = table::requests::TableOptions {
1335 extra_options: extra_options
1336 .iter()
1337 .map(|(k, v)| (k.to_string(), v.to_string()))
1338 .collect(),
1339 ..Default::default()
1340 };
1341 let meta = table::metadata::TableMeta {
1342 schema,
1343 primary_key_indices,
1344 value_indices: vec![],
1345 engine: engine.to_string(),
1346 next_column_id: 1,
1347 options,
1348 created_on: Default::default(),
1349 updated_on: Default::default(),
1350 partition_key_indices: vec![],
1351 column_ids: vec![],
1352 };
1353 table::metadata::TableInfoBuilder::default()
1354 .table_id(1)
1355 .name(SEMANTIC_RELATIONSHIPS_DECLARED_TABLE_NAME)
1356 .catalog_name("greptime")
1357 .schema_name(DEFAULT_PRIVATE_SCHEMA_NAME)
1358 .table_version(0)
1359 .table_type(table::metadata::TableType::Base)
1360 .meta(meta)
1361 .build()
1362 .unwrap()
1363 }
1364
1365 #[test]
1366 fn declared_schema_match_checks_more_than_columns() {
1367 let mito = common_catalog::consts::MITO_ENGINE;
1368 let canonical_pk = || (6..14).collect::<Vec<_>>();
1369
1370 let ok = declared_table_info(|_| {}, mito, canonical_pk(), &[(TTL_KEY, "180d")]);
1371 assert!(declared_relationships_schema_matches(&ok));
1372 assert_eq!(
1375 ok.meta
1376 .schema
1377 .column_schema_by_name("attributes")
1378 .map(|c| c.data_type.clone()),
1379 Some(datatypes::prelude::ConcreteDataType::json_datatype())
1380 );
1381 let ok = declared_table_info(
1382 |_| {},
1383 mito,
1384 canonical_pk(),
1385 &[(MERGE_MODE_KEY, "last_row")],
1386 );
1387 assert!(declared_relationships_schema_matches(&ok));
1388
1389 let wrong_engine = declared_table_info(|_| {}, "metric", canonical_pk(), &[]);
1390 assert!(!declared_relationships_schema_matches(&wrong_engine));
1391
1392 let pk_missing_generation_id = declared_table_info(|_| {}, mito, (6..13).collect(), &[]);
1393 assert!(!declared_relationships_schema_matches(
1394 &pk_missing_generation_id
1395 ));
1396
1397 let wrong_time_index = declared_table_info(
1398 |columns| {
1399 columns[0] = columns[0].clone().with_time_index(false);
1400 columns[4] = datatypes::schema::ColumnSchema::new(
1401 "valid_from",
1402 datatypes::prelude::ConcreteDataType::timestamp_millisecond_datatype(),
1403 false,
1404 )
1405 .with_time_index(true);
1406 },
1407 mito,
1408 canonical_pk(),
1409 &[],
1410 );
1411 assert!(!declared_relationships_schema_matches(&wrong_time_index));
1412
1413 let append_mode =
1414 declared_table_info(|_| {}, mito, canonical_pk(), &[(APPEND_MODE_KEY, "true")]);
1415 assert!(!declared_relationships_schema_matches(&append_mode));
1416
1417 let wrong_column_type = declared_table_info(
1418 |columns| {
1419 columns[14] = datatypes::schema::ColumnSchema::new(
1420 "confidence",
1421 datatypes::prelude::ConcreteDataType::int64_datatype(),
1422 true,
1423 );
1424 },
1425 mito,
1426 canonical_pk(),
1427 &[],
1428 );
1429 assert!(!declared_relationships_schema_matches(&wrong_column_type));
1430 }
1431}