1use super::{Column, ColumnId, DiffContext, Schema, TableId};
2use crate::stmt;
3
4use std::{
5 collections::{HashMap, HashSet},
6 fmt,
7 ops::Deref,
8};
9
10#[derive(Debug, Clone)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct Index {
13 pub id: IndexId,
15
16 pub name: String,
18
19 pub on: TableId,
21
22 pub columns: Vec<IndexColumn>,
24
25 pub unique: bool,
27
28 pub primary_key: bool,
30}
31
32#[derive(Copy, Clone, Eq, PartialEq, Hash)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub struct IndexId {
35 pub table: TableId,
36 pub index: usize,
37}
38
39#[derive(Debug, Clone)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41pub struct IndexColumn {
42 pub column: ColumnId,
44
45 pub op: IndexOp,
47
48 pub scope: IndexScope,
50}
51
52#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54pub enum IndexOp {
55 Eq,
56 Sort(stmt::Direction),
57}
58
59#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61pub enum IndexScope {
62 Partition,
64
65 Local,
67}
68
69impl IndexColumn {
70 pub fn table_column<'a>(&self, schema: &'a Schema) -> &'a Column {
71 schema.column(self.column)
72 }
73}
74
75impl IndexScope {
76 pub fn is_partition(self) -> bool {
77 matches!(self, Self::Partition)
78 }
79
80 pub fn is_local(self) -> bool {
81 matches!(self, Self::Local)
82 }
83}
84
85impl IndexId {
86 pub(crate) fn placeholder() -> Self {
87 Self {
88 table: TableId::placeholder(),
89 index: usize::MAX,
90 }
91 }
92}
93
94impl fmt::Debug for IndexId {
95 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
96 write!(fmt, "IndexId({}/{})", self.table.0, self.index)
97 }
98}
99
100pub struct IndicesDiff<'a> {
101 items: Vec<IndicesDiffItem<'a>>,
102}
103
104impl<'a> IndicesDiff<'a> {
105 pub fn from(cx: &DiffContext<'a>, previous: &'a [Index], next: &'a [Index]) -> Self {
106 fn has_diff(cx: &DiffContext<'_>, previous: &Index, next: &Index) -> bool {
107 if previous.name != next.name
109 || previous.columns.len() != next.columns.len()
110 || previous.unique != next.unique
111 || previous.primary_key != next.primary_key
112 {
113 return true;
114 }
115
116 for (previous_col, next_col) in previous.columns.iter().zip(next.columns.iter()) {
118 if previous_col.op != next_col.op || previous_col.scope != next_col.scope {
120 return true;
121 }
122
123 let columns_match =
125 if let Some(renamed_to) = cx.rename_hints().get_column(previous_col.column) {
126 renamed_to == next_col.column
128 } else {
129 let previous_column = cx.previous().column(previous_col.column);
131 let next_column = cx.next().column(next_col.column);
132 previous_column.name == next_column.name
133 };
134
135 if !columns_match {
136 return true;
137 }
138 }
139
140 false
141 }
142
143 let mut items = vec![];
144 let mut create_ids: HashSet<_> = next.iter().map(|to| to.id).collect();
145
146 let next_map =
147 HashMap::<&str, &'a Index>::from_iter(next.iter().map(|to| (to.name.as_str(), to)));
148
149 for previous in previous {
150 let next = if let Some(next_id) = cx.rename_hints().get_index(previous.id) {
151 cx.next().index(next_id)
152 } else if let Some(next) = next_map.get(previous.name.as_str()) {
153 next
154 } else {
155 items.push(IndicesDiffItem::DropIndex(previous));
156 continue;
157 };
158
159 create_ids.remove(&next.id);
160
161 if has_diff(cx, previous, next) {
162 items.push(IndicesDiffItem::AlterIndex { previous, next });
163 }
164 }
165
166 for index_id in create_ids {
167 items.push(IndicesDiffItem::CreateIndex(cx.next().index(index_id)));
168 }
169
170 Self { items }
171 }
172
173 pub const fn is_empty(&self) -> bool {
174 self.items.is_empty()
175 }
176}
177
178impl<'a> Deref for IndicesDiff<'a> {
179 type Target = Vec<IndicesDiffItem<'a>>;
180
181 fn deref(&self) -> &Self::Target {
182 &self.items
183 }
184}
185
186pub enum IndicesDiffItem<'a> {
187 CreateIndex(&'a Index),
188 DropIndex(&'a Index),
189 AlterIndex {
190 previous: &'a Index,
191 next: &'a Index,
192 },
193}
194
195#[cfg(test)]
196mod tests {
197 use crate::schema::db::{
198 Column, ColumnId, DiffContext, Index, IndexColumn, IndexId, IndexOp, IndexScope,
199 IndicesDiff, IndicesDiffItem, PrimaryKey, RenameHints, Schema, Table, TableId, Type,
200 };
201 use crate::stmt;
202
203 fn make_column(table_id: usize, index: usize, name: &str) -> Column {
204 Column {
205 id: ColumnId {
206 table: TableId(table_id),
207 index,
208 },
209 name: name.to_string(),
210 ty: stmt::Type::String,
211 storage_ty: Type::Text,
212 nullable: false,
213 primary_key: false,
214 auto_increment: false,
215 }
216 }
217
218 fn make_index(
219 table_id: usize,
220 index: usize,
221 name: &str,
222 columns: Vec<(usize, IndexOp, IndexScope)>,
223 unique: bool,
224 ) -> Index {
225 Index {
226 id: IndexId {
227 table: TableId(table_id),
228 index,
229 },
230 name: name.to_string(),
231 on: TableId(table_id),
232 columns: columns
233 .into_iter()
234 .map(|(col_idx, op, scope)| IndexColumn {
235 column: ColumnId {
236 table: TableId(table_id),
237 index: col_idx,
238 },
239 op,
240 scope,
241 })
242 .collect(),
243 unique,
244 primary_key: false,
245 }
246 }
247
248 fn make_schema_with_indices(
249 table_id: usize,
250 columns: Vec<Column>,
251 indices: Vec<Index>,
252 ) -> Schema {
253 let mut schema = Schema::default();
254 schema.tables.push(Table {
255 id: TableId(table_id),
256 name: "test_table".to_string(),
257 columns,
258 primary_key: PrimaryKey {
259 columns: vec![],
260 index: IndexId {
261 table: TableId(table_id),
262 index: 0,
263 },
264 },
265 indices,
266 });
267 schema
268 }
269
270 #[test]
271 fn test_no_diff_same_indices() {
272 let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
273
274 let from_indices = vec![make_index(
275 0,
276 0,
277 "idx_name",
278 vec![(1, IndexOp::Eq, IndexScope::Local)],
279 false,
280 )];
281 let to_indices = vec![make_index(
282 0,
283 0,
284 "idx_name",
285 vec![(1, IndexOp::Eq, IndexScope::Local)],
286 false,
287 )];
288
289 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
290 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
291 let hints = RenameHints::new();
292 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
293
294 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
295 assert!(diff.is_empty());
296 }
297
298 #[test]
299 fn test_create_index() {
300 let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
301
302 let from_indices = vec![];
303 let to_indices = vec![make_index(
304 0,
305 0,
306 "idx_name",
307 vec![(1, IndexOp::Eq, IndexScope::Local)],
308 false,
309 )];
310
311 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
312 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
313 let hints = RenameHints::new();
314 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
315
316 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
317 assert_eq!(diff.items.len(), 1);
318 assert!(matches!(diff.items[0], IndicesDiffItem::CreateIndex(_)));
319 if let IndicesDiffItem::CreateIndex(idx) = diff.items[0] {
320 assert_eq!(idx.name, "idx_name");
321 }
322 }
323
324 #[test]
325 fn test_drop_index() {
326 let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
327
328 let from_indices = vec![make_index(
329 0,
330 0,
331 "idx_name",
332 vec![(1, IndexOp::Eq, IndexScope::Local)],
333 false,
334 )];
335 let to_indices = vec![];
336
337 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
338 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
339 let hints = RenameHints::new();
340 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
341
342 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
343 assert_eq!(diff.items.len(), 1);
344 assert!(matches!(diff.items[0], IndicesDiffItem::DropIndex(_)));
345 if let IndicesDiffItem::DropIndex(idx) = diff.items[0] {
346 assert_eq!(idx.name, "idx_name");
347 }
348 }
349
350 #[test]
351 fn test_alter_index_unique() {
352 let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
353
354 let from_indices = vec![make_index(
355 0,
356 0,
357 "idx_name",
358 vec![(1, IndexOp::Eq, IndexScope::Local)],
359 false,
360 )];
361 let to_indices = vec![make_index(
362 0,
363 0,
364 "idx_name",
365 vec![(1, IndexOp::Eq, IndexScope::Local)],
366 true, )];
368
369 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
370 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
371 let hints = RenameHints::new();
372 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
373
374 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
375 assert_eq!(diff.items.len(), 1);
376 assert!(matches!(diff.items[0], IndicesDiffItem::AlterIndex { .. }));
377 }
378
379 #[test]
380 fn test_alter_index_columns() {
381 let columns = vec![
382 make_column(0, 0, "id"),
383 make_column(0, 1, "name"),
384 make_column(0, 2, "email"),
385 ];
386
387 let from_indices = vec![make_index(
388 0,
389 0,
390 "idx_name",
391 vec![(1, IndexOp::Eq, IndexScope::Local)],
392 false,
393 )];
394 let to_indices = vec![make_index(
395 0,
396 0,
397 "idx_name",
398 vec![
399 (1, IndexOp::Eq, IndexScope::Local),
400 (2, IndexOp::Eq, IndexScope::Local),
401 ],
402 false,
403 )];
404
405 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
406 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
407 let hints = RenameHints::new();
408 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
409
410 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
411 assert_eq!(diff.items.len(), 1);
412 assert!(matches!(diff.items[0], IndicesDiffItem::AlterIndex { .. }));
413 }
414
415 #[test]
416 fn test_alter_index_op() {
417 let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
418
419 let from_indices = vec![make_index(
420 0,
421 0,
422 "idx_name",
423 vec![(1, IndexOp::Eq, IndexScope::Local)],
424 false,
425 )];
426 let to_indices = vec![make_index(
427 0,
428 0,
429 "idx_name",
430 vec![(1, IndexOp::Sort(stmt::Direction::Asc), IndexScope::Local)],
431 false,
432 )];
433
434 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
435 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
436 let hints = RenameHints::new();
437 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
438
439 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
440 assert_eq!(diff.items.len(), 1);
441 assert!(matches!(diff.items[0], IndicesDiffItem::AlterIndex { .. }));
442 }
443
444 #[test]
445 fn test_alter_index_scope() {
446 let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
447
448 let from_indices = vec![make_index(
449 0,
450 0,
451 "idx_name",
452 vec![(1, IndexOp::Eq, IndexScope::Local)],
453 false,
454 )];
455 let to_indices = vec![make_index(
456 0,
457 0,
458 "idx_name",
459 vec![(1, IndexOp::Eq, IndexScope::Partition)],
460 false,
461 )];
462
463 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
464 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
465 let hints = RenameHints::new();
466 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
467
468 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
469 assert_eq!(diff.items.len(), 1);
470 assert!(matches!(diff.items[0], IndicesDiffItem::AlterIndex { .. }));
471 }
472
473 #[test]
474 fn test_rename_index_with_hint() {
475 let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
476
477 let from_indices = vec![make_index(
478 0,
479 0,
480 "old_idx_name",
481 vec![(1, IndexOp::Eq, IndexScope::Local)],
482 false,
483 )];
484 let to_indices = vec![make_index(
485 0,
486 0,
487 "new_idx_name",
488 vec![(1, IndexOp::Eq, IndexScope::Local)],
489 false,
490 )];
491
492 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
493 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
494
495 let mut hints = RenameHints::new();
496 hints.add_index_hint(
497 IndexId {
498 table: TableId(0),
499 index: 0,
500 },
501 IndexId {
502 table: TableId(0),
503 index: 0,
504 },
505 );
506 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
507
508 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
509 assert_eq!(diff.items.len(), 1);
510 assert!(matches!(diff.items[0], IndicesDiffItem::AlterIndex { .. }));
511 if let IndicesDiffItem::AlterIndex { previous, next } = diff.items[0] {
512 assert_eq!(previous.name, "old_idx_name");
513 assert_eq!(next.name, "new_idx_name");
514 }
515 }
516
517 #[test]
518 fn test_rename_index_without_hint_is_drop_and_create() {
519 let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
520
521 let from_indices = vec![make_index(
522 0,
523 0,
524 "old_idx_name",
525 vec![(1, IndexOp::Eq, IndexScope::Local)],
526 false,
527 )];
528 let to_indices = vec![make_index(
529 0,
530 0,
531 "new_idx_name",
532 vec![(1, IndexOp::Eq, IndexScope::Local)],
533 false,
534 )];
535
536 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
537 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
538 let hints = RenameHints::new();
539 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
540
541 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
542 assert_eq!(diff.items.len(), 2);
543
544 let has_drop = diff
545 .items
546 .iter()
547 .any(|item| matches!(item, IndicesDiffItem::DropIndex(_)));
548 let has_create = diff
549 .items
550 .iter()
551 .any(|item| matches!(item, IndicesDiffItem::CreateIndex(_)));
552 assert!(has_drop);
553 assert!(has_create);
554 }
555
556 #[test]
557 fn test_index_with_renamed_column() {
558 let from_columns = vec![make_column(0, 0, "id"), make_column(0, 1, "old_name")];
559 let to_columns = vec![make_column(0, 0, "id"), make_column(0, 1, "new_name")];
560
561 let from_indices = vec![make_index(
562 0,
563 0,
564 "idx_name",
565 vec![(1, IndexOp::Eq, IndexScope::Local)],
566 false,
567 )];
568 let to_indices = vec![make_index(
569 0,
570 0,
571 "idx_name",
572 vec![(1, IndexOp::Eq, IndexScope::Local)],
573 false,
574 )];
575
576 let from_schema = make_schema_with_indices(0, from_columns, from_indices.clone());
577 let to_schema = make_schema_with_indices(0, to_columns, to_indices.clone());
578
579 let mut hints = RenameHints::new();
580 hints.add_column_hint(
581 ColumnId {
582 table: TableId(0),
583 index: 1,
584 },
585 ColumnId {
586 table: TableId(0),
587 index: 1,
588 },
589 );
590 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
591
592 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
593 assert!(diff.is_empty());
595 }
596
597 #[test]
598 fn test_multiple_operations() {
599 let columns = vec![
600 make_column(0, 0, "id"),
601 make_column(0, 1, "name"),
602 make_column(0, 2, "email"),
603 ];
604
605 let from_indices = vec![
606 make_index(
607 0,
608 0,
609 "idx_name",
610 vec![(1, IndexOp::Eq, IndexScope::Local)],
611 false,
612 ),
613 make_index(
614 0,
615 1,
616 "old_idx",
617 vec![(2, IndexOp::Eq, IndexScope::Local)],
618 false,
619 ),
620 make_index(
621 0,
622 2,
623 "idx_to_drop",
624 vec![(0, IndexOp::Eq, IndexScope::Local)],
625 false,
626 ),
627 ];
628 let to_indices = vec![
629 make_index(
630 0,
631 0,
632 "idx_name",
633 vec![(1, IndexOp::Eq, IndexScope::Local)],
634 true, ),
636 make_index(
637 0,
638 1,
639 "new_idx",
640 vec![(2, IndexOp::Eq, IndexScope::Local)],
641 false,
642 ),
643 make_index(
644 0,
645 2,
646 "idx_added",
647 vec![(1, IndexOp::Sort(stmt::Direction::Asc), IndexScope::Local)],
648 false,
649 ),
650 ];
651
652 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
653 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
654
655 let mut hints = RenameHints::new();
656 hints.add_index_hint(
657 IndexId {
658 table: TableId(0),
659 index: 1,
660 },
661 IndexId {
662 table: TableId(0),
663 index: 1,
664 },
665 );
666 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
667
668 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
669 assert_eq!(diff.items.len(), 4);
671 }
672}