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}