Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

eorm: Migrate 功能的支持 #214

Open
Stone-afk opened this issue Jul 17, 2023 · 4 comments
Open

eorm: Migrate 功能的支持 #214

Stone-afk opened this issue Jul 17, 2023 · 4 comments

Comments

@Stone-afk
Copy link
Collaborator

Stone-afk commented Jul 17, 2023

场景分析

虽然在一些大厂中,往往修改一个业务表的字段都要走复杂的流程,但是在绝大多数中小公司是没有如此复杂的流程了,所以为了开发者能绕开复杂的建表语句与修改表的语句,方便直接构建 object 和 数据库 table 的映射,大多数 orm 框架 提供给了使用者 Migrate 的功能。
数据库 Migrate 一直是数据库运维人员最为头痛的问题,如果仅仅是一张表增删字段还比较容易,那如果涉及到外键等复杂的关联关系,数据库的 Migrate 就会变得非常困难。

需求分析

实现一个 Migrator 通常涉及以下步骤:

  1. 确定迁移工具的功能和需求:首先,需要明确迁移工具的目标和功能。这包括确定支持的数据库类型、需要执行的迁移操作(创建表、添加列、修改列、删除列等)以及迁移操作的顺序和依赖关系。
  2. 连接到目标数据库:使用数据库驱动程序,建立与目标数据库的连接。根据选择的工具,您可能需要提供数据库的连接字符串、认证信息和其他配置参数。
  3. 实现迁移操作:根据数据库迁移的需求,编写相应的迁移操作。这涉及使用领域特定语言(DSL)或API来描述迁移操作。例如,创建表可以通过定义表的结构和约束来进行,添加列可以指定列名、数据类型和约束等。
  4. 管理迁移版本:追踪和管理数据库迁移的版本是重要的。您可以使用版本号、时间戳或其他标识符来标记每个迁移操作。在执行迁移时,迁移工具可以检查当前数据库的版本,并根据所需的版本执行相应的迁移操作。还可以记录已经执行的迁移操作,以便在回滚或重新执行迁移时使用。
  5. 提供命令行接口或API:为迁移工具提供方便的界面,使用户可以执行迁移操作。这可以是命令行工具,接受参数和选项来指定要执行的迁移操作,并显示结果和错误信息。也可以是提供API,允许应用程序在代码中调用迁移操作。

功能需求

  1. 创建表
    1. 如果用户没有定义主键怎么办?
    2. 要不要提提供 API 定义表的属性?
  2. 删除表
  3. 新增字段
  4. 删除字段
  5. 修改字段
    1. 修改字段类型
    2. 为字段添加/删除索引
    3. 修改字段其他属性 (是否为空、是否自增、是否为默认值)
  6. 是否考虑不同的 sql 方言

开源实例

GORM

GORM 提供了 Migrator 接口,该接口为每个数据库提供了统一的 API 接口,可用来为您的数据库构建独立迁移,
例如:
SQLite 不支持 ALTER COLUMN、DROP COLUMN,当你试图修改表结构,GORM 将创建一个新表、复制所有数据、删除旧表、重命名新表。
一些版本的 MySQL 不支持 rename 列,索引。GORM 将基于您使用 MySQL 的版本执行不同 SQL。

type Migrator interface {
    // AutoMigrate
    AutoMigrate(dst ...interface{}) error

    // Database
    CurrentDatabase() string
    FullDataTypeOf(*schema.Field) clause.Expr

    // Tables
    CreateTable(dst ...interface{}) error
    DropTable(dst ...interface{}) error
    HasTable(dst interface{}) bool
    RenameTable(oldName, newName interface{}) error
    GetTables() (tableList []string, err error)

    // Columns
    AddColumn(dst interface{}, field string) error
    DropColumn(dst interface{}, field string) error
    AlterColumn(dst interface{}, field string) error
    MigrateColumn(dst interface{}, field *schema.Field, columnType ColumnType) error
    HasColumn(dst interface{}, field string) bool
    RenameColumn(dst interface{}, oldName, field string) error
    ColumnTypes(dst interface{}) ([]ColumnType, error)

    // Views
    CreateView(name string, option ViewOption) error
    DropView(name string) error

    // Constraints
    CreateConstraint(dst interface{}, name string) error
    DropConstraint(dst interface{}, name string) error
    HasConstraint(dst interface{}, name string) bool

    // Indexes
    CreateIndex(dst interface{}, name string) error
    DropIndex(dst interface{}, name string) error
    HasIndex(dst interface{}, name string) bool
    RenameIndex(dst interface{}, oldName, newName string) error
}

使用例子
执行迁移操作:

type User struct {
    ID        uint `gorm:"primary_key"`
    ProfileID uint `gorm:"unique"`
    Profile   Profile
    Posts     []Post
    Roles     []Role `gorm:"many2many:user_roles;"`
}

type Profile struct {
    ID     uint `gorm:"primary_key"`
    UserID uint
}

type Post struct {
    ID     uint `gorm:"primary_key"`
    UserID uint
}

type Role struct {
    ID   uint `gorm:"primary_key"`
    Name string
}

func main() {
    db, err := gorm.Open(mysql.Open("root:123456@tcp(localhost:3306)/ebook"))
    if err != nil {
        // 我只会在初始化过程中 panic
        // panic 相当于整个 goroutine 结束
        // 一旦初始化过程出错,应用就不要启动了
        panic(err)
    }

    err = InitTables(db)
    if err != nil {
        panic(err)
    }
}

func InitTables(db *gorm.DB) error {
    return db.AutoMigrate(&User{}, &Profile{}, &Post{}, &Role{})
}

回滚迁移操作:

package main

import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
    "migrations" // 引入自定义的迁移文件包

    _ "github.com/go-sql-driver/mysql"
)

func main() {
    // 连接数据库
    dsn := "user:password@tcp(localhost:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Silent),
    })
    if err != nil {
        panic(err)
    }
    
    // 回滚迁移操作
    err = db.Migrator().Rollback()
    if err != nil {
        panic(err)
    }
    
    // 关闭数据库连接
    db.Close()
}

这些示例展示了使用Gorm进行数据库迁移的一般模式。可以根据自己的需求定义更多的迁移文件和操作。记得在执行迁移操作之前,需要确保正确配置数据库连接,引入Gorm和相关数据库驱动,并按照示例中的方式调用相应的迁移函数和方法。

版本控制

需要注意的是,GORM 虽然提供了不错的数据库迁移功能,但是距离理想的 “版本控制” 仍有距离。不支持:版本记录、版本回退、版本选择。这些都需要开发者自行封装。

Beego ORM

Beego 的 migrate 的接口设计和gorm差不多,但核心逻辑主要是基于命令行工具和代码生成实现的。

//	CREATE TABLE `migrations` (
//		`id_migration` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'surrogate key',
//		`name` varchar(255) DEFAULT NULL COMMENT 'migration name, unique',
//		`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'date migrated or rolled back',
//		`statements` longtext COMMENT 'SQL statements for this migration',
//		`rollback_statements` longtext,
//		`status` enum('update','rollback') DEFAULT NULL COMMENT 'update indicates it is a normal migration while rollback means this migration is rolled back',
//		PRIMARY KEY (`id_migration`)
//	) ENGINE=InnoDB DEFAULT CHARSET=utf8;

// Migrationer is an interface for all Migration struct
type Migrationer interface {
	Up()
	Down()
	Reset()
	Exec(name, status string) error
	GetCreated() int64
}

// Migration defines the migrations by either SQL or DDL
type Migration struct {
	sqls           []string
	Created        string
	TableName      string
	Engine         string
	Charset        string
	ModifyType     string
	Columns        []*Column
	Indexes        []*Index
	Primary        []*Column
	Uniques        []*Unique
	Foreigns       []*Foreign
	Renames        []*RenameColumn
	RemoveColumns  []*Column
	RemoveIndexes  []*Index
	RemoveUniques  []*Unique
	RemoveForeigns []*Foreign
}

var migrationMap map[string]Migrationer

func init() {
	migrationMap = make(map[string]Migrationer)
}

版本控制

Beego ORM,版本控制是基于 Migration 结构体模型表,该表在初始化时会创建在数据库中,用来记录数据库迁移的版本的变化 ,包括创建、更新、滚等操作。
使用样例
生成数据库迁移文件

bee generate migration User

数据库迁移

// Run the migrations
func (m *User_20210806_105600) Up() {
    // use m.SQL("CREATE TABLE ...") to make schema update
    m.CreateTable("user", "innodb", "utf8mb4")
    m.PriCol("id").SetAuto(true).SetDataType("int").SetUnsigned(true)
    m.NewCol("username").SetDataType("varchar(255)")
    m.NewCol("password").SetDataType("varchar(255)")
    m.NewCol("email").SetDataType("varchar(255)").SetNullable(true)
    m.NewCol("login_count").SetDataType("int").SetUnsigned(true)
    m.NewCol("last_time").SetDataType("datetime")
    m.NewCol("last_ip").SetDataType("varchar(255)").SetNullable(true)
    m.NewCol("state").SetDataType("smallint(2)")
    m.NewCol("created_at").SetDataType("datetime")
    m.NewCol("updated_at").SetDataType("datetime")
    m.SQL(m.GetSQL())
}

// Reverse the migrations
func (m *User_20210806_105600) Down() {
    // use m.SQL("DROP TABLE ...") to reverse schema update
    m.SQL("DROP TABLE IF EXISTS user")
}
bee migrate -driver=mysql -conn=root:123456@tcp(127.0.0.1:3306)/beego-admin

golang-migrate

**golang-migrate **支持非常多的数据库类型,包括: cockroachdb, firebird, postgresql, redshift, clickhouse, postgres, cockroach, firebirdsql, mysql, crdb-postgres, mongodb, mongodb+srv, neo4j, pgx, spanner, sqlserver, stub, cassandra

type Migrate struct {
	sourceName   string
	sourceDrv    source.Driver
	databaseName string
	databaseDrv  database.Driver

	// Log accepts a Logger interface
	Log Logger

	// GracefulStop accepts `true` and will stop executing migrations
	// as soon as possible at a safe break point, so that the database
	// is not corrupted.
	GracefulStop chan bool
	isLockedMu   *sync.Mutex

	isGracefulStop bool
	isLocked       bool

	// PrefetchMigrations defaults to DefaultPrefetchMigrations,
	// but can be set per Migrate instance.
	PrefetchMigrations uint

	// LockTimeout defaults to DefaultLockTimeout,
	// but can be set per Migrate instance.
	LockTimeout time.Duration
}
// Migration holds information about a migration.
// It is initially created from data coming from the source and then
// used when run against the database.
type Migration struct {
	// Identifier can be any string to help identifying
	// the migration in the source.
	Identifier string

	// Version is the version of this migration.
	Version uint

	// TargetVersion is the migration version after this migration
	// has been applied to the database.
	// Can be -1, implying that this is a NilVersion.
	TargetVersion int

	// Body holds an io.ReadCloser to the source.
	Body io.ReadCloser

	// BufferedBody holds an buffered io.Reader to the underlying Body.
	BufferedBody io.Reader

	// BufferSize defaults to DefaultBufferSize
	BufferSize uint

	// bufferWriter holds an io.WriteCloser and pipes to BufferBody.
	// It's an *Closer for flow control.
	bufferWriter io.WriteCloser

	// Scheduled is the time when the migration was scheduled/ queued.
	Scheduled time.Time

	// StartedBuffering is the time when buffering of the migration source started.
	StartedBuffering time.Time

	// FinishedBuffering is the time when buffering of the migration source finished.
	FinishedBuffering time.Time

	// FinishedReading is the time when the migration source is fully read.
	FinishedReading time.Time

	// BytesRead holds the number of Bytes read from the migration source.
	BytesRead int64
}

这是一个简单的工具,可基于文件进行迁移。它带有 Go 库和 CLI 工具,可创建 SQL 迁移文件并管理架构版本。

设计

方案一
提供一个 Migrate API,支持根据 Go Struct 结构自动生成对应的表结构 。
方案二
通过 CLI 工具 辅助生成 sql 迁移文件,用户需要手动将迁移sql 添加至迁移文件中、支持版本管理, 类似于 golang-migrate 。
本框架先选择方案一(gorm等框架的的方式),通过注册结构体来实现对数据库的模型映射。
这个功能的实现要分成两个部分:

  • 一个是纯粹的 DDL 语句生成,也就是对应到我们的各种 sql Builder。
  • 第二个是利用第一个部分,来生成对应的 DDL。

DDL 语句生成

DDL(Data Definition Language):数据定义语言,定义语言就是定义关系模式、删除关系、修改关系模式以及创建数据库中的各种对象,比如表、聚簇、索引、视图、函数、存储过程和触发器等等。
数据定义语言是由SQL语言集中负责数据结构定义与数据库对象定义的语言,并且由CREATE、ALTER、DROP和TRUNCATE四个语法组成。比如:

--创建一个student表
    create table student(
      id int identity(1,1) not null,
      name varchar(20) null,
      course varchar(20) null,
      grade numeric null
    )
