Skip to main content

toasty_core/schema/db/
index.rs

1use super::{Column, ColumnId, Schema, TableId};
2use crate::stmt;
3
4use std::fmt;
5
6/// A database index over one or more columns of a table.
7///
8/// Indices can be unique or non-unique, and can cover the primary key.
9/// Each indexed column specifies an [`IndexOp`] (equality or sort) and an
10/// [`IndexScope`] (partition or local).
11///
12/// # Examples
13///
14/// ```ignore
15/// use toasty_core::schema::db::{Index, IndexColumn, IndexId, IndexOp, IndexScope, ColumnId, TableId};
16///
17/// let index = Index {
18///     id: IndexId { table: TableId(0), index: 0 },
19///     name: "idx_users_email".to_string(),
20///     on: TableId(0),
21///     columns: vec![IndexColumn {
22///         column: ColumnId { table: TableId(0), index: 1 },
23///         op: IndexOp::Eq,
24///         scope: IndexScope::Local,
25///     }],
26///     unique: true,
27///     primary_key: false,
28/// };
29///
30/// assert!(index.unique);
31/// assert_eq!(index.columns.len(), 1);
32/// ```
33#[derive(Debug, Clone)]
34#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35pub struct Index {
36    /// Uniquely identifies the index within the schema
37    pub id: IndexId,
38
39    /// Index name is unique within the schema
40    pub name: String,
41
42    /// The table being indexed
43    pub on: TableId,
44
45    /// Fields included in the index.
46    pub columns: Vec<IndexColumn>,
47
48    /// When `true`, indexed entries are unique
49    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_false"))]
50    pub unique: bool,
51
52    /// When `true`, the index indexes the model's primary key fields.
53    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_false"))]
54    pub primary_key: bool,
55}
56
57#[cfg(feature = "serde")]
58fn is_false(b: &bool) -> bool {
59    !*b
60}
61
62/// Uniquely identifies an index within a schema.
63///
64/// Combines the [`TableId`] of the owning table with the index's positional
65/// offset in that table's index list.
66///
67/// # Examples
68///
69/// ```ignore
70/// use toasty_core::schema::db::{IndexId, TableId};
71///
72/// let id = IndexId { table: TableId(0), index: 1 };
73/// assert_eq!(id.index, 1);
74/// ```
75#[derive(Copy, Clone, Eq, PartialEq, Hash)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
77pub struct IndexId {
78    /// The table this index belongs to.
79    pub table: TableId,
80    /// Zero-based position of this index in the table's index list.
81    pub index: usize,
82}
83
84/// A single column entry within an [`Index`].
85///
86/// Specifies which column is indexed, the comparison operation, and the scope
87/// (partition vs. local).
88///
89/// # Examples
90///
91/// ```ignore
92/// use toasty_core::schema::db::{IndexColumn, IndexOp, IndexScope, ColumnId, TableId};
93///
94/// let ic = IndexColumn {
95///     column: ColumnId { table: TableId(0), index: 0 },
96///     op: IndexOp::Eq,
97///     scope: IndexScope::Local,
98/// };
99///
100/// assert!(ic.scope.is_local());
101/// ```
102#[derive(Debug, Clone)]
103#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
104pub struct IndexColumn {
105    /// The column being indexed
106    pub column: ColumnId,
107
108    /// The comparison operation used to index the column
109    pub op: IndexOp,
110
111    /// Scope of the index
112    pub scope: IndexScope,
113}
114
115/// The comparison operation used by an index column.
116///
117/// # Examples
118///
119/// ```ignore
120/// use toasty_core::schema::db::IndexOp;
121/// use toasty_core::stmt::Direction;
122///
123/// let op = IndexOp::Sort(Direction::Asc);
124/// assert!(matches!(op, IndexOp::Sort(_)));
125/// ```
126#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
127#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
128pub enum IndexOp {
129    /// Equality lookup.
130    Eq,
131    /// Sorted scan in the given direction.
132    Sort(stmt::Direction),
133}
134
135/// Scope of an index column, relevant for distributed databases.
136///
137/// # Examples
138///
139/// ```ignore
140/// use toasty_core::schema::db::IndexScope;
141///
142/// let scope = IndexScope::Partition;
143/// assert!(scope.is_partition());
144/// assert!(!scope.is_local());
145/// ```
146#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
147#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
148pub enum IndexScope {
149    /// The index column is used to partition rows across nodes of a distributed database.
150    Partition,
151
152    /// The index column is scoped to a physical node.
153    Local,
154}
155
156impl IndexColumn {
157    /// Returns the [`Column`] referenced by this index column.
158    pub fn table_column<'a>(&self, schema: &'a Schema) -> &'a Column {
159        schema.column(self.column)
160    }
161}
162
163impl IndexScope {
164    /// Returns `true` if this is the [`Partition`](IndexScope::Partition) scope.
165    pub fn is_partition(self) -> bool {
166        matches!(self, Self::Partition)
167    }
168
169    /// Returns `true` if this is the [`Local`](IndexScope::Local) scope.
170    pub fn is_local(self) -> bool {
171        matches!(self, Self::Local)
172    }
173}
174
175impl IndexId {
176    pub(crate) fn placeholder() -> Self {
177        Self {
178            table: TableId::placeholder(),
179            index: usize::MAX,
180        }
181    }
182}
183
184impl fmt::Debug for IndexId {
185    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
186        write!(fmt, "IndexId({}/{})", self.table.0, self.index)
187    }
188}
189
190#[cfg(all(test, feature = "serde"))]
191mod serde_tests {
192    use crate::schema::db::{ColumnId, Index, IndexColumn, IndexId, IndexOp, IndexScope, TableId};
193
194    fn base_index() -> Index {
195        Index {
196            id: IndexId {
197                table: TableId(0),
198                index: 0,
199            },
200            name: "idx".to_string(),
201            on: TableId(0),
202            columns: vec![IndexColumn {
203                column: ColumnId {
204                    table: TableId(0),
205                    index: 0,
206                },
207                op: IndexOp::Eq,
208                scope: IndexScope::Local,
209            }],
210            unique: false,
211            primary_key: false,
212        }
213    }
214
215    #[test]
216    fn false_booleans_are_omitted() {
217        let toml = toml::to_string(&base_index()).unwrap();
218        assert!(!toml.contains("unique"), "toml: {toml}");
219        assert!(!toml.contains("primary_key"), "toml: {toml}");
220    }
221
222    #[test]
223    fn unique_true_is_included() {
224        let idx = Index {
225            unique: true,
226            ..base_index()
227        };
228        let toml = toml::to_string(&idx).unwrap();
229        assert!(toml.contains("unique = true"), "toml: {toml}");
230    }
231
232    #[test]
233    fn primary_key_true_is_included() {
234        let idx = Index {
235            primary_key: true,
236            ..base_index()
237        };
238        let toml = toml::to_string(&idx).unwrap();
239        assert!(toml.contains("primary_key = true"), "toml: {toml}");
240    }
241
242    #[test]
243    fn missing_bool_fields_deserialize_as_false() {
244        let toml = "name = \"idx\"\non = 0\n\n[id]\ntable = 0\nindex = 0\n\n[[columns]]\nop = \"Eq\"\nscope = \"Local\"\n\n[columns.column]\ntable = 0\nindex = 0\n";
245        let idx: Index = toml::from_str(toml).unwrap();
246        assert!(!idx.unique);
247        assert!(!idx.primary_key);
248    }
249
250    #[test]
251    fn round_trip_all_true() {
252        let original = Index {
253            unique: true,
254            primary_key: true,
255            ..base_index()
256        };
257        let decoded: Index = toml::from_str(&toml::to_string(&original).unwrap()).unwrap();
258        assert_eq!(decoded.unique, original.unique);
259        assert_eq!(decoded.primary_key, original.primary_key);
260        assert_eq!(decoded.name, original.name);
261    }
262}