-
-
Notifications
You must be signed in to change notification settings - Fork 110
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature: Add datatype BinUUID for uuid as binary storage in DB (#264)
This adds a new datatype BinUUID, similar to datatypes.UUID but with a major difference being that BinUUID stores the uuid in the database as a binary (byte) array instead of a string. Developers may use either datatypes.UUID or datatypes.BinUUID as per their preference, either storing uuid in the database as a string or as a binary (byte) array respectively.
- Loading branch information
1 parent
e8a383d
commit 48115e5
Showing
4 changed files
with
326 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
package datatypes | ||
|
||
import ( | ||
"bytes" | ||
"database/sql/driver" | ||
"errors" | ||
|
||
"github.com/google/uuid" | ||
"gorm.io/gorm" | ||
"gorm.io/gorm/schema" | ||
) | ||
|
||
// This datatype is similar to datatypes.UUID, major difference being that | ||
// this datatype stores the uuid in the database as a binary (byte) array | ||
// instead of a string. Developers may use either as per their preference. | ||
type BinUUID uuid.UUID | ||
|
||
// NewBinUUIDv1 generates a uuid version 1, panics on generation failure. | ||
func NewBinUUIDv1() BinUUID { | ||
return BinUUID(uuid.Must(uuid.NewUUID())) | ||
} | ||
|
||
// NewBinUUIDv4 generates a uuid version 4, panics on generation failure. | ||
func NewBinUUIDv4() BinUUID { | ||
return BinUUID(uuid.Must(uuid.NewRandom())) | ||
} | ||
|
||
// BinUUIDFromString returns the BinUUID representation of the specified uuidStr. | ||
func BinUUIDFromString(uuidStr string) BinUUID { | ||
return BinUUID(uuid.MustParse(uuidStr)) | ||
} | ||
|
||
// GormDataType gorm common data type. | ||
func (BinUUID) GormDataType() string { | ||
return "BINARY(16)" | ||
} | ||
|
||
// GormDBDataType gorm db data type. | ||
func (BinUUID) GormDBDataType(db *gorm.DB, field *schema.Field) string { | ||
switch db.Dialector.Name() { | ||
case "mysql": | ||
return "BINARY(16)" | ||
case "postgres": | ||
return "BYTEA" | ||
case "sqlserver": | ||
return "BINARY(16)" | ||
case "sqlite": | ||
return "BLOB" | ||
default: | ||
return "" | ||
} | ||
} | ||
|
||
// Scan is the scanner function for this datatype. | ||
func (u *BinUUID) Scan(value interface{}) error { | ||
valueBytes, ok := value.([]byte) | ||
if !ok { | ||
return errors.New("unable to convert value to bytes") | ||
} | ||
valueUUID, err := uuid.FromBytes(valueBytes) | ||
if err != nil { | ||
return err | ||
} | ||
*u = BinUUID(valueUUID) | ||
return nil | ||
} | ||
|
||
// Value is the valuer function for this datatype. | ||
func (u BinUUID) Value() (driver.Value, error) { | ||
return uuid.UUID(u).MarshalBinary() | ||
} | ||
|
||
// String returns the string form of the UUID. | ||
func (u BinUUID) Bytes() []byte { | ||
bytes, err := uuid.UUID(u).MarshalBinary() | ||
if err != nil { | ||
return nil | ||
} | ||
return bytes | ||
} | ||
|
||
// String returns the string form of the UUID. | ||
func (u BinUUID) String() string { | ||
return uuid.UUID(u).String() | ||
} | ||
|
||
// Equals returns true if bytes form of BinUUID matches other, false otherwise. | ||
func (u BinUUID) Equals(other BinUUID) bool { | ||
return bytes.Equal(u.Bytes(), other.Bytes()) | ||
} | ||
|
||
// Length returns the number of characters in string form of UUID. | ||
func (u BinUUID) LengthBytes() int { | ||
return len(u.Bytes()) | ||
} | ||
|
||
// Length returns the number of characters in string form of UUID. | ||
func (u BinUUID) Length() int { | ||
return len(u.String()) | ||
} | ||
|
||
// IsNil returns true if the BinUUID is nil uuid (all zeroes), false otherwise. | ||
func (u BinUUID) IsNil() bool { | ||
return uuid.UUID(u) == uuid.Nil | ||
} | ||
|
||
// IsEmpty returns true if BinUUID is nil uuid or of zero length, false otherwise. | ||
func (u BinUUID) IsEmpty() bool { | ||
return u.IsNil() || u.Length() == 0 | ||
} | ||
|
||
// IsNilPtr returns true if caller BinUUID ptr is nil, false otherwise. | ||
func (u *BinUUID) IsNilPtr() bool { | ||
return u == nil | ||
} | ||
|
||
// IsEmptyPtr returns true if caller BinUUID ptr is nil or it's value is empty. | ||
func (u *BinUUID) IsEmptyPtr() bool { | ||
return u.IsNilPtr() || u.IsEmpty() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
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.BinUUID{} | ||
|
||
func TestBinUUID(t *testing.T) { | ||
if SupportedDriver("sqlite", "mysql", "postgres", "sqlserver") { | ||
type UserWithBinUUID struct { | ||
gorm.Model | ||
Name string | ||
UserUUID datatypes.BinUUID | ||
} | ||
|
||
DB.Migrator().DropTable(&UserWithBinUUID{}) | ||
if err := DB.Migrator().AutoMigrate(&UserWithBinUUID{}); err != nil { | ||
t.Errorf("failed to migrate, got error: %v", err) | ||
} | ||
|
||
users := []UserWithBinUUID{{ | ||
Name: "uuid-1", | ||
UserUUID: datatypes.NewBinUUIDv1(), | ||
}, { | ||
Name: "uuid-2", | ||
UserUUID: datatypes.NewBinUUIDv1(), | ||
}, { | ||
Name: "uuid-3", | ||
UserUUID: datatypes.NewBinUUIDv4(), | ||
}, { | ||
Name: "uuid-4", | ||
UserUUID: datatypes.NewBinUUIDv4(), | ||
}} | ||
|
||
if err := DB.Create(&users).Error; err != nil { | ||
t.Errorf("Failed to create users %v", err) | ||
} | ||
|
||
for _, user := range users { | ||
result := UserWithBinUUID{} | ||
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) | ||
AssertEqual(t, user.UserUUID.LengthBytes(), 16) | ||
AssertEqual(t, user.UserUUID.Length(), 36) | ||
} | ||
|
||
var tx *gorm.DB | ||
user1 := users[0] | ||
AssertEqual(t, user1.UserUUID.IsNil(), false) | ||
AssertEqual(t, user1.UserUUID.IsEmpty(), false) | ||
tx = DB.Model(&user1).Updates( | ||
map[string]interface{}{ | ||
"user_uuid": datatypes.BinUUIDFromString(uuid.Nil.String()), | ||
}, | ||
) | ||
AssertEqual(t, tx.Error, nil) | ||
AssertEqual(t, user1.UserUUID.IsNil(), true) | ||
AssertEqual(t, user1.UserUUID.IsEmpty(), true) | ||
user1NewUUID := datatypes.NewBinUUIDv4() | ||
tx = DB.Model(&user1).Updates( | ||
map[string]interface{}{ | ||
"user_uuid": user1NewUUID, | ||
}, | ||
) | ||
AssertEqual(t, tx.Error, nil) | ||
AssertEqual(t, user1.UserUUID, user1NewUUID) | ||
|
||
user2 := users[1] | ||
AssertEqual(t, user2.UserUUID.IsNil(), false) | ||
AssertEqual(t, user2.UserUUID.IsEmpty(), false) | ||
tx = DB.Model(&user2).Updates( | ||
map[string]interface{}{"user_uuid": nil}, | ||
) | ||
AssertEqual(t, tx.Error, nil) | ||
AssertEqual(t, user2.UserUUID.IsNil(), true) | ||
AssertEqual(t, user2.UserUUID.IsEmpty(), true) | ||
user2NewUUID := datatypes.NewBinUUIDv4() | ||
tx = DB.Model(&user2).Updates( | ||
map[string]interface{}{ | ||
"user_uuid": user2NewUUID, | ||
}, | ||
) | ||
AssertEqual(t, tx.Error, nil) | ||
AssertEqual(t, user2.UserUUID, user2NewUUID) | ||
} | ||
} | ||
|
||
func TestBinUUIDPtr(t *testing.T) { | ||
if SupportedDriver("sqlite", "mysql", "postgres", "sqlserver") { | ||
type UserWithBinUUIDPtr struct { | ||
gorm.Model | ||
Name string | ||
UserUUID *datatypes.BinUUID | ||
} | ||
|
||
DB.Migrator().DropTable(&UserWithBinUUIDPtr{}) | ||
if err := DB.Migrator().AutoMigrate(&UserWithBinUUIDPtr{}); err != nil { | ||
t.Errorf("failed to migrate, got error: %v", err) | ||
} | ||
|
||
uuid1 := datatypes.NewBinUUIDv1() | ||
uuid2 := datatypes.NewBinUUIDv1() | ||
uuid3 := datatypes.NewBinUUIDv4() | ||
uuid4 := datatypes.NewBinUUIDv4() | ||
|
||
users := []UserWithBinUUIDPtr{{ | ||
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 := UserWithBinUUIDPtr{} | ||
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) | ||
AssertEqual(t, user.UserUUID.LengthBytes(), 16) | ||
AssertEqual(t, user.UserUUID.Length(), 36) | ||
} | ||
|
||
user1 := users[0] | ||
AssertEqual(t, user1.UserUUID.IsNilPtr(), false) | ||
AssertEqual(t, user1.UserUUID.IsEmptyPtr(), false) | ||
tx := DB.Model(&user1).Updates(map[string]interface{}{"user_uuid": nil}) | ||
AssertEqual(t, tx.Error, nil) | ||
AssertEqual(t, user1.UserUUID.IsNilPtr(), true) | ||
AssertEqual(t, user1.UserUUID.IsEmptyPtr(), true) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters