Skip to content

Commit

Permalink
feature: Add datatype BinUUID for uuid as binary storage in DB (#264)
Browse files Browse the repository at this point in the history
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
omkar-foss authored Sep 14, 2024
1 parent e8a383d commit 48115e5
Show file tree
Hide file tree
Showing 4 changed files with 326 additions and 3 deletions.
120 changes: 120 additions & 0 deletions binuuid.go
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()
}
179 changes: 179 additions & 0 deletions binuuid_test.go
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)
}
}
2 changes: 2 additions & 0 deletions uuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"gorm.io/gorm/schema"
)

// This datatype stores the uuid in the database as a string. To store the uuid
// in the database as a binary (byte) array, please refer to datatypes.BinUUID.
type UUID uuid.UUID

// NewUUIDv1 generates a UUID version 1, panics on generation failure.
Expand Down
28 changes: 25 additions & 3 deletions uuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,25 +63,45 @@ func TestUUID(t *testing.T) {
t.Fatalf("failed to get result value, got error: %v", err)
}
AssertEqual(t, valueUser, valueResult)
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)
DB.Model(&user1).Updates(
tx = DB.Model(&user1).Updates(
map[string]interface{}{"user_uuid": uuid.Nil},
)
AssertEqual(t, tx.Error, nil)
AssertEqual(t, user1.UserUUID.IsNil(), true)
AssertEqual(t, user1.UserUUID.IsEmpty(), true)
user1NewUUID := datatypes.NewUUIDv4()
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)
DB.Model(&user2).Updates(
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.NewUUIDv4()
tx = DB.Model(&user2).Updates(
map[string]interface{}{
"user_uuid": user2NewUUID,
},
)
AssertEqual(t, tx.Error, nil)
AssertEqual(t, user2.UserUUID, user2NewUUID)
}
}

Expand Down Expand Up @@ -141,12 +161,14 @@ func TestUUIDPtr(t *testing.T) {
t.Fatalf("failed to get result value, got error: %v", err)
}
AssertEqual(t, valueUser, valueResult)
AssertEqual(t, user.UserUUID.Length(), 36)
}

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})
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)
}
Expand Down

0 comments on commit 48115e5

Please sign in to comment.