Files
greptimedb/tests/cases/standalone/common/system/semantic_graph.sql
dennis zhuang 546625c45a feat: embedded convention pack for the entity graph (prom/k8s, gen_ai naming) (#8854)
* feat: embed the derivation conventions as data and adopt gen_ai entity naming

Move the co-declared edge vocabulary, the agent-edge vocabulary and the
virtual-destination candidates from Rust consts into an embedded
conventions.yaml (include_str!), parsed once behind a LazyLock and
validated against the entity-type grammar and the closed rel_type set; a
broken file propagates as a plan error instead of panicking. The agent
vocabulary entity types follow the GenAI semantic-convention namespace
as written: gen_ai.agent / gen_ai.model / gen_ai.tool.

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

* feat: drop the tag requirement for entity identity columns

Entity declarations no longer require id columns to be tag/primary-key
columns; only column existence is validated. Trace pipelines flatten the
identifying attributes (span_attributes.gen_ai.agent.id, ...) into field
columns, so the tag rule locked real trace tables out of declaring
entities while buying no correctness — the read-time derivation works on
any column.

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

* feat: implicit declarations for well-known prometheus info metrics

Tables stamped signal_type=metric + source=prometheus whose name matches
the conventions.yaml whitelist (kube_pod_info, kube_node_info,
kube_pod_owner, target_info) get implicit entity declarations: k8s.pod /
k8s.node / k8s.workload with name-based identity and target_info's
service / service.instance with the remaining tags as the descriptive
snapshot. The existing co-declared vocabulary then derives runs_on and
part_of from the same rows, so no new edge branch is needed. Explicit
declarations of a type always suppress the implicit one, and the metric
engine's physical table is excluded (it aggregates every logical
table's columns).

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

* test: cover the prometheus conventions in sqlness and compact the graph cases

Add the whitelisted-info-metric scenario (kube_pod_info, kube_pod_owner,
target_info deriving runs_on / part_of, a non-whitelisted metric
contributing nothing), fold the single-table calls, cross-table pairing
and virtual-node cases into one trace scenario (they exercise the same
union-before-join path), merge the two declaring-metric-table cases, and
reuse one rename probe for both reserved names.

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

* fix: reject entity id columns without a stable string form

Review follow-ups: the DDL check now validates against the schema and
rejects binary-backed and nested types for identity columns (the
derivation renders ids via CAST to Utf8, so the failure used to surface
only when the graph was scanned); the agent sqlness case keeps its
identity columns as fields to cover the relaxed tag rule end to end;
stale tag-rule comments and a dangling const reference are cleaned up.

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

* fix: type-check every entity column role, not only ids

The registry renders scope and descriptive values through the same
CAST-to-string path as ids, so a binary-backed column in any role fails
at scan time; the DDL check is now role-independent (and simpler).

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

* refactor: name the code-anchored vocabulary constants

Entity types and edge attributes the derivation code itself anchors on
(service, gen_ai.agent, calls, trace/attribute provenance) become
constants in the conventions module; the rest of the vocabulary stays
YAML-only data. ImplicitEntity is renamed PromImplicitEntity, and the
implicit-declaration path logs each skip of a whitelisted info metric
(wrong stamps, suppressed by an explicit declaration, missing id
column) so a missing graph entity is diagnosable.

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

* refactor: single-source the graph constants

The graph tables' column names move to common-catalog (the schemas
catalog exposes and the plans operator builds must match column by
column), and the conventions module now carries the complete built-in
vocabulary — entity types, rel_types, provenances and connection types —
with the embedded YAML validated by membership against it, so an edit
drifting outside the vocabulary fails the conventions test instead of
deriving nothing.

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

* fix: treat empty identity components as absent

kube-state-metrics emits empty-string labels an entity id must not be
built from: an unscheduled pod's node and an owner-less pod's owner_kind
/ owner_name. Standard Prometheus drops empty labels (they arrive as
NULL and the existing predicate handles them), but other remote-write
agents may keep them, which produced ghost entities with empty ids and
false runs_on / part_of edges. Every identity predicate (registry,
co-declared edges, span endpoints) now requires non-NULL and non-empty
components through one shared helper.

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

* refactor: tighten the conventions DSL semantics

Rename the co-declaration rule lists to what they are (co_declared_edges
/ trace_co_declared_edges — derivation rules, not a relation
vocabulary), stop overstating the GenAI entity types (Greptime types
derived from GenAI attributes; OTel defines no model/tool entities),
move target_info's descriptive snapshot to service.instance (the
remaining labels are the target's resource attributes, and instances
would write conflicting snapshots onto the logical service), and extend
the descriptor whitelist with the stable KSM sources: container info
metrics (closing the k8s.pod contains k8s.container rule),
kube_service_info (new k8s.service entity type) and the fuller
descriptive label sets.

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

* fix: guard entity column types on ALTER as well

