Skip to main content

rupg_analyze/ddl/
reindex.rs

1//! `REINDEX`, as `ExecReindex`, `ReindexIndex`, `ReindexTable`, `ReindexPartitions`, `ReindexMultipleTables`, `ReindexMultipleInternal` and `ReindexRelationConcurrently` of `indexcmds.c` and `reindex_relation` and `reindex_index` of `index.c` run it. rupg builds the entries of an index from the rows of its table, so `REINDEX` gives each index that it builds again a new file, `relfilenode`, and makes the index of a failed `CREATE INDEX CONCURRENTLY` valid and ready. `REINDEX CONCURRENTLY` keeps the OID of an index, where PostgreSQL gives the new index a new OID and swaps the names. An index of the built-in catalog keeps its file, and `VERBOSE` shows no message for it.
2//!
3//! The session runs the parts of `ExecReindex` and `ReindexMultipleTables` that read the state of the session before [`define`](super::define) runs the statement: [`reindex_options`], `PreventInTransactionBlock` of `CONCURRENTLY`, `SCHEMA`, `SYSTEM` and `DATABASE`, the tablespace of `TABLESPACE`, and the checks of the database of `SYSTEM` and `DATABASE`.
4
5use rupg_catalog::{FIRST_UNPINNED_OID, RelKind};
6use rupg_common::{Error, Level, Result, SqlState};
7use rupg_pgcatalog::builtin;
8use rupg_sql::nodes::{Node, RangeVar, ReindexObjectType, ReindexStmt};
9
10use super::reloptions::def_boolean;
11use super::view::def_string;
12use super::{Definer, Found, Message, TopLevel, relname};
13use crate::coerce::AtOpt;
14use crate::typename::place;
15
16/// The detail of `pg_rusage_show` in a message of `VERBOSE`. rupg does not measure the time of the statement.
17const RUSAGE: &str = "CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s";
18
19/// The options of `REINDEX`, as `ExecReindex` reads them from the option list.
20#[derive(Clone, Debug, Default, PartialEq, Eq)]
21pub struct ReindexOptions {
22    /// `VERBOSE`.
23    pub verbose: bool,
24    /// `CONCURRENTLY`.
25    pub concurrently: bool,
26    /// The name of the tablespace of `TABLESPACE`.
27    pub tablespace: Option<String>,
28}
29
30/// The option list of `ExecReindex`.
31///
32/// # Errors
33///
34/// `42601` for an option that `REINDEX` does not have, at the option, and for a value that is not a Boolean value.
35pub fn reindex_options(stmt: &ReindexStmt) -> Result<ReindexOptions> {
36    let mut options = ReindexOptions::default();
37    for node in stmt.params.iter().flatten() {
38        let Node::DefElem(def) = node else { continue };
39        let name = def.defname.as_deref().unwrap_or_default();
40        match name {
41            "verbose" => options.verbose = def_boolean(name, def.arg.as_ref())?,
42            "concurrently" => options.concurrently = def_boolean(name, def.arg.as_ref())?,
43            "tablespace" => options.tablespace = Some(def_string(def.arg.as_ref())),
44            _ => {
45                return Err(Error::new(
46                    SqlState::SYNTAX_ERROR,
47                    format!("unrecognized REINDEX option \"{name}\""),
48                )
49                .at_opt(place(def.location)));
50            }
51        }
52    }
53    Ok(options)
54}
55
56/// The error of `ReindexRelationConcurrently` and `ReindexMultipleTables` for a catalog.
57fn system_concurrently() -> Error {
58    Error::new(SqlState::FEATURE_NOT_SUPPORTED, "cannot reindex system catalogs concurrently")
59}
60
61/// `IsCatalogRelationOid`: true for a relation of `initdb` with an OID below `FirstUnpinnedObjectId`.
62fn is_catalog(oid: u32) -> bool {
63    oid < FIRST_UNPINNED_OID
64}
65
66/// An `INFO` of `VERBOSE`, with the time of `pg_rusage_show` as the detail when `detail` is not `None`.
67fn info(message: String, detail: Option<String>) -> Message {
68    let error = Error::new(SqlState::SUCCESSFUL_COMPLETION, message).with_level(Level::Info);
69    Message::Notice(match detail {
70        Some(detail) => error.with_detail(detail),
71        None => error,
72    })
73}
74
75impl Definer<'_, '_> {
76    /// `ExecReindex` after the checks of the session: `ReindexIndex`, `ReindexTable` or `ReindexMultipleTables`.
77    pub(super) fn reindex(&mut self, stmt: &ReindexStmt) -> Result<()> {
78        let options = reindex_options(stmt)?;
79        let relation = stmt.relation.as_deref();
80        match (stmt.kind, relation) {
81            (ReindexObjectType::REINDEX_OBJECT_INDEX, Some(rv)) => {
82                self.reindex_index_of(rv, &options)
83            }
84            (ReindexObjectType::REINDEX_OBJECT_TABLE, Some(rv)) => {
85                self.reindex_table_of(rv, &options)
86            }
87            (
88                ReindexObjectType::REINDEX_OBJECT_INDEX | ReindexObjectType::REINDEX_OBJECT_TABLE,
89                None,
90            ) => Err(Error::internal("REINDEX with no relation")),
91            (kind, _) => self.reindex_multiple(kind, stmt.name.as_deref(), &options),
92        }
93    }
94
95    /// `ReindexIndex`: the checks of `RangeVarCallbackForReindexIndex`, then `ReindexPartitions` for a partitioned index, `ReindexRelationConcurrently` for `CONCURRENTLY` on an index that is not temporary, or `reindex_index`.
96    fn reindex_index_of(&mut self, rv: &RangeVar, options: &ReindexOptions) -> Result<()> {
97        let found = self.lookup_relation(rv)?;
98        let name = relname(rv);
99        let kind = self.kind_of(found);
100        if !matches!(kind, b'i' | b'I') {
101            return Err(Error::new(
102                SqlState::WRONG_OBJECT_TYPE,
103                format!("\"{name}\" is not an index"),
104            ));
105        }
106        let index = match found {
107            Found::User(oid) => oid,
108            Found::Builtin(row) => row.oid,
109        };
110        let table = self.index_table(index).unwrap_or(index);
111        if !self.may_maintain(table) {
112            return Err(Error::new(
113                SqlState::INSUFFICIENT_PRIVILEGE,
114                format!("permission denied for index {name}"),
115            ));
116        }
117        if let Found::Builtin(row) = found {
118            // The tables of the built-in indexes are catalogs.
119            if options.concurrently && is_catalog(row.oid) {
120                return Err(system_concurrently());
121            }
122            return Ok(());
123        }
124        if kind == b'I' {
125            let context = format!(
126                "while reindexing partitioned index \"{}\"",
127                self.qualified_relation(index)
128            );
129            self.top_level = Some(TopLevel { statement: "REINDEX INDEX", context });
130            let leaves = self.leaf_indexes(index);
131            self.reindex_each(&leaves, options);
132            return Ok(());
133        }
134        if options.concurrently && !self.is_temp(index) {
135            self.reindex_concurrently(&[index], options.verbose, true);
136        } else {
137            self.reindex_one(index, options.verbose);
138        }
139        Ok(())
140    }
141
142    /// `ReindexTable`: the checks of `RangeVarCallbackMaintainsTable`, then `ReindexPartitions` for a partitioned table, `ReindexRelationConcurrently` for `CONCURRENTLY` on a table that is not temporary, or `reindex_relation`, with a notice when the table has no index.
143    fn reindex_table_of(&mut self, rv: &RangeVar, options: &ReindexOptions) -> Result<()> {
144        let found = self.lookup_relation(rv)?;
145        let name = relname(rv);
146        let kind = self.kind_of(found);
147        if !matches!(kind, b'r' | b't' | b'm' | b'p') {
148            return Err(Error::new(
149                SqlState::WRONG_OBJECT_TYPE,
150                format!("\"{name}\" is not a table or materialized view"),
151            ));
152        }
153        let table = match found {
154            Found::User(oid) => oid,
155            Found::Builtin(row) => row.oid,
156        };
157        if !self.may_maintain(table) {
158            let shown = if kind == b'm' { "materialized view" } else { "table" };
159            return Err(Error::new(
160                SqlState::INSUFFICIENT_PRIVILEGE,
161                format!("permission denied for {shown} {name}"),
162            ));
163        }
164        if let Found::Builtin(row) = found {
165            if options.concurrently && is_catalog(row.oid) {
166                return Err(system_concurrently());
167            }
168            return Ok(());
169        }
170        if kind == b'p' {
171            let context = format!(
172                "while reindexing partitioned table \"{}\"",
173                self.qualified_relation(table)
174            );
175            self.top_level = Some(TopLevel { statement: "REINDEX TABLE", context });
176            let leaves: Vec<u32> = self
177                .catalog
178                .all_inheritors(table)
179                .into_iter()
180                .filter(|&oid| {
181                    self.catalog
182                        .relation(oid)
183                        .is_some_and(|r| matches!(r.kind, RelKind::Table | RelKind::MatView))
184                })
185                .collect();
186            self.reindex_each(&leaves, options);
187            return Ok(());
188        }
189        if options.concurrently && !self.is_temp(table) {
190            let indexes = self.concurrent_indexes(table);
191            if indexes.is_empty() {
192                self.notice(
193                    SqlState::SUCCESSFUL_COMPLETION,
194                    format!("table \"{name}\" has no indexes that can be reindexed concurrently"),
195                );
196            } else {
197                self.reindex_concurrently(&indexes, options.verbose, false);
198                self.table_reindexed(table, options.verbose, true);
199            }
200        } else if !self.reindex_relation(table, options.verbose) {
201            self.notice(
202                SqlState::SUCCESSFUL_COMPLETION,
203                format!("table \"{name}\" has no indexes to reindex"),
204            );
205        }
206        Ok(())
207    }
208
209    /// `ReindexMultipleTables` after the checks of the database: the owner of the schema for `SCHEMA`, then each table and materialized view of the schema, of the catalogs for `SYSTEM`, or of the other relations for `DATABASE`. The temporary tables of the other sessions stay as they are, and `CONCURRENTLY` skips the catalogs with one warning.
210    fn reindex_multiple(
211        &mut self,
212        kind: ReindexObjectType,
213        name: Option<&str>,
214        options: &ReindexOptions,
215    ) -> Result<()> {
216        let schema = match (kind, name) {
217            (ReindexObjectType::REINDEX_OBJECT_SCHEMA, Some(name)) => {
218                let namespace = self.an.schema(name).ok_or_else(|| {
219                    Error::new(
220                        SqlState::UNDEFINED_SCHEMA,
221                        format!("schema \"{name}\" does not exist"),
222                    )
223                })?;
224                let roles = rupg_pgcatalog::roles::current();
225                let owner = self.schema_owner(namespace).unwrap_or_default();
226                if !roles.has_privs_of_role(self.user, owner) && !self.has_pg_maintain() {
227                    return Err(Error::new(
228                        SqlState::INSUFFICIENT_PRIVILEGE,
229                        format!("must be owner of schema {name}"),
230                    ));
231                }
232                Some(namespace)
233            }
234            _ => None,
235        };
236        let system = kind == ReindexObjectType::REINDEX_OBJECT_SYSTEM;
237        let wanted = |oid: u32, namespace: u32| match schema {
238            Some(schema) => namespace == schema,
239            None => is_catalog(oid) == system,
240        };
241        let catalogs = builtin::classes().iter().any(|r| {
242            matches!(r.kind, b'r' | b'm') && is_catalog(r.oid) && wanted(r.oid, r.namespace)
243        });
244        if options.concurrently && catalogs {
245            self.warning(
246                SqlState::FEATURE_NOT_SUPPORTED,
247                "cannot reindex system catalogs concurrently, skipping all".to_string(),
248            );
249        }
250        let tables: Vec<u32> = self
251            .catalog
252            .relations()
253            .filter(|r| matches!(r.kind, RelKind::Table | RelKind::MatView))
254            .filter(|r| wanted(r.oid, r.namespace) && !self.other_temp(r.namespace))
255            .map(|r| r.oid)
256            .collect();
257        self.reindex_each(&tables, options);
258        Ok(())
259    }
260
261    /// `ReindexMultipleInternal`: `ReindexRelationConcurrently` for `CONCURRENTLY` on a relation that is not temporary, `reindex_index` for an index, and `reindex_relation` for a table, with the message of `VERBOSE` for each table that has an index.
262    fn reindex_each(&mut self, relations: &[u32], options: &ReindexOptions) {
263        for &oid in relations {
264            let index = self.catalog.relation(oid).is_some_and(|r| r.kind == RelKind::Index);
265            if options.concurrently && !self.is_temp(oid) {
266                let indexes = if index { vec![oid] } else { self.concurrent_indexes(oid) };
267                if !indexes.is_empty() {
268                    self.reindex_concurrently(&indexes, options.verbose, index);
269                    if !index {
270                        self.table_reindexed(oid, options.verbose, true);
271                    }
272                }
273            } else if index {
274                self.reindex_one(oid, options.verbose);
275            } else if self.reindex_relation(oid, options.verbose) {
276                self.table_reindexed(oid, options.verbose, false);
277            }
278        }
279    }
280
281    /// `reindex_relation` with `REINDEX_REL_PROCESS_TOAST`: the indexes of the TOAST table first, then each index of the table in the order of their OIDs. It gives false when neither has an index.
282    fn reindex_relation(&mut self, table: u32, verbose: bool) -> bool {
283        let indexes = self.indexes_on(table);
284        let mut result = !indexes.is_empty();
285        let toast = self.catalog.relation(table).map_or(0, |r| r.toast);
286        if toast != 0 {
287            result |= self.reindex_relation(toast, verbose);
288        }
289        for index in indexes {
290            self.reindex_one(index, verbose);
291        }
292        result
293    }
294
295    /// `reindex_index`: the index gets a new file.
296    fn reindex_one(&mut self, index: u32, verbose: bool) {
297        if self.rebuild(index).is_err() {
298            return;
299        }
300        if verbose {
301            let name = self.catalog.relation(index).map(|r| r.name.clone()).unwrap_or_default();
302            self.flush();
303            self.messages
304                .push(info(format!("index \"{name}\" was reindexed"), Some(RUSAGE.to_string())));
305        }
306    }
307
308    /// The indexes of `ReindexRelationConcurrently` for a table: its valid indexes, then the valid indexes of its TOAST table, with a warning for each invalid index.
309    fn concurrent_indexes(&mut self, table: u32) -> Vec<u32> {
310        let toast = self.catalog.relation(table).map_or(0, |r| r.toast);
311        let mut out = Vec::new();
312        for relation in [table, toast].into_iter().filter(|&oid| oid != 0) {
313            for index in self.indexes_on(relation) {
314                let valid = self
315                    .catalog
316                    .relation(index)
317                    .and_then(|r| r.index.as_ref())
318                    .is_some_and(|i| i.valid);
319                if valid {
320                    out.push(index);
321                    continue;
322                }
323                let shown = self.qualified_relation(index);
324                self.flush();
325                self.messages.push(Message::Warning(
326                    Error::new(
327                        SqlState::OBJECT_NOT_IN_PREREQUISITE_STATE,
328                        format!("skipping reindex of invalid index \"{shown}\""),
329                    )
330                    .with_hint("Use DROP INDEX or REINDEX INDEX."),
331                ));
332            }
333        }
334        out
335    }
336
337    /// `ReindexRelationConcurrently` on the indexes: each one gets a new file. The messages of `VERBOSE` name each index with its schema. `alone` is true when the statement runs on the index itself, not on its table, and then the message of the index has the time. The caller of a table gives the time in the message of the table.
338    fn reindex_concurrently(&mut self, indexes: &[u32], verbose: bool, alone: bool) {
339        for &index in indexes {
340            if self.rebuild(index).is_err() {
341                continue;
342            }
343            if verbose {
344                let shown = self.qualified_relation(index);
345                self.flush();
346                let detail = alone.then(|| format!("{RUSAGE}."));
347                self.messages.push(info(format!("index \"{shown}\" was reindexed"), detail));
348            }
349        }
350    }
351
352    /// The new file of an index that the statement builds again. An index of a failed `CREATE INDEX CONCURRENTLY` becomes valid and ready, as `reindex_index` marks it, and the caller checks the keys of the rows of its table.
353    fn rebuild(&mut self, index: u32) -> Result<()> {
354        self.catalog.set_new_file(index)?;
355        self.catalog.set_index_valid(index, true)?;
356        self.catalog.set_index_ready(index, true)
357    }
358
359    /// The message of `VERBOSE` after a table: `ReindexMultipleInternal` names the table, and `ReindexRelationConcurrently` adds the time.
360    fn table_reindexed(&mut self, table: u32, verbose: bool, concurrently: bool) {
361        if verbose {
362            let shown = self.qualified_relation(table);
363            self.flush();
364            let detail = concurrently.then(|| format!("{RUSAGE}."));
365            self.messages.push(info(format!("table \"{shown}\" was reindexed"), detail));
366        }
367    }
368
369    /// `RelationGetIndexList`: the indexes of a table in the order of their OIDs.
370    fn indexes_on(&self, table: u32) -> Vec<u32> {
371        self.catalog
372            .relations()
373            .filter(|r| r.kind == RelKind::Index)
374            .filter(|r| r.index.as_ref().is_some_and(|i| i.table == table))
375            .map(|r| r.oid)
376            .collect()
377    }
378
379    /// `find_all_inheritors` of a partitioned index, with only the indexes that have storage.
380    fn leaf_indexes(&self, index: u32) -> Vec<u32> {
381        let mut out = Vec::new();
382        let mut parents = vec![index];
383        while let Some(parent) = parents.pop() {
384            for child in self.catalog.relations() {
385                if child.index.as_ref().is_some_and(|i| i.parent == parent) {
386                    match child.kind {
387                        RelKind::PartitionedIndex => parents.push(child.oid),
388                        _ => out.push(child.oid),
389                    }
390                }
391            }
392        }
393        out.sort_unstable();
394        out
395    }
396
397    /// `IndexGetRelation`: the table of an index of the catalog.
398    fn index_table(&self, index: u32) -> Option<u32> {
399        self.catalog.relation(index).and_then(|r| r.index.as_ref()).map(|i| i.table)
400    }
401
402    /// True for a relation in a temporary schema, whose `relpersistence` is `t`.
403    fn is_temp(&self, oid: u32) -> bool {
404        self.catalog.relation(oid).is_some_and(|r| self.catalog.is_temp_schema(r.namespace))
405    }
406
407    /// True for a temporary schema of another session, as `isTempNamespace` is false for it.
408    fn other_temp(&self, namespace: u32) -> bool {
409        if !self.catalog.is_temp_schema(namespace) {
410            return false;
411        }
412        let own = self.an.env.temp_number();
413        let name = self.schema_name(namespace);
414        own.is_none_or(|n| name != format!("pg_temp_{n}") && name != format!("pg_toast_temp_{n}"))
415    }
416
417    /// The name of a relation of the catalog with the name of its schema.
418    fn qualified_relation(&self, oid: u32) -> String {
419        match self.catalog.relation(oid) {
420            Some(rel) => format!("{}.{}", self.schema_name(rel.namespace), rel.name),
421            None => String::new(),
422        }
423    }
424}