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