ALTER MODIFY COLUMN could change a declared entity column to a type
without a stable string form, deferring the failure to graph scan time;
verify_alter now checks the post-alter schema. Dropping a declared
column stays allowed — the read-time derivation skips the stale
declaration, and semantic options cannot be altered off yet.

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

* feat: bridge traces and kube-state-metrics on the pod UID

Trace-v1 tables now get implicit declarations from their flattened
resource attributes (otlp_trace_entities in conventions.yaml): the
service identity — replacing the hardcoded fallback — plus
service.instance and k8s.pod, each applied only when its columns exist.
A new co-declared rule derives service.instance runs_on k8s.pod, and
the whitelisted kube-state-metrics pod identity switches from
namespace+pod names to the UID, so the trace-side pod and every KSM
descriptor land on one entity while names stay descriptive. This also
removes pod identity from the multi-cluster same-name collision.

The conventions rejection tests were passing for the wrong reason (a
half-renamed fixture key failed deserialization before reaching any
validation rule); they now assert the specific error each case targets.
Sqlness covers the UID merge across descriptor tables, pod-contains-
container, the k8s.service node, and the empty-uid/empty-node rows
deriving nothing.

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

* test: cover the OTLP-to-graph chain end to end

One real OTLP export must come out of semantic_relationships as the
zero-configuration chain: service calls service, instance part_of
service, instance runs_on pod (bridged by k8s.pod.uid). Resources
without service.instance.id or k8s.pod.uid derive nothing extra.

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

* fix: identify k8s.service by UID

Same reasoning as pods: a recreated same-name service must not merge
into the old entity and same-named services across clusters must not
collide; kube_service_info carries a stable uid and nothing joins on the
service's name. Also drop a stale tag-rule mention from the option
validation docs.

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

* chore: cut duplicated test coverage and redundant comments

The trace service-fallback test collapsed into the resource-entities
test (same synthesis path since the fallback moved to YAML; only the
invalid-explicit-no-fallback case was distinct), role-duplicate and
subsumed DDL cases are gone, the embedded-conventions test is just the
parse (its assertions were decorative), and the YAML section comments no
longer restate the struct docs.

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

---------

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
2026-08-13 02:31:56 +00:00

