diff --git a/structure/set.go b/structure/set.go new file mode 100644 index 00000000..f3b71ed2 --- /dev/null +++ b/structure/set.go @@ -0,0 +1,397 @@ +package structure + +import ( + "errors" + "github.com/ByteStorage/FlyDB/config" + "github.com/ByteStorage/FlyDB/engine" + _const "github.com/ByteStorage/FlyDB/lib/const" + "github.com/ByteStorage/FlyDB/lib/encoding" +) + +type SetStructure struct { + db *engine.DB +} + +var ( + ErrMemberNotFound = errors.New("ErrMemberNotFound: member not found") + ErrSetNotInitialized = errors.New("wrong operation: set not initialized") +) + +// FSets serves as a set data structure where every key in the map is an element of the set. +type FSets map[string]struct{} + +func NewSetStructure(options config.Options) (*SetStructure, error) { + db, err := engine.NewDB(options) + if err != nil { + return nil, err + } + return &SetStructure{db: db}, nil +} + +// SAdd adds a member to the set stored at key. +// +// If the set did not exist, a new set will be created +// and the member will be added to it. +func (s *SetStructure) SAdd(key, member string) error { + return s.SAdds(key, member) +} + +/* +SAdds attempts to add multiple members to a set identified by the 'key' parameter. The members to add are variadic and come as 'members...' in the function signature. This means the function accepts an arbitrary number of strings to be added to the set. + +Parameters: + - 's': Pointer to an instance of SetStructure. This is the receiver of the SAdds function. + - 'key': String that is used as the identifier for the set. + - 'members': Variadic parameter that represents the members to be added. Since it's a variadic parameter, it can be any number of strings. + +Return: + - It returns an error if it encounters one during execution. Errors might occur if the key is found to be empty or if problems arise when retrieving the set from the database or saving it back. + +Internal logic: + 1. The function first checks if the provided key is an empty string. If so, it returns an error indicating that the key is empty. + 2. Converts the key into bytes format for internal use. + 3. It tries to get the set associated with the key from the database. If it encounters an error during retrieval, it returns that error. + 4. If it can successfully retrieve the set, it attempts to add the members to the set. + 5. After adding the members, it tries to save this updated set back into the database. If this operation throws an error, it returns that error. + +All the methods like 'getZSetFromDB', 'setZSetToDB', and 'add' handle the lower-level logic associated with database interaction and set manipulation. +*/ +func (s *SetStructure) SAdds(key string, members ...string) error { + fs, err := s.checkAndGetSet(key, true) + if err != nil { + return err + } + fs.add(members...) + return s.setSetToDB(stringToBytesWithKey(key), fs) +} + +// SRem removes a member from a set +func (s *SetStructure) SRem(key, member string) error { + return s.SRems(key, member) +} + +// SRems removes multiple members from a set +func (s *SetStructure) SRems(key string, members ...string) error { + fs, err := s.checkAndGetSet(key, false) + if err != nil { + return err + } + if err = fs.remove(members...); err != nil { + return err + } + return s.setSetToDB(stringToBytesWithKey(key), fs) +} + +// SCard gets the cardinality (size) of a set +func (s *SetStructure) SCard(key string) (int, error) { + fs, err := s.checkAndGetSet(key, false) + if err != nil { + return -1, err + } + return len(*fs), nil +} + +// SMembers gets all members of a set identified by the provided key. +// If the set does not exist or the key belongs to a different data type, +// the function will return an error. +// It returns a slice of strings which are the members of the set. +func (s *SetStructure) SMembers(key string) ([]string, error) { + fs, err := s.checkAndGetSet(key, false) + if err != nil { + return nil, err + } + // type of fs is map[string]struct{} + // we need to take the keys and make them a slice + var members []string + for k := range *fs { + members = append(members, k) + } + + return members, nil +} + +// SIsMember checks if a member exists in a set +func (s *SetStructure) SIsMember(key, member string) (bool, error) { + fs, err := s.checkAndGetSet(key, false) + if err != nil { + return false, err + } + + return fs.exists(member), nil +} + +// SUnion gets the union of multiple sets +func (s *SetStructure) SUnion(keys ...string) ([]string, error) { + // if there are no keys provided, then there's no union + if len(keys) == 0 { + return nil, nil + } + mem := make(map[string]struct{}) + var members []string + for _, key := range keys { + fs, err := s.checkAndGetSet(key, false) + if err != nil { + return nil, err + } + for k := range *fs { + if _, ok := mem[k]; !ok { + mem[k] = struct{}{} + members = append(members, k) + } + } + } + return members, nil +} + +// SInter returns the intersection of multiple sets. +// The parameter 'keys' is a variadic parameter, meaning +// it can accept any number of arguments. These arguments are the keys +// of the sets that are to be intersected. +func (s *SetStructure) SInter(keys ...string) ([]string, error) { + // if there are no keys, then there's no intersection + if len(keys) == 0 { + return nil, nil + } + // All elements of 'first' are stored in 'mem' map because it's checked + // against each of the subsequent sets. + first, err := s.checkAndGetSet(keys[0], false) + if err != nil { + return nil, err + } + + mem := make(map[string]struct{}) + for k := range *first { + mem[k] = struct{}{} + } + + var members []string + // For each other key, we get its set and its members. + for _, key := range keys[1:] { + fs, err := s.checkAndGetSet(key, false) + if err != nil { + return nil, err + } + // We check if each member of the current set is in 'mem'. + // If yes, we add it to 'members' array, and remove it from 'mem' map. + for k := range *fs { + if _, ok := mem[k]; ok { + delete(mem, k) + members = append(members, k) + } + } + } + + return members, nil +} + +// SDiff computes the difference between the first and subsequent sets. +// It returns keys unique to the first set and an error if applicable. +func (s *SetStructure) SDiff(keys ...string) ([]string, error) { + // If there are no keys, then there's no difference + if len(keys) == 0 { + return nil, nil + } + + first, err := s.checkAndGetSet(keys[0], false) + if err != nil { + return nil, err + } + + // Initialize a set for members of first set + mem := make(map[string]struct{}) + for k := range *first { + mem[k] = struct{}{} + } + + var diffMembers []string + // If member is in the first set and also in another set, remove it. + for _, key := range keys[1:] { + fs, err := s.checkAndGetSet(key, false) + if err != nil { + return nil, err + } + for k := range *fs { + // Only delete the existing members in the first set. Do not add members from other sets + delete(mem, k) + } + } + // Remaining members in mem are unique to first set. + for k := range mem { + diffMembers = append(diffMembers, k) + } + + return diffMembers, nil +} + +// SUnionStore calculates and stores the union of multiple sets +// in a destination set. +// +// 'destination' is the name of the set where the result will be stored. +// +// 'keys' is a variadic parameter that represents the names of all the sets +// whose union is to be calculated. +// +// The function returns an error if there's any issue computing the union or +// storing the result in the destination set. +// +// Usage example: +// +// set := NewSetStructure(opts) // initialize t +// err := set.SUnionStore("result", "set1", "set2", "set3") +// if err != nil { +// log.Fatal(err) +// } +func (s *SetStructure) SUnionStore(destination string, keys ...string) error { + union, err := s.SUnion(keys...) + if err != nil { + return err + } + return s.SAdds(destination, union...) +} + +/* +SInterStore stores the intersection of multiple sets in a destination set. + +Function parameters: + - destination (string): The key for the set where the intersection result will be stored. + - keys (string[]): An array of keys for the sets to be intersected. + +This function first uses the SInter function to find the intersection of the provided sets (keys). +In case an error occurs during this process (the SInter function returns an error), it will immediately return that error. +Otherwise, it proceeds to use the SAdds function to store the results of the intersection into the destination set. + +The function will return the error from the SAdds function if it occurs or nil if the operation is successful. + +Errors can occur when there's an issue with accessing the data structures involved in the operations. For instance, trying to perform set operations on non-existing keys or keys that point to non-set data types. + +Returns: + - error: An error object that describes an error that occurred during the computation (if any). +*/ +func (s *SetStructure) SInterStore(destination string, keys ...string) error { + inter, err := s.SInter(keys...) + if err != nil { + return err + } + return s.SAdds(destination, inter...) +} +func (s *SetStructure) checkAndGetSet(key string, createIfNotExist bool) (*FSets, error) { + // Check if value is empty + if err := checkKey(key); err != nil { + return nil, err + } + keyBytes := stringToBytesWithKey(key) + // Get the list + set, err := s.getSetFromDB(keyBytes, createIfNotExist) + if err != nil { + return nil, err + } + + return set, nil +} + +// internal/private functions + +// add adds new elements to the set data structure (FSets). +// It allows one or more string parameters. If an element already exists, it won't be added again. +func (s *FSets) add(member ...string) { + for _, m := range member { + // Check if the member to be added already exists in the FSets or not. + // existence of the element in the FSets needs to be checked, not the value. + if _, exists := (*s)[m]; !exists { + // If the elements do not already exist, add it to the set. + // The value here is an empty struct, because we are only interested in the keys. + (*s)[m] = struct{}{} + } + } +} + +// remove is a method on the FSet struct that takes in a variable number +// of string parameters. It takes these strings to be "members", and generates errors +// if any of these strings are not currently present in the FSet. +// +// Parameters: +// +// member: a variable list of strings that are supposed to be "members" of the FSet. +// +// Returns: +// +// If all of the strings in the members parameter are present in the FSet, +// it then proceeds to remove each of these members from the FSet, +// and returns nil, signifying success. +// +// If any string from the members parameter does not exist in the FSet, +// the function immediately returns an ErrInvalidArgs error, +// which signifies that an invalid argument has been provided to the function. +func (s *FSets) remove(member ...string) error { + if s == nil { + return ErrSetNotInitialized + } + for _, m := range member { + // check to see if all members exist + if _, exists := (*s)[m]; !exists { + return ErrMemberNotFound + } + } + // iterate again + for _, m := range member { + // remove elements from FSets + delete(*s, m) + } + return nil +} + +// GetSetFromDB retrieves a set from database given a key. If createIfNotExist is true, +// a new set will be created if the key is not found. It returns the file sets and any write error encountered. +func (s *SetStructure) getSetFromDB(key []byte, createIfNotExist bool) (*FSets, error) { + if s.db == nil { + return nil, ErrSetNotInitialized + } + dbData, err := s.db.Get(key) + var zSetValue FSets + // If key is not found, return nil for both; otherwise return the error. + if err != nil { + if errors.Is(err, _const.ErrKeyNotFound) && createIfNotExist { + return &FSets{}, nil + } + return nil, err + } else { + err = encoding.NewMessagePackDecoder(dbData).Decode(&zSetValue) + if err != nil { + return nil, err + } + return &zSetValue, err + } + // return a pointer to the deserialized ZSetNodes, nil for the error +} + +// setSetToDB +func (s *SetStructure) setSetToDB(key []byte, zSetValue *FSets) error { + val := encoding.NewMessagePackEncoder() + err := val.Encode(zSetValue) + if err != nil { + return err + } + return s.db.Put(key, val.Bytes()) +} + +func (s *SetStructure) exists(key string, member ...string) bool { + if err := checkKey(key); err != nil { + return false + } + keyBytes := stringToBytesWithKey(key) + + zSet, err := s.getSetFromDB(keyBytes, false) + + if err != nil { + return false + } + return zSet.exists(member...) +} +func (s *FSets) exists(member ...string) bool { + for _, s2 := range member { + if _, ok := (*s)[s2]; !ok { + return false + } + } + return true +} diff --git a/structure/set_test.go b/structure/set_test.go new file mode 100644 index 00000000..5e4337a2 --- /dev/null +++ b/structure/set_test.go @@ -0,0 +1,1059 @@ +package structure + +import ( + "errors" + "github.com/ByteStorage/FlyDB/config" + "github.com/ByteStorage/FlyDB/engine" + _const "github.com/ByteStorage/FlyDB/lib/const" + "github.com/stretchr/testify/assert" + "os" + "reflect" + "sort" + "testing" +) + +func initTestSetDb() (*SetStructure, *config.Options) { + opts := config.DefaultOptions + dir, _ := os.MkdirTemp("", "TestSetStructure") + opts.DirPath = dir + str, _ := NewSetStructure(opts) + return str, &opts +} + +func TestSAdd(t *testing.T) { + // Creating a list of cases for testing. + tests := []struct { + name string + setup func(*SetStructure) + key string + membersAdd []string + membersWant []string + wantErr error + }{ + { + name: "test when key empty", + setup: func(s *SetStructure) {}, + key: "", + membersAdd: []string{"key1", "key2"}, + membersWant: []string{}, + wantErr: _const.ErrKeyIsEmpty, + }, + { + name: "test add two members", + setup: func(s *SetStructure) {}, + key: "destination", + membersAdd: []string{"key1", "key2"}, + membersWant: []string{"key1", "key2"}, + wantErr: nil, + }, + { + name: "test add three members one duplicate", + setup: func(s *SetStructure) { + _ = s.SAdds("destination", "key2") + }, + key: "destination", + membersAdd: []string{"key1", "key2"}, + membersWant: []string{"key1", "key2"}, + wantErr: nil, + }, + { + name: "test add db not init", + setup: func(s *SetStructure) { + s.db = nil + }, + key: "destination", + membersAdd: []string{"key1", "key2"}, + membersWant: []string{"key1", "key2"}, + wantErr: ErrSetNotInitialized, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, _ := initTestSetDb() + tt.setup(s) + // Call the method with test case parameters. + for _, s2 := range tt.membersAdd { + err := s.SAdd(tt.key, s2) + if !errors.Is(err, tt.wantErr) { + t.Errorf("SRems() error = %v, wantErr %v", err, tt.wantErr) + } + } + if tt.wantErr == nil { + // positive verify + assert.True(t, s.exists(tt.key, tt.membersWant...)) + } + }) + } + +} + +func TestSAdds(t *testing.T) { + // Creating a list of cases for testing. + tests := []struct { + name string + setup func(*SetStructure) + key string + membersAdd []string + membersWant []string + wantErr error + }{ + { + name: "test when key empty", + setup: func(s *SetStructure) {}, + key: "", + membersAdd: []string{"key1", "key2"}, + membersWant: []string{}, + wantErr: _const.ErrKeyIsEmpty, + }, + { + name: "test add two members", + setup: func(s *SetStructure) {}, + key: "destination", + membersAdd: []string{"key1", "key2"}, + membersWant: []string{"key1", "key2"}, + wantErr: nil, + }, + { + name: "test add three members one duplicate", + setup: func(s *SetStructure) { + _ = s.SAdds("destination", "key2") + }, + key: "destination", + membersAdd: []string{"key1", "key2"}, + membersWant: []string{"key1", "key2"}, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, _ := initTestSetDb() + tt.setup(s) + // Call the method with test case parameters. + err := s.SAdds(tt.key, tt.membersAdd...) + if !errors.Is(err, tt.wantErr) { + t.Errorf("SRems() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.wantErr == nil { + // positive verify + assert.True(t, s.exists(tt.key, tt.membersWant...)) + } + }) + } +} + +func TestSRems(t *testing.T) { + // Creating a list of cases for testing. + tests := []struct { + name string + setup func(*SetStructure) + key string + membersRem []string + membersWant []string + wantErr error + }{ + { + name: "test empty key ", + setup: func(s *SetStructure) {}, + key: "", + membersRem: []string{"key1", "key2"}, + membersWant: []string{}, + wantErr: _const.ErrKeyIsEmpty, + }, + { + name: "test when key not found", + setup: func(s *SetStructure) {}, + key: "destination", + membersRem: []string{"key1", "key2"}, + membersWant: []string{}, + wantErr: _const.ErrKeyNotFound, + }, + { + name: "test when remove one key", + setup: func(s *SetStructure) { + _ = s.SAdds("destination", "key1", "key2") + }, + key: "destination", + membersRem: []string{"key1"}, + membersWant: []string{"key2"}, + wantErr: nil, + }, + { + name: "test remove all keys", + setup: func(s *SetStructure) { + _ = s.SAdds("destination", "key1", "key2") + }, + key: "destination", + membersRem: []string{"key1", "key1"}, + membersWant: []string{}, + wantErr: nil, + }, + { + name: "test remove FSets that don't exist", + setup: func(s *SetStructure) { + _ = s.SAdds("destination", "key1", "key2") + }, + key: "destination", + membersRem: []string{"key10", "key11"}, + membersWant: []string{}, + wantErr: ErrMemberNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, _ := initTestSetDb() + tt.setup(s) + // Call the method with test case parameters. + err := s.SRems(tt.key, tt.membersRem...) + if !errors.Is(err, tt.wantErr) { + t.Errorf("SRems() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.wantErr == nil { + // positive verify + assert.True(t, s.exists(tt.key, tt.membersWant...)) + // verify removal + assert.False(t, s.exists(tt.key, tt.membersRem...)) + } + }) + } +} + +func TestSRem(t *testing.T) { + // Creating a list of cases for testing. + tests := []struct { + name string + setup func(*SetStructure) + key string + membersRem []string + membersWant []string + wantErr error + }{ + { + name: "test when key not found", + setup: func(s *SetStructure) {}, + key: "destination", + membersRem: []string{"key1", "key2"}, + membersWant: []string{}, + wantErr: _const.ErrKeyNotFound, + }, + { + name: "test when remove one key", + setup: func(s *SetStructure) { + _ = s.SAdds("destination", "key1", "key2") + }, + key: "destination", + membersRem: []string{"key1"}, + membersWant: []string{"key2"}, + wantErr: nil, + }, + { + name: "test remove all keys", + setup: func(s *SetStructure) { + _ = s.SAdds("destination", "key3", "key4") + }, + key: "destination", + membersRem: []string{"key3", "key4"}, + membersWant: []string{}, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, _ := initTestSetDb() + tt.setup(s) + for _, s2 := range tt.membersRem { + // Call the method with test case parameters. + err := s.SRem(tt.key, s2) + if !errors.Is(err, tt.wantErr) { + t.Errorf("SRems() error = %v, wantErr %v", err, tt.wantErr) + } + } + if tt.wantErr == nil { + // positive verify + assert.True(t, s.exists(tt.key, tt.membersWant...)) + // verify removal + assert.False(t, s.exists(tt.key, tt.membersRem...)) + } + }) + } +} + +func TestSCard(t *testing.T) { + // Creating a list of cases for testing. + tests := []struct { + name string + setup func(*SetStructure) + key string + want int + wantErr error + }{ + { + name: "test when key empty", + setup: func(s *SetStructure) {}, + key: "", + want: -1, + wantErr: _const.ErrKeyIsEmpty, + }, + { + name: "test two members", + setup: func(s *SetStructure) { + _ = s.SAdds("destination", "mem1", "mem2") + }, + key: "destination", + want: 2, + wantErr: nil, + }, + { + name: "test three members one duplicate", + setup: func(s *SetStructure) { + _ = s.SAdds("destination", "mem1", "mem2", "mem1") + }, + key: "destination", + want: 2, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, _ := initTestSetDb() + tt.setup(s) + // Call the method with test case parameters. + got, err := s.SCard(tt.key) + assert.Equal(t, tt.wantErr, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestSMembers(t *testing.T) { + // Creating a list of cases for testing. + tests := []struct { + name string + setup func(*SetStructure) + key string + want []string + wantErr error + }{ + { + name: "test when key empty", + setup: func(s *SetStructure) {}, + key: "", + want: nil, + wantErr: _const.ErrKeyIsEmpty, + }, + { + name: "test two members", + setup: func(s *SetStructure) { + _ = s.SAdds("destination", "mem1", "mem2") + }, + key: "destination", + want: []string{"mem1", "mem2"}, + wantErr: nil, + }, + { + name: "test three members one duplicate", + setup: func(s *SetStructure) { + _ = s.SAdds("destination", "mem1", "mem2", "mem1") + }, + key: "destination", + want: []string{"mem1", "mem2"}, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, _ := initTestSetDb() + tt.setup(s) + // Call the method with test case parameters. + got, err := s.SMembers(tt.key) + sort.Strings(got) + assert.Equal(t, tt.wantErr, err) + assert.Equal(t, tt.want, got) + }) + } + +} + +func TestSIsMember(t *testing.T) { + // Creating a list of cases for testing. + tests := []struct { + name string + setup func(*SetStructure) + key string + member string + want bool + wantErr error + }{ + { + name: "test when key empty", + setup: func(s *SetStructure) {}, + key: "", + want: false, + wantErr: _const.ErrKeyIsEmpty, + }, + { + name: "test two members, is member", + setup: func(s *SetStructure) { + _ = s.SAdds("destination", "mem1", "mem2") + }, + key: "destination", + member: "mem2", + want: true, + wantErr: nil, + }, + { + name: "test three members, not member", + setup: func(s *SetStructure) { + _ = s.SAdds("destination", "mem1", "mem2", "mem1") + }, + key: "destination", + member: "mem3", + want: false, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, _ := initTestSetDb() + tt.setup(s) + // Call the method with test case parameters. + got, err := s.SIsMember(tt.key, tt.member) + assert.Equal(t, tt.wantErr, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestSUnion(t *testing.T) { + // Creating a list of cases for testing. + tests := []struct { + name string + setup func(*SetStructure) + keys []string + want []string + wantErr error + }{ + { + name: "test when key empty", + setup: func(s *SetStructure) {}, + keys: []string{""}, + want: nil, + wantErr: _const.ErrKeyIsEmpty, + }, + { + name: "test when no keys", + setup: func(s *SetStructure) {}, + keys: []string{}, + want: nil, + wantErr: nil, + }, + { + name: "test two members", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem1", "mem2") + _ = s.SAdds("key2", "mem1", "mem2") + }, + keys: []string{"key1", "key2"}, + want: []string{"mem1", "mem2"}, + wantErr: nil, + }, + { + name: "test three members ", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem2", "mem1") + _ = s.SAdds("key2", "mem3") + }, + keys: []string{"key1", "key2"}, + want: []string{"mem1", "mem2", "mem3"}, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, _ := initTestSetDb() + tt.setup(s) + got, err := s.SUnion(tt.keys...) + assert.Equal(t, tt.wantErr, err) + sort.Strings(got) + assert.True(t, reflect.DeepEqual(tt.want, got)) + + }) + } +} + +func TestSInter(t *testing.T) { + // Creating a list of cases for testing. + tests := []struct { + name string + setup func(*SetStructure) + keys []string + want []string + wantErr error + }{ + { + name: "test when key empty", + setup: func(s *SetStructure) {}, + keys: []string{""}, + want: nil, + wantErr: _const.ErrKeyIsEmpty, + }, + { + name: "test when no keys", + setup: func(s *SetStructure) {}, + keys: []string{}, + want: nil, + wantErr: nil, + }, + { + name: "test when keys not found", + setup: func(s *SetStructure) {}, + keys: []string{"notfound", "notfound2"}, + want: nil, + wantErr: _const.ErrKeyNotFound, + }, + { + name: "test when first keys found but second not found", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem1") + }, + keys: []string{"key1", "notfound"}, + want: nil, + wantErr: _const.ErrKeyNotFound, + }, + { + name: "test two members", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem1", "mem2") + _ = s.SAdds("key2", "mem1", "mem2") + }, + keys: []string{"key1", "key2"}, + want: []string{"mem1", "mem2"}, + wantErr: nil, + }, + { + name: "test three members with no intersect", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem2", "mem1") + _ = s.SAdds("key2", "mem3") + }, + keys: []string{"key1", "key2"}, + want: nil, + wantErr: nil, + }, + { + name: "test three keys with one intersect", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem2", "mem1", "mem3", "mem4") + _ = s.SAdds("key2", "mem3") + _ = s.SAdds("key3", "mem5", "mem3", "mem6") + }, + keys: []string{"key1", "key2", "key3"}, + want: []string{"mem3"}, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, _ := initTestSetDb() + tt.setup(s) + got, err := s.SInter(tt.keys...) + assert.Equal(t, tt.wantErr, err) + sort.Strings(got) + assert.True(t, reflect.DeepEqual(tt.want, got)) + + }) + } +} + +func TestSDiff(t *testing.T) { + // Creating a list of cases for testing. + tests := []struct { + name string + setup func(*SetStructure) + keys []string + want []string + wantErr error + }{ + { + name: "test when key empty", + setup: func(s *SetStructure) {}, + keys: []string{""}, + want: nil, + wantErr: _const.ErrKeyIsEmpty, + }, + { + name: "test when no keys", + setup: func(s *SetStructure) {}, + keys: nil, + want: nil, + wantErr: nil, + }, + { + name: "test when keys not found", + setup: func(s *SetStructure) {}, + keys: []string{"notfound", "notfound2"}, + want: nil, + wantErr: _const.ErrKeyNotFound, + }, + { + name: "test when first keys found but second not found", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem1") + }, + keys: []string{"key1", "notfound"}, + want: nil, + wantErr: _const.ErrKeyNotFound, + }, + { + name: "test two members no diff", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem1", "mem2") + _ = s.SAdds("key2", "mem1", "mem2") + }, + keys: []string{"key1", "key2"}, + want: nil, + wantErr: nil, + }, + { + name: "test three members with two diffs", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem2", "mem1") + _ = s.SAdds("key2", "mem3") + }, + keys: []string{"key1", "key2"}, + want: []string{"mem2", "mem1"}, + wantErr: nil, + }, + { + name: "test three keys with five three", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem2", "mem1", "mem3", "mem4") + _ = s.SAdds("key2", "mem3") + _ = s.SAdds("key3", "mem5", "mem3", "mem6") + }, + keys: []string{"key1", "key2", "key3"}, + want: []string{"mem1", "mem2", "mem4"}, + wantErr: nil, + }, + { + name: "test three keys with no diffs", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem2", "mem1", "mem3", "mem4") + _ = s.SAdds("key2", "mem3") + _ = s.SAdds("key3", "mem5", "mem3", "mem6") + }, + keys: []string{"key2", "key1", "key3"}, // the first key here will be `key2` + want: nil, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, _ := initTestSetDb() + tt.setup(s) + got, err := s.SDiff(tt.keys...) + assert.Equal(t, tt.wantErr, err) + sort.Strings(got) + sort.Strings(tt.want) + assert.EqualValues(t, tt.want, got) + + }) + } +} + +func TestSInterStore(t *testing.T) { + // Creating a list of cases for testing. + tests := []struct { + name string + setup func(*SetStructure) + keys []string + want []string + wantErr error + }{ + { + name: "test when key empty", + setup: func(s *SetStructure) {}, + keys: []string{""}, + want: nil, + wantErr: _const.ErrKeyIsEmpty, + }, + { + name: "test when no keys", + setup: func(s *SetStructure) {}, + keys: nil, + want: nil, + wantErr: nil, + }, + { + name: "test when keys not found", + setup: func(s *SetStructure) {}, + keys: []string{"notfound", "notfound2"}, + want: nil, + wantErr: _const.ErrKeyNotFound, + }, + { + name: "test when first keys found but second not found", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem1") + }, + keys: []string{"key1", "notfound"}, + want: nil, + wantErr: _const.ErrKeyNotFound, + }, + { + name: "test two members no two", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem1", "mem2") + _ = s.SAdds("key2", "mem1", "mem2") + }, + keys: []string{"key1", "key2"}, + want: []string{"mem1", "mem2"}, + wantErr: nil, + }, + { + name: "test three members with no intersect", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem2", "mem1") + _ = s.SAdds("key2", "mem3") + }, + keys: []string{"key1", "key2"}, + want: nil, + wantErr: nil, + }, + { + name: "test three keys with one intersect", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem2", "mem1", "mem3", "mem4") + _ = s.SAdds("key2", "mem3") + _ = s.SAdds("key3", "mem5", "mem3", "mem6") + }, + keys: []string{"key1", "key2", "key3"}, + want: []string{"mem3"}, + wantErr: nil, + }, + { + name: "test three keys with two intersect", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem2", "mem5", "mem3", "mem4") + _ = s.SAdds("key2", "mem3", "mem5") + _ = s.SAdds("key3", "mem5", "mem3", "mem6") + }, + keys: []string{"key2", "key1", "key3"}, // the first key here will be `key2` + want: []string{"mem3", "mem5"}, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, _ := initTestSetDb() + tt.setup(s) + destKey := "dest" + err := s.SInterStore(destKey, tt.keys...) + assert.Equal(t, tt.wantErr, err) + got, _ := s.SUnion(destKey) // get values stored in `destKey` + sort.Strings(tt.want) + sort.Strings(got) + assert.EqualValues(t, tt.want, got) + + }) + } +} + +func TestSUnionStore(t *testing.T) { + // Creating a list of cases for testing. + tests := []struct { + name string + setup func(*SetStructure) + keys []string + want []string + wantErr error + }{ + { + name: "test when key empty", + setup: func(s *SetStructure) {}, + keys: []string{""}, + want: nil, + wantErr: _const.ErrKeyIsEmpty, + }, + { + name: "test when no keys", + setup: func(s *SetStructure) {}, + keys: nil, + want: nil, + wantErr: nil, + }, + { + name: "test when keys not found", + setup: func(s *SetStructure) {}, + keys: []string{"notfound", "notfound2"}, + want: nil, + wantErr: _const.ErrKeyNotFound, + }, + { + name: "test when first keys found but second not found", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem1") + }, + keys: []string{"key1", "notfound"}, + want: nil, + wantErr: _const.ErrKeyNotFound, + }, + { + name: "test two members no diff", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem1", "mem2") + _ = s.SAdds("key2", "mem1", "mem2") + }, + keys: []string{"key1", "key2"}, + want: []string{"mem1", "mem2"}, + wantErr: nil, + }, + { + name: "test three members with two diffs", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem2", "mem1") + _ = s.SAdds("key2", "mem3") + }, + keys: []string{"key1", "key2"}, + want: []string{"mem2", "mem1", "mem3"}, + wantErr: nil, + }, + { + name: "test three keys with five three", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem2") + _ = s.SAdds("key2", "mem3") + _ = s.SAdds("key3", "mem5", "mem3", "mem6") + }, + keys: []string{"key1", "key2", "key3"}, + want: []string{"mem2", "mem3", "mem5", "mem6"}, + wantErr: nil, + }, + { + name: "test three keys with no diffs", + setup: func(s *SetStructure) { + _ = s.SAdds("key1", "mem2", "mem1", "mem3", "mem4") + _ = s.SAdds("key2", "mem3") + _ = s.SAdds("key3", "mem5", "mem3", "mem6") + }, + keys: []string{"key2", "key1", "key3"}, // the first key here will be `key2` + want: []string{"mem1", "mem2", "mem3", "mem4", "mem5", "mem6"}, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, _ := initTestSetDb() + tt.setup(s) + destKey := "dest" + err := s.SUnionStore(destKey, tt.keys...) + assert.Equal(t, tt.wantErr, err) + got, _ := s.SUnion(destKey) // get values stored in `destKey` + sort.Strings(tt.want) + sort.Strings(got) + assert.EqualValues(t, tt.want, got) + + }) + } +} + +func TestNewSetStructure(t *testing.T) { + // expect error + opts := config.DefaultOptions + opts.DirPath = "" // the cause of error + setDB, err := NewSetStructure(opts) + assert.NotNil(t, err) + assert.Nil(t, setDB) + // expect no error + opts = config.DefaultOptions + dir, _ := os.MkdirTemp("", "TestSetStructure") + opts.DirPath = dir + setDB, _ = NewSetStructure(opts) + assert.NotNil(t, setDB) + assert.IsType(t, &engine.DB{}, setDB.db) +} + +func TestSetStructure_exists(t *testing.T) { + tests := []struct { + name string + setup func(*SetStructure) + key string + members []string + found bool + }{ + { + name: "test when empty Set", + setup: func(s *SetStructure) {}, + key: "testKey", + members: []string{"key1", "key2"}, + found: false, + }, + { + name: "test Set with no members found", + setup: func(s *SetStructure) { + _ = s.SAdds("testKey", "non1", "non2") + }, + key: "testKey", + members: []string{"key1", "key2"}, + found: false, + }, + { + name: "test Set with partial members found", + setup: func(s *SetStructure) { + _ = s.SAdds("testKey", "key1", "non2") + }, + key: "testKey", + members: []string{"key1", "key2"}, + found: false, + }, + { + name: "test Set with all members found", + setup: func(s *SetStructure) { + _ = s.SAdds("testKey", "key1", "non2", "key2") + }, + key: "testKey", + members: []string{"key1", "key2"}, + found: true, + }, + { + name: "test Set key empty", + setup: func(s *SetStructure) { + _ = s.SAdds("testKey", "key1", "non2", "key2") + }, + key: "", + members: []string{"key1", "key2"}, + found: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, _ := initTestSetDb() + tt.setup(s) + if tt.found { + assert.True(t, s.exists(tt.key, tt.members...)) + } else { + assert.False(t, s.exists(tt.key, tt.members...)) + } + }) + } +} + +func TestFSetsExists(t *testing.T) { + tests := []struct { + name string + setup func(sets *FSets) + key string + members []string + found bool + }{ + { + name: "test when empty Set", + setup: func(s *FSets) {}, + key: "testKey", + members: []string{"key1", "key2"}, + found: false, + }, + { + name: "test Set with no members found", + setup: func(fs *FSets) { + fs.add("non1", "non2") + }, + key: "testKey", + members: []string{"key1", "key2"}, + found: false, + }, + { + name: "test Set with partial members found", + setup: func(fs *FSets) { + fs.add("key1", "non2") + }, + key: "testKey", + members: []string{"key1", "key2"}, + found: false, + }, + { + name: "test Set with all members found", + setup: func(fs *FSets) { + fs.add("key1", "non2", "key2") + }, + key: "testKey", + members: []string{"key1", "key2"}, + found: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testFSets := &FSets{} + tt.setup(testFSets) + + if tt.found { + assert.True(t, testFSets.exists(tt.members...)) + } else { + assert.False(t, testFSets.exists(tt.members...)) + } + }) + } +} + +func TestFSets_remove(t *testing.T) { + tests := []struct { + name string + setup *FSets + members []string + want []string + wantErrors error + }{ + { + name: "test when empty Set", + setup: &FSets{}, + members: []string{"key1", "key2"}, + want: []string{}, + wantErrors: ErrMemberNotFound, + }, + { + name: "test Set with wrong members ", + setup: &FSets{"non1": struct{}{}, "non2": struct{}{}}, + want: []string{"non1", "non2"}, + members: []string{"key1", "key2"}, + wantErrors: ErrMemberNotFound, + }, + { + name: "test Set with partial members found", // this should not delete the found member neither + setup: &FSets{"key1": struct{}{}, "non2": struct{}{}}, + want: []string{"key1", "non2"}, + members: []string{"key1", "key2"}, + wantErrors: ErrMemberNotFound, + }, + { + name: "test Set with all members found", + setup: &FSets{"key1": struct{}{}, "non2": struct{}{}, "key2": struct{}{}}, + want: []string{"non2"}, + members: []string{"key1", "key2"}, + }, + { + name: "test Set not initialized", + want: []string{}, + members: []string{"key1", "key2"}, + wantErrors: ErrSetNotInitialized, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + err := tt.setup.remove(tt.members...) + assert.Equal(t, tt.wantErrors, err) + assert.True(t, tt.setup.exists(tt.want...)) + }) + } +}