Repository: oom-ai/oomstore
Pull request #1074 (2022-01-25 03:43:25)

Test/join


Patch:
diff --git a/go.mod b/go.mod
index 00079c27..b0f4def9 100644
--- a/go.mod
+++ b/go.mod
@@ -20,6 +20,7 @@ require (
 	github.com/lib/pq v1.10.4
 	github.com/mitchellh/mapstructure v1.4.3
 	github.com/olekukonko/tablewriter v0.0.5
+	github.com/pingcap/errors v0.11.5-0.20201126102027-b0a155152ca3
 	github.com/pingcap/log v0.0.0-20211215031037-e024ba4eb0ee
 	github.com/pkg/errors v0.9.1
 	github.com/snowflakedb/gosnowflake v1.6.5
@@ -29,6 +30,7 @@ require (
 	github.com/tikv/client-go/v2 v2.0.0-rc
 	go.uber.org/zap v1.19.1
 	google.golang.org/api v0.63.0
+	google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb
 	google.golang.org/grpc v1.43.0
 	google.golang.org/protobuf v1.27.1
 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
@@ -91,7 +93,6 @@ require (
 	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/pierrec/lz4/v4 v4.1.12 // indirect
-	github.com/pingcap/errors v0.11.5-0.20201126102027-b0a155152ca3 // indirect
 	github.com/pingcap/failpoint v0.0.0-20210918120811-547c13e3eb00 // indirect
 	github.com/pingcap/kvproto v0.0.0-20211229082925-7a8280c36daf // indirect
 	github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
@@ -119,7 +120,6 @@ require (
 	golang.org/x/tools v0.1.5 // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
 	lukechampine.com/uint128 v1.1.1 // indirect
diff --git a/internal/database/metadata/informer/feature.go b/internal/database/metadata/informer/feature.go
index bdcde282..20c5f15a 100644
--- a/internal/database/metadata/informer/feature.go
+++ b/internal/database/metadata/informer/feature.go
@@ -1,7 +1,6 @@
 package informer
 
 import (
-	"github.com/oom-ai/oomstore/internal/database/metadata"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
 )
 
@@ -17,13 +16,13 @@ func (c *FeatureCache) Enrich(groupCache *GroupCache) {
 	}
 }
 
-func (c *FeatureCache) List(opt metadata.ListCachedFeatureOpt) types.FeatureList {
+func (c *FeatureCache) List(fullNames *[]string) types.FeatureList {
 	features := c.FeatureList
 
-	// filter featureNames
-	if opt.FullNames != nil {
+	// filter names
+	if fullNames != nil {
 		var tmp types.FeatureList
-		for _, fullName := range *opt.FullNames {
+		for _, fullName := range *fullNames {
 			if f := features.Find(func(f *types.Feature) bool {
 				return f.FullName() == fullName
 			}); f != nil {
@@ -32,17 +31,6 @@ func (c *FeatureCache) List(opt metadata.ListCachedFeatureOpt) types.FeatureList
 		}
 		features = tmp
 	}
-	// filter groupName
-	if opt.GroupName != nil {
-		features = features.Filter(func(f *types.Feature) bool {
-			return f.Group.Name == *opt.GroupName
-		})
-	}
-	// filter groupID
-	if opt.GroupID != nil {
-		features = features.Filter(func(f *types.Feature) bool {
-			return f.Group.ID == *opt.GroupID
-		})
-	}
+
 	return features
 }
diff --git a/internal/database/metadata/informer/group.go b/internal/database/metadata/informer/group.go
index f373810b..b97313af 100644
--- a/internal/database/metadata/informer/group.go
+++ b/internal/database/metadata/informer/group.go
@@ -24,13 +24,3 @@ func (c *GroupCache) List(entityID *int) types.GroupList {
 		return g.Entity.ID == *entityID
 	})
 }
-
-func (c *GroupCache) Get(groupID int) *types.Group {
-	groups := c.GroupList.Filter(func(g *types.Group) bool {
-		return g.ID == groupID
-	})
-	if len(groups) == 0 {
-		return nil
-	}
-	return groups[0]
-}
diff --git a/internal/database/metadata/informer/informer.go b/internal/database/metadata/informer/informer.go
index 826ad78f..fbf81c96 100644
--- a/internal/database/metadata/informer/informer.go
+++ b/internal/database/metadata/informer/informer.go
@@ -6,8 +6,6 @@ import (
 	"sync/atomic"
 	"time"
 
-	"github.com/oom-ai/oomstore/pkg/errdefs"
-
 	"github.com/oom-ai/oomstore/internal/database/metadata"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
 )
@@ -90,14 +88,6 @@ func (f *Informer) Cache() *Cache {
 	return f.cache.Load().(*Cache)
 }
 
-func (f *Informer) ListCachedFeature(ctx context.Context, opt metadata.ListCachedFeatureOpt) types.FeatureList {
-	return f.Cache().Features.List(opt).Copy()
-}
-
-func (f *Informer) GetCachedGroup(ctx context.Context, groupID int) (*types.Group, error) {
-	group := f.Cache().Groups.Get(groupID).Copy()
-	if group == nil {
-		return nil, errdefs.NotFound(errdefs.Errorf("group %d not found", groupID))
-	}
-	return group, nil
+func (f *Informer) ListCachedFeature(ctx context.Context, featureNames *[]string) types.FeatureList {
+	return f.Cache().Features.List(featureNames).Copy()
 }
diff --git a/internal/database/metadata/mock_metadata/store.go b/internal/database/metadata/mock_metadata/store.go
index 4f9f81ff..4179e367 100644
--- a/internal/database/metadata/mock_metadata/store.go
+++ b/internal/database/metadata/mock_metadata/store.go
@@ -112,21 +112,6 @@ func (mr *MockStoreMockRecorder) CreateRevision(ctx, opt interface{}) *gomock.Ca
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRevision", reflect.TypeOf((*MockStore)(nil).CreateRevision), ctx, opt)
 }
 
-// GetCachedGroup mocks base method.
-func (m *MockStore) GetCachedGroup(ctx context.Context, id int) (*types.Group, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetCachedGroup", ctx, id)
-	ret0, _ := ret[0].(*types.Group)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// GetCachedGroup indicates an expected call of GetCachedGroup.
-func (mr *MockStoreMockRecorder) GetCachedGroup(ctx, id interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCachedGroup", reflect.TypeOf((*MockStore)(nil).GetCachedGroup), ctx, id)
-}
-
 // GetEntity mocks base method.
 func (m *MockStore) GetEntity(ctx context.Context, id int) (*types.Entity, error) {
 	m.ctrl.T.Helper()
@@ -248,17 +233,17 @@ func (mr *MockStoreMockRecorder) GetRevisionBy(ctx, groupID, revision interface{
 }
 
 // ListCachedFeature mocks base method.
-func (m *MockStore) ListCachedFeature(ctx context.Context, opt metadata.ListCachedFeatureOpt) types.FeatureList {
+func (m *MockStore) ListCachedFeature(ctx context.Context, fullNames *[]string) types.FeatureList {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ListCachedFeature", ctx, opt)
+	ret := m.ctrl.Call(m, "ListCachedFeature", ctx, fullNames)
 	ret0, _ := ret[0].(types.FeatureList)
 	return ret0
 }
 
 // ListCachedFeature indicates an expected call of ListCachedFeature.
-func (mr *MockStoreMockRecorder) ListCachedFeature(ctx, opt interface{}) *gomock.Call {
+func (mr *MockStoreMockRecorder) ListCachedFeature(ctx, fullNames interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCachedFeature", reflect.TypeOf((*MockStore)(nil).ListCachedFeature), ctx, opt)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCachedFeature", reflect.TypeOf((*MockStore)(nil).ListCachedFeature), ctx, fullNames)
 }
 
 // ListEntity mocks base method.
@@ -776,33 +761,18 @@ func (m *MockCacheStore) EXPECT() *MockCacheStoreMockRecorder {
 	return m.recorder
 }
 
-// GetCachedGroup mocks base method.
-func (m *MockCacheStore) GetCachedGroup(ctx context.Context, id int) (*types.Group, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetCachedGroup", ctx, id)
-	ret0, _ := ret[0].(*types.Group)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// GetCachedGroup indicates an expected call of GetCachedGroup.
-func (mr *MockCacheStoreMockRecorder) GetCachedGroup(ctx, id interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCachedGroup", reflect.TypeOf((*MockCacheStore)(nil).GetCachedGroup), ctx, id)
-}
-
 // ListCachedFeature mocks base method.
-func (m *MockCacheStore) ListCachedFeature(ctx context.Context, opt metadata.ListCachedFeatureOpt) types.FeatureList {
+func (m *MockCacheStore) ListCachedFeature(ctx context.Context, fullNames *[]string) types.FeatureList {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ListCachedFeature", ctx, opt)
+	ret := m.ctrl.Call(m, "ListCachedFeature", ctx, fullNames)
 	ret0, _ := ret[0].(types.FeatureList)
 	return ret0
 }
 
 // ListCachedFeature indicates an expected call of ListCachedFeature.
-func (mr *MockCacheStoreMockRecorder) ListCachedFeature(ctx, opt interface{}) *gomock.Call {
+func (mr *MockCacheStoreMockRecorder) ListCachedFeature(ctx, fullNames interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCachedFeature", reflect.TypeOf((*MockCacheStore)(nil).ListCachedFeature), ctx, opt)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCachedFeature", reflect.TypeOf((*MockCacheStore)(nil).ListCachedFeature), ctx, fullNames)
 }
 
 // Refresh mocks base method.
diff --git a/internal/database/metadata/store.go b/internal/database/metadata/store.go
index d6c73c06..b22badb8 100644
--- a/internal/database/metadata/store.go
+++ b/internal/database/metadata/store.go
@@ -16,7 +16,7 @@ type Store interface {
 	io.Closer
 }
 
-// DBStore defines the methods that a database backend store must implement.
+// The interface defines the methods that a database backend store must implement.
 type DBStore interface {
 	// entity
 	CreateEntity(ctx context.Context, opt CreateEntityOpt) (int, error)
@@ -50,12 +50,11 @@ type DBStore interface {
 	WithTransaction(ctx context.Context, fn func(context.Context, DBStore) error) error
 }
 
-// CacheStore defines methods that a memory backend store must implement.
+// The interface defines methods that a memory backend store must implement.
 type CacheStore interface {
-	ListCachedFeature(ctx context.Context, opt ListCachedFeatureOpt) types.FeatureList
-	GetCachedGroup(ctx context.Context, id int) (*types.Group, error)
+	ListCachedFeature(ctx context.Context, fullNames *[]string) types.FeatureList
 
-	// Refresh pulls data from database and update cache.
+	// Pull data from database and update cache.
 	Refresh() error
 }
 
diff --git a/internal/database/metadata/types.go b/internal/database/metadata/types.go
index c9ab48e7..9bcc0478 100644
--- a/internal/database/metadata/types.go
+++ b/internal/database/metadata/types.go
@@ -65,9 +65,3 @@ type ListFeatureOpt struct {
 	GroupID    *int
 	FeatureIDs *[]int
 }
-
-type ListCachedFeatureOpt struct {
-	FullNames *[]string
-	GroupName *string
-	GroupID   *int
-}
diff --git a/pkg/oomstore/import.go b/pkg/oomstore/import.go
index d000a844..dda1af7c 100644
--- a/pkg/oomstore/import.go
+++ b/pkg/oomstore/import.go
@@ -57,7 +57,7 @@ func (s *OomStore) csvReaderImport(ctx context.Context, opt *importOpt, dataSour
 		return 0, err
 	}
 
-	newRevisionID, snapshotTableName, err := s.createRevision(ctx, metadata.CreateRevisionOpt{
+	newRevisionID, snapshotTableName, err := s.CreateRevision(ctx, metadata.CreateRevisionOpt{
 		Revision:      time.Now().UnixMilli(),
 		GroupID:       opt.group.ID,
 		SnapshotTable: nil,
@@ -112,7 +112,7 @@ func (s *OomStore) tableLinkImport(ctx context.Context, opt *importOpt, dataSour
 	} else {
 		revision = *opt.Revision
 	}
-	newRevisionID, _, err := s.createRevision(ctx, metadata.CreateRevisionOpt{
+	newRevisionID, _, err := s.CreateRevision(ctx, metadata.CreateRevisionOpt{
 		Revision:      revision,
 		GroupID:       opt.group.ID,
 		SnapshotTable: &dataSource.TableName,
diff --git a/pkg/oomstore/import_stream.go b/pkg/oomstore/import_stream.go
index 1ea6a554..baa9ec2d 100644
--- a/pkg/oomstore/import_stream.go
+++ b/pkg/oomstore/import_stream.go
@@ -113,7 +113,7 @@ func (s *OomStore) tableLinkImportStream(ctx context.Context, opt types.ImportSt
 		return err
 	}
 
-	_, _, err = s.createRevision(ctx, metadata.CreateRevisionOpt{
+	_, _, err = s.CreateRevision(ctx, metadata.CreateRevisionOpt{
 		Revision:    tableSchema.TimeRange.MinUnixMilli,
 		GroupID:     group.ID,
 		CdcTable:    &dataSource.TableName,
diff --git a/pkg/oomstore/join.go b/pkg/oomstore/join.go
index db4423a9..7f157431 100644
--- a/pkg/oomstore/join.go
+++ b/pkg/oomstore/join.go
@@ -23,21 +23,18 @@ func (s *OomStore) ChannelJoin(ctx context.Context, opt types.ChannelJoinOpt) (*
 	if err := util.ValidateFullFeatureNames(opt.JoinFeatureNames...); err != nil {
 		return nil, err
 	}
-	data := make(chan []interface{})
-	defer close(data)
 
-	emptyResult := &types.JoinResult{
-		Data: data,
-	}
 	features, err := s.ListFeature(ctx, types.ListFeatureOpt{
 		FeatureNames: &opt.JoinFeatureNames,
 	})
 	if err != nil {
 		return nil, err
 	}
-
 	if len(features) == 0 {
-		return emptyResult, nil
+		data := make(chan []interface{})
+		defer close(data)
+
+		return &types.JoinResult{Data: data}, nil
 	}
 
 	entity, err := getSharedEntity(features)
@@ -147,12 +144,12 @@ func (s *OomStore) buildRevisionRanges(ctx context.Context, group *types.Group)
 func GetEntityRowsFromInputFile(inputFilePath string) (<-chan types.EntityRow, []string, error) {
 	input, err := os.Open(inputFilePath)
 	if err != nil {
-		return nil, nil, err
+		return nil, nil, errdefs.WithStack(err)
 	}
 	reader := csv.NewReader(input)
 	header, err := reader.Read()
 	if err != nil {
-		return nil, nil, err
+		return nil, nil, errdefs.WithStack(err)
 	}
 	entityRows := make(chan types.EntityRow)
 	var readErr error
@@ -166,7 +163,7 @@ func GetEntityRowsFromInputFile(inputFilePath string) (<-chan types.EntityRow, [
 				break
 			}
 			if err != nil {
-				readErr = err
+				readErr = errdefs.WithStack(err)
 				return
 			}
 			if len(line) < 2 {
@@ -175,7 +172,7 @@ func GetEntityRowsFromInputFile(inputFilePath string) (<-chan types.EntityRow, [
 			}
 			unixMilli, err := strconv.Atoi(line[1])
 			if err != nil {
-				readErr = err
+				readErr = errdefs.WithStack(err)
 				return
 			}
 			entityRows <- types.EntityRow{
diff --git a/pkg/oomstore/online_query.go b/pkg/oomstore/online_query.go
index d38b6860..88fd0d63 100644
--- a/pkg/oomstore/online_query.go
+++ b/pkg/oomstore/online_query.go
@@ -3,8 +3,6 @@ package oomstore
 import (
 	"context"
 
-	"github.com/oom-ai/oomstore/internal/database/metadata"
-
 	"github.com/oom-ai/oomstore/pkg/errdefs"
 
 	"github.com/oom-ai/oomstore/internal/database/dbutil"
@@ -23,9 +21,7 @@ func (s *OomStore) OnlineGet(ctx context.Context, opt types.OnlineGetOpt) (*type
 		FeatureNames:    opt.FeatureNames,
 		FeatureValueMap: make(map[string]interface{}),
 	}
-	features := s.metadata.ListCachedFeature(ctx, metadata.ListCachedFeatureOpt{
-		FullNames: &opt.FeatureNames,
-	})
+	features := s.metadata.ListCachedFeature(ctx, &opt.FeatureNames)
 	if len(features) == 0 {
 		return &rs, nil
 	}
@@ -67,9 +63,7 @@ func (s *OomStore) OnlineMultiGet(ctx context.Context, opt types.OnlineMultiGetO
 		return nil, err
 	}
 	result := make(map[string]*types.FeatureValues)
-	features := s.metadata.ListCachedFeature(ctx, metadata.ListCachedFeatureOpt{
-		FullNames: &opt.FeatureNames,
-	})
+	features := s.metadata.ListCachedFeature(ctx, &opt.FeatureNames)
 	if len(features) == 0 {
 		return result, nil
 	}
diff --git a/pkg/oomstore/push.go b/pkg/oomstore/push.go
index bceb9462..6789758d 100644
--- a/pkg/oomstore/push.go
+++ b/pkg/oomstore/push.go
@@ -4,8 +4,6 @@ import (
 	"context"
 	"time"
 
-	"github.com/oom-ai/oomstore/internal/database/metadata"
-
 	"github.com/oom-ai/oomstore/pkg/errdefs"
 
 	"github.com/oom-ai/oomstore/internal/database/online"
@@ -14,9 +12,12 @@ import (
 
 // Push inserts stream feature values to online store and offline store
 func (s *OomStore) Push(ctx context.Context, opt types.PushOpt) error {
-	features := s.metadata.ListCachedFeature(ctx, metadata.ListCachedFeatureOpt{
+	features, err := s.ListFeature(ctx, types.ListFeatureOpt{
 		GroupName: &opt.GroupName,
 	})
+	if err != nil {
+		return err
+	}
 	var featureNames []string
 	for name := range opt.FeatureValues {
 		featureNames = append(featureNames, name)
@@ -32,7 +33,7 @@ func (s *OomStore) Push(ctx context.Context, opt types.PushOpt) error {
 		values = append(values, opt.FeatureValues[f.Name])
 	}
 
-	if err := s.online.Push(ctx, online.PushOpt{
+	if err = s.online.Push(ctx, online.PushOpt{
 		Entity:        entity,
 		EntityKey:     opt.EntityKey,
 		GroupID:       group.ID,
diff --git a/pkg/oomstore/push_processor.go b/pkg/oomstore/push_processor.go
index 01c9370e..7eff52b3 100644
--- a/pkg/oomstore/push_processor.go
+++ b/pkg/oomstore/push_processor.go
@@ -125,14 +125,17 @@ func (p *PushProcessor) Push(record types.StreamRecord) {
 }
 
 func (p *PushProcessor) pushToOffline(ctx context.Context, s *OomStore, groupID int) error {
-	features := s.metadata.ListCachedFeature(ctx, metadata.ListCachedFeatureOpt{
+	features, err := s.metadata.ListFeature(ctx, metadata.ListFeatureOpt{
 		GroupID: &groupID,
 	})
+	if err != nil {
+		return err
+	}
 	entity := features[0].Entity()
 
 	value, _ := p.buffer.Load(groupID)
 	b := value.(GroupBuffer)
-	group, err := s.metadata.GetCachedGroup(ctx, groupID)
+	group, err := s.metadata.GetGroup(ctx, groupID)
 	if err != nil {
 		return err
 	}
@@ -147,12 +150,15 @@ func (p *PushProcessor) pushToOffline(ctx context.Context, s *OomStore, groupID
 }
 
 func (s *OomStore) newRevisionForStream(ctx context.Context, groupID int, revision int64) error {
-	features := s.metadata.ListCachedFeature(ctx, metadata.ListCachedFeatureOpt{
+	features, err := s.metadata.ListFeature(ctx, metadata.ListFeatureOpt{
 		GroupID: &groupID,
 	})
+	if err != nil {
+		return err
+	}
 	entity := features[0].Entity()
 
-	if err := s.offline.CreateTable(ctx, offline.CreateTableOpt{
+	if err = s.offline.CreateTable(ctx, offline.CreateTableOpt{
 		TableName: dbutil.OfflineStreamCdcTableName(groupID, revision),
 		Entity:    entity,
 		Features:  features,
@@ -163,7 +169,7 @@ func (s *OomStore) newRevisionForStream(ctx context.Context, groupID int, revisi
 
 	snapshotTable := ""
 	cdcTable := dbutil.OfflineStreamCdcTableName(groupID, revision)
-	if _, _, err := s.createRevision(ctx, metadata.CreateRevisionOpt{
+	if _, _, err = s.CreateRevision(ctx, metadata.CreateRevisionOpt{
 		GroupID:       groupID,
 		Revision:      revision,
 		SnapshotTable: &snapshotTable,
diff --git a/pkg/oomstore/revision.go b/pkg/oomstore/revision.go
index 023ae5c0..14f6ebe5 100644
--- a/pkg/oomstore/revision.go
+++ b/pkg/oomstore/revision.go
@@ -25,7 +25,7 @@ func (s *OomStore) GetRevisionBy(ctx context.Context, groupID int, revision int6
 	return s.metadata.GetRevisionBy(ctx, groupID, revision)
 }
 
-func (s *OomStore) createRevision(ctx context.Context, opt metadata.CreateRevisionOpt) (int, string, error) {
+func (s *OomStore) CreateRevision(ctx context.Context, opt metadata.CreateRevisionOpt) (int, string, error) {
 	var revisionID int
 	var snapshotTable string
 	var dummyRevision *types.Revision


Test patch:
diff --git a/internal/database/metadata/informer/informer_test.go b/internal/database/metadata/informer/informer_test.go
index 46733200..5bf54421 100644
--- a/internal/database/metadata/informer/informer_test.go
+++ b/internal/database/metadata/informer/informer_test.go
@@ -5,8 +5,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/oom-ai/oomstore/internal/database/metadata"
-
 	"github.com/oom-ai/oomstore/internal/database/metadata/informer"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
 	"github.com/oom-ai/oomstore/pkg/oomstore/util"
@@ -52,9 +50,7 @@ func TestInformer(t *testing.T) {
 	defer informer.Close()
 
 	name := util.ComposeFullFeatureName("group", "price")
-	features := informer.ListCachedFeature(ctx, metadata.ListCachedFeatureOpt{
-		FullNames: &[]string{name},
-	})
+	features := informer.ListCachedFeature(ctx, &[]string{name})
 	require.Equal(t, 1, len(features))
 	feature := features[0]
 
@@ -66,25 +62,18 @@ func TestInformer(t *testing.T) {
 	assert.Equal(t, 100, feature.Group.ID)
 	assert.Equal(t, 1, feature.Group.Entity.ID)
 	assert.Equal(t, "entity", feature.Group.Entity.Name)
-
-	groupName := "group"
-	featuresByGroup := informer.ListCachedFeature(ctx, metadata.ListCachedFeatureOpt{
-		GroupName: &groupName,
-	})
-	assert.Equal(t, 1, len(featuresByGroup))
-	assert.Equal(t, feature, featuresByGroup[0])
 }
 
 func TestInformerDeepCopy(t *testing.T) {
 	ctx, informer := prepareInformer(t)
 	defer informer.Close()
 
-	features := informer.ListCachedFeature(ctx, metadata.ListCachedFeatureOpt{})
+	features := informer.ListCachedFeature(ctx, nil)
 	require.Equal(t, 1, len(features))
 	// changing this entity should not change the internal state of the informer
 	features[0].Name = "new_price"
 
-	features = informer.ListCachedFeature(ctx, metadata.ListCachedFeatureOpt{})
+	features = informer.ListCachedFeature(ctx, nil)
 	require.Equal(t, 1, len(features))
 	assert.Equal(t, 1, features[0].ID)
 	assert.Equal(t, 100, features[0].GroupID)
diff --git a/internal/database/metadata/test_impl/feature.go b/internal/database/metadata/test_impl/feature.go
index 7c6a4b8b..b1ad9252 100644
--- a/internal/database/metadata/test_impl/feature.go
+++ b/internal/database/metadata/test_impl/feature.go
@@ -176,7 +176,7 @@ func TestListCachedFeature(t *testing.T, prepareStore PrepareStoreFn, destroySto
 	_, groupID := prepareEntityAndGroup(t, ctx, store)
 
 	// case 1: no feature to list
-	features := store.ListCachedFeature(ctx, metadata.ListCachedFeatureOpt{})
+	features := store.ListCachedFeature(ctx, nil)
 	assert.Equal(t, 0, features.Len())
 
 	_, err := store.CreateFeature(ctx, metadata.CreateFeatureOpt{
@@ -189,13 +189,11 @@ func TestListCachedFeature(t *testing.T, prepareStore PrepareStoreFn, destroySto
 	require.NoError(t, store.Refresh())
 
 	// case 2: no condition, list all features
-	features = store.ListCachedFeature(ctx, metadata.ListCachedFeatureOpt{})
+	features = store.ListCachedFeature(ctx, nil)
 	assert.Equal(t, 1, features.Len())
 
 	// case 8: list features by FeatureNames
-	features = store.ListCachedFeature(ctx, metadata.ListCachedFeatureOpt{
-		FullNames: &[]string{"device_info.phone"},
-	})
+	features = store.ListCachedFeature(ctx, &[]string{"device_info.phone"})
 	assert.Equal(t, 1, len(features))
 }
 
diff --git a/oomcli/test/test_export.sh b/oomcli/test/test_export.sh
index e540126e..c5633544 100755
--- a/oomcli/test/test_export.sh
+++ b/oomcli/test/test_export.sh
@@ -52,15 +52,6 @@ oomcli_export_batch() {
     assert_eq "$case" "$(sort <<< "$expected")" "$(sort <<< "$actual")"
 }
 
-oomcli_export_batch_and_stream_before_import() {
-    case="export batch and stream feature before import"
-    expected='user,student.name,student.gender,student.age,user-click.last_5_click_posts,user-click.number_of_user_starred_posts
-1,,,,"2,3,4,5,6",11
-'
-    actual=$(oomcli export --feature student.name,student.gender,student.age,user-click.last_5_click_posts,user-click.number_of_user_starred_posts --unix-milli $t2 -o csv)
-    assert_eq "$case" "$(sort <<< "$expected")" "$(sort <<< "$actual")"
-}
-
 oomcli_export_batch_and_stream() {
     case="export batch and stream feature"
     expected='user,student.name,student.gender,student.age,user-click.last_5_click_posts,user-click.number_of_user_starred_posts
@@ -80,7 +71,6 @@ main() {
     oomcli_export_push_feature
     oomcli_export_update_feature
     oomcli_export_batch
-    oomcli_export_batch_and_stream_before_import
     oomcli_export_batch_and_stream
 }
 
diff --git a/oomcli/test/test_join.sh b/oomcli/test/test_join.sh
index 2c45fddd..9df1ea2d 100755
--- a/oomcli/test/test_join.sh
+++ b/oomcli/test/test_join.sh
@@ -8,24 +8,29 @@ register_features
 # clean up the tmp file
 trap 'command rm -rf entity_rows.csv entity_rows_with_values.csv' EXIT INT TERM HUP
 
-# import sample data to offline store
-import_student_sample 80
-bt1=50
-bt2=100
-
 # push stream feature
-st0=$(perl -MTime::HiRes -e 'printf("%.0f\n",Time::HiRes::time()*1000)')
+t0=$(perl -MTime::HiRes -e 'printf("%.0f\n",Time::HiRes::time()*1000)')
+
 oomcli push --entity-key 1 --group user-click --features last_5_click_posts=0,1,2,3,4 --features number_of_user_starred_posts=10 >> /dev/null 2>&1
-st1=$(perl -MTime::HiRes -e 'printf("%.0f\n",Time::HiRes::time()*1000)')
+t1=$(perl -MTime::HiRes -e 'printf("%.0f\n",Time::HiRes::time()*1000)')
+sleep 1
+
+# import sample data to offline store
+import_student_sample
+t2=$(perl -MTime::HiRes -e 'printf("%.0f\n",Time::HiRes::time()*1000)')
 sleep 1
+
 oomcli push --entity-key 1 --group user-click --features last_5_click_posts=1,2,3,4,5 --features number_of_user_starred_posts=10 >> /dev/null 2>&1
-st2=$(perl -MTime::HiRes -e 'printf("%.0f\n",Time::HiRes::time()*1000)')
+t3=$(perl -MTime::HiRes -e 'printf("%.0f\n",Time::HiRes::time()*1000)')
 sleep 1
+
 oomcli push --entity-key 2 --group user-click --features last_5_click_posts=2,3,4,5,6 --features number_of_user_starred_posts=11 >> /dev/null 2>&1
-st3=$(perl -MTime::HiRes -e 'printf("%.0f\n",Time::HiRes::time()*1000)')
+t4=$(perl -MTime::HiRes -e 'printf("%.0f\n",Time::HiRes::time()*1000)')
 sleep 1
+
 oomcli push --entity-key 3 --group user-click --features last_5_click_posts=3,4,5,6,7 --features number_of_user_starred_posts=12 >> /dev/null 2>&1
-st4=$(perl -MTime::HiRes -e 'printf("%.0f\n",Time::HiRes::time()*1000)')
+t5=$(perl -MTime::HiRes -e 'printf("%.0f\n",Time::HiRes::time()*1000)')
+
 oomcli snapshot user-click
 
 oomcli_join_historical_feature() {
@@ -33,17 +38,25 @@ oomcli_join_historical_feature() {
 
 cat <<-EOF > entity_rows.csv
 entity_key,unix_milli
-1,$bt1
-2,$bt1
-1,$bt2
-2,$bt2
+1,$t0
+1,$t1
+1,$t2
+1,$t3
+2,$t0
+2,$t1
+2,$t2
+2,$t3
 EOF
     expected="
 entity_key,unix_milli,student.name,student.gender,student.age
-1,$bt1,,,
-2,$bt1,,,
-1,$bt2,lian,m,18
-2,$bt2,gao,m,20
+1,$t0,,,
+1,$t1,,,
+1,$t2,lian,m,18
+1,$t3,lian,m,18
+2,$t0,,,
+2,$t1,,,
+2,$t2,gao,m,20
+2,$t3,gao,m,20
     "
 
     actual=$(oomcli join \
@@ -63,18 +76,26 @@ oomcli_join_historical_feature_with_real_time_feature_values() {
 
 cat <<-EOF > entity_rows_with_values.csv
 entity_key,unix_milli,value_1,value_2
-1,$bt1,1,2
-2,$bt1,3,4
-1,$bt2,5,6
-2,$bt2,7,8
+1,$t0,1,2
+1,$t1,3,4
+1,$t2,5,6
+1,$t3,7,8
+2,$t0,1,2
+2,$t1,3,4
+2,$t2,5,6
+2,$t3,7,8
 EOF
 
     expected="
 entity_key,unix_milli,value_1,value_2,student.name,student.gender,student.age
-1,$bt1,1,2,,,
-2,$bt1,3,4,,,
-1,$bt2,5,6,lian,m,18
-2,$bt2,7,8,gao,m,20
+1,$t0,1,2,,,
+1,$t1,3,4,,,
+1,$t2,5,6,lian,m,18
+1,$t3,7,8,lian,m,18
+2,$t0,1,2,,,
+2,$t1,3,4,,,
+2,$t2,5,6,gao,m,20
+2,$t3,7,8,gao,m,20
 "
 
     actual=$(oomcli join \
@@ -95,34 +116,46 @@ oomcli_join_stream_historical_feature() {
 
     cat <<-EOF > entity_rows.csv
 entity_key,unix_milli
-1,$st0
-1,$st1
-1,$st2
-1,$st3
-2,$st1
-2,$st2
-2,$st3
-2,$st4
-3,$st1
-3,$st2
-3,$st3
-3,$st4
+1,$t0
+1,$t1
+1,$t2
+1,$t3
+1,$t4
+1,$t5
+2,$t0
+2,$t1
+2,$t2
+2,$t3
+2,$t4
+2,$t5
+3,$t0
+3,$t1
+3,$t2
+3,$t3
+3,$t4
+3,$t5
 EOF
 
     expected="
 entity_key,unix_milli,user-click.last_5_click_posts,user-click.number_of_user_starred_posts
-1,$st0,,
-1,$st1,\"0,1,2,3,4\",10
-1,$st2,\"1,2,3,4,5\",10
-1,$st3,\"1,2,3,4,5\",10
-2,$st1,,
-2,$st2,,
-2,$st3,\"2,3,4,5,6\",11
-2,$st4,\"2,3,4,5,6\",11
-3,$st1,,
-3,$st2,,
-3,$st3,,
-3,$st4,\"3,4,5,6,7\",12"
+1,$t0,,
+1,$t1,\"0,1,2,3,4\",10
+1,$t2,\"0,1,2,3,4\",10
+1,$t3,\"1,2,3,4,5\",10
+1,$t4,\"1,2,3,4,5\",10
+1,$t5,\"1,2,3,4,5\",10
+2,$t0,,
+2,$t1,,
+2,$t2,,
+2,$t3,,
+2,$t4,\"2,3,4,5,6\",11
+2,$t5,\"2,3,4,5,6\",11
+3,$t0,,
+3,$t1,,
+3,$t2,,
+3,$t3,,
+3,$t4,,
+3,$t5,\"3,4,5,6,7\",12"
 
     actual=$(oomcli join \
         --feature user-click.last_5_click_posts,user-click.number_of_user_starred_posts \
@@ -139,34 +172,46 @@ oomcli_join_stream_historical_feature_with_real_time_feature_value() {
 
     cat <<-EOF > entity_rows_with_values.csv
 entity_key,unix_milli,value_1,value_2
-1,$st0,1,2
-1,$st1,2,3
-1,$st2,3,4
-1,$st3,4,5
-2,$st1,1,2
-2,$st2,2,3
-2,$st3,3,4
-2,$st4,1,2
-3,$st1,2,3
-3,$st2,3,4
-3,$st3,4,5
-3,$st4,5,6
+1,$t0,1,2
+1,$t1,3,4
+1,$t2,5,6
+1,$t3,7,8
+1,$t4,9,10
+1,$t5,11,12
+2,$t0,1,2
+2,$t1,3,4
+2,$t2,5,6
+2,$t3,7,8
+2,$t4,9,10
+2,$t5,11,12
+3,$t0,1,2
+3,$t1,3,4
+3,$t2,5,6
+3,$t3,7,8
+3,$t4,9,10
+3,$t5,11,12
 EOF
 
     expected="
 entity_key,unix_milli,value_1,value_2,user-click.last_5_click_posts,user-click.number_of_user_starred_posts
-1,$st0,1,2,,
-1,$st1,2,3,\"0,1,2,3,4\",10
-1,$st2,3,4,\"1,2,3,4,5\",10
-1,$st3,4,5,\"1,2,3,4,5\",10
-2,$st1,1,2,,
-2,$st2,2,3,,
-2,$st3,3,4,\"2,3,4,5,6\",11
-2,$st4,1,2,\"2,3,4,5,6\",11
-3,$st1,2,3,,
-3,$st2,3,4,,
-3,$st3,4,5,,
-3,$st4,5,6,\"3,4,5,6,7\",12"
+1,$t0,1,2,,
+1,$t1,3,4,\"0,1,2,3,4\",10
+1,$t2,5,6,\"0,1,2,3,4\",10
+1,$t3,7,8,\"1,2,3,4,5\",10
+1,$t4,9,10,\"1,2,3,4,5\",10
+1,$t5,11,12,\"1,2,3,4,5\",10
+2,$t0,1,2,,
+2,$t1,3,4,,
+2,$t2,5,6,,
+2,$t3,7,8,,
+2,$t4,9,10,\"2,3,4,5,6\",11
+2,$t5,11,12,\"2,3,4,5,6\",11
+3,$t0,1,2,,
+3,$t1,3,4,,
+3,$t2,5,6,,
+3,$t3,7,8,,
+3,$t4,9,10,,
+3,$t5,11,12,\"3,4,5,6,7\",12"
 
     actual=$(oomcli join \
         --feature user-click.last_5_click_posts,user-click.number_of_user_starred_posts \
@@ -184,33 +229,45 @@ oomcli_join_batch_and_stream_historical_feature() {
 
    cat <<-EOF > entity_rows.csv
 entity_key,unix_milli
-1,$st0
-1,$st1
-1,$st2
-1,$st3
-2,$st1
-2,$st2
-2,$st3
-2,$st4
-3,$st1
-3,$st2
-3,$st3
-3,$st4
+1,$t0
+1,$t1
+1,$t2
+1,$t3
+1,$t4
+1,$t5
+2,$t0
+2,$t1
+2,$t2
+2,$t3
+2,$t4
+2,$t5
+3,$t0
+3,$t1
+3,$t2
+3,$t3
+3,$t4
+3,$t5
 EOF
     expected="
 entity_key,unix_milli,student.name,student.gender,student.age,user-click.last_5_click_posts,user-click.number_of_user_starred_posts
-1,$st0,lian,m,18,,
-1,$st1,lian,m,18,\"0,1,2,3,4\",10
-1,$st2,lian,m,18,\"1,2,3,4,5\",10
-1,$st3,lian,m,18,\"1,2,3,4,5\",10
-2,$st1,gao,m,20,,
-2,$st2,gao,m,20,,
-2,$st3,gao,m,20,\"2,3,4,5,6\",11
-2,$st4,gao,m,20,\"2,3,4,5,6\",11
-3,$st1,zhang,f,17,,
-3,$st2,zhang,f,17,,
-3,$st3,zhang,f,17,,
-3,$st4,zhang,f,17,\"3,4,5,6,7\",12"
+1,$t0,,,,,
+1,$t1,,,,\"0,1,2,3,4\",10
+1,$t2,lian,m,18,\"0,1,2,3,4\",10
+1,$t3,lian,m,18,\"1,2,3,4,5\",10
+1,$t4,lian,m,18,\"1,2,3,4,5\",10
+1,$t5,lian,m,18,\"1,2,3,4,5\",10
+2,$t0,,,,,
+2,$t1,,,,,
+2,$t2,gao,m,20,,
+2,$t3,gao,m,20,,
+2,$t4,gao,m,20,\"2,3,4,5,6\",11
+2,$t5,gao,m,20,\"2,3,4,5,6\",11
+3,$t0,,,,,
+3,$t1,,,,,
+3,$t2,zhang,f,17,,
+3,$t3,zhang,f,17,,
+3,$t4,zhang,f,17,,
+3,$t5,zhang,f,17,\"3,4,5,6,7\",12"
 
     actual=$(oomcli join \
         --feature student.name,student.gender,student.age,user-click.last_5_click_posts,user-click.number_of_user_starred_posts \
@@ -229,33 +286,45 @@ oomcli_join_batch_and_stream_historical_feature_with_real_time_feature_value() {
 
    cat <<-EOF > entity_rows.csv
 entity_key,unix_milli,value1,value2
-1,$st0,1,2
-1,$st1,2,3
-1,$st2,3,4
-1,$st3,4,5
-2,$st1,1,2
-2,$st2,2,3
-2,$st3,3,4
-2,$st4,4,5
-3,$st1,1,2
-3,$st2,2,3
-3,$st3,3,4
-3,$st4,4,5
+1,$t0,1,2
+1,$t1,3,4
+1,$t2,5,6
+1,$t3,7,8
+1,$t4,9,10
+1,$t5,11,12
+2,$t0,1,2
+2,$t1,3,4
+2,$t2,5,6
+2,$t3,7,8
+2,$t4,9,10
+2,$t5,11,12
+3,$t0,1,2
+3,$t1,3,4
+3,$t2,5,6
+3,$t3,7,8
+3,$t4,9,10
+3,$t5,11,12
 EOF
     expected="
 entity_key,unix_milli,value1,value2,student.name,student.gender,student.age,user-click.last_5_click_posts,user-click.number_of_user_starred_posts
-1,$st0,1,2,lian,m,18,,
-1,$st1,2,3,lian,m,18,\"0,1,2,3,4\",10
-1,$st2,3,4,lian,m,18,\"1,2,3,4,5\",10
-1,$st3,4,5,lian,m,18,\"1,2,3,4,5\",10
-2,$st1,1,2,gao,m,20,,
-2,$st2,2,3,gao,m,20,,
-2,$st3,3,4,gao,m,20,\"2,3,4,5,6\",11
-2,$st4,4,5,gao,m,20,\"2,3,4,5,6\",11
-3,$st1,1,2,zhang,f,17,,
-3,$st2,2,3,zhang,f,17,,
-3,$st3,3,4,zhang,f,17,,
-3,$st4,4,5,zhang,f,17,\"3,4,5,6,7\",12"
+1,$t0,1,2,,,,,
+1,$t1,3,4,,,,\"0,1,2,3,4\",10
+1,$t2,5,6,lian,m,18,\"0,1,2,3,4\",10
+1,$t3,7,8,lian,m,18,\"1,2,3,4,5\",10
+1,$t4,9,10,lian,m,18,\"1,2,3,4,5\",10
+1,$t5,11,12,lian,m,18,\"1,2,3,4,5\",10
+2,$t0,1,2,,,,,
+2,$t1,3,4,,,,,
+2,$t2,5,6,gao,m,20,,
+2,$t3,7,8,gao,m,20,,
+2,$t4,9,10,gao,m,20,\"2,3,4,5,6\",11
+2,$t5,11,12,gao,m,20,\"2,3,4,5,6\",11
+3,$t0,1,2,,,,,
+3,$t1,3,4,,,,,
+3,$t2,5,6,zhang,f,17,,
+3,$t3,7,8,zhang,f,17,,
+3,$t4,9,10,zhang,f,17,,
+3,$t5,11,12,zhang,f,17,\"3,4,5,6,7\",12"
 
     actual=$(oomcli join \
         --feature student.name,student.gender,student.age,user-click.last_5_click_posts,user-click.number_of_user_starred_posts \
diff --git a/pkg/oomstore/online_query_test.go b/pkg/oomstore/online_query_test.go
index b47c5cf6..534f2e5c 100644
--- a/pkg/oomstore/online_query_test.go
+++ b/pkg/oomstore/online_query_test.go
@@ -5,8 +5,6 @@ import (
 	"fmt"
 	"testing"
 
-	"github.com/oom-ai/oomstore/internal/database/metadata"
-
 	"github.com/golang/mock/gomock"
 	"github.com/oom-ai/oomstore/internal/database/dbutil"
 	"github.com/oom-ai/oomstore/internal/database/metadata/mock_metadata"
@@ -82,9 +80,7 @@ func TestOnlineGet(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.description, func(t *testing.T) {
-			metadataStore.EXPECT().ListCachedFeature(gomock.Any(), metadata.ListCachedFeatureOpt{
-				FullNames: &tc.opt.FeatureNames,
-			}).Return(tc.features)
+			metadataStore.EXPECT().ListCachedFeature(gomock.Any(), &tc.opt.FeatureNames).Return(tc.features)
 			if tc.entityName != nil {
 				onlineStore.EXPECT().Get(gomock.Any(), gomock.Any()).Return(dbutil.RowMap{
 					"price": int64(100),
@@ -178,9 +174,7 @@ func TestOnlineMultiGet(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.description, func(t *testing.T) {
-			metadataStore.EXPECT().ListCachedFeature(gomock.Any(), metadata.ListCachedFeatureOpt{
-				FullNames: &tc.opt.FeatureNames,
-			}).Return(tc.features)
+			metadataStore.EXPECT().ListCachedFeature(gomock.Any(), &tc.opt.FeatureNames).Return(tc.features)
 			if tc.entityName != nil {
 				onlineStore.EXPECT().MultiGet(gomock.Any(), gomock.Any()).Return(map[string]dbutil.RowMap{
 					"1234": {