Repository: oom-ai/oomstore
Pull request #905 (2022-01-05 10:30:09)

feat(metadata): error add stack trace
relate #903 

codecov[bot]: # [Codecov](https://codecov.io/gh/oom-ai/oomstore/pull/905?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai) Report
> Merging [#905](https://codecov.io/gh/oom-ai/oomstore/pull/905?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai) (b1717ae) into [main](https://codecov.io/gh/oom-ai/oomstore/commit/5a01bb788736ca8349b81327a62cb8c280441a3a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai) (5a01bb7) will **decrease** coverage by `0.04%`.
> The diff coverage is `34.66%`.

[![Impacted file tree graph](https://codecov.io/gh/oom-ai/oomstore/pull/905/graphs/tree.svg?width=650&height=150&src=pr&token=C59L7LTRM4&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai)](https://codecov.io/gh/oom-ai/oomstore/pull/905?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai)

```diff
@@            Coverage Diff             @@
##             main     #905      +/-   ##
==========================================
- Coverage   51.16%   51.12%   -0.05%     
==========================================
  Files          84       84              
  Lines        3565     3566       +1     
==========================================
- Hits         1824     1823       -1     
- Misses       1524     1526       +2     
  Partials      217      217              
```


| [Impacted Files](https://codecov.io/gh/oom-ai/oomstore/pull/905?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai) | Coverage Δ | |
|---|---|---|
| [internal/database/dbutil/database.go](https://codecov.io/gh/oom-ai/oomstore/pull/905/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai#diff-aW50ZXJuYWwvZGF0YWJhc2UvZGJ1dGlsL2RhdGFiYXNlLmdv) | `0.00% <0.00%> (ø)` | |
| [internal/database/dbutil/sql.go](https://codecov.io/gh/oom-ai/oomstore/pull/905/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai#diff-aW50ZXJuYWwvZGF0YWJhc2UvZGJ1dGlsL3NxbC5nbw==) | `20.31% <0.00%> (ø)` | |
| [...rnal/database/metadata/postgres/create\_database.go](https://codecov.io/gh/oom-ai/oomstore/pull/905/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai#diff-aW50ZXJuYWwvZGF0YWJhc2UvbWV0YWRhdGEvcG9zdGdyZXMvY3JlYXRlX2RhdGFiYXNlLmdv) | `40.90% <0.00%> (ø)` | |
| [internal/database/metadata/postgres/store.go](https://codecov.io/gh/oom-ai/oomstore/pull/905/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai#diff-aW50ZXJuYWwvZGF0YWJhc2UvbWV0YWRhdGEvcG9zdGdyZXMvc3RvcmUuZ28=) | `42.30% <0.00%> (ø)` | |
| [...ternal/database/metadata/sqlite/create\_database.go](https://codecov.io/gh/oom-ai/oomstore/pull/905/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai#diff-aW50ZXJuYWwvZGF0YWJhc2UvbWV0YWRhdGEvc3FsaXRlL2NyZWF0ZV9kYXRhYmFzZS5nbw==) | `43.47% <0.00%> (ø)` | |
| [internal/database/metadata/sqlite/store.go](https://codecov.io/gh/oom-ai/oomstore/pull/905/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai#diff-aW50ZXJuYWwvZGF0YWJhc2UvbWV0YWRhdGEvc3FsaXRlL3N0b3JlLmdv) | `42.30% <0.00%> (ø)` | |
| [internal/database/metadata/mysql/store.go](https://codecov.io/gh/oom-ai/oomstore/pull/905/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai#diff-aW50ZXJuYWwvZGF0YWJhc2UvbWV0YWRhdGEvbXlzcWwvc3RvcmUuZ28=) | `46.66% <11.11%> (ø)` | |
| [internal/database/metadata/mysql/revision.go](https://codecov.io/gh/oom-ai/oomstore/pull/905/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai#diff-aW50ZXJuYWwvZGF0YWJhc2UvbWV0YWRhdGEvbXlzcWwvcmV2aXNpb24uZ28=) | `54.28% <16.66%> (ø)` | |
| [internal/database/metadata/sqlite/revision.go](https://codecov.io/gh/oom-ai/oomstore/pull/905/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai#diff-aW50ZXJuYWwvZGF0YWJhc2UvbWV0YWRhdGEvc3FsaXRlL3JldmlzaW9uLmdv) | `54.28% <16.66%> (ø)` | |
| [internal/database/metadata/postgres/revision.go](https://codecov.io/gh/oom-ai/oomstore/pull/905/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai#diff-aW50ZXJuYWwvZGF0YWJhc2UvbWV0YWRhdGEvcG9zdGdyZXMvcmV2aXNpb24uZ28=) | `58.06% <20.00%> (ø)` | |
| ... and [10 more](https://codecov.io/gh/oom-ai/oomstore/pull/905/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/oom-ai/oomstore/pull/905?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/oom-ai/oomstore/pull/905?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai). Last update [5a01bb7...b1717ae](https://codecov.io/gh/oom-ai/oomstore/pull/905?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oom-ai).


Patch:
diff --git a/README.md b/README.md
index a5909324..311a328c 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,7 @@ metadata-store: *pg
 ```yaml
 kind: Entity
 name: user
+length: 8
 description: 'user ID'
 groups:
 - name: account
diff --git a/internal/database/dbutil/database.go b/internal/database/dbutil/database.go
index 7bfd41d3..b52e3311 100644
--- a/internal/database/dbutil/database.go
+++ b/internal/database/dbutil/database.go
@@ -9,12 +9,14 @@ import (
 	"github.com/lib/pq"
 	"github.com/mattn/go-sqlite3"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
+	"github.com/pkg/errors"
 	"github.com/snowflakedb/gosnowflake"
 	"google.golang.org/api/googleapi"
 )
 
 func OpenSQLite(dbFile string) (*sqlx.DB, error) {
-	return sqlx.Open("sqlite3", dbFile)
+	db, err := sqlx.Open("sqlite3", dbFile)
+	return db, errors.WithStack(err)
 }
 
 func OpenMysqlDB(host, port, user, password, database string) (*sqlx.DB, error) {
@@ -25,11 +27,12 @@ func OpenMysqlDB(host, port, user, password, database string) (*sqlx.DB, error)
 	cfg.DBName = database
 	cfg.ParseTime = true
 
-	return sqlx.Open("mysql", cfg.FormatDSN())
+	db, err := sqlx.Open("mysql", cfg.FormatDSN())
+	return db, errors.WithStack(err)
 }
 
 func OpenPostgresDB(host, port, user, password, database string) (*sqlx.DB, error) {
-	return sqlx.Open(
+	db, err := sqlx.Open(
 		"postgres",
 		fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
 			user,
@@ -38,6 +41,7 @@ func OpenPostgresDB(host, port, user, password, database string) (*sqlx.DB, erro
 			port,
 			database),
 	)
+	return db, errors.WithStack(err)
 }
 
 var OpenRedshiftDB = OpenPostgresDB
diff --git a/internal/database/dbutil/schema.go b/internal/database/dbutil/schema.go
index b23ae56c..82ffe28d 100644
--- a/internal/database/dbutil/schema.go
+++ b/internal/database/dbutil/schema.go
@@ -80,10 +80,10 @@ func parseColumns(entity *types.Entity, isCDC bool, features types.FeatureList,
 	{
 		c := Column{Name: entity.Name, ValueType: types.String}
 		switch backend {
-		case types.BackendCassandra, types.BackendSQLite, types.BackendPostgres, types.BackendRedshift, types.BackendSnowflake:
+		case types.BackendCassandra, types.BackendSQLite:
 			c.DbType = "TEXT"
-		case types.BackendMySQL:
-			c.DbType = "VARCHAR(255)"
+		case types.BackendPostgres, types.BackendRedshift, types.BackendSnowflake, types.BackendMySQL:
+			c.DbType = fmt.Sprintf(`VARCHAR(%d)`, entity.Length)
 		case types.BackendBigQuery:
 			c.DbType = "STRING"
 		default:
diff --git a/internal/database/dbutil/sql.go b/internal/database/dbutil/sql.go
index 1b17a736..4322e786 100644
--- a/internal/database/dbutil/sql.go
+++ b/internal/database/dbutil/sql.go
@@ -6,6 +6,8 @@ import (
 	"strings"
 
 	"github.com/jmoiron/sqlx"
+	"github.com/pkg/errors"
+
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
 )
 
@@ -19,7 +21,7 @@ func BuildConditions(equal map[string]interface{}, in map[string]interface{}) ([
 	for key, value := range in {
 		s, inArgs, err := sqlx.In(fmt.Sprintf("%s IN (?)", key), value)
 		if err != nil {
-			return nil, nil, err
+			return nil, nil, errors.WithStack(err)
 		}
 		cond = append(cond, s)
 		args = append(args, inArgs...)
diff --git a/internal/database/metadata/mysql/entity.go b/internal/database/metadata/mysql/entity.go
index 80db89fe..79fe2120 100644
--- a/internal/database/metadata/mysql/entity.go
+++ b/internal/database/metadata/mysql/entity.go
@@ -2,27 +2,27 @@ package mysql
 
 import (
 	"context"
-	"fmt"
 
 	"github.com/go-sql-driver/mysql"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
+	"github.com/pkg/errors"
 )
 
 func createEntity(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.CreateEntityOpt) (int, error) {
-	query := "INSERT INTO entity(name, description) VALUES(?, ?)"
-	res, err := sqlxCtx.ExecContext(ctx, query, opt.EntityName, opt.Description)
+	query := "INSERT INTO entity(name, length, description) VALUES(?, ?, ?)"
+	res, err := sqlxCtx.ExecContext(ctx, query, opt.EntityName, opt.Length, opt.Description)
 	if err != nil {
 		if er, ok := err.(*mysql.MySQLError); ok {
 			if er.Number == ER_DUP_ENTRY {
-				return 0, fmt.Errorf("entity %s already exists", opt.EntityName)
+				return 0, errors.Errorf("entity %s already exists", opt.EntityName)
 			}
 		}
-		return 0, err
+		return 0, errors.WithStack(err)
 	}
 
 	entityID, err := res.LastInsertId()
 	if err != nil {
-		return 0, err
+		return 0, errors.WithStack(err)
 	}
-	return int(entityID), err
+	return int(entityID), nil
 }
diff --git a/internal/database/metadata/mysql/feature.go b/internal/database/metadata/mysql/feature.go
index a9d2d213..bb739a86 100644
--- a/internal/database/metadata/mysql/feature.go
+++ b/internal/database/metadata/mysql/feature.go
@@ -2,7 +2,8 @@ package mysql
 
 import (
 	"context"
-	"fmt"
+
+	"github.com/pkg/errors"
 
 	"github.com/go-sql-driver/mysql"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
@@ -10,14 +11,14 @@ import (
 
 func createFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.CreateFeatureOpt) (int, error) {
 	if err := opt.ValueType.Validate(); err != nil {
-		return 0, err
+		return 0, errors.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)
 	if err != nil {
 		if er, ok := err.(*mysql.MySQLError); ok {
 			if er.Number == ER_DUP_ENTRY {
-				return 0, fmt.Errorf("feature %s already exists", opt.FeatureName)
+				return 0, errors.Errorf("feature %s already exists", opt.FeatureName)
 			}
 		}
 		return 0, err
@@ -25,7 +26,7 @@ func createFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metada
 
 	featureID, err := res.LastInsertId()
 	if err != nil {
-		return 0, err
+		return 0, errors.WithStack(err)
 	}
-	return int(featureID), err
+	return int(featureID), nil
 }
diff --git a/internal/database/metadata/mysql/group.go b/internal/database/metadata/mysql/group.go
index 4d255059..9c035ddb 100644
--- a/internal/database/metadata/mysql/group.go
+++ b/internal/database/metadata/mysql/group.go
@@ -2,7 +2,8 @@ package mysql
 
 import (
 	"context"
-	"fmt"
+
+	"github.com/pkg/errors"
 
 	"github.com/go-sql-driver/mysql"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
@@ -12,7 +13,7 @@ import (
 
 func createGroup(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.CreateGroupOpt) (int, error) {
 	if opt.Category != types.CategoryBatch && opt.Category != types.CategoryStream {
-		return 0, errdefs.InvalidAttribute(fmt.Errorf("illegal category '%s', should be either 'stream' or 'batch'", opt.Category))
+		return 0, errdefs.InvalidAttribute(errors.Errorf("illegal category '%s', should be either 'stream' or 'batch'", opt.Category))
 	}
 
 	query := "INSERT INTO feature_group(name, entity_id, category, description) VALUES(?, ?, ?, ?)"
@@ -20,16 +21,16 @@ func createGroup(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata
 	if err != nil {
 		if er, ok := err.(*mysql.MySQLError); ok {
 			if er.Number == ER_DUP_ENTRY {
-				return 0, fmt.Errorf("feature group %s already exists", opt.GroupName)
+				return 0, errors.Errorf("feature group %s already exists", opt.GroupName)
 			}
 		}
-		return 0, err
+		return 0, errors.WithStack(err)
 	}
 
 	groupID, err := res.LastInsertId()
 	if err != nil {
-		return 0, err
+		return 0, errors.WithStack(err)
 	}
 
-	return int(groupID), err
+	return int(groupID), nil
 }
diff --git a/internal/database/metadata/mysql/revision.go b/internal/database/metadata/mysql/revision.go
index d6bd23d3..63a11c9e 100644
--- a/internal/database/metadata/mysql/revision.go
+++ b/internal/database/metadata/mysql/revision.go
@@ -2,9 +2,9 @@ package mysql
 
 import (
 	"context"
-	"fmt"
 
 	"github.com/go-sql-driver/mysql"
+	"github.com/pkg/errors"
 
 	"github.com/oom-ai/oomstore/internal/database/dbutil"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
@@ -24,14 +24,14 @@ func createRevision(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metad
 	if err != nil {
 		if er, ok := err.(*mysql.MySQLError); ok {
 			if er.Number == ER_DUP_ENTRY {
-				return 0, "", fmt.Errorf("revision already exists: groupID=%d, revision=%d", opt.GroupID, opt.Revision)
+				return 0, "", errors.Errorf("revision already exists: groupID=%d, revision=%d", opt.GroupID, opt.Revision)
 			}
 		}
-		return 0, "", err
+		return 0, "", errors.WithStack(err)
 	}
 	revisionID, err := res.LastInsertId()
 	if err != nil {
-		return 0, "", err
+		return 0, "", errors.WithStack(err)
 	}
 
 	if opt.SnapshotTable == nil {
@@ -39,14 +39,14 @@ func createRevision(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metad
 		snapshotTable = dbutil.OfflineBatchTableName(opt.GroupID, revisionID)
 		result, err := sqlxCtx.ExecContext(ctx, sqlxCtx.Rebind(updateQuery), snapshotTable, revisionID)
 		if err != nil {
-			return 0, "", err
+			return 0, "", errors.WithStack(err)
 		}
 		rowsAffected, err := result.RowsAffected()
 		if err != nil {
-			return 0, "", err
+			return 0, "", errors.WithStack(err)
 		}
 		if rowsAffected != 1 {
-			return 0, "", fmt.Errorf("failed to update revision %d: revision not found", revisionID)
+			return 0, "", errors.Errorf("failed to update revision %d: revision not found", revisionID)
 		}
 	}
 	return int(revisionID), snapshotTable, nil
diff --git a/internal/database/metadata/mysql/schema.go b/internal/database/metadata/mysql/schema.go
index b50f2a92..56319a8c 100644
--- a/internal/database/metadata/mysql/schema.go
+++ b/internal/database/metadata/mysql/schema.go
@@ -32,6 +32,7 @@ var META_TABLE_SCHEMAS = map[string]string{
 		CREATE TABLE entity (
 			id				INT 			NOT	NULL AUTO_INCREMENT PRIMARY KEY,
 			name        	VARCHAR(32) 	NOT	NULL,
+			length      	SMALLINT    	NOT	NULL COMMENT "feature entity value max length",
 			description 	VARCHAR(64) 	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/mysql/store.go b/internal/database/metadata/mysql/store.go
index 3d58f5f1..7edb9bc5 100644
--- a/internal/database/metadata/mysql/store.go
+++ b/internal/database/metadata/mysql/store.go
@@ -11,6 +11,7 @@ import (
 	"github.com/oom-ai/oomstore/internal/database/metadata/informer"
 	"github.com/oom-ai/oomstore/internal/database/metadata/sqlutil"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
+	"github.com/pkg/errors"
 )
 
 var _ metadata.Store = &DB{}
@@ -31,10 +32,10 @@ func (db *DB) Ping(ctx context.Context) error {
 
 func (db *DB) Close() error {
 	if err := db.Informer.Close(); err != nil {
-		return err
+		return errors.WithStack(err)
 	}
 	if err := db.DB.Close(); err != nil {
-		return err
+		return errors.WithStack(err)
 	}
 	return nil
 }
@@ -59,20 +60,20 @@ func Open(ctx context.Context, option *types.MySQLOpt) (*DB, error) {
 	}, nil
 }
 
-func CreateDatabase(ctx context.Context, opt *types.MySQLOpt) (err error) {
+func CreateDatabase(ctx context.Context, opt *types.MySQLOpt) error {
 	defaultDB, err := dbutil.OpenMysqlDB(opt.Host, opt.Port, opt.User, opt.Password, "")
 	if err != nil {
-		return
+		return errors.WithStack(err)
 	}
 	defer defaultDB.Close()
 
 	if _, err = defaultDB.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", opt.Database)); err != nil {
-		return
+		return errors.WithStack(err)
 	}
 
 	db, err := dbutil.OpenMysqlDB(opt.Host, opt.Port, opt.User, opt.Password, opt.Database)
 	if err != nil {
-		return
+		return errors.WithStack(err)
 	}
 	defer db.Close()
 	return createMetaSchemas(ctx, db)
@@ -85,21 +86,21 @@ func createMetaSchemas(ctx context.Context, db *sqlx.DB) (err error) {
 		// create meta tables
 		for _, schema := range META_TABLE_SCHEMAS {
 			if _, err = tx.ExecContext(ctx, schema); err != nil {
-				return fmt.Errorf("failed to create table, err=%+v", err)
+				return errors.Errorf("failed to create table, err=%+v", err)
 			}
 		}
 
 		// create foreign keys
 		for _, stmt := range META_TABLE_FOREIGN_KEYS {
 			if _, err = tx.ExecContext(ctx, stmt); err != nil {
-				return fmt.Errorf("failed to add foreign key, err=%+v", err)
+				return errors.Errorf("failed to add foreign key, err=%+v", err)
 			}
 		}
 
 		// create meta views
 		for _, schema := range META_VIEW_SCHEMAS {
 			if _, err = tx.ExecContext(ctx, schema); err != nil {
-				return err
+				return errors.WithStack(err)
 			}
 		}
 
diff --git a/internal/database/metadata/postgres/create_database.go b/internal/database/metadata/postgres/create_database.go
index 93aea8cb..f6e0e4bb 100644
--- a/internal/database/metadata/postgres/create_database.go
+++ b/internal/database/metadata/postgres/create_database.go
@@ -8,6 +8,8 @@ import (
 	"github.com/jackc/pgerrcode"
 	"github.com/jmoiron/sqlx"
 	"github.com/lib/pq"
+	"github.com/pkg/errors"
+
 	"github.com/oom-ai/oomstore/internal/database/dbutil"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
 )
@@ -21,7 +23,7 @@ func CreateDatabase(ctx context.Context, opt *types.PostgresOpt) (err error) {
 
 	if _, err = defaultDB.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE %s", opt.Database)); err != nil {
 		if e2, ok := err.(*pq.Error); ok && e2.Code != pgerrcode.DuplicateDatabase {
-			return err
+			return errors.WithStack(err)
 		}
 	}
 
@@ -40,28 +42,28 @@ func createMetaSchemas(ctx context.Context, db *sqlx.DB) (err error) {
 		// create database functions
 		for _, fn := range DB_FUNCTIONS {
 			if _, err = tx.ExecContext(ctx, fn); err != nil {
-				return err
+				return errors.WithStack(err)
 			}
 		}
 
 		// create meta tables
 		for _, schema := range META_TABLE_SCHEMAS {
 			if _, err = tx.ExecContext(ctx, schema); err != nil {
-				return err
+				return errors.WithStack(err)
 			}
 		}
 
 		// create foreign keys
 		for _, stmt := range META_TABLE_FOREIGN_KEYS {
 			if _, err = tx.ExecContext(ctx, stmt); err != nil {
-				return err
+				return errors.WithStack(err)
 			}
 		}
 
 		// create meta views
 		for _, schema := range META_VIEW_SCHEMAS {
 			if _, err = tx.ExecContext(ctx, schema); err != nil {
-				return err
+				return errors.WithStack(err)
 			}
 		}
 
@@ -69,7 +71,7 @@ func createMetaSchemas(ctx context.Context, db *sqlx.DB) (err error) {
 		for table := range META_TABLE_SCHEMAS {
 			trigger := strings.ReplaceAll(TRIGGER_TEMPLATE, `{{TABLE_NAME}}`, table)
 			if _, err = tx.ExecContext(ctx, trigger); err != nil {
-				return err
+				return errors.WithStack(err)
 			}
 		}
 
diff --git a/internal/database/metadata/postgres/entity.go b/internal/database/metadata/postgres/entity.go
index a67d2a67..c274633f 100644
--- a/internal/database/metadata/postgres/entity.go
+++ b/internal/database/metadata/postgres/entity.go
@@ -2,21 +2,22 @@ package postgres
 
 import (
 	"context"
-	"fmt"
 
 	"github.com/jackc/pgerrcode"
 	"github.com/lib/pq"
+	"github.com/pkg/errors"
+
 	"github.com/oom-ai/oomstore/internal/database/metadata"
 )
 
 func createEntity(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.CreateEntityOpt) (int, error) {
 	var entityID int
-	query := "INSERT INTO entity(name, description) VALUES($1, $2) returning id"
-	err := sqlxCtx.GetContext(ctx, &entityID, query, opt.EntityName, opt.Description)
+	query := "INSERT INTO entity(name, length, description) VALUES($1, $2, $3) returning id"
+	err := sqlxCtx.GetContext(ctx, &entityID, query, opt.EntityName, opt.Length, opt.Description)
 	if er, ok := err.(*pq.Error); ok {
 		if er.Code == pgerrcode.UniqueViolation {
-			return 0, fmt.Errorf("entity %s already exists", opt.EntityName)
+			return 0, errors.Errorf("entity %s already exists", opt.EntityName)
 		}
 	}
-	return entityID, err
+	return entityID, errors.WithStack(err)
 }
diff --git a/internal/database/metadata/postgres/feature.go b/internal/database/metadata/postgres/feature.go
index dfc45ef0..b32bcbd5 100644
--- a/internal/database/metadata/postgres/feature.go
+++ b/internal/database/metadata/postgres/feature.go
@@ -2,11 +2,11 @@ package postgres
 
 import (
 	"context"
-	"fmt"
 
 	"github.com/jackc/pgerrcode"
 	"github.com/lib/pq"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
+	"github.com/pkg/errors"
 )
 
 func createFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.CreateFeatureOpt) (int, error) {
@@ -19,9 +19,9 @@ func createFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metada
 	if err != nil {
 		if e2, ok := err.(*pq.Error); ok {
 			if e2.Code == pgerrcode.UniqueViolation {
-				return 0, fmt.Errorf("feature %s already exists", opt.FeatureName)
+				return 0, errors.Errorf("feature %s already exists", opt.FeatureName)
 			}
 		}
 	}
-	return featureID, err
+	return featureID, errors.WithStack(err)
 }
diff --git a/internal/database/metadata/postgres/group.go b/internal/database/metadata/postgres/group.go
index a1e5ccda..ebcc446c 100644
--- a/internal/database/metadata/postgres/group.go
+++ b/internal/database/metadata/postgres/group.go
@@ -2,10 +2,11 @@ package postgres
 
 import (
 	"context"
-	"fmt"
 
 	"github.com/jackc/pgerrcode"
 	"github.com/lib/pq"
+	"github.com/pkg/errors"
+
 	"github.com/oom-ai/oomstore/internal/database/metadata"
 	"github.com/oom-ai/oomstore/pkg/errdefs"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
@@ -13,7 +14,7 @@ import (
 
 func createGroup(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.CreateGroupOpt) (int, error) {
 	if opt.Category != types.CategoryBatch && opt.Category != types.CategoryStream {
-		return 0, errdefs.InvalidAttribute(fmt.Errorf("illegal category '%s', should be either 'stream' or 'batch'", opt.Category))
+		return 0, errdefs.InvalidAttribute(errors.Errorf("illegal category '%s', should be either 'stream' or 'batch'", opt.Category))
 	}
 	var groupID int
 	query := "insert into feature_group(name, entity_id, category, description) values($1, $2, $3, $4) returning id"
@@ -21,9 +22,9 @@ func createGroup(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata
 	if err != nil {
 		if e2, ok := err.(*pq.Error); ok {
 			if e2.Code == pgerrcode.UniqueViolation {
-				return 0, fmt.Errorf("feature group %s already exists", opt.GroupName)
+				return 0, errors.Errorf("feature group %s already exists", opt.GroupName)
 			}
 		}
 	}
-	return groupID, err
+	return groupID, errors.WithStack(err)
 }
diff --git a/internal/database/metadata/postgres/revision.go b/internal/database/metadata/postgres/revision.go
index 25abc1b4..7592bf1b 100644
--- a/internal/database/metadata/postgres/revision.go
+++ b/internal/database/metadata/postgres/revision.go
@@ -2,10 +2,11 @@ package postgres
 
 import (
 	"context"
-	"fmt"
 
 	"github.com/jackc/pgerrcode"
 	"github.com/lib/pq"
+	"github.com/pkg/errors"
+
 	"github.com/oom-ai/oomstore/internal/database/dbutil"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
 )
@@ -24,24 +25,24 @@ func createRevision(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metad
 	if err := sqlxCtx.GetContext(ctx, &revisionID, insertQuery, opt.GroupID, opt.Revision, snapshotTable, cdcTable, opt.Anchored, opt.Description); err != nil {
 		if e2, ok := err.(*pq.Error); ok {
 			if e2.Code == pgerrcode.UniqueViolation {
-				return 0, "", fmt.Errorf("revision already exists: groupID=%d, revision=%d", opt.GroupID, opt.Revision)
+				return 0, "", errors.Errorf("revision already exists: groupID=%d, revision=%d", opt.GroupID, opt.Revision)
 			}
 		}
-		return 0, "", err
+		return 0, "", errors.WithStack(err)
 	}
 	if opt.SnapshotTable == nil {
 		updateQuery := "UPDATE feature_group_revision SET snapshot_table = $1 WHERE id = $2"
 		snapshotTable = dbutil.OfflineBatchTableName(opt.GroupID, int64(revisionID))
 		result, err := sqlxCtx.ExecContext(ctx, updateQuery, snapshotTable, revisionID)
 		if err != nil {
-			return 0, "", err
+			return 0, "", errors.WithStack(err)
 		}
 		rowsAffected, err := result.RowsAffected()
 		if err != nil {
-			return 0, "", err
+			return 0, "", errors.WithStack(err)
 		}
 		if rowsAffected != 1 {
-			return 0, "", fmt.Errorf("failed to update revision %d: revision not found", revisionID)
+			return 0, "", errors.Errorf("failed to update revision %d: revision not found", revisionID)
 		}
 	}
 
diff --git a/internal/database/metadata/postgres/schema.go b/internal/database/metadata/postgres/schema.go
index 3ff4a79a..b976b440 100644
--- a/internal/database/metadata/postgres/schema.go
+++ b/internal/database/metadata/postgres/schema.go
@@ -53,11 +53,13 @@ var META_TABLE_SCHEMAS = map[string]string{
 		CREATE TABLE entity (
 			id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
 			name        VARCHAR(32) NOT     NULL,
+			length      SMALLINT    NOT     NULL,
 			description VARCHAR(64) DEFAULT '',
 			create_time TIMESTAMP   NOT     NULL DEFAULT CURRENT_TIMESTAMP,
 			modify_time TIMESTAMP   NOT     NULL DEFAULT CURRENT_TIMESTAMP,
 			UNIQUE (name)
 		);
+		COMMENT ON COLUMN entity.length IS 'feature entity value max length';
 		`,
 	"feature_group_revision": `
 		CREATE TABLE feature_group_revision (
diff --git a/internal/database/metadata/postgres/store.go b/internal/database/metadata/postgres/store.go
index 4edca74f..da391477 100644
--- a/internal/database/metadata/postgres/store.go
+++ b/internal/database/metadata/postgres/store.go
@@ -8,6 +8,7 @@ import (
 	_ "github.com/lib/pq"
 	"github.com/oom-ai/oomstore/internal/database/dbutil"
 	"github.com/oom-ai/oomstore/internal/database/metadata/sqlutil"
+	"github.com/pkg/errors"
 
 	"github.com/oom-ai/oomstore/internal/database/metadata"
 	"github.com/oom-ai/oomstore/internal/database/metadata/informer"
@@ -55,7 +56,7 @@ func (db *DB) Close() error {
 		return err
 	}
 	if err := db.DB.Close(); err != nil {
-		return err
+		return errors.WithStack(err)
 	}
 	return nil
 }
diff --git a/internal/database/metadata/sqlite/create_database.go b/internal/database/metadata/sqlite/create_database.go
index 98ff4445..260a3e2f 100644
--- a/internal/database/metadata/sqlite/create_database.go
+++ b/internal/database/metadata/sqlite/create_database.go
@@ -5,6 +5,8 @@ import (
 	"strings"
 
 	"github.com/jmoiron/sqlx"
+	"github.com/pkg/errors"
+
 	"github.com/oom-ai/oomstore/internal/database/dbutil"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
 )
@@ -22,20 +24,20 @@ func createMetaSchemas(ctx context.Context, db *sqlx.DB) (err error) {
 	return dbutil.WithTransaction(db, ctx, func(ctx context.Context, tx *sqlx.Tx) error {
 		for _, schema := range META_TABLE_SCHEMAS {
 			if _, err = tx.ExecContext(ctx, schema); err != nil {
-				return err
+				return errors.WithStack(err)
 			}
 		}
 
 		for _, schema := range META_VIEW_SCHEMAS {
 			if _, err = tx.ExecContext(ctx, schema); err != nil {
-				return err
+				return errors.WithStack(err)
 			}
 		}
 
 		for table := range META_TABLE_SCHEMAS {
 			trigger := strings.ReplaceAll(TRIGGER_TEMPLATE, `{{TABLE_NAME}}`, table)
 			if _, err = tx.ExecContext(ctx, trigger); err != nil {
-				return err
+				return errors.WithStack(err)
 			}
 		}
 
diff --git a/internal/database/metadata/sqlite/entity.go b/internal/database/metadata/sqlite/entity.go
index 4f719da9..4320c86d 100644
--- a/internal/database/metadata/sqlite/entity.go
+++ b/internal/database/metadata/sqlite/entity.go
@@ -2,20 +2,20 @@ package sqlite
 
 import (
 	"context"
-	"fmt"
 
 	"github.com/mattn/go-sqlite3"
+	"github.com/pkg/errors"
 
 	"github.com/oom-ai/oomstore/internal/database/metadata"
 )
 
 func createEntity(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.CreateEntityOpt) (int, error) {
-	query := "INSERT INTO entity(name, description) VALUES(?, ?)"
-	res, err := sqlxCtx.ExecContext(ctx, query, opt.EntityName, opt.Description)
+	query := "INSERT INTO entity(name, length, description) VALUES(?, ?, ?)"
+	res, err := sqlxCtx.ExecContext(ctx, query, opt.EntityName, opt.Length, opt.Description)
 	if err != nil {
 		if er, ok := err.(sqlite3.Error); ok {
 			if er.ExtendedCode == sqlite3.ErrConstraintUnique {
-				return 0, fmt.Errorf("entity %s already exists", opt.EntityName)
+				return 0, errors.Errorf("entity %s already exists", opt.EntityName)
 			}
 		}
 		return 0, err
@@ -23,7 +23,7 @@ func createEntity(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadat
 
 	entityID, err := res.LastInsertId()
 	if err != nil {
-		return 0, err
+		return 0, errors.WithStack(err)
 	}
-	return int(entityID), err
+	return int(entityID), nil
 }
diff --git a/internal/database/metadata/sqlite/feature.go b/internal/database/metadata/sqlite/feature.go
index 17b7264b..fd35068d 100644
--- a/internal/database/metadata/sqlite/feature.go
+++ b/internal/database/metadata/sqlite/feature.go
@@ -2,9 +2,9 @@ package sqlite
 
 import (
 	"context"
-	"fmt"
 
 	"github.com/mattn/go-sqlite3"
+	"github.com/pkg/errors"
 
 	"github.com/oom-ai/oomstore/internal/database/metadata"
 )
@@ -18,15 +18,15 @@ func createFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metada
 	if err != nil {
 		if sqliteErr, ok := err.(sqlite3.Error); ok {
 			if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
-				return 0, fmt.Errorf("feature %s already exists", opt.FeatureName)
+				return 0, errors.Errorf("feature %s already exists", opt.FeatureName)
 			}
 		}
-		return 0, err
+		return 0, errors.WithStack(err)
 	}
 
 	featureID, err := res.LastInsertId()
 	if err != nil {
-		return 0, err
+		return 0, errors.WithStack(err)
 	}
-	return int(featureID), err
+	return int(featureID), nil
 }
diff --git a/internal/database/metadata/sqlite/group.go b/internal/database/metadata/sqlite/group.go
index 57e6603d..329d4fe5 100644
--- a/internal/database/metadata/sqlite/group.go
+++ b/internal/database/metadata/sqlite/group.go
@@ -2,7 +2,8 @@ package sqlite
 
 import (
 	"context"
-	"fmt"
+
+	"github.com/pkg/errors"
 
 	"github.com/mattn/go-sqlite3"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
@@ -12,7 +13,7 @@ import (
 
 func createGroup(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.CreateGroupOpt) (int, error) {
 	if opt.Category != types.CategoryBatch && opt.Category != types.CategoryStream {
-		return 0, errdefs.InvalidAttribute(fmt.Errorf("illegal category '%s', should be either 'stream' or 'batch'", opt.Category))
+		return 0, errdefs.InvalidAttribute(errors.Errorf("illegal category '%s', should be either 'stream' or 'batch'", opt.Category))
 	}
 
 	query := "INSERT INTO feature_group(name, entity_id, category, description) VALUES(?, ?, ?, ?)"
@@ -20,16 +21,16 @@ func createGroup(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata
 	if err != nil {
 		if sqliteErr, ok := err.(sqlite3.Error); ok {
 			if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
-				return 0, fmt.Errorf("feature group %s already exists", opt.GroupName)
+				return 0, errors.Errorf("feature group %s already exists", opt.GroupName)
 			}
 		}
-		return 0, err
+		return 0, errors.WithStack(err)
 	}
 
 	groupID, err := res.LastInsertId()
 	if err != nil {
-		return 0, err
+		return 0, errors.WithStack(err)
 	}
 
-	return int(groupID), err
+	return int(groupID), nil
 }
diff --git a/internal/database/metadata/sqlite/revision.go b/internal/database/metadata/sqlite/revision.go
index 5f9e79c2..04cd0137 100644
--- a/internal/database/metadata/sqlite/revision.go
+++ b/internal/database/metadata/sqlite/revision.go
@@ -2,11 +2,11 @@ package sqlite
 
 import (
 	"context"
-	"fmt"
 
 	"github.com/mattn/go-sqlite3"
 	"github.com/oom-ai/oomstore/internal/database/dbutil"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
+	"github.com/pkg/errors"
 )
 
 func createRevision(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.CreateRevisionOpt) (int, string, error) {
@@ -23,14 +23,14 @@ func createRevision(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metad
 	if err != nil {
 		if sqliteErr, ok := err.(sqlite3.Error); ok {
 			if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
-				return 0, "", fmt.Errorf("revision already exists: groupID=%d, revision=%d", opt.GroupID, opt.Revision)
+				return 0, "", errors.Errorf("revision already exists: groupID=%d, revision=%d", opt.GroupID, opt.Revision)
 			}
 		}
-		return 0, "", err
+		return 0, "", errors.WithStack(err)
 	}
 	revisionID, err := res.LastInsertId()
 	if err != nil {
-		return 0, "", err
+		return 0, "", errors.WithStack(err)
 	}
 
 	if opt.SnapshotTable == nil {
@@ -38,14 +38,14 @@ func createRevision(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metad
 		snapshotTable = dbutil.OfflineBatchTableName(opt.GroupID, revisionID)
 		result, err := sqlxCtx.ExecContext(ctx, sqlxCtx.Rebind(updateQuery), snapshotTable, revisionID)
 		if err != nil {
-			return 0, "", err
+			return 0, "", errors.WithStack(err)
 		}
 		rowsAffected, err := result.RowsAffected()
 		if err != nil {
-			return 0, "", err
+			return 0, "", errors.WithStack(err)
 		}
 		if rowsAffected != 1 {
-			return 0, "", fmt.Errorf("failed to update revision %d: revision not found", revisionID)
+			return 0, "", errors.Errorf("failed to update revision %d: revision not found", revisionID)
 		}
 	}
 	return int(revisionID), snapshotTable, nil
diff --git a/internal/database/metadata/sqlite/schema.go b/internal/database/metadata/sqlite/schema.go
index 29f969bb..65cea474 100644
--- a/internal/database/metadata/sqlite/schema.go
+++ b/internal/database/metadata/sqlite/schema.go
@@ -43,6 +43,7 @@ var META_TABLE_SCHEMAS = map[string]string{
 		CREATE TABLE entity (
 			id				INTEGER 		NOT	NULL PRIMARY KEY AUTOINCREMENT,
 			name        	VARCHAR(32) 	NOT	NULL,
+			length      	SMALLINT    	NOT	NULL,
 			description 	VARCHAR(64) 	DEFAULT '',
 			create_time 	TIMESTAMP   	NOT	NULL DEFAULT CURRENT_TIMESTAMP,
 			modify_time 	TIMESTAMP   	NOT	NULL DEFAULT CURRENT_TIMESTAMP,
diff --git a/internal/database/metadata/sqlite/store.go b/internal/database/metadata/sqlite/store.go
index fa2880e9..e19376d4 100644
--- a/internal/database/metadata/sqlite/store.go
+++ b/internal/database/metadata/sqlite/store.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/oom-ai/oomstore/internal/database/metadata/informer"
 	"github.com/oom-ai/oomstore/internal/database/metadata/sqlutil"
+	"github.com/pkg/errors"
 
 	"github.com/oom-ai/oomstore/internal/database/metadata"
 
@@ -54,7 +55,7 @@ func (db *DB) Close() error {
 		return err
 	}
 	if err := db.DB.Close(); err != nil {
-		return err
+		return errors.WithStack(err)
 	}
 	return nil
 }
diff --git a/internal/database/metadata/sqlutil/db.go b/internal/database/metadata/sqlutil/db.go
index b4a5169f..ee8e8f40 100644
--- a/internal/database/metadata/sqlutil/db.go
+++ b/internal/database/metadata/sqlutil/db.go
@@ -4,6 +4,8 @@ import (
 	"context"
 
 	"github.com/jmoiron/sqlx"
+	"github.com/pkg/errors"
+
 	"github.com/oom-ai/oomstore/internal/database/dbutil"
 	"github.com/oom-ai/oomstore/internal/database/metadata/informer"
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
@@ -14,17 +16,17 @@ func ListMetadata(ctx context.Context, db *sqlx.DB) (*informer.Cache, error) {
 	err := dbutil.WithTransaction(db, ctx, func(ctx context.Context, tx *sqlx.Tx) error {
 		entities := types.EntityList{}
 		if err := tx.SelectContext(ctx, &entities, `SELECT * FROM entity`); err != nil {
-			return err
+			return errors.WithStack(err)
 		}
 
 		features := types.FeatureList{}
 		if err := tx.SelectContext(ctx, &features, `SELECT * FROM feature`); err != nil {
-			return err
+			return errors.WithStack(err)
 		}
 
 		groups := types.GroupList{}
 		if err := tx.SelectContext(ctx, &groups, `SELECT * FROM feature_group`); err != nil {
-			return err
+			return errors.WithStack(err)
 		}
 
 		cache = informer.NewCache(entities, features, groups)
diff --git a/internal/database/metadata/sqlutil/entity.go b/internal/database/metadata/sqlutil/entity.go
index 0be8e7f6..93a89420 100644
--- a/internal/database/metadata/sqlutil/entity.go
+++ b/internal/database/metadata/sqlutil/entity.go
@@ -5,6 +5,8 @@ import (
 	"database/sql"
 	"fmt"
 
+	"github.com/pkg/errors"
+
 	"github.com/jmoiron/sqlx"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
 	"github.com/oom-ai/oomstore/pkg/errdefs"
@@ -13,22 +15,22 @@ import (
 
 func UpdateEntity(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.UpdateEntityOpt) error {
 	if opt.NewDescription == nil {
-		return fmt.Errorf("invalid option: nothing to update")
+		return errors.Errorf("invalid option: nothing to update")
 	}
 
 	query := "UPDATE entity SET description = ? WHERE id = ?"
 	result, err := sqlxCtx.ExecContext(ctx, sqlxCtx.Rebind(query), opt.NewDescription, opt.EntityID)
 	if err != nil {
-		return err
+		return errors.WithStack(err)
 	}
 
 	rowsAffected, err := result.RowsAffected()
 	if err != nil {
-		return err
+		return errors.WithStack(err)
 	}
 
 	if rowsAffected != 1 {
-		return fmt.Errorf("failed to update entity %d: entity not found", opt.EntityID)
+		return errors.Errorf("failed to update entity %d: entity not found", opt.EntityID)
 	}
 	return nil
 }
@@ -38,9 +40,9 @@ func GetEntity(ctx context.Context, sqlxCtx metadata.SqlxContext, id int) (*type
 	query := "SELECT * FROM entity WHERE id = ?"
 	if err := sqlxCtx.GetContext(ctx, &entity, sqlxCtx.Rebind(query), id); err != nil {
 		if err == sql.ErrNoRows {
-			return nil, errdefs.NotFound(fmt.Errorf("feature entity %d not found", id))
+			return nil, errdefs.NotFound(errors.Errorf("feature entity %d not found", id))
 		}
-		return nil, err
+		return nil, errors.WithStack(err)
 	}
 
 	return &entity, nil
@@ -51,13 +53,12 @@ func GetEntityByName(ctx context.Context, sqlxCtx metadata.SqlxContext, name str
 	query := "SELECT * FROM entity WHERE name = ?"
 	if err := sqlxCtx.GetContext(ctx, &entity, sqlxCtx.Rebind(query), name); err != nil {
 		if err == sql.ErrNoRows {
-			return nil, errdefs.NotFound(fmt.Errorf("feature entity %s not found", name))
+			return nil, errdefs.NotFound(errors.Errorf("feature entity %s not found", name))
 		}
-		return nil, err
+		return nil, errors.WithStack(err)
 	}
 
 	return &entity, nil
-
 }
 
 func ListEntity(ctx context.Context, sqlxCtx metadata.SqlxContext, entityIDs *[]int) (types.EntityList, error) {
@@ -70,12 +71,12 @@ func ListEntity(ctx context.Context, sqlxCtx metadata.SqlxContext, entityIDs *[]
 		}
 		query, args, err = sqlx.In(fmt.Sprintf("%s WHERE id IN (?)", query), *entityIDs)
 		if err != nil {
-			return nil, err
+			return nil, errors.WithStack(err)
 		}
 	}
 	entities := types.EntityList{}
 	if err := sqlxCtx.SelectContext(ctx, &entities, sqlxCtx.Rebind(query), args...); err != nil {
-		return nil, err
+		return nil, errors.WithStack(err)
 	}
 	return entities, nil
 }
diff --git a/internal/database/metadata/sqlutil/feature.go b/internal/database/metadata/sqlutil/feature.go
index 05cb9d43..23b02ef3 100644
--- a/internal/database/metadata/sqlutil/feature.go
+++ b/internal/database/metadata/sqlutil/feature.go
@@ -6,6 +6,8 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/pkg/errors"
+
 	"github.com/oom-ai/oomstore/internal/database/dbutil"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
 	"github.com/oom-ai/oomstore/pkg/errdefs"
@@ -14,20 +16,20 @@ import (
 
 func UpdateFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata.UpdateFeatureOpt) error {
 	if opt.NewDescription == nil {
-		return fmt.Errorf("invalid option: nothing to update")
+		return errors.Errorf("invalid option: nothing to update")
 	}
 
 	query := "UPDATE feature SET description = ? WHERE id = ?"
 	result, err := sqlxCtx.ExecContext(ctx, sqlxCtx.Rebind(query), opt.NewDescription, opt.FeatureID)
 	if err != nil {
-		return err
+		return errors.WithStack(err)
 	}
 	rowsAffected, err := result.RowsAffected()
 	if err != nil {
-		return err
+		return errors.WithStack(err)
 	}
 	if rowsAffected != 1 {
-		return fmt.Errorf("failed to update feature %d: feature not found", opt.FeatureID)
+		return errors.Errorf("failed to update feature %d: feature not found", opt.FeatureID)
 	}
 	return nil
 }
@@ -42,13 +44,13 @@ func GetFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, id int) (*typ
 	query := `SELECT * FROM feature WHERE id = ?`
 	if err := sqlxCtx.GetContext(ctx, &feature, sqlxCtx.Rebind(query), id); err != nil {
 		if err == sql.ErrNoRows {
-			return nil, errdefs.NotFound(fmt.Errorf("feature %d not found", id))
+			return nil, errdefs.NotFound(errors.Errorf("feature %d not found", id))
 		}
-		return nil, err
+		return nil, errors.WithStack(err)
 	}
 
 	if group, err = GetGroup(ctx, sqlxCtx, feature.GroupID); err != nil {
-		return nil, err
+		return nil, errors.WithStack(err)
 	}
 	feature.Group = group
 
@@ -65,13 +67,13 @@ func GetFeatureByName(ctx context.Context, sqlxCtx metadata.SqlxContext, fullNam
 	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(fmt.Errorf("feature %s not found", fullName))
+			return nil, errdefs.NotFound(errors.Errorf("feature %s not found", fullName))
 		}
-		return nil, err
+		return nil, errors.WithStack(err)
 	}
 
 	if group, err = GetGroup(ctx, sqlxCtx, feature.GroupID); err != nil {
-		return nil, err
+		return nil, errors.WithStack(err)
 	}
 	feature.Group = group
 
@@ -94,7 +96,7 @@ func ListFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata
 	}
 	query = fmt.Sprintf("%s ORDER BY id ASC", query)
 	if err := sqlxCtx.SelectContext(ctx, &features, sqlxCtx.Rebind(query), args...); err != nil {
-		return nil, err
+		return nil, errors.WithStack(err)
 	}
 
 	// enrich group
@@ -106,7 +108,7 @@ func ListFeature(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata
 	for _, f := range features {
 		group := groups.Find(func(g *types.Group) bool { return g.ID == f.GroupID })
 		if group == nil {
-			return nil, errdefs.InvalidAttribute(fmt.Errorf("no group found for feature %s", f.Name))
+			return nil, errdefs.InvalidAttribute(errors.Errorf("no group found for feature %s", f.Name))
 		}
 		f.Group = group
 	}
diff --git a/internal/database/metadata/sqlutil/group.go b/internal/database/metadata/sqlutil/group.go
index c7b789a7..3991e970 100644
--- a/internal/database/metadata/sqlutil/group.go
+++ b/internal/database/metadata/sqlutil/group.go
@@ -6,6 +6,8 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/pkg/errors"
+
 	"github.com/jmoiron/sqlx"
 	"github.com/oom-ai/oomstore/internal/database/dbutil"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
@@ -28,20 +30,20 @@ func UpdateGroup(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metadata
 	args = append(args, opt.GroupID)
 
 	if len(cond) == 0 {
-		return fmt.Errorf("invalid option: nothing to update")
+		return errors.Errorf("invalid option: nothing to update")
 	}
 
 	query := fmt.Sprintf("UPDATE feature_group SET %s WHERE id = ?", strings.Join(cond, ","))
 	result, err := sqlxCtx.ExecContext(ctx, sqlxCtx.Rebind(query), args...)
 	if err != nil {
-		return err
+		return errors.WithStack(err)
 	}
 	rowsAffected, err := result.RowsAffected()
 	if err != nil {
-		return err
+		return errors.WithStack(err)
 	}
 	if rowsAffected != 1 {
-		return fmt.Errorf("failed to update feature group %d: feature group not found", opt.GroupID)
+		return errors.Errorf("failed to update feature group %d: feature group not found", opt.GroupID)
 	}
 	return nil
 }
@@ -51,9 +53,9 @@ func GetGroup(ctx context.Context, sqlxCtx metadata.SqlxContext, id int) (*types
 	query := `SELECT * FROM feature_group WHERE id = ?`
 	if err := sqlxCtx.GetContext(ctx, &group, sqlxCtx.Rebind(query), id); err != nil {
 		if err == sql.ErrNoRows {
-			return nil, errdefs.NotFound(fmt.Errorf("feature group %d not found", id))
+			return nil, errdefs.NotFound(errors.Errorf("feature group %d not found", id))
 		}
-		return nil, err
+		return nil, errors.WithStack(err)
 	}
 
 	entity, err := GetEntity(ctx, sqlxCtx, group.EntityID)
@@ -69,14 +71,14 @@ func GetGroupByName(ctx context.Context, sqlxCtx metadata.SqlxContext, name stri
 	query := `SELECT * FROM feature_group WHERE name = ?`
 	if err := sqlxCtx.GetContext(ctx, &group, sqlxCtx.Rebind(query), name); err != nil {
 		if err == sql.ErrNoRows {
-			return nil, errdefs.NotFound(fmt.Errorf("feature group %s not found", name))
+			return nil, errdefs.NotFound(errors.Errorf("feature group %s not found", name))
 		}
-		return nil, err
+		return nil, errors.WithStack(err)
 	}
 
 	entity, err := GetEntity(ctx, sqlxCtx, group.EntityID)
 	if err != nil {
-		return nil, err
+		return nil, errors.WithStack(err)
 	}
 	group.Entity = entity
 	return &group, nil
@@ -85,7 +87,7 @@ func GetGroupByName(ctx context.Context, sqlxCtx metadata.SqlxContext, name stri
 func ListGroup(ctx context.Context, sqlxCtx metadata.SqlxContext, entityID *int, groupIDs *[]int) (types.GroupList, error) {
 	cond, args, err := buildListGroupCond(entityID, groupIDs)
 	if err != nil {
-		return nil, err
+		return nil, errors.WithStack(err)
 	}
 
 	query := `SELECT * FROM feature_group`
@@ -95,7 +97,7 @@ func ListGroup(ctx context.Context, sqlxCtx metadata.SqlxContext, entityID *int,
 	query = fmt.Sprintf("%s ORDER BY id ASC", query)
 	var groups types.GroupList
 	if err := sqlxCtx.SelectContext(ctx, &groups, sqlxCtx.Rebind(query), args...); err != nil {
-		return nil, err
+		return nil, errors.WithStack(err)
 	}
 
 	if err := enrichGroups(ctx, sqlxCtx, groups); err != nil {
@@ -118,7 +120,7 @@ func buildListGroupCond(entityID *int, groupIDs *[]int) (string, []interface{},
 		}
 		s, inArgs, err := sqlx.In("id IN (?)", *groupIDs)
 		if err != nil {
-			return "", nil, err
+			return "", nil, errors.WithStack(err)
 		}
 		cond = append(cond, s)
 		args = append(args, inArgs...)
@@ -130,14 +132,14 @@ func enrichGroups(ctx context.Context, sqlxCtx metadata.SqlxContext, groups type
 	entityIDs := groups.EntityIDs()
 	entities, err := ListEntity(ctx, sqlxCtx, &entityIDs)
 	if err != nil {
-		return err
+		return errors.WithStack(err)
 	}
 	for _, group := range groups {
 		entity := entities.Find(func(e *types.Entity) bool {
 			return group.EntityID == e.ID
 		})
 		if entity == nil {
-			return fmt.Errorf("cannot find entity %d for group %d", group.EntityID, group.ID)
+			return errors.Errorf("cannot find entity %d for group %d", group.EntityID, group.ID)
 		}
 		group.Entity = entity
 	}
diff --git a/internal/database/metadata/sqlutil/revision.go b/internal/database/metadata/sqlutil/revision.go
index 0c81658c..f8688738 100644
--- a/internal/database/metadata/sqlutil/revision.go
+++ b/internal/database/metadata/sqlutil/revision.go
@@ -6,6 +6,8 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/pkg/errors"
+
 	"github.com/oom-ai/oomstore/internal/database/dbutil"
 	"github.com/oom-ai/oomstore/internal/database/metadata"
 	"github.com/oom-ai/oomstore/pkg/errdefs"
@@ -30,21 +32,21 @@ func UpdateRevision(ctx context.Context, sqlxCtx metadata.SqlxContext, opt metad
 		return err
 	}
 	if len(cond) == 0 {
-		return fmt.Errorf("invliad option: nothing to update")
+		return errors.Errorf("invliad option: nothing to update")
 	}
 	args = append(args, opt.RevisionID)
 
 	query := fmt.Sprintf("UPDATE feature_group_revision SET %s WHERE id = ?", strings.Join(cond, ","))
 	result, err := sqlxCtx.ExecContext(ctx, sqlxCtx.Rebind(query), args...)
 	if err != nil {
-		return err
+		return errors.WithStack(err)
 	}
 	rowsAffected, err := result.RowsAffected()
 	if err != nil {
-		return err
+		return errors.WithStack(err)
 	}
 	if rowsAffected != 1 {
-		return fmt.Errorf("failed to update revision %d: revision not found", opt.RevisionID)
+		return errors.Errorf("failed to update revision %d: revision not found", opt.RevisionID)
 	}
 	return nil
 }
@@ -54,9 +56,9 @@ func GetRevision(ctx context.Context, sqlxCtx metadata.SqlxContext, id int) (*ty
 	query := `SELECT * FROM feature_group_revision WHERE id = ?`
 	if err := sqlxCtx.GetContext(ctx, &revision, sqlxCtx.Rebind(query), id); err != nil {
 		if err == sql.ErrNoRows {
-			return nil, errdefs.NotFound(fmt.Errorf("revision %d not found", id))
+			return nil, errdefs.NotFound(errors.Errorf("revision %d not found", id))
 		}
-		return nil, err
+		return nil, errors.WithStack(err)
 	}
 
 	group, err := GetGroup(ctx, sqlxCtx, revision.GroupID)
@@ -72,9 +74,9 @@ func GetRevisionBy(ctx context.Context, sqlxCtx metadata.SqlxContext, groupID in
 	query := `SELECT * FROM feature_group_revision WHERE group_id = ? AND revision = ?`
 	if err := sqlxCtx.GetContext(ctx, &r, sqlxCtx.Rebind(query), groupID, revision); err != nil {
 		if err == sql.ErrNoRows {
-			return nil, errdefs.NotFound(fmt.Errorf("revision not found by group %d and revision %d", groupID, revision))
+			return nil, errdefs.NotFound(errors.Errorf("revision not found by group %d and revision %d", groupID, revision))
 		}
-		return nil, err
+		return nil, errors.WithStack(err)
 	}
 
 	group, err := GetGroup(ctx, sqlxCtx, r.GroupID)
@@ -95,7 +97,7 @@ func ListRevision(ctx context.Context, sqlxCtx metadata.SqlxContext, groupID *in
 	query = fmt.Sprintf("%s ORDER BY id ASC", query)
 	var revisions types.RevisionList
 	if err := sqlxCtx.SelectContext(ctx, &revisions, sqlxCtx.Rebind(query), args...); err != nil {
-		return nil, err
+		return nil, errors.WithStack(err)
 	}
 
 	if err := enrichRevisions(ctx, sqlxCtx, revisions); err != nil {
@@ -115,7 +117,7 @@ func enrichRevisions(ctx context.Context, sqlxCtx metadata.SqlxContext, revision
 			return revision.GroupID == g.ID
 		})
 		if group == nil {
-			return fmt.Errorf("cannot find group %d for revision %d", revision.GroupID, revision.ID)
+			return errors.Errorf("cannot find group %d for revision %d", revision.GroupID, revision.ID)
 		}
 		revision.Group = group
 	}
diff --git a/internal/database/offline/sqlutil/join_helper.go b/internal/database/offline/sqlutil/join_helper.go
index aadc3be4..aa3b00f9 100644
--- a/internal/database/offline/sqlutil/join_helper.go
+++ b/internal/database/offline/sqlutil/join_helper.go
@@ -166,12 +166,8 @@ func prepareTableSchema(dbOpt dbutil.DBOpt, entity types.Entity, tableName strin
 		entityType = "STRING"
 		valueType = "STRING"
 		qtTableName = fmt.Sprintf("%s.%s", *dbOpt.DatasetID, qt(tableName))
-	case types.BackendMySQL:
-		entityType = "VARCHAR(255)"
-		valueType = "TEXT"
-		qtTableName = qt(tableName)
 	default:
-		entityType = "TEXT"
+		entityType = fmt.Sprintf("VARCHAR(%d)", entity.Length)
 		valueType = "TEXT"
 		qtTableName = qt(tableName)
 	}
diff --git a/internal/database/online/sqlutil/util.go b/internal/database/online/sqlutil/util.go
index 7b3a89b4..d54bc687 100644
--- a/internal/database/online/sqlutil/util.go
+++ b/internal/database/online/sqlutil/util.go
@@ -25,8 +25,10 @@ func CreateStreamTableSchema(ctx context.Context, tableName string, entity *type
 	)
 	switch backend {
 	case types.BackendMySQL:
-		entityFormat = fmt.Sprintf("`%s` VARCHAR(255) PRIMARY KEY", entity.Name)
-	case types.BackendPostgres, types.BackendCassandra, types.BackendSQLite:
+		entityFormat = fmt.Sprintf("`%s` VARCHAR(%d) PRIMARY KEY", entity.Name, entity.Length)
+	case types.BackendPostgres:
+		entityFormat = fmt.Sprintf(`"%s" VARCHAR(%d) PRIMARY KEY`, entity.Name, entity.Length)
+	case types.BackendCassandra, types.BackendSQLite:
 		entityFormat = fmt.Sprintf(`"%s" TEXT PRIMARY KEY`, entity.Name)
 	default:
 		return "", errdefs.InvalidAttribute(fmt.Errorf("backend %s not support", backend))
diff --git a/oomcli/cmd/register_entity.go b/oomcli/cmd/register_entity.go
index 24da9ba0..4d37e349 100644
--- a/oomcli/cmd/register_entity.go
+++ b/oomcli/cmd/register_entity.go
@@ -33,5 +33,8 @@ func init() {
 
 	flags := registerEntityCmd.Flags()
 
+	flags.IntVarP(&registerEntityOpt.Length, "length", "l", 0, "entity value length")
+	_ = registerEntityCmd.MarkFlagRequired("length")
+
 	flags.StringVarP(&registerEntityOpt.Description, "description", "d", "", "entity description")
 }
diff --git a/oomcli/cmd/types.go b/oomcli/cmd/types.go
index d46438e0..539fcdfa 100644
--- a/oomcli/cmd/types.go
+++ b/oomcli/cmd/types.go
@@ -10,6 +10,7 @@ import (
 type FlattenEntity struct {
 	ID          int    `oomcli:"ID"`
 	Name        string `oomcli:"NAME"`
+	Length      int    `oomcli:"LENGTH"`
 	Description string `oomcli:"DESCRIPTION,truncate"`
 
 	CreateTime time.Time `oomcli:"CREATE-TIME,wide"`
@@ -65,6 +66,7 @@ func parseTokenLists(i interface{}) (headerTokens TokenList, dataTokens []TokenL
 			tokens, err = parseTokens(FlattenEntity{
 				ID:          e.ID,
 				Name:        e.Name,
+				Length:      e.Length,
 				Description: e.Description,
 				CreateTime:  e.CreateTime,
 				ModifyTime:  e.ModifyTime,
diff --git a/pkg/oomstore/apply.go b/pkg/oomstore/apply.go
index 1be400bd..e819bc99 100644
--- a/pkg/oomstore/apply.go
+++ b/pkg/oomstore/apply.go
@@ -60,6 +60,7 @@ func (s *OomStore) applyEntity(ctx context.Context, tx metadata.DBStore, newEnti
 		_, err = tx.CreateEntity(ctx, metadata.CreateEntityOpt{
 			CreateEntityOpt: types.CreateEntityOpt{
 				EntityName:  newEntity.Name,
+				Length:      newEntity.Length,
 				Description: newEntity.Description,
 			},
 		})
@@ -304,6 +305,7 @@ func buildApplyEntity(entity apply.Entity) apply.Entity {
 	return apply.Entity{
 		Kind:        entity.Kind,
 		Name:        entity.Name,
+		Length:      entity.Length,
 		Description: entity.Description,
 	}
 }
diff --git a/pkg/oomstore/types/apply/apply.go b/pkg/oomstore/types/apply/apply.go
index 16637c34..f0f73bf4 100644
--- a/pkg/oomstore/types/apply/apply.go
+++ b/pkg/oomstore/types/apply/apply.go
@@ -136,6 +136,7 @@ func (g *Group) Validate() error {
 type Entity struct {
 	Kind        string `mapstructure:"kind" yaml:"kind"`
 	Name        string `mapstructure:"name" yaml:"name"`
+	Length      int    `mapstructure:"length" yaml:"length"`
 	Description string `mapstructure:"description" yaml:"description"`
 
 	Groups []Group `mapstructure:"groups" yaml:"groups,omitempty"`
@@ -157,6 +158,7 @@ func FromEntityList(entities types.EntityList, groups *GroupItems) (items Entity
 		items.Items = append(items.Items, Entity{
 			Kind:        "Entity",
 			Name:        entity.Name,
+			Length:      entity.Length,
 			Description: entity.Description,
 			Groups: groups.Filter(func(g Group) bool {
 				return g.EntityName == entity.Name
diff --git a/pkg/oomstore/types/entity.go b/pkg/oomstore/types/entity.go
index 81eada28..f893b455 100644
--- a/pkg/oomstore/types/entity.go
+++ b/pkg/oomstore/types/entity.go
@@ -5,8 +5,9 @@ import (
 )
 
 type Entity struct {
-	ID   int    `db:"id"`
-	Name string `db:"name"`
+	ID     int    `db:"id"`
+	Name   string `db:"name"`
+	Length int    `db:"length"`
 
 	Description string    `db:"description"`
 	CreateTime  time.Time `db:"create_time"`
diff --git a/pkg/oomstore/types/options.go b/pkg/oomstore/types/options.go
index 732c8037..e05bc4ad 100644
--- a/pkg/oomstore/types/options.go
+++ b/pkg/oomstore/types/options.go
@@ -20,6 +20,7 @@ type UpdateFeatureOpt struct {
 
 type CreateEntityOpt struct {
 	EntityName  string
+	Length      int
 	Description string
 }
 
diff --git a/pkg/oomstore/types/value_type.go b/pkg/oomstore/types/value_type.go
index 73d8e738..ea8de622 100644
--- a/pkg/oomstore/types/value_type.go
+++ b/pkg/oomstore/types/value_type.go
@@ -1,9 +1,10 @@
 package types
 
 import (
-	"fmt"
 	"strconv"
 
+	"github.com/pkg/errors"
+
 	"github.com/oom-ai/oomstore/pkg/errdefs"
 )
 
@@ -49,7 +50,7 @@ func (v ValueType) Validate() error {
 	if _, ok := allValueTypes[v]; ok {
 		return nil
 	} else {
-		return errdefs.InvalidAttribute(fmt.Errorf("Invalid value type: %d", v))
+		return errdefs.InvalidAttribute(errors.Errorf("Invalid value type: %d", v))
 	}
 }
 
@@ -57,6 +58,6 @@ func ParseValueType(s string) (ValueType, error) {
 	if v, ok := allValueTypeStrings[s]; ok {
 		return v, nil
 	} else {
-		return Invalid, errdefs.InvalidAttribute(fmt.Errorf("Unknown value type: %s", s))
+		return Invalid, errdefs.InvalidAttribute(errors.Errorf("Unknown value type: %s", s))
 	}
 }


Test patch:
diff --git a/internal/database/dbutil/schema_test.go b/internal/database/dbutil/schema_test.go
index 36bb6270..37846274 100644
--- a/internal/database/dbutil/schema_test.go
+++ b/internal/database/dbutil/schema_test.go
@@ -35,12 +35,15 @@ func TestBuildSchema(t *testing.T) {
 			description: "postgres schema",
 			backend:     types.BackendPostgres,
 			tableName:   "user",
-			entity:      &types.Entity{Name: "user_id"},
-			features:    features,
+			entity: &types.Entity{
+				Name:   "user_id",
+				Length: 32,
+			},
+			features: features,
 
 			want: `
 CREATE TABLE "user" (
-	"user_id" TEXT,
+	"user_id" VARCHAR(32),
 	"age" bigint,
 	"gender" text
 )`,
@@ -50,12 +53,15 @@ CREATE TABLE "user" (
 			description: "mysql schema",
 			backend:     types.BackendMySQL,
 			tableName:   "user",
-			entity:      &types.Entity{Name: "user_id"},
-			features:    features,
+			entity: &types.Entity{
+				Name:   "user_id",
+				Length: 32,
+			},
+			features: features,
 
 			want: "\n" +
 				"CREATE TABLE `user` (\n" +
-				"	`user_id` VARCHAR(255),\n" +
+				"	`user_id` VARCHAR(32),\n" +
 				"	`age` bigint,\n" +
 				"	`gender` text\n)",
 			wantErr: nil,
diff --git a/internal/database/metadata/informer/informer_test.go b/internal/database/metadata/informer/informer_test.go
index 1dc0b638..9c5b2a4e 100644
--- a/internal/database/metadata/informer/informer_test.go
+++ b/internal/database/metadata/informer/informer_test.go
@@ -14,8 +14,9 @@ import (
 
 func sampleCache() *informer.Cache {
 	entity := types.Entity{
-		ID:   1,
-		Name: "entity",
+		ID:     1,
+		Length: 10,
+		Name:   "entity",
 	}
 	group := types.Group{
 		ID:       100,
diff --git a/internal/database/metadata/test_impl/entity.go b/internal/database/metadata/test_impl/entity.go
index cd23c45f..f0d50bbd 100644
--- a/internal/database/metadata/test_impl/entity.go
+++ b/internal/database/metadata/test_impl/entity.go
@@ -1,7 +1,6 @@
 package test_impl
 
 import (
-	"fmt"
 	"testing"
 
 	"github.com/oom-ai/oomstore/pkg/oomstore/types"
@@ -20,6 +19,7 @@ func TestCreateEntity(t *testing.T, prepareStore PrepareStoreFn, destroyStore De
 	opt := metadata.CreateEntityOpt{
 		CreateEntityOpt: types.CreateEntityOpt{
 			EntityName:  "device",
+			Length:      32,
 			Description: "description",
 		},
 	}
@@ -29,10 +29,11 @@ func TestCreateEntity(t *testing.T, prepareStore PrepareStoreFn, destroyStore De
 	_, err = store.CreateEntity(ctx, metadata.CreateEntityOpt{
 		CreateEntityOpt: types.CreateEntityOpt{
 			EntityName:  "device",
+			Length:      32,
 			Description: "description",
 		},
 	})
-	require.Equal(t, fmt.Errorf("entity device already exists"), err)
+	require.Equal(t, "entity device already exists", err.Error())
 }
 
 func TestGetEntity(t *testing.T, prepareStore PrepareStoreFn, destroyStore DestroyStoreFn) {
@@ -44,6 +45,7 @@ func TestGetEntity(t *testing.T, prepareStore PrepareStoreFn, destroyStore Destr
 	opt := metadata.CreateEntityOpt{
 		CreateEntityOpt: types.CreateEntityOpt{
 			EntityName:  "device",
+			Length:      32,
 			Description: "description",
 		},
 	}
@@ -54,6 +56,7 @@ func TestGetEntity(t *testing.T, prepareStore PrepareStoreFn, destroyStore Destr
 	entity, err := store.GetEntity(ctx, id)
 	require.NoError(t, err)
 	require.Equal(t, opt.EntityName, entity.Name)
+	require.Equal(t, opt.Length, entity.Length)
 	require.Equal(t, opt.Description, entity.Description)
 
 	_, err = store.GetEntity(ctx, 0)
@@ -69,6 +72,7 @@ func TestUpdateEntity(t *testing.T, prepareStore PrepareStoreFn, destroyStore De
 	id, err := store.CreateEntity(ctx, metadata.CreateEntityOpt{
 		CreateEntityOpt: types.CreateEntityOpt{
 			EntityName:  "device",
+			Length:      32,
 			Description: "description",
 		},
 	})
@@ -104,6 +108,7 @@ func TestListEntity(t *testing.T, prepareStore PrepareStoreFn, destroyStore Dest
 	_, err = store.CreateEntity(ctx, metadata.CreateEntityOpt{
 		CreateEntityOpt: types.CreateEntityOpt{
 			EntityName:  "device",
+			Length:      32,
 			Description: "description",
 		},
 	})
@@ -116,6 +121,7 @@ func TestListEntity(t *testing.T, prepareStore PrepareStoreFn, destroyStore Dest
 	_, err = store.CreateEntity(ctx, metadata.CreateEntityOpt{
 		CreateEntityOpt: types.CreateEntityOpt{
 			EntityName:  "user",
+			Length:      16,
 			Description: "description",
 		},
 	})
diff --git a/internal/database/metadata/test_impl/feature.go b/internal/database/metadata/test_impl/feature.go
index 9f3b8129..088e2695 100644
--- a/internal/database/metadata/test_impl/feature.go
+++ b/internal/database/metadata/test_impl/feature.go
@@ -2,7 +2,6 @@ package test_impl
 
 import (
 	"context"
-	"fmt"
 	"testing"
 	"time"
 
@@ -17,6 +16,7 @@ func prepareEntityAndGroup(t *testing.T, ctx context.Context, store metadata.Sto
 	entityID, err := store.CreateEntity(ctx, metadata.CreateEntityOpt{
 		CreateEntityOpt: types.CreateEntityOpt{
 			EntityName:  "device",
+			Length:      32,
 			Description: "description",
 		},
 	})
@@ -68,7 +68,7 @@ func TestCreateFeatureWithSameName(t *testing.T, prepareStore PrepareStoreFn, de
 	require.NoError(t, err)
 
 	_, err = store.CreateFeature(ctx, opt)
-	assert.Equal(t, err, fmt.Errorf("feature phone already exists"))
+	assert.Equal(t, "feature phone already exists", err.Error())
 }
 
 func TestCreateFeatureWithSQLKeyword(t *testing.T, prepareStore PrepareStoreFn, destroyStore DestroyStoreFn) {
diff --git a/internal/database/metadata/test_impl/group.go b/internal/database/metadata/test_impl/group.go
index 085c5846..d69d1bc0 100644
--- a/internal/database/metadata/test_impl/group.go
+++ b/internal/database/metadata/test_impl/group.go
@@ -2,7 +2,6 @@ package test_impl
 
 import (
 	"context"
-	"fmt"
 	"testing"
 
 	"github.com/oom-ai/oomstore/internal/database/metadata"
@@ -15,6 +14,7 @@ func prepareEntity(t *testing.T, ctx context.Context, store metadata.Store, name
 	entityID, err := store.CreateEntity(ctx, metadata.CreateEntityOpt{
 		CreateEntityOpt: types.CreateEntityOpt{
 			EntityName:  name,
+			Length:      32,
 			Description: "description",
 		},
 	})
@@ -146,7 +146,7 @@ func TestCreateGroup(t *testing.T, prepareStore PrepareStoreFn, destroyStore Des
 
 	// cannot create feature group with same name
 	_, err = store.CreateGroup(ctx, opt)
-	require.Equal(t, fmt.Errorf("feature group device_info already exists"), err)
+	require.Equal(t, "feature group device_info already exists", err.Error())
 
 	// cannot create feature group with invalid category
 	opt.Category = "invalid-category"
diff --git a/internal/database/metadata/test_impl/revision.go b/internal/database/metadata/test_impl/revision.go
index 6f0568a8..fbea169f 100644
--- a/internal/database/metadata/test_impl/revision.go
+++ b/internal/database/metadata/test_impl/revision.go
@@ -357,6 +357,7 @@ func prepareRevisions(t *testing.T, ctx context.Context, store metadata.Store) (
 	entityID, err := store.CreateEntity(ctx, metadata.CreateEntityOpt{
 		CreateEntityOpt: types.CreateEntityOpt{
 			EntityName:  "device",
+			Length:      32,
 			Description: "description",
 		},
 	})
diff --git a/internal/database/offline/test_impl/create_table.go b/internal/database/offline/test_impl/create_table.go
index 0058a10c..33573366 100644
--- a/internal/database/offline/test_impl/create_table.go
+++ b/internal/database/offline/test_impl/create_table.go
@@ -15,7 +15,10 @@ func TestCreateTable(t *testing.T, prepareStore PrepareStoreFn, destroyStore Des
 	defer store.Close()
 
 	tableName := "new_table"
-	entity := types.Entity{Name: "device"}
+	entity := types.Entity{
+		Name:   "device",
+		Length: 16,
+	}
 	features := []*types.Feature{
 		{
 			Name:      "price",
diff --git a/internal/database/offline/test_impl/import.go b/internal/database/offline/test_impl/import.go
index 3d613e29..87a9fc8f 100644
--- a/internal/database/offline/test_impl/import.go
+++ b/internal/database/offline/test_impl/import.go
@@ -19,7 +19,10 @@ func TestImport(t *testing.T, prepareStore PrepareStoreFn, destroyStore DestroyS
 	ctx, store := prepareStore(t)
 	defer store.Close()
 
-	entity := types.Entity{Name: "device"}
+	entity := types.Entity{
+		Name:   "device",
+		Length: 16,
+	}
 	snapshotTable := "offline_1_1"
 
 	opt := offline.ImportOpt{
diff --git a/internal/database/offline/test_impl/join.go b/internal/database/offline/test_impl/join.go
index fb550314..9282cfd2 100644
--- a/internal/database/offline/test_impl/join.go
+++ b/internal/database/offline/test_impl/join.go
@@ -23,7 +23,10 @@ func TestJoin(t *testing.T, prepareStore PrepareStoreFn, destroyStore DestroySto
 	ctx, store := prepareStore(t)
 	defer store.Close()
 
-	entity := &types.Entity{Name: "device"}
+	entity := &types.Entity{
+		Name:   "device",
+		Length: 10,
+	}
 	unixMilli := &types.Feature{
 		Name:      "unix_milli",
 		ValueType: types.Int64,
diff --git a/internal/database/offline/test_impl/push.go b/internal/database/offline/test_impl/push.go
index c2b142b2..7063cb9e 100644
--- a/internal/database/offline/test_impl/push.go
+++ b/internal/database/offline/test_impl/push.go
@@ -17,8 +17,9 @@ func TestPush(t *testing.T, prepareStore PrepareStoreFn, destroyStore DestroySto
 	defer store.Close()
 
 	entity := types.Entity{
-		ID:   18,
-		Name: "user",
+		ID:     18,
+		Name:   "user",
+		Length: 10,
 	}
 	group := types.Group{
 		ID:       1,
diff --git a/internal/database/offline/test_impl/snapshot.go b/internal/database/offline/test_impl/snapshot.go
index 37701b37..21299a94 100644
--- a/internal/database/offline/test_impl/snapshot.go
+++ b/internal/database/offline/test_impl/snapshot.go
@@ -21,8 +21,11 @@ func TestSnapshot(t *testing.T, prepareStore PrepareStoreFn, destroyStore Destro
 	defer store.Close()
 
 	group := &types.Group{
-		ID:     1,
-		Entity: &types.Entity{Name: "device"},
+		ID: 1,
+		Entity: &types.Entity{
+			Name:   "device",
+			Length: 10,
+		},
 	}
 	unixMilli := &types.Feature{
 		Name:      "unix_milli",
diff --git a/internal/database/offline/test_impl/util.go b/internal/database/offline/test_impl/util.go
index 47396ed8..8a18f29f 100644
--- a/internal/database/offline/test_impl/util.go
+++ b/internal/database/offline/test_impl/util.go
@@ -22,7 +22,10 @@ func buildTestSnapshotTable(
 	snapshotTable string,
 	source *offline.CSVSource,
 ) {
-	entity := &types.Entity{Name: "device"}
+	entity := &types.Entity{
+		Name:   "device",
+		Length: 10,
+	}
 	header := []string{"device"}
 	for _, f := range features {
 		header = append(header, f.Name)
diff --git a/internal/database/online/test_impl/stream.go b/internal/database/online/test_impl/stream.go
index 4850deba..7e1c99c2 100644
--- a/internal/database/online/test_impl/stream.go
+++ b/internal/database/online/test_impl/stream.go
@@ -19,8 +19,9 @@ var simpleStreamData StreamData
 
 func init() {
 	entity := types.Entity{
-		ID:   1,
-		Name: "user",
+		ID:     1,
+		Name:   "user",
+		Length: 16,
 	}
 
 	group1 := &types.Group{
diff --git a/internal/database/online/test_impl/util.go b/internal/database/online/test_impl/util.go
index fb3b9840..466e38ad 100644
--- a/internal/database/online/test_impl/util.go
+++ b/internal/database/online/test_impl/util.go
@@ -83,7 +83,7 @@ func init() {
 					Category: types.CategoryBatch,
 				},
 			},
-			Entity: &types.Entity{ID: 5, Name: "user"},
+			Entity: &types.Entity{ID: 5, Name: "user", Length: 4},
 			Data: []types.ExportRecord{
 				[]interface{}{"3215", int64(18), "F", 1.1, true, time.Now()},
 				[]interface{}{"3216", int64(29), nil, 2.0, false, time.Now()},
@@ -106,11 +106,11 @@ func init() {
 		}
 
 		revision := &types.Revision{ID: 9, GroupID: 2, Group: &types.Group{ID: 2, Category: types.CategoryBatch}}
-		entity := &types.Entity{ID: 5, Name: "user"}
+		entity := &types.Entity{ID: 5, Name: "user", Length: 5}
 		var data []types.ExportRecord
 
 		for i := 0; i < 100; i++ {
-			record := []interface{}{dbutil.RandString(10), rand.Float64()}
+			record := []interface{}{dbutil.RandString(entity.Length), rand.Float64()}
 			data = append(data, record)
 		}
 		SampleMedium = Sample{features, revision, entity, data}
diff --git a/oomagent/test/data/driver_stats.yaml b/oomagent/test/data/driver_stats.yaml
index 48cfa4a8..3eb8401e 100644
--- a/oomagent/test/data/driver_stats.yaml
+++ b/oomagent/test/data/driver_stats.yaml
@@ -1,6 +1,7 @@
 ---
 kind: Entity
 name: driver
+length: 2
 groups:
   - name: driver_stats
     category: batch
diff --git a/oomagent/test/data/fraud_detection.yaml b/oomagent/test/data/fraud_detection.yaml
index 1244259f..7edcf39b 100644
--- a/oomagent/test/data/fraud_detection.yaml
+++ b/oomagent/test/data/fraud_detection.yaml
@@ -1,5 +1,6 @@
 kind: Entity
 name: user
+length: 12
 description: 'user id'
 groups:
 - name: account
diff --git a/oomcli/test/test_apply_entity.sh b/oomcli/test/test_apply_entity.sh
index 61bb744c..cfe252fb 100755
--- a/oomcli/test/test_apply_entity.sh
+++ b/oomcli/test/test_apply_entity.sh
@@ -8,6 +8,7 @@ apply_single_complex_entity() {
     cat <<EOF | oomcli apply -f /dev/stdin
 kind: Entity
 name: user
+length: 8
 description: 'description'
 groups:
 - name: device
@@ -33,8 +34,8 @@ groups:
 EOF
 
     entity_expected='
-ID,NAME,DESCRIPTION
-1,user,description
+ID,NAME,LENGTH,DESCRIPTION
+1,user,8,description
 '
     entity_actual=$(oomcli get meta entity -o csv)
     assert_eq "apply_single_complex_entity: check entity" "$(sort <<< "$entity_expected")" "$(sort <<< "$entity_actual")"
@@ -65,6 +66,7 @@ apply_multiple_files_of_entity() {
     cat <<EOF | oomcli apply -f /dev/stdin
 kind: Entity
 name: user
+length: 8
 description: 'description'
 groups:
 - name: student
@@ -73,19 +75,21 @@ groups:
 ---
 kind: Entity
 name: device
+length: 16
 description: 'description'
 ---
 kind: Entity
 name: test
+length: 32
 description: 'description'
 EOF
 
 
   entity_expected='
-ID,NAME,DESCRIPTION
-1,user,description
-2,device,description
-3,test,description
+ID,NAME,LENGTH,DESCRIPTION
+1,user,8,description
+2,device,16,description
+3,test,32,description
 '
     entity_actual=$(oomcli get meta entity -o csv)
     assert_eq "apply_multiple_files_of_entity: oomcli get meta entity" "$entity_expected" "$entity_actual"
@@ -106,6 +110,7 @@ apply_entity_items() {
 items:
   - kind: Entity
     name: user
+    length: 8
     description: user ID
     groups:
       - name: account
@@ -133,6 +138,7 @@ items:
             description: transaction_count_30d description
   - kind: Entity
     name: device
+    length: 8
     description: device info
     groups:
       - name: phone
@@ -148,9 +154,9 @@ items:
 EOF
 
       entity_expected='
-ID,NAME,DESCRIPTION
-1,user,user ID
-2,device,device info
+ID,NAME,LENGTH,DESCRIPTION
+1,user,8,user ID
+2,device,8,device info
 '
       entity_actual=$(oomcli get meta entity -o csv)
       assert_eq "apply_entity_items: oomcli apply mutiple entity: check entity" "$(sort <<< "$entity_expected")" "$(sort <<< "$entity_actual")"
diff --git a/oomcli/test/test_apply_feature.sh b/oomcli/test/test_apply_feature.sh
index de5f2494..bdf15553 100755
--- a/oomcli/test/test_apply_feature.sh
+++ b/oomcli/test/test_apply_feature.sh
@@ -10,6 +10,7 @@ apply_multiple_files_of_feature() {
     cat <<EOF | oomcli apply -f /dev/stdin
 kind: Entity
 name: user
+length: 8
 description: 'description'
 ---
 kind: Group
@@ -48,6 +49,7 @@ apply_feature_items() {
     cat <<EOF | oomcli apply -f /dev/stdin
 kind: Entity
 name: user
+length: 8
 description: user ID
 ---
 kind: Group
diff --git a/oomcli/test/test_apply_group.sh b/oomcli/test/test_apply_group.sh
index ca59442f..d73b40f4 100755
--- a/oomcli/test/test_apply_group.sh
+++ b/oomcli/test/test_apply_group.sh
@@ -10,6 +10,7 @@ apply_single_complex_group() {
     cat <<EOF | oomcli apply -f /dev/stdin
 kind: Entity
 name: user
+length: 8
 description: 'description'
 ---
 kind: Group
@@ -53,6 +54,7 @@ apply_multiple_files_of_group() {
     cat <<EOF | oomcli apply -f /dev/stdin
 kind: Entity
 name: user
+length: 8
 description: 'description'
 ---
 kind: Group
@@ -84,10 +86,12 @@ apply_group_items() {
     cat <<EOF | oomcli apply -f /dev/stdin
 kind: Entity
 name: user
+length: 8
 description: 'description'
 ---
 kind: Entity
 name: device
+length: 8
 description: 'description'
 ---
 items:
diff --git a/oomcli/test/test_get_meta_entity.sh b/oomcli/test/test_get_meta_entity.sh
index f4905fa2..ffac5278 100755
--- a/oomcli/test/test_get_meta_entity.sh
+++ b/oomcli/test/test_get_meta_entity.sh
@@ -7,19 +7,19 @@ register_features
 import_sample > /dev/null
 
 case='oomcli get meta entity works'
-expected='ID,NAME,DESCRIPTION,CREATE-TIME,MODIFY-TIME
-1,device,device,2021-10-19T06:56:07Z,2021-10-19T06:56:07Z
-2,user,user,2021-10-19T06:56:07Z,2021-10-19T06:56:07Z
+expected='ID,NAME,LENGTH,DESCRIPTION,CREATE-TIME,MODIFY-TIME
+1,device,32,device,2021-10-19T06:56:07Z,2021-10-19T06:56:07Z
+2,user,64,user,2021-10-19T06:56:07Z,2021-10-19T06:56:07Z
 '
 actual=$(oomcli get meta entity -o csv)
-ignore_time() { cut -d ',' -f 1-3 <<<"$1"; }
+ignore_time() { cut -d ',' -f 1-4 <<<"$1"; }
 assert_eq "$case" "$(ignore_time "$expected" | sort)" "$(ignore_time "$actual" | sort)"
 
 
 case='oomcli get simplified meta entity works'
-expected='ID,NAME,DESCRIPTION
-1,device,device
-2,user,user
+expected='ID,NAME,LENGTH,DESCRIPTION
+1,device,32,device
+2,user,64,user
 '
 actual=$(oomcli get meta entity -o csv)
 assert_eq "$case" "$(sort <<< "$expected")" "$(sort <<< "$actual")"
@@ -28,6 +28,7 @@ case='oomcli get meta entity -o yaml: one entity'
 expected='
 kind: Entity
 name: device
+length: 32
 description: device
 groups:
     - name: phone
@@ -50,6 +51,7 @@ expected='
 items:
     - kind: Entity
       name: device
+      length: 32
       description: device
       groups:
         - name: phone
@@ -64,6 +66,7 @@ items:
               description: model
     - kind: Entity
       name: user
+      length: 64
       description: user
       groups:
         - name: student
diff --git a/oomcli/test/test_update_entity.sh b/oomcli/test/test_update_entity.sh
index e4ef0699..480f9918 100755
--- a/oomcli/test/test_update_entity.sh
+++ b/oomcli/test/test_update_entity.sh
@@ -9,8 +9,8 @@ import_sample > /dev/null
 case='oomcli update entity works'
 oomcli update entity device --description "new description"
 expected='
-ID,NAME,DESCRIPTION
-1,device,new description
+ID,NAME,LENGTH,DESCRIPTION
+1,device,32,new description
 '
 actual=$(oomcli get meta entity device -o csv)
 assert_eq "$case" "$expected" "$actual"
diff --git a/oomcli/test/util.sh b/oomcli/test/util.sh
index 059a4888..4c2f4b73 100644
--- a/oomcli/test/util.sh
+++ b/oomcli/test/util.sh
@@ -52,8 +52,8 @@ assert_eq() {
 
 # register features for the sample data
 register_features() {
-    oomcli register entity device --description "device"
-    oomcli register entity user   --description "user"
+    oomcli register entity device --length 32     --description "device"
+    oomcli register entity user   --length 64     --description "user"
 
     oomcli register group phone      --entity device --category "batch"  --description "phone"
     oomcli register group student    --entity user   --category "batch"  --description "student"
diff --git a/pkg/oomstore/apply_test.go b/pkg/oomstore/apply_test.go
index 745fe01f..ca696f21 100644
--- a/pkg/oomstore/apply_test.go
+++ b/pkg/oomstore/apply_test.go
@@ -24,6 +24,7 @@ func TestBuildApplyStage(t *testing.T) {
 			opt: apply.ApplyOpt{R: strings.NewReader(`
 # kind: Entity
 name: user
+length: 8
 description: 'User ID'
 `)},
 			wantStage: nil,
@@ -34,6 +35,7 @@ description: 'User ID'
 			opt: apply.ApplyOpt{R: strings.NewReader(`
 kind: Entit
 name: user
+length: 8
 description: 'description'
 `)},
 			wantStage: nil,
@@ -44,6 +46,7 @@ description: 'description'
 			opt: apply.ApplyOpt{R: strings.NewReader(`
 kind: Entity
 name: user
+length: 8
 description: 'description'
 `)},
 			wantStage: &apply.ApplyStage{
@@ -51,6 +54,7 @@ description: 'description'
 					{
 						Kind:        "Entity",
 						Name:        "user",
+						Length:      8,
 						Description: "description",
 					},
 				},
@@ -64,6 +68,7 @@ description: 'description'
 			opt: apply.ApplyOpt{R: strings.NewReader(`
 kind: Entity
 name: user
+length: 8
 description: 'description'
 ---
 kind: Group
@@ -97,6 +102,7 @@ description: 'description'
 					{
 						Kind:        "Entity",
 						Name:        "user",
+						Length:      8,
 						Description: "description",
 					},
 				}, NewGroups: []apply.Group{
@@ -186,6 +192,7 @@ features:
 			opt: apply.ApplyOpt{R: strings.NewReader(`
 kind: Entity
 name: user
+length: 8
 description: 'description'
 groups:
 - name: device
@@ -214,6 +221,7 @@ groups:
 					{
 						Kind:        "Entity",
 						Name:        "user",
+						Length:      8,
 						Description: "description",
 					},
 				},
@@ -439,6 +447,7 @@ items:
 items:
     - kind: Entity
       name: user
+      length: 8
       description: user ID
       groups:
         - name: account
@@ -466,6 +475,7 @@ items:
               description: transaction_count_30d description
     - kind: Entity
       name: device
+      length: 8
       description: device info
       groups:
         - name: phone
@@ -485,11 +495,13 @@ items:
 					{
 						Kind:        "Entity",
 						Name:        "user",
+						Length:      8,
 						Description: "user ID",
 					},
 					{
 						Kind:        "Entity",
 						Name:        "device",
+						Length:      8,
 						Description: "device info",
 					},
 				},
diff --git a/pkg/oomstore/join_test.go b/pkg/oomstore/join_test.go
index 5f505f29..51b6b2bb 100644
--- a/pkg/oomstore/join_test.go
+++ b/pkg/oomstore/join_test.go
@@ -26,7 +26,8 @@ func TestChannelJoin(t *testing.T) {
 	consistentFeatures := prepareFeatures(true, true)
 
 	entity := types.Entity{
-		Name: "device",
+		Name:   "device",
+		Length: 10,
 	}
 	revisions := types.RevisionList{
 		{
diff --git a/sdk/python/src/test/test_client.py b/sdk/python/src/test/test_client.py
index 24ec65ab..1e3d659f 100644
--- a/sdk/python/src/test/test_client.py
+++ b/sdk/python/src/test/test_client.py
@@ -25,6 +25,7 @@ class Data:
             f.write(
                 """kind: Entity
 name: user
+length: 8
 description: "user ID"
 groups:
   - name: account