1package tests_test
2
3import (
4	"reflect"
5	"sort"
6	"testing"
7
8	"gorm.io/gorm"
9	. "gorm.io/gorm/utils/tests"
10)
11
12type Blog struct {
13	ID         uint   `gorm:"primary_key"`
14	Locale     string `gorm:"primary_key"`
15	Subject    string
16	Body       string
17	Tags       []Tag `gorm:"many2many:blog_tags;"`
18	SharedTags []Tag `gorm:"many2many:shared_blog_tags;ForeignKey:id;References:id"`
19	LocaleTags []Tag `gorm:"many2many:locale_blog_tags;ForeignKey:id,locale;References:id"`
20}
21
22type Tag struct {
23	ID     uint   `gorm:"primary_key"`
24	Locale string `gorm:"primary_key"`
25	Value  string
26	Blogs  []*Blog `gorm:"many2many:blog_tags"`
27}
28
29func compareTags(tags []Tag, contents []string) bool {
30	var tagContents []string
31	for _, tag := range tags {
32		tagContents = append(tagContents, tag.Value)
33	}
34	sort.Strings(tagContents)
35	sort.Strings(contents)
36	return reflect.DeepEqual(tagContents, contents)
37}
38
39func TestManyToManyWithMultiPrimaryKeys(t *testing.T) {
40	if name := DB.Dialector.Name(); name == "sqlite" || name == "sqlserver" {
41		t.Skip("skip sqlite, sqlserver due to it doesn't support multiple primary keys with auto increment")
42	}
43
44	if name := DB.Dialector.Name(); name == "postgres" {
45		stmt := gorm.Statement{DB: DB}
46		stmt.Parse(&Blog{})
47		stmt.Schema.LookUpField("ID").Unique = true
48		stmt.Parse(&Tag{})
49		stmt.Schema.LookUpField("ID").Unique = true
50		// postgers only allow unique constraint matching given keys
51	}
52
53	DB.Migrator().DropTable(&Blog{}, &Tag{}, "blog_tags", "locale_blog_tags", "shared_blog_tags")
54	if err := DB.AutoMigrate(&Blog{}, &Tag{}); err != nil {
55		t.Fatalf("Failed to auto migrate, got error: %v", err)
56	}
57
58	blog := Blog{
59		Locale:  "ZH",
60		Subject: "subject",
61		Body:    "body",
62		Tags: []Tag{
63			{Locale: "ZH", Value: "tag1"},
64			{Locale: "ZH", Value: "tag2"},
65		},
66	}
67
68	DB.Save(&blog)
69	if !compareTags(blog.Tags, []string{"tag1", "tag2"}) {
70		t.Fatalf("Blog should has two tags")
71	}
72
73	// Append
74	var tag3 = &Tag{Locale: "ZH", Value: "tag3"}
75	DB.Model(&blog).Association("Tags").Append([]*Tag{tag3})
76
77	if !compareTags(blog.Tags, []string{"tag1", "tag2", "tag3"}) {
78		t.Fatalf("Blog should has three tags after Append")
79	}
80
81	if count := DB.Model(&blog).Association("Tags").Count(); count != 3 {
82		t.Fatalf("Blog should has 3 tags after Append, got %v", count)
83	}
84
85	var tags []Tag
86	DB.Model(&blog).Association("Tags").Find(&tags)
87	if !compareTags(tags, []string{"tag1", "tag2", "tag3"}) {
88		t.Fatalf("Should find 3 tags")
89	}
90
91	var blog1 Blog
92	DB.Preload("Tags").Find(&blog1)
93	if !compareTags(blog1.Tags, []string{"tag1", "tag2", "tag3"}) {
94		t.Fatalf("Preload many2many relations")
95	}
96
97	// Replace
98	var tag5 = &Tag{Locale: "ZH", Value: "tag5"}
99	var tag6 = &Tag{Locale: "ZH", Value: "tag6"}
100	DB.Model(&blog).Association("Tags").Replace(tag5, tag6)
101	var tags2 []Tag
102	DB.Model(&blog).Association("Tags").Find(&tags2)
103	if !compareTags(tags2, []string{"tag5", "tag6"}) {
104		t.Fatalf("Should find 2 tags after Replace")
105	}
106
107	if DB.Model(&blog).Association("Tags").Count() != 2 {
108		t.Fatalf("Blog should has three tags after Replace")
109	}
110
111	// Delete
112	DB.Model(&blog).Association("Tags").Delete(tag5)
113	var tags3 []Tag
114	DB.Model(&blog).Association("Tags").Find(&tags3)
115	if !compareTags(tags3, []string{"tag6"}) {
116		t.Fatalf("Should find 1 tags after Delete")
117	}
118
119	if DB.Model(&blog).Association("Tags").Count() != 1 {
120		t.Fatalf("Blog should has three tags after Delete")
121	}
122
123	DB.Model(&blog).Association("Tags").Delete(tag3)
124	var tags4 []Tag
125	DB.Model(&blog).Association("Tags").Find(&tags4)
126	if !compareTags(tags4, []string{"tag6"}) {
127		t.Fatalf("Tag should not be deleted when Delete with a unrelated tag")
128	}
129
130	// Clear
131	DB.Model(&blog).Association("Tags").Clear()
132	if DB.Model(&blog).Association("Tags").Count() != 0 {
133		t.Fatalf("All tags should be cleared")
134	}
135}
136
137func TestManyToManyWithCustomizedForeignKeys(t *testing.T) {
138	if name := DB.Dialector.Name(); name == "sqlite" || name == "sqlserver" {
139		t.Skip("skip sqlite, sqlserver due to it doesn't support multiple primary keys with auto increment")
140	}
141
142	if name := DB.Dialector.Name(); name == "postgres" {
143		t.Skip("skip postgres due to it only allow unique constraint matching given keys")
144	}
145
146	DB.Migrator().DropTable(&Blog{}, &Tag{}, "blog_tags", "locale_blog_tags", "shared_blog_tags")
147	if err := DB.AutoMigrate(&Blog{}, &Tag{}); err != nil {
148		t.Fatalf("Failed to auto migrate, got error: %v", err)
149	}
150
151	blog := Blog{
152		Locale:  "ZH",
153		Subject: "subject",
154		Body:    "body",
155		SharedTags: []Tag{
156			{Locale: "ZH", Value: "tag1"},
157			{Locale: "ZH", Value: "tag2"},
158		},
159	}
160	DB.Save(&blog)
161
162	blog2 := Blog{
163		ID:     blog.ID,
164		Locale: "EN",
165	}
166	DB.Create(&blog2)
167
168	if !compareTags(blog.SharedTags, []string{"tag1", "tag2"}) {
169		t.Fatalf("Blog should has two tags")
170	}
171
172	// Append
173	var tag3 = &Tag{Locale: "ZH", Value: "tag3"}
174	DB.Model(&blog).Association("SharedTags").Append([]*Tag{tag3})
175	if !compareTags(blog.SharedTags, []string{"tag1", "tag2", "tag3"}) {
176		t.Fatalf("Blog should has three tags after Append")
177	}
178
179	if DB.Model(&blog).Association("SharedTags").Count() != 3 {
180		t.Fatalf("Blog should has three tags after Append")
181	}
182
183	if DB.Model(&blog2).Association("SharedTags").Count() != 3 {
184		t.Fatalf("Blog should has three tags after Append")
185	}
186
187	var tags []Tag
188	DB.Model(&blog).Association("SharedTags").Find(&tags)
189	if !compareTags(tags, []string{"tag1", "tag2", "tag3"}) {
190		t.Fatalf("Should find 3 tags")
191	}
192
193	DB.Model(&blog2).Association("SharedTags").Find(&tags)
194	if !compareTags(tags, []string{"tag1", "tag2", "tag3"}) {
195		t.Fatalf("Should find 3 tags")
196	}
197
198	var blog1 Blog
199	DB.Preload("SharedTags").Find(&blog1)
200	if !compareTags(blog1.SharedTags, []string{"tag1", "tag2", "tag3"}) {
201		t.Fatalf("Preload many2many relations")
202	}
203
204	var tag4 = &Tag{Locale: "ZH", Value: "tag4"}
205	DB.Model(&blog2).Association("SharedTags").Append(tag4)
206
207	DB.Model(&blog).Association("SharedTags").Find(&tags)
208	if !compareTags(tags, []string{"tag1", "tag2", "tag3", "tag4"}) {
209		t.Fatalf("Should find 3 tags")
210	}
211
212	DB.Model(&blog2).Association("SharedTags").Find(&tags)
213	if !compareTags(tags, []string{"tag1", "tag2", "tag3", "tag4"}) {
214		t.Fatalf("Should find 3 tags")
215	}
216
217	// Replace
218	var tag5 = &Tag{Locale: "ZH", Value: "tag5"}
219	var tag6 = &Tag{Locale: "ZH", Value: "tag6"}
220	DB.Model(&blog2).Association("SharedTags").Replace(tag5, tag6)
221	var tags2 []Tag
222	DB.Model(&blog).Association("SharedTags").Find(&tags2)
223	if !compareTags(tags2, []string{"tag5", "tag6"}) {
224		t.Fatalf("Should find 2 tags after Replace")
225	}
226
227	DB.Model(&blog2).Association("SharedTags").Find(&tags2)
228	if !compareTags(tags2, []string{"tag5", "tag6"}) {
229		t.Fatalf("Should find 2 tags after Replace")
230	}
231
232	if DB.Model(&blog).Association("SharedTags").Count() != 2 {
233		t.Fatalf("Blog should has three tags after Replace")
234	}
235
236	// Delete
237	DB.Model(&blog).Association("SharedTags").Delete(tag5)
238	var tags3 []Tag
239	DB.Model(&blog).Association("SharedTags").Find(&tags3)
240	if !compareTags(tags3, []string{"tag6"}) {
241		t.Fatalf("Should find 1 tags after Delete")
242	}
243
244	if DB.Model(&blog).Association("SharedTags").Count() != 1 {
245		t.Fatalf("Blog should has three tags after Delete")
246	}
247
248	DB.Model(&blog2).Association("SharedTags").Delete(tag3)
249	var tags4 []Tag
250	DB.Model(&blog).Association("SharedTags").Find(&tags4)
251	if !compareTags(tags4, []string{"tag6"}) {
252		t.Fatalf("Tag should not be deleted when Delete with a unrelated tag")
253	}
254
255	// Clear
256	DB.Model(&blog2).Association("SharedTags").Clear()
257	if DB.Model(&blog).Association("SharedTags").Count() != 0 {
258		t.Fatalf("All tags should be cleared")
259	}
260}
261
262func TestManyToManyWithCustomizedForeignKeys2(t *testing.T) {
263	if name := DB.Dialector.Name(); name == "sqlite" || name == "sqlserver" {
264		t.Skip("skip sqlite, sqlserver due to it doesn't support multiple primary keys with auto increment")
265	}
266
267	if name := DB.Dialector.Name(); name == "postgres" {
268		t.Skip("skip postgres due to it only allow unique constraint matching given keys")
269	}
270
271	DB.Migrator().DropTable(&Blog{}, &Tag{}, "blog_tags", "locale_blog_tags", "shared_blog_tags")
272	if err := DB.AutoMigrate(&Blog{}, &Tag{}); err != nil {
273		t.Fatalf("Failed to auto migrate, got error: %v", err)
274	}
275
276	blog := Blog{
277		Locale:  "ZH",
278		Subject: "subject",
279		Body:    "body",
280		LocaleTags: []Tag{
281			{Locale: "ZH", Value: "tag1"},
282			{Locale: "ZH", Value: "tag2"},
283		},
284	}
285	DB.Save(&blog)
286
287	blog2 := Blog{
288		ID:     blog.ID,
289		Locale: "EN",
290	}
291	DB.Create(&blog2)
292
293	// Append
294	var tag3 = &Tag{Locale: "ZH", Value: "tag3"}
295	DB.Model(&blog).Association("LocaleTags").Append([]*Tag{tag3})
296	if !compareTags(blog.LocaleTags, []string{"tag1", "tag2", "tag3"}) {
297		t.Fatalf("Blog should has three tags after Append")
298	}
299
300	if DB.Model(&blog).Association("LocaleTags").Count() != 3 {
301		t.Fatalf("Blog should has three tags after Append")
302	}
303
304	if DB.Model(&blog2).Association("LocaleTags").Count() != 0 {
305		t.Fatalf("EN Blog should has 0 tags after ZH Blog Append")
306	}
307
308	var tags []Tag
309	DB.Model(&blog).Association("LocaleTags").Find(&tags)
310	if !compareTags(tags, []string{"tag1", "tag2", "tag3"}) {
311		t.Fatalf("Should find 3 tags")
312	}
313
314	DB.Model(&blog2).Association("LocaleTags").Find(&tags)
315	if len(tags) != 0 {
316		t.Fatalf("Should find 0 tags for EN Blog")
317	}
318
319	var blog1 Blog
320	DB.Preload("LocaleTags").Find(&blog1, "locale = ? AND id = ?", "ZH", blog.ID)
321	if !compareTags(blog1.LocaleTags, []string{"tag1", "tag2", "tag3"}) {
322		t.Fatalf("Preload many2many relations")
323	}
324
325	var tag4 = &Tag{Locale: "ZH", Value: "tag4"}
326	DB.Model(&blog2).Association("LocaleTags").Append(tag4)
327
328	DB.Model(&blog).Association("LocaleTags").Find(&tags)
329	if !compareTags(tags, []string{"tag1", "tag2", "tag3"}) {
330		t.Fatalf("Should find 3 tags for EN Blog")
331	}
332
333	DB.Model(&blog2).Association("LocaleTags").Find(&tags)
334	if !compareTags(tags, []string{"tag4"}) {
335		t.Fatalf("Should find 1 tags  for EN Blog")
336	}
337
338	// Replace
339	var tag5 = &Tag{Locale: "ZH", Value: "tag5"}
340	var tag6 = &Tag{Locale: "ZH", Value: "tag6"}
341	DB.Model(&blog2).Association("LocaleTags").Replace(tag5, tag6)
342
343	var tags2 []Tag
344	DB.Model(&blog).Association("LocaleTags").Find(&tags2)
345	if !compareTags(tags2, []string{"tag1", "tag2", "tag3"}) {
346		t.Fatalf("CN Blog's tags should not be changed after EN Blog Replace")
347	}
348
349	var blog11 Blog
350	DB.Preload("LocaleTags").First(&blog11, "id = ? AND locale = ?", blog.ID, blog.Locale)
351	if !compareTags(blog11.LocaleTags, []string{"tag1", "tag2", "tag3"}) {
352		t.Fatalf("CN Blog's tags should not be changed after EN Blog Replace")
353	}
354
355	DB.Model(&blog2).Association("LocaleTags").Find(&tags2)
356	if !compareTags(tags2, []string{"tag5", "tag6"}) {
357		t.Fatalf("Should find 2 tags after Replace")
358	}
359
360	var blog21 Blog
361	DB.Preload("LocaleTags").First(&blog21, "id = ? AND locale = ?", blog2.ID, blog2.Locale)
362	if !compareTags(blog21.LocaleTags, []string{"tag5", "tag6"}) {
363		t.Fatalf("EN Blog's tags should be changed after Replace")
364	}
365
366	if DB.Model(&blog).Association("LocaleTags").Count() != 3 {
367		t.Fatalf("ZH Blog should has three tags after Replace")
368	}
369
370	if DB.Model(&blog2).Association("LocaleTags").Count() != 2 {
371		t.Fatalf("EN Blog should has two tags after Replace")
372	}
373
374	// Delete
375	DB.Model(&blog).Association("LocaleTags").Delete(tag5)
376
377	if DB.Model(&blog).Association("LocaleTags").Count() != 3 {
378		t.Fatalf("ZH Blog should has three tags after Delete with EN's tag")
379	}
380
381	if DB.Model(&blog2).Association("LocaleTags").Count() != 2 {
382		t.Fatalf("EN Blog should has two tags after ZH Blog Delete with EN's tag")
383	}
384
385	DB.Model(&blog2).Association("LocaleTags").Delete(tag5)
386
387	if DB.Model(&blog).Association("LocaleTags").Count() != 3 {
388		t.Fatalf("ZH Blog should has three tags after EN Blog Delete with EN's tag")
389	}
390
391	if DB.Model(&blog2).Association("LocaleTags").Count() != 1 {
392		t.Fatalf("EN Blog should has 1 tags after EN Blog Delete with EN's tag")
393	}
394
395	// Clear
396	DB.Model(&blog2).Association("LocaleTags").Clear()
397	if DB.Model(&blog).Association("LocaleTags").Count() != 3 {
398		t.Fatalf("ZH Blog's tags should not be cleared when clear EN Blog's tags")
399	}
400
401	if DB.Model(&blog2).Association("LocaleTags").Count() != 0 {
402		t.Fatalf("EN Blog's tags should be cleared when clear EN Blog's tags")
403	}
404
405	DB.Model(&blog).Association("LocaleTags").Clear()
406	if DB.Model(&blog).Association("LocaleTags").Count() != 0 {
407		t.Fatalf("ZH Blog's tags should be cleared when clear ZH Blog's tags")
408	}
409
410	if DB.Model(&blog2).Association("LocaleTags").Count() != 0 {
411		t.Fatalf("EN Blog's tags should be cleared")
412	}
413}
414
415func TestCompositePrimaryKeysAssociations(t *testing.T) {
416	type Label struct {
417		BookID *uint  `gorm:"primarykey"`
418		Name   string `gorm:"primarykey"`
419		Value  string
420	}
421
422	type Book struct {
423		ID     int
424		Name   string
425		Labels []Label
426	}
427
428	DB.Migrator().DropTable(&Label{}, &Book{})
429	if err := DB.AutoMigrate(&Label{}, &Book{}); err != nil {
430		t.Fatalf("failed to migrate")
431	}
432
433	book := Book{
434		Name: "my book",
435		Labels: []Label{
436			{Name: "region", Value: "emea"},
437		},
438	}
439
440	DB.Create(&book)
441
442	var result Book
443	if err := DB.Preload("Labels").First(&result, book.ID).Error; err != nil {
444		t.Fatalf("failed to preload, got error %v", err)
445	}
446
447	AssertEqual(t, book, result)
448}
449