361 lines
14 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;
-- Renaming a physical table INTO a reserved name would let the overlay shadow
-- it (or squat the declared table's canonical schema), orphaning its data.
create table greptime_private.graph_rename_probe (ts timestamp time index);
alter table greptime_private.graph_rename_probe rename semantic_entities;
alter table greptime_private.graph_rename_probe rename semantic_relationships_declared;
drop table greptime_private.graph_rename_probe;
-- Read-time derivation from one declaring metric table: entity identities in
-- the computed registry (single-column and composite ids, scope and
-- descriptive columns, JSON output columns), and the same rows witnessing the
-- co-declared vocabulary edges (runs_on / part_of, direction built in).
create table graph_app_metrics (
ts timestamp time index,
service_name string,
instance string,
host string,
env string,
latency double,
primary key (service_name, instance, host, env)
) with (
'greptime.semantic.entity.service.id' = 'service_name',
'greptime.semantic.entity.service.scope' = 'env',
'greptime.semantic.entity.service.instance.id' = 'instance',
'greptime.semantic.entity.host.id' = 'host',
'greptime.semantic.entity.process.id' = 'service_name,host',
'greptime.semantic.entity.process.descriptive' = 'env'
);
insert into graph_app_metrics values (now(), 'cart', 'cart-0', '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;
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_app_metrics;
-- Calls derivation over trace-v1 tables, all branches in one scan: the client
-- span and its child server span land in different tables and still pair into
-- one edge with RED metrics; the epoch-timestamped pair falls outside the
-- default one-hour window; unmatched clients become virtual-node edges named
-- from span attributes (confidence < 1.0, connection type); a trace-model
-- table that lost the fixed span columns is skipped instead of failing the
-- scan. Trace tables need no entity declaration: service entities are
-- implicit.
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,
"span_attributes.peer.service" string,
"span_attributes.db.name" string,
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, NULL, NULL),
(now(), 't2', 'c2', NULL, 'SPAN_KIND_CLIENT', 'STATUS_CODE_UNSET', 'frontend', 0, NULL, NULL),
(0, 't0', 'c0', NULL, 'SPAN_KIND_CLIENT', 'STATUS_CODE_UNSET', 'stale-src', 0, NULL, NULL),
(0, 't0', 's0', 'c0', 'SPAN_KIND_SERVER', 'STATUS_CODE_UNSET', 'stale-dst', 100, NULL, NULL),
(now(), 't3', 'c3', NULL, 'SPAN_KIND_CLIENT', 'STATUS_CODE_UNSET', 'frontend', 250000000, 'redis', NULL),
(now(), 't4', 'c4', NULL, 'SPAN_KIND_CLIENT', 'STATUS_CODE_UNSET', 'frontend', 100000000, NULL, 'orders-db');
insert into graph_traces_b values
(now(), 't1', 's1', 'c1', 'SPAN_KIND_SERVER', 'STATUS_CODE_ERROR', 'cart', 1500000000),
(now(), 't2', 's2', 'c2', 'SPAN_KIND_SERVER', 'STATUS_CODE_UNSET', 'cart', 500000000);
insert into graph_traces_malformed values (now(), 'not a trace');
-- SQLNESS PROTOCOL MYSQL
select src_id, dst_id, rel_type, provenance, confidence,
request_count, error_count, duration_sum, duration_count, attributes
from greptime_private.semantic_relationships
order by dst_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_a;
drop table graph_traces_b;
drop table graph_traces_malformed;
-- 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, 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;
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;
-- Agent edges: span structure derives parent_agent calls agent, and span rows
-- co-declaring gen_ai.agent+gen_ai.model / gen_ai.agent+gen_ai.tool witness
-- uses / invokes. The identity columns are fields, not tags.
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)
) with (
'table_data_model' = 'greptime_trace_v1',
'append_mode' = 'true',
'greptime.semantic.entity.gen_ai.agent.id' = 'agent_id',
'greptime.semantic.entity.gen_ai.model.id' = 'model_name',
'greptime.semantic.entity.gen_ai.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;
-- Prometheus conventions: whitelisted entity-descriptor metrics (stamped
-- signal_type=metric + source=prometheus by the remote-write path) get
-- implicit declarations, and the co-declared rules derive runs_on / part_of /
-- contains from their rows. Pod identity is the UID, so every KSM descriptor
-- lands on one entity; the empty labels kube-state-metrics emits (unscheduled
-- pod's node, ownerless pod's owner_*) identify nothing. A non-whitelisted
-- metric contributes nothing.
create table kube_pod_info (
greptime_timestamp timestamp(3) time index,
uid string,
namespace string,
pod string,
node string,
greptime_value double,
primary key (uid, namespace, pod, node)
) with (
'greptime.semantic.signal_type' = 'metric',
'greptime.semantic.source' = 'prometheus'
);
create table kube_pod_owner (
greptime_timestamp timestamp(3) time index,
uid string,
namespace string,
pod string,
owner_kind string,
owner_name string,
greptime_value double,
primary key (uid, namespace, pod, owner_kind, owner_name)
) with (
'greptime.semantic.signal_type' = 'metric',
'greptime.semantic.source' = 'prometheus'
);
create table kube_pod_container_info (
greptime_timestamp timestamp(3) time index,
uid string,
container string,
image string,
greptime_value double,
primary key (uid, container, image)
) with (
'greptime.semantic.signal_type' = 'metric',
'greptime.semantic.source' = 'prometheus'
);
create table kube_service_info (
greptime_timestamp timestamp(3) time index,
uid string,
namespace string,
"service" string,
cluster_ip string,
greptime_value double,
primary key (uid, namespace, "service", cluster_ip)
) with (
'greptime.semantic.signal_type' = 'metric',
'greptime.semantic.source' = 'prometheus'
);
create table target_info (
greptime_timestamp timestamp(3) time index,
job string,
instance string,
k8s_cluster_name string,
greptime_value double,
primary key (job, instance, k8s_cluster_name)
) with (
'greptime.semantic.signal_type' = 'metric',
'greptime.semantic.source' = 'prometheus'
);
create table http_requests_total (
greptime_timestamp timestamp(3) time index,
job string,
instance string,
greptime_value double,
primary key (job, instance)
) with (
'greptime.semantic.signal_type' = 'metric',
'greptime.semantic.source' = 'prometheus'
);
insert into kube_pod_info values
(now(), 'uid-1', 'default', 'api-1', 'node-a', 1),
(now(), 'uid-2', 'default', 'api-2', 'node-a', 1),
(now(), 'uid-3', 'default', 'pending-1', '', 1),
(now(), '', 'default', 'ghost', 'node-a', 1);
insert into kube_pod_owner values
(now(), 'uid-1', 'default', 'api-1', 'ReplicaSet', 'api-rs', 1),
(now(), 'uid-2', 'default', 'api-2', 'ReplicaSet', 'api-rs', 1);
insert into kube_pod_container_info values
(now(), 'uid-1', 'main', 'nginx:1.25', 1);
insert into kube_service_info values
(now(), 'svc-uid-1', 'default', 'api-svc', '10.0.0.1', 1);
insert into target_info values
(now(), 'shop/api', 'inst-1', 'prod', 1);
insert into http_requests_total values
(now(), 'shop/api', 'inst-1', 42);
-- SQLNESS PROTOCOL MYSQL
select entity_type, entity_id, source_tables
from greptime_private.semantic_entities
order by entity_type, entity_id, source_tables;
-- SQLNESS PROTOCOL MYSQL
select src_type, src_id, dst_type, dst_id, rel_type, provenance
from greptime_private.semantic_relationships
order by rel_type, src_id, dst_id;
drop table kube_pod_info;
drop table kube_pod_owner;
drop table kube_pod_container_info;
drop table kube_service_info;
drop table target_info;
drop table http_requests_total;