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};
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 descriptive_columns: Vec<String>,
297 pub scope_columns: Vec<String>,
300}
301
302#[derive(Debug, Clone)]
319pub struct GraphQueryWindow {
320 observed_start_ms: i64,
321 observed_end_ms: i64,
322 source_start_ms: i64,
323 source_end_ms: i64,
324}
325
326impl GraphQueryWindow {
327 pub fn from_observed(start_ms: i64, end_ms: i64) -> Self {
329 const BIN_MS: i64 = BIN_NANOS / 1_000_000;
330 let ceil_to_bin = |ms: i64| {
332 ms.div_euclid(BIN_MS) * BIN_MS
333 + if ms.rem_euclid(BIN_MS) == 0 {
334 0
335 } else {
336 BIN_MS
337 }
338 };
339 Self {
343 observed_start_ms: start_ms,
344 observed_end_ms: end_ms,
345 source_start_ms: ceil_to_bin(start_ms),
346 source_end_ms: ceil_to_bin(end_ms),
347 }
348 }
349
350 pub fn default_last_hour() -> Self {
354 let end_ms = common_time::util::current_time_millis();
355 Self::from_observed(end_ms - 60 * 60 * 1000, end_ms)
356 }
357
358 pub fn observed_start(&self) -> Expr {
360 ts_ms_lit(self.observed_start_ms)
361 }
362
363 pub fn observed_end(&self) -> Expr {
365 ts_ms_lit(self.observed_end_ms)
366 }
367
368 pub fn source_start(&self) -> Expr {
370 ts_ms_lit(self.source_start_ms)
371 }
372
373 pub fn source_end(&self) -> Expr {
375 ts_ms_lit(self.source_end_ms)
376 }
377}
378
379fn ts_ms_lit(ms: i64) -> Expr {
380 lit(ScalarValue::TimestampMillisecond(Some(ms), None))
381}
382
383fn interval(nanos: i64) -> Expr {
385 lit(ScalarValue::new_interval_mdn(0, 0, nanos))
386}
387
388fn bin_interval() -> Expr {
389 interval(BIN_NANOS)
390}
391
392fn bin_ms(ts: Expr) -> Expr {
396 cast(
397 datetime_fns::date_bin().call(vec![bin_interval(), ts]),
398 DataType::Timestamp(TimeUnit::Millisecond, None),
399 )
400}
401
402fn union_all(acc: Option<DataFrame>, branch: DataFrame) -> DfResult<Option<DataFrame>> {
404 Ok(Some(match acc {
405 Some(acc) => acc.union(branch)?,
406 None => branch,
407 }))
408}
409
410fn qcol(relation: &str, name: &str) -> Expr {
413 Expr::Column(Column::new(Some(relation), name))
414}
415
416fn concat_expr(parts: Vec<Expr>) -> Expr {
417 string_fns::concat().call(parts)
418}
419
420fn cast_string_or_empty(column: &str) -> Expr {
423 core_fns::coalesce().call(vec![cast(ident(column), DataType::Utf8), lit("")])
424}
425
426pub(crate) fn identifies(column: &str) -> Expr {
432 ident(column)
433 .is_not_null()
434 .and(cast(ident(column), DataType::Utf8).not_eq(lit("")))
435}
436
437const ID_SEPARATOR: &str = ",";
438const ID_ESCAPE: &str = "\\";
439const ID_QUALIFIER_SEPARATOR: &str = "/";
441
442fn escaped_id_value(value: Expr) -> Expr {
446 let escape_escape = string_fns::replace().call(vec![
447 value,
448 lit(ID_ESCAPE),
449 lit(format!("{ID_ESCAPE}{ID_ESCAPE}")),
450 ]);
451 string_fns::replace().call(vec![
452 escape_escape,
453 lit(ID_SEPARATOR),
454 lit(format!("{ID_ESCAPE}{ID_SEPARATOR}")),
455 ])
456}
457
458fn entity_id_expr(
470 id_columns: &[String],
471 qualifier: Option<&str>,
472 col: &dyn Fn(&str) -> Expr,
473) -> Expr {
474 let mut parts = Vec::with_capacity(id_columns.len() * 2);
475 for (index, column) in id_columns.iter().enumerate() {
476 if index > 0 {
477 parts.push(lit(ID_SEPARATOR));
478 }
479 let value = escaped_id_value(cast(col(column), DataType::Utf8));
480 parts.push(match qualifier {
481 Some(qualifier) if index == 0 => qualified_id_expr(qualifier, value, col),
482 _ => value,
483 });
484 }
485 if let [single] = parts.as_slice() {
486 single.clone()
487 } else {
488 concat_expr(parts)
489 }
490}
491
492fn qualified_id_expr(qualifier: &str, value: Expr, col: &dyn Fn(&str) -> Expr) -> Expr {
496 let qualifier = escaped_id_value(
497 core_fns::coalesce().call(vec![cast(col(qualifier), DataType::Utf8), lit("")]),
498 );
499 Expr::Case(Case::new(
500 None,
501 vec![(
502 Box::new(qualifier.clone().eq(lit(""))),
503 Box::new(value.clone()),
504 )],
505 Some(Box::new(concat_expr(vec![
506 qualifier,
507 lit(ID_QUALIFIER_SEPARATOR),
508 value,
509 ]))),
510 ))
511}
512
513static PARSE_JSON_UDF: LazyLock<Arc<ScalarUDF>> = LazyLock::new(|| {
516 Arc::new(
517 FUNCTION_REGISTRY
518 .get_function("parse_json")
519 .expect("parse_json must be registered")
520 .provide(FunctionContext::default()),
521 )
522});
523
524fn parse_json_expr(json_text: Expr) -> Expr {
528 cast(PARSE_JSON_UDF.call(vec![json_text]), DataType::Binary)
529}
530
531fn null_json() -> Expr {
534 lit(ScalarValue::Binary(None))
535}
536
537fn json_quote(value: &str) -> String {
539 serde_json::Value::from(value).to_string()
540}
541
542static JSON_OBJECT_UDF: LazyLock<Arc<ScalarUDF>> = LazyLock::new(|| {
546 Arc::new(
547 FUNCTION_REGISTRY
548 .get_function("json_object")
549 .expect("json_object must be registered")
550 .provide(FunctionContext::default()),
551 )
552});
553
554fn json_object_expr(columns: &[String]) -> Expr {
559 let mut args = Vec::with_capacity(columns.len() * 2);
560 for column in columns {
561 args.push(lit(column.as_str()));
562 args.push(cast_string_or_empty(column));
563 }
564 cast(JSON_OBJECT_UDF.call(args), DataType::Binary)
565}
566
567fn sorted_kv_expr_with(sorted_cols: &[String], nullable: bool, col: &dyn Fn(&str) -> Expr) -> Expr {
571 let mut parts = Vec::with_capacity(sorted_cols.len() * 3);
572 for (i, column) in sorted_cols.iter().enumerate() {
573 if i > 0 {
574 parts.push(lit(","));
575 }
576 parts.push(lit(format!("{column}=")));
577 parts.push(if nullable {
578 core_fns::coalesce().call(vec![cast(col(column), DataType::Utf8), lit("")])
579 } else {
580 cast(col(column), DataType::Utf8)
581 });
582 }
583 concat_expr(parts)
584}
585
586const REGISTRY_COLUMNS: [&str; 10] = [
587 OBSERVED_AT_COLUMN,
588 WINDOW_START_COLUMN,
589 WINDOW_END_COLUMN,
590 FRESH_UNTIL_COLUMN,
591 ENTITY_TYPE_COLUMN,
592 ENTITY_ID_COLUMN,
593 ENTITY_ID_ATTRS_COLUMN,
594 ENTITY_SCOPE_COLUMN,
595 ENTITY_DESCRIPTIVE_COLUMN,
596 SOURCE_TABLES_COLUMN,
597];
598
599const REGISTRY_VALID_COLUMN: &str = "__entity_valid";
600
601fn unnest_rows(
607 df: DataFrame,
608 window_predicate: Expr,
609 valid_column: &'static str,
610 columns: &[&'static str],
611 rows: Vec<Vec<Expr>>,
612) -> DfResult<DataFrame> {
613 let mut arrays = vec![Vec::with_capacity(rows.len()); columns.len() + 1];
614 for row in rows {
615 debug_assert_eq!(row.len(), arrays.len());
616 for (array, value) in arrays.iter_mut().zip(row) {
617 array.push(value);
618 }
619 }
620 let array_names = std::iter::once(valid_column)
621 .chain(columns.iter().copied())
622 .collect::<Vec<_>>();
623 let array_projection = array_names
624 .iter()
625 .zip(arrays)
626 .map(|(name, values)| make_array(values).alias(*name))
627 .collect::<Vec<_>>();
628
629 df.filter(window_predicate)?
630 .select(array_projection)?
631 .unnest_columns(&array_names)?
632 .filter(ident(valid_column))?
633 .select(columns.iter().map(|c| ident(*c)).collect::<Vec<_>>())?
634 .distinct()
635}
636
637fn registry_source(
640 first: &EntityDeclaration,
641 rest: &[EntityDeclaration],
642 df: DataFrame,
643 window: &GraphQueryWindow,
644) -> DfResult<DataFrame> {
645 let ts = ident(&first.time_index);
646 let bin = bin_ms(ts.clone());
647 let window_predicate = ts
648 .clone()
649 .gt_eq(window.source_start())
650 .and(ts.lt(window.source_end()));
651
652 let mut rows = Vec::with_capacity(1 + rest.len());
653 for decl in std::iter::once(first).chain(rest) {
654 let entity_id = entity_id_expr(&decl.id_columns, decl.id_qualifier.as_deref(), &|c| {
659 ident(c)
660 });
661 let id_parts = decl
662 .id_qualifier
663 .iter()
664 .chain(&decl.id_columns)
665 .cloned()
666 .collect::<Vec<_>>();
667 let entity_id_attrs = if id_parts.len() == 1 {
668 null_json()
669 } else {
670 json_object_expr(&id_parts)
671 };
672
673 let scope = match decl.scope_columns.as_slice() {
674 [] => lit(""),
675 [single] => cast_string_or_empty(single),
677 _ => {
678 let mut cols = decl.scope_columns.clone();
679 cols.sort();
680 sorted_kv_expr_with(&cols, true, &|c| ident(c))
681 }
682 };
683
684 let descriptive = if decl.descriptive_columns.is_empty() {
685 null_json()
686 } else {
687 json_object_expr(&decl.descriptive_columns)
688 };
689
690 let source_tables = parse_json_expr(lit(format!(
691 "[{}]",
692 json_quote(&format!("{}.{}", decl.schema, decl.table))
693 )));
694
695 let valid = decl
698 .id_columns
699 .iter()
700 .fold(lit(true), |predicate, id| predicate.and(identifies(id)));
701
702 rows.push(vec![
703 valid,
704 bin.clone(),
705 bin.clone(),
706 bin.clone() + bin_interval(),
707 bin.clone() + bin_interval(),
708 lit(decl.entity_type.as_str()),
709 entity_id,
710 entity_id_attrs,
711 scope,
712 descriptive,
713 source_tables,
714 ]);
715 }
716
717 unnest_rows(
718 df,
719 window_predicate,
720 REGISTRY_VALID_COLUMN,
721 ®ISTRY_COLUMNS,
722 rows,
723 )
724}
725
726pub struct RegistrySource {
729 pub declarations: Vec<EntityDeclaration>,
730 pub scan: DataFrame,
731}
732
733pub fn build_registry_plan(
738 sources: Vec<RegistrySource>,
739 window: &GraphQueryWindow,
740) -> DfResult<Option<LogicalPlan>> {
741 let mut union_df: Option<DataFrame> = None;
742 for source in sources {
743 let Some((first, rest)) = source.declarations.split_first() else {
744 continue;
745 };
746 union_df = union_all(union_df, registry_source(first, rest, source.scan, window)?)?;
747 }
748 Ok(union_df.map(DataFrame::into_unoptimized_plan))
749}
750
751#[cfg(test)]
754pub(crate) mod test_util {
755 use datafusion::arrow::array::{Array, BinaryArray, StringArray, TimestampMillisecondArray};
756 use datafusion::arrow::record_batch::RecordBatch;
757 use datafusion::prelude::SessionContext;
758 use datafusion_expr::LogicalPlan;
759
760 pub(crate) async fn collect(ctx: &SessionContext, plan: LogicalPlan) -> Vec<RecordBatch> {
761 ctx.execute_logical_plan(plan)
762 .await
763 .unwrap()
764 .collect()
765 .await
766 .unwrap()
767 }
768
769 pub(crate) fn json_texts(batch: &RecordBatch, column: usize) -> Vec<Option<String>> {
770 let array = batch
771 .column(column)
772 .as_any()
773 .downcast_ref::<BinaryArray>()
774 .unwrap();
775 (0..array.len())
776 .map(|i| {
777 array
778 .is_valid(i)
779 .then(|| jsonb::from_slice(array.value(i)).unwrap().to_string())
780 })
781 .collect()
782 }
783
784 pub(crate) fn strings(batch: &RecordBatch, column: usize) -> Vec<String> {
785 let array = batch
786 .column(column)
787 .as_any()
788 .downcast_ref::<StringArray>()
789 .unwrap();
790 (0..array.len())
791 .map(|i| array.value(i).to_string())
792 .collect()
793 }
794
795 pub(crate) fn ts_values(batch: &RecordBatch, column: usize) -> Vec<i64> {
796 let array = batch
797 .column(column)
798 .as_any()
799 .downcast_ref::<TimestampMillisecondArray>()
800 .unwrap();
801 (0..array.len()).map(|i| array.value(i)).collect()
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use std::sync::Arc;
808
809 use datafusion::arrow::array::{ArrayRef, Int64Array, StringArray, TimestampMillisecondArray};
810 use datafusion::arrow::datatypes::{Field, Schema};
811 use datafusion::arrow::record_batch::RecordBatch;
812 use datafusion::datasource::MemTable;
813 use datafusion::prelude::SessionContext;
814
815 use super::test_util::{collect, json_texts, strings};
816 use super::*;
817
818 fn test_window() -> GraphQueryWindow {
819 GraphQueryWindow::from_observed(0, 10 * 60 * 1000)
820 }
821
822 #[test]
823 fn window_source_scan_covers_whole_buckets() {
824 let window = GraphQueryWindow::from_observed(630_000, 690_000);
827 assert_eq!(
828 (window.source_start_ms, window.source_end_ms),
829 (660_000, 720_000)
830 );
831
832 let window = GraphQueryWindow::from_observed(600_000, 720_000);
834 assert_eq!(
835 (window.source_start_ms, window.source_end_ms),
836 (600_000, 720_000)
837 );
838
839 let window = GraphQueryWindow::from_observed(610_000, 650_000);
841 assert!(window.source_start_ms >= window.source_end_ms);
842
843 let window = GraphQueryWindow::from_observed(-90_000, -30_000);
845 assert_eq!((window.source_start_ms, window.source_end_ms), (-60_000, 0));
846 }
847
848 fn metric_table_ctx() -> SessionContext {
851 let schema = Arc::new(Schema::new(vec![
852 Field::new(
853 "ts",
854 DataType::Timestamp(TimeUnit::Millisecond, None),
855 false,
856 ),
857 Field::new("service_name", DataType::Utf8, false),
858 Field::new("pid", DataType::Int64, false),
859 Field::new("host", DataType::Utf8, true),
860 ]));
861 let batch = RecordBatch::try_new(
862 schema.clone(),
863 vec![
864 Arc::new(TimestampMillisecondArray::from(vec![1_000, 2_000, 61_000])) as ArrayRef,
865 Arc::new(StringArray::from(vec!["cart", "cart", "cart"])),
866 Arc::new(Int64Array::from(vec![42, 42, 42])),
867 Arc::new(StringArray::from(vec![
868 Some("we\"ird\\\nhost"),
869 None,
870 Some("h2"),
871 ])),
872 ],
873 )
874 .unwrap();
875 let ctx = SessionContext::new();
876 ctx.register_table(
877 "app_latency",
878 Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap()),
879 )
880 .unwrap();
881 ctx
882 }
883
884 fn decl(entity_type: &str, id_columns: &[&str]) -> EntityDeclaration {
885 EntityDeclaration {
886 schema: "public".to_string(),
887 table: "app_latency".to_string(),
888 time_index: "ts".to_string(),
889 entity_type: entity_type.to_string(),
890 id_columns: id_columns.iter().map(|s| s.to_string()).collect(),
891 id_qualifier: None,
892 descriptive_columns: vec![],
893 scope_columns: vec![],
894 }
895 }
896
897 #[tokio::test]
900 async fn registry_id_escaping_and_qualifier() {
901 let schema = Arc::new(Schema::new(vec![
902 Field::new(
903 "ts",
904 DataType::Timestamp(TimeUnit::Millisecond, None),
905 false,
906 ),
907 Field::new("service_name", DataType::Utf8, false),
908 Field::new("instance", DataType::Utf8, false),
909 Field::new("namespace", DataType::Utf8, true),
910 ]));
911 let batch = RecordBatch::try_new(
912 schema.clone(),
913 vec![
914 Arc::new(TimestampMillisecondArray::from(vec![1_000, 2_000, 3_000])) as ArrayRef,
915 Arc::new(StringArray::from(vec!["cart", "a,b", "we\\ird"])),
916 Arc::new(StringArray::from(vec!["i-1", "c", "i-3"])),
917 Arc::new(StringArray::from(vec![Some("shop"), None, Some("")])),
918 ],
919 )
920 .unwrap();
921 let ctx = SessionContext::new();
922 ctx.register_table(
923 "svc",
924 Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap()),
925 )
926 .unwrap();
927
928 let mut declaration = decl("service.instance", &["service_name", "instance"]);
929 declaration.id_qualifier = Some("namespace".to_string());
930 declaration.time_index = "ts".to_string();
931 let plan = build_registry_plan(
932 vec![RegistrySource {
933 declarations: vec![declaration],
934 scan: ctx.table("svc").await.unwrap(),
935 }],
936 &test_window(),
937 )
938 .unwrap()
939 .unwrap();
940
941 let mut ids: Vec<String> = collect(&ctx, plan)
942 .await
943 .iter()
944 .flat_map(|batch| strings(batch, 5))
945 .collect();
946 ids.sort();
947 assert_eq!(
948 ids,
949 vec![
950 "a\\,b,c".to_string(),
953 "shop/cart,i-1".to_string(),
954 "we\\\\ird,i-3".to_string(),
955 ]
956 );
957 }
958
959 #[tokio::test]
960 async fn registry_single_column_identity() {
961 let ctx = metric_table_ctx();
962 let df = ctx.table("app_latency").await.unwrap();
963 let plan = build_registry_plan(
964 vec![RegistrySource {
965 declarations: vec![decl("service", &["service_name"])],
966 scan: df,
967 }],
968 &test_window(),
969 )
970 .unwrap()
971 .unwrap();
972
973 let names = plan
974 .schema()
975 .fields()
976 .iter()
977 .map(|f| f.name().as_str())
978 .collect::<Vec<_>>();
979 assert_eq!(
980 names,
981 [
982 "observed_at",
983 "window_start",
984 "window_end",
985 "fresh_until",
986 "entity_type",
987 "entity_id",
988 "entity_id_attrs",
989 "scope",
990 "descriptive",
991 "source_tables",
992 ]
993 );
994
995 let batches = collect(&ctx, plan).await;
996 let total: usize = batches.iter().map(|b| b.num_rows()).sum();
997 assert_eq!(total, 2);
999 let batch = &batches[0];
1000 assert_eq!(strings(batch, 4), vec!["service"; batch.num_rows()]);
1001 assert_eq!(strings(batch, 5), vec!["cart"; batch.num_rows()]);
1002 assert!(json_texts(batch, 6).iter().all(Option::is_none));
1004 assert!(json_texts(batch, 8).iter().all(Option::is_none));
1005 assert_eq!(
1006 json_texts(batch, 9),
1007 vec![Some(r#"["public.app_latency"]"#.to_string()); batch.num_rows()]
1008 );
1009 }
1010
1011 #[tokio::test]
1012 async fn registry_composite_identity_and_descriptive_escaping() {
1013 let ctx = metric_table_ctx();
1014 let df = ctx.table("app_latency").await.unwrap();
1015 let mut declaration = decl("process", &["service_name", "pid"]);
1016 declaration.descriptive_columns = vec!["host".to_string()];
1017 let plan = build_registry_plan(
1018 vec![RegistrySource {
1019 declarations: vec![declaration],
1020 scan: df,
1021 }],
1022 &test_window(),
1023 )
1024 .unwrap()
1025 .unwrap();
1026
1027 let batches = collect(&ctx, plan).await;
1028 let mut rows: Vec<(String, Option<String>, Option<String>)> = batches
1029 .iter()
1030 .flat_map(|batch| {
1031 let ids = strings(batch, 5);
1032 let id_attrs = json_texts(batch, 6);
1033 let descriptives = json_texts(batch, 8);
1034 ids.into_iter()
1035 .zip(id_attrs)
1036 .zip(descriptives)
1037 .map(|((id, attrs), descriptive)| (id, attrs, descriptive))
1038 .collect::<Vec<_>>()
1039 })
1040 .collect();
1041 rows.sort();
1042
1043 assert_eq!(
1047 rows,
1048 vec![
1049 (
1050 "cart,42".to_string(),
1051 Some(r#"{"pid":"42","service_name":"cart"}"#.to_string()),
1052 Some(r#"{"host":""}"#.to_string()),
1053 ),
1054 (
1055 "cart,42".to_string(),
1056 Some(r#"{"pid":"42","service_name":"cart"}"#.to_string()),
1057 Some(r#"{"host":"h2"}"#.to_string()),
1058 ),
1059 (
1060 "cart,42".to_string(),
1061 Some(r#"{"pid":"42","service_name":"cart"}"#.to_string()),
1062 Some(r#"{"host":"we\"ird\\\nhost"}"#.to_string()),
1063 ),
1064 ]
1065 );
1066 }
1067
1068 #[tokio::test]
1069 async fn registry_scope_variants() {
1070 let ctx = metric_table_ctx();
1071
1072 let mut single = decl("service", &["service_name"]);
1074 single.scope_columns = vec!["host".to_string()];
1075 let df = ctx.table("app_latency").await.unwrap();
1076 let plan = build_registry_plan(
1077 vec![RegistrySource {
1078 declarations: vec![single],
1079 scan: df,
1080 }],
1081 &test_window(),
1082 )
1083 .unwrap()
1084 .unwrap();
1085 let batches = collect(&ctx, plan).await;
1086 let mut scopes: Vec<String> = batches.iter().flat_map(|b| strings(b, 7)).collect();
1087 scopes.sort();
1088 assert_eq!(scopes, vec!["", "h2", "we\"ird\\\nhost"]);
1089
1090 let mut multi = decl("service", &["service_name"]);
1092 multi.scope_columns = vec!["pid".to_string(), "host".to_string()];
1093 let df = ctx.table("app_latency").await.unwrap();
1094 let plan = build_registry_plan(
1095 vec![RegistrySource {
1096 declarations: vec![multi],
1097 scan: df,
1098 }],
1099 &test_window(),
1100 )
1101 .unwrap()
1102 .unwrap();
1103 let batches = collect(&ctx, plan).await;
1104 let mut scopes: Vec<String> = batches.iter().flat_map(|b| strings(b, 7)).collect();
1105 scopes.sort();
1106 assert_eq!(
1107 scopes,
1108 vec![
1109 "host=,pid=42",
1110 "host=h2,pid=42",
1111 "host=we\"ird\\\nhost,pid=42"
1112 ]
1113 );
1114 }
1115
1116 #[tokio::test]
1117 async fn registry_skips_absent_identity_rows() {
1118 let schema = Arc::new(Schema::new(vec![
1121 Field::new(
1122 "ts",
1123 DataType::Timestamp(TimeUnit::Millisecond, None),
1124 false,
1125 ),
1126 Field::new("host", DataType::Utf8, true),
1127 ]));
1128 let batch = RecordBatch::try_new(
1129 schema.clone(),
1130 vec![
1131 Arc::new(TimestampMillisecondArray::from(vec![1_000, 2_000, 3_000])) as ArrayRef,
1132 Arc::new(StringArray::from(vec![Some("h2"), None, Some("")])),
1133 ],
1134 )
1135 .unwrap();
1136 let ctx = SessionContext::new();
1137 ctx.register_table(
1138 "app_latency",
1139 Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap()),
1140 )
1141 .unwrap();
1142 let df = ctx.table("app_latency").await.unwrap();
1143 let plan = build_registry_plan(
1144 vec![RegistrySource {
1145 declarations: vec![decl("host", &["host"])],
1146 scan: df,
1147 }],
1148 &test_window(),
1149 )
1150 .unwrap()
1151 .unwrap();
1152 let batches = collect(&ctx, plan).await;
1153 let ids: Vec<String> = batches.iter().flat_map(|b| strings(b, 5)).collect();
1154 assert_eq!(ids, vec!["h2"]);
1155 }
1156
1157 #[tokio::test]
1158 async fn registry_expands_declarations_with_one_source_scan() {
1159 let ctx = metric_table_ctx();
1160 let df = ctx.table("app_latency").await.unwrap();
1161 let plan = build_registry_plan(
1162 vec![RegistrySource {
1163 declarations: vec![decl("service", &["service_name"]), decl("host", &["pid"])],
1164 scan: df,
1165 }],
1166 &test_window(),
1167 )
1168 .unwrap()
1169 .unwrap();
1170
1171 assert_eq!(
1172 plan.display_indent()
1173 .to_string()
1174 .matches("TableScan: app_latency")
1175 .count(),
1176 1
1177 );
1178 let batches = collect(&ctx, plan).await;
1179 let mut types: Vec<String> = batches.iter().flat_map(|b| strings(b, 4)).collect();
1180 types.sort();
1181 assert_eq!(types, vec!["host", "host", "service", "service"]);
1182
1183 assert!(
1185 build_registry_plan(vec![], &test_window())
1186 .unwrap()
1187 .is_none()
1188 );
1189 }
1190
1191 fn declared_table_info(
1194 mutate_columns: impl FnOnce(&mut Vec<datatypes::schema::ColumnSchema>),
1195 engine: &str,
1196 primary_key_indices: Vec<usize>,
1197 extra_options: &[(&str, &str)],
1198 ) -> table::metadata::TableInfo {
1199 let canonical = build_declared_relationships_expr("greptime");
1200 let mut columns: Vec<datatypes::schema::ColumnSchema> = canonical
1201 .column_defs
1202 .iter()
1203 .map(|def| {
1204 let wrapper = api::helper::ColumnDataTypeWrapper::try_new(
1205 def.data_type,
1206 def.datatype_extension.clone(),
1207 )
1208 .unwrap();
1209 datatypes::schema::ColumnSchema::new(
1210 &def.name,
1211 datatypes::prelude::ConcreteDataType::from(wrapper),
1212 def.is_nullable,
1213 )
1214 .with_time_index(def.name == OBSERVED_AT_COLUMN)
1215 })
1216 .collect();
1217 mutate_columns(&mut columns);
1218 let schema = Arc::new(
1219 datatypes::schema::SchemaBuilder::try_from_columns(columns)
1220 .unwrap()
1221 .build()
1222 .unwrap(),
1223 );
1224 let options = table::requests::TableOptions {
1225 extra_options: extra_options
1226 .iter()
1227 .map(|(k, v)| (k.to_string(), v.to_string()))
1228 .collect(),
1229 ..Default::default()
1230 };
1231 let meta = table::metadata::TableMeta {
1232 schema,
1233 primary_key_indices,
1234 value_indices: vec![],
1235 engine: engine.to_string(),
1236 next_column_id: 1,
1237 options,
1238 created_on: Default::default(),
1239 updated_on: Default::default(),
1240 partition_key_indices: vec![],
1241 column_ids: vec![],
1242 };
1243 table::metadata::TableInfoBuilder::default()
1244 .table_id(1)
1245 .name(SEMANTIC_RELATIONSHIPS_DECLARED_TABLE_NAME)
1246 .catalog_name("greptime")
1247 .schema_name(DEFAULT_PRIVATE_SCHEMA_NAME)
1248 .table_version(0)
1249 .table_type(table::metadata::TableType::Base)
1250 .meta(meta)
1251 .build()
1252 .unwrap()
1253 }
1254
1255 #[test]
1256 fn declared_schema_match_checks_more_than_columns() {
1257 let mito = common_catalog::consts::MITO_ENGINE;
1258 let canonical_pk = || (6..14).collect::<Vec<_>>();
1259
1260 let ok = declared_table_info(|_| {}, mito, canonical_pk(), &[(TTL_KEY, "180d")]);
1261 assert!(declared_relationships_schema_matches(&ok));
1262 assert_eq!(
1265 ok.meta
1266 .schema
1267 .column_schema_by_name("attributes")
1268 .map(|c| c.data_type.clone()),
1269 Some(datatypes::prelude::ConcreteDataType::json_datatype())
1270 );
1271 let ok = declared_table_info(
1272 |_| {},
1273 mito,
1274 canonical_pk(),
1275 &[(MERGE_MODE_KEY, "last_row")],
1276 );
1277 assert!(declared_relationships_schema_matches(&ok));
1278
1279 let wrong_engine = declared_table_info(|_| {}, "metric", canonical_pk(), &[]);
1280 assert!(!declared_relationships_schema_matches(&wrong_engine));
1281
1282 let pk_missing_generation_id = declared_table_info(|_| {}, mito, (6..13).collect(), &[]);
1283 assert!(!declared_relationships_schema_matches(
1284 &pk_missing_generation_id
1285 ));
1286
1287 let wrong_time_index = declared_table_info(
1288 |columns| {
1289 columns[0] = columns[0].clone().with_time_index(false);
1290 columns[4] = datatypes::schema::ColumnSchema::new(
1291 "valid_from",
1292 datatypes::prelude::ConcreteDataType::timestamp_millisecond_datatype(),
1293 false,
1294 )
1295 .with_time_index(true);
1296 },
1297 mito,
1298 canonical_pk(),
1299 &[],
1300 );
1301 assert!(!declared_relationships_schema_matches(&wrong_time_index));
1302
1303 let append_mode =
1304 declared_table_info(|_| {}, mito, canonical_pk(), &[(APPEND_MODE_KEY, "true")]);
1305 assert!(!declared_relationships_schema_matches(&append_mode));
1306
1307 let wrong_column_type = declared_table_info(
1308 |columns| {
1309 columns[14] = datatypes::schema::ColumnSchema::new(
1310 "confidence",
1311 datatypes::prelude::ConcreteDataType::int64_datatype(),
1312 true,
1313 );
1314 },
1315 mito,
1316 canonical_pk(),
1317 &[],
1318 );
1319 assert!(!declared_relationships_schema_matches(&wrong_column_type));
1320 }
1321}