1use super::{Column, ColumnId, Index, IndexId, PrimaryKey};
2use crate::{
3 schema::db::{column::ColumnsDiff, diff::DiffContext, index::IndicesDiff},
4 stmt,
5};
6
7use hashbrown::{HashMap, HashSet};
8use std::{fmt, ops::Deref};
9
10#[derive(Debug, Clone)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub struct Table {
24 pub id: TableId,
26
27 pub name: String,
29
30 pub columns: Vec<Column>,
32
33 pub primary_key: PrimaryKey,
35
36 pub indices: Vec<Index>,
38}
39
40#[derive(PartialEq, Eq, Clone, Copy, Hash)]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54pub struct TableId(pub usize);
55
56impl Table {
57 pub fn primary_key_column(&self, i: usize) -> &Column {
63 &self.columns[self.primary_key.columns[i].index]
64 }
65
66 pub fn primary_key_columns(&self) -> impl ExactSizeIterator<Item = &Column> + '_ {
68 self.primary_key
69 .columns
70 .iter()
71 .map(|column_id| &self.columns[column_id.index])
72 }
73
74 pub fn column(&self, id: impl Into<ColumnId>) -> &Column {
82 &self.columns[id.into().index]
83 }
84
85 pub fn resolve(&self, projection: &stmt::Projection) -> &Column {
91 let [first, rest @ ..] = projection.as_slice() else {
92 panic!("need at most one path step")
93 };
94 assert!(rest.is_empty());
95
96 &self.columns[*first]
97 }
98
99 pub(crate) fn new(id: TableId, name: String) -> Self {
100 Self {
101 id,
102 name,
103 columns: vec![],
104 primary_key: PrimaryKey {
105 columns: vec![],
106 index: IndexId {
107 table: id,
108 index: 0,
109 },
110 },
111 indices: vec![],
112 }
113 }
114}
115
116impl TableId {
117 pub(crate) fn placeholder() -> Self {
118 Self(usize::MAX)
119 }
120}
121
122impl fmt::Debug for TableId {
123 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
124 write!(fmt, "TableId({})", self.0)
125 }
126}
127
128pub struct TablesDiff<'a> {
146 items: Vec<TablesDiffItem<'a>>,
147}
148
149impl<'a> TablesDiff<'a> {
150 pub fn from(cx: &DiffContext<'a>, previous: &'a [Table], next: &'a [Table]) -> Self {
157 let mut items = vec![];
158 let mut create_ids: HashSet<_> = next.iter().map(|next| next.id).collect();
159
160 let next_map = HashMap::<&str, &'a Table>::from_iter(
161 next.iter().map(|next| (next.name.as_str(), next)),
162 );
163
164 for previous in previous {
165 let next = if let Some(next_id) = cx.rename_hints().get_table(previous.id) {
166 cx.next().table(next_id)
167 } else if let Some(to) = next_map.get(previous.name.as_str()) {
168 to
169 } else {
170 items.push(TablesDiffItem::DropTable(previous));
171 continue;
172 };
173
174 create_ids.remove(&next.id);
175
176 let columns = ColumnsDiff::from(cx, &previous.columns, &next.columns);
177 let indices = IndicesDiff::from(cx, &previous.indices, &next.indices);
178 if previous.name != next.name || !columns.is_empty() || !indices.is_empty() {
179 items.push(TablesDiffItem::AlterTable {
180 previous,
181 next,
182 columns,
183 indices,
184 });
185 }
186 }
187
188 for table_id in create_ids {
189 items.push(TablesDiffItem::CreateTable(cx.next().table(table_id)));
190 }
191
192 Self { items }
193 }
194}
195
196impl<'a> Deref for TablesDiff<'a> {
197 type Target = Vec<TablesDiffItem<'a>>;
198
199 fn deref(&self) -> &Self::Target {
200 &self.items
201 }
202}
203
204pub enum TablesDiffItem<'a> {
206 CreateTable(&'a Table),
208 DropTable(&'a Table),
210 AlterTable {
212 previous: &'a Table,
214 next: &'a Table,
216 columns: ColumnsDiff<'a>,
218 indices: IndicesDiff<'a>,
220 },
221}
222
223#[cfg(test)]
224mod tests {
225 use crate::schema::db::{
226 Column, ColumnId, DiffContext, IndexId, PrimaryKey, RenameHints, Schema, Table, TableId,
227 TablesDiff, TablesDiffItem, Type,
228 };
229 use crate::stmt;
230
231 fn make_table(id: usize, name: &str, num_columns: usize) -> Table {
232 let mut columns = vec![];
233 for i in 0..num_columns {
234 columns.push(Column {
235 id: ColumnId {
236 table: TableId(id),
237 index: i,
238 },
239 name: format!("col{}", i),
240 ty: stmt::Type::String,
241 storage_ty: Type::Text,
242 nullable: false,
243 primary_key: false,
244 auto_increment: false,
245 versionable: false,
246 });
247 }
248
249 Table {
250 id: TableId(id),
251 name: name.to_string(),
252 columns,
253 primary_key: PrimaryKey {
254 columns: vec![],
255 index: IndexId {
256 table: TableId(id),
257 index: 0,
258 },
259 },
260 indices: vec![],
261 }
262 }
263
264 fn make_schema(tables: Vec<Table>) -> Schema {
265 Schema { tables }
266 }
267
268 #[test]
269 fn test_no_diff_same_tables() {
270 let from_tables = vec![make_table(0, "users", 2), make_table(1, "posts", 3)];
271 let to_tables = vec![make_table(0, "users", 2), make_table(1, "posts", 3)];
272
273 let from_schema = make_schema(from_tables.clone());
274 let to_schema = make_schema(to_tables.clone());
275 let hints = RenameHints::new();
276 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
277
278 let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
279 assert_eq!(diff.items.len(), 0);
280 }
281
282 #[test]
283 fn test_create_table() {
284 let from_tables = vec![make_table(0, "users", 2)];
285 let to_tables = vec![make_table(0, "users", 2), make_table(1, "posts", 3)];
286
287 let from_schema = make_schema(from_tables.clone());
288 let to_schema = make_schema(to_tables.clone());
289 let hints = RenameHints::new();
290 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
291
292 let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
293 assert_eq!(diff.items.len(), 1);
294 assert!(matches!(diff.items[0], TablesDiffItem::CreateTable(_)));
295 if let TablesDiffItem::CreateTable(table) = diff.items[0] {
296 assert_eq!(table.name, "posts");
297 }
298 }
299
300 #[test]
301 fn test_drop_table() {
302 let from_tables = vec![make_table(0, "users", 2), make_table(1, "posts", 3)];
303 let to_tables = vec![make_table(0, "users", 2)];
304
305 let from_schema = make_schema(from_tables.clone());
306 let to_schema = make_schema(to_tables.clone());
307 let hints = RenameHints::new();
308 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
309
310 let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
311 assert_eq!(diff.items.len(), 1);
312 assert!(matches!(diff.items[0], TablesDiffItem::DropTable(_)));
313 if let TablesDiffItem::DropTable(table) = diff.items[0] {
314 assert_eq!(table.name, "posts");
315 }
316 }
317
318 #[test]
319 fn test_rename_table_with_hint() {
320 let from_tables = vec![make_table(0, "old_users", 2)];
321 let to_tables = vec![make_table(0, "new_users", 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(0), TableId(0));
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(), 1);
332 assert!(matches!(diff.items[0], TablesDiffItem::AlterTable { .. }));
333 if let TablesDiffItem::AlterTable { previous, next, .. } = &diff.items[0] {
334 assert_eq!(previous.name, "old_users");
335 assert_eq!(next.name, "new_users");
336 }
337 }
338
339 #[test]
340 fn test_rename_table_without_hint_is_drop_and_create() {
341 let from_tables = vec![make_table(0, "old_users", 2)];
342 let to_tables = vec![make_table(0, "new_users", 2)];
343
344 let from_schema = make_schema(from_tables.clone());
345 let to_schema = make_schema(to_tables.clone());
346 let hints = RenameHints::new();
347 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
348
349 let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
350 assert_eq!(diff.items.len(), 2);
351
352 let has_drop = diff
353 .items
354 .iter()
355 .any(|item| matches!(item, TablesDiffItem::DropTable(_)));
356 let has_create = diff
357 .items
358 .iter()
359 .any(|item| matches!(item, TablesDiffItem::CreateTable(_)));
360 assert!(has_drop);
361 assert!(has_create);
362 }
363
364 #[test]
365 fn test_alter_table_column_change() {
366 let from_tables = vec![make_table(0, "users", 2)];
367 let to_tables = vec![make_table(0, "users", 3)]; let from_schema = make_schema(from_tables.clone());
370 let to_schema = make_schema(to_tables.clone());
371 let hints = RenameHints::new();
372 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
373
374 let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
375 assert_eq!(diff.items.len(), 1);
376 assert!(matches!(diff.items[0], TablesDiffItem::AlterTable { .. }));
377 }
378
379 #[test]
380 fn test_multiple_operations() {
381 let from_tables = vec![
382 make_table(0, "users", 2),
383 make_table(1, "posts", 3),
384 make_table(2, "old_table", 1),
385 ];
386 let to_tables = vec![
387 make_table(0, "users", 3), make_table(1, "new_posts", 3), make_table(2, "comments", 2), ];
391
392 let from_schema = make_schema(from_tables.clone());
393 let to_schema = make_schema(to_tables.clone());
394
395 let mut hints = RenameHints::new();
396 hints.add_table_hint(TableId(1), TableId(1));
397 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
398
399 let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
400 assert_eq!(diff.items.len(), 4);
402 }
403}