1use 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
16const RUSAGE: &str = "CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s";
18
19#[derive(Clone, Debug, Default, PartialEq, Eq)]
21pub struct ReindexOptions {
22 pub verbose: bool,
24 pub concurrently: bool,
26 pub tablespace: Option<String>,
28}
29
30pub 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
56fn system_concurrently() -> Error {
58 Error::new(SqlState::FEATURE_NOT_SUPPORTED, "cannot reindex system catalogs concurrently")
59}
60
61fn is_catalog(oid: u32) -> bool {
63 oid < FIRST_UNPINNED_OID
64}
65
66fn 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}