From 66412447348b965fc567edb4ec5ccbdb91bcc00f Mon Sep 17 00:00:00 2001 From: Jim Date: Wed, 24 Jun 2020 12:33:49 -0400 Subject: [PATCH] add support for WithVersion option to updates (#126) --- internal/db/db_test/db_test.pb.go | 98 +++++++++++-------- internal/db/domains_test.go | 64 ++++++++++++ internal/db/migrations/postgres.gen.go | 44 ++++++++- .../postgres/01_domain_types.down.sql | 2 + .../postgres/01_domain_types.up.sql | 34 ++++++- internal/db/migrations/postgres/03_db.up.sql | 8 +- internal/db/option.go | 10 ++ internal/db/option_test.go | 12 +++ internal/db/read_writer.go | 17 +++- internal/db/read_writer_test.go | 78 ++++++++++++++- .../storage/db/db_test/v1/db_test.proto | 6 ++ 11 files changed, 323 insertions(+), 50 deletions(-) diff --git a/internal/db/db_test/db_test.pb.go b/internal/db/db_test/db_test.pb.go index b91f75ea05..eec2896277 100644 --- a/internal/db/db_test/db_test.pb.go +++ b/internal/db/db_test/db_test.pb.go @@ -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 ( diff --git a/internal/db/domains_test.go b/internal/db/domains_test.go index ac0f319d4d..8803b525bf 100644 --- a/internal/db/domains_test.go +++ b/internal/db/domains_test.go @@ -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) +} diff --git a/internal/db/migrations/postgres.gen.go b/internal/db/migrations/postgres.gen.go index fbe0630b53..a3883023e8 100644 --- a/internal/db/migrations/postgres.gen.go +++ b/internal/db/migrations/postgres.gen.go @@ -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, diff --git a/internal/db/migrations/postgres/01_domain_types.down.sql b/internal/db/migrations/postgres/01_domain_types.down.sql index cc3bd506f9..f48bbb2175 100644 --- a/internal/db/migrations/postgres/01_domain_types.down.sql +++ b/internal/db/migrations/postgres/01_domain_types.down.sql @@ -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; diff --git a/internal/db/migrations/postgres/01_domain_types.up.sql b/internal/db/migrations/postgres/01_domain_types.up.sql index 02f184e9b3..14ef65f12b 100644 --- a/internal/db/migrations/postgres/01_domain_types.up.sql +++ b/internal/db/migrations/postgres/01_domain_types.up.sql @@ -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; diff --git a/internal/db/migrations/postgres/03_db.up.sql b/internal/db/migrations/postgres/03_db.up.sql index b5277d93c0..c80b900a0b 100644 --- a/internal/db/migrations/postgres/03_db.up.sql +++ b/internal/db/migrations/postgres/03_db.up.sql @@ -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, diff --git a/internal/db/option.go b/internal/db/option.go index 1e3d313007..d86f7a446a 100644 --- a/internal/db/option.go +++ b/internal/db/option.go @@ -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 + } +} diff --git a/internal/db/option_test.go b/internal/db/option_test.go index 5836b216ed..5a1b3c1c95 100644 --- a/internal/db/option_test.go +++ b/internal/db/option_test.go @@ -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) + }) } diff --git a/internal/db/read_writer.go b/internal/db/read_writer.go index ed2ec12037..3b2ddc1a54 100644 --- a/internal/db/read_writer.go +++ b/internal/db/read_writer.go @@ -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) diff --git a/internal/db/read_writer_test.go b/internal/db/read_writer_test.go index 37f73a5b39..614a65a2ff 100644 --- a/internal/db/read_writer_test.go +++ b/internal/db/read_writer_test.go @@ -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() diff --git a/internal/proto/local/controller/storage/db/db_test/v1/db_test.proto b/internal/proto/local/controller/storage/db/db_test/v1/db_test.proto index 06455050f1..7f277d018e 100644 --- a/internal/proto/local/controller/storage/db/db_test/v1/db_test.proto +++ b/internal/proto/local/controller/storage/db/db_test/v1/db_test.proto @@ -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 {