Skip to content

Commit f9116fd

Browse files
mtabebeantiguru
andcommitted
metric sinks: SHOW, catalog visibility, and RBAC (SQL-572)
Problem: A metric sink was invisible, `mz_objects` did not carry it, and there was no way to read back the SQL that created it. Solution: `SHOW METRIC SINKS [FROM <schema>] [IN CLUSTER <c>] [LIKE ...]` lists name, `from`, and cluster. `SHOW [REDACTED] CREATE METRIC SINK` replays `create_sql`. Both require `enable_metric_sink`. `mz_internal.mz_metric_sinks` exposes what the catalog knows about a sink, shaped like `mz_catalog.mz_sinks`, and joins into `mz_objects` as type `metric-sink`. It is a materialized view derived from `mz_catalog_raw`. It carries `owner_id`, so a sink's owner is now visible in the catalog and its ownership checks are testable. Like `mz_sinks`, it is indexed on `id` so the `mz_objects` union reuses the arrangement. The relation carries only the columns something reads. `create_sql` and `redacted_create_sql` have no reader, and `SHOW CREATE` is how you get the SQL back. `enable_metric_sink` gates the DDL and the SHOW verbs, not the catalog relation. `mz_metric_sinks` is always present and public, so with the flag off it is simply empty. Testing: - `metric_sink.slt` covers discovery and access control: the `mz_metric_sinks` row resolving to its `FROM` relation, cluster, schema, and owner, `mz_objects` and `mz_show_all_objects` membership, the three `SHOW METRIC SINKS` filters, the `SHOW CREATE` round-trip (plain and redacted), the audit rows, and privileges. - The restart platform check reads `SHOW METRIC SINKS` instead of probing with a `CREATE` expected to fail. Co-authored-by: Moritz Hoffmann <antiguru@gmail.com>
1 parent 30e4b1d commit f9116fd

26 files changed

Lines changed: 667 additions & 99 deletions

File tree

