Repository: oom-ai/oomstore
Pull request #824 (2021-12-27 06:42:37)

rename CreateBatchFeature with CreateFeature
this PR is renamed `CreateBatchFeature` to `CreateFeature` and makes it support create stream feature.

relate #820 


Patch:
diff --git a/internal/database/metadata/informer/feature.go b/internal/database/metadata/informer/feature.go
index 9cb4a05e..90efddb6 100644
--- a/internal/database/metadata/informer/feature.go
+++ b/internal/database/metadata/informer/feature.go
@@ -34,11 +34,11 @@ func (c *FeatureCache) List(opt metadata.ListFeatureOpt) types.FeatureList {
 	}
 
 	// filter names
-	if opt.FeatureFullNames != nil {
+	if opt.FeatureNames != nil {
 		var tmp types.FeatureList
-		for _, fullName := range *opt.FeatureFullNames {
+		for _, name := range *opt.FeatureNames {
 			if f := features.Find(func(f *types.Feature) bool {
-				return f.FullName == fullName
+				return f.Name == name
 			}); f != nil {
 				tmp = append(tmp, f)
 			}
diff --git a/internal/database/metadata/mock_metadata/store.go b/internal/database/metadata/mock_metadata/store.go
index 2ac445dd..276d592c 100644
--- a/internal/database/metadata/mock_metadata/store.go
+++ b/internal/database/metadata/mock_metadata/store.go
@@ -172,18 +172,18 @@ func (mr *MockStoreMockRecorder) GetFeature(ctx, id interface{}) *gomock.Call {
 }
 
 // GetFeatureByName mocks base method.
-func (m *MockStore) GetFeatureByName(ctx context.Context, fullName string) (*types.Feature, error) {
+func (m *MockStore) GetFeatureByName(ctx context.Context, name string) (*types.Feature, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetFeatureByName", ctx, fullName)
+	ret := m.ctrl.Call(m, "GetFeatureByName", ctx, name)
 	ret0, _ := ret[0].(*types.Feature)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // GetFeatureByName indicates an expected call of GetFeatureByName.
-func (mr *MockStoreMockRecorder) GetFeatureByName(ctx, fullName interface{}) *gomock.Call {
+func (mr *MockStoreMockRecorder) GetFeatureByName(ctx, name interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeatureByName", reflect.TypeOf((*MockStore)(nil).GetFeatureByName), ctx, fullName)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeatureByName", reflect.TypeOf((*MockStore)(nil).GetFeatureByName), ctx, name)
 }
 
 // GetGroup mocks base method.
@@ -534,18 +534,18 @@ func (mr *MockDBStoreMockRecorder) GetFeature(ctx, id interface{}) *gomock.Call
 }
 
 // GetFeatureByName mocks base method.
-func (m *MockDBStore) GetFeatureByName(ctx context.Context, fullName string) (*types.Feature, error) {
+func (m *MockDBStore) GetFeatureByName(ctx context.Context, name string) (*types.Feature, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetFeatureByName", ctx, fullName)
+	ret := m.ctrl.Call(m, "GetFeatureByName", ctx, name)
 	ret0, _ := ret[0].(*types.Feature)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // GetFeatureByName indicates an expected call of GetFeatureByName.
-func (mr *MockDBStoreMockRecorder) GetFeatureByName(ctx, fullName interface{}) *gomock.Call {
+func (mr *MockDBStoreMockRecorder) GetFeatureByName(ctx, name interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeatureByName", reflect.TypeOf((*MockDBStore)(nil).GetFeatureByName), ctx, fullName)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeatureByName", reflect.TypeOf((*MockDBStore)(nil).GetFeatureByName), ctx, name)
 }
 
 // GetGroup mocks base method.
diff --git a/internal/database/metadata/mysql/group.go b/internal/database/metadata/mysql/group.go
index 865fd269..4d255059 100644
--- a/internal/database/metadata/mysql/group.go
+++ b/internal/database/metadata/mysql/group.go
@@ -6,12 +6,13 @@ import (
 
 	"github.com/go-sql-driver/mysql"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
+	"github.com/oom-ai/oomstore/pkg/errdefs"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
 )
 
 func createGroup(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.CreateGroupOpt) (int, error) {
 	if opt.Category != types.CategoryBatch && opt.Category != types.CategoryStream {
-		return 0, fmt.Errorf("illegal category '%s', should be either 'stream' or 'batch'", opt.Category)
+		return 0, errdefs.InvalidAttribute(fmt.Errorf("illegal category '%s', should be either 'stream' or 'batch'", opt.Category))
 	}
 
 	query := "INSERT INTO feature_group(name, entity_id, category, description) VALUES(?, ?, ?, ?)"
diff --git a/internal/database/metadata/postgres/group.go b/internal/database/metadata/postgres/group.go
index 2df24e87..a1e5ccda 100644
--- a/internal/database/metadata/postgres/group.go
+++ b/internal/database/metadata/postgres/group.go
@@ -7,12 +7,13 @@ import (
 	"github.com/jackc/pgerrcode"
 	"github.com/lib/pq"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
+	"github.com/oom-ai/oomstore/pkg/errdefs"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
 )
 
 func createGroup(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.CreateGroupOpt) (int, error) {
 	if opt.Category != types.CategoryBatch && opt.Category != types.CategoryStream {
-		return 0, fmt.Errorf("illegal category '%s', should be either 'stream' or 'batch'", opt.Category)
+		return 0, errdefs.InvalidAttribute(fmt.Errorf("illegal category '%s', should be either 'stream' or 'batch'", opt.Category))
 	}
 	var groupID int
 	query := "insert into feature_group(name, entity_id, category, description) values($1, $2, $3, $4) returning id"
diff --git a/internal/database/metadata/sqlite/group.go b/internal/database/metadata/sqlite/group.go
index 55fd4800..57e6603d 100644
--- a/internal/database/metadata/sqlite/group.go
+++ b/internal/database/metadata/sqlite/group.go
@@ -6,12 +6,13 @@ import (
 
 	"github.com/mattn/go-sqlite3"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
+	"github.com/oom-ai/oomstore/pkg/errdefs"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
 )
 
 func createGroup(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.CreateGroupOpt) (int, error) {
 	if opt.Category != types.CategoryBatch && opt.Category != types.CategoryStream {
-		return 0, fmt.Errorf("illegal category '%s', should be either 'stream' or 'batch'", opt.Category)
+		return 0, errdefs.InvalidAttribute(fmt.Errorf("illegal category '%s', should be either 'stream' or 'batch'", opt.Category))
 	}
 
 	query := "INSERT INTO feature_group(name, entity_id, category, description) VALUES(?, ?, ?, ?)"
diff --git a/internal/database/metadata/sqlutil/feature.go b/internal/database/metadata/sqlutil/feature.go
index 7d8a9e22..895e7a2f 100644
--- a/internal/database/metadata/sqlutil/feature.go
+++ b/internal/database/metadata/sqlutil/feature.go
@@ -55,17 +55,17 @@ func GetFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, id int) (*typ
 	return &feature, nil
 }
 
-func GetFeatureByName(ctx context.Context, sqlxCtx metadata.SqlxContext, fullName string) (*types.Feature, error) {
+func GetFeatureByName(ctx context.Context, sqlxCtx metadata.SqlxContext, name string) (*types.Feature, error) {
 	var (
 		feature types.Feature
 		group   *types.Group
 		err     error
 	)
 
-	query := `SELECT * FROM feature WHERE full_name = ?`
-	if err := sqlxCtx.GetContext(ctx, &feature, sqlxCtx.Rebind(query), fullName); err != nil {
+	query := `SELECT * FROM feature WHERE name = ?`
+	if err := sqlxCtx.GetContext(ctx, &feature, sqlxCtx.Rebind(query), name); err != nil {
 		if err == sql.ErrNoRows {
-			return nil, errdefs.NotFound(fmt.Errorf("feature %s not found", fullName))
+			return nil, errdefs.NotFound(fmt.Errorf("feature %s not found", name))
 		}
 		return nil, err
 	}
@@ -134,11 +134,11 @@ func buildListFeatureCond(opt metadata.ListFeatureOpt) ([]string, []interface{},
 		in["id"] = *opt.FeatureIDs
 	}
 
-	if opt.FeatureFullNames != nil {
-		if len(*opt.FeatureFullNames) == 0 {
+	if opt.FeatureNames != nil {
+		if len(*opt.FeatureNames) == 0 {
 			return []string{"false"}, nil, nil
 		}
-		in["full_name"] = *opt.FeatureFullNames
+		in["name"] = *opt.FeatureNames
 	}
 	return dbutil.BuildConditions(and, in)
 }
diff --git a/internal/database/metadata/store.go b/internal/database/metadata/store.go
index 88747c09..b456b3df 100644
--- a/internal/database/metadata/store.go
+++ b/internal/database/metadata/store.go
@@ -29,7 +29,7 @@ type DBStore interface {
 	CreateFeature(ctx context.Context, opt CreateFeatureOpt) (int, error)
 	UpdateFeature(ctx context.Context, opt UpdateFeatureOpt) error
 	GetFeature(ctx context.Context, id int) (*types.Feature, error)
-	GetFeatureByName(ctx context.Context, fullName string) (*types.Feature, error)
+	GetFeatureByName(ctx context.Context, name string) (*types.Feature, error)
 	ListFeature(ctx context.Context, opt ListFeatureOpt) (types.FeatureList, error)
 
 	// feature group
diff --git a/internal/database/metadata/types.go b/internal/database/metadata/types.go
index 0a2c863a..a0914439 100644
--- a/internal/database/metadata/types.go
+++ b/internal/database/metadata/types.go
@@ -60,8 +60,8 @@ type UpdateRevisionOpt struct {
 }
 
 type ListFeatureOpt struct {
-	EntityID         *int
-	GroupID          *int
-	FeatureIDs       *[]int
-	FeatureFullNames *[]string
+	EntityID     *int
+	GroupID      *int
+	FeatureIDs   *[]int
+	FeatureNames *[]string
 }
diff --git a/oomcli/cmd/edit_feature.go b/oomcli/cmd/edit_feature.go
index 154bd7e4..4891c77d 100644
--- a/oomcli/cmd/edit_feature.go
+++ b/oomcli/cmd/edit_feature.go
@@ -29,7 +29,7 @@ var editFeatureCmd = &cobra.Command{
 		if len(args) > 1 {
 			log.Fatalf("argument at most one, got %d", len(args))
 		} else if len(args) == 1 {
-			editFeatureOpt.FeatureFullNames = &[]string{args[0]}
+			editFeatureOpt.FeatureNames = &[]string{args[0]}
 		}
 
 	},
diff --git a/oomcli/cmd/get_meta_feature.go b/oomcli/cmd/get_meta_feature.go
index 2d37c89b..2c15d320 100644
--- a/oomcli/cmd/get_meta_feature.go
+++ b/oomcli/cmd/get_meta_feature.go
@@ -28,7 +28,7 @@ var getMetaFeatureCmd = &cobra.Command{
 			getMetaFeatureOpt.GroupName = nil
 		}
 		if len(args) == 1 {
-			getMetaFeatureOpt.FeatureFullNames = &[]string{args[0]}
+			getMetaFeatureOpt.FeatureNames = &[]string{args[0]}
 		}
 	},
 	Run: func(cmd *cobra.Command, args []string) {
@@ -61,8 +61,8 @@ func queryFeatures(ctx context.Context, oomStore *oomstore.OomStore, opt types.L
 		return nil, fmt.Errorf("failed getting features, error %v\n", err)
 	}
 
-	if opt.FeatureFullNames != nil && len(features) == 0 {
-		return nil, fmt.Errorf("feature '%s' not found", (*opt.FeatureFullNames)[0])
+	if opt.FeatureNames != nil && len(features) == 0 {
+		return nil, fmt.Errorf("feature '%s' not found", (*opt.FeatureNames)[0])
 	}
 
 	return features, nil
diff --git a/oomcli/cmd/register_batch_feature.go b/oomcli/cmd/register_batch_feature.go
index 00a2a5a2..2e27360b 100644
--- a/oomcli/cmd/register_batch_feature.go
+++ b/oomcli/cmd/register_batch_feature.go
@@ -28,7 +28,7 @@ var registerBatchFeatureCmd = &cobra.Command{
 		oomStore := mustOpenOomStore(ctx, oomStoreCfg)
 		defer oomStore.Close()
 
-		if _, err := oomStore.CreateBatchFeature(ctx, registerBatchFeatureOpt); err != nil {
+		if _, err := oomStore.CreateFeature(ctx, registerBatchFeatureOpt); err != nil {
 			log.Fatalf("failed registering new feature: %v\n", err)
 		}
 	},
diff --git a/oomcli/cmd/update_feature.go b/oomcli/cmd/update_feature.go
index b83d3ade..6b32fc3c 100644
--- a/oomcli/cmd/update_feature.go
+++ b/oomcli/cmd/update_feature.go
@@ -15,7 +15,7 @@ var updateFeatureCmd = &cobra.Command{
 	Short: "Update a particular feature",
 	Args:  cobra.ExactArgs(1),
 	PreRun: func(cmd *cobra.Command, args []string) {
-		updateFeatureOpt.FeatureFullName = args[0]
+		updateFeatureOpt.FeatureName = args[0]
 		if !cmd.Flags().Changed("description") {
 			updateFeatureOpt.NewDescription = nil
 		}
@@ -26,7 +26,7 @@ var updateFeatureCmd = &cobra.Command{
 		defer oomStore.Close()
 
 		if err := oomStore.UpdateFeature(ctx, updateFeatureOpt); err != nil {
-			log.Fatalf("failed to update feature %s, err %v\n", updateFeatureOpt.FeatureFullName, err)
+			log.Fatalf("failed to update feature %s, err %v\n", updateFeatureOpt.FeatureName, err)
 		}
 	},
 }
diff --git a/pkg/oomstore/export.go b/pkg/oomstore/export.go
index 3f8807bf..50f3d47c 100644
--- a/pkg/oomstore/export.go
+++ b/pkg/oomstore/export.go
@@ -36,12 +36,8 @@ func (s *OomStore) ChannelExport(ctx context.Context, opt types.ChannelExportOpt
 			GroupName: &revision.Group.Name,
 		})
 	} else {
-		fullNames := make([]string, 0, len(opt.FeatureNames))
-		for _, name := range opt.FeatureNames {
-			fullNames = append(fullNames, fmt.Sprintf("%s.%s", revision.Group.Name, name))
-		}
 		features, err = s.ListFeature(ctx, types.ListFeatureOpt{
-			FeatureFullNames: &fullNames,
+			FeatureNames: &opt.FeatureNames,
 		})
 	}
 	if err != nil {
diff --git a/pkg/oomstore/feature.go b/pkg/oomstore/feature.go
index bb019b2b..968664fd 100644
--- a/pkg/oomstore/feature.go
+++ b/pkg/oomstore/feature.go
@@ -13,15 +13,15 @@ func (s *OomStore) GetFeature(ctx context.Context, id int) (*types.Feature, erro
 	return s.metadata.GetFeature(ctx, id)
 }
 
-// Get metadata of a feature by full name.
-func (s *OomStore) GetFeatureByName(ctx context.Context, fullName string) (*types.Feature, error) {
-	return s.metadata.GetFeatureByName(ctx, fullName)
+// Get metadata of a feature by name.
+func (s *OomStore) GetFeatureByName(ctx context.Context, name string) (*types.Feature, error) {
+	return s.metadata.GetFeatureByName(ctx, name)
 }
 
 // List metadata of features meeting particular criteria.
 func (s *OomStore) ListFeature(ctx context.Context, opt types.ListFeatureOpt) (types.FeatureList, error) {
 	metadataOpt := metadata.ListFeatureOpt{
-		FeatureFullNames: opt.FeatureFullNames,
+		FeatureNames: opt.FeatureNames,
 	}
 	if opt.EntityName != nil {
 		entity, err := s.metadata.GetEntityByName(ctx, *opt.EntityName)
@@ -42,7 +42,7 @@ func (s *OomStore) ListFeature(ctx context.Context, opt types.ListFeatureOpt) (t
 
 // Update metadata of a feature.
 func (s *OomStore) UpdateFeature(ctx context.Context, opt types.UpdateFeatureOpt) error {
-	feature, err := s.metadata.GetFeatureByName(ctx, opt.FeatureFullName)
+	feature, err := s.metadata.GetFeatureByName(ctx, opt.FeatureName)
 	if err != nil {
 		return err
 	}
@@ -52,15 +52,12 @@ func (s *OomStore) UpdateFeature(ctx context.Context, opt types.UpdateFeatureOpt
 	})
 }
 
-// Create metadata of a batch feature.
-func (s *OomStore) CreateBatchFeature(ctx context.Context, opt types.CreateFeatureOpt) (int, error) {
+// Create metadata of a feature.
+func (s *OomStore) CreateFeature(ctx context.Context, opt types.CreateFeatureOpt) (int, error) {
 	group, err := s.metadata.GetGroupByName(ctx, opt.GroupName)
 	if err != nil {
 		return 0, err
 	}
-	if group.Category != types.CategoryBatch {
-		return 0, fmt.Errorf("expected batch feature group, got %s feature group", group.Category)
-	}
 
 	return s.metadata.CreateFeature(ctx, metadata.CreateFeatureOpt{
 		FeatureName: opt.FeatureName,
diff --git a/pkg/oomstore/group.go b/pkg/oomstore/group.go
index af72fb11..b4633268 100644
--- a/pkg/oomstore/group.go
+++ b/pkg/oomstore/group.go
@@ -13,13 +13,12 @@ func (s *OomStore) CreateGroup(ctx context.Context, opt types.CreateGroupOpt) (i
 	if err != nil {
 		return 0, err
 	}
+
 	return s.metadata.CreateGroup(ctx, metadata.CreateGroupOpt{
 		GroupName:   opt.GroupName,
 		EntityID:    entity.ID,
 		Description: opt.Description,
-		// Via the oomstore API, we can only create a batch feature group
-		// So we hardcode the category to be batch
-		Category: types.CategoryBatch,
+		Category:    opt.Category,
 	})
 }
 
diff --git a/pkg/oomstore/join.go b/pkg/oomstore/join.go
index a578e1ab..997e7369 100644
--- a/pkg/oomstore/join.go
+++ b/pkg/oomstore/join.go
@@ -20,7 +20,7 @@ import (
 // Currently, this API only supports batch features.
 func (s *OomStore) ChannelJoin(ctx context.Context, opt types.ChannelJoinOpt) (*types.JoinResult, error) {
 	features, err := s.metadata.ListFeature(ctx, metadata.ListFeatureOpt{
-		FeatureFullNames: &opt.FeatureNames,
+		FeatureNames: &opt.FeatureNames,
 	})
 	if err != nil {
 		return nil, err
diff --git a/pkg/oomstore/online_query.go b/pkg/oomstore/online_query.go
index 8050df4a..58ff4a98 100644
--- a/pkg/oomstore/online_query.go
+++ b/pkg/oomstore/online_query.go
@@ -13,7 +13,7 @@ import (
 // Get online features of a particular entity instance.
 func (s *OomStore) OnlineGet(ctx context.Context, opt types.OnlineGetOpt) (*types.FeatureValues, error) {
 	features := s.metadata.CacheListFeature(ctx, metadata.ListFeatureOpt{
-		FeatureFullNames: &opt.FeatureNames,
+		FeatureNames: &opt.FeatureNames,
 	}).Filter(func(f *types.Feature) bool {
 		return f.Group.OnlineRevisionID != nil
 	})
@@ -58,7 +58,7 @@ func (s *OomStore) OnlineGet(ctx context.Context, opt types.OnlineGetOpt) (*type
 // Get online features of multiple entity instances.
 func (s *OomStore) OnlineMultiGet(ctx context.Context, opt types.OnlineMultiGetOpt) (map[string]*types.FeatureValues, error) {
 	features := s.metadata.CacheListFeature(ctx, metadata.ListFeatureOpt{
-		FeatureFullNames: &opt.FeatureNames,
+		FeatureNames: &opt.FeatureNames,
 	})
 
 	features = features.Filter(func(f *types.Feature) bool {
diff --git a/pkg/oomstore/types/feature.go b/pkg/oomstore/types/feature.go
index cc2f3c34..e22f44b0 100644
--- a/pkg/oomstore/types/feature.go
+++ b/pkg/oomstore/types/feature.go
@@ -60,13 +60,6 @@ func (l *FeatureList) Names() (names []string) {
 	return
 }
 
-func (l *FeatureList) FullNames() (fullNames []string) {
-	for _, f := range *l {
-		fullNames = append(fullNames, f.FullName)
-	}
-	return
-}
-
 func (l *FeatureList) IDs() (ids []int) {
 	for _, f := range *l {
 		ids = append(ids, f.ID)
diff --git a/pkg/oomstore/types/options.go b/pkg/oomstore/types/options.go
index c01a8b3a..5d303daf 100644
--- a/pkg/oomstore/types/options.go
+++ b/pkg/oomstore/types/options.go
@@ -8,14 +8,14 @@ type CreateFeatureOpt struct {
 }
 
 type ListFeatureOpt struct {
-	EntityName       *string
-	GroupName        *string
-	FeatureFullNames *[]string
+	EntityName   *string
+	GroupName    *string
+	FeatureNames *[]string
 }
 
 type UpdateFeatureOpt struct {
-	FeatureFullName string
-	NewDescription  *string
+	FeatureName    string
+	NewDescription *string
 }
 
 type CreateEntityOpt struct {
@@ -27,6 +27,7 @@ type CreateEntityOpt struct {
 type CreateGroupOpt struct {
 	GroupName   string
 	EntityName  string
+	Category    Category
 	Description string
 }
 


Test patch:
diff --git a/internal/database/metadata/test_impl/feature.go b/internal/database/metadata/test_impl/feature.go
index 2a9b7c08..11786ea0 100644
--- a/internal/database/metadata/test_impl/feature.go
+++ b/internal/database/metadata/test_impl/feature.go
@@ -146,7 +146,6 @@ func TestGetFeatureByName(t *testing.T, prepareStore PrepareStoreFn, destroyStor
 
 	_, err := store.CreateFeature(ctx, metadata.CreateFeatureOpt{
 		FeatureName: "phone",
-		FullName:    "device_info.phone",
 		GroupID:     groupID,
 		Description: "description",
 		ValueType:   types.String,
@@ -158,12 +157,11 @@ func TestGetFeatureByName(t *testing.T, prepareStore PrepareStoreFn, destroyStor
 	assert.EqualError(t, err, "feature p not found")
 
 	// case 2: correct feature name, return feature `phone`
-	feature, err := store.GetFeatureByName(ctx, "device_info.phone")
+	feature, err := store.GetFeatureByName(ctx, "phone")
 	assert.NoError(t, err)
 	expected := &types.Feature{
 		ID:          1,
 		Name:        "phone",
-		FullName:    "device_info.phone",
 		ValueType:   types.String,
 		Description: "description",
 		GroupID:     1,
@@ -185,7 +183,6 @@ func TestCacheListFeature(t *testing.T, prepareStore PrepareStoreFn, destroyStor
 
 	featureID, err := store.CreateFeature(ctx, metadata.CreateFeatureOpt{
 		FeatureName: "phone",
-		FullName:    "device_info.phone",
 		GroupID:     groupID,
 		Description: "description",
 		ValueType:   types.String,
@@ -229,13 +226,6 @@ func TestCacheListFeature(t *testing.T, prepareStore PrepareStoreFn, destroyStor
 		EntityID: &entityID,
 	})
 	assert.Equal(t, 1, len(features))
-
-	// case 8: list features by FeatureFullNames
-	features, err = store.ListFeature(ctx, metadata.ListFeatureOpt{
-		FeatureFullNames: &[]string{"device_info.phone"},
-	})
-	assert.NoError(t, err)
-	assert.Equal(t, 1, len(features))
 }
 
 func TestListFeature(t *testing.T, prepareStore PrepareStoreFn, destroyStore DestroyStoreFn) {
@@ -252,7 +242,6 @@ func TestListFeature(t *testing.T, prepareStore PrepareStoreFn, destroyStore Des
 
 	featureID, err := store.CreateFeature(ctx, metadata.CreateFeatureOpt{
 		FeatureName: "phone",
-		FullName:    "device_info.phone",
 		GroupID:     groupID,
 		Description: "description",
 		ValueType:   types.String,
@@ -301,13 +290,6 @@ func TestListFeature(t *testing.T, prepareStore PrepareStoreFn, destroyStore Des
 	})
 	assert.NoError(t, err)
 	assert.Equal(t, 1, len(features))
-
-	// case 8: list features by FeatureFullNames
-	features, err = store.ListFeature(ctx, metadata.ListFeatureOpt{
-		FeatureFullNames: &[]string{"device_info.phone"},
-	})
-	assert.NoError(t, err)
-	assert.Equal(t, 1, len(features))
 }
 
 func TestUpdateFeature(t *testing.T, prepareStore PrepareStoreFn, destroyStore DestroyStoreFn) {
diff --git a/oomcli/test/test_get_meta_feature.sh b/oomcli/test/test_get_meta_feature.sh
index 5f215439..4f240ba2 100755
--- a/oomcli/test/test_get_meta_feature.sh
+++ b/oomcli/test/test_get_meta_feature.sh
@@ -31,12 +31,12 @@ expected='
 ID,NAME,GROUP,ENTITY,CATEGORY,VALUE-TYPE,DESCRIPTION,ONLINE-REVISION-ID
 2,model,phone,device,batch,string,model,<NULL>
 '
-actual=$(oomcli get meta feature phone.model -o csv --wide)
+actual=$(oomcli get meta feature model -o csv --wide)
 ignore_time() { cut -d ',' -f 1-8 <<<"$1"; }
 assert_eq "$case" "$(sort <<< "$expected")" "$(ignore_time "$actual" | sort)"
 
 
-case='oomcli get meta feature in yaml: one feature'
+case='oomcli get meta feature: one feature'
 expected='
 kind: Feature
 name: model
@@ -45,7 +45,7 @@ value-type: string
 description: model
 '
 
-actual=$(oomcli get meta feature phone.model -o yaml)
+actual=$(oomcli get meta feature model -o yaml)
 assert_eq "$case" "$expected" "$actual"
 
 case='oomcli get meta feature: multiple features'
diff --git a/pkg/oomstore/feature_test.go b/pkg/oomstore/feature_test.go
index 1eda22b9..fe75406a 100644
--- a/pkg/oomstore/feature_test.go
+++ b/pkg/oomstore/feature_test.go
@@ -15,7 +15,7 @@ import (
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
 )
 
-func TestCreateBatchFeature(t *testing.T) {
+func TestCreateFeature(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 	ctx := context.Background()
@@ -29,7 +29,6 @@ func TestCreateBatchFeature(t *testing.T) {
 		opt         types.CreateFeatureOpt
 		valueType   types.ValueType
 		group       types.Group
-		expectError bool
 	}{
 		{
 			description: "create batch feature, succeed",
@@ -44,10 +43,9 @@ func TestCreateBatchFeature(t *testing.T) {
 				Name:     "device_info",
 				Category: types.CategoryBatch,
 			},
-			expectError: false,
 		},
 		{
-			description: "create stream feature, fail",
+			description: "create stream feature, succeed",
 			opt: types.CreateFeatureOpt{
 				FeatureName: "model",
 				GroupName:   "device_info",
@@ -59,7 +57,6 @@ func TestCreateBatchFeature(t *testing.T) {
 				Name:     "device_info",
 				Category: types.CategoryStream,
 			},
-			expectError: true,
 		},
 	}
 
@@ -68,22 +65,17 @@ func TestCreateBatchFeature(t *testing.T) {
 			metadataStore.EXPECT().Refresh().Return(nil).AnyTimes()
 			metadataStore.EXPECT().GetGroupByName(ctx, tc.opt.GroupName).Return(&tc.group, nil)
 
-			if tc.group.Category == types.CategoryBatch {
-				metadataOpt := metadata.CreateFeatureOpt{
-					FeatureName: tc.opt.FeatureName,
-					FullName:    fmt.Sprintf("%s.%s", tc.opt.GroupName, tc.opt.FeatureName),
-					GroupID:     tc.group.ID,
-					ValueType:   tc.valueType,
-					Description: tc.opt.Description,
-				}
-				metadataStore.EXPECT().CreateFeature(ctx, metadataOpt).Return(0, nil)
-			}
-			_, err := store.CreateBatchFeature(ctx, tc.opt)
-			if tc.expectError {
-				assert.Error(t, err, fmt.Errorf("expected batch feature group, got %s feature group", tc.group.Category))
-			} else {
-				assert.NoError(t, err)
+			metadataOpt := metadata.CreateFeatureOpt{
+				FeatureName: tc.opt.FeatureName,
+				FullName:    fmt.Sprintf("%s.%s", tc.opt.GroupName, tc.opt.FeatureName),
+				GroupID:     tc.group.ID,
+				ValueType:   tc.valueType,
+				Description: tc.opt.Description,
 			}
+			metadataStore.EXPECT().CreateFeature(ctx, metadataOpt).Return(0, nil)
+
+			_, err := store.CreateFeature(ctx, tc.opt)
+			assert.NoError(t, err)
 		})
 	}
 }
diff --git a/pkg/oomstore/join_test.go b/pkg/oomstore/join_test.go
index d2194f74..11ae70a2 100644
--- a/pkg/oomstore/join_test.go
+++ b/pkg/oomstore/join_test.go
@@ -111,7 +111,7 @@ func TestChannelJoin(t *testing.T) {
 	for _, tc := range testCases {
 		t.Run(tc.description, func(t *testing.T) {
 			metadataStore.EXPECT().Refresh().Return(nil).AnyTimes()
-			metadataStore.EXPECT().ListFeature(gomock.Any(), metadata.ListFeatureOpt{FeatureFullNames: &tc.opt.FeatureNames}).Return(tc.features, nil)
+			metadataStore.EXPECT().ListFeature(gomock.Any(), metadata.ListFeatureOpt{FeatureNames: &tc.opt.FeatureNames}).Return(tc.features, nil)
 			if tc.entity != nil {
 				for _, featureList := range tc.featureMap {
 					metadataStore.EXPECT().ListRevision(gomock.Any(), &featureList[0].GroupID).Return(revisions, nil).AnyTimes()
diff --git a/pkg/oomstore/online_query_test.go b/pkg/oomstore/online_query_test.go
index 512f1bac..c5439aed 100644
--- a/pkg/oomstore/online_query_test.go
+++ b/pkg/oomstore/online_query_test.go
@@ -78,7 +78,7 @@ func TestOnlineGet(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.description, func(t *testing.T) {
-			metadataStore.EXPECT().CacheListFeature(gomock.Any(), metadata.ListFeatureOpt{FeatureFullNames: &tc.opt.FeatureNames}).Return(tc.features)
+			metadataStore.EXPECT().CacheListFeature(gomock.Any(), metadata.ListFeatureOpt{FeatureNames: &tc.opt.FeatureNames}).Return(tc.features)
 			if tc.entityName != nil {
 				onlineStore.EXPECT().Get(gomock.Any(), gomock.Any()).Return(dbutil.RowMap{
 					"price": int64(100),
@@ -173,7 +173,7 @@ func TestOnlineMultiGet(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.description, func(t *testing.T) {
-			metadataStore.EXPECT().CacheListFeature(gomock.Any(), metadata.ListFeatureOpt{FeatureFullNames: &tc.opt.FeatureNames}).Return(tc.features)
+			metadataStore.EXPECT().CacheListFeature(gomock.Any(), metadata.ListFeatureOpt{FeatureNames: &tc.opt.FeatureNames}).Return(tc.features)
 			if tc.entityName != nil {
 				onlineStore.EXPECT().MultiGet(gomock.Any(), gomock.Any()).Return(map[string]dbutil.RowMap{
 					"1234": {