Repository: oom-ai/oomstore
Pull request #979 (2022-01-17 01:13:33)

Remove fullname from feature table


Patch:
diff --git a/internal/database/metadata/informer/feature.go b/internal/database/metadata/informer/feature.go
index 9cb4a05e..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,28 +16,15 @@ func (c *FeatureCache) Enrich(groupCache *GroupCache) {
 	}
 }
 
-func (c *FeatureCache) List(opt metadata.ListFeatureOpt) types.FeatureList {
+func (c *FeatureCache) List(fullNames *[]string) types.FeatureList {
 	features := c.FeatureList
 
-	// filter ids
-	if opt.FeatureIDs != nil {
-		var tmp types.FeatureList
-		for _, id := range *opt.FeatureIDs {
-			if f := c.Find(func(f *types.Feature) bool {
-				return f.ID == id
-			}); f != nil {
-				tmp = append(tmp, f)
-			}
-		}
-		features = tmp
-	}
-
 	// filter names
-	if opt.FeatureFullNames != nil {
+	if fullNames != nil {
 		var tmp types.FeatureList
-		for _, fullName := range *opt.FeatureFullNames {
+		for _, fullName := range *fullNames {
 			if f := features.Find(func(f *types.Feature) bool {
-				return f.FullName == fullName
+				return f.FullName() == fullName
 			}); f != nil {
 				tmp = append(tmp, f)
 			}
@@ -46,18 +32,5 @@ func (c *FeatureCache) List(opt metadata.ListFeatureOpt) types.FeatureList {
 		features = tmp
 	}
 
-	// filter entity
-	if opt.EntityID != nil {
-		features = features.Filter(func(f *types.Feature) bool {
-			return f.Entity().ID == *opt.EntityID
-		})
-	}
-
-	// filter group
-	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/informer.go b/internal/database/metadata/informer/informer.go
index 60f57461..fdbb38b5 100644
--- a/internal/database/metadata/informer/informer.go
+++ b/internal/database/metadata/informer/informer.go
@@ -88,6 +88,6 @@ func (f *Informer) Cache() *Cache {
 	return f.cache.Load().(*Cache)
 }
 
-func (f *Informer) CacheListFeature(ctx context.Context, opt metadata.ListFeatureOpt) types.FeatureList {
-	return f.Cache().Features.List(opt).Copy()
+func (f *Informer) ListCachedFeature(ctx context.Context, fullNames *[]string) types.FeatureList {
+	return f.Cache().Features.List(fullNames).Copy()
 }
diff --git a/internal/database/metadata/mock_metadata/store.go b/internal/database/metadata/mock_metadata/store.go
index 2ac445dd..4179e367 100644
--- a/internal/database/metadata/mock_metadata/store.go
+++ b/internal/database/metadata/mock_metadata/store.go
@@ -37,20 +37,6 @@ func (m *MockStore) EXPECT() *MockStoreMockRecorder {
 	return m.recorder
 }
 
-// CacheListFeature mocks base method.
-func (m *MockStore) CacheListFeature(ctx context.Context, opt metadata.ListFeatureOpt) types.FeatureList {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "CacheListFeature", ctx, opt)
-	ret0, _ := ret[0].(types.FeatureList)
-	return ret0
-}
-
-// CacheListFeature indicates an expected call of CacheListFeature.
-func (mr *MockStoreMockRecorder) CacheListFeature(ctx, opt interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CacheListFeature", reflect.TypeOf((*MockStore)(nil).CacheListFeature), ctx, opt)
-}
-
 // Close mocks base method.
 func (m *MockStore) Close() error {
 	m.ctrl.T.Helper()
@@ -172,18 +158,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, groupName, featureName string) (*types.Feature, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetFeatureByName", ctx, fullName)
+	ret := m.ctrl.Call(m, "GetFeatureByName", ctx, groupName, featureName)
 	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, groupName, featureName 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, groupName, featureName)
 }
 
 // GetGroup mocks base method.
@@ -246,6 +232,20 @@ func (mr *MockStoreMockRecorder) GetRevisionBy(ctx, groupID, revision interface{
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRevisionBy", reflect.TypeOf((*MockStore)(nil).GetRevisionBy), ctx, groupID, revision)
 }
 
+// ListCachedFeature mocks base method.
+func (m *MockStore) ListCachedFeature(ctx context.Context, fullNames *[]string) types.FeatureList {
+	m.ctrl.T.Helper()
+	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, fullNames interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCachedFeature", reflect.TypeOf((*MockStore)(nil).ListCachedFeature), ctx, fullNames)
+}
+
 // ListEntity mocks base method.
 func (m *MockStore) ListEntity(ctx context.Context, entityIDs *[]int) (types.EntityList, error) {
 	m.ctrl.T.Helper()
@@ -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, groupName, featureName string) (*types.Feature, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetFeatureByName", ctx, fullName)
+	ret := m.ctrl.Call(m, "GetFeatureByName", ctx, groupName, featureName)
 	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, groupName, featureName 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, groupName, featureName)
 }
 
 // GetGroup mocks base method.
@@ -761,18 +761,18 @@ func (m *MockCacheStore) EXPECT() *MockCacheStoreMockRecorder {
 	return m.recorder
 }
 
-// CacheListFeature mocks base method.
-func (m *MockCacheStore) CacheListFeature(ctx context.Context, opt metadata.ListFeatureOpt) types.FeatureList {
+// ListCachedFeature mocks base method.
+func (m *MockCacheStore) ListCachedFeature(ctx context.Context, fullNames *[]string) types.FeatureList {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "CacheListFeature", ctx, opt)
+	ret := m.ctrl.Call(m, "ListCachedFeature", ctx, fullNames)
 	ret0, _ := ret[0].(types.FeatureList)
 	return ret0
 }
 
-// CacheListFeature indicates an expected call of CacheListFeature.
-func (mr *MockCacheStoreMockRecorder) CacheListFeature(ctx, opt interface{}) *gomock.Call {
+// ListCachedFeature indicates an expected call of ListCachedFeature.
+func (mr *MockCacheStoreMockRecorder) ListCachedFeature(ctx, fullNames interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CacheListFeature", reflect.TypeOf((*MockCacheStore)(nil).CacheListFeature), 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/mysql/db.go b/internal/database/metadata/mysql/db.go
index c92043e3..7bffa8d6 100644
--- a/internal/database/metadata/mysql/db.go
+++ b/internal/database/metadata/mysql/db.go
@@ -76,8 +76,8 @@ func (db *DB) GetFeature(ctx context.Context, id int) (*types.Feature, error) {
 	return sqlutil.GetFeature(ctx, db, id)
 }
 
-func (db *DB) GetFeatureByName(ctx context.Context, name string) (*types.Feature, error) {
-	return sqlutil.GetFeatureByName(ctx, db, name)
+func (db *DB) GetFeatureByName(ctx context.Context, groupName string, featureName string) (*types.Feature, error) {
+	return sqlutil.GetFeatureByName(ctx, db, groupName, featureName)
 }
 
 func (db *DB) ListFeature(ctx context.Context, opt metadata.ListFeatureOpt) (types.FeatureList, error) {
diff --git a/internal/database/metadata/mysql/feature.go b/internal/database/metadata/mysql/feature.go
index 61a38525..bcc965ac 100644
--- a/internal/database/metadata/mysql/feature.go
+++ b/internal/database/metadata/mysql/feature.go
@@ -13,8 +13,8 @@ func createFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metada
 	if err := opt.ValueType.Validate(); err != nil {
 		return 0, errdefs.WithStack(err)
 	}
-	query := "INSERT INTO feature(name, full_name, group_id, value_type, description) VALUES (?, ?, ?, ?, ?)"
-	res, err := sqlxCtx.ExecContext(ctx, sqlxCtx.Rebind(query), opt.FeatureName, opt.FullName, opt.GroupID, opt.ValueType, opt.Description)
+	query := "INSERT INTO feature(name, group_id, value_type, description) VALUES (?, ?, ?, ?)"
+	res, err := sqlxCtx.ExecContext(ctx, sqlxCtx.Rebind(query), opt.FeatureName, opt.GroupID, opt.ValueType, opt.Description)
 	if err != nil {
 		if er, ok := err.(*mysql.MySQLError); ok {
 			if er.Number == ER_DUP_ENTRY {
diff --git a/internal/database/metadata/mysql/schema.go b/internal/database/metadata/mysql/schema.go
index b50f2a92..e2d05b5f 100644
--- a/internal/database/metadata/mysql/schema.go
+++ b/internal/database/metadata/mysql/schema.go
@@ -5,14 +5,12 @@ var META_TABLE_SCHEMAS = map[string]string{
 		CREATE TABLE feature (
 			id				INT 			NOT NULL AUTO_INCREMENT PRIMARY KEY,
 			name          	VARCHAR(32) 	NOT	NULL,
-			full_name   	VARCHAR(65)		NOT NULL COMMENT "<group_name>:<feature_name>",
 			group_id      	INT         	NOT	NULL,
 			value_type    	INT         	NOT	NULL COMMENT "data type of feature value",
 			description   	VARCHAR(128)	DEFAULT '',
 			create_time   	TIMESTAMP    	NOT	NULL DEFAULT CURRENT_TIMESTAMP,
 			modify_time   	TIMESTAMP    	NOT	NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-			UNIQUE (group_id, name),
-			UNIQUE (full_name)
+			UNIQUE (group_id, name)
 		);
 		`,
 	"feature_group": `
diff --git a/internal/database/metadata/mysql/tx.go b/internal/database/metadata/mysql/tx.go
index 03a3c1de..81b0a0bb 100644
--- a/internal/database/metadata/mysql/tx.go
+++ b/internal/database/metadata/mysql/tx.go
@@ -68,8 +68,8 @@ func (tx *Tx) ListFeature(ctx context.Context, opt metadata.ListFeatureOpt) (typ
 	return sqlutil.ListFeature(ctx, tx, opt)
 }
 
-func (tx *Tx) GetFeatureByName(ctx context.Context, name string) (*types.Feature, error) {
-	return sqlutil.GetFeatureByName(ctx, tx, name)
+func (tx *Tx) GetFeatureByName(ctx context.Context, groupName string, featureName string) (*types.Feature, error) {
+	return sqlutil.GetFeatureByName(ctx, tx, groupName, featureName)
 }
 
 func (tx *Tx) CreateRevision(ctx context.Context, opt metadata.CreateRevisionOpt) (int, string, error) {
diff --git a/internal/database/metadata/postgres/db.go b/internal/database/metadata/postgres/db.go
index 307ff2e1..9c5179b0 100644
--- a/internal/database/metadata/postgres/db.go
+++ b/internal/database/metadata/postgres/db.go
@@ -89,8 +89,8 @@ func (db *DB) ListFeature(ctx context.Context, opt metadata.ListFeatureOpt) (typ
 	return sqlutil.ListFeature(ctx, db, opt)
 }
 
-func (db *DB) GetFeatureByName(ctx context.Context, name string) (*types.Feature, error) {
-	return sqlutil.GetFeatureByName(ctx, db, name)
+func (db *DB) GetFeatureByName(ctx context.Context, groupName string, featureName string) (*types.Feature, error) {
+	return sqlutil.GetFeatureByName(ctx, db, groupName, featureName)
 }
 
 func (db *DB) CreateRevision(ctx context.Context, opt metadata.CreateRevisionOpt) (int, string, error) {
diff --git a/internal/database/metadata/postgres/feature.go b/internal/database/metadata/postgres/feature.go
index 425bbe72..6114d447 100644
--- a/internal/database/metadata/postgres/feature.go
+++ b/internal/database/metadata/postgres/feature.go
@@ -14,8 +14,8 @@ func createFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metada
 		return 0, err
 	}
 	var featureID int
-	query := "INSERT INTO feature(name, full_name, group_id, value_type, description) VALUES ($1, $2, $3, $4, $5) RETURNING id"
-	err := sqlxCtx.GetContext(ctx, &featureID, query, opt.FeatureName, opt.FullName, opt.GroupID, opt.ValueType, opt.Description)
+	query := "INSERT INTO feature(name, group_id, value_type, description) VALUES ($1, $2, $3, $4) RETURNING id"
+	err := sqlxCtx.GetContext(ctx, &featureID, query, opt.FeatureName, opt.GroupID, opt.ValueType, opt.Description)
 	if err != nil {
 		if e2, ok := err.(*pq.Error); ok {
 			if e2.Code == pgerrcode.UniqueViolation {
diff --git a/internal/database/metadata/postgres/schema.go b/internal/database/metadata/postgres/schema.go
index 3ff4a79a..9e494c02 100644
--- a/internal/database/metadata/postgres/schema.go
+++ b/internal/database/metadata/postgres/schema.go
@@ -23,17 +23,14 @@ var META_TABLE_SCHEMAS = map[string]string{
 		CREATE TABLE feature (
 			id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
 			name        	VARCHAR(32)  	NOT NULL,
-			full_name   	VARCHAR(65)		NOT NULL,
 			group_id    	INT          	NOT NULL,
 			value_type  	INT  		   	NOT NULL,
 			description 	VARCHAR(128) 	DEFAULT '',
 			create_time   	TIMESTAMP    	NOT NULL DEFAULT CURRENT_TIMESTAMP,
 			modify_time   	TIMESTAMP    	NOT NULL DEFAULT CURRENT_TIMESTAMP,
-			UNIQUE (group_id, name),
-			UNIQUE (full_name)
+			UNIQUE (group_id, name)
 		);
-		COMMENT ON COLUMN feature.value_type    IS 'data type of feature value';
-		COMMENT ON COLUMN feature.full_name		IS '<group_name>:<feature_name>';
+		COMMENT ON COLUMN feature.value_type IS 'data type of feature value';
 		`,
 	"feature_group": `
 		CREATE TABLE feature_group (
diff --git a/internal/database/metadata/postgres/tx.go b/internal/database/metadata/postgres/tx.go
index 6af374cc..fb911e38 100644
--- a/internal/database/metadata/postgres/tx.go
+++ b/internal/database/metadata/postgres/tx.go
@@ -68,8 +68,8 @@ func (tx *Tx) ListFeature(ctx context.Context, opt metadata.ListFeatureOpt) (typ
 	return sqlutil.ListFeature(ctx, tx, opt)
 }
 
-func (tx *Tx) GetFeatureByName(ctx context.Context, name string) (*types.Feature, error) {
-	return sqlutil.GetFeatureByName(ctx, tx, name)
+func (tx *Tx) GetFeatureByName(ctx context.Context, groupName string, featureName string) (*types.Feature, error) {
+	return sqlutil.GetFeatureByName(ctx, tx, groupName, featureName)
 }
 
 func (tx *Tx) CreateRevision(ctx context.Context, opt metadata.CreateRevisionOpt) (int, string, error) {
diff --git a/internal/database/metadata/sqlite/db.go b/internal/database/metadata/sqlite/db.go
index 24fcc9fa..ebb6a680 100644
--- a/internal/database/metadata/sqlite/db.go
+++ b/internal/database/metadata/sqlite/db.go
@@ -66,8 +66,8 @@ func (db *DB) GetFeature(ctx context.Context, id int) (*types.Feature, error) {
 	return sqlutil.GetFeature(ctx, db, id)
 }
 
-func (db *DB) GetFeatureByName(ctx context.Context, name string) (*types.Feature, error) {
-	return sqlutil.GetFeatureByName(ctx, db, name)
+func (db *DB) GetFeatureByName(ctx context.Context, groupName string, featureName string) (*types.Feature, error) {
+	return sqlutil.GetFeatureByName(ctx, db, groupName, featureName)
 }
 
 func (db *DB) ListFeature(ctx context.Context, opt metadata.ListFeatureOpt) (types.FeatureList, error) {
diff --git a/internal/database/metadata/sqlite/feature.go b/internal/database/metadata/sqlite/feature.go
index 6e29dc6f..04b5d7f8 100644
--- a/internal/database/metadata/sqlite/feature.go
+++ b/internal/database/metadata/sqlite/feature.go
@@ -14,8 +14,8 @@ func createFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metada
 	if err := opt.ValueType.Validate(); err != nil {
 		return 0, err
 	}
-	query := "INSERT INTO feature(name, full_name, group_id, value_type, description) VALUES (?, ?, ?, ?, ?)"
-	res, err := sqlxCtx.ExecContext(ctx, sqlxCtx.Rebind(query), opt.FeatureName, opt.FullName, opt.GroupID, opt.ValueType, opt.Description)
+	query := "INSERT INTO feature(name, group_id, value_type, description) VALUES (?, ?, ?, ?)"
+	res, err := sqlxCtx.ExecContext(ctx, sqlxCtx.Rebind(query), opt.FeatureName, opt.GroupID, opt.ValueType, opt.Description)
 	if err != nil {
 		if sqliteErr, ok := err.(*sqlite.Error); ok {
 			if sqliteErr.Code() == sqlite3.SQLITE_CONSTRAINT_UNIQUE {
diff --git a/internal/database/metadata/sqlite/schema.go b/internal/database/metadata/sqlite/schema.go
index 29f969bb..4b12bcfc 100644
--- a/internal/database/metadata/sqlite/schema.go
+++ b/internal/database/metadata/sqlite/schema.go
@@ -13,14 +13,12 @@ var META_TABLE_SCHEMAS = map[string]string{
 		CREATE TABLE feature (
 			id				INTEGER 		NOT NULL PRIMARY KEY AUTOINCREMENT,
 			name          	VARCHAR(32) 	NOT	NULL,
-			full_name   	VARCHAR(65)		NOT NULL,
 			group_id      	INT         	NOT	NULL,
 			value_type    	INT          	NOT	NULL,
 			description   	VARCHAR(128)	DEFAULT '',
 			create_time   	TIMESTAMP    	NOT	NULL DEFAULT CURRENT_TIMESTAMP,
 			modify_time   	TIMESTAMP    	NOT	NULL DEFAULT CURRENT_TIMESTAMP,
 			UNIQUE (group_id, name),
-			UNIQUE (full_name),
 			FOREIGN KEY (group_id) REFERENCES feature_group(id)
 		);
 		`,
diff --git a/internal/database/metadata/sqlite/tx.go b/internal/database/metadata/sqlite/tx.go
index 01cb0750..5d21cb35 100644
--- a/internal/database/metadata/sqlite/tx.go
+++ b/internal/database/metadata/sqlite/tx.go
@@ -68,8 +68,9 @@ func (tx *Tx) ListFeature(ctx context.Context, opt metadata.ListFeatureOpt) (typ
 	return sqlutil.ListFeature(ctx, tx, opt)
 }
 
-func (tx *Tx) GetFeatureByName(ctx context.Context, name string) (*types.Feature, error) {
-	return sqlutil.GetFeatureByName(ctx, tx, name)
+func (tx *Tx) GetFeatureByName(ctx context.Context, groupName string, featureName string) (*types.Feature, error) {
+	return sqlutil.GetFeatureByName(ctx, tx, groupName, featureName)
+
 }
 
 func (tx *Tx) CreateRevision(ctx context.Context, opt metadata.CreateRevisionOpt) (int, string, error) {
diff --git a/internal/database/metadata/sqlutil/feature.go b/internal/database/metadata/sqlutil/feature.go
index c40edae3..da9e9a1a 100644
--- a/internal/database/metadata/sqlutil/feature.go
+++ b/internal/database/metadata/sqlutil/feature.go
@@ -55,25 +55,25 @@ 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, groupName string, featureName 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 {
-		if err == sql.ErrNoRows {
-			return nil, errdefs.NotFound(errdefs.Errorf("feature %s not found", fullName))
-		}
+	if group, err = GetGroupByName(ctx, sqlxCtx, groupName); err != nil {
 		return nil, errdefs.WithStack(err)
 	}
+	feature.Group = group
 
-	if group, err = GetGroup(ctx, sqlxCtx, feature.GroupID); err != nil {
+	query := `SELECT * FROM feature WHERE name = ? AND group_id = ?`
+	if err := sqlxCtx.GetContext(ctx, &feature, sqlxCtx.Rebind(query), featureName, group.ID); err != nil {
+		if err == sql.ErrNoRows {
+			return nil, errdefs.NotFound(errdefs.Errorf("feature %s not found in group %s", featureName, groupName))
+		}
 		return nil, errdefs.WithStack(err)
 	}
-	feature.Group = group
 
 	return &feature, nil
 }
@@ -135,11 +135,5 @@ func buildListFeatureCond(opt metadata.ListFeatureOpt) ([]string, []interface{},
 		in["id"] = *opt.FeatureIDs
 	}
 
-	if opt.FeatureFullNames != nil {
-		if len(*opt.FeatureFullNames) == 0 {
-			return []string{"false"}, nil, nil
-		}
-		in["full_name"] = *opt.FeatureFullNames
-	}
 	return dbutil.BuildConditions(and, in)
 }
diff --git a/internal/database/metadata/store.go b/internal/database/metadata/store.go
index 8fdec7ba..b22badb8 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, groupName string, featureName string) (*types.Feature, error)
 	ListFeature(ctx context.Context, opt ListFeatureOpt) (types.FeatureList, error)
 
 	// feature group
@@ -52,7 +52,7 @@ type DBStore interface {
 
 // The interface defines methods that a memory backend store must implement.
 type CacheStore interface {
-	CacheListFeature(ctx context.Context, opt ListFeatureOpt) types.FeatureList
+	ListCachedFeature(ctx context.Context, fullNames *[]string) types.FeatureList
 
 	// Pull data from database and update cache.
 	Refresh() error
diff --git a/internal/database/metadata/types.go b/internal/database/metadata/types.go
index b5a88195..10f52751 100644
--- a/internal/database/metadata/types.go
+++ b/internal/database/metadata/types.go
@@ -9,7 +9,6 @@ type CreateEntityOpt struct {
 
 type CreateFeatureOpt struct {
 	FeatureName string
-	FullName    string
 	GroupID     int
 	Description string
 	ValueType   types.ValueType
@@ -56,8 +55,7 @@ type UpdateRevisionOpt struct {
 }
 
 type ListFeatureOpt struct {
-	EntityID         *int
-	GroupID          *int
-	FeatureIDs       *[]int
-	FeatureFullNames *[]string
+	EntityID   *int
+	GroupID    *int
+	FeatureIDs *[]int
 }
diff --git a/internal/database/offline/sqlutil/export.go b/internal/database/offline/sqlutil/export.go
index 49c4db93..68452e18 100644
--- a/internal/database/offline/sqlutil/export.go
+++ b/internal/database/offline/sqlutil/export.go
@@ -76,14 +76,14 @@ func DoExport(ctx context.Context, dbOpt dbutil.DBOpt, opt DoExportOpt) (*types.
 		features := opt.Features[groupID]
 		if features[0].Group.Category == types.CategoryBatch {
 			for _, f := range features {
-				fields = append(fields, fmt.Sprintf("%s.%s AS %s", qt(opt.SnapshotTables[groupID]), qt(f.Name), qt(f.FullName)))
+				fields = append(fields, fmt.Sprintf("%s.%s AS %s", qt(opt.SnapshotTables[groupID]), qt(f.Name), qt(f.FullName())))
 				featureList = append(featureList, f)
 			}
 		} else {
 			for _, f := range features {
 				cdc := fmt.Sprintf("%s.%s", opt.CdcTables[groupID]+"_0", qt(f.Name))
 				snapshot := fmt.Sprintf("%s.%s", qt(opt.SnapshotTables[groupID]), qt(f.Name))
-				fields = append(fields, fmt.Sprintf("(CASE WHEN %s IS NULL THEN %s ELSE %s END) AS %s", cdc, snapshot, cdc, qt(f.FullName)))
+				fields = append(fields, fmt.Sprintf("(CASE WHEN %s IS NULL THEN %s ELSE %s END) AS %s", cdc, snapshot, cdc, qt(f.FullName())))
 				featureList = append(featureList, f)
 			}
 		}
diff --git a/internal/database/offline/sqlutil/join.go b/internal/database/offline/sqlutil/join.go
index c3d7be45..8d3fc981 100644
--- a/internal/database/offline/sqlutil/join.go
+++ b/internal/database/offline/sqlutil/join.go
@@ -224,7 +224,7 @@ func ReadJoinedTable(ctx context.Context, dbOpt dbutil.DBOpt, opt ReadJoinedTabl
 		for _, f := range opt.FeatureMap[name] {
 			fields = append(fields, fmt.Sprintf("%s.%s", qt(name), qt(f.Name)))
 			header = append(header, dbutil.Column{
-				Name:      f.FullName,
+				Name:      f.FullName(),
 				ValueType: f.ValueType,
 			})
 		}
diff --git a/internal/database/online/cassandra/get.go b/internal/database/online/cassandra/get.go
index 0f1e5186..3ff2af86 100644
--- a/internal/database/online/cassandra/get.go
+++ b/internal/database/online/cassandra/get.go
@@ -39,7 +39,7 @@ func (db *DB) Get(ctx context.Context, opt online.GetOpt) (dbutil.RowMap, error)
 	rs := make(map[string]interface{}, len(scan))
 	for _, feature := range opt.Features {
 		deserializedValue, _ := dbutil.DeserializeByValueType(scan[feature.Name], feature.ValueType, types.BackendCassandra)
-		rs[feature.FullName] = deserializedValue
+		rs[feature.FullName()] = deserializedValue
 	}
 	return rs, nil
 }
@@ -89,7 +89,7 @@ func deserializeIntoRowMap(values map[string]interface{}, entityName string, fea
 	rs := make(dbutil.RowMap)
 	for _, feature := range features {
 		deserializedValue, _ := dbutil.DeserializeByValueType(values[feature.Name], feature.ValueType, types.BackendCassandra)
-		rs[feature.FullName] = deserializedValue
+		rs[feature.FullName()] = deserializedValue
 	}
 	return entityKey, rs
 }
diff --git a/internal/database/online/dynamodb/get.go b/internal/database/online/dynamodb/get.go
index e52f2b38..15ebc97e 100644
--- a/internal/database/online/dynamodb/get.go
+++ b/internal/database/online/dynamodb/get.go
@@ -129,7 +129,7 @@ func deserializeFeatureValues(features oomTypes.FeatureList, item map[string]typ
 		if err != nil {
 			return nil, err
 		}
-		rowMap[feature.FullName] = deserializedValue
+		rowMap[feature.FullName()] = deserializedValue
 	}
 	return rowMap, nil
 }
diff --git a/internal/database/online/redis/get.go b/internal/database/online/redis/get.go
index 04b9cbae..c6fc357f 100644
--- a/internal/database/online/redis/get.go
+++ b/internal/database/online/redis/get.go
@@ -48,7 +48,7 @@ func (db *DB) Get(ctx context.Context, opt online.GetOpt) (dbutil.RowMap, error)
 		if err != nil {
 			return nil, err
 		}
-		rowMap[opt.Features[i].FullName] = deserializedValue
+		rowMap[opt.Features[i].FullName()] = deserializedValue
 	}
 	return rowMap, nil
 }
diff --git a/internal/database/online/sqlutil/get.go b/internal/database/online/sqlutil/get.go
index 4a3978ba..da9357fb 100644
--- a/internal/database/online/sqlutil/get.go
+++ b/internal/database/online/sqlutil/get.go
@@ -94,7 +94,7 @@ func deserializeIntoRowMap(values []interface{}, features types.FeatureList, bac
 		if err != nil {
 			return nil, err
 		}
-		rs[features[i].FullName] = deserializedValue
+		rs[features[i].FullName()] = deserializedValue
 	}
 	return rs, nil
 }
diff --git a/internal/database/online/tikv/get.go b/internal/database/online/tikv/get.go
index ca2b2aa7..81dab74c 100644
--- a/internal/database/online/tikv/get.go
+++ b/internal/database/online/tikv/get.go
@@ -109,7 +109,7 @@ func (db *DB) MultiGet(ctx context.Context, opt online.MultiGetOpt) (map[string]
 		if _, ok := res[entityKey]; !ok {
 			res[entityKey] = make(dbutil.RowMap)
 		}
-		res[entityKey][inputs[i].feature.FullName] = deserializedValue
+		res[entityKey][inputs[i].feature.FullName()] = deserializedValue
 	}
 	return res, nil
 }
diff --git a/pkg/oomstore/apply.go b/pkg/oomstore/apply.go
index 0aa1392d..6024949d 100644
--- a/pkg/oomstore/apply.go
+++ b/pkg/oomstore/apply.go
@@ -2,7 +2,6 @@ package oomstore
 
 import (
 	"context"
-	"fmt"
 	"io"
 
 	"github.com/mitchellh/mapstructure"
@@ -150,8 +149,7 @@ func (s *OomStore) applyFeature(ctx context.Context, tx metadata.DBStore, newFea
 		return nil, err
 	}
 
-	featureFullName := fmt.Sprintf("%s.%s", newFeature.GroupName, newFeature.Name)
-	feature, err := tx.GetFeatureByName(ctx, featureFullName)
+	feature, err := tx.GetFeatureByName(ctx, newFeature.GroupName, newFeature.Name)
 	if err != nil {
 		if !errdefs.IsNotFound(err) {
 			return nil, err
@@ -162,7 +160,6 @@ func (s *OomStore) applyFeature(ctx context.Context, tx metadata.DBStore, newFea
 		}
 		id, err := tx.CreateFeature(ctx, metadata.CreateFeatureOpt{
 			FeatureName: newFeature.Name,
-			FullName:    featureFullName,
 			GroupID:     group.ID,
 			Description: newFeature.Description,
 			ValueType:   valueType,
diff --git a/pkg/oomstore/export.go b/pkg/oomstore/export.go
index 79e314a7..edb73f10 100644
--- a/pkg/oomstore/export.go
+++ b/pkg/oomstore/export.go
@@ -26,7 +26,7 @@ Usage Example:
 	return exportResult.CheckStreamError()
 */
 func (s *OomStore) ChannelExport(ctx context.Context, opt types.ChannelExportOpt) (*types.ExportResult, error) {
-	if err := validateFeatureFullNames(opt.FeatureFullNames); err != nil {
+	if err := validateFullFeatureNames(opt.FeatureFullNames...); err != nil {
 		return nil, err
 	}
 	features, err := s.ListFeature(ctx, types.ListFeatureOpt{
diff --git a/pkg/oomstore/feature.go b/pkg/oomstore/feature.go
index d936a163..7181d330 100644
--- a/pkg/oomstore/feature.go
+++ b/pkg/oomstore/feature.go
@@ -2,7 +2,6 @@ package oomstore
 
 import (
 	"context"
-	"fmt"
 	"strings"
 
 	"github.com/oom-ai/oomstore/pkg/errdefs"
@@ -10,10 +9,7 @@ import (
 	"github.com/oom-ai/oomstore/internal/database/metadata"
 	"github.com/oom-ai/oomstore/internal/database/online"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
-)
-
-const (
-	FeatureFullNameSeparator = "."
+	"github.com/oom-ai/oomstore/pkg/oomstore/util"
 )
 
 // Get metadata of a feature by ID.
@@ -22,20 +18,22 @@ func (s *OomStore) GetFeature(ctx context.Context, id int) (*types.Feature, erro
 }
 
 // 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)
+func (s *OomStore) GetFeatureByFullName(ctx context.Context, fullName string) (*types.Feature, error) {
+	groupName, featureName, err := util.SplitFullFeatureName(fullName)
+	if err != nil {
+		return nil, err
+	}
+	return s.GetFeatureByName(ctx, groupName, featureName)
+}
+
+// Get metadata of a feature by group name and feature name.
+func (s *OomStore) GetFeatureByName(ctx context.Context, groupName string, featureName string) (*types.Feature, error) {
+	return s.metadata.GetFeatureByName(ctx, groupName, featureName)
 }
 
 // List metadata of features meeting particular criteria.
 func (s *OomStore) ListFeature(ctx context.Context, opt types.ListFeatureOpt) (types.FeatureList, error) {
-	if opt.FeatureFullNames != nil {
-		if err := validateFeatureFullNames(*opt.FeatureFullNames); err != nil {
-			return nil, err
-		}
-	}
-	metadataOpt := metadata.ListFeatureOpt{
-		FeatureFullNames: opt.FeatureFullNames,
-	}
+	metadataOpt := metadata.ListFeatureOpt{}
 	if opt.EntityName != nil {
 		entity, err := s.metadata.GetEntityByName(ctx, *opt.EntityName)
 		if err != nil {
@@ -50,15 +48,22 @@ func (s *OomStore) ListFeature(ctx context.Context, opt types.ListFeatureOpt) (t
 		}
 		metadataOpt.GroupID = &group.ID
 	}
-	return s.metadata.ListFeature(ctx, metadataOpt)
+	features, err := s.metadata.ListFeature(ctx, metadataOpt)
+	if err != nil {
+		return nil, err
+	}
+	if opt.FeatureFullNames != nil {
+		features = features.FilterFullnames(*opt.FeatureFullNames)
+	}
+	return features, nil
 }
 
 // Update metadata of a feature.
 func (s *OomStore) UpdateFeature(ctx context.Context, opt types.UpdateFeatureOpt) error {
-	if err := validateFeatureFullNames([]string{opt.FeatureFullName}); err != nil {
+	if err := validateFullFeatureNames(opt.FeatureFullName); err != nil {
 		return err
 	}
-	feature, err := s.metadata.GetFeatureByName(ctx, opt.FeatureFullName)
+	feature, err := s.GetFeatureByFullName(ctx, opt.FeatureFullName)
 	if err != nil {
 		return err
 	}
@@ -77,7 +82,6 @@ func (s *OomStore) CreateFeature(ctx context.Context, opt types.CreateFeatureOpt
 
 	id, err := s.metadata.CreateFeature(ctx, metadata.CreateFeatureOpt{
 		FeatureName: opt.FeatureName,
-		FullName:    fmt.Sprintf("%s.%s", group.Name, opt.FeatureName),
 		GroupID:     group.ID,
 		ValueType:   opt.ValueType,
 		Description: opt.Description,
@@ -103,11 +107,11 @@ func (s *OomStore) CreateFeature(ctx context.Context, opt types.CreateFeatureOpt
 	return id, nil
 }
 
-func validateFeatureFullNames(names []string) error {
-	for _, name := range names {
-		nameSlice := strings.Split(name, FeatureFullNameSeparator)
+func validateFullFeatureNames(fullnames ...string) error {
+	for _, fullname := range fullnames {
+		nameSlice := strings.Split(fullname, util.SepFullFeatureName)
 		if len(nameSlice) != 2 {
-			return errdefs.Errorf("invalid feature full name %s", name)
+			return errdefs.Errorf("invalid full feature name: '%s'", fullname)
 		}
 	}
 	return nil
diff --git a/pkg/oomstore/join.go b/pkg/oomstore/join.go
index 8ec5d4e5..9a3804b0 100644
--- a/pkg/oomstore/join.go
+++ b/pkg/oomstore/join.go
@@ -11,7 +11,6 @@ import (
 
 	"github.com/spf13/cast"
 
-	"github.com/oom-ai/oomstore/internal/database/metadata"
 	"github.com/oom-ai/oomstore/internal/database/offline"
 	"github.com/oom-ai/oomstore/pkg/errdefs"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
@@ -20,7 +19,7 @@ import (
 // ChannelJoin gets point-in-time correct feature values for each entity row.
 // Currently, this API only supports batch features.
 func (s *OomStore) ChannelJoin(ctx context.Context, opt types.ChannelJoinOpt) (*types.JoinResult, error) {
-	if err := validateFeatureFullNames(opt.FeatureFullNames); err != nil {
+	if err := validateFullFeatureNames(opt.FeatureFullNames...); err != nil {
 		return nil, err
 	}
 	data := make(chan []interface{})
@@ -29,7 +28,7 @@ func (s *OomStore) ChannelJoin(ctx context.Context, opt types.ChannelJoinOpt) (*
 	emptyResult := &types.JoinResult{
 		Data: data,
 	}
-	features, err := s.metadata.ListFeature(ctx, metadata.ListFeatureOpt{
+	features, err := s.ListFeature(ctx, types.ListFeatureOpt{
 		FeatureFullNames: &opt.FeatureFullNames,
 	})
 	if err != nil {
@@ -75,7 +74,7 @@ func (s *OomStore) ChannelJoin(ctx context.Context, opt types.ChannelJoinOpt) (*
 // Input File should contain header, the first two columns of Input File should be
 // entity_key, unix_milli, then followed by other real-time feature values.
 func (s *OomStore) Join(ctx context.Context, opt types.JoinOpt) error {
-	if err := validateFeatureFullNames(opt.FeatureFullNames); err != nil {
+	if err := validateFullFeatureNames(opt.FeatureFullNames...); err != nil {
 		return err
 	}
 	entityRows, header, err := GetEntityRowsFromInputFile(opt.InputFilePath)
diff --git a/pkg/oomstore/online_query.go b/pkg/oomstore/online_query.go
index 9929703a..28b1e994 100644
--- a/pkg/oomstore/online_query.go
+++ b/pkg/oomstore/online_query.go
@@ -6,14 +6,13 @@ import (
 	"github.com/oom-ai/oomstore/pkg/errdefs"
 
 	"github.com/oom-ai/oomstore/internal/database/dbutil"
-	"github.com/oom-ai/oomstore/internal/database/metadata"
 	"github.com/oom-ai/oomstore/internal/database/online"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
 )
 
 // OnlineGet gets online features of a particular entity instance.
 func (s *OomStore) OnlineGet(ctx context.Context, opt types.OnlineGetOpt) (*types.FeatureValues, error) {
-	if err := validateFeatureFullNames(opt.FeatureFullNames); err != nil {
+	if err := validateFullFeatureNames(opt.FeatureFullNames...); err != nil {
 		return nil, err
 	}
 	rs := types.FeatureValues{
@@ -21,9 +20,7 @@ func (s *OomStore) OnlineGet(ctx context.Context, opt types.OnlineGetOpt) (*type
 		FeatureFullNames: opt.FeatureFullNames,
 		FeatureValueMap:  make(map[string]interface{}),
 	}
-	features := s.metadata.CacheListFeature(ctx, metadata.ListFeatureOpt{
-		FeatureFullNames: &opt.FeatureFullNames,
-	})
+	features := s.metadata.ListCachedFeature(ctx, &opt.FeatureFullNames)
 	if len(features) == 0 {
 		return &rs, nil
 	}
@@ -61,13 +58,11 @@ func (s *OomStore) OnlineGet(ctx context.Context, opt types.OnlineGetOpt) (*type
 
 // OnlineMultiGet gets online features of multiple entity instances.
 func (s *OomStore) OnlineMultiGet(ctx context.Context, opt types.OnlineMultiGetOpt) (map[string]*types.FeatureValues, error) {
-	if err := validateFeatureFullNames(opt.FeatureFullNames); err != nil {
+	if err := validateFullFeatureNames(opt.FeatureFullNames...); err != nil {
 		return nil, err
 	}
 	result := make(map[string]*types.FeatureValues)
-	features := s.metadata.CacheListFeature(ctx, metadata.ListFeatureOpt{
-		FeatureFullNames: &opt.FeatureFullNames,
-	})
+	features := s.metadata.ListCachedFeature(ctx, &opt.FeatureFullNames)
 	if len(features) == 0 {
 		return result, nil
 	}
diff --git a/pkg/oomstore/types/feature.go b/pkg/oomstore/types/feature.go
index 4aff841c..599c6d29 100644
--- a/pkg/oomstore/types/feature.go
+++ b/pkg/oomstore/types/feature.go
@@ -2,12 +2,13 @@ package types
 
 import (
 	"time"
+
+	"github.com/oom-ai/oomstore/pkg/oomstore/util"
 )
 
 type Feature struct {
 	ID        int       `db:"id"`
 	Name      string    `db:"name"`
-	FullName  string    `db:"full_name"`
 	ValueType ValueType `db:"value_type"`
 
 	Description string    `db:"description"`
@@ -18,6 +19,13 @@ type Feature struct {
 	Group   *Group
 }
 
+func (f *Feature) FullName() string {
+	if f.Group == nil {
+		panic("expected group to be not nil")
+	}
+	return util.ComposeFullFeatureName(f.Group.Name, f.Name)
+}
+
 func (f *Feature) Copy() *Feature {
 	if f == nil {
 		return nil
@@ -65,7 +73,7 @@ func (l *FeatureList) Names() (names []string) {
 
 func (l *FeatureList) FullNames() (fullNames []string) {
 	for _, f := range *l {
-		fullNames = append(fullNames, f.FullName)
+		fullNames = append(fullNames, f.FullName())
 	}
 	return
 }
@@ -86,6 +94,17 @@ func (l FeatureList) Filter(filter func(*Feature) bool) (rs FeatureList) {
 	return
 }
 
+func (l FeatureList) FilterFullnames(fullnames []string) (rs FeatureList) {
+	return l.Filter(func(f *Feature) bool {
+		for _, fullname := range fullnames {
+			if f.FullName() == fullname {
+				return true
+			}
+		}
+		return false
+	})
+}
+
 func (l FeatureList) Find(find func(*Feature) bool) *Feature {
 	for _, f := range l {
 		if find(f) {
diff --git a/pkg/oomstore/util/feature.go b/pkg/oomstore/util/feature.go
new file mode 100644
index 00000000..1d87bdd4
--- /dev/null
+++ b/pkg/oomstore/util/feature.go
@@ -0,0 +1,31 @@
+package util
+
+import (
+	"strings"
+
+	"github.com/oom-ai/oomstore/pkg/errdefs"
+)
+
+const SepFullFeatureName = "."
+
+func ComposeFullFeatureName(groupName string, featureName string) string {
+	return groupName + SepFullFeatureName + featureName
+}
+
+func SplitFullFeatureName(fullname string) (string, string, error) {
+	parts := strings.SplitN(fullname, SepFullFeatureName, 2)
+	if len(parts) != 2 {
+		return "", "", errdefs.Errorf("invalid full feature name: '%s'", fullname)
+	}
+	return parts[0], parts[1], nil
+}
+
+func ValidateFullFeatureNames(fullnames ...string) error {
+	for _, fullname := range fullnames {
+		nameSlice := strings.Split(fullname, SepFullFeatureName)
+		if len(nameSlice) != 2 {
+			return errdefs.Errorf("invalid full feature name: '%s'", fullname)
+		}
+	}
+	return nil
+}


Test patch:
diff --git a/internal/database/metadata/informer/informer_test.go b/internal/database/metadata/informer/informer_test.go
index 1dc0b638..5bf54421 100644
--- a/internal/database/metadata/informer/informer_test.go
+++ b/internal/database/metadata/informer/informer_test.go
@@ -5,9 +5,9 @@ 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"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -49,10 +49,8 @@ func TestInformer(t *testing.T) {
 	ctx, informer := prepareInformer(t)
 	defer informer.Close()
 
-	entityID := 1
-	features := informer.CacheListFeature(ctx, metadata.ListFeatureOpt{
-		EntityID: &entityID,
-	})
+	name := util.ComposeFullFeatureName("group", "price")
+	features := informer.ListCachedFeature(ctx, &[]string{name})
 	require.Equal(t, 1, len(features))
 	feature := features[0]
 
@@ -70,17 +68,12 @@ func TestInformerDeepCopy(t *testing.T) {
 	ctx, informer := prepareInformer(t)
 	defer informer.Close()
 
-	entityID := 1
-	features := informer.CacheListFeature(ctx, metadata.ListFeatureOpt{
-		EntityID: &entityID,
-	})
+	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.CacheListFeature(ctx, metadata.ListFeatureOpt{
-		EntityID: &entityID,
-	})
+	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/mysql/db_test.go b/internal/database/metadata/mysql/db_test.go
index fc0a41c2..6d19da7b 100644
--- a/internal/database/metadata/mysql/db_test.go
+++ b/internal/database/metadata/mysql/db_test.go
@@ -134,7 +134,7 @@ func TestListFeature(t *testing.T) {
 }
 
 func TestCacheListFeature(t *testing.T) {
-	test_impl.TestCacheListFeature(t, prepareStore, runtime_mysql.DestroyStore(DATABASE))
+	test_impl.TestListCachedFeature(t, prepareStore, runtime_mysql.DestroyStore(DATABASE))
 }
 
 func TestUpdateFeature(t *testing.T) {
diff --git a/internal/database/metadata/postgres/db_test.go b/internal/database/metadata/postgres/db_test.go
index 3996d9f6..a8225582 100644
--- a/internal/database/metadata/postgres/db_test.go
+++ b/internal/database/metadata/postgres/db_test.go
@@ -115,7 +115,7 @@ func TestListFeature(t *testing.T) {
 }
 
 func TestCacheListFeature(t *testing.T) {
-	test_impl.TestCacheListFeature(t, prepareStore, runtime_pg.DestroyStore(DATABASE))
+	test_impl.TestListCachedFeature(t, prepareStore, runtime_pg.DestroyStore(DATABASE))
 }
 
 func TestUpdateFeature(t *testing.T) {
diff --git a/internal/database/metadata/sqlite/db_test.go b/internal/database/metadata/sqlite/db_test.go
index 2de2b1f1..18b95765 100644
--- a/internal/database/metadata/sqlite/db_test.go
+++ b/internal/database/metadata/sqlite/db_test.go
@@ -138,7 +138,7 @@ func TestListFeature(t *testing.T) {
 }
 
 func TestCacheListFeature(t *testing.T) {
-	test_impl.TestCacheListFeature(t, prepareStore, destroyStore)
+	test_impl.TestListCachedFeature(t, prepareStore, destroyStore)
 }
 
 func TestCreateRevision(t *testing.T) {
diff --git a/internal/database/metadata/test_impl/feature.go b/internal/database/metadata/test_impl/feature.go
index 4c45f396..a9bed41e 100644
--- a/internal/database/metadata/test_impl/feature.go
+++ b/internal/database/metadata/test_impl/feature.go
@@ -144,7 +144,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,
@@ -152,16 +151,15 @@ func TestGetFeatureByName(t *testing.T, prepareStore PrepareStoreFn, destroyStor
 	require.NoError(t, err)
 
 	// case 1: wrong feature name, return error
-	_, err = store.GetFeatureByName(ctx, "p")
-	assert.EqualError(t, err, "feature p not found")
+	_, err = store.GetFeatureByName(ctx, "g", "f")
+	assert.EqualError(t, err, "feature group g not found")
 
 	// case 2: correct feature name, return feature `phone`
-	feature, err := store.GetFeatureByName(ctx, "device_info.phone")
+	feature, err := store.GetFeatureByName(ctx, "device_info", "phone")
 	assert.NoError(t, err)
 	expected := &types.Feature{
 		ID:          1,
 		Name:        "phone",
-		FullName:    "device_info.phone",
 		ValueType:   types.String,
 		Description: "description",
 		GroupID:     1,
@@ -170,20 +168,19 @@ func TestGetFeatureByName(t *testing.T, prepareStore PrepareStoreFn, destroyStor
 	assert.Equal(t, expected, feature)
 }
 
-func TestCacheListFeature(t *testing.T, prepareStore PrepareStoreFn, destroyStore DestroyStoreFn) {
+func TestListCachedFeature(t *testing.T, prepareStore PrepareStoreFn, destroyStore DestroyStoreFn) {
 	t.Cleanup(destroyStore)
 
 	ctx, store := prepareStore(t)
 	defer store.Close()
-	entityID, groupID := prepareEntityAndGroup(t, ctx, store)
+	_, groupID := prepareEntityAndGroup(t, ctx, store)
 
 	// case 1: no feature to list
-	features := store.CacheListFeature(ctx, metadata.ListFeatureOpt{})
+	features := store.ListCachedFeature(ctx, nil)
 	assert.Equal(t, 0, features.Len())
 
-	featureID, err := store.CreateFeature(ctx, metadata.CreateFeatureOpt{
+	_, err := store.CreateFeature(ctx, metadata.CreateFeatureOpt{
 		FeatureName: "phone",
-		FullName:    "device_info.phone",
 		GroupID:     groupID,
 		Description: "description",
 		ValueType:   types.String,
@@ -192,47 +189,11 @@ func TestCacheListFeature(t *testing.T, prepareStore PrepareStoreFn, destroyStor
 	require.NoError(t, store.Refresh())
 
 	// case 2: no condition, list all features
-	features = store.CacheListFeature(ctx, metadata.ListFeatureOpt{})
+	features = store.ListCachedFeature(ctx, nil)
 	assert.Equal(t, 1, features.Len())
 
-	// case 3: list features by FeatureIDs
-	features = store.CacheListFeature(ctx, metadata.ListFeatureOpt{
-		FeatureIDs: &[]int{featureID},
-	})
-	assert.Equal(t, 1, features.Len())
-
-	// case 4: list features by EntityID and FeatureIDs
-	features = store.CacheListFeature(ctx, metadata.ListFeatureOpt{
-		EntityID:   intPtr(entityID + 1),
-		FeatureIDs: &[]int{featureID},
-	})
-	assert.Equal(t, 0, features.Len())
-
-	// case 5: list features by GroupID and FeatureIDs
-	features = store.CacheListFeature(ctx, metadata.ListFeatureOpt{
-		GroupID:    intPtr(groupID + 1),
-		FeatureIDs: &[]int{featureID},
-	})
-	assert.Equal(t, 0, features.Len())
-
-	// case 6: list features by EntityID and empty FeatureIDs, return no feature
-	features = store.CacheListFeature(ctx, metadata.ListFeatureOpt{
-		EntityID:   &entityID,
-		FeatureIDs: &[]int{},
-	})
-	assert.Equal(t, 0, len(features))
-
-	// case 7: list features by EntityID
-	features = store.CacheListFeature(ctx, metadata.ListFeatureOpt{
-		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)
+	features = store.ListCachedFeature(ctx, &[]string{"device_info.phone"})
 	assert.Equal(t, 1, len(features))
 }
 
@@ -250,7 +211,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,
@@ -299,13 +259,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/internal/database/metadata/tidb/db_test.go b/internal/database/metadata/tidb/db_test.go
index 03cd7eb1..5ccd618b 100644
--- a/internal/database/metadata/tidb/db_test.go
+++ b/internal/database/metadata/tidb/db_test.go
@@ -134,7 +134,7 @@ func TestListFeature(t *testing.T) {
 }
 
 func TestCacheListFeature(t *testing.T) {
-	test_impl.TestCacheListFeature(t, prepareStore, runtime_tidb.DestroyStore(DATABASE))
+	test_impl.TestListCachedFeature(t, prepareStore, runtime_tidb.DestroyStore(DATABASE))
 }
 
 func TestUpdateFeature(t *testing.T) {
diff --git a/internal/database/offline/test_impl/export.go b/internal/database/offline/test_impl/export.go
index e9e6c176..ec5f1e9a 100644
--- a/internal/database/offline/test_impl/export.go
+++ b/internal/database/offline/test_impl/export.go
@@ -126,13 +126,11 @@ func prepareFeaturesForExport() (batchFeatures types.FeatureList, streamFeatures
 	batchFeatures = []*types.Feature{
 		{
 			Name:      "model",
-			FullName:  "device.model",
 			ValueType: types.String,
 			Group:     batchGroup,
 		},
 		{
 			Name:      "price",
-			FullName:  "device.price",
 			ValueType: types.Int64,
 			Group:     batchGroup,
 		},
@@ -140,13 +138,11 @@ func prepareFeaturesForExport() (batchFeatures types.FeatureList, streamFeatures
 	streamFeatures = []*types.Feature{
 		{
 			Name:      "last_txn_amount",
-			FullName:  "account.last_txn_amount",
 			ValueType: types.Int64,
 			Group:     streamGroup,
 		},
 		{
 			Name:      "is_vip",
-			FullName:  "account.is_vip",
 			ValueType: types.Bool,
 			Group:     streamGroup,
 		},
diff --git a/internal/database/offline/test_impl/import.go b/internal/database/offline/test_impl/import.go
index 5dc039fa..5c171ecb 100644
--- a/internal/database/offline/test_impl/import.go
+++ b/internal/database/offline/test_impl/import.go
@@ -62,8 +62,8 @@ func TestImport(t *testing.T, prepareStore PrepareStoreFn, destroyStore DestroyS
 			EntityName:     entity.Name,
 			Features: map[int]types.FeatureList{
 				1: []*types.Feature{
-					{Name: "model", FullName: "device.model", ValueType: types.String, Group: group},
-					{Name: "price", FullName: "device.price", ValueType: types.Int64, Group: group},
+					{Name: "model", ValueType: types.String, Group: group},
+					{Name: "price", ValueType: types.Int64, Group: group},
 				}},
 		})
 		records := make([][]interface{}, 0)
diff --git a/internal/database/offline/test_impl/join.go b/internal/database/offline/test_impl/join.go
index 7b20a373..933a86d7 100644
--- a/internal/database/offline/test_impl/join.go
+++ b/internal/database/offline/test_impl/join.go
@@ -156,21 +156,18 @@ func prepareFeatures(oneGroup bool) (types.FeatureList, map[string]types.Feature
 	}
 	price := &types.Feature{
 		Name:      "price",
-		FullName:  "device_basic.price",
 		ValueType: types.Int64,
 		GroupID:   1,
 		Group:     groupBasic,
 	}
 	model := &types.Feature{
 		Name:      "model",
-		FullName:  "device_basic.model",
 		ValueType: types.String,
 		GroupID:   1,
 		Group:     groupBasic,
 	}
 	isActive := &types.Feature{
 		Name:      "is_active",
-		FullName:  "device_advanced.is_active",
 		ValueType: types.Bool,
 		GroupID:   2,
 		Group:     groupAdvanced,
@@ -179,18 +176,14 @@ func prepareFeatures(oneGroup bool) (types.FeatureList, map[string]types.Feature
 	if oneGroup {
 		features := types.FeatureList{model, price}
 		featureMap := map[string]types.FeatureList{
-			"device_basic": {
-				model, price,
-			},
+			groupBasic.Name: {model, price},
 		}
 		return features, featureMap
 	} else {
 		features := types.FeatureList{model, price, isActive}
 		featureMap := map[string]types.FeatureList{
-			"device_basic": {
-				model, price,
-			},
-			"device_advanced": {isActive},
+			groupBasic.Name:     {model, price},
+			isActive.Group.Name: {isActive},
 		}
 		return features, featureMap
 	}
diff --git a/internal/database/online/test_impl/get.go b/internal/database/online/test_impl/get.go
index 32aa871d..27dc6c06 100644
--- a/internal/database/online/test_impl/get.go
+++ b/internal/database/online/test_impl/get.go
@@ -27,7 +27,7 @@ func TestGetExisted(t *testing.T, prepareStore PrepareStoreFn, destroyStore Dest
 		require.NoError(t, err)
 
 		for i, f := range s.Features {
-			compareFeatureValue(t, target.ValueAt(i), rs[f.FullName], f.ValueType)
+			compareFeatureValue(t, target.ValueAt(i), rs[f.FullName()], f.ValueType)
 		}
 	}
 }
@@ -73,7 +73,7 @@ func TestMultiGet(t *testing.T, prepareStore PrepareStoreFn, destroystore Destro
 	require.NoError(t, err)
 	for _, record := range s.Data {
 		for i, feature := range s.Features {
-			compareFeatureValue(t, record.ValueAt(i), rs[record.EntityKey()][feature.FullName], feature.ValueType)
+			compareFeatureValue(t, record.ValueAt(i), rs[record.EntityKey()][feature.FullName()], feature.ValueType)
 		}
 	}
 
diff --git a/internal/database/online/test_impl/purge.go b/internal/database/online/test_impl/purge.go
index 651bc293..8fe2ce37 100644
--- a/internal/database/online/test_impl/purge.go
+++ b/internal/database/online/test_impl/purge.go
@@ -50,7 +50,7 @@ func TestPurgeNotRemovesOtherRevisions(t *testing.T, prepareStore PrepareStoreFn
 		})
 		require.NoError(t, err)
 		for i, f := range SampleSmall.Features {
-			compareFeatureValue(t, record.ValueAt(i), rs[f.FullName], f.ValueType)
+			compareFeatureValue(t, record.ValueAt(i), rs[f.FullName()], f.ValueType)
 		}
 	}
 }
diff --git a/internal/database/online/test_impl/stream.go b/internal/database/online/test_impl/stream.go
index 4850deba..564c5a58 100644
--- a/internal/database/online/test_impl/stream.go
+++ b/internal/database/online/test_impl/stream.go
@@ -46,7 +46,6 @@ func init() {
 			&types.Feature{
 				ID:        1,
 				Name:      "last_5_click_post",
-				FullName:  "user_clicks.last_5_click_post",
 				ValueType: types.String,
 				GroupID:   1,
 				Group:     group1,
@@ -54,7 +53,6 @@ func init() {
 			&types.Feature{
 				ID:        2,
 				Name:      "last_10_click_post",
-				FullName:  "user_clicks.last_10_click_post",
 				ValueType: types.String,
 				GroupID:   1,
 				Group:     group1,
@@ -62,7 +60,6 @@ func init() {
 			&types.Feature{
 				ID:        3,
 				Name:      "recent_5_read_post",
-				FullName:  "user_reads.recent_5_read_post",
 				ValueType: types.String,
 				GroupID:   2,
 				Group:     group2,
@@ -139,7 +136,7 @@ func TestPush(t *testing.T, prepareStore PrepareStoreFn, destoryStore DestroySto
 	})
 	assert.NoError(t, err)
 	assert.Equal(t, map[string]interface{}{
-		feature1.FullName: "post1",
+		feature1.FullName(): "post1",
 	}, rs)
 
 	assert.NoError(t, store.Push(ctx, online.PushOpt{
@@ -158,7 +155,7 @@ func TestPush(t *testing.T, prepareStore PrepareStoreFn, destoryStore DestroySto
 	})
 	assert.NoError(t, err)
 	assert.Equal(t, map[string]interface{}{
-		feature1.FullName: "post2",
+		feature1.FullName(): "post2",
 	}, rs)
 
 	assert.NoError(t, store.PrepareStreamTable(ctx, online.PrepareStreamTableOpt{
@@ -183,7 +180,7 @@ func TestPush(t *testing.T, prepareStore PrepareStoreFn, destoryStore DestroySto
 	})
 	assert.NoError(t, err)
 	assert.Equal(t, map[string]interface{}{
-		feature1.FullName: "post1",
-		feature2.FullName: "post2",
+		feature1.FullName(): "post1",
+		feature2.FullName(): "post2",
 	}, rs)
 }
diff --git a/internal/database/online/test_impl/util.go b/internal/database/online/test_impl/util.go
index fb3b9840..223e1a71 100644
--- a/internal/database/online/test_impl/util.go
+++ b/internal/database/online/test_impl/util.go
@@ -37,7 +37,6 @@ func init() {
 				&types.Feature{
 					ID:        1,
 					Name:      "age",
-					FullName:  "user.age",
 					GroupID:   1,
 					Group:     &types.Group{ID: 1, Category: types.CategoryBatch},
 					ValueType: types.Int64,
@@ -45,7 +44,6 @@ func init() {
 				&types.Feature{
 					ID:        2,
 					Name:      "gender",
-					FullName:  "user.gender",
 					GroupID:   1,
 					Group:     &types.Group{ID: 1, Category: types.CategoryBatch},
 					ValueType: types.String,
@@ -53,7 +51,6 @@ func init() {
 				&types.Feature{
 					ID:        3,
 					Name:      "account",
-					FullName:  "user.account",
 					GroupID:   1,
 					Group:     &types.Group{ID: 1, Category: types.CategoryBatch},
 					ValueType: types.Float64,
@@ -61,7 +58,6 @@ func init() {
 				&types.Feature{
 					ID:        4,
 					Name:      "is_active",
-					FullName:  "user.is_active",
 					GroupID:   1,
 					Group:     &types.Group{ID: 1, Category: types.CategoryBatch},
 					ValueType: types.Bool,
@@ -69,7 +65,6 @@ func init() {
 				&types.Feature{
 					ID:        5,
 					Name:      "register_time",
-					FullName:  "user.register_time",
 					GroupID:   1,
 					Group:     &types.Group{ID: 1, Category: types.CategoryBatch},
 					ValueType: types.Time,
@@ -98,7 +93,6 @@ func init() {
 			&types.Feature{
 				ID:        2,
 				Name:      "charge",
-				FullName:  "user.charge",
 				GroupID:   2,
 				Group:     &types.Group{ID: 2, Category: types.CategoryBatch},
 				ValueType: types.Float64,
diff --git a/pkg/oomstore/feature_test.go b/pkg/oomstore/feature_test.go
index ec67e8bf..2b4c66f5 100644
--- a/pkg/oomstore/feature_test.go
+++ b/pkg/oomstore/feature_test.go
@@ -2,7 +2,6 @@ package oomstore_test
 
 import (
 	"context"
-	"fmt"
 	"testing"
 
 	"github.com/golang/mock/gomock"
@@ -70,7 +69,6 @@ func TestCreateFeature(t *testing.T) {
 
 			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,
diff --git a/pkg/oomstore/join_test.go b/pkg/oomstore/join_test.go
index d92e841a..1355a418 100644
--- a/pkg/oomstore/join_test.go
+++ b/pkg/oomstore/join_test.go
@@ -108,7 +108,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.FeatureFullNames}).Return(tc.features, nil)
+			metadataStore.EXPECT().ListFeature(gomock.Any(), metadata.ListFeatureOpt{}).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 8d91da66..ece4c4ee 100644
--- a/pkg/oomstore/online_query_test.go
+++ b/pkg/oomstore/online_query_test.go
@@ -7,7 +7,6 @@ import (
 
 	"github.com/golang/mock/gomock"
 	"github.com/oom-ai/oomstore/internal/database/dbutil"
-	"github.com/oom-ai/oomstore/internal/database/metadata"
 	"github.com/oom-ai/oomstore/internal/database/metadata/mock_metadata"
 	"github.com/oom-ai/oomstore/internal/database/online/mock_online"
 	"github.com/oom-ai/oomstore/pkg/oomstore"
@@ -81,7 +80,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.FeatureFullNames}).Return(tc.features)
+			metadataStore.EXPECT().ListCachedFeature(gomock.Any(), &tc.opt.FeatureFullNames).Return(tc.features)
 			if tc.entityName != nil {
 				onlineStore.EXPECT().Get(gomock.Any(), gomock.Any()).Return(dbutil.RowMap{
 					"price": int64(100),
@@ -175,7 +174,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.FeatureFullNames}).Return(tc.features)
+			metadataStore.EXPECT().ListCachedFeature(gomock.Any(), &tc.opt.FeatureFullNames).Return(tc.features)
 			if tc.entityName != nil {
 				onlineStore.EXPECT().MultiGet(gomock.Any(), gomock.Any()).Return(map[string]dbutil.RowMap{
 					"1234": {
@@ -219,7 +218,6 @@ func prepareFeatures(isConsistent bool, isAvailable bool) types.FeatureList {
 	features := types.FeatureList{
 		{
 			Name:      "model",
-			FullName:  "device.model",
 			ValueType: types.String,
 			GroupID:   1,
 			Group: &types.Group{
@@ -232,7 +230,6 @@ func prepareFeatures(isConsistent bool, isAvailable bool) types.FeatureList {
 		},
 		{
 			Name:      "price",
-			FullName:  "device.price",
 			ValueType: types.Int64,
 			GroupID:   2,
 			Group: &types.Group{
@@ -245,7 +242,6 @@ func prepareFeatures(isConsistent bool, isAvailable bool) types.FeatureList {
 		},
 		{
 			Name:      "age",
-			FullName:  "user.age",
 			ValueType: types.Int64,
 			GroupID:   3,
 			Group: &types.Group{