Repository: oom-ai/oomstore
Pull request #778 (2021-12-22 08:37:23)

feat: apply feature value type


Patch:
diff --git a/internal/database/dbutil/db_type.go b/internal/database/dbutil/db_type.go
deleted file mode 100644
index 45911350..00000000
--- a/internal/database/dbutil/db_type.go
+++ /dev/null
@@ -1,88 +0,0 @@
-package dbutil
-
-import (
-	"fmt"
-
-	"github.com/oom-ai/oomstore/pkg/errdefs"
-	"github.com/oom-ai/oomstore/pkg/oomstore/types"
-)
-
-func GetDbTypeFrom(backend types.BackendType, valueType types.ValueType) (string, error) {
-	var mp map[types.ValueType]string
-	switch backend {
-	case types.POSTGRES:
-		mp = postgresType
-	case types.SQLite:
-		mp = sqliteType
-	case types.MYSQL:
-		mp = mysqlType
-	case types.CASSANDRA:
-		mp = cassandraType
-	case types.SNOWFLAKE:
-		mp = snowFlake
-	case types.DYNAMODB:
-		mp = dynamoDB
-	case types.REDSHIFT:
-		mp = redshiftType
-	default:
-		return "", errdefs.InvalidAttribute(fmt.Errorf("unsupported backend: %s", backend))
-	}
-
-	t, ok := mp[valueType]
-	if !ok {
-		return "", errdefs.InvalidAttribute(fmt.Errorf("unsupported value type: %s", valueType))
-	}
-	return t, nil
-}
-
-var (
-	sqliteType = map[types.ValueType]string{
-		types.STRING:  "TEXT",
-		types.INT64:   "INTEGER",
-		types.FLOAT64: "FLOAT",
-		types.BOOL:    "INTEGER",
-		types.BYTES:   "BLOB",
-		types.TIME:    "TIMESTAMP",
-	}
-	mysqlType = map[types.ValueType]string{
-		types.STRING:  "text",
-		types.INT64:   "bigint",
-		types.FLOAT64: "double",
-		types.BOOL:    "bool",
-		types.TIME:    "datetime",
-		types.BYTES:   "binary",
-	}
-	postgresType = map[types.ValueType]string{
-		types.STRING:  "text",
-		types.INT64:   "bigint",
-		types.FLOAT64: "double precision",
-		types.BOOL:    "boolean",
-		types.TIME:    "timestamptz",
-		types.BYTES:   "bytea",
-	}
-	snowFlake = map[types.ValueType]string{
-		types.STRING:  "VARCHAR",
-		types.INT64:   "INTEGER",
-		types.FLOAT64: "DOUBLE",
-		types.BOOL:    "BOOLEAN",
-		types.TIME:    "TIME",
-		types.BYTES:   "BINARY",
-	}
-	dynamoDB = map[types.ValueType]string{
-		types.STRING:  "String",
-		types.INT64:   "BigInteger",
-		types.FLOAT64: "Float",
-		types.BOOL:    "Boolean",
-		types.TIME:    "Date",
-		types.BYTES:   "Byte",
-	}
-	cassandraType = map[types.ValueType]string{
-		types.STRING:  "text",
-		types.INT64:   "bigint",
-		types.FLOAT64: "double",
-		types.BOOL:    "boolean",
-		types.TIME:    "timestamp",
-		types.BYTES:   "text",
-	}
-	redshiftType = postgresType
-)
diff --git a/internal/database/dbutil/schema.go b/internal/database/dbutil/schema.go
index 761ee8aa..5dd41776 100644
--- a/internal/database/dbutil/schema.go
+++ b/internal/database/dbutil/schema.go
@@ -91,7 +91,7 @@ func BuildCreateSchema(tableName string, entity *types.Entity, features types.Fe
 func newSchema(tableName string, entity types.Entity, features types.FeatureList, backend types.BackendType) (CreateSchema, error) {
 	columns := make([]Column, 0, len(features))
 	for _, feature := range features {
-		dbType, err := GetDbTypeFrom(backend, feature.ValueType)
+		dbType, err := DBValueType(backend, feature.ValueType)
 		if err != nil {
 			return CreateSchema{}, err
 		}
diff --git a/internal/database/dbutil/type_mapping.go b/internal/database/dbutil/type_mapping.go
new file mode 100644
index 00000000..cd82125a
--- /dev/null
+++ b/internal/database/dbutil/type_mapping.go
@@ -0,0 +1,302 @@
+package dbutil
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/oom-ai/oomstore/pkg/errdefs"
+	"github.com/oom-ai/oomstore/pkg/oomstore/types"
+)
+
+func DBValueType(backend types.BackendType, valueType types.ValueType) (string, error) {
+	var mp map[types.ValueType]string
+	switch backend {
+	case types.POSTGRES:
+		mp = valueTypeToPostgresType
+	case types.SQLite:
+		mp = valueTypeToSQLiteType
+	case types.MYSQL:
+		mp = valueTypeToMySQLType
+	case types.CASSANDRA:
+		mp = valueTypeToCassandraType
+	case types.SNOWFLAKE:
+		mp = valueTypeToSnowFlakeType
+	case types.DYNAMODB:
+		mp = valueTypeToDynamoDBType
+	case types.REDSHIFT:
+		mp = valueTypeToRedshiftType
+	default:
+		return "", errdefs.InvalidAttribute(fmt.Errorf("unsupported backend: %s", backend))
+	}
+
+	t, ok := mp[valueType]
+	if !ok {
+		return "", errdefs.InvalidAttribute(fmt.Errorf("unsupported value type: %s", valueType))
+	}
+	return t, nil
+}
+
+// Used for inferring feature value type from a supported offline db value type
+func ValueType(backend types.BackendType, dbValueType string) (types.ValueType, error) {
+	var mp map[string]types.ValueType
+	switch backend {
+	case types.POSTGRES:
+		mp = postgresTypeToValueType
+	case types.MYSQL:
+		mp = mysqlTypeToValueType
+	case types.SNOWFLAKE:
+		mp = snowflakeTypeToValueType
+	case types.BIGQUERY:
+		mp = bigQueryTypeToValueType
+	case types.SQLite:
+		mp = sqliteTypeToValueType
+	case types.REDSHIFT:
+		mp = redshiftTypeToValueType
+	default:
+		return 0, errdefs.InvalidAttribute(fmt.Errorf("unsupported backend: %s", backend))
+	}
+
+	t, ok := mp[strings.ToLower(dbValueType)]
+	if !ok {
+		return 0, errdefs.InvalidAttribute(fmt.Errorf("unsupported db value type: %s", dbValueType))
+	}
+	return t, nil
+}
+
+// Mapping feature value type to database data type
+var (
+	valueTypeToSQLiteType = map[types.ValueType]string{
+		types.STRING:  "text",
+		types.INT64:   "integer",
+		types.FLOAT64: "float",
+		types.BOOL:    "integer",
+		types.BYTES:   "blob",
+		types.TIME:    "timestamp",
+	}
+	valueTypeToMySQLType = map[types.ValueType]string{
+		types.STRING:  "text",
+		types.INT64:   "bigint",
+		types.FLOAT64: "double",
+		types.BOOL:    "bool",
+		types.TIME:    "datetime",
+		types.BYTES:   "varbinary",
+	}
+	valueTypeToPostgresType = map[types.ValueType]string{
+		types.STRING:  "text",
+		types.INT64:   "bigint",
+		types.FLOAT64: "double precision",
+		types.BOOL:    "boolean",
+		types.TIME:    "timestamp",
+		types.BYTES:   "bytea",
+	}
+	valueTypeToSnowFlakeType = map[types.ValueType]string{
+		types.STRING:  "varchar",
+		types.INT64:   "bigint",
+		types.FLOAT64: "double",
+		types.BOOL:    "boolean",
+		types.TIME:    "timestamp",
+		types.BYTES:   "varbinary",
+	}
+	valueTypeToDynamoDBType = map[types.ValueType]string{
+		types.STRING:  "String",
+		types.INT64:   "BigInteger",
+		types.FLOAT64: "Double",
+		types.BOOL:    "Boolean",
+		types.TIME:    "Date",
+		types.BYTES:   "ByteBuffer",
+	}
+	valueTypeToCassandraType = map[types.ValueType]string{
+		types.STRING:  "text",
+		types.INT64:   "bigint",
+		types.FLOAT64: "double",
+		types.BOOL:    "boolean",
+		types.TIME:    "timestamp",
+		types.BYTES:   "blob",
+	}
+	valueTypeToRedshiftType = valueTypeToPostgresType
+)
+
+// Mapping database data type to feature value type
+var (
+	postgresTypeToValueType = map[string]types.ValueType{
+		"bigint":    types.INT64,
+		"int8":      types.INT64,
+		"bigserial": types.INT64,
+		"serial8":   types.INT64,
+
+		"boolean": types.BOOL,
+		"bool":    types.BOOL,
+
+		"bytea":       types.BYTES,
+		"jsonb":       types.BYTES,
+		"uuid":        types.BYTES,
+		"bit":         types.BYTES,
+		"bit varying": types.BYTES,
+		"character":   types.BYTES,
+		"char":        types.BYTES,
+		"json":        types.BYTES,
+		"money":       types.BYTES,
+		"numeric":     types.BYTES,
+
+		"character varying": types.STRING,
+		"text":              types.STRING,
+		"varchar":           types.STRING,
+
+		"double precision": types.FLOAT64,
+		"float8":           types.FLOAT64,
+
+		"integer": types.INT64,
+		"int":     types.INT64,
+		"int4":    types.INT64,
+		"serial":  types.INT64,
+		"serial4": types.INT64,
+
+		"real":   types.FLOAT64,
+		"float4": types.FLOAT64,
+
+		"smallint":    types.INT64,
+		"int2":        types.INT64,
+		"smallserial": types.INT64,
+		"serial2":     types.INT64,
+
+		"date":                        types.TIME,
+		"time":                        types.TIME,
+		"time without time zone":      types.TIME,
+		"time with time zone":         types.TIME,
+		"timetz":                      types.TIME,
+		"timestamp":                   types.TIME,
+		"timestamp without time zone": types.TIME,
+		"timestamp with time zone":    types.TIME,
+		"timestamptz":                 types.TIME,
+	}
+	bigQueryTypeToValueType = map[string]types.ValueType{
+		"bool":     types.BOOL,
+		"bytes":    types.BYTES,
+		"datetime": types.TIME,
+		"string":   types.STRING,
+
+		"bigint":   types.INT64,
+		"smallint": types.INT64,
+		"int64":    types.INT64,
+		"integer":  types.INT64,
+		"int":      types.INT64,
+
+		"float64": types.FLOAT64,
+		"numeric": types.FLOAT64,
+		"decimal": types.FLOAT64,
+	}
+	mysqlTypeToValueType = map[string]types.ValueType{
+		"boolean": types.BOOL,
+		"bool":    types.BOOL,
+
+		"binary":    types.BYTES,
+		"varbinary": types.BYTES,
+
+		"integer":   types.INT64,
+		"int":       types.INT64,
+		"smallint":  types.INT64,
+		"bigint":    types.INT64,
+		"tinyint":   types.INT64,
+		"mediumint": types.INT64,
+
+		"double": types.FLOAT64,
+		"float":  types.FLOAT64,
+
+		"text":    types.STRING,
+		"varchar": types.STRING,
+		"char":    types.STRING,
+
+		"date":      types.TIME,
+		"time":      types.TIME,
+		"datetime":  types.TIME,
+		"timestamp": types.TIME,
+		"year":      types.TIME,
+	}
+	// TODO: add type NUMBER, DECIMAL, NUMERIC
+	snowflakeTypeToValueType = map[string]types.ValueType{
+		"boolean": types.BOOL,
+
+		"binary":    types.BYTES,
+		"varbinary": types.BYTES,
+
+		"integer":  types.INT64,
+		"int":      types.INT64,
+		"smallint": types.INT64,
+		"bigint":   types.INT64,
+		"tinyint":  types.INT64,
+		"byteint":  types.INT64,
+
+		"double":           types.FLOAT64,
+		"double precision": types.FLOAT64,
+		"real":             types.FLOAT64,
+		"float":            types.FLOAT64,
+		"float4":           types.FLOAT64,
+		"float8":           types.FLOAT64,
+
+		"string":    types.STRING,
+		"text":      types.STRING,
+		"varchar":   types.STRING,
+		"char":      types.STRING,
+		"character": types.STRING,
+
+		"date":      types.TIME,
+		"time":      types.TIME,
+		"datetime":  types.TIME,
+		"timestamp": types.TIME,
+	}
+	sqliteTypeToValueType = map[string]types.ValueType{
+		"integer":   types.INT64,
+		"float":     types.FLOAT64,
+		"blob":      types.BYTES,
+		"text":      types.STRING,
+		"timestamp": types.TIME,
+		"datetime":  types.TIME,
+	}
+	// Redshift data type: https://docs.aws.amazon.com/redshift/latest/dg/c_Supported_data_types.html
+	redshiftTypeToValueType = map[string]types.ValueType{
+		"bigint": types.INT64,
+		"int8":   types.INT64,
+
+		"boolean": types.BOOL,
+		"bool":    types.BOOL,
+
+		"character":      types.BYTES,
+		"char":           types.BYTES,
+		"nchar":          types.BYTES,
+		"bpchar":         types.BYTES,
+		"varbyte":        types.BYTES,
+		"varbinary":      types.BYTES,
+		"binary varying": types.BYTES,
+		"numeric":        types.BYTES,
+		"decimal":        types.BYTES,
+
+		"character varying": types.STRING,
+		"text":              types.STRING,
+		"varchar":           types.STRING,
+		"nvarchar":          types.STRING,
+
+		"double precision": types.FLOAT64,
+		"float8":           types.FLOAT64,
+		"float":            types.FLOAT64,
+
+		"integer": types.INT64,
+		"int":     types.INT64,
+		"int4":    types.INT64,
+
+		"real":   types.FLOAT64,
+		"float4": types.FLOAT64,
+
+		"smallint": types.INT64,
+		"int2":     types.INT64,
+
+		"date":                        types.TIME,
+		"time":                        types.TIME,
+		"time without time zone":      types.TIME,
+		"time with time zone":         types.TIME,
+		"timetz":                      types.TIME,
+		"timestamp":                   types.TIME,
+		"timestamp without time zone": types.TIME,
+		"timestamp with time zone":    types.TIME,
+		"timestamptz":                 types.TIME,
+	}
+)
diff --git a/internal/database/metadata/mysql/feature.go b/internal/database/metadata/mysql/feature.go
index 208005c8..0eaf691e 100644
--- a/internal/database/metadata/mysql/feature.go
+++ b/internal/database/metadata/mysql/feature.go
@@ -5,16 +5,15 @@ import (
 	"fmt"
 
 	"github.com/go-sql-driver/mysql"
-	"github.com/oom-ai/oomstore/internal/database/dbutil"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
 )
 
 func createFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.CreateFeatureOpt) (int, error) {
-	if err := validateDataType(ctx, sqlxCtx, opt.DBValueType); err != nil {
-		return 0, fmt.Errorf("error validating value_type input, details: %s", err.Error())
+	if err := opt.ValueType.Validate(); err != nil {
+		return 0, err
 	}
-	query := "INSERT INTO feature(name, group_id, db_value_type, value_type, description) VALUES (?, ?, ?, ?, ?)"
-	res, err := sqlxCtx.ExecContext(ctx, sqlxCtx.Rebind(query), opt.FeatureName, opt.GroupID, opt.DBValueType, 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 {
@@ -30,13 +29,3 @@ func createFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metada
 	}
 	return int(featureID), err
 }
-
-func validateDataType(ctx context.Context, sqlxCtx metadata.SqlxContext, dataType string) error {
-	tmpTable := dbutil.TempTable("validate_data_type")
-	query := fmt.Sprintf("CREATE TEMPORARY TABLE %s (a %s)", tmpTable, dataType)
-	if _, err := sqlxCtx.ExecContext(ctx, query); err != nil {
-		return err
-	}
-	_, err := sqlxCtx.ExecContext(ctx, fmt.Sprintf("DROP TEMPORARY TABLE %s", tmpTable))
-	return err
-}
diff --git a/internal/database/metadata/mysql/schema.go b/internal/database/metadata/mysql/schema.go
index 178d5ee2..28d2c59e 100644
--- a/internal/database/metadata/mysql/schema.go
+++ b/internal/database/metadata/mysql/schema.go
@@ -6,8 +6,7 @@ var META_TABLE_SCHEMAS = map[string]string{
 			id				INT 			NOT NULL AUTO_INCREMENT PRIMARY KEY,
 			name          	VARCHAR(32) 	NOT	NULL,
 			group_id      	INT         	NOT	NULL,
-			db_value_type 	VARCHAR(32) 	NOT	NULL COMMENT "database data type of feature value",
-			value_type    	VARCHAR(16) 	NOT	NULL COMMENT "data type of feature value",
+			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,
diff --git a/internal/database/metadata/postgres/feature.go b/internal/database/metadata/postgres/feature.go
index 0f4f529a..2cce8175 100644
--- a/internal/database/metadata/postgres/feature.go
+++ b/internal/database/metadata/postgres/feature.go
@@ -6,17 +6,16 @@ import (
 
 	"github.com/jackc/pgerrcode"
 	"github.com/lib/pq"
-	"github.com/oom-ai/oomstore/internal/database/dbutil"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
 )
 
 func createFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.CreateFeatureOpt) (int, error) {
-	if err := validateDataType(ctx, sqlxCtx, opt.DBValueType); err != nil {
-		return 0, fmt.Errorf("err when validating value_type input, details: %s", err.Error())
+	if err := opt.ValueType.Validate(); err != nil {
+		return 0, err
 	}
 	var featureID int
-	query := "INSERT INTO feature(name, group_id, db_value_type, value_type, description) VALUES ($1, $2, $3, $4, $5) RETURNING id"
-	err := sqlxCtx.GetContext(ctx, &featureID, query, opt.FeatureName, opt.GroupID, opt.DBValueType, 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 {
@@ -26,10 +25,3 @@ func createFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metada
 	}
 	return featureID, err
 }
-
-func validateDataType(ctx context.Context, sqlxCtx metadata.SqlxContext, dataType string) error {
-	tmpTable := dbutil.TempTable("validate_data_type")
-	stmt := fmt.Sprintf("CREATE TEMPORARY TABLE %s (a %s) ON COMMIT DROP", tmpTable, dataType)
-	_, err := sqlxCtx.ExecContext(ctx, stmt)
-	return err
-}
diff --git a/internal/database/metadata/postgres/schema.go b/internal/database/metadata/postgres/schema.go
index c54f965b..8801258f 100644
--- a/internal/database/metadata/postgres/schema.go
+++ b/internal/database/metadata/postgres/schema.go
@@ -24,15 +24,13 @@ var META_TABLE_SCHEMAS = map[string]string{
 			id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
 			name          VARCHAR(32)  NOT     NULL,
 			group_id      INT          NOT     NULL,
-			db_value_type VARCHAR(32)  NOT     NULL,
-			value_type    VARCHAR(16)  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 (name)
 		);
 		COMMENT ON COLUMN feature.value_type    IS 'data type of feature value';
-		COMMENT ON COLUMN feature.db_value_type IS 'database data type of feature value';
 		`,
 	"feature_group": `
 		CREATE TABLE feature_group (
diff --git a/internal/database/metadata/sqlite/feature.go b/internal/database/metadata/sqlite/feature.go
index f3174cb4..11ea2381 100644
--- a/internal/database/metadata/sqlite/feature.go
+++ b/internal/database/metadata/sqlite/feature.go
@@ -10,8 +10,11 @@ import (
 )
 
 func createFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.CreateFeatureOpt) (int, error) {
-	query := "INSERT INTO feature(name, group_id, db_value_type, value_type, description) VALUES (?, ?, ?, ?, ?)"
-	res, err := sqlxCtx.ExecContext(ctx, sqlxCtx.Rebind(query), opt.FeatureName, opt.GroupID, opt.DBValueType, opt.ValueType, opt.Description)
+	if err := opt.ValueType.Validate(); err != nil {
+		return 0, err
+	}
+	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.(sqlite3.Error); ok {
 			if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
diff --git a/internal/database/metadata/sqlite/schema.go b/internal/database/metadata/sqlite/schema.go
index c665ab82..c9a6c883 100644
--- a/internal/database/metadata/sqlite/schema.go
+++ b/internal/database/metadata/sqlite/schema.go
@@ -14,8 +14,7 @@ var META_TABLE_SCHEMAS = map[string]string{
 			id				INTEGER 		NOT NULL PRIMARY KEY AUTOINCREMENT,
 			name          	VARCHAR(32) 	NOT	NULL,
 			group_id      	INT         	NOT	NULL,
-			db_value_type 	VARCHAR(32) 	NOT	NULL,
-			value_type    	VARCHAR(16) 	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,
diff --git a/internal/database/metadata/types.go b/internal/database/metadata/types.go
index a2102337..6809e368 100644
--- a/internal/database/metadata/types.go
+++ b/internal/database/metadata/types.go
@@ -16,7 +16,6 @@ type CreateEntityOpt struct {
 type CreateFeatureOpt struct {
 	FeatureName string
 	GroupID     int
-	DBValueType string
 	Description string
 	ValueType   types.ValueType
 }
diff --git a/internal/database/offline/bigquery/store.go b/internal/database/offline/bigquery/store.go
index b9310604..828efe85 100644
--- a/internal/database/offline/bigquery/store.go
+++ b/internal/database/offline/bigquery/store.go
@@ -8,28 +8,13 @@ import (
 	"google.golang.org/api/iterator"
 
 	"cloud.google.com/go/bigquery"
+	"github.com/oom-ai/oomstore/internal/database/dbutil"
 	"github.com/oom-ai/oomstore/internal/database/offline"
-	"github.com/oom-ai/oomstore/internal/database/offline/sqlutil"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
 	"google.golang.org/api/option"
 )
 
-var BigQueryTypeMap = map[string]types.ValueType{
-	"bool":     types.BOOL,
-	"bytes":    types.BYTES,
-	"datetime": types.TIME,
-	"string":   types.STRING,
-
-	"bigint":   types.INT64,
-	"smallint": types.INT64,
-	"int64":    types.INT64,
-	"integer":  types.INT64,
-	"int":      types.INT64,
-
-	"float64": types.FLOAT64,
-	"numeric": types.FLOAT64,
-	"decimal": types.FLOAT64,
-}
+const BackendType = types.BIGQUERY
 
 var _ offline.Store = &DB{}
 
@@ -71,7 +56,7 @@ func (db *DB) TableSchema(ctx context.Context, tableName string) (*types.DataTab
 		if err != nil {
 			return nil, err
 		}
-		valueType, err := db.TypeTag(cast.ToString(recordMap["data_type"]))
+		valueType, err := dbutil.ValueType(BackendType, cast.ToString(recordMap["data_type"]))
 		if err != nil {
 			return nil, err
 		}
@@ -83,7 +68,3 @@ func (db *DB) TableSchema(ctx context.Context, tableName string) (*types.DataTab
 
 	return &schema, nil
 }
-
-func (db *DB) TypeTag(dbType string) (types.ValueType, error) {
-	return sqlutil.GetValueType(BigQueryTypeMap, dbType)
-}
diff --git a/internal/database/offline/mock_offline/store.go b/internal/database/offline/mock_offline/store.go
index e11855fc..82151d9f 100644
--- a/internal/database/offline/mock_offline/store.go
+++ b/internal/database/offline/mock_offline/store.go
@@ -123,18 +123,3 @@ func (mr *MockStoreMockRecorder) TableSchema(ctx, tableName interface{}) *gomock
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TableSchema", reflect.TypeOf((*MockStore)(nil).TableSchema), ctx, tableName)
 }
-
-// TypeTag mocks base method.
-func (m *MockStore) TypeTag(dbType string) (types.ValueType, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "TypeTag", dbType)
-	ret0, _ := ret[0].(types.ValueType)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// TypeTag indicates an expected call of TypeTag.
-func (mr *MockStoreMockRecorder) TypeTag(dbType interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TypeTag", reflect.TypeOf((*MockStore)(nil).TypeTag), dbType)
-}
diff --git a/internal/database/offline/mysql/store.go b/internal/database/offline/mysql/store.go
index 8c03dd70..f1361280 100644
--- a/internal/database/offline/mysql/store.go
+++ b/internal/database/offline/mysql/store.go
@@ -13,34 +13,6 @@ import (
 
 const MySQLBatchSize = 20
 
-var MySQLTypeMap = map[string]types.ValueType{
-	"boolean": types.BOOL,
-	"bool":    types.BOOL,
-
-	"binary":    types.BYTES,
-	"varbinary": types.BYTES,
-
-	"integer":   types.INT64,
-	"int":       types.INT64,
-	"smallint":  types.INT64,
-	"bigint":    types.INT64,
-	"tinyint":   types.INT64,
-	"mediumint": types.INT64,
-
-	"double": types.FLOAT64,
-	"float":  types.FLOAT64,
-
-	"text":    types.STRING,
-	"varchar": types.STRING,
-	"char":    types.STRING,
-
-	"date":      types.TIME,
-	"time":      types.TIME,
-	"datetime":  types.TIME,
-	"timestamp": types.TIME,
-	"year":      types.TIME,
-}
-
 var _ offline.Store = &DB{}
 
 type DB struct {
@@ -68,10 +40,6 @@ func (db *DB) Join(ctx context.Context, opt offline.JoinOpt) (*types.JoinResult,
 	return sqlutil.Join(ctx, db.DB, opt, types.MYSQL)
 }
 
-func (db *DB) TypeTag(dbType string) (types.ValueType, error) {
-	return sqlutil.GetValueType(MySQLTypeMap, dbType)
-}
-
 func (db *DB) TableSchema(ctx context.Context, tableName string) (*types.DataTableSchema, error) {
 	return nil, fmt.Errorf("not implemented")
 }
diff --git a/internal/database/offline/postgres/store.go b/internal/database/offline/postgres/store.go
index 2fdc849e..cb650262 100644
--- a/internal/database/offline/postgres/store.go
+++ b/internal/database/offline/postgres/store.go
@@ -10,57 +10,7 @@ import (
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
 )
 
-var PostgresTypeMap = map[string]types.ValueType{
-	"bigint":    types.INT64,
-	"int8":      types.INT64,
-	"bigserial": types.INT64,
-	"serial8":   types.INT64,
-
-	"boolean": types.BOOL,
-	"bool":    types.BOOL,
-
-	"bytea":       types.BYTES,
-	"jsonb":       types.BYTES,
-	"uuid":        types.BYTES,
-	"bit":         types.BYTES,
-	"bit varying": types.BYTES,
-	"character":   types.BYTES,
-	"char":        types.BYTES,
-	"json":        types.BYTES,
-	"money":       types.BYTES,
-	"numeric":     types.BYTES,
-
-	"character varying": types.STRING,
-	"text":              types.STRING,
-	"varchar":           types.STRING,
-
-	"double precision": types.FLOAT64,
-	"float8":           types.FLOAT64,
-
-	"integer": types.INT64,
-	"int":     types.INT64,
-	"int4":    types.INT64,
-	"serial":  types.INT64,
-	"serial4": types.INT64,
-
-	"real":   types.FLOAT64,
-	"float4": types.FLOAT64,
-
-	"smallint":    types.INT64,
-	"int2":        types.INT64,
-	"smallserial": types.INT64,
-	"serial2":     types.INT64,
-
-	"date":                        types.TIME,
-	"time":                        types.TIME,
-	"time without time zone":      types.TIME,
-	"time with time zone":         types.TIME,
-	"timetz":                      types.TIME,
-	"timestamp":                   types.TIME,
-	"timestamp without time zone": types.TIME,
-	"timestamp with time zone":    types.TIME,
-	"timestamptz":                 types.TIME,
-}
+const BackendType = types.POSTGRES
 
 var _ offline.Store = &DB{}
 
@@ -89,14 +39,10 @@ func (db *DB) Join(ctx context.Context, opt offline.JoinOpt) (*types.JoinResult,
 	return sqlutil.Join(ctx, db.DB, opt, types.POSTGRES)
 }
 
-func (db *DB) TypeTag(dbType string) (types.ValueType, error) {
-	return sqlutil.GetValueType(PostgresTypeMap, dbType)
-}
-
 func (db *DB) TableSchema(ctx context.Context, tableName string) (*types.DataTableSchema, error) {
 	rows, err := db.QueryxContext(ctx, "select column_name, data_type from information_schema.columns where table_name = $1", tableName)
 	if err != nil {
 		return nil, err
 	}
-	return sqlutil.SqlxTableSchema(ctx, db, rows)
+	return sqlutil.SqlxTableSchema(ctx, db, BackendType, rows)
 }
diff --git a/internal/database/offline/redshift/store.go b/internal/database/offline/redshift/store.go
index 573d4056..d4f90c4e 100644
--- a/internal/database/offline/redshift/store.go
+++ b/internal/database/offline/redshift/store.go
@@ -10,55 +10,9 @@ import (
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
 )
 
-const RedshiftBatchSize = 20
-
-// Redshift data type: https://docs.aws.amazon.com/redshift/latest/dg/c_Supported_data_types.html
-var RedshiftTypeMap = map[string]types.ValueType{
-	"bigint": types.INT64,
-	"int8":   types.INT64,
-
-	"boolean": types.BOOL,
-	"bool":    types.BOOL,
-
-	"character":      types.BYTES,
-	"char":           types.BYTES,
-	"nchar":          types.BYTES,
-	"bpchar":         types.BYTES,
-	"varbyte":        types.BYTES,
-	"varbinary":      types.BYTES,
-	"binary varying": types.BYTES,
-	"numeric":        types.BYTES,
-	"decimal":        types.BYTES,
-
-	"character varying": types.STRING,
-	"text":              types.STRING,
-	"varchar":           types.STRING,
-	"nvarchar":          types.STRING,
-
-	"double precision": types.FLOAT64,
-	"float8":           types.FLOAT64,
-	"float":            types.FLOAT64,
-
-	"integer": types.INT64,
-	"int":     types.INT64,
-	"int4":    types.INT64,
+const BackendType = types.REDSHIFT
 
-	"real":   types.FLOAT64,
-	"float4": types.FLOAT64,
-
-	"smallint": types.INT64,
-	"int2":     types.INT64,
-
-	"date":                        types.TIME,
-	"time":                        types.TIME,
-	"time without time zone":      types.TIME,
-	"time with time zone":         types.TIME,
-	"timetz":                      types.TIME,
-	"timestamp":                   types.TIME,
-	"timestamp without time zone": types.TIME,
-	"timestamp with time zone":    types.TIME,
-	"timestamptz":                 types.TIME,
-}
+const RedshiftBatchSize = 20
 
 var _ offline.Store = &DB{}
 
@@ -76,19 +30,15 @@ func Open(option *types.RedshiftOpt) (*DB, error) {
 }
 
 func (db *DB) Import(ctx context.Context, opt offline.ImportOpt) (int64, error) {
-	return sqlutil.Import(ctx, db.DB, opt, dbutil.LoadDataFromSource(types.REDSHIFT, RedshiftBatchSize), types.REDSHIFT)
+	return sqlutil.Import(ctx, db.DB, opt, dbutil.LoadDataFromSource(BackendType, RedshiftBatchSize), BackendType)
 }
 
 func (db *DB) Export(ctx context.Context, opt offline.ExportOpt) (<-chan types.ExportRecord, <-chan error) {
-	return sqlutil.Export(ctx, db.DB, opt, types.REDSHIFT)
+	return sqlutil.Export(ctx, db.DB, opt, BackendType)
 }
 
 func (db *DB) Join(ctx context.Context, opt offline.JoinOpt) (*types.JoinResult, error) {
-	return sqlutil.Join(ctx, db.DB, opt, types.REDSHIFT)
-}
-
-func (db *DB) TypeTag(dbType string) (types.ValueType, error) {
-	return sqlutil.GetValueType(RedshiftTypeMap, dbType)
+	return sqlutil.Join(ctx, db.DB, opt, BackendType)
 }
 
 func (db *DB) TableSchema(ctx context.Context, tableName string) (*types.DataTableSchema, error) {
@@ -96,5 +46,5 @@ func (db *DB) TableSchema(ctx context.Context, tableName string) (*types.DataTab
 	if err != nil {
 		return nil, err
 	}
-	return sqlutil.SqlxTableSchema(ctx, db, rows)
+	return sqlutil.SqlxTableSchema(ctx, db, BackendType, rows)
 }
diff --git a/internal/database/offline/snowflake/store.go b/internal/database/offline/snowflake/store.go
index eebabf5b..d733f9f4 100644
--- a/internal/database/offline/snowflake/store.go
+++ b/internal/database/offline/snowflake/store.go
@@ -14,39 +14,6 @@ import (
 
 const SnowflakeBatchSize = 100
 
-// TODO: add type NUMBER, DECIMAL, NUMERIC
-var SnowflakeTypeMap = map[string]types.ValueType{
-	"boolean": types.BOOL,
-
-	"binary":    types.BYTES,
-	"varbinary": types.BYTES,
-
-	"integer":  types.INT64,
-	"int":      types.INT64,
-	"smallint": types.INT64,
-	"bigint":   types.INT64,
-	"tinyint":  types.INT64,
-	"byteint":  types.INT64,
-
-	"double":           types.FLOAT64,
-	"double precision": types.FLOAT64,
-	"real":             types.FLOAT64,
-	"float":            types.FLOAT64,
-	"float4":           types.FLOAT64,
-	"float8":           types.FLOAT64,
-
-	"string":    types.STRING,
-	"text":      types.STRING,
-	"varchar":   types.STRING,
-	"char":      types.STRING,
-	"character": types.STRING,
-
-	"date":      types.TIME,
-	"time":      types.TIME,
-	"datetime":  types.TIME,
-	"timestamp": types.TIME,
-}
-
 var _ offline.Store = &DB{}
 
 type DB struct {
@@ -88,10 +55,6 @@ func (db *DB) Join(ctx context.Context, opt offline.JoinOpt) (*types.JoinResult,
 	return sqlutil.Join(ctx, db.DB, opt, types.SNOWFLAKE)
 }
 
-func (db *DB) TypeTag(dbType string) (types.ValueType, error) {
-	return sqlutil.GetValueType(SnowflakeTypeMap, dbType)
-}
-
 func (db *DB) TableSchema(ctx context.Context, tableName string) (*types.DataTableSchema, error) {
 	return nil, fmt.Errorf("not implemented")
 }
diff --git a/internal/database/offline/sqlite/store.go b/internal/database/offline/sqlite/store.go
index 72c09576..fd29fbfa 100644
--- a/internal/database/offline/sqlite/store.go
+++ b/internal/database/offline/sqlite/store.go
@@ -14,15 +14,6 @@ import (
 
 const SQLiteBatchSize = 20
 
-var SQLiteTypeMap = map[string]types.ValueType{
-	"integer":   types.INT64,
-	"float":     types.FLOAT64,
-	"blob":      types.BYTES,
-	"text":      types.STRING,
-	"timestamp": types.TIME,
-	"datetime":  types.TIME,
-}
-
 var _ offline.Store = &DB{}
 
 type DB struct {
@@ -50,10 +41,6 @@ func (db *DB) Join(ctx context.Context, opt offline.JoinOpt) (*types.JoinResult,
 	return sqlutil.Join(ctx, db.DB, opt, types.SQLite)
 }
 
-func (db *DB) TypeTag(dbType string) (types.ValueType, error) {
-	return sqlutil.GetValueType(SQLiteTypeMap, dbType)
-}
-
 func (db *DB) TableSchema(ctx context.Context, tableName string) (*types.DataTableSchema, error) {
 	return nil, fmt.Errorf("not implemented")
 }
diff --git a/internal/database/offline/sqlutil/join_helper.go b/internal/database/offline/sqlutil/join_helper.go
index adf2a3bc..0b6ebec8 100644
--- a/internal/database/offline/sqlutil/join_helper.go
+++ b/internal/database/offline/sqlutil/join_helper.go
@@ -26,7 +26,15 @@ func supportIndex(backendType types.BackendType) bool {
 	return true
 }
 
-func createTableJoined(ctx context.Context, db *sqlx.DB, features types.FeatureList, entity types.Entity, groupName string, valueNames []string, backendType types.BackendType) (string, error) {
+func createTableJoined(
+	ctx context.Context,
+	db *sqlx.DB,
+	features types.FeatureList,
+	entity types.Entity,
+	groupName string,
+	valueNames []string,
+	backendType types.BackendType,
+) (string, error) {
 	columnFormat, err := dbutil.GetColumnFormat(backendType)
 	if err != nil {
 		return "", err
@@ -45,7 +53,11 @@ func createTableJoined(ctx context.Context, db *sqlx.DB, features types.FeatureL
 		columnDefs = append(columnDefs, fmt.Sprintf(columnFormat, name, "TEXT"))
 	}
 	for _, f := range features {
-		columnDefs = append(columnDefs, fmt.Sprintf(columnFormat, f.Name, f.DBValueType))
+		dbValueType, err := dbutil.DBValueType(backendType, f.ValueType)
+		if err != nil {
+			return "", err
+		}
+		columnDefs = append(columnDefs, fmt.Sprintf(columnFormat, f.Name, dbValueType))
 	}
 	schema := `
 		CREATE TABLE %s (
@@ -67,7 +79,12 @@ func createTableJoined(ctx context.Context, db *sqlx.DB, features types.FeatureL
 	return tableName, nil
 }
 
-func PrepareEntityRowsTable(ctx context.Context, dbOpt dbutil.DBOpt, entity types.Entity, entityRows <-chan types.EntityRow, valueNames []string) (string, error) {
+func PrepareEntityRowsTable(ctx context.Context,
+	dbOpt dbutil.DBOpt,
+	entity types.Entity,
+	entityRows <-chan types.EntityRow,
+	valueNames []string,
+) (string, error) {
 	// Step 1: create entity_rows table
 	columnFormat, err := dbutil.GetColumnFormat(dbOpt.Backend)
 	if err != nil {
@@ -125,7 +142,12 @@ func PrepareEntityRowsTable(ctx context.Context, dbOpt dbutil.DBOpt, entity type
 	return tableName, nil
 }
 
-func insertEntityRows(ctx context.Context, dbOpt dbutil.DBOpt, tableName string, entityRows <-chan types.EntityRow, valueNames []string) error {
+func insertEntityRows(ctx context.Context,
+	dbOpt dbutil.DBOpt,
+	tableName string,
+	entityRows <-chan types.EntityRow,
+	valueNames []string,
+) error {
 	records := make([]interface{}, 0, InsertBatchSize)
 	columns := []string{"entity_key", "unix_milli"}
 	columns = append(columns, valueNames...)
diff --git a/internal/database/offline/sqlutil/table_schema.go b/internal/database/offline/sqlutil/table_schema.go
index 2d93506c..de37139a 100644
--- a/internal/database/offline/sqlutil/table_schema.go
+++ b/internal/database/offline/sqlutil/table_schema.go
@@ -5,12 +5,13 @@ import (
 	"fmt"
 
 	"github.com/jmoiron/sqlx"
+	"github.com/oom-ai/oomstore/internal/database/dbutil"
 	"github.com/oom-ai/oomstore/internal/database/offline"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
 )
 
 // SqlxTableSchema returns the schema of the given table.
-func SqlxTableSchema(ctx context.Context, store offline.Store, rows *sqlx.Rows) (*types.DataTableSchema, error) {
+func SqlxTableSchema(ctx context.Context, store offline.Store, backend types.BackendType, rows *sqlx.Rows) (*types.DataTableSchema, error) {
 	defer rows.Close()
 
 	var schema types.DataTableSchema
@@ -19,7 +20,7 @@ func SqlxTableSchema(ctx context.Context, store offline.Store, rows *sqlx.Rows)
 		if err := rows.Scan(&fieldName, &dbValueType); err != nil {
 			return nil, err
 		}
-		valueType, err := store.TypeTag(dbValueType)
+		valueType, err := dbutil.ValueType(backend, dbValueType)
 		if err != nil {
 			return nil, err
 		}
diff --git a/internal/database/offline/store.go b/internal/database/offline/store.go
index 1137161b..b09dc275 100644
--- a/internal/database/offline/store.go
+++ b/internal/database/offline/store.go
@@ -13,7 +13,6 @@ type Store interface {
 	Import(ctx context.Context, opt ImportOpt) (int64, error)
 
 	TableSchema(ctx context.Context, tableName string) (*types.DataTableSchema, error)
-	TypeTag(dbType string) (types.ValueType, error)
 
 	Ping(ctx context.Context) error
 	io.Closer
diff --git a/internal/database/online/redis/util.go b/internal/database/online/redis/util.go
index 0b6c11af..5483b3d3 100644
--- a/internal/database/online/redis/util.go
+++ b/internal/database/online/redis/util.go
@@ -123,7 +123,7 @@ func DeserializeByTag(i interface{}, valueType types.ValueType) (interface{}, er
 	case types.BYTES:
 		return []byte(s), nil
 	default:
-		return "", fmt.Errorf("unsupported type tag: %s", valueType)
+		return "", fmt.Errorf("unsupported value type: %s", valueType)
 	}
 }
 
diff --git a/oomcli/cmd/register_batch_feature.go b/oomcli/cmd/register_batch_feature.go
index 83b397ff..a2cdcbbc 100644
--- a/oomcli/cmd/register_batch_feature.go
+++ b/oomcli/cmd/register_batch_feature.go
@@ -9,6 +9,8 @@ import (
 )
 
 var registerBatchFeatureOpt types.CreateFeatureOpt
+var registerBatchFeatureValueType string
+
 var registerBatchFeatureCmd = &cobra.Command{
 	Use:   "batch-feature <feature_name>",
 	Short: "register a new batch feature",
@@ -35,8 +37,8 @@ func init() {
 	flags.StringVarP(&registerBatchFeatureOpt.GroupName, "group", "g", "", "feature group")
 	_ = registerBatchFeatureCmd.MarkFlagRequired("group")
 
-	flags.StringVarP(&registerBatchFeatureOpt.DBValueType, "db-value-type", "", "", "feature value type in database")
-	_ = registerBatchFeatureCmd.MarkFlagRequired("db-value-type")
+	flags.StringVarP(&registerBatchFeatureValueType, "value-type", "", "", "feature value type")
+	_ = registerBatchFeatureCmd.MarkFlagRequired("value-type")
 
 	flags.StringVar(&registerBatchFeatureOpt.Description, "description", "", "feature description")
 }
diff --git a/oomcli/cmd/types.go b/oomcli/cmd/types.go
index 66036d0d..e928a6b8 100644
--- a/oomcli/cmd/types.go
+++ b/oomcli/cmd/types.go
@@ -37,7 +37,6 @@ type FlattenFeature struct {
 	ValueType   types.ValueType `oomcli:"VALUE-TYPE"`
 	Description string          `oomcli:"DESCRIPTION,truncate"`
 
-	DBValueType      string    `oomcli:"DB-VALUE-TYPE,wide"`
 	OnlineRevisionID *int      `oomcli:"ONLINE-REVISION-ID,wide"`
 	CreateTime       time.Time `oomcli:"CREATE-TIME,wide"`
 	ModifyTime       time.Time `oomcli:"MODIFY-TIME,wide"`
@@ -86,7 +85,6 @@ func parseTokenLists(i interface{}) (headerTokens TokenList, dataTokens []TokenL
 				Category:         e.Group.Category,
 				ValueType:        e.ValueType,
 				Description:      e.Description,
-				DBValueType:      e.DBValueType,
 				OnlineRevisionID: e.OnlineRevisionID(),
 				CreateTime:       e.CreateTime,
 				ModifyTime:       e.ModifyTime,
diff --git a/pkg/oomstore/apply.go b/pkg/oomstore/apply.go
index 92a9fe01..00e4bd01 100644
--- a/pkg/oomstore/apply.go
+++ b/pkg/oomstore/apply.go
@@ -124,14 +124,13 @@ func (s *OomStore) applyFeature(ctx context.Context, tx metadata.DBStore, newFea
 		if !errdefs.IsNotFound(err) {
 			return err
 		}
-		valueType, err := s.offline.TypeTag(newFeature.DBValueType)
+		valueType, err := types.ParseValueType(newFeature.ValueType)
 		if err != nil {
 			return err
 		}
 		_, err = tx.CreateFeature(ctx, metadata.CreateFeatureOpt{
 			FeatureName: newFeature.Name,
 			GroupID:     group.ID,
-			DBValueType: newFeature.DBValueType,
 			Description: newFeature.Description,
 			ValueType:   valueType,
 		})
@@ -207,7 +206,7 @@ func buildApplyStage(ctx context.Context, opt apply.ApplyOpt) (*apply.ApplyStage
 					Kind:        "Feature",
 					Name:        feature.Name,
 					GroupName:   group.Name,
-					DBValueType: feature.DBValueType,
+					ValueType:   feature.ValueType,
 					Description: feature.Description,
 				})
 			}
@@ -319,7 +318,7 @@ func buildApplyFeature(feature apply.Feature, groupName string) apply.Feature {
 		Kind:        "Feature",
 		Name:        feature.Name,
 		GroupName:   groupName,
-		DBValueType: feature.DBValueType,
+		ValueType:   feature.ValueType,
 		Description: feature.Description,
 	}
 }
diff --git a/pkg/oomstore/feature.go b/pkg/oomstore/feature.go
index 326c5eb2..7604c5e6 100644
--- a/pkg/oomstore/feature.go
+++ b/pkg/oomstore/feature.go
@@ -62,15 +62,10 @@ func (s *OomStore) CreateBatchFeature(ctx context.Context, opt types.CreateFeatu
 		return 0, fmt.Errorf("expected batch feature group, got %s feature group", group.Category)
 	}
 
-	valueType, err := s.offline.TypeTag(opt.DBValueType)
-	if err != nil {
-		return 0, err
-	}
 	return s.metadata.CreateFeature(ctx, metadata.CreateFeatureOpt{
 		FeatureName: opt.FeatureName,
 		GroupID:     group.ID,
-		DBValueType: opt.DBValueType,
-		ValueType:   valueType,
+		ValueType:   opt.ValueType,
 		Description: opt.Description,
 	})
 }
diff --git a/pkg/oomstore/types/apply/apply.go b/pkg/oomstore/types/apply/apply.go
index fffe0951..d0e2d122 100644
--- a/pkg/oomstore/types/apply/apply.go
+++ b/pkg/oomstore/types/apply/apply.go
@@ -30,7 +30,7 @@ type Feature struct {
 	Kind        string `mapstructure:"kind" yaml:"kind,omitempty"`
 	Name        string `mapstructure:"name" yaml:"name"`
 	GroupName   string `mapstructure:"group-name" yaml:"group-name,omitempty"`
-	DBValueType string `mapstructure:"db-value-type" yaml:"db-value-type"`
+	ValueType   string `mapstructure:"value-type" yaml:"value-type"`
 	Description string `mapstructure:"description" yaml:"description"`
 }
 
@@ -38,7 +38,7 @@ func (f *Feature) Validate() error {
 	if f.Name == "" {
 		return errdefs.InvalidAttribute(fmt.Errorf("the name of feature should not be empty"))
 	}
-	if f.DBValueType == "" {
+	if f.ValueType == "" {
 		return errdefs.InvalidAttribute(fmt.Errorf("the db value type of feature should not be empty"))
 	}
 	return nil
@@ -65,7 +65,7 @@ func FromFeatureList(features types.FeatureList) FeatureItems {
 			Kind:        "Feature",
 			Name:        f.Name,
 			GroupName:   f.Group.Name,
-			DBValueType: f.DBValueType,
+			ValueType:   f.ValueType.String(),
 			Description: f.Description,
 		})
 	}
diff --git a/pkg/oomstore/types/entity.go b/pkg/oomstore/types/entity.go
index 5cffc606..f893b455 100644
--- a/pkg/oomstore/types/entity.go
+++ b/pkg/oomstore/types/entity.go
@@ -1,8 +1,6 @@
 package types
 
 import (
-	"fmt"
-	"strings"
 	"time"
 )
 
@@ -58,13 +56,3 @@ func (l *EntityList) Filter(filter func(*Entity) bool) (rs EntityList) {
 	}
 	return
 }
-
-func (e *Entity) String() string {
-	return strings.Join([]string{
-		fmt.Sprintf("Name:        %s", e.Name),
-		fmt.Sprintf("Length:      %d", e.Length),
-		fmt.Sprintf("Description: %s", e.Description),
-		fmt.Sprintf("CreateTime:  %s", e.CreateTime.Format(time.RFC3339)),
-		fmt.Sprintf("ModifyTime:  %s", e.ModifyTime.Format(time.RFC3339)),
-	}, "\n")
-}
diff --git a/pkg/oomstore/types/feature.go b/pkg/oomstore/types/feature.go
index 51bdf416..33270caa 100644
--- a/pkg/oomstore/types/feature.go
+++ b/pkg/oomstore/types/feature.go
@@ -1,16 +1,13 @@
 package types
 
 import (
-	"fmt"
-	"strings"
 	"time"
 )
 
 type Feature struct {
-	ID          int       `db:"id"`
-	Name        string    `db:"name"`
-	ValueType   ValueType `db:"value_type"`
-	DBValueType string    `db:"db_value_type"`
+	ID        int       `db:"id"`
+	Name      string    `db:"name"`
+	ValueType ValueType `db:"value_type"`
 
 	Description string    `db:"description"`
 	CreateTime  time.Time `db:"create_time"`
@@ -93,24 +90,3 @@ func (l FeatureList) GroupIDs() (ids []int) {
 	}
 	return ids
 }
-
-func (f *Feature) String() string {
-	onlineRevisionID := "<NULL>"
-
-	if f.OnlineRevisionID() != nil {
-		onlineRevisionID = fmt.Sprint(*f.OnlineRevisionID())
-	}
-
-	return strings.Join([]string{
-		fmt.Sprintf("Name:             %s", f.Name),
-		fmt.Sprintf("Group:            %s", f.Group.Name),
-		fmt.Sprintf("Entity:           %s", f.Entity().Name),
-		fmt.Sprintf("Category:         %s", f.Group.Category),
-		fmt.Sprintf("DBValueType:      %s", f.DBValueType),
-		fmt.Sprintf("ValueType:        %s", f.ValueType),
-		fmt.Sprintf("Description:      %s", f.Description),
-		fmt.Sprintf("OnlineRevisionID: %s", onlineRevisionID),
-		fmt.Sprintf("CreateTime:       %s", f.CreateTime.Format(time.RFC3339)),
-		fmt.Sprintf("ModifyTime:       %s", f.ModifyTime.Format(time.RFC3339)),
-	}, "\n")
-}
diff --git a/pkg/oomstore/types/group.go b/pkg/oomstore/types/group.go
index 731d0577..1b2a27e3 100644
--- a/pkg/oomstore/types/group.go
+++ b/pkg/oomstore/types/group.go
@@ -1,8 +1,6 @@
 package types
 
 import (
-	"fmt"
-	"strings"
 	"time"
 )
 
@@ -82,19 +80,3 @@ func (l GroupList) EntityIDs() []int {
 	}
 	return entityIDs
 }
-
-func (fg *Group) String() string {
-	onlineRevisionID := "<NULL>"
-
-	if fg.OnlineRevisionID != nil {
-		onlineRevisionID = fmt.Sprint(*fg.OnlineRevisionID)
-	}
-	return strings.Join([]string{
-		fmt.Sprintf("Name:             %s", fg.Name),
-		fmt.Sprintf("Entity:           %s", fg.Entity.Name),
-		fmt.Sprintf("Description:      %s", fg.Description),
-		fmt.Sprintf("OnlineRevisionID: %s", onlineRevisionID),
-		fmt.Sprintf("CreateTime:       %s", fg.CreateTime.Format(time.RFC3339)),
-		fmt.Sprintf("ModifyTime:       %s", fg.ModifyTime.Format(time.RFC3339)),
-	}, "\n")
-}
diff --git a/pkg/oomstore/types/options.go b/pkg/oomstore/types/options.go
index 4556d42b..02092cde 100644
--- a/pkg/oomstore/types/options.go
+++ b/pkg/oomstore/types/options.go
@@ -3,7 +3,7 @@ package types
 type CreateFeatureOpt struct {
 	FeatureName string
 	GroupName   string
-	DBValueType string
+	ValueType   ValueType
 	Description string
 }
 
diff --git a/pkg/oomstore/types/type_tag.go b/pkg/oomstore/types/type_tag.go
deleted file mode 100644
index d126c1cb..00000000
--- a/pkg/oomstore/types/type_tag.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package types
-
-import "strconv"
-
-type ValueType int
-
-const (
-	STRING ValueType = iota + 1
-	INT64
-	FLOAT64
-	BOOL
-	TIME
-	BYTES
-)
-
-func (t ValueType) String() string {
-	switch t {
-	case STRING:
-		return "string"
-	case INT64:
-		return "int64"
-	case FLOAT64:
-		return "float64"
-	case BOOL:
-		return "bool"
-	case TIME:
-		return "time"
-	case BYTES:
-		return "bytes"
-	default:
-		return "Unknown(" + strconv.Itoa(int(t)) + ")"
-	}
-}
diff --git a/pkg/oomstore/types/value_type.go b/pkg/oomstore/types/value_type.go
new file mode 100644
index 00000000..8e5f0acd
--- /dev/null
+++ b/pkg/oomstore/types/value_type.go
@@ -0,0 +1,63 @@
+package types
+
+import (
+	"fmt"
+	"strconv"
+
+	"github.com/oom-ai/oomstore/pkg/errdefs"
+)
+
+type ValueType int
+
+const (
+	INVALID ValueType = iota
+	STRING
+	INT64
+	FLOAT64
+	BOOL
+	TIME
+	BYTES
+)
+
+var allValueTypes = map[ValueType]string{
+	STRING:  "string",
+	INT64:   "int64",
+	FLOAT64: "float64",
+	BOOL:    "bool",
+	TIME:    "time",
+	BYTES:   "bytes",
+}
+
+func (t ValueType) String() string {
+	if s, ok := allValueTypes[t]; ok {
+		return s
+	} else {
+		return "Unknown(" + strconv.Itoa(int(t)) + ")"
+	}
+}
+
+func (v ValueType) Validate() error {
+	if _, ok := allValueTypes[v]; ok {
+		return nil
+	} else {
+		return errdefs.InvalidAttribute(fmt.Errorf("Invalid value type: %d", v))
+	}
+}
+
+func ParseValueType(s string) (ValueType, error) {
+	switch s {
+	case "string":
+		return BYTES, nil
+	case "int64":
+		return INT64, nil
+	case "float64":
+		return FLOAT64, nil
+	case "bool":
+		return BOOL, nil
+	case "time":
+		return TIME, nil
+	case "bytes":
+		return BYTES, nil
+	}
+	return INVALID, errdefs.InvalidAttribute(fmt.Errorf("Unknown value type: %s", s))
+}


Test patch:
diff --git a/internal/database/metadata/test_impl/feature.go b/internal/database/metadata/test_impl/feature.go
index bc3abdc5..15c2a76a 100644
--- a/internal/database/metadata/test_impl/feature.go
+++ b/internal/database/metadata/test_impl/feature.go
@@ -42,7 +42,6 @@ func TestCreateFeature(t *testing.T, prepareStore PrepareStoreFn) {
 	opt := metadata.CreateFeatureOpt{
 		FeatureName: "phone",
 		GroupID:     groupID,
-		DBValueType: "varchar(16)",
 		Description: "description",
 		ValueType:   types.STRING,
 	}
@@ -59,7 +58,7 @@ func TestCreateFeatureWithSameName(t *testing.T, prepareStore PrepareStoreFn) {
 	opt := metadata.CreateFeatureOpt{
 		FeatureName: "phone",
 		GroupID:     groupID,
-		DBValueType: "varchar(16)",
+		ValueType:   types.STRING,
 	}
 
 	_, err := store.CreateFeature(ctx, opt)
@@ -77,7 +76,7 @@ func TestCreateFeatureWithSQLKeyword(t *testing.T, prepareStore PrepareStoreFn)
 	opt := metadata.CreateFeatureOpt{
 		FeatureName: "user",
 		GroupID:     groupID,
-		DBValueType: "int",
+		ValueType:   types.INT64,
 		Description: "order",
 	}
 
@@ -93,7 +92,6 @@ func TestCreateFeatureWithInvalidDataType(t *testing.T, prepareStore PrepareStor
 	_, err := store.CreateFeature(ctx, metadata.CreateFeatureOpt{
 		FeatureName: "model",
 		GroupID:     groupID,
-		DBValueType: "invalid_type",
 	})
 	assert.Error(t, err)
 }
@@ -106,7 +104,6 @@ func TestGetFeature(t *testing.T, prepareStore PrepareStoreFn) {
 	id, err := store.CreateFeature(ctx, metadata.CreateFeatureOpt{
 		FeatureName: "phone",
 		GroupID:     groupID,
-		DBValueType: "varchar(16)",
 		Description: "description",
 		ValueType:   types.STRING,
 	})
@@ -123,7 +120,6 @@ func TestGetFeature(t *testing.T, prepareStore PrepareStoreFn) {
 		ID:          1,
 		Name:        "phone",
 		ValueType:   types.STRING,
-		DBValueType: "varchar(16)",
 		Description: "description",
 		GroupID:     1,
 	}
@@ -139,7 +135,6 @@ func TestGetFeatureByName(t *testing.T, prepareStore PrepareStoreFn) {
 	_, err := store.CreateFeature(ctx, metadata.CreateFeatureOpt{
 		FeatureName: "phone",
 		GroupID:     groupID,
-		DBValueType: "varchar(16)",
 		Description: "description",
 		ValueType:   types.STRING,
 	})
@@ -156,7 +151,6 @@ func TestGetFeatureByName(t *testing.T, prepareStore PrepareStoreFn) {
 		ID:          1,
 		Name:        "phone",
 		ValueType:   types.STRING,
-		DBValueType: "varchar(16)",
 		Description: "description",
 		GroupID:     1,
 	}
@@ -176,7 +170,6 @@ func TestCacheListFeature(t *testing.T, prepareStore PrepareStoreFn) {
 	featureID, err := store.CreateFeature(ctx, metadata.CreateFeatureOpt{
 		FeatureName: "phone",
 		GroupID:     groupID,
-		DBValueType: "varchar(16)",
 		Description: "description",
 		ValueType:   types.STRING,
 	})
@@ -234,7 +227,6 @@ func TestListFeature(t *testing.T, prepareStore PrepareStoreFn) {
 	featureID, err := store.CreateFeature(ctx, metadata.CreateFeatureOpt{
 		FeatureName: "phone",
 		GroupID:     groupID,
-		DBValueType: "varchar(16)",
 		Description: "description",
 		ValueType:   types.STRING,
 	})
@@ -292,7 +284,6 @@ func TestUpdateFeature(t *testing.T, prepareStore PrepareStoreFn) {
 	opt := metadata.CreateFeatureOpt{
 		FeatureName: "phone",
 		GroupID:     groupID,
-		DBValueType: "varchar(16)",
 		Description: "description",
 		ValueType:   types.STRING,
 	}
@@ -318,7 +309,6 @@ func TestUpdateFeature(t *testing.T, prepareStore PrepareStoreFn) {
 		ID:          1,
 		Name:        "phone",
 		ValueType:   types.STRING,
-		DBValueType: "varchar(16)",
 		Description: "new description",
 		GroupID:     1,
 	}
diff --git a/internal/database/offline/sqlutil/type_tag_test.go b/internal/database/offline/sqlutil/type_tag_test.go
index 5c4172c1..84c17905 100644
--- a/internal/database/offline/sqlutil/type_tag_test.go
+++ b/internal/database/offline/sqlutil/type_tag_test.go
@@ -16,7 +16,7 @@ var typeMap = map[string]types.ValueType{
 	"datetime": types.TIME,
 }
 
-func TestTypeTag(t *testing.T) {
+func TestValueType(t *testing.T) {
 	for _, tt := range []struct {
 		input    string
 		expected types.ValueType
diff --git a/internal/database/offline/test_impl/export.go b/internal/database/offline/test_impl/export.go
index e4f16395..c05c517f 100644
--- a/internal/database/offline/test_impl/export.go
+++ b/internal/database/offline/test_impl/export.go
@@ -17,14 +17,12 @@ func TestExport(t *testing.T, prepareStore PrepareStoreFn) {
 	dataTable := "offline_1_1"
 	features := []*types.Feature{
 		{
-			Name:        "model",
-			DBValueType: "VARCHAR(32)",
-			ValueType:   types.STRING,
+			Name:      "model",
+			ValueType: types.STRING,
 		},
 		{
-			Name:        "price",
-			DBValueType: "INT",
-			ValueType:   types.INT64,
+			Name:      "price",
+			ValueType: types.INT64,
 		},
 	}
 	buildTestDataTable(ctx, t, store, features, dataTable, &offline.CSVSource{
diff --git a/internal/database/offline/test_impl/join.go b/internal/database/offline/test_impl/join.go
index c9d03450..fa61d3d2 100644
--- a/internal/database/offline/test_impl/join.go
+++ b/internal/database/offline/test_impl/join.go
@@ -117,22 +117,19 @@ func TestJoin(t *testing.T, prepareStore PrepareStoreFn) {
 
 func prepareFeatures(oneGroup bool) (types.FeatureList, map[string]types.FeatureList) {
 	price := &types.Feature{
-		Name:        "price",
-		DBValueType: "INT",
-		ValueType:   types.INT64,
-		GroupID:     1,
+		Name:      "price",
+		ValueType: types.INT64,
+		GroupID:   1,
 	}
 	model := &types.Feature{
-		Name:        "model",
-		DBValueType: "VARCHAR(32)",
-		ValueType:   types.STRING,
-		GroupID:     1,
+		Name:      "model",
+		ValueType: types.STRING,
+		GroupID:   1,
 	}
 	isActive := &types.Feature{
-		Name:        "is_active",
-		DBValueType: "boolean",
-		ValueType:   types.BOOL,
-		GroupID:     2,
+		Name:      "is_active",
+		ValueType: types.BOOL,
+		GroupID:   2,
 	}
 
 	if oneGroup {
diff --git a/internal/database/online/test_impl/util.go b/internal/database/online/test_impl/util.go
index 0954f77d..4fec8ef7 100644
--- a/internal/database/online/test_impl/util.go
+++ b/internal/database/online/test_impl/util.go
@@ -33,39 +33,34 @@ func init() {
 		SampleSmall = Sample{
 			Features: types.FeatureList{
 				&types.Feature{
-					ID:          1,
-					Name:        "age",
-					GroupID:     1,
-					ValueType:   types.INT64,
-					DBValueType: "int",
+					ID:        1,
+					Name:      "age",
+					GroupID:   1,
+					ValueType: types.INT64,
 				},
 				&types.Feature{
-					ID:          2,
-					Name:        "gender",
-					GroupID:     1,
-					ValueType:   types.STRING,
-					DBValueType: "varchar(1)",
+					ID:        2,
+					Name:      "gender",
+					GroupID:   1,
+					ValueType: types.STRING,
 				},
 				&types.Feature{
-					ID:          3,
-					Name:        "account",
-					GroupID:     1,
-					ValueType:   types.FLOAT64,
-					DBValueType: "real",
+					ID:        3,
+					Name:      "account",
+					GroupID:   1,
+					ValueType: types.FLOAT64,
 				},
 				&types.Feature{
-					ID:          4,
-					Name:        "is_active",
-					GroupID:     1,
-					ValueType:   types.BOOL,
-					DBValueType: "bool",
+					ID:        4,
+					Name:      "is_active",
+					GroupID:   1,
+					ValueType: types.BOOL,
 				},
 				&types.Feature{
-					ID:          5,
-					Name:        "register_time",
-					GroupID:     1,
-					ValueType:   types.TIME,
-					DBValueType: "timestamp",
+					ID:        5,
+					Name:      "register_time",
+					GroupID:   1,
+					ValueType: types.TIME,
 				},
 			},
 			Revision: &types.Revision{ID: 3, GroupID: 1},
@@ -82,11 +77,10 @@ func init() {
 	{
 		features := types.FeatureList{
 			&types.Feature{
-				ID:          2,
-				Name:        "charge",
-				GroupID:     2,
-				ValueType:   types.FLOAT64,
-				DBValueType: "float",
+				ID:        2,
+				Name:      "charge",
+				GroupID:   2,
+				ValueType: types.FLOAT64,
 			},
 		}
 
diff --git a/pkg/oomstore/apply_test.go b/pkg/oomstore/apply_test.go
index 179407ba..e4b88e6d 100644
--- a/pkg/oomstore/apply_test.go
+++ b/pkg/oomstore/apply_test.go
@@ -86,14 +86,14 @@ kind: Feature
 name: model
 group-name: device
 category: batch
-db-value-type: varchar(16)
+value-type: string
 description: 'description'
 ---
 kind: Feature
 name: price
 group-name: device
 category: batch
-db-value-type: int
+value-type: int64
 description: 'description'
 `)},
 			wantStage: &apply.ApplyStage{
@@ -127,14 +127,14 @@ description: 'description'
 						Kind:        "Feature",
 						Name:        "model",
 						GroupName:   "device",
-						DBValueType: "varchar(16)",
+						ValueType:   "string",
 						Description: "description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "price",
 						GroupName:   "device",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "description",
 					},
 				},
@@ -151,10 +151,10 @@ category: batch
 description: 'description'
 features:
 - name: model
-  db-value-type: varchar(16)
+  value-type: string
   description: 'description'
 - name: price
-  db-value-type: int
+  value-type: int64
   description: 'description'
 `)},
 			wantStage: &apply.ApplyStage{
@@ -174,7 +174,7 @@ features:
 						Kind:        "Feature",
 						Name:        "model",
 						GroupName:   "device",
-						DBValueType: "varchar(16)",
+						ValueType:   "string",
 						Description: "description",
 					},
 					{
@@ -182,7 +182,7 @@ features:
 						Kind:        "Feature",
 						Name:        "price",
 						GroupName:   "device",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "description",
 					},
 				},
@@ -201,19 +201,19 @@ batch-features:
   description: description
   features:
   - name: model
-    db-value-type: varchar(16)
+    value-type: string
     description: 'description'
   - name: price
-    db-value-type: int
+    value-type: int64
     description: 'description'
 - group: user
   description: description
   features:
   - name: age
-    db-value-type: int
+    value-type: int64
     description: 'description'
   - name: gender
-    db-value-type: int
+    value-type: int64
     description: 'description'
 `)},
 			wantStage: &apply.ApplyStage{
@@ -246,7 +246,7 @@ batch-features:
 						Kind:        "Feature",
 						Name:        "model",
 						GroupName:   "device",
-						DBValueType: "varchar(16)",
+						ValueType:   "string",
 						Description: "description",
 					},
 					{
@@ -254,21 +254,21 @@ batch-features:
 						Kind:        "Feature",
 						Name:        "price",
 						GroupName:   "device",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "age",
 						GroupName:   "user",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "gender",
 						GroupName:   "user",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "description",
 					},
 				},
@@ -282,27 +282,27 @@ items:
     - kind: Feature
       name: credit_score
       group-name: account
-      db-value-type: int
+      value-type: int64
       description: "credit_score description"
     - kind: Feature
       name: account_age_days
       group-name: account
-      db-value-type: int
+      value-type: int64
       description: "account_age_days description"
     - kind: Feature
       name: has_2fa_installed
       group-name: account
-      db-value-type: bool
+      value-type: bool
       description: "has_2fa_installed description"
     - kind: Feature
       name: transaction_count_7d
       group-name: transaction_stats
-      db-value-type: int
+      value-type: int64
       description: "transaction_count_7d description"
     - kind: Feature
       name: transaction_count_30d
       group-name: transaction_stats
-      db-value-type: int
+      value-type: int64
       description: "transaction_count_30d description"
 `),
 			},
@@ -314,35 +314,35 @@ items:
 						Kind:        "Feature",
 						Name:        "credit_score",
 						GroupName:   "account",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "credit_score description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "account_age_days",
 						GroupName:   "account",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "account_age_days description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "has_2fa_installed",
 						GroupName:   "account",
-						DBValueType: "bool",
+						ValueType:   "bool",
 						Description: "has_2fa_installed description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "transaction_count_7d",
 						GroupName:   "transaction_stats",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "transaction_count_7d description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "transaction_count_30d",
 						GroupName:   "transaction_stats",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "transaction_count_30d description",
 					},
 				},
@@ -360,13 +360,13 @@ items:
       description: user account info
       features:
         - name: credit_score
-          db-value-type: int
+          value-type: int64
           description: credit_score description
         - name: account_age_days
-          db-value-type: int
+          value-type: int64
           description: account_age_days description
         - name: has_2fa_installed
-          db-value-type: bool
+          value-type: bool
           description: has_2fa_installed description
     - kind: Group
       name: transaction_stats
@@ -375,10 +375,10 @@ items:
       description: user transaction statistics
       features:
         - name: transaction_count_7d
-          db-value-type: int
+          value-type: int64
           description: transaction_count_7d description
         - name: transaction_count_30d
-          db-value-type: int
+          value-type: int64
           description: transaction_count_30d description
 `),
 			},
@@ -407,35 +407,35 @@ items:
 						Kind:        "Feature",
 						Name:        "credit_score",
 						GroupName:   "account",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "credit_score description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "account_age_days",
 						GroupName:   "account",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "account_age_days description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "has_2fa_installed",
 						GroupName:   "account",
-						DBValueType: "bool",
+						ValueType:   "bool",
 						Description: "has_2fa_installed description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "transaction_count_7d",
 						GroupName:   "transaction_stats",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "transaction_count_7d description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "transaction_count_30d",
 						GroupName:   "transaction_stats",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "transaction_count_30d description",
 					},
 				},
@@ -456,22 +456,22 @@ items:
           description: user account info
           features:
             - name: credit_score
-              db-value-type: int
+              value-type: int64
               description: credit_score description
             - name: account_age_days
-              db-value-type: int
+              value-type: int64
               description: account_age_days description
             - name: has_2fa_installed
-              db-value-type: bool
+              value-type: bool
               description: has_2fa_installed description
         - group: transaction_stats
           description: user transaction statistics
           features:
             - name: transaction_count_7d
-              db-value-type: int
+              value-type: int64
               description: transaction_count_7d description
             - name: transaction_count_30d
-              db-value-type: int
+              value-type: int64
               description: transaction_count_30d description
     - kind: Entity
       name: device
@@ -482,10 +482,10 @@ items:
           description: phone info
           features:
             - name: model
-              db-value-type: varchar(32)
+              value-type: string
               description: model description
             - name: price
-              db-value-type: int
+              value-type: int64
               description: price description
 `),
 			},
@@ -535,49 +535,49 @@ items:
 						Kind:        "Feature",
 						Name:        "credit_score",
 						GroupName:   "account",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "credit_score description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "account_age_days",
 						GroupName:   "account",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "account_age_days description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "has_2fa_installed",
 						GroupName:   "account",
-						DBValueType: "bool",
+						ValueType:   "bool",
 						Description: "has_2fa_installed description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "transaction_count_7d",
 						GroupName:   "transaction_stats",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "transaction_count_7d description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "transaction_count_30d",
 						GroupName:   "transaction_stats",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "transaction_count_30d description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "model",
 						GroupName:   "phone",
-						DBValueType: "varchar(32)",
+						ValueType:   "string",
 						Description: "model description",
 					},
 					{
 						Kind:        "Feature",
 						Name:        "price",
 						GroupName:   "phone",
-						DBValueType: "int",
+						ValueType:   "int64",
 						Description: "price description",
 					},
 				},
diff --git a/pkg/oomstore/export_test.go b/pkg/oomstore/export_test.go
index 9b3984cd..bedcfefd 100644
--- a/pkg/oomstore/export_test.go
+++ b/pkg/oomstore/export_test.go
@@ -27,12 +27,12 @@ func TestChannelExport(t *testing.T) {
 	revisionID := 5
 	features := types.FeatureList{
 		{
-			Name:        "model",
-			DBValueType: "VARCHAR(32)",
+			Name:      "model",
+			ValueType: types.STRING,
 		},
 		{
-			Name:        "price",
-			DBValueType: "INT",
+			Name:      "price",
+			ValueType: types.INT64,
 		},
 	}
 
diff --git a/pkg/oomstore/feature_test.go b/pkg/oomstore/feature_test.go
index 5c8cabd8..d22a34ee 100644
--- a/pkg/oomstore/feature_test.go
+++ b/pkg/oomstore/feature_test.go
@@ -36,7 +36,7 @@ func TestCreateBatchFeature(t *testing.T) {
 			opt: types.CreateFeatureOpt{
 				FeatureName: "model",
 				GroupName:   "device_info",
-				DBValueType: "VARCHAR(32)",
+				ValueType:   types.STRING,
 			},
 			valueType: types.STRING,
 			group: types.Group{
@@ -51,7 +51,7 @@ func TestCreateBatchFeature(t *testing.T) {
 			opt: types.CreateFeatureOpt{
 				FeatureName: "model",
 				GroupName:   "device_info",
-				DBValueType: "BIGINT",
+				ValueType:   types.INT64,
 			},
 			valueType: types.INT64,
 			group: types.Group{
@@ -72,11 +72,9 @@ func TestCreateBatchFeature(t *testing.T) {
 				metadataOpt := metadata.CreateFeatureOpt{
 					FeatureName: tc.opt.FeatureName,
 					GroupID:     tc.group.ID,
-					DBValueType: tc.opt.DBValueType,
 					ValueType:   tc.valueType,
 					Description: tc.opt.Description,
 				}
-				offlineStore.EXPECT().TypeTag(tc.opt.DBValueType).Return(tc.valueType, nil)
 				metadataStore.EXPECT().CreateFeature(ctx, metadataOpt).Return(0, nil)
 			}
 			_, err := store.CreateBatchFeature(ctx, tc.opt)
diff --git a/pkg/oomstore/online_query_test.go b/pkg/oomstore/online_query_test.go
index 73e9ff3d..49b8eda0 100644
--- a/pkg/oomstore/online_query_test.go
+++ b/pkg/oomstore/online_query_test.go
@@ -216,9 +216,9 @@ func prepareFeatures(isConsistent bool, isAvailable bool) types.FeatureList {
 	}
 	features := types.FeatureList{
 		{
-			Name:        "model",
-			DBValueType: "VARCHAR(32)",
-			GroupID:     1,
+			Name:      "model",
+			ValueType: types.STRING,
+			GroupID:   1,
 			Group: &types.Group{
 				EntityID:         1,
 				OnlineRevisionID: &revision1,
@@ -227,9 +227,9 @@ func prepareFeatures(isConsistent bool, isAvailable bool) types.FeatureList {
 			},
 		},
 		{
-			Name:        "price",
-			DBValueType: "INT",
-			GroupID:     2,
+			Name:      "price",
+			ValueType: types.INT64,
+			GroupID:   2,
 			Group: &types.Group{
 				EntityID:         1,
 				OnlineRevisionID: &revision2,
@@ -238,9 +238,9 @@ func prepareFeatures(isConsistent bool, isAvailable bool) types.FeatureList {
 			},
 		},
 		{
-			Name:        "age",
-			DBValueType: "INT",
-			GroupID:     3,
+			Name:      "age",
+			ValueType: types.INT64,
+			GroupID:   3,
 			Group: &types.Group{
 				EntityID:         2,
 				OnlineRevisionID: &revision2,