Skip to content

catalog: convert the connection-detail tables to materialized views (SQL-497) - #37725

Open
mtabebe wants to merge 3 commits into
MaterializeInc:mainfrom
mtabebe:ma/convert-builtins/sql-497-connection-details
Open

catalog: convert the connection-detail tables to materialized views (SQL-497)#37725
mtabebe wants to merge 3 commits into
MaterializeInc:mainfrom
mtabebe:ma/convert-builtins/sql-497-connection-details

Conversation

@mtabebe

@mtabebe mtabebe commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Converts the four connection-detail builtin tables to materialized views over mz_catalog_raw, so they are derived from each connection's persisted create_sql instead of being written by a Rust packer on every catalog change on the leader environmentd (the SQL-118 / multi-envd motivation).

  • mz_kafka_connections
  • mz_ssh_tunnel_connections
  • mz_aws_connections
  • mz_aws_privatelink_connections

Do this in two parts:

  1. kafka + ssh — adds the parse_connection_details SQL helper and flips both tables to MVs. The kafka progress-topic default is reconstructed in SQL to match KafkaConnection::progress_topic.
  2. aws + aws-privatelink — extends the helper with an aws branch and adds three plan-time fold functions (mz_aws_account_id, mz_aws_external_id_prefix, mz_aws_connection_role_arn) for the values that come from environment context rather than create_sql. The aws view reconstructs the assume-role principal, external id, and trust policy. The privatelink view reconstructs the principal ARN and keeps WHERE principal IS NOT NULL, so it emits no row when the AWS principal context is absent, matching the packer it replaces.

RBAC note. The three new fold functions are deliberately not gated by restrict_to_user_objects, exactly like mz_environment_id.

Testing:

  • parse_connection_details unit tests cover every connection kind and branch plus the null cases.
  • New lockdown SLTs pin the column shape, nullability, and wrong-type exclusion for each view. The mz_analytics builtin connection doubles as the SQL/Rust parity check for the aws assume-role columns.

@mtabebe
mtabebe force-pushed the ma/convert-builtins/sql-497-connection-details branch from 48e66e8 to e2243b3 Compare July 20, 2026 20:22
@mtabebe
mtabebe force-pushed the ma/convert-builtins/sql-497-connection-details branch from e2243b3 to 8253589 Compare August 3, 2026 20:59
@mtabebe
mtabebe marked this pull request as ready for review August 4, 2026 13:19
@mtabebe
mtabebe requested review from a team and sjwiesman as code owners August 4, 2026 13:19
@mtabebe
mtabebe requested review from aljoscha and ggevay and removed request for a team and sjwiesman August 4, 2026 13:19
mtabebe and others added 2 commits August 4, 2026 10:20
…views

Problem:
mz_kafka_connections and mz_ssh_tunnel_connections were populated by Rust
packers (pack_kafka_connection_update and pack_ssh_tunnel_connection_update)
that ran on every catalog change on the leader environmentd.

Solution:
Add parse_connection_details, a connection-type-agnostic SQL helper that pulls
the broker addresses and any explicit progress topic (kafka) or the two public
keys (ssh) out of a connection's persisted create_sql. It returns jsonb null
for anything that is not one of those connection types, so callers filter on
IS NOT NULL and gate on the connection type separately, the way mz_connections
already does.

Convert both tables to BuiltinMaterializedView over mz_internal.mz_catalog_raw.
Each view parses create_sql with the helper and reconstructs the default kafka
progress topic (_materialize-progress-<env>-<conn>) in SQL, matching
KafkaConnection::progress_topic. The two packers and their dispatch arms are
removed, along with the now-unused imports.

Testing:
- Unit tests in jsonb.rs cover kafka (single broker, broker list, explicit and
  defaulted progress topic), ssh, a connection type without a detail view, and
  a non-connection statement.
- New lockdown SLTs (mz_kafka_connections.slt, mz_ssh_tunnel_connections.slt)
  pin the column shape and nullability, confirm the views are empty on a fresh
  env, check the progress-topic default against its format string, and verify
  the wrong connection type is excluded.
