Files
greptimedb/tests/cases/standalone/common/system/semantic_graph.sql
T
dennis zhuang e778a72829 feat: complete the derived-edge vocabulary of the entity graph (#8836)
* feat(operator): pair calls edges across trace tables and derive virtual-node edges

Union the normalized client and server spans of all trace tables before the
join, so a client span pairs with a server span stored in a different table.
A client span with no matching server span becomes an edge to a virtual node
named by span attributes (peer.service / db.name / server.address), with
confidence < 1.0 and attributes.connection_type; a window's real pairs win
over virtual candidates for the same edge key.

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

* feat(operator): derive same-row co-declared edges from the built-in vocabulary

A table declaring both entity types of a vocabulary pair witnesses the edge
on every row carrying both identities: runs_on / contains / part_of for any
declaring table (provenance 'attribute'), agent uses model / agent invoked
tool only for trace sources (span-structure observations, provenance
'trace').

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

* feat(operator): derive parent_agent-calls-agent edges from span structure

Trace tables declaring an agent entity pair each span with its child span
across tables (no span-kind filter), keep pairs whose agent identities
differ, and aggregate RED metrics per window, anchored on the parent span
like the service derivation is anchored on the client.

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

* feat(frontend): feed co-declared and agent sources into the relationships scan

scan_relationships now passes every declaring table (with its trace-ness)
to the co-declared branch and the trace tables' agent declarations to the
agent-calls derivation. enumerate validates the fixed trace-v1 columns and
derives around a malformed trace table instead of failing the whole scan.

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

* test: cover cross-table pairing, virtual nodes, co-declared and agent edges

sqlness exercises the new derivations end to end (including a malformed
trace-model table being skipped); the integration authorization test now
also pins that a pair split across tables derives no edge when the caller
cannot read one side.

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

* chore: update the relationships module doc for the new branches

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

* chore: import shared derivation helpers via crate paths

The fmt CI gate rejects module-level 'use super::' imports.

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

* fix: fold co-declared duplicates, decouple agent calls, verify the trace time index

Review findings: the co-declared branch lacked a cross-source DISTINCT, so
two tables witnessing the same edge in one window emitted duplicate rows;
the agent-calls derivation was gated on a usable service declaration; the
trace schema guard accepted a table whose time index is not the column the
derivations bucket by. The empty-trace-table test asserted a union
invariant with no information and is dropped.

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

* fix: rename the agent-tool edge to invokes and track current OTel peer attributes

The vocabulary's other relation names are present tense; semconv 1.39/1.26
replaced peer.service and db.name with service.peer.name and db.namespace,
so the virtual-node candidates now check the current names first and keep
the deprecated ones for existing telemetry.

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

* fix: trust the trace-v1 table option instead of matching the fixed schema

The option is only ever stamped by the ingest path, which guarantees the
fixed span columns; matching column types here couples the graph to every
trace schema evolution (e.g. #8816) for a case that cannot occur.

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

---------

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
2026-08-12 04:06:31 +00:00

301 lines
12 KiB
SQL

-- The computed entity-graph tables under greptime_private are read-only
-- virtual tables: readable, but rejecting every DDL/DML path.
select observed_at, entity_type, entity_id, scope from greptime_private.semantic_entities;
select observed_at, src_id, dst_id, rel_type from greptime_private.semantic_relationships;
insert into greptime_private.semantic_entities (observed_at, entity_type, entity_id) values (now(), 'service', 'svc-a');
-- Plain-literal VALUES takes the direct region-insert path, not the query
-- engine; the table-level guard must hold there too.
insert into greptime_private.semantic_entities (observed_at, entity_type, entity_id) values (0, 'service', 'svc-b');
create table greptime_private.semantic_entities (ts timestamp time index);
create table greptime_private.semantic_relationships (ts timestamp time index);
alter table greptime_private.semantic_entities add column extra string;
truncate table greptime_private.semantic_entities;
drop table greptime_private.semantic_entities;
drop table greptime_private.semantic_relationships;
-- Read-time derivation: declare entity identities on a user table and observe
-- them in the computed registry (single-column and composite ids, scope and
-- descriptive columns, JSON output columns).
create table graph_app_latency (
ts timestamp time index,
service_name string,
host string,
env string,
latency double,
primary key (service_name, host, env)
) with (
'greptime.semantic.entity.service.id' = 'service_name',
'greptime.semantic.entity.service.scope' = 'env',
'greptime.semantic.entity.process.id' = 'service_name,host',
'greptime.semantic.entity.process.descriptive' = 'env'
);
insert into graph_app_latency values (now(), 'cart', 'h1', 'us-east', 3.5);
-- SQLNESS PROTOCOL MYSQL
select entity_type, entity_id, entity_id_attrs, scope, descriptive, source_tables
from greptime_private.semantic_entities
order by entity_type, entity_id;
drop table graph_app_latency;
-- Renaming a physical table INTO a reserved computed-table name would let the
-- overlay shadow it, orphaning its data.
create table greptime_private.graph_rename_probe (ts timestamp time index);
alter table greptime_private.graph_rename_probe rename semantic_entities;
drop table greptime_private.graph_rename_probe;
-- Calls derivation: a trace-v1-model table with client/server span pairs.
-- Trace tables need no entity declaration: the service entities are implicit.
create table graph_traces (
"timestamp" timestamp(9) time index,
trace_id string,
span_id string,
parent_span_id string,
span_kind string,
span_status_code string,
service_name string,
duration_nano bigint unsigned,
primary key (service_name)
) with ('table_data_model' = 'greptime_trace_v1', 'append_mode' = 'true');
-- The epoch-timestamped pair falls outside the default one-hour derivation
-- window and must not appear in the results.
insert into graph_traces values
(now(), 't1', 'c1', NULL, 'SPAN_KIND_CLIENT', 'STATUS_CODE_UNSET', 'frontend', 0),
(now(), 't1', 's1', 'c1', 'SPAN_KIND_SERVER', 'STATUS_CODE_ERROR', 'cart', 1500000000),
(now(), 't2', 'c2', NULL, 'SPAN_KIND_CLIENT', 'STATUS_CODE_UNSET', 'frontend', 0),
(now(), 't2', 's2', 'c2', 'SPAN_KIND_SERVER', 'STATUS_CODE_UNSET', 'cart', 500000000),
(0, 't0', 'c0', NULL, 'SPAN_KIND_CLIENT', 'STATUS_CODE_UNSET', 'stale-src', 0),
(0, 't0', 's0', 'c0', 'SPAN_KIND_SERVER', 'STATUS_CODE_UNSET', 'stale-dst', 100);
-- SQLNESS PROTOCOL MYSQL
select src_type, src_id, dst_type, dst_id, rel_type, provenance, confidence,
request_count, error_count, duration_sum, duration_count, attributes
from greptime_private.semantic_relationships
order by src_id;
-- SQLNESS PROTOCOL MYSQL
select entity_type, entity_id, entity_id_attrs, scope, source_tables
from greptime_private.semantic_entities
order by entity_id;
drop table graph_traces;
-- Declared edges: the physical table is created by the system on the first
-- INSERT with its canonical schema. Re-asserting an edge stores a new revision;
-- reads keep only the latest one per edge key.
insert into greptime_private.semantic_relationships_declared
(observed_at, src_type, src_id, rel_type, dst_type, dst_id, provenance, scope, generation_id, confidence)
values (now() - interval '10 minute', 'service', 'frontend', 'depends_on', 'service', 'users-db', 'declared', '', '', 0.5);
insert into greptime_private.semantic_relationships_declared
(observed_at, src_type, src_id, rel_type, dst_type, dst_id, provenance, scope, generation_id, confidence)
values (now() - interval '5 minute', 'service', 'frontend', 'depends_on', 'service', 'users-db', 'declared', '', '', 1.0);
-- An old open-ended declaration stays valid until its row expires; an edge
-- retired in the past must not appear.
insert into greptime_private.semantic_relationships_declared
(observed_at, src_type, src_id, rel_type, dst_type, dst_id, provenance, scope, generation_id, valid_until)
values
('1970-01-01 00:00:01', 'service', 'legacy', 'depends_on', 'service', 'mainframe', 'declared', '', '', NULL),
('1970-01-01 00:00:01', 'service', 'retired', 'depends_on', 'service', 'oldsys', 'declared', '', '', '2000-01-01 00:00:00');
select src_id, dst_id, rel_type, provenance, confidence
from greptime_private.semantic_relationships
order by src_id;
-- An explicit observed_at window replaces the default last hour; the emitted
-- timestamps of declared edges are synthesized inside the queried window.
select observed_at, window_start, fresh_until, src_id, dst_id
from greptime_private.semantic_relationships
where observed_at >= '2001-01-01 00:00:00' and observed_at < '2001-01-02 00:00:00'
order by src_id;
-- A lower bound alone is fine (the upper bound defaults to now)...
select src_id, dst_id, provenance
from greptime_private.semantic_relationships
where observed_at >= now() - interval '30 minute'
order by src_id;
-- ...but an upper bound alone would scan unbounded history: explicit error.
select src_id from greptime_private.semantic_relationships
where observed_at < '2001-01-02 00:00:00';
-- The declared-edge table's definition is system-owned: user CREATE/ALTER are
-- rejected and a physical table cannot be renamed into the name, while plain
-- DML stays allowed.
create table greptime_private.semantic_relationships_declared (ts timestamp time index);
alter table greptime_private.semantic_relationships_declared add column extra string;
create table greptime_private.declared_rename_probe (ts timestamp time index);
alter table greptime_private.declared_rename_probe rename semantic_relationships_declared;
drop table greptime_private.declared_rename_probe;
delete from greptime_private.semantic_relationships_declared;
select src_id from greptime_private.semantic_relationships order by src_id;
-- DROP is allowed (nothing structural is lost: the next INSERT recreates the
-- canonical table) and cleans up after this test.
drop table greptime_private.semantic_relationships_declared;
insert into greptime_private.semantic_relationships_declared
(observed_at, src_type, src_id, rel_type, dst_type, dst_id, provenance, scope, generation_id)
values (now(), 'service', 'reborn', 'depends_on', 'service', 'db', 'declared', '', '');
select src_id from greptime_private.semantic_relationships order by src_id;
drop table greptime_private.semantic_relationships_declared;
-- Same-row co-declaration: a table declaring both entity types of a built-in
-- vocabulary pair witnesses the edge on every row carrying both identities;
-- the vocabulary fixes the direction.
create table graph_pod_metrics (
ts timestamp time index,
instance string,
host string,
"service" string,
cpu double,
primary key (instance, host, "service")
) with (
'greptime.semantic.entity.service.instance.id' = 'instance',
'greptime.semantic.entity.host.id' = 'host',
'greptime.semantic.entity.service.id' = 'service'
);
insert into graph_pod_metrics values (now(), 'cart-0', 'node-1', 'cart', 0.5);
select src_type, src_id, dst_type, dst_id, rel_type, provenance, confidence
from greptime_private.semantic_relationships
order by rel_type, src_id;
drop table graph_pod_metrics;
-- Cross-table calls pairing: the client span and its child server span land in
-- different trace tables and must still pair into one edge. A trace-model
-- table that lost the fixed span columns is skipped instead of failing the
-- scan.
create table graph_traces_a (
"timestamp" timestamp(9) time index,
trace_id string,
span_id string,
parent_span_id string,
span_kind string,
span_status_code string,
service_name string,
duration_nano bigint unsigned,
primary key (service_name)
) with ('table_data_model' = 'greptime_trace_v1', 'append_mode' = 'true');
create table graph_traces_b (
"timestamp" timestamp(9) time index,
trace_id string,
span_id string,
parent_span_id string,
span_kind string,
span_status_code string,
service_name string,
duration_nano bigint unsigned,
primary key (service_name)
) with ('table_data_model' = 'greptime_trace_v1', 'append_mode' = 'true');
create table graph_traces_malformed (
ts timestamp time index,
note string
) with ('table_data_model' = 'greptime_trace_v1');
insert into graph_traces_a values
(now(), 't1', 'c1', NULL, 'SPAN_KIND_CLIENT', 'STATUS_CODE_UNSET', 'frontend', 0);
insert into graph_traces_b values
(now(), 't1', 's1', 'c1', 'SPAN_KIND_SERVER', 'STATUS_CODE_ERROR', 'cart', 1500000000);
insert into graph_traces_malformed values (now(), 'not a trace');
select src_id, dst_id, rel_type, provenance, confidence, request_count, error_count
from greptime_private.semantic_relationships
order by src_id;
drop table graph_traces_a;
drop table graph_traces_b;
drop table graph_traces_malformed;
-- Virtual nodes: a client span with no matching server span names its peer
-- through span attributes; the edge carries confidence < 1.0 and the
-- connection type.
create table graph_traces_virtual (
"timestamp" timestamp(9) time index,
trace_id string,
span_id string,
parent_span_id string,
span_kind string,
span_status_code string,
service_name string,
duration_nano bigint unsigned,
"span_attributes.peer.service" string,
"span_attributes.db.name" string,
primary key (service_name)
) with ('table_data_model' = 'greptime_trace_v1', 'append_mode' = 'true');
insert into graph_traces_virtual values
(now(), 't1', 'c1', NULL, 'SPAN_KIND_CLIENT', 'STATUS_CODE_UNSET', 'frontend', 250000000, 'redis', NULL),
(now(), 't2', 'c2', NULL, 'SPAN_KIND_CLIENT', 'STATUS_CODE_UNSET', 'frontend', 100000000, NULL, 'orders-db');
-- SQLNESS PROTOCOL MYSQL
select src_id, dst_id, rel_type, confidence, request_count, attributes
from greptime_private.semantic_relationships
order by dst_id;
drop table graph_traces_virtual;
-- Agent edges: span structure derives parent_agent calls agent, and span rows
-- co-declaring agent+model / agent+tool witness uses / invokes.
create table graph_agent_traces (
"timestamp" timestamp(9) time index,
trace_id string,
span_id string,
parent_span_id string,
span_kind string,
span_status_code string,
service_name string,
duration_nano bigint unsigned,
agent_id string,
model_name string,
tool_name string,
primary key (service_name, agent_id, model_name, tool_name)
) with (
'table_data_model' = 'greptime_trace_v1',
'append_mode' = 'true',
'greptime.semantic.entity.agent.id' = 'agent_id',
'greptime.semantic.entity.model.id' = 'model_name',
'greptime.semantic.entity.tool.id' = 'tool_name'
);
insert into graph_agent_traces values
(now(), 't1', 'p1', NULL, 'SPAN_KIND_INTERNAL', 'STATUS_CODE_UNSET', 'app', 0, 'orchestrator', NULL, NULL),
(now(), 't1', 'a1', 'p1', 'SPAN_KIND_INTERNAL', 'STATUS_CODE_UNSET', 'app', 2000000000, 'researcher', 'gpt-5', NULL),
(now(), 't1', 'a2', 'a1', 'SPAN_KIND_INTERNAL', 'STATUS_CODE_UNSET', 'app', 500000000, 'researcher', NULL, 'web_search');
select src_type, src_id, dst_type, dst_id, rel_type, provenance
from greptime_private.semantic_relationships
order by rel_type, dst_id;
drop table graph_agent_traces;