diff --git a/aggregation.go b/aggregation.go new file mode 100644 index 0000000..e4462cf --- /dev/null +++ b/aggregation.go @@ -0,0 +1,79 @@ +package norm + +import "context" + +type Merge[M1, M2 any] struct { + L M1 + R []M2 +} + +type Keyer[K comparable] interface { + Key() K +} + +// Lookup performs left outer join. +func Lookup[ + M1 ~[]T1, + M2 ~[]T2, + T1, T2 Keyer[K], + A1, A2 any, + K comparable, +]( + ctx context.Context, + lhs Reader[M1, A1], lhsArgs A1, + rhs Reader[M2, A2], rhsArgs A2, +) ( + merge []Merge[T1, T2], + err error, +) { + l, err := lhs.Read(ctx, lhsArgs) + if err != nil { + return nil, err + } + + r, err := rhs.Read(ctx, rhsArgs) + if err != nil { + return nil, err + } + + return _lookup[M1, M2, T1, T2, K](l, r), nil +} + +func _lookup[ + M1 ~[]T1, + M2 ~[]T2, + T1 Keyer[K], + T2 Keyer[K], + K comparable, +]( + lhs M1, + rhs M2, +) ( + out []Merge[T1, T2], +) { + out = make([]Merge[T1, T2], 0, len(lhs)) + m := make(map[K][]int, len(rhs)) + + for i, v := range rhs { + k := v.Key() + m[k] = append(m[k], i) + } + + for _, l := range lhs { + merge := Merge[T1, T2]{ + L: l, + R: nil, + } + + rii, ok := m[l.Key()] + if ok { + for _, i := range rii { + merge.R = append(merge.R, rhs[i]) + } + } + + out = append(out, merge) + } + + return out +} diff --git a/aggregation_test.go b/aggregation_test.go new file mode 100644 index 0000000..28f812e --- /dev/null +++ b/aggregation_test.go @@ -0,0 +1,155 @@ +package norm + +import ( + "context" + "reflect" + "testing" +) + +type kint int + +func (v kint) Key() int { + return int(v) +} + +type mem struct { + v []kint +} + +func (m mem) Read(ctx context.Context, args struct{}) (value []kint, err error) { + return []kint(m.v), nil +} + +func TestLookup(t *testing.T) { + type args struct { + ctx context.Context + lhs Reader[[]kint, struct{}] + lhsArgs struct{} + rhs Reader[[]kint, struct{}] + rhsArgs struct{} + } + tests := []struct { + name string + args args + wantMerge []Merge[kint, kint] + wantErr bool + }{ + { + name: "with mathes", + args: args{ + ctx: context.Background(), + lhs: mem{[]kint{1, 2, 3, 4, 5}}, + lhsArgs: struct{}{}, + rhs: mem{[]kint{5, 5, 2, 2, 4}}, + rhsArgs: struct{}{}, + }, + wantMerge: []Merge[kint, kint]{ + {1, nil}, + {2, []kint{2, 2}}, + {3, nil}, + {4, []kint{4}}, + {5, []kint{5, 5}}, + }, + wantErr: false, + }, + { + name: "no match", + args: args{ + ctx: context.Background(), + lhs: mem{[]kint{1, 2, 3, 4, 5}}, + lhsArgs: struct{}{}, + rhs: mem{[]kint{10, 11, 12, 13, 14}}, + rhsArgs: struct{}{}, + }, + wantMerge: []Merge[kint, kint]{ + {1, nil}, + {2, nil}, + {3, nil}, + {4, nil}, + {5, nil}, + }, + wantErr: false, + }, + { + name: "right is empty", + args: args{ + ctx: context.Background(), + lhs: mem{[]kint{1, 2, 3, 4, 5}}, + lhsArgs: struct{}{}, + rhs: mem{[]kint{}}, + rhsArgs: struct{}{}, + }, + wantMerge: []Merge[kint, kint]{ + {1, nil}, + {2, nil}, + {3, nil}, + {4, nil}, + {5, nil}, + }, + wantErr: false, + }, + { + name: "right is nil", + args: args{ + ctx: context.Background(), + lhs: mem{[]kint{1, 2, 3, 4, 5}}, + lhsArgs: struct{}{}, + rhs: mem{}, + rhsArgs: struct{}{}, + }, + wantMerge: []Merge[kint, kint]{ + {1, nil}, + {2, nil}, + {3, nil}, + {4, nil}, + {5, nil}, + }, + wantErr: false, + }, + { + name: "left is empty", + args: args{ + ctx: context.Background(), + lhs: mem{[]kint{}}, + lhsArgs: struct{}{}, + rhs: mem{[]kint{1, 2, 3, 4, 5}}, + rhsArgs: struct{}{}, + }, + wantMerge: []Merge[kint, kint]{}, + wantErr: false, + }, + { + name: "left is nil", + args: args{ + ctx: context.Background(), + lhs: mem{}, + lhsArgs: struct{}{}, + rhs: mem{[]kint{1, 2, 3, 4, 5}}, + rhsArgs: struct{}{}, + }, + wantMerge: []Merge[kint, kint]{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotMerge, err := Lookup[ + []kint, []kint, + kint, kint, + struct{}, struct{}, + int, + ]( + tt.args.ctx, + tt.args.lhs, tt.args.lhsArgs, + tt.args.rhs, tt.args.rhsArgs, + ) + if (err != nil) != tt.wantErr { + t.Errorf("Lookup() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotMerge, tt.wantMerge) { + t.Errorf("Lookup() = %v, want %v", gotMerge, tt.wantMerge) + } + }) + } +}