1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7use use_pg_identifier::{PgIdentifier, PgIdentifierError};
8use use_pg_table::PgTableRef;
9
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub struct PgIndexName(PgIdentifier);
13
14impl PgIndexName {
15 pub fn new(input: impl AsRef<str>) -> Result<Self, PgIndexError> {
21 PgIdentifier::new(input)
22 .map(Self)
23 .map_err(PgIndexError::Identifier)
24 }
25
26 #[must_use]
28 pub fn as_str(&self) -> &str {
29 self.0.as_str()
30 }
31}
32
33impl fmt::Display for PgIndexName {
34 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35 self.0.fmt(formatter)
36 }
37}
38
39impl FromStr for PgIndexName {
40 type Err = PgIndexError;
41
42 fn from_str(input: &str) -> Result<Self, Self::Err> {
43 Self::new(input)
44 }
45}
46
47#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
49pub enum PgIndexMethod {
50 #[default]
52 Btree,
53 Hash,
55 Gist,
57 Spgist,
59 Gin,
61 Brin,
63}
64
65impl PgIndexMethod {
66 #[must_use]
68 pub const fn as_str(self) -> &'static str {
69 match self {
70 Self::Btree => "btree",
71 Self::Hash => "hash",
72 Self::Gist => "gist",
73 Self::Spgist => "spgist",
74 Self::Gin => "gin",
75 Self::Brin => "brin",
76 }
77 }
78}
79
80impl fmt::Display for PgIndexMethod {
81 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82 formatter.write_str(self.as_str())
83 }
84}
85
86impl FromStr for PgIndexMethod {
87 type Err = PgIndexError;
88
89 fn from_str(input: &str) -> Result<Self, Self::Err> {
90 match normalized_label(input)?.as_str() {
91 "btree" | "b tree" => Ok(Self::Btree),
92 "hash" => Ok(Self::Hash),
93 "gist" => Ok(Self::Gist),
94 "spgist" | "sp gist" => Ok(Self::Spgist),
95 "gin" => Ok(Self::Gin),
96 "brin" => Ok(Self::Brin),
97 _ => Err(PgIndexError::UnknownMethod),
98 }
99 }
100}
101
102#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
104pub struct PgIndexColumn(PgIdentifier);
105
106impl PgIndexColumn {
107 pub fn new(input: impl AsRef<str>) -> Result<Self, PgIndexError> {
113 PgIdentifier::new(input)
114 .map(Self)
115 .map_err(PgIndexError::Identifier)
116 }
117
118 #[must_use]
120 pub fn as_str(&self) -> &str {
121 self.0.as_str()
122 }
123}
124
125impl fmt::Display for PgIndexColumn {
126 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127 self.0.fmt(formatter)
128 }
129}
130
131#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
133pub struct PgIndexExpression(String);
134
135impl PgIndexExpression {
136 pub fn new(input: impl AsRef<str>) -> Result<Self, PgIndexError> {
142 validate_label(input.as_ref(), PgIndexError::EmptyExpression)
143 .map(|value| Self(value.to_owned()))
144 }
145
146 #[must_use]
148 pub fn as_str(&self) -> &str {
149 &self.0
150 }
151}
152
153impl fmt::Display for PgIndexExpression {
154 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
155 formatter.write_str(self.as_str())
156 }
157}
158
159#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
161pub struct PgIndexFlags {
162 bits: u8,
163}
164
165const UNIQUE_FLAG: u8 = 1 << 0;
166const PRIMARY_FLAG: u8 = 1 << 1;
167const PARTIAL_FLAG: u8 = 1 << 2;
168const EXPRESSION_FLAG: u8 = 1 << 3;
169const CONCURRENT_FLAG: u8 = 1 << 4;
170const INVALID_FLAG: u8 = 1 << 5;
171
172impl PgIndexFlags {
173 #[must_use]
175 pub const fn unique(mut self, value: bool) -> Self {
176 self.set_flag(UNIQUE_FLAG, value);
177 self
178 }
179
180 #[must_use]
182 pub const fn primary(mut self, value: bool) -> Self {
183 self.set_flag(PRIMARY_FLAG, value);
184 self
185 }
186
187 #[must_use]
189 pub const fn partial(mut self, value: bool) -> Self {
190 self.set_flag(PARTIAL_FLAG, value);
191 self
192 }
193
194 #[must_use]
196 pub const fn expression(mut self, value: bool) -> Self {
197 self.set_flag(EXPRESSION_FLAG, value);
198 self
199 }
200
201 #[must_use]
203 pub const fn concurrent(mut self, value: bool) -> Self {
204 self.set_flag(CONCURRENT_FLAG, value);
205 self
206 }
207
208 #[must_use]
210 pub const fn invalid(mut self, value: bool) -> Self {
211 self.set_flag(INVALID_FLAG, value);
212 self
213 }
214
215 #[must_use]
217 pub const fn is_unique(self) -> bool {
218 self.has_flag(UNIQUE_FLAG)
219 }
220
221 #[must_use]
223 pub const fn is_primary(self) -> bool {
224 self.has_flag(PRIMARY_FLAG)
225 }
226
227 #[must_use]
229 pub const fn is_partial(self) -> bool {
230 self.has_flag(PARTIAL_FLAG)
231 }
232
233 #[must_use]
235 pub const fn is_expression(self) -> bool {
236 self.has_flag(EXPRESSION_FLAG)
237 }
238
239 #[must_use]
241 pub const fn is_concurrent(self) -> bool {
242 self.has_flag(CONCURRENT_FLAG)
243 }
244
245 #[must_use]
247 pub const fn is_invalid(self) -> bool {
248 self.has_flag(INVALID_FLAG)
249 }
250
251 const fn set_flag(&mut self, flag: u8, value: bool) {
252 if value {
253 self.bits |= flag;
254 } else {
255 self.bits &= !flag;
256 }
257 }
258
259 const fn has_flag(self, flag: u8) -> bool {
260 self.bits & flag != 0
261 }
262}
263
264#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
266pub struct PgIndex {
267 name: PgIndexName,
268 table: Option<PgTableRef>,
269 method: PgIndexMethod,
270 columns: Vec<PgIndexColumn>,
271 expressions: Vec<PgIndexExpression>,
272 predicate: Option<String>,
273 flags: PgIndexFlags,
274}
275
276impl PgIndex {
277 #[must_use]
279 pub const fn new(name: PgIndexName) -> Self {
280 Self {
281 name,
282 table: None,
283 method: PgIndexMethod::Btree,
284 columns: Vec::new(),
285 expressions: Vec::new(),
286 predicate: None,
287 flags: PgIndexFlags { bits: 0 },
288 }
289 }
290
291 #[must_use]
293 pub fn with_table(mut self, table: PgTableRef) -> Self {
294 self.table = Some(table);
295 self
296 }
297
298 #[must_use]
300 pub const fn with_method(mut self, method: PgIndexMethod) -> Self {
301 self.method = method;
302 self
303 }
304
305 #[must_use]
307 pub fn with_columns(mut self, columns: Vec<PgIndexColumn>) -> Self {
308 self.columns = columns;
309 self
310 }
311
312 #[must_use]
314 pub fn with_expression(mut self, expression: PgIndexExpression) -> Self {
315 self.expressions.push(expression);
316 self.flags = self.flags.expression(true);
317 self
318 }
319
320 pub fn with_predicate(mut self, predicate: impl AsRef<str>) -> Result<Self, PgIndexError> {
326 self.predicate =
327 Some(validate_label(predicate.as_ref(), PgIndexError::EmptyPredicate)?.to_owned());
328 self.flags = self.flags.partial(true);
329 Ok(self)
330 }
331
332 #[must_use]
334 pub const fn with_flags(mut self, flags: PgIndexFlags) -> Self {
335 self.flags = flags;
336 self
337 }
338
339 #[must_use]
341 pub const fn name(&self) -> &PgIndexName {
342 &self.name
343 }
344
345 #[must_use]
347 pub const fn table(&self) -> Option<&PgTableRef> {
348 self.table.as_ref()
349 }
350
351 #[must_use]
353 pub const fn method(&self) -> PgIndexMethod {
354 self.method
355 }
356
357 #[must_use]
359 pub fn columns(&self) -> &[PgIndexColumn] {
360 &self.columns
361 }
362
363 #[must_use]
365 pub fn expressions(&self) -> &[PgIndexExpression] {
366 &self.expressions
367 }
368
369 #[must_use]
371 pub fn predicate(&self) -> Option<&str> {
372 self.predicate.as_deref()
373 }
374
375 #[must_use]
377 pub const fn flags(&self) -> PgIndexFlags {
378 self.flags
379 }
380}
381
382#[derive(Clone, Debug, Eq, PartialEq)]
384pub enum PgIndexError {
385 Empty,
386 EmptyExpression,
387 EmptyPredicate,
388 UnknownMethod,
389 ControlCharacter,
390 Identifier(PgIdentifierError),
391}
392
393impl fmt::Display for PgIndexError {
394 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
395 match self {
396 Self::Empty => formatter.write_str("PostgreSQL index label cannot be empty"),
397 Self::EmptyExpression => {
398 formatter.write_str("PostgreSQL index expression cannot be empty")
399 }
400 Self::EmptyPredicate => {
401 formatter.write_str("PostgreSQL index predicate cannot be empty")
402 }
403 Self::UnknownMethod => formatter.write_str("unknown PostgreSQL index method"),
404 Self::ControlCharacter => {
405 formatter.write_str("PostgreSQL index label cannot contain control characters")
406 }
407 Self::Identifier(error) => {
408 write!(formatter, "invalid PostgreSQL index identifier: {error}")
409 }
410 }
411 }
412}
413
414impl Error for PgIndexError {}
415
416fn normalized_label(input: &str) -> Result<String, PgIndexError> {
417 let trimmed = validate_label(input, PgIndexError::Empty)?;
418 Ok(trimmed
419 .replace('_', " ")
420 .split_whitespace()
421 .collect::<Vec<_>>()
422 .join(" ")
423 .to_ascii_lowercase())
424}
425
426fn validate_label(input: &str, empty_error: PgIndexError) -> Result<&str, PgIndexError> {
427 let trimmed = input.trim();
428 if trimmed.is_empty() {
429 return Err(empty_error);
430 }
431 if trimmed.chars().any(char::is_control) {
432 return Err(PgIndexError::ControlCharacter);
433 }
434 Ok(trimmed)
435}
436
437#[cfg(test)]
438mod tests {
439 use super::{
440 PgIndex, PgIndexColumn, PgIndexError, PgIndexExpression, PgIndexFlags, PgIndexMethod,
441 PgIndexName,
442 };
443
444 #[test]
445 fn parses_and_renders_index_methods() -> Result<(), PgIndexError> {
446 assert_eq!("btree".parse::<PgIndexMethod>()?, PgIndexMethod::Btree);
447 assert_eq!("sp gist".parse::<PgIndexMethod>()?, PgIndexMethod::Spgist);
448 assert_eq!(PgIndexMethod::Brin.to_string(), "brin");
449 Ok(())
450 }
451
452 #[test]
453 fn tracks_index_flags() {
454 let flags = PgIndexFlags::default()
455 .unique(true)
456 .primary(true)
457 .concurrent(true)
458 .invalid(true);
459 assert!(flags.is_unique());
460 assert!(flags.is_primary());
461 assert!(flags.is_concurrent());
462 assert!(flags.is_invalid());
463 }
464
465 #[test]
466 fn creates_btree_index_metadata() -> Result<(), PgIndexError> {
467 let index = PgIndex::new(PgIndexName::new("users_email_idx")?)
468 .with_method(PgIndexMethod::Btree)
469 .with_columns(vec![PgIndexColumn::new("email")?])
470 .with_flags(PgIndexFlags::default().unique(true));
471
472 assert_eq!(index.name().as_str(), "users_email_idx");
473 assert_eq!(index.method(), PgIndexMethod::Btree);
474 assert_eq!(index.columns().len(), 1);
475 assert!(index.flags().is_unique());
476 Ok(())
477 }
478
479 #[test]
480 fn tracks_expression_and_partial_labels() -> Result<(), PgIndexError> {
481 let index = PgIndex::new(PgIndexName::new("users_lower_email_idx")?)
482 .with_expression(PgIndexExpression::new("lower(email)")?)
483 .with_predicate("deleted_at IS NULL")?;
484 assert_eq!(index.expressions().len(), 1);
485 assert_eq!(index.predicate(), Some("deleted_at IS NULL"));
486 assert!(index.flags().is_expression());
487 assert!(index.flags().is_partial());
488 Ok(())
489 }
490}