doc/user/content/reference/system-catalog/mz_internal.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,12 @@ The view is defined as the transitive closure of [`mz_object_dependencies`](#mz_
716716
| `object_id` | [`text`] | The ID of the dependent object. Corresponds to [`mz_objects.id`](../mz_catalog/#mz_objects). |
717717
| `referenced_object_id` | [`text`] | The ID of the (possibly transitively) referenced object. Corresponds to [`mz_objects.id`](../mz_catalog/#mz_objects). |
718718

719+
<!-- RELATION_SPEC_UNDOCUMENTED mz_internal.mz_metric_sinks -->
720+
<!-- TODO(metric-sink): promote to a documented RELATION_SPEC once
721+
`enable_metric_sink` defaults on. The relation already ships full
722+
per-column comments, so this "undocumented" marker is only correct while
723+
the feature is gated off. -->
724+
719725
## `mz_notices`
720726

721727
{{< public-preview />}}

misc/python/materialize/checks/all_checks/metric_sink.py

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -61,24 +61,13 @@ def manipulate(self) -> list[Testdrive]:
6161
]
6262

6363
def validate(self) -> Testdrive:
64-
# Recreating a sink that is already there is how we probe for it without
65-
# mutating anything: metric sinks are not in `mz_objects` yet, and
66-
# `mz_catalog_raw` needs a system connection.
67-
#
68-
# TODO(SQL-572): once metric sinks join `mz_objects` (and the
69-
# `mz_metric_sinks` builtin view), probe survival by SELECTing the sink
70-
# row instead of re-issuing CREATE and matching "already exists". The
71-
# current probe is a proxy: it confirms the catalog item was re-parsed on
72-
# boot without needing a system connection.
7364
return Testdrive(dedent("""
74-
! CREATE METRIC SINK metric_sink_schema_renamed.metric_sink_one FROM metric_sink_view_renamed WITH (PREFIX = 'mz_metric_sink_one_')
75-
contains:metric sink "materialize.metric_sink_schema_renamed.metric_sink_one" already exists
65+
> SHOW METRIC SINKS
66+
metric_sink_three metric_sink_view_renamed quickstart
67+
metric_sink_two metric_sink_view_renamed quickstart
7668
77-
! CREATE METRIC SINK metric_sink_two FROM metric_sink_view_renamed WITH (PREFIX = 'mz_metric_sink_two_')
78-
contains:metric sink "materialize.public.metric_sink_two" already exists
79-
80-
! CREATE METRIC SINK metric_sink_three FROM metric_sink_view_renamed WITH (PREFIX = 'mz_metric_sink_three_')
81-
contains:metric sink "materialize.public.metric_sink_three" already exists
69+
> SHOW METRIC SINKS FROM metric_sink_schema_renamed
70+
metric_sink_one metric_sink_view_renamed quickstart
8271
8372
# The FROM edge came back too, so the view is still pinned, under the
8473
# name the rename gave it.

misc/wasm/src/sql-parser-wasm/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ fn refine_show_kind(stmt: &ShowStatement<Raw>) -> &'static str {
100100
ShowStatement::ShowCreateSource(_) => "show_create_source",
101101
ShowStatement::ShowCreateTable(_) => "show_create_table",
102102
ShowStatement::ShowCreateSink(_) => "show_create_sink",
103+
ShowStatement::ShowCreateMetricSink(_) => "show_create_metric_sink",
103104
ShowStatement::ShowCreateIndex(_) => "show_create_index",
104105
ShowStatement::ShowCreateConnection(_) => "show_create_connection",
105106
ShowStatement::ShowCreateCluster(_) => "show_create_cluster",

src/adapter/src/catalog/builtin_table_updates.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -190,14 +190,9 @@ impl CatalogState {
190190
CatalogItem::Func(func) => {
191191
self.pack_func_update(id, schema_id, name, owner_id, func, diff)
192192
}
193-
// A metric sink packs no builtin-table row, so it holds a catalog name that no
194-
// catalog relation reports. SQL-572 adds the `mz_metric_sinks` view. Until then,
195-
// listing or dropping a metric sink requires knowing its name out of band.
196-
//
197-
// NOTE: creating a metric sink takes SELECT on the FROM relation, not ownership
198-
// of it, so once metric sinks are user-creatable this gap would let a reader
199-
// egress another role's rows with no catalog relation the owner could see it in.
200-
// SQL-572 has to land before user-facing metric sink DDL does.
193+
// Tables, views, and metric sinks are exposed through materialized
194+
// views derived from `mz_catalog_raw`, and logs and secrets never
195+
// had builtin-table rows, so none pack a row here.
201196
CatalogItem::Table(_)
202197
| CatalogItem::View(_)
203198
| CatalogItem::Log(_)

src/adapter/src/catalog/open/builtin_schema_migration.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,10 @@ static MIGRATIONS: LazyLock<Vec<MigrationStep>> = LazyLock::new(|| {
390390
MZ_CATALOG_SCHEMA,
391391
"mz_audit_events",
392392
),
393-
// Required because we added the `mz_object_graph_edges_ind` builtin index.
394-
// make_mz_indexes inlines the builtin-index set as VALUES, so any add or
395-
// remove changes its SQL fingerprint and requires an explicit replacement.
393+
// Required because we added the `mz_object_graph_edges_ind` and
394+
// `mz_metric_sinks_ind` builtin indexes. make_mz_indexes inlines the
395+
// builtin-index set as VALUES, so any add or remove changes its SQL
396+
// fingerprint and requires an explicit replacement.
396397
MigrationStep::replacement(
397398
"26.39.0-dev.0",
398399
CatalogItemType::MaterializedView,

src/catalog/src/builtin.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,6 +1172,7 @@ pub static BUILTINS_STATIC: LazyLock<Vec<Builtin<NameReference>>> = LazyLock::ne
11721172
Builtin::MaterializedView(&MZ_SYSTEM_PRIVILEGES),
11731173
Builtin::MaterializedView(&MZ_COMMENTS),
11741174
Builtin::Table(&MZ_WEBHOOKS_SOURCES),
1175+
Builtin::MaterializedView(&MZ_METRIC_SINKS),
11751176
Builtin::Table(&MZ_HISTORY_RETENTION_STRATEGIES),
11761177
Builtin::MaterializedView(&MZ_MATERIALIZED_VIEWS),
11771178
Builtin::Table(&MZ_MATERIALIZED_VIEW_REFRESH_STRATEGIES),
@@ -1419,6 +1420,7 @@ pub static BUILTINS_STATIC: LazyLock<Vec<Builtin<NameReference>>> = LazyLock::ne
14191420
Builtin::Index(&MZ_ROLES_IND),
14201421
Builtin::Index(&MZ_SOURCES_IND),
14211422
Builtin::Index(&MZ_SINKS_IND),
1423+
Builtin::Index(&MZ_METRIC_SINKS_IND),
14221424
Builtin::Index(&MZ_MATERIALIZED_VIEWS_IND),
14231425
Builtin::Index(&MZ_SOURCE_STATUSES_IND),
14241426
Builtin::Index(&MZ_SOURCE_STATUS_HISTORY_IND),

src/catalog/src/builtin/mz_catalog.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3455,7 +3455,7 @@ pub static MZ_OBJECTS: LazyLock<BuiltinView> = LazyLock::new(|| {
34553455
("oid", "A PostgreSQL-compatible OID for the object."),
34563456
("schema_id", "The ID of the schema to which the object belongs. Corresponds to `mz_schemas.id`."),
34573457
("name", "The name of the object."),
3458-
("type", "The type of the object: one of `table`, `source`, `view`, `materialized-view`, `sink`, `index`, `connection`, `secret`, `type`, or `function`."),
3458+
("type", "The type of the object: one of `table`, `source`, `view`, `materialized-view`, `sink`, `metric-sink`, `index`, `connection`, `secret`, `type`, or `function`."),
34593459
("owner_id", "The role ID of the owner of the object. Corresponds to `mz_roles.id`."),
34603460
("cluster_id", "The ID of the cluster maintaining the source, materialized view, index, or sink. Corresponds to `mz_clusters.id`. `NULL` for other object types."),
34613461
("privileges", "The privileges belonging to the object."),
@@ -3464,6 +3464,8 @@ pub static MZ_OBJECTS: LazyLock<BuiltinView> = LazyLock::new(|| {
34643464
"SELECT id, oid, schema_id, name, type, owner_id, cluster_id, privileges FROM mz_catalog.mz_relations
34653465
UNION ALL
34663466
SELECT id, oid, schema_id, name, 'sink', owner_id, cluster_id, NULL::mz_catalog.mz_aclitem[] FROM mz_catalog.mz_sinks
3467+
UNION ALL
3468+
SELECT id, oid, schema_id, name, 'metric-sink', owner_id, cluster_id, NULL::mz_catalog.mz_aclitem[] FROM mz_internal.mz_metric_sinks
34673469
UNION ALL
34683470
SELECT mz_indexes.id, mz_indexes.oid, mz_relations.schema_id, mz_indexes.name, 'index', mz_indexes.owner_id, mz_indexes.cluster_id, NULL::mz_catalog.mz_aclitem[]
34693471
FROM mz_catalog.mz_indexes

src/catalog/src/builtin/mz_internal.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3689,6 +3689,122 @@ pub static MZ_WEBHOOKS_SOURCES: LazyLock<BuiltinTable> = LazyLock::new(|| Builti
36893689
}),
36903690
});
36913691

3692+
pub static MZ_METRIC_SINKS: LazyLock<BuiltinMaterializedView> = LazyLock::new(|| {
3693+
BuiltinMaterializedView {
3694+
name: "mz_metric_sinks",
3695+
schema: MZ_INTERNAL_SCHEMA,
3696+
oid: oid::MV_MZ_METRIC_SINKS_OID,
3697+
desc: RelationDesc::builder()
3698+
.with_column("id", SqlScalarType::String.nullable(false))
3699+
.with_column("oid", SqlScalarType::Oid.nullable(false))
3700+
.with_column("schema_id", SqlScalarType::String.nullable(false))
3701+
.with_column("name", SqlScalarType::String.nullable(false))
3702+
.with_column("from_id", SqlScalarType::String.nullable(false))
3703+
.with_column("cluster_id", SqlScalarType::String.nullable(false))
3704+
.with_column("owner_id", SqlScalarType::String.nullable(false))
3705+
.with_key(vec![0])
3706+
.with_key(vec![1])
3707+
.finish(),
3708+
column_comments: BTreeMap::from_iter([
3709+
("id", "Materialize's unique ID for the metric sink."),
3710+
("oid", "A PostgreSQL-compatible OID for the metric sink."),
3711+
(
3712+
"schema_id",
3713+
"The ID of the schema to which the metric sink belongs. Corresponds to `mz_schemas.id`.",
3714+
),
3715+
("name", "The name of the metric sink."),
3716+
(
3717+
"from_id",
3718+
"The ID of the relation the metric sink reads. Corresponds to `mz_objects.id`.",
3719+
),
3720+
(
3721+
"cluster_id",
3722+
"The ID of the cluster maintaining the metric sink. Corresponds to `mz_clusters.id`.",
3723+
),
3724+
(
3725+
"owner_id",
3726+
"The role ID of the owner of the metric sink. Corresponds to `mz_roles.id`.",
3727+
),
3728+
]),
3729+
sql: "
3730+
IN CLUSTER mz_catalog_server
3731+
WITH (
3732+
ASSERT NOT NULL id,
3733+
ASSERT NOT NULL oid,
3734+
ASSERT NOT NULL schema_id,
3735+
ASSERT NOT NULL name,
3736+
ASSERT NOT NULL from_id,
3737+
ASSERT NOT NULL cluster_id,
3738+
ASSERT NOT NULL owner_id
3739+
) AS
3740+
SELECT
3741+
mz_internal.parse_catalog_id(data->'key'->'gid') AS id,
3742+
(data->'value'->>'oid')::oid AS oid,
3743+
mz_internal.parse_catalog_id(data->'value'->'schema_id') AS schema_id,
3744+
data->'value'->>'name' AS name,
3745+
parsed->>'from_id' AS from_id,
3746+
parsed->>'cluster_id' AS cluster_id,
3747+
mz_internal.parse_catalog_id(data->'value'->'owner_id') AS owner_id
3748+
FROM
3749+
mz_internal.mz_catalog_raw
3750+
CROSS JOIN LATERAL (
3751+
SELECT mz_internal.parse_catalog_create_sql(data->'value'->'definition'->'V1'->>'create_sql')
3752+
) AS l(parsed)
3753+
WHERE
3754+
data->>'kind' = 'Item' AND
3755+
parsed->>'type' = 'metric-sink'",
3756+
is_retained_metrics_object: false,
3757+
access: vec![PUBLIC_SELECT],
3758+
ontology: Some(Ontology {
3759+
entity_name: "metric-sink",
3760+
description: "A sink that exports metrics about a relation",
3761+
links: &const {
3762+
[
3763+
OntologyLink {
3764+
name: "in_schema",
3765+
target: "schema",
3766+
properties: LinkProperties::fk("schema_id", "id", Cardinality::ManyToOne),
3767+
},
3768+
OntologyLink {
3769+
name: "reads_relation",
3770+
target: "relation",
3771+
properties: LinkProperties::fk("from_id", "id", Cardinality::ManyToOne),
3772+
},
3773+
OntologyLink {
3774+
name: "runs_on_cluster",
3775+
target: "cluster",
3776+
properties: LinkProperties::fk("cluster_id", "id", Cardinality::ManyToOne),
3777+
},
3778+
OntologyLink {
3779+
name: "owned_by",
3780+
target: "role",
3781+
properties: LinkProperties::fk("owner_id", "id", Cardinality::ManyToOne),
3782+
},
3783+
]
3784+
},
3785+
column_semantic_types: &const {
3786+
[
3787+
("id", SemanticType::CatalogItemId),
3788+
("oid", SemanticType::OID),
3789+
("schema_id", SemanticType::SchemaId),
3790+
("from_id", SemanticType::CatalogItemId),
3791+
("cluster_id", SemanticType::ClusterId),
3792+
("owner_id", SemanticType::RoleId),
3793+
]
3794+
},
3795+
}),
3796+
}
3797+
});
3798+
3799+
pub const MZ_METRIC_SINKS_IND: BuiltinIndex = BuiltinIndex {
3800+
name: "mz_metric_sinks_ind",
3801+
schema: MZ_INTERNAL_SCHEMA,
3802+
oid: oid::INDEX_MZ_METRIC_SINKS_IND_OID,
3803+
sql: "IN CLUSTER mz_catalog_server
3804+
ON mz_internal.mz_metric_sinks (id)",
3805+
is_retained_metrics_object: false,
3806+
};
3807+
36923808
pub static MZ_HISTORY_RETENTION_STRATEGIES: LazyLock<BuiltinTable> = LazyLock::new(|| {
36933809
BuiltinTable {
36943810
name: "mz_history_retention_strategies",
@@ -3887,6 +4003,7 @@ pub static MZ_OBJECTS_ID_NAMESPACE_TYPES: LazyLock<BuiltinView> = LazyLock::new(
38874003
('materialized-view'),
38884004
('source'),
38894005
('sink'),
4006+
('metric-sink'),
38904007
('index'),
38914008
('connection'),
38924009
('type'),

src/pgrepr-consts/src/oid.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,3 +826,5 @@ pub const VIEW_MZ_OBJECT_GRAPH_EDGES_OID: u32 = 17116;
826826
pub const INDEX_MZ_OBJECT_GRAPH_EDGES_IND_OID: u32 = 17117;
827827
pub const VIEW_MZ_BUILTIN_TABLES_OID: u32 = 17118;
828828
pub const VIEW_MZ_BUILTIN_VIEWS_OID: u32 = 17119;
829+
pub const MV_MZ_METRIC_SINKS_OID: u32 = 17120;
830+
pub const INDEX_MZ_METRIC_SINKS_IND_OID: u32 = 17121;

src/sql-parser/src/ast/defs/statement.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3589,6 +3589,9 @@ pub enum ShowObjectType<T: AstInfo> {
35893589
Sink {
35903590
in_cluster: Option<T::ClusterName>,
35913591
},
3592+
MetricSink {
3593+
in_cluster: Option<T::ClusterName>,
3594+
},
35923595
Type,
35933596
Role,
35943597
Cluster,
@@ -3641,6 +3644,7 @@ impl<T: AstInfo> AstDisplay for ShowObjectsStatement<T> {
36413644
ShowObjectType::View => "VIEWS",
36423645
ShowObjectType::Source { .. } => "SOURCES",
36433646
ShowObjectType::Sink { .. } => "SINKS",
3647+
ShowObjectType::MetricSink { .. } => "METRIC SINKS",
36443648
ShowObjectType::Type => "TYPES",
36453649
ShowObjectType::Role => "ROLES",
36463650
ShowObjectType::Cluster => "CLUSTERS",
@@ -3681,6 +3685,7 @@ impl<T: AstInfo> AstDisplay for ShowObjectsStatement<T> {
36813685
ShowObjectType::MaterializedView { in_cluster }
36823686
| ShowObjectType::Index { in_cluster, .. }
36833687
| ShowObjectType::Sink { in_cluster }
3688+
| ShowObjectType::MetricSink { in_cluster }
36843689
| ShowObjectType::Source { in_cluster } => {
36853690
if let Some(cluster) = in_cluster {
36863691
f.write_str(" IN CLUSTER ");
@@ -3863,6 +3868,25 @@ impl<T: AstInfo> AstDisplay for ShowCreateSinkStatement<T> {
38633868
}
38643869
impl_display_t!(ShowCreateSinkStatement);
38653870

3871+
/// `SHOW [REDACTED] CREATE METRIC SINK <sink>`
3872+
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
3873+
pub struct ShowCreateMetricSinkStatement<T: AstInfo> {
3874+
pub metric_sink_name: T::ItemName,
3875+
pub redacted: bool,
3876+
}
3877+
3878+
impl<T: AstInfo> AstDisplay for ShowCreateMetricSinkStatement<T> {
3879+
fn fmt<W: fmt::Write>(&self, f: &mut AstFormatter<W>) {
3880+
f.write_str("SHOW ");
3881+
if self.redacted {
3882+
f.write_str("REDACTED ");
3883+
}
3884+
f.write_str("CREATE METRIC SINK ");
3885+
f.write_node(&self.metric_sink_name);
3886+
}
3887+
}
3888+
impl_display_t!(ShowCreateMetricSinkStatement);
3889+
38663890
/// `SHOW [REDACTED] CREATE INDEX <index>`
38673891
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
38683892
pub struct ShowCreateIndexStatement<T: AstInfo> {
@@ -5517,6 +5541,7 @@ pub enum ShowStatement<T: AstInfo> {
55175541
ShowCreateSource(ShowCreateSourceStatement<T>),
55185542
ShowCreateTable(ShowCreateTableStatement<T>),
55195543
ShowCreateSink(ShowCreateSinkStatement<T>),
5544+
ShowCreateMetricSink(ShowCreateMetricSinkStatement<T>),
55205545
ShowCreateIndex(ShowCreateIndexStatement<T>),
55215546
ShowCreateConnection(ShowCreateConnectionStatement<T>),
55225547
ShowCreateCluster(ShowCreateClusterStatement<T>),
@@ -5535,6 +5560,7 @@ impl<T: AstInfo> AstDisplay for ShowStatement<T> {
55355560
ShowStatement::ShowCreateSource(stmt) => f.write_node(stmt),
55365561
ShowStatement::ShowCreateTable(stmt) => f.write_node(stmt),
55375562
ShowStatement::ShowCreateSink(stmt) => f.write_node(stmt),
5563+
ShowStatement::ShowCreateMetricSink(stmt) => f.write_node(stmt),
55385564
ShowStatement::ShowCreateIndex(stmt) => f.write_node(stmt),
55395565
ShowStatement::ShowCreateConnection(stmt) => f.write_node(stmt),
55405566
ShowStatement::ShowCreateCluster(stmt) => f.write_node(stmt),

0 commit comments

Comments
 (0)