1use super::{Column, ColumnId, Index, IndexId, PrimaryKey};
2use crate::{
3 schema::db::{column::ColumnsDiff, diff::DiffContext, index::IndicesDiff},
4 stmt,
5};
6
7use std::{
8 collections::{HashMap, HashSet},
9 fmt,
10 ops::Deref,
11};
12
13#[derive(Debug, Clone)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct Table {
17 pub id: TableId,
19
20 pub name: String,
22
23 pub columns: Vec<Column>,
25
26 pub primary_key: PrimaryKey,
27
28 pub indices: Vec<Index>,
29}
30
31#[derive(PartialEq, Eq, Clone, Copy, Hash)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub struct TableId(pub usize);
35
36impl Table {
37 pub fn primary_key_column(&self, i: usize) -> &Column {
38 &self.columns[self.primary_key.columns[i].index]
39 }
40
41 pub fn primary_key_columns(&self) -> impl ExactSizeIterator<Item = &Column> + '_ {
42 self.primary_key
43 .columns
44 .iter()
45 .map(|column_id| &self.columns[column_id.index])
46 }
47
48 pub fn column(&self, id: impl Into<ColumnId>) -> &Column {
49 &self.columns[id.into().index]
50 }
51
52 pub fn resolve(&self, projection: &stmt::Projection) -> &Column {
54 let [first, rest @ ..] = projection.as_slice() else {
55 panic!("need at most one path step")
56 };
57 assert!(rest.is_empty());
58
59 &self.columns[*first]
60 }
61
62 pub(crate) fn new(id: TableId, name: String) -> Self {
63 Self {
64 id,
65 name,
66 columns: vec![],
67 primary_key: PrimaryKey {
68 columns: vec![],
69 index: IndexId {
70 table: id,
71 index: 0,
72 },
73 },
74 indices: vec![],
75 }
76 }
77}
78
79impl TableId {
80 pub(crate) fn placeholder() -> Self {
81 Self(usize::MAX)
82 }
83}
84
85impl fmt::Debug for TableId {
86 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
87 write!(fmt, "TableId({})", self.0)
88 }
89}
90
91pub struct TablesDiff<'a> {
92 items: Vec<TablesDiffItem<'a>>,
93}
94
95impl<'a> TablesDiff<'a> {
96 pub fn from(cx: &DiffContext<'a>, previous: &'a [Table], next: &'a [Table]) -> Self {
97 let mut items = vec![];
98 let mut create_ids: HashSet<_> = next.iter().map(|next| next.id).collect();
99
100 let next_map = HashMap::<&str, &'a Table>::from_iter(
101 next.iter().map(|next| (next.name.as_str(), next)),
102 );
103
104 for previous in previous {
105 let next = if let Some(next_id) = cx.rename_hints().get_table(previous.id) {
106 cx.next().table(next_id)
107 } else if let Some(to) = next_map.get(previous.name.as_str()) {
108 to
109 } else {
110 items.push(TablesDiffItem::DropTable(previous));
111 continue;
112 };
113
114 create_ids.remove(&next.id);
115
116 let columns = ColumnsDiff::from(cx, &previous.columns, &next.columns);
117 let indices = IndicesDiff::from(cx, &previous.indices, &next.indices);
118 if previous.name != next.name || !columns.is_empty() || !indices.is_empty() {
119 items.push(TablesDiffItem::AlterTable {
120 previous,
121 next,
122 columns,
123 indices,
124 });
125 }
126 }
127
128 for table_id in create_ids {
129 items.push(TablesDiffItem::CreateTable(cx.next().table(table_id)));
130 }
131
132 Self { items }
133 }
134}
135
136impl<'a> Deref for TablesDiff<'a> {
137 type Target = Vec<TablesDiffItem<'a>>;
138
139 fn deref(&self) -> &Self::Target {
140 &self.items
141 }
142}
143
144pub enum TablesDiffItem<'a> {
145 CreateTable(&'a Table),
146 DropTable(&'a Table),
147 AlterTable {
148 previous: &'a Table,
149 next: &'a Table,
150 columns: ColumnsDiff<'a>,
151 indices: IndicesDiff<'a>,
152 },
153}
154
155#[cfg(test)]
156mod tests {
157 use crate::schema::db::{
158 Column, ColumnId, DiffContext, IndexId, PrimaryKey, RenameHints, Schema, Table, TableId,
159 TablesDiff, TablesDiffItem, Type,
160 };
161 use crate::stmt;
162
163 fn make_table(id: usize, name: &str, num_columns: usize) -> Table {
164 let mut columns = vec![];
165 for i in 0..num_columns {
166 columns.push(Column {
167 id: ColumnId {
168 table: TableId(id),
169 index: i,
170 },
171 name: format!("col{}", i),
172 ty: stmt::Type::String,
173 storage_ty: Type::Text,
174 nullable: false,
175 primary_key: false,
176 auto_increment: false,
177 });
178 }
179
180 Table {
181 id: TableId(id),
182 name: name.to_string(),
183 columns,
184 primary_key: PrimaryKey {
185 columns: vec![],
186 index: IndexId {
187 table: TableId(id),
188 index: 0,
189 },
190 },
191 indices: vec![],
192 }
193 }
194
195 fn make_schema(tables: Vec<Table>) -> Schema {
196 Schema { tables }
197 }
198
199 #[test]
200 fn test_no_diff_same_tables() {
201 let from_tables = vec![make_table(0, "users", 2), make_table(1, "posts", 3)];
202 let to_tables = vec![make_table(0, "users", 2), make_table(1, "posts", 3)];
203
204 let from_schema = make_schema(from_tables.clone());
205 let to_schema = make_schema(to_tables.clone());
206 let hints = RenameHints::new();
207 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
208
209 let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
210 assert_eq!(diff.items.len(), 0);
211 }
212
213 #[test]
214 fn test_create_table() {
215 let from_tables = vec![make_table(0, "users", 2)];
216 let to_tables = vec![make_table(0, "users", 2), make_table(1, "posts", 3)];
217
218 let from_schema = make_schema(from_tables.clone());
219 let to_schema = make_schema(to_tables.clone());
220 let hints = RenameHints::new();
221 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
222
223 let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
224 assert_eq!(diff.items.len(), 1);
225 assert!(matches!(diff.items[0], TablesDiffItem::CreateTable(_)));
226 if let TablesDiffItem::CreateTable(table) = diff.items[0] {
227 assert_eq!(table.name, "posts");
228 }
229 }
230
231 #[test]
232 fn test_drop_table() {
233 let from_tables = vec![make_table(0, "users", 2), make_table(1, "posts", 3)];
234 let to_tables = vec![make_table(0, "users", 2)];
235
236 let from_schema = make_schema(from_tables.clone());
237 let to_schema = make_schema(to_tables.clone());
238 let hints = RenameHints::new();
239 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
240
241 let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
242 assert_eq!(diff.items.len(), 1);
243 assert!(matches!(diff.items[0], TablesDiffItem::DropTable(_)));
244 if let TablesDiffItem::DropTable(table) = diff.items[0] {
245 assert_eq!(table.name, "posts");
246 }
247 }
248
249 #[test]
250 fn test_rename_table_with_hint() {
251 let from_tables = vec![make_table(0, "old_users", 2)];
252 let to_tables = vec![make_table(0, "new_users", 2)];
253
254 let from_schema = make_schema(from_tables.clone());
255 let to_schema = make_schema(to_tables.clone());
256
257 let mut hints = RenameHints::new();
258 hints.add_table_hint(TableId(0), TableId(0));
259 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
260
261 let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
262 assert_eq!(diff.items.len(), 1);
263 assert!(matches!(diff.items[0], TablesDiffItem::AlterTable { .. }));
264 if let TablesDiffItem::AlterTable { previous, next, .. } = &diff.items[0] {
265 assert_eq!(previous.name, "old_users");
266 assert_eq!(next.name, "new_users");
267 }
268 }
269
270 #[test]
271 fn test_rename_table_without_hint_is_drop_and_create() {
272 let from_tables = vec![make_table(0, "old_users", 2)];
273 let to_tables = vec![make_table(0, "new_users", 2)];
274
275 let from_schema = make_schema(from_tables.clone());
276 let to_schema = make_schema(to_tables.clone());
277 let hints = RenameHints::new();
278 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
279
280 let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
281 assert_eq!(diff.items.len(), 2);
282
283 let has_drop = diff
284 .items
285 .iter()
286 .any(|item| matches!(item, TablesDiffItem::DropTable(_)));
287 let has_create = diff
288 .items
289 .iter()
290 .any(|item| matches!(item, TablesDiffItem::CreateTable(_)));
291 assert!(has_drop);
292 assert!(has_create);
293 }
294
295 #[test]
296 fn test_alter_table_column_change() {
297 let from_tables = vec![make_table(0, "users", 2)];
298 let to_tables = vec![make_table(0, "users", 3)]; let from_schema = make_schema(from_tables.clone());
301 let to_schema = make_schema(to_tables.clone());
302 let hints = RenameHints::new();
303 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
304
305 let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
306 assert_eq!(diff.items.len(), 1);
307 assert!(matches!(diff.items[0], TablesDiffItem::AlterTable { .. }));
308 }
309
310 #[test]
311 fn test_multiple_operations() {
312 let from_tables = vec![
313 make_table(0, "users", 2),
314 make_table(1, "posts", 3),
315 make_table(2, "old_table", 1),
316 ];
317 let to_tables = vec![
318 make_table(0, "users", 3), make_table(1, "new_posts", 3), make_table(2, "comments", 2), ];
322
323 let from_schema = make_schema(from_tables.clone());
324 let to_schema = make_schema(to_tables.clone());
325
326 let mut hints = RenameHints::new();
327 hints.add_table_hint(TableId(1), TableId(1));
328 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
329
330 let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
331 assert_eq!(diff.items.len(), 4);
333 }
334}