--student表增加一个年龄字段
alter table student add age int NULL
--student表删除年龄字段,删除的字段前面需要加column,不然会报错,而添加字段不需要加column
alter table student drop Column age
--删除student表
drop table student --删除表的数据和表的结构
truncate table student -- 只是清空表的数据,,但并不删除表的结构,student表还在只是数据为空

Creater

MySQL
CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name
    (create_definition,...)
    [table_options]
    [partition_options]

CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name
    [(create_definition,...)]
    [table_options]
    [partition_options]
    [IGNORE | REPLACE]
    [AS] query_expression

CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name
    { LIKE old_tbl_name | (LIKE old_tbl_name) }

create_definition: {
    col_name column_definition
  | {INDEX | KEY} [index_name] [index_type] (key_part,...)
      [index_option] ...
  | {FULLTEXT | SPATIAL} [INDEX | KEY] [index_name] (key_part,...)
      [index_option] ...
  | [CONSTRAINT [symbol]] PRIMARY KEY
      [index_type] (key_part,...)
      [index_option] ...
  | [CONSTRAINT [symbol]] UNIQUE [INDEX | KEY]
      [index_name] [index_type] (key_part,...)
      [index_option] ...
  | [CONSTRAINT [symbol]] FOREIGN KEY
      [index_name] (col_name,...)
      reference_definition
  | check_constraint_definition
}

column_definition: {
    data_type [NOT NULL | NULL] [DEFAULT {literal | (expr)} ]
      [VISIBLE | INVISIBLE]
      [AUTO_INCREMENT] [UNIQUE [KEY]] [[PRIMARY] KEY]
      [COMMENT 'string']
      [COLLATE collation_name]
      [COLUMN_FORMAT {FIXED | DYNAMIC | DEFAULT}]
      [ENGINE_ATTRIBUTE [=] 'string']
      [SECONDARY_ENGINE_ATTRIBUTE [=] 'string']
      [STORAGE {DISK | MEMORY}]
      [reference_definition]
      [check_constraint_definition]
  | data_type
      [COLLATE collation_name]
      [GENERATED ALWAYS] AS (expr)
      [VIRTUAL | STORED] [NOT NULL | NULL]
      [VISIBLE | INVISIBLE]
      [UNIQUE [KEY]] [[PRIMARY] KEY]
      [COMMENT 'string']
      [reference_definition]
      [check_constraint_definition]
}

data_type:
    (see Chapter 11, Data Types)

key_part: {col_name [(length)] | (expr)} [ASC | DESC]

index_type:
    USING {BTREE | HASH}

index_option: {
    KEY_BLOCK_SIZE [=] value
  | index_type
  | WITH PARSER parser_name
  | COMMENT 'string'
  | {VISIBLE | INVISIBLE}
  |ENGINE_ATTRIBUTE [=] 'string'
  |SECONDARY_ENGINE_ATTRIBUTE [=] 'string'
}

check_constraint_definition:
    [CONSTRAINT [symbol]] CHECK (expr) [[NOT] ENFORCED]

reference_definition:
    REFERENCES tbl_name (key_part,...)
      [MATCH FULL | MATCH PARTIAL | MATCH SIMPLE]
      [ON DELETE reference_option]
      [ON UPDATE reference_option]

reference_option:
    RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT

table_options:
    table_option [[,] table_option] ...

table_option: {
    AUTOEXTEND_SIZE [=] value
  | AUTO_INCREMENT [=] value
  | AVG_ROW_LENGTH [=] value
  | [DEFAULT] CHARACTER SET [=] charset_name
  | CHECKSUM [=] {0 | 1}
  | [DEFAULT] COLLATE [=] collation_name
  | COMMENT [=] 'string'
  | COMPRESSION [=] {'ZLIB' | 'LZ4' | 'NONE'}
  | CONNECTION [=] 'connect_string'
  | {DATA | INDEX} DIRECTORY [=] 'absolute path to directory'
  | DELAY_KEY_WRITE [=] {0 | 1}
  | ENCRYPTION [=] {'Y' | 'N'}
  | ENGINE [=] engine_name
  | ENGINE_ATTRIBUTE [=] 'string'
  | INSERT_METHOD [=] { NO | FIRST | LAST }
  | KEY_BLOCK_SIZE [=] value
  | MAX_ROWS [=] value
  | MIN_ROWS [=] value
  | PACK_KEYS [=] {0 | 1 | DEFAULT}
  | PASSWORD [=] 'string'
  | ROW_FORMAT [=] {DEFAULT | DYNAMIC | FIXED | COMPRESSED | REDUNDANT | COMPACT}
  | START TRANSACTION 
  | SECONDARY_ENGINE_ATTRIBUTE [=] 'string'
  | STATS_AUTO_RECALC [=] {DEFAULT | 0 | 1}
  | STATS_PERSISTENT [=] {DEFAULT | 0 | 1}
  | STATS_SAMPLE_PAGES [=] value
  | tablespace_option
  | UNION [=] (tbl_name[,tbl_name]...)
}

partition_options:
    PARTITION BY
        { [LINEAR] HASH(expr)
        | [LINEAR] KEY [ALGORITHM={1 | 2}] (column_list)
        | RANGE{(expr) | COLUMNS(column_list)}
        | LIST{(expr) | COLUMNS(column_list)} }
    [PARTITIONS num]
    [SUBPARTITION BY
        { [LINEAR] HASH(expr)
        | [LINEAR] KEY [ALGORITHM={1 | 2}] (column_list) }
      [SUBPARTITIONS num]
    ]
    [(partition_definition [, partition_definition] ...)]

partition_definition:
    PARTITION partition_name
        [VALUES
            {LESS THAN {(expr | value_list) | MAXVALUE}
            |
            IN (value_list)}]
        [[STORAGE] ENGINE [=] engine_name]
        [COMMENT [=] 'string' ]
        [DATA DIRECTORY [=] 'data_dir']
        [INDEX DIRECTORY [=] 'index_dir']
        [MAX_ROWS [=] max_number_of_rows]
        [MIN_ROWS [=] min_number_of_rows]
        [TABLESPACE [=] tablespace_name]
        [(subpartition_definition [, subpartition_definition] ...)]

subpartition_definition:
    SUBPARTITION logical_name
        [[STORAGE] ENGINE [=] engine_name]
        [COMMENT [=] 'string' ]
        [DATA DIRECTORY [=] 'data_dir']
        [INDEX DIRECTORY [=] 'index_dir']
        [MAX_ROWS [=] max_number_of_rows]
        [MIN_ROWS [=] min_number_of_rows]
        [TABLESPACE [=] tablespace_name]

tablespace_option:
    TABLESPACE tablespace_name [STORAGE DISK]
  | [TABLESPACE tablespace_name] STORAGE MEMORY

query_expression:
    SELECT ...   (Some valid select or union statement)

先看简化后的语法解析

CREATE [TEMPORARY] TABLE [IF NOT EXISTS] table_name
[(
COLUMN_DEFINITION,
...
)]
[TABLE_OPTIONS]
[SELECT_STATEMENT]
  1. TEMPORARY

用于创建临时表,CREATE TEMPORARY TABLE table_name;这个临时表在当前会话结束或者连接断开后将自动消失。

  1. IF NOT EXISTS

实际上就是在建表前加上一个判断,只有该表目前尚不存在时才执行CREATE TABLE操作。用此选项可以避免出现表已经存在无法再新建的错误。

  1. table_name

需要创建的表名。该表名必须符合标识符规则,通常的做法是在表名中仅使用字母、数字及下划线。

  1. COLUMN_DEFINITION

所创建的表中各列的相关属性定义。
语法如下:

column_name type [NOT NULL | NULL] [DEFAULT default_value] [AUTO_INCREMENT] 
[PRIMARY KEY]
| PRIMARY KEY (index_col_name,...) 
| KEY [index_name] (index_col_name,...) 
| INDEX [index_name] (index_col_name,...) 
| UNIQUE [INDEX] [index_name] (index_col_name,...) 
| [CONSTRAINT symbol] FOREIGN KEY index_name (index_col_name,...) 
[COMMENT reference_definition] 
or CHECK (expr) 

如上所示,列的相关属性定义语法内容相当丰富。
1)column_name
表中列的名字。必须符合标识符规则,而且在表中要唯一。
2)type
列的数据类型。有的数据类型需要指明长度n,并用括号括起。
3)NOT NULL | NULL
指定该列是否允许为空。如果既不指定NULL也不指定NOT NULL,列被认为指定了NULL。
4)DEFAULT default_value
为列指定默认值。如果没有为列指定默认值,MySQL自动地分配一个。
如果列可以取NULL作为值,缺省值是NULL。
如果列被声明为NOT NULL,缺省值取决于列类型:

  • 对于没有声明AUTO_INCREMENT属性的数字类型,缺省值是0。对于一个AUTO_INCREMENT列,缺省值是在顺序中的下一个值。
  • 对于除TIMESTAMP的日期和时间类型,缺省值是该类型适当的“零”值。对于表中第一个TIMESTAMP列,缺省值是当前的日期和时间。
  • 对于除ENUM的字符串类型,缺省是空字符串。对于ENUM,缺省值是第一个枚举值。

5)AUTO_INCREMENT
设置该列有自增属性,只有整型列才能设置此属性。
当你插入NULL值或0到一个AUTO_INCREMENT列中时,列被设置为value+1,在这里value是此前表中该列的最大值。AUTO_INCREMENT顺序从1开始。每个表只能有一个AUTO_INCREMENT列,并且它必须被索引。

  1. TABLE_OPTIONS

设置表的一些属性定义:
1)引擎设置
例如:ENGINE|TYPE = MYISAM
2)字符集设置
例如:CHARACTER SET gbk
3)排序规则设置
例如:COLLATE gbk_chinese_ci
COLLATE是用来做什么的?
是用来排序的规则。对于mysql中那些字符类型的列,如VARCHAR,CHAR,TEXT类型的列,都需要有一个COLLATE类型来告知mysql如何对该列进行排序和比较。简而言之,COLLATE会影响到ORDER BY语句的顺序,会影响到WHERE条件中大于小于号筛选出来的结果,会影响DISTINCT、GROUP BY、HAVING语句的查询结果。另外,mysql建索引的时候,如果索引列是字符类型,也会影响索引创建,只不过这种影响我们感知不到。总之,凡是涉及到字符类型比较或排序的地方,都会和COLLATE有关。

  1. SELECT_STATEMENT

//如下直接通过select查询出指定数据存入新建的表中

CREATE TABLE ...
SELECT ...

通常,MySQL的 create 语句如下:

CREATE TABLE `orders` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `user_id` int(11) NOT NULL,
  `product_id` int(11) NOT NULL,
  `quantity` int(11) NOT NULL,
  `code` varchar(50) NOT NULL UNIQUE,
  `status` tinyint(1) NOT NULL DEFAULT 0,
  `detail` text NOT NULL,
  PRIMARY KEY (`id`),
  INDEX `idx_name` (`name`),
  FULLTEXT INDEX `idx_detail` (`detail`),
  FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
  FOREIGN KEY (`product_id`) REFERENCES `products`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `age` int(11) NOT NULL,
  CHECK (`age` >= 18),
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
SQLite

image.png

“CREATE TABLE”命令用于在 SQLite 数据库中创建一个新表。CREATE TABLE 命令指定新表的以下属性:

  • 新表的名称。
  • 在其中创建新表的数据库。可以在主数据库、临时数据库或任何附加数据库中创建表。
  • 表中每一列的名称。
  • 表中每一列的声明类型。
  • 表中每一列的默认值或表达式。
  • 用于每列的默认排序规则序列。
  • (可选)表的 PRIMARY KEY。支持单列和复合(多列)主键。
  • 每个表的一组 SQL 约束。SQLite 支持 UNIQUE、NOT NULL、CHECK 和 FOREIGN KEY 约束。
  • (可选)生成的列约束。
  • 该表是否为WITHOUT ROWID表。
  • 表是否经过严格的类型检查。

create table 的基本语法如下

CREATE TABLE database_name.table_name(
   column1 datatype  PRIMARY KEY(one or more columns),
   column2 datatype,
   column3 datatype,
   .....
   columnN datatype,
);

通用实例如下:

sqlite> CREATE TABLE COMPANY(
   ID INT PRIMARY KEY     NOT NULL,
   NAME           TEXT    NOT NULL,
   AGE            INT     NOT NULL,
   ADDRESS        CHAR(50),
   SALARY         REAL
);

SQLite 同样也包括类型约束,索引约束、外键约束等语法。约束是在表的数据列上强制执行的规则。这些是用来限制可以插入到表中的数据类型。这确保了数据库中数据的准确性和可靠性。
约束可以是列级或表级。列级约束仅适用于列,表级约束被应用到整个表。
以下是在 SQLite 中常用的约束。

  • NOT NULL 约束:确保某列不能有 NULL 值。
  • DEFAULT 约束:当某列没有指定值时,为该列提供默认值。
  • UNIQUE 约束:确保某列中的所有值是不同的。
  • PRIMARY Key 约束:唯一标识数据库表中的各行/记录。
  • CHECK 约束:CHECK 约束确保某列中的所有值满足一定条件。
  • INDEX 约束: 通过创建语句时的INDEX语法创建该列的索引