- connection-create-drop.td and test/ssh-connection/* query these views
  unchanged.
- Regenerated oid.slt, information_schema_tables.slt, catalog_server_explain.slt,
  mz_catalog_server_index_accounting.slt, and catalog.td.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erialized views

Problem:
mz_aws_connections and mz_aws_privatelink_connections were populated by Rust
packers (pack_aws_connection_update and pack_aws_privatelink_connection_update)
that ran on every catalog change on the leader environmentd.

These connections carry values that are not in create_sql: the AWS principal,
external id, and example trust policy for an assume-role aws connection, and
the privatelink principal ARN. All of these derive from environment context
(the AWS account id, external-id prefix, and connection role ARN), not from the
connection definition.

Solution:
Extend parse_connection_details with an aws branch that pulls the eight
create_sql-derived fields (endpoint, region, the inline/secret credentials, and
the assume-role options) out of the connection's persisted create_sql. A secret
reference resolves to the referenced secret's catalog id.

Expose the three environment-context values that the views need through nullary
functions that fold to a literal at plan time, the same mechanism
mz_environment_id uses: mz_aws_account_id, mz_aws_external_id_prefix, and
mz_aws_connection_role_arn. The account id is plumbed onto CatalogConfig (the
external-id prefix and role ARN already live on its ConnectionContext). Each
folds to NULL on an environment without the corresponding context. Like
mz_environment_id, they are deliberately not gated by restrict_to_user_objects,
because a fold cannot be gated meaningfully. The system connection views stay
blocked for restricted sessions because they are system relations.

Convert both tables to BuiltinMaterializedView. The aws view reconstructs the
assume-role principal, external id, and trust policy from the context functions,
matching AwsAssumeRole::external_id / example_trust_policy. The privatelink view
reconstructs the principal ARN, matching AwsPrincipalContext::to_principal_string,
and keeps WHERE principal IS NOT NULL so it emits no row when the AWS principal
context is absent, matching the packer that skipped the row in that case. The
two packers, their dispatch, and now-unused imports are removed.

Testing:
- Unit tests in jsonb.rs cover the aws branch: inline access key, all-secret
  credentials with a session token, and assume-role.
- New lockdown SLTs (mz_aws_connections.slt, mz_aws_privatelink_connections.slt)
  pin the column shape and nullability and the wrong-type exclusion. The
  sqllogictest env configures an AWS context, so the built-in mz_analytics
  assume-role connection doubles as the SQL<->Rust parity check for principal,
  external_id, and example_trust_policy. Privatelink has no context there, so
  the view is empty, matching the packer's skip.
- Regenerated oid.slt, information_schema_tables.slt, catalog_server_explain.slt,
  mz_catalog_server_index_accounting.slt, and catalog.td.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mtabebe
mtabebe force-pushed the ma/convert-builtins/sql-497-connection-details branch from 8253589 to efed4ce Compare August 4, 2026 14:20

@ggevay ggevay left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments:

Comment thread src/expr/src/scalar/func/impls/jsonb.rs Outdated
name: ConnectionOptionName,
) -> Option<String> {
values.iter().find_map(|o| match &o.value {
Some(WithOptionValue::Value(Value::String(s))) if o.name == name => Some(s.clone()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old packer read planned values, and planning accepts these option values in more forms than Value::String: the blanket impl in src/sql/src/plan/with_options.rs (impl<V: TryFromValue<Value>, ...> TryFromValue<WithOptionValue<T>> for V) also converts Ident and 1-part UnresolvedItemName to strings, and neither normalization nor AstDisplay canonicalizes them, so unquoted values persist unquoted. In-tree proof: the SHOW CREATE golden in test/testdrive/connection-create-drop.td:62 shows a persisted SECURITY PROTOCOL = plaintext. So CREATE CONNECTION ... (PROGRESS TOPIC = my_topic) plans fine, but this helper returns null for it, and the view then silently reports the default progress topic instead of the real one. Unquoted aws options (e.g. ASSUME ROLE SESSION NAME) similarly come out NULL.

Could you match WithOptionValue::Ident and 1-part WithOptionValue::UnresolvedItemName here too, mirroring the blanket impl? Two related suggestions:

  • Consider making this a shared helper used by all the catalog-raw parsers. The already-merged parse_kafka_source_details (catalog: convert mz_postgres_sources and mz_kafka_sources to materialized views #37465) has the identical gap, where it is worse: an unquoted TOPIC = t makes the whole mz_kafka_sources view error ("missing TOPIC option"), and an unquoted GROUP ID PREFIX silently drops the prefix from the reconstructed group id. A shared helper would fix both and keep the next conversion from reintroducing the class.
  • Longer-term: it would be great to make this parity executable, in the spirit of the mz_audit_events round-trip test, e.g. comparing parser output against planned state for the statements the test corpus creates. Both instances of this bug would have been caught mechanically while the packer (the oracle) still existed.

Minor, same neighborhood: planning also coerces ENDPOINT = '' to None (see the TODO in plan/statement/ddl/connection.rs), so the packer emitted NULL where the view now yields ''. Probably fine to just fold the same coercion into the helper or note it.

Comment thread src/pgrepr-consts/src/oid.rs Outdated
pub const MV_MZ_SQL_SERVER_SOURCE_TABLES_OID: u32 = 17109;
pub const MV_MZ_KAFKA_SOURCE_TABLES_OID: u32 = 17110;
pub const FUNC_PARSE_SOURCE_EXPORT_DETAILS_OID: u32 = 17111;
pub const MV_MZ_KAFKA_CONNECTIONS_OID: u32 = 17112;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We agreed on the opposite convention in #36912 (comment): reuse the old OIDs and just rename the constant (seven of the eight conversions before that had done so), and you even realigned mz_indexes (17088 back to 16705) in that PR for consistency: #36912 (comment). Then #37679 minted new OIDs, and my comment there somehow got missed (#37679 (comment)). This PR now also makes the same mistake.

Please rename in place here (TABLE_MZ_*_CONNECTIONS_OID values 16695/16709/16756/16757), and realign #37679's four mz_*_source_tables OIDs (17107-17110 back to 16948/16949/17057/17025) in this PR or a follow-up. #37679 merged after the 26.36 release cut, so neither PR's new OIDs have shipped in any release and the realignment is invisible to users. (It would be safe even post-release: builtin OIDs are compile-time constants, not part of the fingerprint or durable state, and realigning restores the values every released version has had.)

(Or if there is a reason to deviate from the agreed convention, please say so explicitly and remove the dead consts instead.)

Comment thread src/catalog/src/builtin/mz_catalog.rs Outdated
) AS d(details)
WHERE
r.data->>'kind' = 'Item' AND
details IS NOT NULL AND

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: details IS NOT NULL reads like it excludes the non-kafka items, but the helper returns jsonb null for those, which passes SQL IS NOT NULL. The connection_type filter does the real work. The predicate is also fully redundant: when the create_sql path is SQL NULL, connection_type is SQL NULL too and the type filter already drops the row. Suggest removing it (or replacing it with a comment), in all three views here. Same pattern as discussed on the #37679 review.

@ggevay

ggevay commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Btw., it looks like our review comments on #37679 somehow got missed when that PR merged. Recap so they don't get lost:

@mtabebe

mtabebe commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Ok, I've pushed an update. I'm not sure what happened with the other PR. I had definitely made the changes locally and committed them. I thought I had pushed them but I guess I didn't. My mistake.

Regarding not following the feedback. My bad, I had stacked this PR on top of the other one, and should have updated it based on the feedback. Sorry 😞

Thanks for taking the time to look.

@aljoscha aljoscha left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did take a peek at this one, but maybe not as thoroughly a Gábor. Looks quite nice! The one thing that made me ponder is the AWS/injection stuff. Looks complicated, but there's a probably a good reason. Why do we need that again?

@ggevay

ggevay commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

No problem, it happens!

CdcV2 => "materialize",
})
}
None if source_type == "kafka" => Some("none"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This default also fires for new-syntax kafka sources (no FORMAT/ENVELOPE on the source, envelopes defined per-table), which currently report envelope_type NULL — was that flip intended? The history is murky in both directions: until d38735c (2025-09) the packer reported 'none' for them, but only because the planner built a hardcoded placeholder primary_export for a collection that "will not output" anything (the code there said so, with a TODO to remove the field). Since that commit the packer returned NULL (DataSourceDesc::Ingestion { .. } => None), and the current view preserves that, so 'none' here changes about a year of released behavior.

NULL seems preferable to us: the new-syntax source's own relation is the progress relation, nothing is ingested into it with any envelope, and envelope_type = 'none' on a source whose tables use ENVELOPE UPSERT would read oddly. The planner's own old-vs-new-syntax discriminator is the progress subsource (plan_create_source builds OldSyntaxIngestion iff progress_subsource is present, and durable old-syntax create_sql always carries EXPOSE PROGRESS AS), so gating on stmt.progress_subsource.is_some() reproduces the current behavior exactly. If you take the gate, catalog_kafka_source_omitted_envelope_defaults_none's statement should gain EXPOSE PROGRESS AS (that's the realistic persisted shape) plus a new-syntax case asserting the field stays absent. Either way, could you update the envelope_type column comment to spell out the chosen behavior for table-based kafka sources?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good feedback, I'll gate the 'none' default on stmt.progress_subsource.is_some() so only old-syntax sources get it and new-syntax stays NULL

@mtabebe

mtabebe commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

I did take a peek at this one, but maybe not as thoroughly a Gábor. Looks quite nice! The one thing that made me ponder is the AWS/injection stuff. Looks complicated, but there's a probably a good reason. Why do we need that again?

The reason is the AWS columns came from the environment ConnectionContext which the packer could pull and fill in. But we don't have that with the materialized view anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants