Skip to main content

mz_sql/plan/
plan_utils.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Helper code used throughout the planner.
11
12use std::fmt;
13
14use mz_repr::RelationDesc;
15
16use crate::ast::Ident;
17use crate::catalog::SessionCatalog;
18use crate::normalize;
19use crate::plan::PlanError;
20use crate::plan::query::SelectOptionExtracted;
21
22/// Renames the columns in `desc` with the names in `column_names` if
23/// `column_names` is non-empty.
24///
25/// Returns an error if the length of `column_names` is greater than the arity
26/// of `desc`.
27pub fn maybe_rename_columns(
28    context: impl fmt::Display,
29    desc: &mut RelationDesc,
30    column_names: &[Ident],
31) -> Result<(), PlanError> {
32    if column_names.len() > desc.typ().column_types.len() {
33        return Err(column_count_mismatch(&context, desc, column_names));
34    }
35
36    for (i, name) in column_names.iter().enumerate() {
37        *desc.get_name_mut(i) = normalize::column_name(name.clone());
38    }
39
40    Ok(())
41}
42
43/// Like [`maybe_rename_columns`], but requires the length of `column_names`,
44/// when non-empty, to match the arity of `desc` exactly.
45///
46/// The exactness requirement is lifted while re-planning a persisted catalog
47/// item, signaled by the `unsafe_enable_incomplete_view_column_lists` flag
48/// that `SystemVars::enable_for_item_parsing` force-enables during bootstrap.
49/// A view that an earlier version accepted with fewer names than columns must
50/// keep re-planning, or rehydration would turn a graceful planning error into
51/// a fatal bootstrap panic.
52pub fn maybe_rename_columns_exact(
53    catalog: &dyn SessionCatalog,
54    context: impl fmt::Display,
55    desc: &mut RelationDesc,
56    column_names: &[Ident],
57) -> Result<(), PlanError> {
58    if !column_names.is_empty()
59        && column_names.len() < desc.typ().column_types.len()
60        && !catalog
61            .system_vars()
62            .unsafe_enable_incomplete_view_column_lists()
63    {
64        return Err(column_count_mismatch(&context, desc, column_names));
65    }
66    maybe_rename_columns(context, desc, column_names)
67}
68
69fn column_count_mismatch(
70    context: &dyn fmt::Display,
71    desc: &RelationDesc,
72    column_names: &[Ident],
73) -> PlanError {
74    sql_err!(
75        "{0} definition names {1} column{2}, but {0} has {3} column{4}",
76        context,
77        column_names.len(),
78        if column_names.len() == 1 { "" } else { "s" },
79        desc.typ().column_types.len(),
80        if desc.typ().column_types.len() == 1 {
81            ""
82        } else {
83            "s"
84        },
85    )
86}
87
88/// Specifies the side of a join.
89///
90/// Intended for use in error messages.
91#[derive(Debug, Clone, Copy)]
92pub enum JoinSide {
93    /// The left side.
94    Left,
95    /// The right side.
96    Right,
97}
98
99impl fmt::Display for JoinSide {
100    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
101        match self {
102            JoinSide::Left => f.write_str("left"),
103            JoinSide::Right => f.write_str("right"),
104        }
105    }
106}
107
108/// Specifies a bundle of group size query hints.
109///
110/// This struct bridges from old to new syntax for group size query hints,
111/// making it easier to pass these hints along and make use of a group size
112/// hint configuration.
113#[derive(Debug, Default, Clone, Copy)]
114pub struct GroupSizeHints {
115    pub aggregate_input_group_size: Option<u64>,
116    pub distinct_on_input_group_size: Option<u64>,
117    pub limit_input_group_size: Option<u64>,
118}
119
120impl TryFrom<SelectOptionExtracted> for GroupSizeHints {
121    type Error = PlanError;
122
123    /// Creates group size hints from extracted `SELECT` `OPTIONS` validating that
124    /// either the old `EXPECTED GROUP SIZE` syntax was used or alternatively the
125    /// new syntax with `AGGREGATE INPUT GROUP SIZE`, `DISTINCT ON INPUT GROUP SIZE`,
126    /// and `LIMIT INPUT GROUP SIZE`. If the two syntax versions are mixed in the
127    /// same `OPTIONS` clause, an error is returned.[^1]
128    /// [^1]: <https://github.com/MaterializeInc/materialize/blob/main/doc/developer/design/20230829_topk_size_hint.md>
129    fn try_from(select_option_extracted: SelectOptionExtracted) -> Result<Self, Self::Error> {
130        let SelectOptionExtracted {
131            expected_group_size,
132            aggregate_input_group_size,
133            distinct_on_input_group_size,
134            limit_input_group_size,
135            ..
136        } = select_option_extracted;
137        if expected_group_size.is_some()
138            && (aggregate_input_group_size.is_some()
139                || distinct_on_input_group_size.is_some()
140                || limit_input_group_size.is_some())
141        {
142            Err(PlanError::InvalidGroupSizeHints)
143        } else {
144            let aggregate_input_group_size = aggregate_input_group_size.or(expected_group_size);
145            let distinct_on_input_group_size = distinct_on_input_group_size.or(expected_group_size);
146            let limit_input_group_size = limit_input_group_size.or(expected_group_size);
147            Ok(GroupSizeHints {
148                aggregate_input_group_size,
149                distinct_on_input_group_size,
150                limit_input_group_size,
151            })
152        }
153    }
154}