NOT NULL 约束
默认情况下,列可以保存 NULL 值。如果您不想某列有 NULL 值,那么需要在该列上定义此约束,指定在该列上不允许 NULL 值。NULL 与没有数据是不一样的,它代表着未知的数据。

CREATE TABLE COMPANY(
   ID INT PRIMARY KEY     NOT NULL,
   NAME           TEXT    NOT NULL,
   AGE            INT     NOT NULL,
   ADDRESS        CHAR(50),
   SALARY         REAL
);

DEFAULT 约束
DEFAULT 约束在 INSERT INTO 语句没有提供一个特定的值时,为列提供一个默认值。

CREATE TABLE COMPANY(
   ID INT PRIMARY KEY     NOT NULL,
   NAME           TEXT    NOT NULL,
   AGE            INT     NOT NULL,
   ADDRESS        CHAR(50),
   SALARY         REAL    DEFAULT 50000.00
);

UNIQUE 约束
UNIQUE 约束防止在一个特定的列存在两个记录具有相同的值。在 COMPANY 表中,例如,您可能要防止两个或两个以上的人具有相同的年龄。

CREATE TABLE COMPANY(
   ID INT PRIMARY KEY     NOT NULL,
   NAME           TEXT    NOT NULL,
   AGE            INT     NOT NULL UNIQUE,
   ADDRESS        CHAR(50),
   SALARY         REAL    DEFAULT 50000.00
);

INDEX 约束
普通索引
CREATE TABLE 语句中,您可以使用 INDEX 子句为表的某个列创建索引。以下是一个示例,演示了如何在创建表时为某个列创建索引:

CREATE TABLE your_table_name (
  column1 datatype,
  column2 datatype,
  column3 datatype,
  ...
  INDEX index_name (column_name),
  ...
);

联合索引
当在 MySQL 中创建表时,您可以使用 CREATE TABLE 语句,并通过 INDEX 子句定义联合索引。以下是一个更详细的示例,展示了如何创建一个表并添加联合索引:

CREATE TABLE your_table_name (
  column1 datatype,
  column2 datatype,
  column3 datatype,
  ...
  INDEX index_name (column1, column2, column3),
  ...
);

在上面的示例中,需要将 your_table_name 替换为表的实际名称。然后,根据需要定义表的列,并将 column1、column2、column3 替换为实际的列名,以及 datatype 替换为相应的数据类型。
接下来,在 INDEX 子句中定义联合索引。您可以为联合索引指定一个名称,将 index_name 替换为索引的实际名称。然后,列出要包含在联合索引中的列名,按照所需的顺序列出。在示例中,我们使用 column1、column2 和 column3 列作为联合索引的组成部分。
需要注意的是,可以在一个 CREATE TABLE 语句中定义多个索引,包括单列索引和联合索引。每个索引都可以使用不同的列组合。如果要定义多个索引,请确保为每个索引指定唯一的名称。
通过使用适当的列和数据类型,以及定义所需的联合索引,可以根据您的表结构和查询需求来创建具有联合索引的表。
PRIMARY KEY 约束
PRIMARY KEY 约束唯一标识数据库表中的每个记录。在一个表中可以有多个 UNIQUE 列,但只能有一个主键。在设计数据库表时,主键是很重要的。主键是唯一的 ID。
使用主键来引用表中的行。可通过把主键设置为其他表的外键,来创建表之间的关系。由于"长期存在编码监督",在 SQLite 中,主键可以是 NULL,这是与其他数据库不同的地方。
主键是表中的一个字段,唯一标识数据库表中的各行/记录。主键必须包含唯一值。主键列不能有 NULL 值。
一个表只能有一个主键,它可以由一个或多个字段组成。当多个字段作为主键,它们被称为复合键
如果一个表在任何字段上定义了一个主键,那么在这些字段上不能有两个记录具有相同的值。

CREATE TABLE COMPANY(
   ID INT PRIMARY KEY     NOT NULL,
   NAME           TEXT    NOT NULL,
   AGE            INT     NOT NULL,
   ADDRESS        CHAR(50),
   SALARY         REAL
);

CHECK 约束
CHECK 约束启用输入一条记录要检查值的条件。如果条件值为 false,则记录违反了约束,且不能输入到表。

CREATE TABLE COMPANY3(
   ID INT PRIMARY KEY     NOT NULL,
   NAME           TEXT    NOT NULL,
   AGE            INT     NOT NULL,
   ADDRESS        CHAR(50),
   SALARY         REAL    CHECK(SALARY > 0)
);

删除约束
SQLite 支持 ALTER TABLE 的有限子集。在 SQLite 中,ALTER TABLE 命令允许用户重命名表,或向现有表添加一个新的列。重命名列,删除一列,或从一个表中添加或删除约束都是不可能的。
外键约束
在 SQLite 中启用外键功能需要进行一些设置。默认情况下,SQLite 中的外键功能是禁用的,我们需要手动开启它。在使用外键之前,我们需要确认 SQLite 版本是否支持外键功能。执行以下命令可以获取当前版本的 SQLite 支持情况:

sqlite> SELECT sqlite_version();

如果返回的版本号中包含“foreign_key”,则说明 SQLite 支持外键功能。
下面是一个示例,演示了如何在 SQLite 中创建表并添加外键约束:

-- 创建产品表
CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    name TEXT,
    price REAL
);

-- 创建订单表并添加外键约束
CREATE TABLE orders (
    id INTEGER PRIMARY KEY,
    product_id INTEGER,
    quantity INTEGER,
    FOREIGN KEY (product_id) REFERENCES products(id)
);

设计分析
CREATE 操作需要考虑表的字段属性,字段长度、是否为Null、是否有默认值、是否是主键、是否自增、用什么存储引擎、是否外键约束等等。
用框架已有的功能,那就是通过在模型结构体的字段中使用 tag 标识,通过解析结构体时检测。
所以必须在表元数据和列元数据的结构体中加上,表结构定义时候的 Tag 属性。
定义如下常见的eorm模型标签:

  1. eorm:"primary_key": 将字段设置为主键。
  2. eorm:"auto_increment": 将字段设置为自增。
  3. eorm:"type:data_type": 指定字段的数据库数据类型。
  4. eorm:"size:length": 指定字段的长度或大小。
  5. eorm:"default:default_value": 指定字段的默认值。
  6. eorm:"not null": 指定字段不能为空。
  7. eorm:"unique": 指定字段的值在表中必须唯一。
  8. eorm:"index": 为字段创建索引。
  9. eorm:"uniqueIndex": 为字段创建唯一索引。
  10. eorm:"primary_key;auto_increment": 将字段同时设置为主键和自增。
  11. eorm:"-": 忽略字段,不会映射到数据库表中。
  12. eorm:"comment:text" 为字段添加注释。

以下是示例结构体模型:

type User struct {
    ID        uint   `eorm:"id;primary_key;auto_increment"`
    Name      string `eorm:"size:255"`
    Age       int    `eorm:"age"`
    Email     string `eorm:"uniqueIndex"`
    CreatedAt time.Time
    UpdatedAt time.Time
}

索引示例

type User struct {
    ID        uint   `gorm:"primary_key;auto_increment"`
    Name      string `gorm:"index"`
    Age       int    `gorm:"index:idx_age"`
    Email     string `gorm:"uniqueIndex"`
    CreatedAt time.Time
    UpdatedAt time.Time
}

type User struct {
    ID   uint   `gorm:"column:id;primary_key;auto_increment"`
    Name string `gorm:"column:name;uniqueIndex:idx_name"`
    // ...
}

联合索引示例

type User struct {
    ID        uint   `eorm:"primary_key;auto_increment"`
    Name      string `eorm:"index:idx_name_age"`
    Age       int    `eorm:"index:idx_name_age"`
    Email     string `eorm:"uniqueIndex"`
    CreatedAt time.Time
    UpdatedAt time.Time
}

指定类型示例

type User struct {
    ID   uint   `eorm:"column:id;primary_key;auto_increment"`
    Name string `eorm:"column:name;type:string"`
    Age  int    `eorm:"column:age;type:int"`
    // ...
}

默认值

