Repository: zeromicro/go-zero
Pull request #4062 (2024-04-10 07:49:40)

(goctl)feature/model config
1. Support model data type reading from configuration files when GOCTL_EXPERIMENTAL is turned on.
2. Support custom data type mapping when GOCTL_EXPERIMENTAL is turned on
3. More info pls refer to 
https://go-zero.dev/docs/tutorials/cli/config(中文)
https://go-zero.dev/en/docs/tutorials/cli/config(English)
https://go-zero.dev/docs/tutorials/cli/model#%E7%B1%BB%E5%9E%8B%E6%98%A0%E5%B0%84%E8%87%AA%E5%AE%9A%E4%B9%89(中文)
https://go-zero.dev/en/docs/tutorials/cli/model#type-mapping-customization(English)

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/zeromicro/go-zero/pull/4062?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zeromicro) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 95.17%. Comparing base [(`8690859`)](https://app.codecov.io/gh/zeromicro/go-zero/commit/8690859c7d1c9461bdc422c3c65608bde30904db?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zeromicro) to head [(`0e62114`)](https://app.codecov.io/gh/zeromicro/go-zero/pull/4062?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zeromicro).
> Report is 8 commits behind head on master.

<details><summary>Additional details and impacted files</summary>



[see 255 files with indirect coverage changes](https://app.codecov.io/gh/zeromicro/go-zero/pull/4062/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zeromicro)

</details>

Patch:
diff --git a/tools/goctl/cmd/root.go b/tools/goctl/cmd/root.go
index c19b9c7b..5cbfa4cc 100644
--- a/tools/goctl/cmd/root.go
+++ b/tools/goctl/cmd/root.go
@@ -13,6 +13,7 @@ import (
 	"github.com/withfig/autocomplete-tools/integrations/cobra"
 	"github.com/zeromicro/go-zero/tools/goctl/api"
 	"github.com/zeromicro/go-zero/tools/goctl/bug"
+	"github.com/zeromicro/go-zero/tools/goctl/config"
 	"github.com/zeromicro/go-zero/tools/goctl/docker"
 	"github.com/zeromicro/go-zero/tools/goctl/env"
 	"github.com/zeromicro/go-zero/tools/goctl/gateway"
@@ -113,7 +114,7 @@ func init() {
 
 	rootCmd.SetUsageTemplate(usageTpl)
 	rootCmd.AddCommand(api.Cmd, bug.Cmd, docker.Cmd, kube.Cmd, env.Cmd, gateway.Cmd, model.Cmd)
-	rootCmd.AddCommand(migrate.Cmd, quickstart.Cmd, rpc.Cmd, tpl.Cmd, upgrade.Cmd)
+	rootCmd.AddCommand(migrate.Cmd, quickstart.Cmd, rpc.Cmd, tpl.Cmd, upgrade.Cmd, config.Cmd)
 	rootCmd.Command.AddCommand(cobracompletefig.CreateCompletionSpecCommand())
 	rootCmd.MustInit()
 }
diff --git a/tools/goctl/config/cmd.go b/tools/goctl/config/cmd.go
new file mode 100644
index 00000000..474c6340
--- /dev/null
+++ b/tools/goctl/config/cmd.go
@@ -0,0 +1,59 @@
+package config
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/spf13/cobra"
+	"github.com/zeromicro/go-zero/tools/goctl/internal/cobrax"
+	"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
+)
+
+var (
+	// Cmd describes a bug command.
+	Cmd = cobrax.NewCommand("config")
+
+	initCmd  = cobrax.NewCommand("init", cobrax.WithRunE(runConfigInit))
+	cleanCmd = cobrax.NewCommand("clean", cobrax.WithRunE(runConfigClean))
+)
+
+func init() {
+	Cmd.AddCommand(initCmd, cleanCmd)
+}
+
+func runConfigInit(*cobra.Command, []string) error {
+	wd, err := os.Getwd()
+	if err != nil {
+		return err
+	}
+
+	cfgFile, err := getConfigPath(wd)
+	if err != nil {
+		return err
+	}
+	if pathx.FileExists(cfgFile) {
+		fmt.Printf("%s already exists, path: %s\n", configFile, cfgFile)
+		return nil
+	}
+
+	err = os.WriteFile(cfgFile, defaultConfig, 0644)
+	if err != nil {
+		return err
+	}
+	fmt.Printf("%s generated in %s\n", configFile, cfgFile)
+	return nil
+}
+
+func runConfigClean(*cobra.Command, []string) error {
+	wd, err := os.Getwd()
+	if err != nil {
+		return err
+	}
+
+	cfgFile, err := getConfigPath(wd)
+	if err != nil {
+		return err
+	}
+
+	return pathx.RemoveIfExist(cfgFile)
+}
diff --git a/tools/goctl/config/config.go b/tools/goctl/config/config.go
index 49e80075..f14caa16 100644
--- a/tools/goctl/config/config.go
+++ b/tools/goctl/config/config.go
@@ -1,25 +1,71 @@
 package config
 
 import (
+	_ "embed"
 	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
 	"strings"
+
+	"github.com/zeromicro/go-zero/tools/goctl/util/ctx"
+	"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
+	"gopkg.in/yaml.v2"
+)
+
+const (
+	// DefaultFormat defines a default naming style
+	DefaultFormat = "gozero"
+	configFile    = "goctl.yaml"
 )
 
-// DefaultFormat defines a default naming style
-const DefaultFormat = "gozero"
+var (
+	//go:embed default.yaml
+	defaultConfig []byte
+
+	ExternalConfig *External
+)
 
 // Config defines the file naming style
-type Config struct {
-	// NamingFormat is used to define the naming format of the generated file name.
-	// just like time formatting, you can specify the formatting style through the
-	// two format characters go, and zero. for example: snake format you can
-	// define as go_zero, camel case format you can it is defined as goZero,
-	// and even split characters can be specified, such as go#zero. in theory,
-	// any combination can be used, but the prerequisite must meet the naming conventions
-	// of each operating system file name.
-	// Note: NamingFormat is based on snake or camel string
-	NamingFormat string `yaml:"namingFormat"`
-}
+type (
+	Config struct {
+		// NamingFormat is used to define the naming format of the generated file name.
+		// just like time formatting, you can specify the formatting style through the
+		// two format characters go, and zero. for example: snake format you can
+		// define as go_zero, camel case format you can it is defined as goZero,
+		// and even split characters can be specified, such as go#zero. in theory,
+		// any combination can be used, but the prerequisite must meet the naming conventions
+		// of each operating system file name.
+		// Note: NamingFormat is based on snake or camel string
+		NamingFormat string `yaml:"namingFormat"`
+	}
+
+	External struct {
+		// Model is the configuration for the model code generation.
+		Model Model `yaml:"model,omitempty"`
+	}
+
+	// Model defines the configuration for the model code generation.
+	Model struct {
+		// TypesMap: custom Data Type Mapping Table.
+		TypesMap map[string]ModelTypeMapOption `yaml:"types_map,omitempty" `
+	}
+
+	// ModelTypeMapOption custom Type Options.
+	ModelTypeMapOption struct {
+		// Type: valid when not using UnsignedType and NullType.
+		Type string `yaml:"type"`
+
+		// UnsignedType: valid when not using  NullType.
+		UnsignedType string `yaml:"unsigned_type,omitempty"`
+
+		// NullType: priority use.
+		NullType string `yaml:"null_type,omitempty"`
+
+		// Pkg defines the package of the custom type.
+		Pkg string `yaml:"pkg,omitempty"`
+	}
+)
 
 // NewConfig creates an instance for Config
 func NewConfig(format string) (*Config, error) {
@@ -31,6 +77,58 @@ func NewConfig(format string) (*Config, error) {
 	return cfg, err
 }
 
+func init() {
+	var cfg External
+	err := loadConfig(&cfg)
+	if err != nil {
+		fmt.Println(err.Error())
+	} else {
+		ExternalConfig = &cfg
+	}
+}
+
+func loadConfig(cfg *External) error {
+	wd, err := os.Getwd()
+	if err != nil {
+		return err
+	}
+
+	cfgFile, err := getConfigPath(wd)
+	if err != nil {
+		return err
+	}
+	var content []byte
+	if pathx.FileExists(cfgFile) {
+		content, err = os.ReadFile(cfgFile)
+		if err != nil {
+			return err
+		}
+	}
+	if len(content) == 0 {
+		content = append(content, defaultConfig...)
+	}
+	return yaml.Unmarshal(content, cfg)
+}
+
+// getConfigPath returns the configuration file path, but not create the file.
+func getConfigPath(workDir string) (string, error) {
+	abs, err := filepath.Abs(workDir)
+	if err != nil {
+		return "", err
+	}
+
+	err = pathx.MkdirIfNotExist(abs)
+	if err != nil {
+		return "", err
+	}
+
+	projectCtx, err := ctx.Prepare(abs)
+	if err != nil {
+		return "", err
+	}
+	return filepath.Join(projectCtx.Dir, configFile), nil
+}
+
 func validate(cfg *Config) error {
 	if len(strings.TrimSpace(cfg.NamingFormat)) == 0 {
 		return errors.New("missing namingFormat")
diff --git a/tools/goctl/config/default.yaml b/tools/goctl/config/default.yaml
new file mode 100644
index 00000000..6da7fa6b
--- /dev/null
+++ b/tools/goctl/config/default.yaml
@@ -0,0 +1,157 @@
+model:
+  types_map:
+    bigint:
+      null_type: sql.NullInt64
+      type: int64
+      unsigned_type: uint64
+    dec:
+      null_type: sql.NullFloat64
+      type: float64
+    decimal:
+      null_type: sql.NullFloat64
+      type: float64
+    double:
+      null_type: sql.NullFloat64
+      type: float64
+    float:
+      null_type: sql.NullFloat64
+      type: float64
+    float4:
+      null_type: sql.NullFloat64
+      type: float64
+    float8:
+      null_type: sql.NullFloat64
+      type: float64
+    int:
+      null_type: sql.NullInt64
+      type: int64
+      unsigned_type: uint64
+    int1:
+      null_type: sql.NullInt64
+      type: int64
+      unsigned_type: uint64
+    int2:
+      null_type: sql.NullInt64
+      type: int64
+      unsigned_type: uint64
+    int3:
+      null_type: sql.NullInt64
+      type: int64
+      unsigned_type: uint64
+    int4:
+      null_type: sql.NullInt64
+      type: int64
+      unsigned_type: uint64
+    int8:
+      null_type: sql.NullInt64
+      type: int64
+      unsigned_type: uint64
+    integer:
+      null_type: sql.NullInt64
+      type: int64
+      unsigned_type: uint64
+    mediumint:
+      null_type: sql.NullInt64
+      type: int64
+      unsigned_type: uint64
+    middleint:
+      null_type: sql.NullInt64
+      type: int64
+      unsigned_type: uint64
+    smallint:
+      null_type: sql.NullInt64
+      type: int64
+      unsigned_type: uint64
+    tinyint:
+      null_type: sql.NullInt64
+      type: int64
+      unsigned_type: uint64
+    date:
+      null_type: sql.NullTime
+      type: time.Time
+    datetime:
+      null_type: sql.NullTime
+      type: time.Time
+    timestamp:
+      null_type: sql.NullTime
+      type: time.Time
+    time:
+      null_type: sql.NullString
+      type: string
+    year:
+      null_type: sql.NullInt64
+      type: int64
+      unsigned_type: uint64
+    bit:
+      null_type: sql.NullByte
+      type: byte
+      unsigned_type: byte
+    bool:
+      null_type: sql.NullBool
+      type: bool
+    boolean:
+      null_type: sql.NullBool
+      type: bool
+    char:
+      null_type: sql.NullString
+      type: string
+    varchar:
+      null_type: sql.NullString
+      type: string
+    nvarchar:
+      null_type: sql.NullString
+      type: string
+    nchar:
+      null_type: sql.NullString
+      type: string
+    character:
+      null_type: sql.NullString
+      type: string
+    longvarchar:
+      null_type: sql.NullString
+      type: string
+    linestring:
+      null_type: sql.NullString
+      type: string
+    multilinestring:
+      null_type: sql.NullString
+      type: string
+    binary:
+      null_type: sql.NullString
+      type: string
+    varbinary:
+      null_type: sql.NullString
+      type: string
+    tinytext:
+      null_type: sql.NullString
+      type: string
+    text:
+      null_type: sql.NullString
+      type: string
+    mediumtext:
+      null_type: sql.NullString
+      type: string
+    longtext:
+      null_type: sql.NullString
+      type: string
+    enum:
+      null_type: sql.NullString
+      type: string
+    set:
+      null_type: sql.NullString
+      type: string
+    json:
+      null_type: sql.NullString
+      type: string
+    blob:
+      null_type: sql.NullString
+      type: string
+    longblob:
+      null_type: sql.NullString
+      type: string
+    mediumblob:
+      null_type: sql.NullString
+      type: string
+    tinyblob:
+      null_type: sql.NullString
+      type: string
diff --git a/tools/goctl/internal/flags/default_en.json b/tools/goctl/internal/flags/default_en.json
index a2253f0b..d26ec292 100644
--- a/tools/goctl/internal/flags/default_en.json
+++ b/tools/goctl/internal/flags/default_en.json
@@ -274,6 +274,14 @@
     },
     "upgrade": {
       "short": "Upgrade goctl to latest version"
+    },
+    "config": {
+      "init": {
+        "short": "Initialize goctl config file"
+      },
+      "clean": {
+        "short": "Clean goctl config file"
+      }
     }
   },
   "global": {
diff --git a/tools/goctl/model/sql/converter/types.go b/tools/goctl/model/sql/converter/types.go
index 587f5fc9..2cd803df 100644
--- a/tools/goctl/model/sql/converter/types.go
+++ b/tools/goctl/model/sql/converter/types.go
@@ -5,6 +5,8 @@ import (
 	"strings"
 
 	"github.com/zeromicro/ddl-parser/parser"
+	"github.com/zeromicro/go-zero/tools/goctl/config"
+	"github.com/zeromicro/go-zero/tools/goctl/pkg/env"
 )
 
 var unsignedTypeMap = map[string]string{
@@ -73,6 +75,63 @@ var commonMysqlDataTypeMapInt = map[int]string{
 	parser.Boolean: "bool",
 }
 
+var commonMysqlDataTypeMap = map[int]string{
+	// number
+	parser.Bit:       "bit",
+	parser.TinyInt:   "tinyint",
+	parser.SmallInt:  "smallint",
+	parser.MediumInt: "mediumint",
+	parser.Int:       "int",
+	parser.MiddleInt: "middleint",
+	parser.Int1:      "int1",
+	parser.Int2:      "int2",
+	parser.Int3:      "int3",
+	parser.Int4:      "int4",
+	parser.Int8:      "int8",
+	parser.Integer:   "integer",
+	parser.BigInt:    "bigint",
+	parser.Float:     "float",
+	parser.Float4:    "float4",
+	parser.Float8:    "float8",
+	parser.Double:    "double",
+	parser.Decimal:   "decimal",
+	parser.Dec:       "dec",
+	parser.Fixed:     "fixed",
+	parser.Numeric:   "numeric",
+	parser.Real:      "real",
+	// date&time
+	parser.Date:      "date",
+	parser.DateTime:  "datetime",
+	parser.Timestamp: "timestamp",
+	parser.Time:      "time",
+	parser.Year:      "year",
+	// string
+	parser.Char:            "char",
+	parser.VarChar:         "varchar",
+	parser.NVarChar:        "nvarchar",
+	parser.NChar:           "nchar",
+	parser.Character:       "character",
+	parser.LongVarChar:     "longvarchar",
+	parser.LineString:      "linestring",
+	parser.MultiLineString: "multilinestring",
+	parser.Binary:          "binary",
+	parser.VarBinary:       "varbinary",
+	parser.TinyText:        "tinytext",
+	parser.Text:            "text",
+	parser.MediumText:      "mediumtext",
+	parser.LongText:        "longtext",
+	parser.Enum:            "enum",
+	parser.Set:             "set",
+	parser.Json:            "json",
+	parser.Blob:            "blob",
+	parser.LongBlob:        "longblob",
+	parser.MediumBlob:      "mediumblob",
+	parser.TinyBlob:        "tinyblob",
+	// bool
+	parser.Bool:    "bool",
+	parser.Boolean: "boolean",
+}
+
 var commonMysqlDataTypeMapString = map[string]string{
 	// For consistency, all integer types are converted to int64
 	// bool
@@ -144,28 +203,79 @@ var commonMysqlDataTypeMapString = map[string]string{
 }
 
 // ConvertDataType converts mysql column type into golang type
-func ConvertDataType(dataBaseType int, isDefaultNull, unsigned, strict bool) (string, error) {
+func ConvertDataType(dataBaseType int, isDefaultNull, unsigned, strict bool) (string, string, error) {
+	if env.UseExperimental() {
+		tp, ok := commonMysqlDataTypeMap[dataBaseType]
+		if !ok {
+			return "", "", fmt.Errorf("unsupported database type: %v", dataBaseType)
+		}
+
+		goType, thirdPkg, _, err := ConvertStringDataType(tp, isDefaultNull, unsigned, strict)
+		return goType, thirdPkg, err
+	}
+
+	// the following are the old version compatibility code.
 	tp, ok := commonMysqlDataTypeMapInt[dataBaseType]
 	if !ok {
-		return "", fmt.Errorf("unsupported database type: %v", dataBaseType)
+		return "", "", fmt.Errorf("unsupported database type: %v", dataBaseType)
 	}
 
-	return mayConvertNullType(tp, isDefaultNull, unsigned, strict), nil
+	return mayConvertNullType(tp, isDefaultNull, unsigned, strict), "", nil
 }
 
 // ConvertStringDataType converts mysql column type into golang type
 func ConvertStringDataType(dataBaseType string, isDefaultNull, unsigned, strict bool) (
-	goType string, isPQArray bool, err error) {
+	goType string, thirdPkg string, isPQArray bool, err error) {
+	if env.UseExperimental() {
+		customTp, thirdImport := convertDatatypeWithConfig(dataBaseType, isDefaultNull, unsigned)
+		if len(customTp) != 0 {
+			return customTp, thirdImport, false, nil
+		}
+
+		tp, ok := commonMysqlDataTypeMapString[strings.ToLower(dataBaseType)]
+		if !ok {
+			return "", "", false, fmt.Errorf("unsupported database type: %s", dataBaseType)
+		}
+
+		if strings.HasPrefix(dataBaseType, "_") {
+			return tp, "", true, nil
+		}
+
+		return mayConvertNullType(tp, isDefaultNull, unsigned, strict), "", false, nil
+	}
+
+	// the following are the old version compatibility code.
 	tp, ok := commonMysqlDataTypeMapString[strings.ToLower(dataBaseType)]
 	if !ok {
-		return "", false, fmt.Errorf("unsupported database type: %s", dataBaseType)
+		return "", "", false, fmt.Errorf("unsupported database type: %s", dataBaseType)
 	}
 
 	if strings.HasPrefix(dataBaseType, "_") {
-		return tp, true, nil
+		return tp, "", true, nil
 	}
 
-	return mayConvertNullType(tp, isDefaultNull, unsigned, strict), false, nil
+	return mayConvertNullType(tp, isDefaultNull, unsigned, strict), "", false, nil
+}
+
+func convertDatatypeWithConfig(dataBaseType string, isDefaultNull, unsigned bool) (string, string) {
+	if config.ExternalConfig == nil {
+		return "", ""
+	}
+	opt, ok := config.ExternalConfig.Model.TypesMap[strings.ToLower(dataBaseType)]
+	if !ok || (len(opt.Type) == 0 && len(opt.UnsignedType) == 0 && len(opt.NullType) == 0) {
+		return "", ""
+	}
+
+	if isDefaultNull {
+		if len(opt.NullType) != 0 {
+			return opt.NullType, opt.Pkg
+		}
+	} else if unsigned {
+		if len(opt.UnsignedType) != 0 {
+			return opt.UnsignedType, opt.Pkg
+		}
+	}
+	return opt.Type, opt.Pkg
 }
 
 func mayConvertNullType(goDataType string, isDefaultNull, unsigned, strict bool) string {
diff --git a/tools/goctl/model/sql/example/sql/user.sql b/tools/goctl/model/sql/example/sql/user.sql
index 01be0696..ba59b5de 100644
--- a/tools/goctl/model/sql/example/sql/user.sql
+++ b/tools/goctl/model/sql/example/sql/user.sql
@@ -9,7 +9,7 @@ CREATE TABLE `user`
     `mobile`      varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '手机号',
     `gender`      char(5) COLLATE utf8mb4_general_ci      NOT NULL COMMENT '男｜女｜未公\r开',
     `nickname`    varchar(255) COLLATE utf8mb4_general_ci          DEFAULT '' COMMENT '用户昵称',
-    `type`    tinyint(1) COLLATE utf8mb4_general_ci          DEFAULT 0 COMMENT '用户类型',
+    `type`        tinyint(1) COLLATE utf8mb4_general_ci DEFAULT 0 COMMENT '用户类型',
     `create_time` timestamp NULL,
     `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     PRIMARY KEY (`id`),
@@ -22,12 +22,13 @@ CREATE TABLE `user`
 
 CREATE TABLE `student`
 (
-    `type`    bigint                          NOT NULL,
-    `class` varchar(255) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
-    `name`  varchar(255) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
-    `age`   tinyint                                   DEFAULT NULL,
+    `type`  bigint       NOT NULL,
+    `class` varchar(255) NOT NULL DEFAULT '',
+    `name`  varchar(255) NOT NULL DEFAULT '',
+    `age`   tinyint               DEFAULT NULL,
     `score` float(10, 0
 ) DEFAULT NULL,
+    `amount` decimal DEFAULT NULL,
   `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
   `update_time` timestamp NULL DEFAULT NULL,
   `delete_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
diff --git a/tools/goctl/model/sql/gen/imports.go b/tools/goctl/model/sql/gen/imports.go
index dbe5e6ea..81980fca 100644
--- a/tools/goctl/model/sql/gen/imports.go
+++ b/tools/goctl/model/sql/gen/imports.go
@@ -1,12 +1,27 @@
 package gen
 
 import (
+	"fmt"
+	"strings"
+
 	"github.com/zeromicro/go-zero/tools/goctl/model/sql/template"
 	"github.com/zeromicro/go-zero/tools/goctl/util"
 	"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
 )
 
 func genImports(table Table, withCache, timeImport bool) (string, error) {
+	var thirdImports []string
+	var m = map[string]struct{}{}
+	for _, c := range table.Fields {
+		if len(c.ThirdPkg) > 0 {
+			if _, ok := m[c.ThirdPkg]; ok {
+				continue
+			}
+			m[c.ThirdPkg] = struct{}{}
+			thirdImports = append(thirdImports, fmt.Sprintf("%q", c.ThirdPkg))
+		}
+	}
+
 	if withCache {
 		text, err := pathx.LoadTemplate(category, importsTemplateFile, template.Imports)
 		if err != nil {
@@ -17,6 +32,7 @@ func genImports(table Table, withCache, timeImport bool) (string, error) {
 			"time":       timeImport,
 			"containsPQ": table.ContainsPQ,
 			"data":       table,
+			"third":      strings.Join(thirdImports, "\n"),
 		})
 		if err != nil {
 			return "", err
@@ -34,6 +50,7 @@ func genImports(table Table, withCache, timeImport bool) (string, error) {
 		"time":       timeImport,
 		"containsPQ": table.ContainsPQ,
 		"data":       table,
+		"third":      strings.Join(thirdImports, "\n"),
 	})
 	if err != nil {
 		return "", err
diff --git a/tools/goctl/model/sql/parser/parser.go b/tools/goctl/model/sql/parser/parser.go
index 628d30ad..e93d9bed 100644
--- a/tools/goctl/model/sql/parser/parser.go
+++ b/tools/goctl/model/sql/parser/parser.go
@@ -38,6 +38,7 @@ type (
 	Field struct {
 		NameOriginal    string
 		Name            stringx.String
+		ThirdPkg        string
 		DataType        string
 		Comment         string
 		SeqInIndex      int
@@ -219,7 +220,7 @@ func convertColumns(columns []*parser.Column, primaryColumn string, strict bool)
 			}
 		}
 
-		dataType, err := converter.ConvertDataType(column.DataType.Type(), isDefaultNull, column.DataType.Unsigned(), strict)
+		dataType, thirdPkg, err := converter.ConvertDataType(column.DataType.Type(), isDefaultNull, column.DataType.Unsigned(), strict)
 		if err != nil {
 			return Primary{}, nil, err
 		}
@@ -236,6 +237,7 @@ func convertColumns(columns []*parser.Column, primaryColumn string, strict bool)
 
 		var field Field
 		field.Name = stringx.From(column.Name)
+		field.ThirdPkg = thirdPkg
 		field.DataType = dataType
 		field.Comment = util.TrimNewLine(comment)
 
@@ -267,7 +269,7 @@ func (t *Table) ContainsTime() bool {
 func ConvertDataType(table *model.Table, strict bool) (*Table, error) {
 	isPrimaryDefaultNull := table.PrimaryKey.ColumnDefault == nil && table.PrimaryKey.IsNullAble == "YES"
 	isPrimaryUnsigned := strings.Contains(table.PrimaryKey.DbColumn.ColumnType, "unsigned")
-	primaryDataType, containsPQ, err := converter.ConvertStringDataType(table.PrimaryKey.DataType, isPrimaryDefaultNull, isPrimaryUnsigned, strict)
+	primaryDataType, thirdPkg, containsPQ, err := converter.ConvertStringDataType(table.PrimaryKey.DataType, isPrimaryDefaultNull, isPrimaryUnsigned, strict)
 	if err != nil {
 		return nil, err
 	}
@@ -285,6 +287,7 @@ func ConvertDataType(table *model.Table, strict bool) (*Table, error) {
 	reply.PrimaryKey = Primary{
 		Field: Field{
 			Name:            stringx.From(table.PrimaryKey.Name),
+			ThirdPkg:        thirdPkg,
 			DataType:        primaryDataType,
 			Comment:         table.PrimaryKey.Comment,
 			SeqInIndex:      seqInIndex,
@@ -351,7 +354,7 @@ func getTableFields(table *model.Table, strict bool) (map[string]*Field, error)
 	for _, each := range table.Columns {
 		isDefaultNull := each.ColumnDefault == nil && each.IsNullAble == "YES"
 		isPrimaryUnsigned := strings.Contains(each.ColumnType, "unsigned")
-		dt, containsPQ, err := converter.ConvertStringDataType(each.DataType, isDefaultNull, isPrimaryUnsigned, strict)
+		dt, thirdPkg, containsPQ, err := converter.ConvertStringDataType(each.DataType, isDefaultNull, isPrimaryUnsigned, strict)
 		if err != nil {
 			return nil, err
 		}
@@ -363,6 +366,7 @@ func getTableFields(table *model.Table, strict bool) (map[string]*Field, error)
 		field := &Field{
 			NameOriginal:    each.Name,
 			Name:            stringx.From(each.Name),
+			ThirdPkg:        thirdPkg,
 			DataType:        dt,
 			Comment:         each.Comment,
 			SeqInIndex:      columnSeqInIndex,
diff --git a/tools/goctl/model/sql/template/tpl/import-no-cache.tpl b/tools/goctl/model/sql/template/tpl/import-no-cache.tpl
index ba9c6b4d..0ffbc242 100644
--- a/tools/goctl/model/sql/template/tpl/import-no-cache.tpl
+++ b/tools/goctl/model/sql/template/tpl/import-no-cache.tpl
@@ -9,4 +9,6 @@ import (
 	"github.com/zeromicro/go-zero/core/stores/builder"
 	"github.com/zeromicro/go-zero/core/stores/sqlx"
 	"github.com/zeromicro/go-zero/core/stringx"
+
+	{{.third}}
 )
diff --git a/tools/goctl/model/sql/template/tpl/import.tpl b/tools/goctl/model/sql/template/tpl/import.tpl
index 7e0c40f5..96650fd5 100644
--- a/tools/goctl/model/sql/template/tpl/import.tpl
+++ b/tools/goctl/model/sql/template/tpl/import.tpl
@@ -11,4 +11,6 @@ import (
 	"github.com/zeromicro/go-zero/core/stores/sqlc"
 	"github.com/zeromicro/go-zero/core/stores/sqlx"
 	"github.com/zeromicro/go-zero/core/stringx"
+
+	{{.third}}
 )


Test patch:
diff --git a/tools/goctl/model/sql/converter/types_test.go b/tools/goctl/model/sql/converter/types_test.go
index c5f8353d..8a5e17e5 100644
--- a/tools/goctl/model/sql/converter/types_test.go
+++ b/tools/goctl/model/sql/converter/types_test.go
@@ -8,23 +8,102 @@ import (
 )
 
 func TestConvertDataType(t *testing.T) {
-	v, err := ConvertDataType(parser.TinyInt, false, false, true)
+	v, _, err := ConvertDataType(parser.TinyInt, false, false, true)
 	assert.Nil(t, err)
 	assert.Equal(t, "int64", v)
 
-	v, err = ConvertDataType(parser.TinyInt, false, true, true)
+	v, _, err = ConvertDataType(parser.TinyInt, false, true, true)
 	assert.Nil(t, err)
 	assert.Equal(t, "uint64", v)
 
-	v, err = ConvertDataType(parser.TinyInt, true, false, true)
+	v, _, err = ConvertDataType(parser.TinyInt, true, false, true)
 	assert.Nil(t, err)
 	assert.Equal(t, "sql.NullInt64", v)
 
-	v, err = ConvertDataType(parser.Timestamp, false, false, true)
+	v, _, err = ConvertDataType(parser.Timestamp, false, false, true)
 	assert.Nil(t, err)
 	assert.Equal(t, "time.Time", v)
 
-	v, err = ConvertDataType(parser.Timestamp, true, false, true)
+	v, _, err = ConvertDataType(parser.Timestamp, true, false, true)
 	assert.Nil(t, err)
 	assert.Equal(t, "sql.NullTime", v)
+
+	v, _, err = ConvertDataType(parser.Decimal, false, false, true)
+	assert.Nil(t, err)
+	assert.Equal(t, "float64", v)
+}
+
+func TestConvertStringDataType(t *testing.T) {
+	type (
+		input struct {
+			dataType      string
+			isDefaultNull bool
+			unsigned      bool
+			strict        bool
+		}
+		result struct {
+			goType    string
+			thirdPkg  string
+			isPQArray bool
+		}
+	)
+	var testData = []struct {
+		input input
+		want  result
+	}{
+		{
+			input: input{
+				dataType:      "bigint",
+				isDefaultNull: false,
+				unsigned:      false,
+				strict:        false,
+			},
+			want: result{
+				goType: "int64",
+			},
+		},
+		{
+			input: input{
+				dataType:      "bigint",
+				isDefaultNull: true,
+				unsigned:      false,
+				strict:        false,
+			},
+			want: result{
+				goType: "sql.NullInt64",
+			},
+		},
+		{
+			input: input{
+				dataType:      "bigint",
+				isDefaultNull: false,
+				unsigned:      true,
+				strict:        false,
+			},
+			want: result{
+				goType: "uint64",
+			},
+		},
+		{
+			input: input{
+				dataType:      "_int2",
+				isDefaultNull: false,
+				unsigned:      false,
+				strict:        false,
+			},
+			want: result{
+				goType:    "pq.Int64Array",
+				isPQArray: true,
+			},
+		},
+	}
+	for _, data := range testData {
+		tp, thirdPkg, isPQArray, err := ConvertStringDataType(data.input.dataType, data.input.isDefaultNull, data.input.unsigned, data.input.strict)
+		assert.NoError(t, err)
+		assert.Equal(t, data.want, result{
+			goType:    tp,
+			thirdPkg:  thirdPkg,
+			isPQArray: isPQArray,
+		})
+	}
 }