add support for WithVersion option to updates (#126)

pull/145/head
Jim 6 years ago committed by GitHub
parent c110c86ee5
commit 6641244734
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -49,9 +49,13 @@ type StoreTestUser struct {
// name is the optional friendly name used to
// access the Scope via an API
// @inject_tag: `gorm:"default:null"`
Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"`
PhoneNumber string `protobuf:"bytes,6,opt,name=phone_number,json=phoneNumber,proto3" json:"phone_number,omitempty"`
Email string `protobuf:"bytes,7,opt,name=email,proto3" json:"email,omitempty"`
Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"`
// @inject_tag: `gorm:"default:null"`
PhoneNumber string `protobuf:"bytes,6,opt,name=phone_number,json=phoneNumber,proto3" json:"phone_number,omitempty" gorm:"default:null"`
// @inject_tag: `gorm:"default:null"`
Email string `protobuf:"bytes,7,opt,name=email,proto3" json:"email,omitempty" gorm:"default:null"`
// @inject_tag: `gorm:"default:null"`
Version uint32 `protobuf:"varint,8,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"`
}
func (x *StoreTestUser) Reset() {
@ -135,6 +139,13 @@ func (x *StoreTestUser) GetEmail() string {
return ""
}
func (x *StoreTestUser) GetVersion() uint32 {
if x != nil {
return x.Version
}
return 0
}
// TestCar for gorm test car model
type StoreTestCar struct {
state protoimpl.MessageState
@ -356,7 +367,7 @@ var file_controller_storage_db_db_test_v1_db_test_proto_rawDesc = []byte{
0x76, 0x31, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73,
0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x22, 0xa3, 0x02, 0x0a, 0x0d, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x54, 0x65, 0x73,
0x6f, 0x74, 0x6f, 0x22, 0xbd, 0x02, 0x0a, 0x0d, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x54, 0x65, 0x73,
0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f,
0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e,
@ -374,47 +385,48 @@ var file_controller_storage_db_db_test_v1_db_test_proto_rawDesc = []byte{
0x12, 0x21, 0x0a, 0x0c, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72,
0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x4e, 0x75, 0x6d,
0x62, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01,
0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x91, 0x02, 0x0a, 0x0c, 0x53, 0x74,
0x6f, 0x72, 0x65, 0x54, 0x65, 0x73, 0x74, 0x43, 0x61, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72,
0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f,
0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76,
0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65,
0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74,
0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63,
0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54,
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65,
0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69,
0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49,
0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52,
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x06,
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x6d,
0x70, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x6d, 0x70, 0x67, 0x22, 0x9c, 0x02,
0x0a, 0x0f, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x54, 0x65, 0x73, 0x74, 0x52, 0x65, 0x6e, 0x74, 0x61,
0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69,
0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c,
0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72,
0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73,
0x69, 0x6f, 0x6e, 0x22, 0x91, 0x02, 0x0a, 0x0c, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x54, 0x65, 0x73,
0x74, 0x43, 0x61, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d,
0x52, 0x02, 0x69, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74,
0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74,
0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74,
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65,
0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d,
0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65,
0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c,
0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65,
0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b,
0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72,
0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,
0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70,
0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x07,
0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x75,
0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x63, 0x61, 0x72, 0x5f, 0x69, 0x64, 0x18,
0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x63, 0x61, 0x72, 0x49, 0x64, 0x42, 0x3d, 0x5a, 0x3b,
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69,
0x63, 0x6f, 0x72, 0x70, 0x2f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2f,
0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x64, 0x62, 0x2f, 0x64, 0x62, 0x5f, 0x74,
0x65, 0x73, 0x74, 0x3b, 0x64, 0x62, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b,
0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28,
0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12,
0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x70, 0x67, 0x18, 0x07, 0x20, 0x01,
0x28, 0x05, 0x52, 0x03, 0x6d, 0x70, 0x67, 0x22, 0x9c, 0x02, 0x0a, 0x0f, 0x53, 0x74, 0x6f, 0x72,
0x65, 0x54, 0x65, 0x73, 0x74, 0x52, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63,
0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74,
0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e,
0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72,
0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61,
0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e,
0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61,
0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e,
0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74,
0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63,
0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12,
0x15, 0x0a, 0x06, 0x63, 0x61, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52,
0x05, 0x63, 0x61, 0x72, 0x49, 0x64, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x77,
0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
0x61, 0x6c, 0x2f, 0x64, 0x62, 0x2f, 0x64, 0x62, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x3b, 0x64, 0x62,
0x5f, 0x74, 0x65, 0x73, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (

@ -7,6 +7,7 @@ import (
"github.com/golang-sql/civil"
_ "github.com/jackc/pgx/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDomain_PublicId(t *testing.T) {
@ -316,3 +317,66 @@ set update_time = null;
nextUpdated := civil.DateTimeOf(nextUpdateTime)
assert.True(nextUpdated.After(updated))
}
func TestDomain_wt_version(t *testing.T) {
const (
createTable = `
create table if not exists test_table_wt_version(
public_id bigint generated always as identity primary key,
name text,
version wt_version
);
`
addTrigger = `
create trigger
update_version_column
after update on test_table_wt_version
for each row execute procedure update_version_column();
`
insert = `
insert into test_table_wt_version (name)
values ($1)
returning public_id;
`
update = `update test_table_wt_version set name = $1 where public_id = $2;`
updateVersion = `update test_table_wt_version set version = $1 where public_id = $2;`
search = `select version from test_table_wt_version where public_id = $1`
)
cleanup, conn, _ := TestSetup(t, "postgres")
assert, require := assert.New(t), require.New(t)
defer func() {
assert.NoError(conn.Close())
assert.NoError(cleanup())
}()
db := conn.DB()
_, err := db.Exec(createTable)
require.NoError(err)
_, err = db.Exec(addTrigger)
require.NoError(err)
var id int
err = db.QueryRow(insert, "alice").Scan(&id)
require.NoError(err)
require.NotEmpty(id)
var v int
err = db.QueryRow(search, id).Scan(&v)
require.NoError(err)
assert.Equal(1, v)
_, err = db.Exec(update, "bob", id)
require.NoError(err)
err = db.QueryRow(search, id).Scan(&v)
require.NoError(err)
assert.Equal(2, v)
_, err = db.Exec(updateVersion, 22, id)
require.NoError(err)
err = db.QueryRow(search, id).Scan(&v)
require.NoError(err)
assert.Equal(3, v)
}

@ -12,10 +12,12 @@ begin;
drop domain wt_timestamp;
drop domain wt_public_id;
drop domain wt_version;
drop function default_create_time;
drop function immutable_create_time_func;
drop function update_time_column;
drop function update_version_column;
commit;
@ -39,7 +41,6 @@ create domain wt_timestamp as
comment on domain wt_timestamp is
'Standard timestamp for all create_time and update_time columns';
create or replace function
update_time_column()
returns trigger
@ -95,6 +96,39 @@ comment on function
is
'function used in before insert triggers to set create_time column to now';
create domain wt_version as bigint
default 1
check(
value > 0
);
comment on domain wt_version is
'standard column for row version';
-- update_version_column() will increment the version column whenever row data
-- is updated and should only be used in an update after trigger. This function
-- will overwrite any explicit updates to the version column.
create or replace function
update_version_column()
returns trigger
as $$
begin
if pg_trigger_depth() = 1 then
if row(new.*) is distinct from row(old.*) then
execute format('update %I set version = $1 where public_id = $2', tg_relid::regclass) using old.version+1, new.public_id;
new.version = old.version + 1;
return new;
end if;
end if;
return new;
end;
$$ language plpgsql;
comment on function
update_version_column()
is
'function used in after update triggers to properly set version columns';
commit;
`),
@ -250,7 +284,8 @@ create table if not exists db_test_user (
public_id text not null unique,
name text unique,
phone_number text,
email text
email text,
version wt_version
);
create trigger
@ -271,6 +306,11 @@ before
insert on db_test_user
for each row execute procedure default_create_time();
create trigger
update_version_column
after update on db_test_user
for each row execute procedure update_version_column();
create table if not exists db_test_car (
id bigint generated always as identity primary key,
create_time wt_timestamp,

@ -2,9 +2,11 @@ begin;
drop domain wt_timestamp;
drop domain wt_public_id;
drop domain wt_version;
drop function default_create_time;
drop function immutable_create_time_func;
drop function update_time_column;
drop function update_version_column;
commit;

@ -13,7 +13,6 @@ create domain wt_timestamp as
comment on domain wt_timestamp is
'Standard timestamp for all create_time and update_time columns';
create or replace function
update_time_column()
returns trigger
@ -69,4 +68,37 @@ comment on function
is
'function used in before insert triggers to set create_time column to now';
create domain wt_version as bigint
default 1
check(
value > 0
);
comment on domain wt_version is
'standard column for row version';
-- update_version_column() will increment the version column whenever row data
-- is updated and should only be used in an update after trigger. This function
-- will overwrite any explicit updates to the version column.
create or replace function
update_version_column()
returns trigger
as $$
begin
if pg_trigger_depth() = 1 then
if row(new.*) is distinct from row(old.*) then
execute format('update %I set version = $1 where public_id = $2', tg_relid::regclass) using old.version+1, new.public_id;
new.version = old.version + 1;
return new;
end if;
end if;
return new;
end;
$$ language plpgsql;
comment on function
update_version_column()
is
'function used in after update triggers to properly set version columns';
commit;

@ -10,7 +10,8 @@ create table if not exists db_test_user (
public_id text not null unique,
name text unique,
phone_number text,
email text
email text,
version wt_version
);
create trigger
@ -31,6 +32,11 @@ before
insert on db_test_user
for each row execute procedure default_create_time();
create trigger
update_version_column
after update on db_test_user
for each row execute procedure update_version_column();
create table if not exists db_test_car (
id bigint generated always as identity primary key,
create_time wt_timestamp,

@ -28,6 +28,8 @@ type Options struct {
WithFieldMaskPaths []string
// WithNullPaths must be accessible from other packages.
WithNullPaths []string
// WithVersion must be accessible from other packages
WithVersion int
}
type oplogOpts struct {
@ -46,6 +48,7 @@ func getDefaultOptions() Options {
WithFieldMaskPaths: []string{},
WithNullPaths: []string{},
WithLimit: 0,
WithVersion: 0,
}
}
@ -89,3 +92,10 @@ func WithLimit(limit int) Option {
o.WithLimit = limit
}
}
// WithVersion provides an option version number for update operations.
func WithVersion(version int) Option {
return func(o *Options) {
o.WithVersion = version
}
}

@ -94,4 +94,16 @@ func Test_getOpts(t *testing.T) {
testOpts.WithLimit = 1
assert.Equal(opts, testOpts)
})
t.Run("WithVersion", func(t *testing.T) {
assert := assert.New(t)
// test default of 0
opts := GetOpts()
testOpts := getDefaultOptions()
testOpts.WithVersion = 0
assert.Equal(opts, testOpts)
opts = GetOpts(WithVersion(2))
testOpts = getDefaultOptions()
testOpts.WithVersion = 2
assert.Equal(opts, testOpts)
})
}

@ -227,7 +227,11 @@ func (rw *Db) Create(ctx context.Context, i interface{}, opt ...Option) error {
// is responsible for the transaction life cycle of the writer and if an
// error is returned the caller must decide what to do with the transaction,
// which almost always should be to rollback. Update returns the number of
// rows updated. Supported options: WithOplog.
// rows updated. Supported options: WithOplog and WithVersion. If WithVersion
// is used, then the update will include the version number in the update where
// clause, which basically makes the update use optimistic locking and the
// update will only succeed if the existing rows version matches the WithVersion
// option.
func (rw *Db) Update(ctx context.Context, i interface{}, fieldMaskPaths []string, setToNullPaths []string, opt ...Option) (int, error) {
if rw.underlying == nil {
return NoRowsAffected, fmt.Errorf("update: missing underlying db %w", ErrNilParameter)
@ -289,7 +293,16 @@ func (rw *Db) Update(ctx context.Context, i interface{}, fieldMaskPaths []string
return NoRowsAffected, fmt.Errorf("update: unable to get ticket: %w", err)
}
}
underlying := rw.underlying.Model(i).Updates(updateFields)
var underlying *gorm.DB
switch {
case opts.WithVersion > 0:
if _, ok := scope.FieldByName("version"); !ok {
return NoRowsAffected, fmt.Errorf("update: %s does not have a version field", scope.TableName())
}
underlying = rw.underlying.Model(i).Where("version = ?", opts.WithVersion).Updates(updateFields)
default:
underlying = rw.underlying.Model(i).Updates(updateFields)
}
if underlying.Error != nil {
if err == gorm.ErrRecordNotFound {
return NoRowsAffected, fmt.Errorf("update: failed %w", ErrRecordNotFound)

@ -53,6 +53,7 @@ func TestDb_Update(t *testing.T) {
wantName string
wantEmail string
wantPhoneNumber string
wantVersion int
}{
{
name: "simple",
@ -74,6 +75,46 @@ func TestDb_Update(t *testing.T) {
wantEmail: "",
wantPhoneNumber: "updated" + id,
},
{
name: "simple-with-bad-version",
args: args{
i: &db_test.TestUser{
StoreTestUser: &db_test.StoreTestUser{
Name: "simple-with-bad-version" + id,
Email: "updated" + id,
PhoneNumber: "updated" + id,
},
},
fieldMaskPaths: []string{"Name", "PhoneNumber"},
setToNullPaths: []string{"Email"},
opt: []Option{WithVersion(22)},
},
want: 0,
wantErr: false,
wantErrMsg: "",
},
{
name: "simple-with-version",
args: args{
i: &db_test.TestUser{
StoreTestUser: &db_test.StoreTestUser{
Name: "simple-with-version" + id,
Email: "updated" + id,
PhoneNumber: "updated" + id,
},
},
fieldMaskPaths: []string{"Name", "PhoneNumber"},
setToNullPaths: []string{"Email"},
opt: []Option{WithVersion(1)},
},
want: 1,
wantErr: false,
wantErrMsg: "",
wantName: "simple-with-version" + id,
wantEmail: "",
wantPhoneNumber: "updated" + id,
wantVersion: 2,
},
{
name: "multiple-null",
args: args{
@ -204,7 +245,9 @@ func TestDb_Update(t *testing.T) {
}
assert.NoError(err)
assert.Equal(tt.want, rowsUpdated)
if tt.want == 0 {
return
}
foundUser, err := db_test.NewTestUser()
assert.NoError(err)
foundUser.PublicId = tt.args.i.PublicId
@ -225,8 +268,21 @@ func TestDb_Update(t *testing.T) {
assert.NotEqual(now, foundUser.CreateTime)
assert.NotEqual(now, foundUser.UpdateTime)
assert.NotEqual(publicId, foundUser.PublicId)
assert.Equal(u.Version+1, foundUser.Version)
})
}
t.Run("no-version-field", func(t *testing.T) {
assert := assert.New(t)
w := Db{underlying: db}
id, err := uuid.GenerateUUID()
assert.NoError(err)
car := testCar(t, db, "foo-"+id, id, int32(100))
car.Name = "friendly-" + id
rowsUpdated, err := w.Update(context.Background(), car, []string{"Name"}, nil, WithVersion(1))
assert.Error(err)
assert.Equal(0, rowsUpdated)
})
t.Run("valid-WithOplog", func(t *testing.T) {
assert := assert.New(t)
w := Db{underlying: db}
@ -1201,6 +1257,26 @@ func testUser(t *testing.T, conn *gorm.DB, name, email, phoneNumber string) *db_
}
return u
}
func testCar(t *testing.T, conn *gorm.DB, name, model string, mpg int32) *db_test.TestCar {
t.Helper()
require := require.New(t)
publicId, err := base62.Random(20)
require.NoError(err)
c := &db_test.TestCar{
StoreTestCar: &db_test.StoreTestCar{
PublicId: publicId,
Name: name,
Model: model,
Mpg: mpg,
},
}
if conn != nil {
err = conn.Create(c).Error
require.NoError(err)
}
return c
}
func testId(t *testing.T) string {
t.Helper()

@ -29,8 +29,14 @@ message StoreTestUser {
// @inject_tag: `gorm:"default:null"`
string name = 5;
// @inject_tag: `gorm:"default:null"`
string phone_number = 6;
// @inject_tag: `gorm:"default:null"`
string email = 7;
// @inject_tag: `gorm:"default:null"`
uint32 version = 8;
}
// TestCar for gorm test car model
message StoreTestCar {

Loading…
Cancel
Save