diff --git a/go.mod b/go.mod index 7d48416..cf7ad5e 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module gorm.io/datatypes go 1.19 require ( + github.com/google/uuid v1.3.0 github.com/jinzhu/now v1.1.5 gorm.io/driver/mysql v1.5.6 gorm.io/driver/postgres v1.5.0 diff --git a/go.sum b/go.sum index 44dc2e4..237896c 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,7 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2V github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= diff --git a/uuid.go b/uuid.go new file mode 100644 index 0000000..d3d1d74 --- /dev/null +++ b/uuid.go @@ -0,0 +1,73 @@ +package datatypes + +import ( + "database/sql/driver" + + "github.com/google/uuid" + "gorm.io/gorm" + "gorm.io/gorm/schema" +) + +type UUID uuid.UUID + +// GormDataType gorm common data type +func (UUID) GormDataType() string { + return "string" +} + +// GormDBDataType gorm db data type +func (UUID) GormDBDataType(db *gorm.DB, field *schema.Field) string { + switch db.Dialector.Name() { + case "mysql": + return "LONGTEXT" + case "postgres": + return "UUID" + case "sqlserver": + return "NVARCHAR" + case "sqlite": + return "TEXT" + default: + return "" + } +} + +func (u *UUID) Scan(value interface{}) error { + var result uuid.UUID + if err := result.Scan(value); err != nil { + return err + } + *u = UUID(result) + return nil +} + +func (u UUID) Value() (driver.Value, error) { + return uuid.UUID(u).Value() +} + +func (u UUID) String() string { + return uuid.UUID(u).String() +} + +func (u UUID) Equals(other UUID) bool { + return u.String() == other.String() +} + +func (u UUID) Length() int { + return len(u.String()) +} + +func (u UUID) IsNil() bool { + return uuid.UUID(u) == uuid.Nil +} + +func (u UUID) IsEmpty() bool { + return u.IsNil() || u.Length() == 0 +} + +func (u *UUID) IsNilPtr() bool { + return u == nil +} + +func (u *UUID) IsEmptyPtr() bool { + return u.IsNilPtr() || u.IsEmpty() +} diff --git a/uuid_test.go b/uuid_test.go new file mode 100644 index 0000000..4a7abff --- /dev/null +++ b/uuid_test.go @@ -0,0 +1,187 @@ +package datatypes_test + +import ( + "database/sql/driver" + "testing" + + "github.com/google/uuid" + "gorm.io/datatypes" + "gorm.io/gorm" + . "gorm.io/gorm/utils/tests" +) + +var _ driver.Valuer = &datatypes.UUID{} + +func TestUUID(t *testing.T) { + if SupportedDriver("sqlite", "mysql", "postgres", "sqlserver") { + type UserWithUUID struct { + gorm.Model + Name string + UserUUID datatypes.UUID + } + + DB.Migrator().DropTable(&UserWithUUID{}) + if err := DB.Migrator().AutoMigrate(&UserWithUUID{}); err != nil { + t.Errorf("failed to migrate, got error: %v", err) + } + + user1UUID, uuidGenErr := uuid.NewUUID() + if uuidGenErr != nil { + t.Fatalf("failed to generate a new UUID, got error %v", uuidGenErr) + } + user2UUID, uuidGenErr := uuid.NewUUID() + if uuidGenErr != nil { + t.Fatalf("failed to generate a new UUID, got error %v", uuidGenErr) + } + user3UUID, uuidGenErr := uuid.NewUUID() + if uuidGenErr != nil { + t.Fatalf("failed to generate a new UUID, got error %v", uuidGenErr) + } + user4UUID, uuidGenErr := uuid.NewUUID() + if uuidGenErr != nil { + t.Fatalf("failed to generate a new UUID, got error %v", uuidGenErr) + } + + users := []UserWithUUID{{ + Name: "uuid-1", + UserUUID: datatypes.UUID(user1UUID), + }, { + Name: "uuid-2", + UserUUID: datatypes.UUID(user2UUID), + }, { + Name: "uuid-3", + UserUUID: datatypes.UUID(user3UUID), + }, { + Name: "uuid-4", + UserUUID: datatypes.UUID(user4UUID), + }} + + if err := DB.Create(&users).Error; err != nil { + t.Errorf("Failed to create users %v", err) + } + + for _, user := range users { + result := UserWithUUID{} + if err := DB.First( + &result, "name = ? AND user_uuid = ?", + user.Name, + user.UserUUID, + ).Error; err != nil { + t.Fatalf("failed to find user with uuid, got error: %v", err) + } + AssertEqual(t, !result.UserUUID.IsEmpty(), true) + AssertEqual(t, user.UserUUID.Equals(result.UserUUID), true) + valueUser, err := user.UserUUID.Value() + if err != nil { + t.Fatalf("failed to get user value, got error: %v", err) + } + valueResult, err := result.UserUUID.Value() + if err != nil { + t.Fatalf("failed to get result value, got error: %v", err) + } + AssertEqual(t, valueUser, valueResult) + } + + user1 := users[0] + AssertEqual(t, user1.UserUUID.IsNil(), false) + AssertEqual(t, user1.UserUUID.IsEmpty(), false) + DB.Model(&user1).Updates( + map[string]interface{}{"user_uuid": uuid.Nil}, + ) + AssertEqual(t, user1.UserUUID.IsNil(), true) + AssertEqual(t, user1.UserUUID.IsEmpty(), true) + + user2 := users[1] + AssertEqual(t, user2.UserUUID.IsNil(), false) + AssertEqual(t, user2.UserUUID.IsEmpty(), false) + DB.Model(&user2).Updates( + map[string]interface{}{"user_uuid": nil}, + ) + AssertEqual(t, user2.UserUUID.IsNil(), true) + AssertEqual(t, user2.UserUUID.IsEmpty(), true) + } +} + +func TestUUIDPtr(t *testing.T) { + if SupportedDriver("sqlite", "mysql", "postgres", "sqlserver") { + type UserWithUUIDPtr struct { + gorm.Model + Name string + UserUUID *datatypes.UUID + } + + DB.Migrator().DropTable(&UserWithUUIDPtr{}) + if err := DB.Migrator().AutoMigrate(&UserWithUUIDPtr{}); err != nil { + t.Errorf("failed to migrate, got error: %v", err) + } + + user1UUID, uuidGenErr := uuid.NewUUID() + if uuidGenErr != nil { + t.Fatalf("failed to generate a new UUID, got error %v", uuidGenErr) + } + user2UUID, uuidGenErr := uuid.NewUUID() + if uuidGenErr != nil { + t.Fatalf("failed to generate a new UUID, got error %v", uuidGenErr) + } + user3UUID, uuidGenErr := uuid.NewUUID() + if uuidGenErr != nil { + t.Fatalf("failed to generate a new UUID, got error %v", uuidGenErr) + } + user4UUID, uuidGenErr := uuid.NewUUID() + if uuidGenErr != nil { + t.Fatalf("failed to generate a new UUID, got error %v", uuidGenErr) + } + + uuid1 := datatypes.UUID(user1UUID) + uuid2 := datatypes.UUID(user2UUID) + uuid3 := datatypes.UUID(user3UUID) + uuid4 := datatypes.UUID(user4UUID) + + users := []UserWithUUIDPtr{{ + Name: "uuid-1", + UserUUID: &uuid1, + }, { + Name: "uuid-2", + UserUUID: &uuid2, + }, { + Name: "uuid-3", + UserUUID: &uuid3, + }, { + Name: "uuid-4", + UserUUID: &uuid4, + }} + + if err := DB.Create(&users).Error; err != nil { + t.Errorf("Failed to create users %v", err) + } + + for _, user := range users { + result := UserWithUUIDPtr{} + if err := DB.First( + &result, "name = ? AND user_uuid = ?", + user.Name, + *user.UserUUID, + ).Error; err != nil { + t.Fatalf("failed to find user with uuid, got error: %v", err) + } + AssertEqual(t, !result.UserUUID.IsEmpty(), true) + AssertEqual(t, user.UserUUID, result.UserUUID) + valueUser, err := user.UserUUID.Value() + if err != nil { + t.Fatalf("failed to get user value, got error: %v", err) + } + valueResult, err := result.UserUUID.Value() + if err != nil { + t.Fatalf("failed to get result value, got error: %v", err) + } + AssertEqual(t, valueUser, valueResult) + } + + user1 := users[0] + AssertEqual(t, user1.UserUUID.IsNilPtr(), false) + AssertEqual(t, user1.UserUUID.IsEmptyPtr(), false) + DB.Model(&user1).Updates(map[string]interface{}{"user_uuid": nil}) + AssertEqual(t, user1.UserUUID.IsNilPtr(), true) + AssertEqual(t, user1.UserUUID.IsEmptyPtr(), true) + } +}