Files
greptimedb/tests/runner/src/formatter.rs
T
dennis zhuang 3d12273c84 feat: read-time entity relationships graph over telemetry (M0+M1) (#8614)
* feat(table): add entity semantic declarations

Define open-ended greptime.semantic.entity.* options, validate entity columns at DDL time, and stamp OTLP trace tables with the service entity declaration.

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* feat: add read-time entity relationships graph

Add computed semantic graph tables, typed DataFusion derivation plans for entity registry and trace calls edges, and streaming read-time execution.

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* test: exclude semantic graph tables from table constraints

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* refactor(operator): name the plan-builder source groupings

Review feedback: build_registry_plan / build_calls_plan took anonymous
(declarations, DataFrame) tuples while the caller already grouped the same
fields. Introduce RegistrySource { declarations, scan } and CallsSource
{ service, scan } next to the builders and flow them through the frontend
caller and tests. The frontend-side EntitySource keeps holding a TableRef
(the operator builders stay pure over already-built scans), so the named
structs live in operator rather than reusing that type. No behavior change.

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

---------

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
2026-08-07 03:37:14 +00:00

280 lines
8.7 KiB
Rust

// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::borrow::Cow;
use std::fmt::Display;
use std::sync::Arc;
use client::{Output, OutputData, RecordBatches};
use common_error::ext::ErrorExt;
use datatypes::prelude::ConcreteDataType;
use datatypes::scalars::ScalarVectorBuilder;
use datatypes::schema::{ColumnSchema, Schema};
use datatypes::vectors::{StringVectorBuilder, VectorRef};
use mysql::Row as MySqlRow;
use tokio_postgres::SimpleQueryMessage as PgRow;
use crate::client::MysqlSqlResult;
/// A formatter for errors.
pub struct ErrorFormatter<E: ErrorExt>(E);
impl<E: ErrorExt> From<E> for ErrorFormatter<E> {
fn from(error: E) -> Self {
ErrorFormatter(error)
}
}
impl<E: ErrorExt> Display for ErrorFormatter<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let status_code = self.0.status_code();
let root_cause = self.0.output_msg();
let root_cause = normalize_meta_client_error(&root_cause, status_code);
write!(
f,
"Error: {}({status_code}), {root_cause}",
status_code as u32
)
}
}
fn normalize_meta_client_error(
root_cause: &str,
status_code: common_error::status_code::StatusCode,
) -> &str {
let details_prefix = format!(", code: {status_code}, tonic code: ");
match root_cause.rsplit_once(&details_prefix) {
Some((message, tonic_code)) if !tonic_code.is_empty() => message,
_ => root_cause,
}
}
/// A formatter for [`Output`].
pub struct OutputFormatter(Output);
impl From<Output> for OutputFormatter {
fn from(output: Output) -> Self {
OutputFormatter(output)
}
}
impl Display for OutputFormatter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.0.data {
OutputData::AffectedRows(rows) => {
write!(f, "Affected Rows: {rows}")
}
OutputData::RecordBatches(recordbatches) => {
let pretty = recordbatches.pretty_print().map_err(|e| e.to_string());
match pretty {
Ok(s) => write!(f, "{s}"),
Err(e) => {
write!(f, "Failed to pretty format {recordbatches:?}, error: {e}")
}
}
}
OutputData::Stream(_) => unreachable!(),
}
}
}
pub struct PostgresqlFormatter(Vec<PgRow>);
impl From<Vec<PgRow>> for PostgresqlFormatter {
fn from(rows: Vec<PgRow>) -> Self {
PostgresqlFormatter(rows)
}
}
impl Display for PostgresqlFormatter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.0.is_empty() {
return f.write_fmt(format_args!("(Empty response)"));
}
if let PgRow::CommandComplete(affected_rows) = &self.0[0] {
return write!(
f,
"{}",
OutputFormatter(Output::new_with_affected_rows(*affected_rows as usize))
);
};
let Some(recordbatches) = build_recordbatches_from_postgres_rows(&self.0) else {
return Ok(());
};
write!(
f,
"{}",
OutputFormatter(Output::new_with_record_batches(recordbatches))
)
}
}
fn build_recordbatches_from_postgres_rows(rows: &[PgRow]) -> Option<RecordBatches> {
// create schema
let schema = match &rows[0] {
PgRow::RowDescription(desc) => Arc::new(Schema::new(
desc.iter()
.map(|column| {
ColumnSchema::new(column.name(), ConcreteDataType::string_datatype(), true)
})
.collect(),
)),
_ => unreachable!(),
};
if schema.num_columns() == 0 {
return None;
}
// convert to string vectors
let mut columns: Vec<StringVectorBuilder> = (0..schema.num_columns())
.map(|_| StringVectorBuilder::with_capacity(schema.num_columns()))
.collect();
for row in rows.iter().skip(1) {
if let PgRow::Row(row) = row {
for (i, column) in columns.iter_mut().enumerate().take(schema.num_columns()) {
column.push(row.get(i));
}
}
}
let columns: Vec<VectorRef> = columns
.into_iter()
.map(|mut col| Arc::new(col.finish()) as VectorRef)
.collect();
// construct recordbatch
let recordbatches = RecordBatches::try_from_columns(schema, columns)
.expect("Failed to construct recordbatches from columns. Please check the schema.");
Some(recordbatches)
}
/// A formatter for [`MysqlSqlResult`].
pub struct MysqlFormatter(MysqlSqlResult);
impl From<MysqlSqlResult> for MysqlFormatter {
fn from(result: MysqlSqlResult) -> Self {
MysqlFormatter(result)
}
}
impl Display for MysqlFormatter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.0 {
MysqlSqlResult::AffectedRows(rows) => {
write!(f, "affected_rows: {rows}")
}
MysqlSqlResult::Rows(rows) => {
if rows.is_empty() {
return f.write_fmt(format_args!("(Empty response)"));
}
let recordbatches = build_recordbatches_from_mysql_rows(rows);
write!(
f,
"{}",
OutputFormatter(Output::new_with_record_batches(recordbatches))
)
}
}
}
}
pub fn build_recordbatches_from_mysql_rows(rows: &[MySqlRow]) -> RecordBatches {
// create schema
let head_column = &rows[0];
let head_binding = head_column.columns();
let names = head_binding
.iter()
.map(|column| column.name_str())
.collect::<Vec<Cow<str>>>();
let schema = Arc::new(Schema::new(
names
.iter()
.map(|name| {
ColumnSchema::new(name.to_string(), ConcreteDataType::string_datatype(), true)
})
.collect(),
));
// convert to string vectors
let mut columns: Vec<StringVectorBuilder> = (0..schema.num_columns())
.map(|_| StringVectorBuilder::with_capacity(schema.num_columns()))
.collect();
for row in rows.iter() {
for (i, name) in names.iter().enumerate() {
// `get::<String, _>` panics on NULL values, so fetch as Option.
let value: Option<String> = row.get::<Option<String>, &str>(name).flatten();
columns[i].push(value.as_deref());
}
}
let columns: Vec<VectorRef> = columns
.into_iter()
.map(|mut col| Arc::new(col.finish()) as VectorRef)
.collect();
// construct recordbatch
RecordBatches::try_from_columns(schema, columns)
.expect("Failed to construct recordbatches from columns. Please check the schema.")
}
#[cfg(test)]
mod tests {
use common_error::ext::PlainError;
use common_error::status_code::StatusCode;
use super::*;
#[test]
fn test_normalize_meta_client_error() {
let error = PlainError::new(
"Invalid options, code: InvalidArguments, tonic code: Client specified an invalid argument"
.to_string(),
StatusCode::InvalidArguments,
);
assert_eq!(
"Error: 1004(InvalidArguments), Invalid options",
ErrorFormatter::from(error).to_string()
);
}
#[test]
fn test_preserve_regular_error() {
let error = PlainError::new(
"Invalid options without transport details".to_string(),
StatusCode::InvalidArguments,
);
assert_eq!(
"Error: 1004(InvalidArguments), Invalid options without transport details",
ErrorFormatter::from(error).to_string()
);
}
#[test]
fn test_preserve_error_with_mismatched_code() {
let error = PlainError::new(
"Invalid options, code: Unsupported, tonic code: Operation is not supported"
.to_string(),
StatusCode::InvalidArguments,
);
assert_eq!(
"Error: 1004(InvalidArguments), Invalid options, code: Unsupported, tonic code: Operation is not supported",
ErrorFormatter::from(error).to_string()
);
}
}