Skip to content

Commit 2599599

Browse files
committed
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. Creating one required ownership of the `FROM` relation, stricter than any other sink. 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`. Create now needs `CREATE` on the schema, `CREATE` on the cluster, and read on the `FROM` relation, the same shape as `CREATE SINK`. You need read on it, not ownership. 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. 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, the audit rows, and privileges. - The restart platform check reads `SHOW METRIC SINKS` instead of probing with a `CREATE` expected to fail.
1 parent a503c81 commit 2599599

26 files changed

Lines changed: 617 additions & 101 deletions

File tree

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

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

703+
<!-- RELATION_SPEC_UNDOCUMENTED mz_internal.mz_metric_sinks -->
704+
703705
## `mz_notices`
704706

705707
{{< public-preview />}}

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

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,24 +52,11 @@ def manipulate(self) -> list[Testdrive]:
5252
]
5353

5454
def validate(self) -> Testdrive:
55-
# Recreating a sink that is already there is how we probe for it without
56-
# mutating anything: metric sinks are not in `mz_objects` yet, and
57-
# `mz_catalog_raw` needs a system connection.
58-
#
59-
# TODO(SQL-572): once metric sinks join `mz_objects` (and the
60-
# `mz_metric_sinks` builtin view), probe survival by SELECTing the sink
61-
# row instead of re-issuing CREATE and matching "already exists". The
62-
# current probe is a proxy: it confirms the catalog item was re-parsed on
63-
# boot without needing a system connection.
6455
return Testdrive(dedent("""
65-
! CREATE METRIC SINK metric_sink_one IN CLUSTER quickstart FROM metric_sink_view
66-
contains:metric sink "materialize.public.metric_sink_one" already exists
67-
68-
! CREATE METRIC SINK metric_sink_two IN CLUSTER quickstart FROM metric_sink_view
69-
contains:metric sink "materialize.public.metric_sink_two" already exists
70-
71-
! CREATE METRIC SINK metric_sink_three IN CLUSTER quickstart FROM metric_sink_view
72-
contains:metric sink "materialize.public.metric_sink_three" already exists
56+
> SHOW METRIC SINKS
57+
metric_sink_one metric_sink_view quickstart
58+
metric_sink_three metric_sink_view quickstart
59+
metric_sink_two metric_sink_view quickstart
7360
7461
# The FROM edge came back too, so the view is still pinned.
7562
! DROP VIEW metric_sink_view

src/adapter/src/catalog/builtin_table_updates.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@ impl CatalogState {
201201
CatalogItem::Func(func) => {
202202
self.pack_func_update(id, schema_id, name, owner_id, func, diff)
203203
}
204+
// `mz_metric_sinks` is a materialized view derived from
205+
// `mz_catalog_raw`, so metric sinks emit no builtin-table row here.
204206
CatalogItem::Log(_) | CatalogItem::Secret(_) | CatalogItem::MetricSink(_) => vec![],
205207
// Connection details (mz_kafka_connections, mz_ssh_tunnel_connections,
206208
// mz_aws_connections, mz_aws_privatelink_connections) are now derived

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,17 @@ static MIGRATIONS: LazyLock<Vec<MigrationStep>> = LazyLock::new(|| {
355355
MZ_CATALOG_SCHEMA,
356356
"mz_aws_privatelink_connections",
357357
),
358+
// `mz_audit_events` learned the `metric-sink` object type. Its `object_type`
359+
// column carries `ASSERT NOT NULL`, so without the new CASE arm a single
360+
// metric-sink audit event errors the whole collection. See the NOTE above:
361+
// this version must stay at the workspace's current dev version until the
362+
// change ships.
363+
MigrationStep::replacement(
364+
"26.37.0-dev.0",
365+
CatalogItemType::MaterializedView,
366+
MZ_CATALOG_SCHEMA,
367+
"mz_audit_events",
368+
),
358369
]
359370
});
360371

src/catalog/src/builtin.rs

Lines changed: 1 addition & 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),

src/catalog/src/builtin/mz_catalog.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2731,6 +2731,7 @@ SELECT
27312731
WHEN '16' THEN 'system'
27322732
WHEN '17' THEN 'continual-task'
27332733
WHEN '18' THEN 'network-policy'
2734+
WHEN '19' THEN 'metric-sink'
27342735
END AS object_type,
27352736
mz_internal.parse_catalog_audit_log_details(e->'details') AS details,
27362737
e->'user'->>'inner' AS \"user\",
@@ -3246,6 +3247,8 @@ pub static MZ_OBJECTS: LazyLock<BuiltinView> = LazyLock::new(|| {
32463247
"SELECT id, oid, schema_id, name, type, owner_id, cluster_id, privileges FROM mz_catalog.mz_relations
32473248
UNION ALL
32483249
SELECT id, oid, schema_id, name, 'sink', owner_id, cluster_id, NULL::mz_catalog.mz_aclitem[] FROM mz_catalog.mz_sinks
3250+
UNION ALL
3251+
SELECT id, oid, schema_id, name, 'metric-sink', owner_id, cluster_id, NULL::mz_catalog.mz_aclitem[] FROM mz_internal.mz_metric_sinks
32493252
UNION ALL
32503253
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[]
32513254
FROM mz_catalog.mz_indexes

src/catalog/src/builtin/mz_internal.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3687,6 +3687,113 @@ pub static MZ_WEBHOOKS_SOURCES: LazyLock<BuiltinTable> = LazyLock::new(|| Builti
36873687
}),
36883688
});
36893689

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

src/pgrepr-consts/src/oid.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,3 +822,4 @@ pub const FUNC_PARSE_CONNECTION_DETAILS_OID: u32 = 17112;
822822
pub const FUNC_MZ_AWS_ACCOUNT_ID_OID: u32 = 17113;
823823
pub const FUNC_MZ_AWS_EXTERNAL_ID_PREFIX_OID: u32 = 17114;
824824
pub const FUNC_MZ_AWS_CONNECTION_ROLE_ARN_OID: u32 = 17115;
825+
pub const MV_MZ_METRIC_SINKS_OID: u32 = 17116;

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3544,6 +3544,9 @@ pub enum ShowObjectType<T: AstInfo> {
35443544
Sink {
35453545
in_cluster: Option<T::ClusterName>,
35463546
},
3547+
MetricSink {
3548+
in_cluster: Option<T::ClusterName>,
3549+
},
35473550
Type,
35483551
Role,
35493552
Cluster,
@@ -3596,6 +3599,7 @@ impl<T: AstInfo> AstDisplay for ShowObjectsStatement<T> {
35963599
ShowObjectType::View => "VIEWS",
35973600
ShowObjectType::Source { .. } => "SOURCES",
35983601
ShowObjectType::Sink { .. } => "SINKS",
3602+
ShowObjectType::MetricSink { .. } => "METRIC SINKS",
35993603
ShowObjectType::Type => "TYPES",
36003604
ShowObjectType::Role => "ROLES",
36013605
ShowObjectType::Cluster => "CLUSTERS",
@@ -3636,6 +3640,7 @@ impl<T: AstInfo> AstDisplay for ShowObjectsStatement<T> {
36363640
ShowObjectType::MaterializedView { in_cluster }
36373641
| ShowObjectType::Index { in_cluster, .. }
36383642
| ShowObjectType::Sink { in_cluster }
3643+
| ShowObjectType::MetricSink { in_cluster }
36393644
| ShowObjectType::Source { in_cluster } => {
36403645
if let Some(cluster) = in_cluster {
36413646
f.write_str(" IN CLUSTER ");
@@ -3818,6 +3823,25 @@ impl<T: AstInfo> AstDisplay for ShowCreateSinkStatement<T> {
38183823
}
38193824
impl_display_t!(ShowCreateSinkStatement);
38203825

3826+
/// `SHOW [REDACTED] CREATE METRIC SINK <sink>`
3827+
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
3828+
pub struct ShowCreateMetricSinkStatement<T: AstInfo> {
3829+
pub metric_sink_name: T::ItemName,
3830+
pub redacted: bool,
3831+
}
3832+
3833+
impl<T: AstInfo> AstDisplay for ShowCreateMetricSinkStatement<T> {
3834+
fn fmt<W: fmt::Write>(&self, f: &mut AstFormatter<W>) {
3835+
f.write_str("SHOW ");
3836+
if self.redacted {
3837+
f.write_str("REDACTED ");
3838+
}
3839+
f.write_str("CREATE METRIC SINK ");
3840+
f.write_node(&self.metric_sink_name);
3841+
}
3842+
}
3843+
impl_display_t!(ShowCreateMetricSinkStatement);
3844+
38213845
/// `SHOW [REDACTED] CREATE INDEX <index>`
38223846
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
38233847
pub struct ShowCreateIndexStatement<T: AstInfo> {
@@ -5472,6 +5496,7 @@ pub enum ShowStatement<T: AstInfo> {
54725496
ShowCreateSource(ShowCreateSourceStatement<T>),
54735497
ShowCreateTable(ShowCreateTableStatement<T>),
54745498
ShowCreateSink(ShowCreateSinkStatement<T>),
5499+
ShowCreateMetricSink(ShowCreateMetricSinkStatement<T>),
54755500
ShowCreateIndex(ShowCreateIndexStatement<T>),
54765501
ShowCreateConnection(ShowCreateConnectionStatement<T>),
54775502
ShowCreateCluster(ShowCreateClusterStatement<T>),
@@ -5490,6 +5515,7 @@ impl<T: AstInfo> AstDisplay for ShowStatement<T> {
54905515
ShowStatement::ShowCreateSource(stmt) => f.write_node(stmt),
54915516
ShowStatement::ShowCreateTable(stmt) => f.write_node(stmt),
54925517
ShowStatement::ShowCreateSink(stmt) => f.write_node(stmt),
5518+
ShowStatement::ShowCreateMetricSink(stmt) => f.write_node(stmt),
54935519
ShowStatement::ShowCreateIndex(stmt) => f.write_node(stmt),
54945520
ShowStatement::ShowCreateConnection(stmt) => f.write_node(stmt),
54955521
ShowStatement::ShowCreateCluster(stmt) => f.write_node(stmt),

src/sql-parser/src/parser.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8301,6 +8301,10 @@ impl<'a> Parser<'a> {
83018301
let in_cluster = self.parse_optional_in_cluster()?;
83028302
ShowObjectType::Sink { in_cluster }
83038303
}
8304+
ObjectType::MetricSink => {
8305+
let in_cluster = self.parse_optional_in_cluster()?;
8306+
ShowObjectType::MetricSink { in_cluster }
8307+
}
83048308
ObjectType::Type => ShowObjectType::Type,
83058309
ObjectType::Role => ShowObjectType::Role,
83068310
ObjectType::ClusterReplica => ShowObjectType::ClusterReplica,
@@ -8333,7 +8337,7 @@ impl<'a> Parser<'a> {
83338337
on_object,
83348338
}
83358339
}
8336-
ObjectType::Func | ObjectType::MetricSink => {
8340+
ObjectType::Func => {
83378341
return parser_err!(
83388342
self,
83398343
self.peek_prev_pos(),
@@ -8393,6 +8397,13 @@ impl<'a> Parser<'a> {
83938397
sink_name: self.parse_raw_name()?,
83948398
redacted,
83958399
}))
8400+
} else if self.parse_keywords(&[CREATE, METRIC, SINK]) {
8401+
Ok(ShowStatement::ShowCreateMetricSink(
8402+
ShowCreateMetricSinkStatement {
8403+
metric_sink_name: self.parse_raw_name()?,
8404+
redacted,
8405+
},
8406+
))
83968407
} else if self.parse_keywords(&[CREATE, INDEX]) {
83978408
Ok(ShowStatement::ShowCreateIndex(ShowCreateIndexStatement {
83988409
index_name: self.parse_raw_name()?,
@@ -10132,6 +10143,7 @@ impl<'a> Parser<'a> {
1013210143
MATERIALIZED,
1013310144
SOURCES,
1013410145
SINKS,
10146+
METRIC,
1013510147
INDEXES,
1013610148
TYPES,
1013710149
ROLES,
@@ -10157,6 +10169,14 @@ impl<'a> Parser<'a> {
1015710169
}
1015810170
SOURCES => ObjectType::Source,
1015910171
SINKS => ObjectType::Sink,
10172+
METRIC => {
10173+
if self.parse_keyword(SINKS) {
10174+
ObjectType::MetricSink
10175+
} else {
10176+
self.prev_token();
10177+
return None;
10178+
}
10179+
}
1016010180
INDEXES => ObjectType::Index,
1016110181
TYPES => ObjectType::Type,
1016210182
ROLES | USERS => ObjectType::Role,

0 commit comments

Comments
 (0)