Skip to main content

reinhardt_db/orm/
indexes.rs

1/// Database indexes similar to Django's indexes
2use serde::{Deserialize, Serialize};
3
4/// Index definition
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Index {
7	/// The name.
8	pub name: String,
9	/// The fields.
10	pub fields: Vec<String>,
11	/// The unique.
12	pub unique: bool,
13	/// The condition.
14	pub condition: Option<String>, // Partial index
15	/// The include.
16	pub include: Vec<String>, // Covering index
17	/// The opclass.
18	pub opclass: Option<String>, // Operator class
19}
20
21impl Index {
22	/// Create a new database index on specified fields
23	///
24	/// # Examples
25	///
26	/// ```
27	/// use reinhardt_db::orm::indexes::Index;
28	///
29	/// let index = Index::new("user_email_idx", vec!["email".to_string()]);
30	/// assert_eq!(index.name, "user_email_idx");
31	/// assert_eq!(index.fields.len(), 1);
32	/// assert!(!index.unique); // Not unique by default
33	/// ```
34	pub fn new(name: impl Into<String>, fields: Vec<String>) -> Self {
35		Self {
36			name: name.into(),
37			fields,
38			unique: false,
39			condition: None,
40			include: Vec::new(),
41			opclass: None,
42		}
43	}
44	/// Documentation for `unique`
45	///
46	pub fn unique(mut self) -> Self {
47		self.unique = true;
48		self
49	}
50	/// Documentation for `with_condition`
51	pub fn with_condition(mut self, condition: String) -> Self {
52		self.condition = Some(condition);
53		self
54	}
55	/// Documentation for `include`
56	///
57	pub fn include(mut self, fields: Vec<String>) -> Self {
58		self.include = fields;
59		self
60	}
61	/// Documentation for `with_opclass`
62	pub fn with_opclass(mut self, opclass: String) -> Self {
63		self.opclass = Some(opclass);
64		self
65	}
66	/// Documentation for `to_sql`
67	///
68	pub fn to_sql(&self, table: &str) -> String {
69		let unique = if self.unique { "UNIQUE " } else { "" };
70		let fields = self.fields.join(", ");
71
72		let mut sql = format!(
73			"CREATE {}INDEX {} ON {} ({})",
74			unique, self.name, table, fields
75		);
76
77		if let Some(ref opclass) = self.opclass {
78			sql = format!(
79				"CREATE {}INDEX {} ON {} ({} {})",
80				unique, self.name, table, fields, opclass
81			);
82		}
83
84		if !self.include.is_empty() {
85			sql.push_str(&format!(" INCLUDE ({})", self.include.join(", ")));
86		}
87
88		if let Some(ref cond) = self.condition {
89			sql.push_str(&format!(" WHERE {}", cond));
90		}
91
92		sql
93	}
94}
95
96/// B-Tree index (default)
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct BTreeIndex {
99	/// The index.
100	pub index: Index,
101}
102
103impl BTreeIndex {
104	/// Create a new B-Tree index (default index type)
105	///
106	/// # Examples
107	///
108	/// ```
109	/// use reinhardt_db::orm::indexes::BTreeIndex;
110	///
111	/// let btree = BTreeIndex::new("user_name_idx", vec!["name".to_string()]);
112	/// assert_eq!(btree.index.name, "user_name_idx");
113	/// ```
114	pub fn new(name: impl Into<String>, fields: Vec<String>) -> Self {
115		Self {
116			index: Index::new(name, fields),
117		}
118	}
119	/// Documentation for `to_sql`
120	///
121	pub fn to_sql(&self, table: &str) -> String {
122		self.index.to_sql(table)
123	}
124}
125
126/// Hash index
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct HashIndex {
129	/// The index.
130	pub index: Index,
131}
132
133impl HashIndex {
134	/// Create a new Hash index for equality comparisons
135	///
136	/// # Examples
137	///
138	/// ```
139	/// use reinhardt_db::orm::indexes::HashIndex;
140	///
141	/// let hash = HashIndex::new("user_email_hash", vec!["email".to_string()]);
142	/// assert_eq!(hash.index.name, "user_email_hash");
143	/// // Hash indexes are fast for equality but don't support range queries
144	/// ```
145	pub fn new(name: impl Into<String>, fields: Vec<String>) -> Self {
146		let mut index = Index::new(name, fields);
147		index.opclass = Some("USING HASH".to_string());
148		Self { index }
149	}
150	/// Documentation for `to_sql`
151	///
152	pub fn to_sql(&self, table: &str) -> String {
153		format!(
154			"CREATE INDEX {} ON {} USING HASH ({})",
155			self.index.name,
156			table,
157			self.index.fields.join(", ")
158		)
159	}
160}
161
162/// GIN index (for arrays, JSONB, full-text search)
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct GinIndex {
165	/// The index.
166	pub index: Index,
167}
168
169impl GinIndex {
170	/// Create a new GIN index for arrays, JSONB, and full-text search
171	///
172	/// # Examples
173	///
174	/// ```
175	/// use reinhardt_db::orm::indexes::GinIndex;
176	///
177	/// let gin = GinIndex::new("post_tags_gin", vec!["tags".to_string()]);
178	/// assert_eq!(gin.index.name, "post_tags_gin");
179	/// // GIN indexes are ideal for array and JSONB column searches
180	/// ```
181	pub fn new(name: impl Into<String>, fields: Vec<String>) -> Self {
182		let mut index = Index::new(name, fields);
183		index.opclass = Some("USING GIN".to_string());
184		Self { index }
185	}
186	/// Documentation for `to_sql`
187	///
188	pub fn to_sql(&self, table: &str) -> String {
189		format!(
190			"CREATE INDEX {} ON {} USING GIN ({})",
191			self.index.name,
192			table,
193			self.index.fields.join(", ")
194		)
195	}
196}
197
198/// GiST index (for geometric data, full-text search)
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct GistIndex {
201	/// The index.
202	pub index: Index,
203}
204
205impl GistIndex {
206	/// Create a new GiST index for geometric data and full-text search
207	///
208	/// # Examples
209	///
210	/// ```
211	/// use reinhardt_db::orm::indexes::GistIndex;
212	///
213	/// let gist = GistIndex::new("location_gist", vec!["coordinates".to_string()]);
214	/// assert_eq!(gist.index.name, "location_gist");
215	/// // GiST indexes support geometric and spatial queries
216	/// ```
217	pub fn new(name: impl Into<String>, fields: Vec<String>) -> Self {
218		let mut index = Index::new(name, fields);
219		index.opclass = Some("USING GIST".to_string());
220		Self { index }
221	}
222	/// Documentation for `to_sql`
223	///
224	pub fn to_sql(&self, table: &str) -> String {
225		format!(
226			"CREATE INDEX {} ON {} USING GIST ({})",
227			self.index.name,
228			table,
229			self.index.fields.join(", ")
230		)
231	}
232}
233
234#[cfg(test)]
235mod tests {
236	use super::*;
237
238	#[test]
239	fn test_basic_index() {
240		let index = Index::new("idx_email", vec!["email".to_string()]);
241		let sql = index.to_sql("users");
242		assert_eq!(sql, "CREATE INDEX idx_email ON users (email)");
243	}
244
245	#[test]
246	fn test_unique_index() {
247		let index = Index::new("idx_unique_email", vec!["email".to_string()]).unique();
248		let sql = index.to_sql("users");
249		assert_eq!(sql, "CREATE UNIQUE INDEX idx_unique_email ON users (email)");
250	}
251
252	#[test]
253	fn test_orm_indexes_composite() {
254		let index = Index::new(
255			"idx_user_email",
256			vec!["user_id".to_string(), "email".to_string()],
257		);
258		let sql = index.to_sql("messages");
259		assert_eq!(
260			sql,
261			"CREATE INDEX idx_user_email ON messages (user_id, email)"
262		);
263	}
264
265	#[test]
266	fn test_partial_index() {
267		let index = Index::new("idx_active_users", vec!["email".to_string()])
268			.with_condition("deleted_at IS NULL".to_string());
269		let sql = index.to_sql("users");
270		assert_eq!(
271			sql,
272			"CREATE INDEX idx_active_users ON users (email) WHERE deleted_at IS NULL"
273		);
274	}
275
276	#[test]
277	fn test_orm_indexes_covering() {
278		let index = Index::new("idx_email_covering", vec!["email".to_string()])
279			.include(vec!["name".to_string(), "created_at".to_string()]);
280		let sql = index.to_sql("users");
281		assert_eq!(
282			sql,
283			"CREATE INDEX idx_email_covering ON users (email) INCLUDE (name, created_at)"
284		);
285	}
286
287	#[test]
288	fn test_btree_index() {
289		let index = BTreeIndex::new("idx_btree", vec!["id".to_string()]);
290		let sql = index.to_sql("users");
291		assert_eq!(sql, "CREATE INDEX idx_btree ON users (id)");
292	}
293
294	#[test]
295	fn test_hash_index() {
296		let index = HashIndex::new("idx_hash", vec!["email".to_string()]);
297		let sql = index.to_sql("users");
298		assert_eq!(sql, "CREATE INDEX idx_hash ON users USING HASH (email)");
299	}
300
301	#[test]
302	fn test_gin_index() {
303		let index = GinIndex::new("idx_gin", vec!["tags".to_string()]);
304		let sql = index.to_sql("posts");
305		assert_eq!(sql, "CREATE INDEX idx_gin ON posts USING GIN (tags)");
306	}
307
308	#[test]
309	fn test_gist_index() {
310		let index = GistIndex::new("idx_gist", vec!["location".to_string()]);
311		let sql = index.to_sql("places");
312		assert_eq!(sql, "CREATE INDEX idx_gist ON places USING GIST (location)");
313	}
314}