type User struct {
    ID       uint   `eorm:"primary_key;auto_increment"`
    Name     string `eorm:"default:'John Doe'"`
    Age      int    `eorm:"default:18"`
    IsActive bool   `eorm:"default:true"`
    // .

字段注释

type User struct {
    ID   uint   `eorm:"column:id;primary_key;auto_increment" comment:"用户ID"`
    Name string `eorm:"comment:"用户姓名"`
    Age  int    `eorm:"comment:"用户年龄"`
    // ...
}

元数据模型,需要引入索引、约束、字段类型、默认值等复杂的结构体字段,来给上层的 build 语句构建语法

var (
	TimeReflectType    = reflect.TypeOf(time.Time{})
	TimePtrReflectType = reflect.TypeOf(&time.Time{})
	ByteReflectType    = reflect.TypeOf(uint8(0))
)

type (
	DataType string
	TimeType int64
)

const (
	Bool   DataType = "bool"
	Int    DataType = "int"
	Uint   DataType = "uint"
	Float  DataType = "float"
	String DataType = "string"
	Time   DataType = "time"
	Bytes  DataType = "bytes"
)

type ColumnIndex struct {
	IsPrimaryKey bool
	Type         string
	name         string
	Columns      []string
}

// TableMeta represents data model, or a table
type TableMeta struct {
	TableName string
	Engine    string
	Charset   string
	Columns   []*ColumnMeta
	// FieldMap 是字段名到列元数据的映射
	FieldMap map[string]*ColumnMeta
	// ColumnMap 是列名到列元数据的映射
	ColumnMap map[string]*ColumnMeta
	Typ       reflect.Type

	ShardingAlgorithm sharding.Algorithm
}

// ColumnMeta represents model's field, or column
type ColumnMeta struct {
	ColumnName        string
	FieldName         string
	Type              reflect.Type
	IndirectFieldType reflect.Type
	Val               reflect.Value
	Tag               reflect.StructTag
	TagMap            map[string]string

	FieldType string
	DataType  DataType

	// 是否主键
	IsPrimaryKey bool
	// 是否自增
	IsAutoIncrement        bool
	AutoIncrementIncrement int64
	// 默认值
	HasDefaultValue       bool
	DefaultValue          string
	DefaultValueInterface any
	// 是否非空
	IsNull bool
	// 是否唯一索引
	Unique bool
	// 列注释
	Comment string
	// 列大小
	Size int
	// 是否忽略迁移
	IgnoreMigration bool

	// decimal
	Precision int
	Scale     int

	Offset uintptr
	// FieldPos
	// FieldIndexes 用于表达从最外层结构体找到当前ColumnMeta对应的Field所需要的索引集
	FieldPos      []int
	ColumnIndexes map[string]ColumnIndex
}

索引的解析,注意:索引的解析也是通过模型结构体的tag来解析的。

type ColumnIndex struct {
	Name         string
	IsPrimaryKey bool
	IsUnique     bool
	ColumnName   string
	ColumnList   []string
	IndexClass   string // UNIQUE | FULLTEXT | SPATIAL
	Comment      string
	Length       int
	Columns      []*ColumnMeta
}

func (meta *ColumnMeta) ParseIndexes() (map[string]ColumnIndex, error) {
	var (
		err     error
		indexes map[string]ColumnIndex
	)
	if meta.TagMap["INDEX"] != "" || meta.TagMap["UNIQUEINDEX"] != "" {
		fieldIndexes, err := parseColumnIndexes(meta)
		if err != nil {
			return indexes, err
		}
		for _, index := range fieldIndexes {
			idx := indexes[index.Name]
			idx.Name = index.Name
			if idx.IndexClass == "" {
				idx.IndexClass = index.IndexClass
			}
			if idx.Comment == "" {
				idx.Comment = index.Comment
			}

			idx.Columns = append(idx.Columns, index.Columns...)

			indexes[index.Name] = idx

			if index.IndexClass == "UNIQUE" && len(index.Columns) == 1 {
				index.Columns[0].Unique = true
			}
		}
	}
	return indexes, err
}

func (meta *TableMeta) LoadIndex(name string) *ColumnIndex {
	if meta != nil {
		for _, colMeta := range meta.Columns {
			idx := colMeta.LoadColumnIndex(name)
			if idx != nil {
				return idx
			}
		}
	}
	return nil
}

func (meta *ColumnMeta) LoadColumnIndex(colName string) *ColumnIndex {
	if meta != nil {
		for _, index := range meta.ColumnIndexes {
			if index.Name == colName {
				return &index
			}

			for _, field := range index.Columns {
				if field.ColumnName == colName {
					return &index
				}
			}
		}
	}
	return nil
}

func parseColumnIndexes(field *ColumnMeta) (indexes []ColumnIndex, err error) {
	for _, value := range strings.Split(field.Tag.Get("eorm"), ";") {
		if value != "" {
			v := strings.Split(value, ":")
			k := strings.TrimSpace(strings.ToUpper(v[0]))
			if k == "INDEX" || k == "UNIQUEINDEX" {
				var (
					name       string
					tag        = strings.Join(v[1:], ":")
					idx        = strings.Index(tag, ",")
					tagSetting = strings.Join(strings.Split(tag, ",")[1:], ",")
					settings   = ParseTagMap(tagSetting, ",")
					length, _  = strconv.Atoi(settings["LENGTH"])
				)

				if idx == -1 {
					idx = len(tag)
				}

				if idx != -1 {
					name = tag[0:idx]
				}

				var isUnique bool
				if (k == "UNIQUEINDEX") || settings["UNIQUE"] != "" {
					settings["CLASS"] = "UNIQUE"
					isUnique = true
				}

				indexes = append(indexes, ColumnIndex{
					Name:       name,
					ColumnName: field.ColumnName,
					IsUnique:   isUnique,
					IndexClass: settings["CLASS"],
					Comment:    settings["COMMENT"],
					Length:     length,
					Columns:    []*ColumnMeta{field},
				})
			}
		}
	}
	err = nil
	return
}

改造注册模型下的解析字段方法 parseFields 。

func ParseTagMap(str string, sep string) map[string]string {
	tagMap := map[string]string{}
	names := strings.Split(str, sep)

	for i := 0; i < len(names); i++ {
		j := i
		if len(names[j]) > 0 {
			for {
				if names[j][len(names[j])-1] == '\\' {
					i++
					names[j] = names[j][0:len(names[j])-1] + sep + names[i]
					names[i] = ""
				} else {
					break
				}
			}
		}

		values := strings.Split(names[j], ":")
		// k := strings.TrimSpace(strings.ToUpper(values[0]))
		ks := strings.Split(values[0], "_")
		k := strings.TrimSpace(strings.ToUpper(strings.Join(ks, "")))

		if len(values) >= 2 {
			tagMap[k] = strings.Join(values[1:], ":")
		} else if k != "" {
			tagMap[k] = k
		}
	}

	return tagMap
}

// CheckTruth check string true or not
func CheckTruth(vals ...string) bool {
	for _, val := range vals {
		if val != "" && !strings.EqualFold(val, "false") {
			return true
		}
	}
	return false
}

func (t *tagMetaRegistry) parseFields(v reflect.Type, fieldPos []int,
	columnMetas *[]*ColumnMeta, fieldMap map[string]*ColumnMeta,
	pOffset uintptr) error {
	var err error
	lens := v.NumField()
	for i := 0; i < lens; i++ {
		structField := v.Field(i)
		// tag := structField.Tag.Get("eorm")
		tagMap := ParseTagMap(structField.Tag.Get("eorm"), ";")
		if CheckTruth(tagMap["-"]) {
			// skip the field
			continue
		}

		// 检查列有没有冲突
		if fieldMap[structField.Name] != nil {
			return errs.NewFieldConflictError(v.Name() + "." + structField.Name)
		}
		// 是组合
		if structField.Anonymous {
			// 不支持使用指针的组合
			if structField.Type.Kind() != reflect.Struct {
				return errs.ErrCombinationIsNotStruct
			}
			// 递归解析
			o := structField.Offset + pOffset
			err = t.parseFields(structField.Type, append(fieldPos, i), columnMetas, fieldMap, o)
			if err != nil {
				return err
			}
			continue
		}

		size, err := strconv.Atoi(tagMap["SIZE"])
		if err != nil {
			return err
		}
		columnMeta := &ColumnMeta{
			//FieldType:       fieldType,
			Size:              size,
			Tag:               structField.Tag,
			TagMap:            tagMap,
			FieldName:         structField.Name,
			Type:              structField.Type,
			IndirectFieldType: structField.Type,
			Offset:            structField.Offset + pOffset,
			FieldPos:          append(fieldPos, i),
			IsPrimaryKey:      CheckTruth(tagMap["PRIMARYKEY"], tagMap["PRIMARY_KEY"]),
			IsAutoIncrement:   CheckTruth(tagMap["AUTOINCREMENT"]),
			HasDefaultValue:   CheckTruth(tagMap["DEFAULT"]),
			IsNull:            CheckTruth(tagMap["NULL"]),
			ColumnName:        underscoreName(structField.Name),
			Comment:           tagMap["COMMENT"],
		}

		if v, ok := columnMeta.TagMap["DEFAULT"]; ok {
			columnMeta.HasDefaultValue = true
			columnMeta.DefaultValue = v
		}

		fieldValue := reflect.New(columnMeta.IndirectFieldType)
		valuer, isValuer := fieldValue.Interface().(driver.Valuer)
		if isValuer {
			if _, ok := fieldValue.Interface().(ColumnTypeInterface); !ok {
				if v, err := valuer.Value(); reflect.ValueOf(v).IsValid() && err == nil {
					fieldValue = reflect.ValueOf(v)
				}

				// Use the field struct's first field type as data type, e.g: use `string` for sql.NullString
				var getRealFieldValue func(reflect.Value)
				getRealFieldValue = func(v reflect.Value) {
					var (
						rv     = reflect.Indirect(v)
						rvType = rv.Type()
					)

					if rv.Kind() == reflect.Struct && !rvType.ConvertibleTo(TimeReflectType) {
						for i := 0; i < rvType.NumField(); i++ {
							for key, value := range ParseTagMap(rvType.Field(i).Tag.Get("eorm"), ";") {
								if _, ok := columnMeta.TagMap[key]; !ok {
									columnMeta.TagMap[key] = value
								}
							}
						}

						for i := 0; i < rvType.NumField(); i++ {
							newFieldType := rvType.Field(i).Type
							for newFieldType.Kind() == reflect.Ptr {
								newFieldType = newFieldType.Elem()
							}

							fieldValue = reflect.New(newFieldType)
							if rvType != reflect.Indirect(fieldValue).Type() {
								getRealFieldValue(fieldValue)
							}

							if fieldValue.IsValid() {
								return
							}
						}
					}
				}

				getRealFieldValue(fieldValue)
			}
		}

		// default value is function or null or blank (primary keys)
		columnMeta.DefaultValue = strings.TrimSpace(columnMeta.DefaultValue)
		skipParseDefaultValue := strings.Contains(columnMeta.DefaultValue, "(") &&
			strings.Contains(columnMeta.DefaultValue, ")") ||
			strings.ToLower(columnMeta.DefaultValue) == "null" || columnMeta.DefaultValue == ""

		switch reflect.Indirect(fieldValue).Kind() {
		case reflect.Bool:
			columnMeta.DataType = Bool
			if columnMeta.HasDefaultValue && !skipParseDefaultValue {
				if columnMeta.DefaultValueInterface, err = strconv.ParseBool(columnMeta.DefaultValue); err != nil {
					err = fmt.Errorf("failed to parse %s as default value for bool, got error: %v", columnMeta.DefaultValue, err)
				}
			}
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			columnMeta.DataType = Int
			if columnMeta.HasDefaultValue && !skipParseDefaultValue {
				if columnMeta.DefaultValueInterface, err = strconv.ParseInt(columnMeta.DefaultValue, 0, 64); err != nil {
					err = fmt.Errorf("failed to parse %s as default value for int, got error: %v", columnMeta.DefaultValue, err)
				}
			}
		case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
			columnMeta.DataType = Uint
			if columnMeta.HasDefaultValue && !skipParseDefaultValue {
				if columnMeta.DefaultValueInterface, err = strconv.ParseUint(columnMeta.DefaultValue, 0, 64); err != nil {
					err = fmt.Errorf("failed to parse %s as default value for uint, got error: %v", columnMeta.DefaultValue, err)
				}
			}
		case reflect.Float32, reflect.Float64:
			columnMeta.DataType = Float
			if columnMeta.HasDefaultValue && !skipParseDefaultValue {
				if columnMeta.DefaultValueInterface, err = strconv.ParseFloat(columnMeta.DefaultValue, 64); err != nil {
					err = fmt.Errorf("failed to parse %s as default value for float, got error: %v", columnMeta.DefaultValue, err)
				}
			}
		case reflect.String:
			columnMeta.DataType = String
			if columnMeta.HasDefaultValue && !skipParseDefaultValue {
				columnMeta.DefaultValue = strings.Trim(columnMeta.DefaultValue, "'")
				columnMeta.DefaultValue = strings.Trim(columnMeta.DefaultValue, `"`)
				columnMeta.DefaultValueInterface = columnMeta.DefaultValue
			}
		case reflect.Struct:
			if _, ok := fieldValue.Interface().(*time.Time); ok {
				columnMeta.DataType = Time
			} else if fieldValue.Type().ConvertibleTo(TimeReflectType) {
				columnMeta.DataType = Time
			} else if fieldValue.Type().ConvertibleTo(TimePtrReflectType) {
				columnMeta.DataType = Time
			}
			if columnMeta.HasDefaultValue && !skipParseDefaultValue && columnMeta.DataType == Time {
				if t, err := now.Parse(columnMeta.DefaultValue); err == nil {
					columnMeta.DefaultValueInterface = t
				}
			}
		case reflect.Array, reflect.Slice:
			if reflect.Indirect(fieldValue).Type().Elem() == ByteReflectType && columnMeta.DataType == "" {
				columnMeta.DataType = Bytes
			}
		}

		if val, ok := columnMeta.TagMap["TYPE"]; ok {
			switch DataType(strings.ToLower(val)) {
			case Bool, Int, Uint, Float, String, Time, Bytes:
				columnMeta.DataType = DataType(strings.ToLower(val))
				columnMeta.FieldType = strings.ToUpper(val)
			default:
				columnMeta.FieldType = strings.ToUpper(val)
			}
		}

		if columnMeta.Size == 0 {
			switch reflect.Indirect(fieldValue).Kind() {
			case reflect.Int, reflect.Int64, reflect.Uint, reflect.Uint64, reflect.Float64:
				columnMeta.Size = 64
			case reflect.Int8, reflect.Uint8:
				columnMeta.Size = 8
			case reflect.Int16, reflect.Uint16:
				columnMeta.Size = 16
			case reflect.Int32, reflect.Uint32, reflect.Float32:
				columnMeta.Size = 332
			}
		}
    	// 为了给 builder 调用,所以单列解析的索引结果必须保存下来。
    	if columnMeta.TagMap["INDEX"] != "" || columnMeta.TagMap["UNIQUEINDEX"] != "" {
			columnIndexes, err := columnMeta.ParseIndexes()
			if err != nil {
				return err
			}
			columnMeta.ColumnIndexes = columnIndexes
		}

		columnMeta.Val = fieldValue
		*columnMetas = append(*columnMetas, columnMeta)
		fieldMap[columnMeta.FieldName] = columnMeta
	}
	return err
}

增加 FieldType 的目的是用于保存该字段再数据库中的实际类型,例如 “varchar”,在构建语句时根据DataType转换得来,默认DataType是根据结构体字段的类型,也支持使用者根据tag定义的类型。
当模型结构体中的tag出解析出类似与 “type:string”这部分描述时,DataType用来保存 string,之后的构建中将会转换成数据库中的实际类型,并保存在FieldType中。
定义一个 ColumnTypeInterface, 同时用来支持使用者直接自己定义数据库的字段的类型。

type ColumnTypeInterface interface {
	ColumnType() string
}

示例:

type MyStringType string

func (t MyCustomType) DataType() string {
    return "varchar(255)"
}

对于类型的转换,不同的数据库语法中的类型描述符是不一样的,所以需要改造方言模块,做成统一接口抽象,用于支持类型转换的1方法,也方便后续的方言相关功能扩展。

var (
	MySQL   Dialect = (*mysqlDialect)(nil)
	SQLite3 Dialect = (*sqlite3Dialect)(nil)
)

type Dialect interface {
	// quoter 返回一个引号,引用列名,表名的引号
	quoter() byte
	ColumnTypeOf(columnMeta *model.ColumnMeta) string
	ColumnDesc(columnMeta *model.ColumnMeta) string
}

type standardSQL struct{}

func (d *standardSQL) quoter() byte {
	// TODO implement me
	panic("implement me")
}

func (d *standardSQL) ColumnTypeOf(columnMeta *model.ColumnMeta) string {
	// TODO implement me
	panic("implement me")
}

func (d *standardSQL) ColumnDesc(columnMeta *model.ColumnMeta) string {
	// TODO implement me
	panic("implement me")
}

mysql 类型

const (
	AutoRandomTag = "auto_random()" // Treated as an auto_random field for tidb
)

type mysqlDialect struct {
	standardSQL
}

func (d *mysqlDialect) quoter() byte {
	return '`'
}

func (d *mysqlDialect) ColumnTypeOf(columnMeta *model.ColumnMeta) string {
	switch columnMeta.DataType {
	case model.Bool:
		return "BOOLEAN"
	case model.Int, model.Uint:
		return d.getIntAndUnitType(columnMeta)
	case model.Float:
		return d.getFloatType(columnMeta)
	case model.String:
		return d.getStringType(columnMeta)
	case model.Time:
		return d.getTimeType(columnMeta)
	case model.Bytes:
		return d.getBytesType(columnMeta)
	default:
		return d.getCustomType(columnMeta)
	}
}

func (d *mysqlDialect) getFloatType(columnMeta *model.ColumnMeta) string {
	if columnMeta.Precision > 0 {
		return fmt.Sprintf("DECIMAL(%d, %d)", columnMeta.Precision, columnMeta.Scale)
	}
	if columnMeta.Size <= 32 {
		return "FLOAT"
	}

	return "DOUBLE"
}

func (d *mysqlDialect) getStringType(columnMeta *model.ColumnMeta) string {
	size := columnMeta.Size
	if size == 0 {
		size = 255
	} else {
		hasIndex := columnMeta.TagMap["INDEX"] != "" || columnMeta.TagMap["UNIQUE"] != ""
		// TEXT, GEOMETRY or JSON column can't have a default value
		if columnMeta.IsPrimaryKey || columnMeta.HasDefaultValue || hasIndex {
			size = 191 // utf8mb4
		}
	}

	if size >= 65536 && size <= int(math.Pow(2, 24)) {
		return "MEDIUMTEXT"
	}

	if size > int(math.Pow(2, 24)) || size <= 0 {
		return "LONGTEXT"
	}

	return fmt.Sprintf("VARCHAR(%d)", size)
}

func (d *mysqlDialect) getTimeType(columnMeta *model.ColumnMeta) string {
	var precision string
	if columnMeta.Precision > 0 {
		precision = fmt.Sprintf("(%d)", columnMeta.Precision)
	}
	return "DATETIME" + precision
}

func (d *mysqlDialect) getBytesType(columnMeta *model.ColumnMeta) string {
	if columnMeta.Size > 0 && columnMeta.Size < 65536 {
		return fmt.Sprintf("VARBINARY(%d)", columnMeta.Size)
	}

	if columnMeta.Size >= 65536 && columnMeta.Size <= int(math.Pow(2, 24)) {
		return "MEDIUMBLOB"
	}

	return "LONGBLOB"
}

func autoRandomType(columnMeta *model.ColumnMeta) (bool, string) {
	if columnMeta.IsPrimaryKey && columnMeta.HasDefaultValue &&
		strings.ToLower(strings.TrimSpace(columnMeta.DefaultValue)) == AutoRandomTag {
		columnMeta.DefaultValue = ""

		sqlType := "BIGINT"
		if columnMeta.DataType == model.Uint {
			sqlType += " UNSIGNED"
		}
		sqlType += " AUTO_RANDOM"
		return true, sqlType
	}

	return false, ""
}

func (d *mysqlDialect) getIntAndUnitType(columnMeta *model.ColumnMeta) string {
	if autoRandom, typeString := autoRandomType(columnMeta); autoRandom {
		return typeString
	}

	constraint := func(sqlType string) string {
		if columnMeta.DataType == model.Uint {
			sqlType += " UNSIGNED"
		}
		if columnMeta.IsAutoIncrement {
			sqlType += " AUTO_INCREMENT"
		}
		return sqlType
	}

	switch {
	case columnMeta.Size <= 8:
		return constraint("TINYINT")
	case columnMeta.Size <= 16:
		return constraint("SMALLINT")
	case columnMeta.Size <= 24:
		return constraint("MEDIUMINT")
	case columnMeta.Size <= 32:
		return constraint("INT")
	default:
		return constraint("BIGINT")
	}
}

func (d *mysqlDialect) getCustomType(columnMeta *model.ColumnMeta) string {
	sqlType := string(columnMeta.DataType)

	if columnMeta.IsAutoIncrement && !strings.Contains(strings.ToLower(sqlType), " auto_increment") {
		sqlType += " AUTO_INCREMENT"
	}

	return sqlType
}

func (d *mysqlDialect) ColumnDesc(columnMeta *model.ColumnMeta) string {
	var sb strings.Builder
	if columnMeta.IsPrimaryKey {
		if columnMeta.IsAutoIncrement {
			sb.WriteString("AUTO_INCREMENT")
		}
		sb.WriteString(" PRIMARY KEY")
	} else {
		if columnMeta.IsNull {
			sb.WriteString("NULL")
		}
		if columnMeta.HasDefaultValue {
			sb.WriteString(" DEFAULT ")
			sb.WriteString(columnMeta.DefaultValue)
		}
		if columnMeta.IsAutoIncrement {
			sb.WriteString(" AUTO_INCREMENT")
		}
	}
	if columnMeta.Comment != "" {
		sb.WriteString(" COMMENT")
		sb.WriteString("'")
		sb.WriteString(columnMeta.Comment)
		sb.WriteString("'")
	}
	return sb.String()
}

sqlite 类型示例也是如此

type sqlite3Dialect struct {
	standardSQL
}

func (d *sqlite3Dialect) quoter() byte {
	return '`'
}

func (d *sqlite3Dialect) ColumnTypeOf(columnMeta *model.ColumnMeta) string {
	switch columnMeta.DataType {
	case model.Bool:
		return "BOOLEAN"
	case model.Int, model.Uint:
		return d.getIntAndUnitType(columnMeta)
	case model.Float:
		return d.getFloatType(columnMeta)
	case model.String:
		return d.getStringType(columnMeta)
	case model.Time:
		return d.getTimeType(columnMeta)
	case model.Bytes:
		return d.getBytesType(columnMeta)
	default:
		return d.getCustomType(columnMeta)
	}
}

func (d *sqlite3Dialect) ColumnDesc(columnMeta *model.ColumnMeta) string {
	// TODO implement me
	panic("implement me")
}

最上层的 builder 实现

type Creator[T any] struct {
	builder
	Session
	table TableReference
}

func NewCreator[T any](sess Session) *Creator[T] {
	return &Creator[T]{
		builder: builder{
			core:   sess.getCore(),
			buffer: bytebufferpool.Get(),
		},
		Session: sess,
	}
}

func (c *Creator[T]) Table(tbl TableReference) *Creator[T] {
	c.table = tbl
	return c
}

func (c *Creator[T]) tableOf() any {
	switch tb := c.table.(type) {
	case Table:
		return tb.entity
	default:
		// 不使用 new(T) 来规避内存分配
		return (*T)(nil)
	}
}

func (c *Creator[T]) Build() (Query, error) {
	defer bytebufferpool.Put(c.buffer)
	var err error
	c.meta, err = c.metaRegistry.Get(c.tableOf())
	if err != nil {
		return EmptyQuery, err
	}
	c.writeString("CREATE TABLE IF NOT EXISTS ")
	c.quote(c.meta.TableName)
	c.space()
	c.writeByte('(')
	err = c.buildColumns()
	if err != nil {
		return EmptyQuery, err
	}
	err = c.buildIndex()
	if err != nil {
		return EmptyQuery, err
	}
	c.writeByte(')')
	c.writeString(" ENGINE=")
	c.writeString(c.meta.Engine)
	c.writeString(" DEFAULT ")
	c.writeString("CHARSET=")
	c.writeString(c.meta.Charset)
	c.end()
	return Query{SQL: c.buffer.String(), Args: c.args}, nil
}

func (c *Creator[T]) buildColumns() error {
	for idx, colMeta := range c.meta.Columns {
		c.quote(colMeta.FieldName)
		c.space()
		fieldValue := colMeta.Val
		if valInterface, ok := fieldValue.Interface().(model.ColumnTypeInterface); ok {
			fieldType := valInterface.ColumnType()
			c.writeString(fieldType)

		} else {
			fieldType := c.dialect.ColumnTypeOf(colMeta)
			c.writeString(fieldType)
		}
		c.space()
		c.writeString(c.dialect.ColumnDesc(colMeta))
		if idx < len(c.meta.Columns) {
			c.writeByte(',')
		}
	}
	return nil
}

func (c *Creator[T]) buildIndex() error {
	for idx, colMeta := range c.meta.Columns {
		index := colMeta.LoadColumnIndex(colMeta.ColumnName)
		if index != nil {
			if index.IsUnique && colMeta.Unique {
				c.writeString("UNIQUE ")
			}
			c.writeString("INDEX")
			c.space()
			c.writeString(index.Name)
			c.space()
			c.writeByte('(')
			for pos, colIdx := range index.Columns {
				if pos > 0 {
					c.writeByte(',')
				}
				c.writeString(colIdx.ColumnName)
			}
			c.writeByte(')')
			if idx < len(c.meta.Columns) {
				c.writeByte(',')
			}
		}
	}
	return nil
}


func (c *Creator[T]) Get(ctx context.Context) (*T, error) {
	query, err := c.Build()
	if err != nil {
		return nil, err
	}
	return newQuerier[T](c.Session, query, c.meta, CREATE).Get(ctx)
}

Drop

MySQL

删除表语句

DROP [TEMPORARY] TABLE [IF EXISTS]
    tbl_name [, tbl_name] ...
    [RESTRICT | CASCADE]

DROP TABLE 删除一个或多个表。您必须具有 DROP 每个表的权限。
对于每个表,它将删除表定义和所有表数据。如果表已分区,则该语句将删除表定义、其所有分区、存储在这些分区中的所有数据以及与已删除的表关联的所有分区定义。删除表也会删除表的所有触发器。
DROP TABLE 导致隐式提交,除非与 TEMPORARY 关键字一起使用。
删除索引语句

DROP INDEX index_name ON tbl_name
    [algorithm_option | lock_option] ...

algorithm_option:
    ALGORITHM [=] {DEFAULT | INPLACE | COPY}

lock_option:
    LOCK [=] {DEFAULT | NONE | SHARED | EXCLUSIVE}

DROP INDEX 删除表中命名 index_name 的索引 tbl_name 。此语句映射到要删除索引的 ALTER TABLE 语句。
要删除主键,索引名始终 PRIMARY 为 ,必须将其指定为带引号的标识符,因为 PRIMARY 它是一个保留字:

DROP INDEX `PRIMARY` ON t;
SQLite

SQLite 的 DROP TABLE 语句用来删除表定义及其所有相关数据、索引、触发器、约束和该表的权限规范。
删除表
image.png

DROP TABLE IF EXISTS database_name.table_name;

删除索引
image.png

DROP INDEX index_name
具体代码
var _ QueryBuilder = &Drop[any]{}

type Drop[T any] struct {
	builder
	Session
	ts []any
}

func NewDrop[T any](sess Session) *Drop[T] {
	return &Drop[T]{
		builder: builder{
			core:   sess.getCore(),
			buffer: bytebufferpool.Get(),
		},
		Session: sess,
	}
}

func (d *Drop[T]) tables(ts []any) *Drop[T] {
	d.ts = ts 
	return d
}

func (d *Drop[T]) Build() (Query, error) {}

Alter

MySQL

更改表的结构。例如,可以添加或删除列、创建或销毁索引、更改现有列的类型或者重命名列或表本身。还可以更改特征,例如用于表或表注释的存储引擎。

ALTER TABLE tbl_name
    [alter_option [, alter_option] ...]
    [partition_options]

alter_option: {
    table_options
  | ADD [COLUMN] col_name column_definition
        [FIRST | AFTER col_name]
  | ADD [COLUMN] (col_name column_definition,...)
  | ADD {INDEX | KEY} [index_name]
        [index_type] (key_part,...) [index_option] ...
  | ADD {FULLTEXT | SPATIAL} [INDEX | KEY] [index_name]
        (key_part,...) [index_option] ...
  | ADD [CONSTRAINT [symbol]] PRIMARY KEY
        [index_type] (key_part,...)
        [index_option] ...
  | ADD [CONSTRAINT [symbol]] UNIQUE [INDEX | KEY]
        [index_name] [index_type] (key_part,...)
        [index_option] ...
  | ADD [CONSTRAINT [symbol]] FOREIGN KEY
        [index_name] (col_name,...)
        reference_definition
  | ADD [CONSTRAINT [symbol]] CHECK (expr) [[NOT] ENFORCED]
  | DROP {CHECK | CONSTRAINT} symbol
  | ALTER {CHECK | CONSTRAINT} symbol [NOT] ENFORCED
  | ALGORITHM [=] {DEFAULT | INSTANT | INPLACE | COPY}
  | ALTER [COLUMN] col_name {
        SET DEFAULT {literal | (expr)}
      | SET {VISIBLE | INVISIBLE}
      | DROP DEFAULT
    }
  | ALTER INDEX index_name {VISIBLE | INVISIBLE}
  | CHANGE [COLUMN] old_col_name new_col_name column_definition
        [FIRST | AFTER col_name]
  | [DEFAULT] CHARACTER SET [=] charset_name [COLLATE [=] collation_name]
  | CONVERT TO CHARACTER SET charset_name [COLLATE collation_name]
  | {DISABLE | ENABLE} KEYS
  | {DISCARD | IMPORT} TABLESPACE
  | DROP [COLUMN] col_name
  | DROP {INDEX | KEY} index_name
  | DROP PRIMARY KEY
  | DROP FOREIGN KEY fk_symbol
  | FORCE
  | LOCK [=] {DEFAULT | NONE | SHARED | EXCLUSIVE}
  | MODIFY [COLUMN] col_name column_definition
        [FIRST | AFTER col_name]
  | ORDER BY col_name [, col_name] ...
  | RENAME COLUMN old_col_name TO new_col_name
  | RENAME {INDEX | KEY} old_index_name TO new_index_name
  | RENAME [TO | AS] new_tbl_name
  | {WITHOUT | WITH} VALIDATION
}

partition_options:
    partition_option [partition_option] ...

partition_option: {
    ADD PARTITION (partition_definition)
  | DROP PARTITION partition_names
  | DISCARD PARTITION {partition_names | ALL} TABLESPACE
  | IMPORT PARTITION {partition_names | ALL} TABLESPACE
  | TRUNCATE PARTITION {partition_names | ALL}
  | COALESCE PARTITION number
  | REORGANIZE PARTITION partition_names INTO (partition_definitions)
  | EXCHANGE PARTITION partition_name WITH TABLE tbl_name [{WITH | WITHOUT} VALIDATION]
  | ANALYZE PARTITION {partition_names | ALL}
  | CHECK PARTITION {partition_names | ALL}
  | OPTIMIZE PARTITION {partition_names | ALL}
  | REBUILD PARTITION {partition_names | ALL}
  | REPAIR PARTITION {partition_names | ALL}
  | REMOVE PARTITIONING
}

key_part: {col_name [(length)] | (expr)} [ASC | DESC]

index_type:
    USING {BTREE | HASH}

index_option: {
    KEY_BLOCK_SIZE [=] value
  | index_type
  | WITH PARSER parser_name
  | COMMENT 'string'
  | {VISIBLE | INVISIBLE}
}

table_options:
    table_option [[,] table_option] ...

table_option: {
    AUTOEXTEND_SIZE [=] value
  | AUTO_INCREMENT [=] value
  | AVG_ROW_LENGTH [=] value
  | [DEFAULT] CHARACTER SET [=] charset_name
  | CHECKSUM [=] {0 | 1}
  | [DEFAULT] COLLATE [=] collation_name
  | COMMENT [=] 'string'
  | COMPRESSION [=] {'ZLIB' | 'LZ4' | 'NONE'}
  | CONNECTION [=] 'connect_string'
  | {DATA | INDEX} DIRECTORY [=] 'absolute path to directory'
  | DELAY_KEY_WRITE [=] {0 | 1}
  | ENCRYPTION [=] {'Y' | 'N'}
  | ENGINE [=] engine_name
  | ENGINE_ATTRIBUTE [=] 'string'
  | INSERT_METHOD [=] { NO | FIRST | LAST }
  | KEY_BLOCK_SIZE [=] value
  | MAX_ROWS [=] value
  | MIN_ROWS [=] value
  | PACK_KEYS [=] {0 | 1 | DEFAULT}
  | PASSWORD [=] 'string'
  | ROW_FORMAT [=] {DEFAULT | DYNAMIC | FIXED | COMPRESSED | REDUNDANT | COMPACT}
  | SECONDARY_ENGINE_ATTRIBUTE [=] 'string'
  | STATS_AUTO_RECALC [=] {DEFAULT | 0 | 1}
  | STATS_PERSISTENT [=] {DEFAULT | 0 | 1}
  | STATS_SAMPLE_PAGES [=] value
  | TABLESPACE tablespace_name [STORAGE {DISK | MEMORY}]
  | UNION [=] (tbl_name[,tbl_name]...)
}

partition_options:
    (see CREATE TABLE options)

添加和删除列
用于ADD向表中添加新列以及 DROP删除现有列。是标准 SQL 的 MySQL 扩展。 DROP col_name
要在表行中的特定位置添加列,请使用 FIRST或。默认情况下最后添加列。 AFTER col_name
如果表仅包含一列,则无法删除该列。
如果从表中删除列,则这些列也会从它们所属的任何索引中删除。如果构成索引的所有列都被删除,则索引也会被删除。如果使用 CHANGE或MODIFY来缩短列上存在索引的列,并且结果列长度小于索引长度,MySQL 会自动缩短索引。
对于ALTER TABLE ... ADD,如果列具有使用不确定性函数的表达式默认值,则该语句可能会产生警告或错误。

  • 单个语句中允许使用 多个ADD, ALTER, DROP, 和子句,以逗号分隔。这是 MySQL 对标准 SQL 的扩展,每个语句仅允许每个子句之一 。例如,要删除单个语句中的多个列,请执行以下操作: CHANGEALTER TABLEALTER TABLE
ALTER TABLE t2 DROP COLUMN c, DROP COLUMN d;

重命名、重新定义
要更改列以更改其名称和定义,请使用 CHANGE,指定旧名称、新名称以及新定义。例如,要将INT NOT NULL列从a重 命名为b并将其定义更改为使用 BIGINT数据类型,同时保留 NOT NULL属性,请执行以下操作:

ALTER TABLE t1 CHANGE a b BIGINT NOT NULL;

要更改列定义但不更改其名称,请使用 CHANGE或MODIFY。对于 CHANGE,语法需要两个列名称,因此您必须指定相同的名称两次才能保持名称不变。例如,要更改 column 的定义 b,请执行以下操作:

ALTER TABLE t1 CHANGE b b INT NOT NULL;

MODIFY更改定义而不更改名称更方便,因为它只需要一次列名:

ALTER TABLE t1 MODIFY b INT NOT NULL;

要更改列名称但不更改其定义,请使用 CHANGE或RENAME COLUMN。对于CHANGE,语法需要列定义,因此要保持定义不变,您必须重新指定列当前具有的定义。例如,要将INT NOT NULL列从 重 命名b为a,请执行以下操作:

ALTER TABLE t1 CHANGE b a INT NOT NULL;

RENAME COLUMN更改名称而不更改定义更方便,因为它只需要旧名称和新名称:

ALTER TABLE t1 RENAME COLUMN b TO a;

通常,您不能将列重命名为表中已存在的名称。然而,有时情况并非如此,例如当您交换名称或在循环中移动它们时。如果表具有名为a、b和 的 列c,则这些是有效的操作:

-- swap a and b
ALTER TABLE t1 RENAME COLUMN a TO b,
               RENAME COLUMN b TO a;
-- "rotate" a, b, c through a cycle
ALTER TABLE t1 RENAME COLUMN a TO b,
               RENAME COLUMN b TO c,
               RENAME COLUMN c TO a;

CHANGE对于使用或 进行的列定义更改MODIFY,定义必须包括数据类型以及应应用于新列的所有属性(索引属性(例如PRIMARY KEY或 ) 除外UNIQUE)。原始定义中存在但未为新定义指定的属性不会被保留。假设列col1定义为,INT UNSIGNED DEFAULT 1 COMMENT 'my column'并且您按如下方式修改该列,打算仅更改INT为 BIGINT:

ALTER TABLE t1 MODIFY col1 BIGINT;

该语句将数据类型从 更改为INT ,BIGINT但也删除了 UNSIGNED、DEFAULT和 COMMENT属性。要保留它们,语句必须明确包含它们:

ALTER TABLE t1 MODIFY col1 BIGINT UNSIGNED DEFAULT 1 COMMENT 'my column';

主键和索引
创建主键:

ALTER TABLE table_name ADD PRIMARY KEY (column_name);

创建普通索引:

ALTER TABLE table_name ADD INDEX index_name (column1 [ASC|DESC], column2 [ASC|DESC], ...);

创建唯一索引:

ALTER TABLE table_name ADD UNIQUE INDEX index_name (column1 [ASC|DESC], column2 [ASC|DESC], ...);

删除索引:

ALTER TABLE table_name DROP INDEX index_name;

外键和其他约束
FOREIGN KEY 子句由和存储引擎REFERENCES支持 ,“外键约束”。对于其他存储引擎,这些子句会被解析但被忽略。 InnoDB NDB ADD [CONSTRAINT [symbol]] FOREIGN KEY [index_name] (...) REFERENCES ... (...)
与 不同的是ALTER TABLE,对于 CREATE TABLE,如果给定则ADD FOREIGN KEY忽略_index_name_并使用自动生成的外键名称。作为解决方法,请包含CONSTRAINT用于指定外键名称的子句:

ADD CONSTRAINT name FOREIGN KEY (....) ...

添加外键:

ALTER TABLE table_name ADD FOREIGN KEY (column_name) REFERENCES referenced_table(ref_column_name);

添加唯一约束:

ALTER TABLE table_name ADD CONSTRAINT constraint_name UNIQUE (column_name);

MySQL 默默地忽略内联REFERENCES 规范,其中引用被定义为列规范的一部分。MySQL 只接受 REFERENCES作为单独FOREIGN KEY规范的一部分定义的子句。
分区InnoDB表不支持外键。此限制不适用于 NDB表,包括那些由[LINEAR] KEY. 有关更多信息,请参阅 第 24.6.2 节 “与存储引擎相关的分区限制”
MySQL Server 和 NDB Cluster 都支持使用 ALTER TABLE删除外键:

ALTER TABLE tbl_name DROP FOREIGN KEY fk_symbol;

ALTER TABLE支持 在同一语句中添加和删除外键, ALTER TABLE ... ALGORITHM=INPLACE但不支持 ALTER TABLE ... ALGORITHM=COPY。
服务器禁止对可能导致引用完整性丢失的外键列进行更改。ALTER TABLE ... DROP FOREIGN KEY解决方法是在更改列定义之前和之后使用ALTER TABLE ... ADD FOREIGN KEY。禁止更改的示例包括:

  • 对可能不安全的外键列的数据类型的更改。例如,允许更改 VARCHAR(20)VARCHAR(30),但更改为VARCHAR(1024)并不是因为这会改变存储各个值所需的长度字节数。
  • NULL禁止将列 更改NOT NULL为非严格模式,以防止将NULL值转换为默认非NULL值,因为在引用的表中没有对应的值。在严格模式下允许该操作,但如果需要任何此类转换,则会返回错误。
SQLlite

image.png
SQLite 支持 ALTER TABLE 的有限子集。SQLite 中的 ALTER TABLE 命令允许对现有表进行这些更改:它可以重命名;可以重命名列;可以向其中添加一列;或者可以从中删除一列。
在 SQLite 中,除了重命名表和在已有的表中添加列,ALTER TABLE 命令不支持其他操作。
重命名
用来重命名已有的表的 ALTER TABLE 的基本语法如下:

ALTER TABLE database_name.table_name RENAME TO new_table_name;

新增列
用来在已有的表中添加一个新的列的 ALTER TABLE 的基本语法如下:

ALTER TABLE database_name.table_name ADD COLUMN column_def...;

测试发现 sqlite3 并不支持直接添加带有 unique 约束的列:

sqlite> alter table company add department text unique; Error: Cannot add a UNIQUE column

可以先直接添加一列数据,然后再添加 unique 索引。其实在建表的时候如果列有 unique 约束,通过查询系统表 SQLITE_MASTER 可以看到,会自动创建相应的索引。
SQLITE_TEMP_MASTER 跟 SQLITE_MASTER 是 sqlite 的系统表,SQLITE_TEMP_MASTER 表存储临时表有关的所有内容。
删除列
DROP COLUMN 语法用于从表中删除现有列。DROP COLUMN 命令从表中删除命名列,并重写其内容以清除与该列关联的数据。DROP COLUMN 命令仅在该列未被模式的任何其他部分引用并且不是 PRIMARY KEY 且没有 UNIQUE 约束时才有效

ALTER TABLE table_name DROP COLUMN column_name;

重新定义、外键和其他约束
请注意,不是所有的ALTER操作都在SQLite中都是支持的,例如修改列的约束等操作。SQLite的ALTER语句的功能相对有限。如果需要更复杂的表结构修改,可能需要使用其他方法,例如创建新表并将数据迁移至新表。

具体代码
type AlterExpress interface {
    alterExpr()
}

type Constraint struct {
	name       string
	col        Column
	References any
}

func (c Column) ForeignKey(references any) Constraint {
	return Constraint{
		name: "FOREIGN KEY",
		col: c,
		References: references,
	}
}

func (_ Constraint) alterExpr() {}

type IndexExpress struct {
    name         string
    colNames     []string
    isPrimaryKey bool
    isUnique     bool
}

func (_ IndexExpress) alterExpr() {}

func Unique(name string, cols ...string) IndexExpress {
    return IndexExpress{
        isUnique: true,
        name:     name,
        colNames: cols,
    }
}

func Primary(col string) IndexExpress {
    return IndexExpress{
        isPrimaryKey: true,
        colNames:     []string{col},
    }
}

func Index(name string, cols ...string) IndexExpress {
    return IndexExpress{
        name:     name,
        colNames: cols,
    }
}

type ColumnDefinition struct {
    Size         int
    NotNull      bool
    Comment      string
    DefaultValue string
    ColType      string
}

func (_ ColumnDefinition) alterExpr() {}

func (c Column) Definition(val ColumnDefinition) Column {
    return Column{
        table:      c.table,
        definition: val,
        name:       c.name,
        alias:      c.alias,
    }
}

// Column represents column
// it could have alias
// in general, we use it in two ways
// 1. specify the column in query
// 2. it's the start point of building complex expression
type Column struct {
    table      TableReference
    definition ColumnDefinition
    oldName    string
    name       string
    alias      string
}

func (c Column) Rename(name string) Column {
	return Column{
		name:       name,
		oldName:    c.name,
		table:      c.table,
		alias:      c.alias,
		definition: c.definition,
	}
}
package eorm

type AlterOption struct { // RENAME,DROP,ADD,MODIFY,CHANGE
    Op string
    //col Column
    expr AlterExpress
}

func Add(expr AlterExpress) AlterOption {
    return AlterOption{
        Op:   "ADD",
        expr: expr,
    }
}

func Drop(expr AlterExpress) AlterOption {
    return AlterOption{
        Op:   "DROP",
        expr: expr,
    }
}

func Change(expr AlterExpress) AlterOption {
    return AlterOption{
        Op:   "CHANGE",
        expr: expr,
    }
}

func Modify(expr AlterExpress) AlterOption {
    return AlterOption{
        Op:   "MODIFY",
        expr: expr,
    }
}

var _ QueryBuilder = &Alter[any]{}

type Alter[T any] struct {
    builder
    Session
    table interface{}
    ops   []AlterOption
}

func NewAlter[T any](sess Session) *Alter[T] {
    return &Alter[T]{
        builder: builder{
            core:   sess.getCore(),
            buffer: bytebufferpool.Get(),
        },
        Session: sess,
    }
}

func (d *Alter[T]) Table(tbl TableReference) *Alter[T] {
	d.table = tbl
	return d
}

func (d *Alter[T]) AddColumn(name string) *Alter[T] {
	d.ops = append(d.ops, Add(Column{name: name}))
	return d
}

func (d *Alter[T]) Build() (Query, error) {
	return Query{}, nil
}

func (d *Alter[T]) Get(ctx context.Context) (*T, error) {
	query, err := d.Build()
	if err != nil {
		return nil, err
	}
	return newQuerier[T](d.Session, query, d.meta, ALTER).Get(ctx)
}

对 DDL 封装

提供一个 Migrator 的接口, 该接口组装了 Creater、Drop、Alter 等DDL接口,除此之外对于表迁移的接口还需要将上述接口的的颗粒度细分为:修改表名、得到表类型、 修改列类型、修改列名、修改索引等接口。
为了满足ORM迁移工具类的需求,一个Migrator接口通常需要具备以下方法:

  1. CreateTable(table interface{}) error:创建新表的方法,传入一个代表表结构的对象,并在数据库中创建对应的表。
  2. DropTable(table interface{}) error:删除表的方法,传入一个代表表结构的对象,并在数据库中删除对应的表。
  3. AlterTable(table interface{}) error:修改表结构的方法,传入一个代表表结构的对象,并根据对象的定义,在数据库中修改表结构。
  4. AddColumn(table interface{}, columnName string, columnType string) error:添加列的方法,传入一个代表表结构的对象、列名称和列类型,并在数据库中的表中添加对应的列。
  5. DropColumn(table interface{}, columnName string) error:删除列的方法,传入一个代表表结构的对象和要删除的列名称,并从数据库的表中删除对应的列。
  6. ModifyColumn(table interface{}, columnName string, newColumnType string) error:修改列类型的方法,传入一个代表表结构的对象、要修改的列名称和新的列类型,并在数据库中修改对应的列类型。
  7. RenameTable(oldTableName string, newTableName string) error:重命名表的方法,传入旧的表名和新的表名,并在数据库中修改对应的表名。
  8. AddIndex(table interface{}, indexName string, columnNames ...string) error:添加索引的方法,传入一个代表表结构的对象、索引名称和要添加索引的列名称,为数据库中的表添加对应的索引。
  9. DropIndex(table interface{}, indexName string) error:删除索引的方法,传入一个代表表结构的对象和要删除的索引名称,并从数据库的表中删除对应的索引。
  10. HasTable(tableName string) bool:检查表是否存在的方法,传入表名,判断数据库中是否存在对应的表。
  11. HasColumn(tableName string, columnName string) bool:检查列是否存在的方法,传入表名和列名,判断数据库的表中是否存在对应的列。

一般情况参数table应该是一个结构体类型,而不是一个接口。这是因为我们在创建表时需要明确指定表的结构和字段信息,而结构体能够提供这些信息的定义和描述。
通过传递一个代表表结构的结构体作为参数,Migrator接口可以根据结构体的定义,在数据库中创建相应的表。结构体可以灵活地定义表的字段、类型、约束和其他元数据信息,这样Migrator实现可以更准确地生成相应的SQL语句来创建表。
而接口通常用于定义一组方法,而不会提供具体的结构信息。因此,在创建表时,使用结构体作为参数更能满足需求。例如Migrator接口的CreateTable方法,应该将一个代表表结构的结构体作为参数传递进去。对于具体的ORM库和数据库类型。
通过使用接口,可以实现更高层次的抽象和灵活性。可以定义一个通用的表结构接口,然后针对不同的数据库和ORM库,实现具体的表结构结构体。
综上所述,使用接口来定义表结构是一种常见且有效的做法,可以提供更大的灵活性和可扩展性。
这些方法可以提供基本的表和列操作,从而满足 ORM 迁移工具类的需求。具体的实现方式可能因具体的ORM 库和数据库类型而有所差异。
在实现 Migrator** **接口时,需要根据具体的需求和使用的 ORM 库进行相应的调整和扩展。
Migrator 可用来为 ORM Schemas 实现自定义的迁移逻辑。Migrator 还为不同类型的数据库提供了统一的 API 抽象,例如:SQLite 不支持 ALTER COLUMN、DROP COLUMN 等 SQL 子句,所以当我们调用 Migrator API 试图修改表结构时,会自定为在 SQLite 创建一张新表、并复制所有数据,然后删除旧表、重命名新表。

type Migrator interface {
	// Run
	Run(dst ...interface{}) error

	// Database
	CurrentDatabase() (name string, err error)
	FullColumnTypeOf(meta *model.ColumnMeta) string
	GetTypeAliases(databaseTypeName string) []string

	// Tables
	CreateTable(dst ...interface{}) error
	DropTable(dst ...interface{}) error
	HasTable(model *model.TableMeta) (bool, error)
	RenameTable(oldName, newName interface{}) error
	GetTables() (tableList []string, err error)
	TableType(dst interface{}) (TableType, error)

	// Columns
	AddColumn(dst interface{}, field string) error
	DropColumn(dst interface{}, field string) error
	AlterColumn(dst interface{}, field string) error
	MigrateColumn(dst interface{}, meta *model.ColumnMeta, columnType ColumnType) error
	HasColumn(dst interface{}, field string) bool
	RenameColumn(dst interface{}, oldName, field string) error
	ColumnTypes(dst interface{}) ([]ColumnType, error)

	// Indexes
	CreateIndex(dst interface{}, name string) error
	DropIndex(dst interface{}, name string) error
	HasIndex(dst interface{}, name string) (bool, error)
	RenameIndex(dst interface{}, newName string) error
	GetIndexes(dst interface{}) ([]ColumnIndex, error)
}
type ColumnIndex interface {
	Table() string
	Name() string
	Columns() []string
	PrimaryKey() (isPrimaryKey bool, ok bool)
	Unique() (unique bool, ok bool)
	Option() string
}

type TableType interface {
	Schema() string
	Name() string
	Type() string
	Comment() (comment string, ok bool)
}

那么 Migrate 的实现这里就主要参考gorm的实现。

type AutoMigrator struct {
	core
	db *DB
}

func NewAutoMigrator(db *DB) migrator.Migrator {
	return &AutoMigrator{
		db:   db,
		core: db.getCore(),
	}
}

创建一个 AutoMigrate 结构体,该结构体实现Migrator接口,接收一个数据库连接和要迁移的模型对象作为参数。

Run 方法

Run 方法为 Migrator 最核心的方法,也就是负责模型类迁移启动的方法

// Run auto migrate values
func (m *AutoMigrator) Run(values ...any) error {
	metaRegistry := m.core.metaRegistry
	for _, value := range values {
		tableMeta, err := metaRegistry.ParseTableMeta(value)
		if err != nil {
			return err
		}
		hasTable, err := m.HasTable(tableMeta)
		if err != nil {
			return err
		}
		if !hasTable {
			if err = m.CreateTable(value); err != nil {
				return err
			}
		} else {
			var columnTypes []ColumnType
			columnTypes, err = m.ColumnTypes(value)
			if err != nil {
				return err
			}
			for columnName, columnMeta := range tableMeta.ColumnMap {
				var foundColumn ColumnType
				for _, columnType := range columnTypes {
					if columnType.Name() == columnName {
						foundColumn = columnType
						break
					}
					if foundColumn == nil {
						if err = m.AddColumn(value, columnName); err != nil {
							return err
						}
					} else {
						if err = m.MigrateColumn(value, columnMeta, foundColumn); err != nil {
							return err
						}
					}
				}
				for _, idx := range columnMeta.ColumnIndexes {
					hasIndex, err := m.HasIndex(value, idx.Name)
					if err != nil {
						return err
					}
					if !hasIndex {
						if err = m.CreateIndex(value, idx.Name); err != nil {
							return err
						}
					} else {
						if err = m.MigrateIndex(value, columnMeta, idx.Name); err != nil {
							return err
						}
					}
				}
			}
		}
	}
	return nil
}

func (m *AutoMigrator) HasTable(model *model.TableMeta) bool {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) MigrateColumn(dst interface{}, columnMeta *model.ColumnMeta, columnType ColumnType) error {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) AddColumn(dst any, field string) error {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) DropColumn(dst any, field string) error {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) AlterColumn(dst any, field string) error {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) CreateIndex(dst any, name string) error {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) DropIndex(dst interface{}, name string) error {
	// TODO implement me
	panic("implement me")
}


func (m *AutoMigrator) CurrentDatabase() (name string, err error) {
	// TODO implement me
	panic("implement me")
}


func (m *AutoMigrator) CreateTable(dst ...interface{}) error {
	// TODO implement me
	panic("implement me")
}


func (m *AutoMigrator) ColumnTypes(dst interface{}) ([]ColumnType, error) {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) FullDataTypeOf(field *model.ColumnMeta) string {
	// TODO implement me
	panic("implement me")
}


func (m *AutoMigrator) DataTypeOf(field *model.ColumnMeta) string {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) GetTypeAliases(databaseTypeName string) []string {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) DropTable(dst ...interface{}) error {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) RenameTable(oldName, newName interface{}) error {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) GetTables() (tableList []string, err error) {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) TableType(dst interface{}) (TableType, error) {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) HasColumn(dst interface{}, field string) bool {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) RenameColumn(dst interface{}, oldName, field string) error {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) HasIndex(dst interface{}, name string) bool {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) RenameIndex(dst interface{}, oldName, newName string) error {
	// TODO implement me
	panic("implement me")
}

func (m *AutoMigrator) GetIndexes(dst interface{}) ([]ColumnIndex, error) {
	// TODO implement me
	panic("implement me")
}

HasTable 方法

HasTable 判断表是否存在,返回布尔值。实现的核心是通过sql去查询目标表是否存在于当前连接的数据库中。
获得当前连接的数据库名称;

func (m *AutoMigrator) CurrentDatabase() (name string, err error) {
	res, err := RawQuery[string](
		m.db, "SELECT DATABASE()").Get(context.Background())
	return *res, err
}

进行raw sql 拼接,查询目标表是否存在;

// HasTable returns table exists or not for value, value could be a struct or string
func (m *AutoMigrator) HasTable(model *model.TableMeta) bool {
	query := "SELECT count(*) FROM information_schema.tables WHERE table_schema = ? AND table_name = ? AND table_type = ?"
	currentDatabase, err := m.CurrentDatabase()
	if err != nil {
		panic(err)
	}
	res, err := RawQuery[int64](
		m.db, query, currentDatabase, model.TableName, "BASE TABLE").Get(context.Background())
	if err != nil {
		panic(err)
	}
	return *res > 0
}

CreateTable 方法

基于 builder 创建数据库表;

func (m *AutoMigrator) CreateTable(dst ...interface{}) error {
	_, err := NewCreator[int](m.db).Table(TableOf(dst, "")).Get(context.Background())
	return err
}

ColumnTypes 方法

获取表结构的字段信息;
在目标表原先就已经建立的情况下,查找目前元数据模型的列, 将迁移动作分为了两个分支, :

  1. 发现新字段,添加新字段;
  2. 未发现新字段,检查旧字段中是否有被修改的属性,如果有就对表里该字段属性进行修改;

以上两个分支需要当前表的表结构字段信息,为了和最新解析的元数据模型的信息做对比。
新增 ColumnType ,为字段信息的抽象;

type ColumnType interface {
	Name() string
	DatabaseTypeName() string                 // varchar
	ColumnType() (columnType string, ok bool) // varchar(64)
	PrimaryKey() (isPrimaryKey bool, ok bool)
	AutoIncrement() (isAutoIncrement bool, ok bool)
	Length() (length int64, ok bool)
	DecimalSize() (precision int64, scale int64, ok bool)
	Nullable() (nullable bool, ok bool)
	Unique() (unique bool, ok bool)
	ScanType() reflect.Type
	Comment() (value string, ok bool)
	DefaultValue() (value string, ok bool)
} 

实现 ColumnType,用于接收, sql/database 的表结构字段的信息。

type ColumnType struct {
	SQLColumnType      *sql.ColumnType
	NameValue          sql.NullString
	DataTypeValue      sql.NullString
	ColumnTypeValue    sql.NullString
	PrimaryKeyValue    sql.NullBool
	UniqueValue        sql.NullBool
	AutoIncrementValue sql.NullBool
	LengthValue        sql.NullInt64
	DecimalSizeValue   sql.NullInt64
	ScaleValue         sql.NullInt64
	NullableValue      sql.NullBool
	ScanTypeValue      reflect.Type
	CommentValue       sql.NullString
	DefaultValueValue  sql.NullString
}

func (ct ColumnType) Name() string {
	if ct.NameValue.Valid {
		return ct.NameValue.String
	}
	return ct.SQLColumnType.Name()
}

func (ct ColumnType) DatabaseTypeName() string {
	if ct.DataTypeValue.Valid {
		return ct.DataTypeValue.String
	}
	return ct.SQLColumnType.DatabaseTypeName()
}

func (ct ColumnType) ColumnType() (columnType string, ok bool) {
	return ct.ColumnTypeValue.String, ct.ColumnTypeValue.Valid
}

func (ct ColumnType) PrimaryKey() (isPrimaryKey bool, ok bool) {
	return ct.PrimaryKeyValue.Bool, ct.PrimaryKeyValue.Valid
}

func (ct ColumnType) AutoIncrement() (isAutoIncrement bool, ok bool) {
	return ct.AutoIncrementValue.Bool, ct.AutoIncrementValue.Valid
}

func (ct ColumnType) Length() (length int64, ok bool) {
	if ct.LengthValue.Valid {
		return ct.LengthValue.Int64, true
	}
	return ct.SQLColumnType.Length()
}

func (ct ColumnType) DecimalSize() (precision int64, scale int64, ok bool) {
	if ct.DecimalSizeValue.Valid {
		return ct.DecimalSizeValue.Int64, ct.ScaleValue.Int64, true
	}
	return ct.SQLColumnType.DecimalSize()
}

func (ct ColumnType) Nullable() (nullable bool, ok bool) {
	if ct.NullableValue.Valid {
		return ct.NullableValue.Bool, true
	}
	return ct.SQLColumnType.Nullable()
}

func (ct ColumnType) Unique() (unique bool, ok bool) {
	return ct.UniqueValue.Bool, ct.UniqueValue.Valid
}

func (ct ColumnType) ScanType() reflect.Type {
	if ct.ScanTypeValue != nil {
		return ct.ScanTypeValue
	}
	return ct.SQLColumnType.ScanType()
}

func (ct ColumnType) Comment() (value string, ok bool) {
	return ct.CommentValue.String, ct.CommentValue.Valid
}

func (ct ColumnType) DefaultValue() (value string, ok bool) {
	return ct.DefaultValueValue.String, ct.DefaultValueValue.Valid
}

实现 ColumnTypes 方法:

func (m *AutoMigrator) ColumnTypes(dst interface{}) ([]ColumnType, error) {
	columnTypes := make([]ColumnType, 0)
	query, err := NewSelector[dst](m.db).Build()
	if err != nil {
		return nil, err
	}
	rows, err := m.db.queryContext(context.Background(), query)
	if err != nil {
		return nil, err
	}
	defer func() {
		err = rows.Close()
	}()
	var rawColumnTypes []*sql.ColumnType
	rawColumnTypes, err = rows.ColumnTypes()
	if err != nil {
		return nil, err
	}

	for _, c := range rawColumnTypes {
		columnTypes = append(columnTypes, migrator.ColumnType{SQLColumnType: c})
	}
	return columnTypes, nil
}

AddColumn 方法

为当前表添加列

func (m *AutoMigrator) AddColumn(dst any, colName string) error {
	_, err := NewAlter[int](m.db).Table(TableOf(dst, "")).AddColumn(colName).Get(context.Background())
	return err
}

MigrateColumn 方法

将目标列迁移到数据库中;
FullColumnTypeOf :获取模型元数据中的全部字段类型;
DatabaseTypeName :获取当前表该字段在数据库中的类型;
然后经过逐一对比,字段的属性,来判断字段是否需要被修改。

func (m *AutoMigrator) MigrateColumn(dst interface{}, columnMeta *model.ColumnMeta, columnType ColumnType) error {
    fullDataType := strings.TrimSpace(strings.ToLower(m.FullColumnTypeOf(columnMeta)))
    realDataType := strings.ToLower(columnType.DatabaseTypeName())
    var (
        alterColumn bool
        isSameType  = fullDataType == realDataType
    )
    if !columnMeta.IsPrimaryKey {
        // check type
        if !strings.HasPrefix(fullDataType, realDataType) {
            // check type aliases
            aliases := m.dialect.GetTypeAliases(realDataType)
            for _, alias := range aliases {
                if strings.HasPrefix(fullDataType, alias) {
                    isSameType = true
                    break
                }
            }

            if !isSameType {
                alterColumn = true
            }
        }
    }

    if !isSameType {
        // check size
        if length, ok := columnType.Length(); length != int64(columnMeta.Size) {
            if length > 0 && columnMeta.Size > 0 {
                alterColumn = true
            } else {
                matches2 := regFullDataType.FindAllStringSubmatch(fullDataType, -1)
                if !columnMeta.IsPrimaryKey && (len(matches2) == 1 && matches2[0][1] != fmt.Sprint(length) && ok) {
                    alterColumn = true
                }
            }
        }

        if precision, _, ok := columnType.DecimalSize(); ok && int64(columnMeta.Precision) != precision {
            if regexp.MustCompile(fmt.Sprintf("[^0-9]%d[^0-9]", columnMeta.Precision)).MatchString(
                m.ColumnTypeOf(columnMeta)) {
                alterColumn = true
            }
        }

        if nullable, ok := columnType.Nullable(); ok && nullable != columnMeta.IsNull {
            if !columnMeta.IsPrimaryKey && nullable {
                alterColumn = true
            }
        }

        // check unique
        if unique, ok := columnType.Unique(); ok && unique != columnMeta.Unique {
            // not primary key
            if !columnMeta.IsPrimaryKey {
                alterColumn = true
            }
        }

        if !columnMeta.IsPrimaryKey {
            currentDefaultNotNull := columnMeta.HasDefaultValue && (columnMeta.DefaultValueInterface != nil || !strings.EqualFold(columnMeta.DefaultValue, "NULL"))
            dv, dvNotNull := columnType.DefaultValue()
            if dvNotNull && !currentDefaultNotNull {
                // default value -> null
                alterColumn = true
            } else if !dvNotNull && currentDefaultNotNull {
                // null -> default value
                alterColumn = true
            } else if (columnMeta.DataType != model.Time && dv != columnMeta.DefaultValue) ||
            (columnMeta.DataType != model.Time && !strings.EqualFold(strings.TrimSuffix(dv, "()"), strings.TrimSuffix(columnMeta.DefaultValue, "()"))) {
                if currentDefaultNotNull || dvNotNull {
                    alterColumn = true
                }
            }
        }

        if comment, ok := columnType.Comment(); ok && comment != columnMeta.Comment {
            if !columnMeta.IsPrimaryKey {
                alterColumn = true
            }
        }

        if alterColumn && !columnMeta.IgnoreMigration {
            return m.AlterColumn(dst, columnMeta.ColumnName)
        }
    }

    return nil
}

HasIndex 方法

判断在索引是否存在。

func (m *AutoMigrator) HasIndex(dst interface{}, name string) (bool, error) {
	res := new(int64)
	err := m.RunWithValue(dst, func(meta *model.TableMeta) error {
		currentDatabase, err := m.CurrentDatabase()
		if err != nil {
			return err
		}
		if idx := meta.LoadIndex(name); idx != nil {
			name = idx.Name
		}
		res, err = RawQuery[int64](
			m.db, "SELECT count(*) FROM information_schema.statistics WHERE table_schema = ? AND table_name = ? AND index_name = ?", currentDatabase, meta.TableName, name,
		).Get(context.Background())
		return err
	})
	return *res > 0, err
}

CreateIndex 方法

添加索引。

func (m *AutoMigrator) CreateIndex(dst any, name string) error {
	var (
		res = new(int64)
		err error
	)
	err = m.RunWithValue(dst, func(meta *model.TableMeta) error {
		if idx := meta.LoadIndex(name); idx != nil {
			res, err = NewAlter[int64](m.db).Table(TableOf(dst, "")).AddIndex(IndexExpress{
				name:       idx.Name,
				colNames:   idx.ColumnList,
				IndexClass: idx.IndexClass,
				Comment:    idx.Comment,
				Length:     idx.Length,
				IndexType:  idx.IndexType,
			}).Get(context.Background())
			return err
		}
		return fmt.Errorf("failed to create index with name %s", name)
	})
	if err != nil {
		return err
	}
	if *res <= 0 {
		return errors.New("变更失败,变更行数为 0")
	}
	return nil
}

版本控制

为了先实现简单的DDL操作,和迁移封装,暂不支持版本控制。

单元测试

  • 创建模型映射表
  • 删除模型映射表
  • 修改字段
  • 新增字段
  • 删除字段

集成测试

  • 创建模型映射表
  • 删除模型映射表
  • 修改字段
  • 新增字段
  • 删除字段
package migrator

import (
    "testing"
)


func TestMigrator_IntegrationTest_Run(t *testing.T) {
	db, err := single.OpenDB(s.driver, s.dsn)
	if err != nil {
		t.Fatal(err)
	}
	if err = db.Wait(); err != nil {
		t.Fatal(err)
	}
	orm, err := eorm.Open(s.driver, db)
	if err != nil {
		t.Fatal(err)
	}
    migrator := NewMigrator(orm)

    // Define the table schema
    tableSchema := []Column{
        {Name: "id", Type: "int", Constraints: "PRIMARY KEY"},
        {Name: "name", Type: "varchar(255)", Constraints: "NOT NULL"},
    }

    err := migrator.Run(&User{}, &Profile{}, &Post{}, &Role{})
    if err != nil {
        t.Errorf("Error creating table: %v", err)
    }

    // Optionally, perform assertions to verify the table creation
    // ...
}
@Stone-afk Stone-afk changed the title (WIP) erom: Migrate 功能的支持 (WIP) eorm: Migrate 功能的支持 Jul 17, 2023
@flycash
Copy link
Contributor

flycash commented Jul 18, 2023

这个功能的实现要分成两个部分:

  • 一个是纯粹的 DDL 语句生成,也就是对应到我们的各种 sql Builder。
  • 第二个是利用第一个部分,来生成对应的 DDL。

在这两个的基础上,再考虑执行对应的 DDL。

举个例子来说:

  • 用户可以完全手动的构造 DDL,然后他自己去调度执行。
  • 你可以根据模型来直接生成 creat table 的语句
  • 你也可以根据模型和数据库中对应的表结构来生成 DDL 语句

而执行 DDL 本身如果要做成命令行工具,我的建议是拆出去作为一个单独仓库。

此外,还有一个很关键的问题,就是用户怎么指定我 GO 某个类型对应到什么的数据库类型?

再进一步考虑,用户能不能在测试环境的时候使用 sqlite,生成一种 DDL?在生产环境是 mysql,又是另外一种 DDL?

@Stone-afk
Copy link
Collaborator Author

方案选型按gorm 还是 goalng-migrate 呢?如果要做成命令行要单独拆出去或者考虑封装 goalng-migrate ??如果按照 gorm,那就设计一个 migrate 的接口,去封装 DDL 的生成就可以了!!

@flycash
Copy link
Contributor

flycash commented Jul 20, 2023

成年人做啥选择,我都要!意思就是,用户可以自己在代码里面调用一下,也可以用命令行执行一下。暂时都放在 eorm 吧

@Stone-afk Stone-afk changed the title (WIP) eorm: Migrate 功能的支持 eorm: Migrate 功能的支持 Nov 13, 2023
@Stone-afk Stone-afk assigned Stone-afk and unassigned Stone-afk Nov 13, 2023
@flycash
Copy link
Contributor

flycash commented Dec 6, 2023

可以的,非常赞!可以搞起来!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants