feat(database): 添加ClickHouse数据库驱动支持

- 实现了ClickHouse数据库驱动程序,支持基本的数据库操作
- 添加了ClickHouse特定的迁移功能,包括表、列、索引的创建和管理
- 集成了ClickHouse的语法特性,如MergeTree引擎和Nullable类型
- 实现了数据库连接池管理和SQL执行接口
- 添加了对系统表查询的支持,用于检查表和列的存在性
This commit is contained in:
2026-04-13 16:07:54 +08:00
parent a083b74f9b
commit 284f9380ed
23 changed files with 3599 additions and 8 deletions
+10
View File
@@ -61,3 +61,13 @@ func (d *Driver) injectNeedParsedSql(ctx context.Context) context.Context {
}
return context.WithValue(ctx, needParsedSqlInCtx, true)
}
// Migration returns a Migration instance for ClickHouse database operations.
func (d *Driver) Migration() *Migration {
return NewMigration(d)
}
// GetMigration returns a Migration instance implementing the Migration interface.
func (d *Driver) GetMigration() database.Migration {
return d.Migration()
}
@@ -0,0 +1,321 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package clickhouse
import (
"context"
"fmt"
"strings"
"git.magicany.cc/black1552/gin-base/database"
)
// Migration implements database migration operations for ClickHouse.
type Migration struct {
*database.MigrationCore
*database.AutoMigrateCore
}
// NewMigration creates a new ClickHouse Migration instance.
func NewMigration(db database.DB) *Migration {
return &Migration{
MigrationCore: database.NewMigrationCore(db),
AutoMigrateCore: database.NewAutoMigrateCore(db),
}
}
// CreateTable creates a new table with the given name and column definitions.
func (m *Migration) CreateTable(ctx context.Context, table string, columns map[string]*database.ColumnDefinition, options ...database.TableOption) error {
if len(columns) == 0 {
return fmt.Errorf("cannot create table without columns")
}
var opts database.TableOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE TABLE ")
if opts.IfNotExists {
sql.WriteString("IF NOT EXISTS ")
}
sql.WriteString(database.QuoteIdentifier(table))
sql.WriteString(" (\n")
// Add columns
var colDefs []string
for name, def := range columns {
colDef := m.buildColumnDefinition(name, def)
colDefs = append(colDefs, " "+colDef)
}
sql.WriteString(strings.Join(colDefs, ",\n"))
sql.WriteString("\n)")
// Add engine specification (required for ClickHouse)
sql.WriteString(" ENGINE = MergeTree()")
if opts.Comment != "" {
sql.WriteString(fmt.Sprintf(" COMMENT '%s'", escapeString(opts.Comment)))
}
return m.ExecuteSQL(ctx, sql.String())
}
// buildColumnDefinition builds column definition for ClickHouse.
func (m *Migration) buildColumnDefinition(name string, def *database.ColumnDefinition) string {
var parts []string
parts = append(parts, database.QuoteIdentifier(name))
// Handle ClickHouse-specific types
dbType := def.Type
parts = append(parts, dbType)
if !def.Null {
// ClickHouse uses Nullable type wrapper
if !strings.HasPrefix(dbType, "Nullable(") {
// Type is already non-nullable by default in ClickHouse
}
} else {
// Make type nullable if needed
if !strings.HasPrefix(dbType, "Nullable(") {
parts[len(parts)-1] = fmt.Sprintf("Nullable(%s)", dbType)
}
}
if def.Default != nil {
defaultValue := formatDefaultValue(def.Default)
parts = append(parts, fmt.Sprintf("DEFAULT %s", defaultValue))
}
if def.Comment != "" {
parts = append(parts, fmt.Sprintf("COMMENT '%s'", escapeString(def.Comment)))
}
return strings.Join(parts, " ")
}
// DropTable drops an existing table from the database.
func (m *Migration) DropTable(ctx context.Context, table string, ifExists ...bool) error {
sql := "DROP TABLE "
if len(ifExists) > 0 && ifExists[0] {
sql += "IF EXISTS "
}
sql += database.QuoteIdentifier(table)
return m.ExecuteSQL(ctx, sql)
}
// HasTable checks if a table exists in the database.
func (m *Migration) HasTable(ctx context.Context, table string) (bool, error) {
schema := m.GetDB().GetSchema()
if schema == "" {
schema = "currentDatabase()"
} else {
schema = fmt.Sprintf("'%s'", schema)
}
query := fmt.Sprintf(
"SELECT COUNT(*) FROM system.tables WHERE database = %s AND name = '%s'",
schema,
table,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// RenameTable renames an existing table from oldName to newName.
func (m *Migration) RenameTable(ctx context.Context, oldName, newName string) error {
sql := fmt.Sprintf(
"RENAME TABLE %s TO %s",
database.QuoteIdentifier(oldName),
database.QuoteIdentifier(newName),
)
return m.ExecuteSQL(ctx, sql)
}
// TruncateTable removes all records from a table but keeps the table structure.
func (m *Migration) TruncateTable(ctx context.Context, table string) error {
sql := fmt.Sprintf("TRUNCATE TABLE %s", database.QuoteIdentifier(table))
return m.ExecuteSQL(ctx, sql)
}
// AddColumn adds a new column to an existing table.
func (m *Migration) AddColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.buildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s", database.QuoteIdentifier(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// DropColumn removes a column from an existing table.
func (m *Migration) DropColumn(ctx context.Context, table, column string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP COLUMN %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(column),
)
return m.ExecuteSQL(ctx, sql)
}
// RenameColumn renames a column in an existing table.
func (m *Migration) RenameColumn(ctx context.Context, table, oldName, newName string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s RENAME COLUMN %s TO %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(oldName),
database.QuoteIdentifier(newName),
)
return m.ExecuteSQL(ctx, sql)
}
// ModifyColumn modifies an existing column's definition.
func (m *Migration) ModifyColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.buildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s MODIFY COLUMN %s", database.QuoteIdentifier(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// HasColumn checks if a column exists in a table.
func (m *Migration) HasColumn(ctx context.Context, table, column string) (bool, error) {
fields, err := m.GetDB().TableFields(ctx, table)
if err != nil {
return false, err
}
_, exists := fields[column]
return exists, nil
}
// CreateIndex creates a new index on the specified table and columns.
// Note: ClickHouse has limited index support compared to traditional databases.
func (m *Migration) CreateIndex(ctx context.Context, table, index string, columns []string, options ...database.IndexOption) error {
var opts database.IndexOptions
for _, opt := range options {
opt(&opts)
}
// ClickHouse uses different indexing mechanisms
// For now, we'll use ALTER TABLE ADD INDEX syntax
colList := m.BuildIndexColumns(columns)
sql := fmt.Sprintf(
"ALTER TABLE %s ADD INDEX %s (%s) TYPE minmax GRANULARITY 1",
database.QuoteIdentifier(table),
database.QuoteIdentifier(index),
colList,
)
return m.ExecuteSQL(ctx, sql)
}
// DropIndex drops an existing index from a table.
func (m *Migration) DropIndex(ctx context.Context, table, index string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP INDEX %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(index),
)
return m.ExecuteSQL(ctx, sql)
}
// HasIndex checks if an index exists on a table.
func (m *Migration) HasIndex(ctx context.Context, table, index string) (bool, error) {
schema := m.GetDB().GetSchema()
if schema == "" {
schema = "currentDatabase()"
} else {
schema = fmt.Sprintf("'%s'", schema)
}
query := fmt.Sprintf(
"SELECT COUNT(*) FROM system.data_skipping_indices WHERE database = %s AND table = '%s' AND name = '%s'",
schema,
table,
index,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateForeignKey is not supported in ClickHouse.
func (m *Migration) CreateForeignKey(ctx context.Context, table, constraint string, columns []string, refTable string, refColumns []string, options ...database.ForeignKeyOption) error {
// ClickHouse does not support foreign keys
return fmt.Errorf("ClickHouse does not support foreign key constraints")
}
// DropForeignKey is not supported in ClickHouse.
func (m *Migration) DropForeignKey(ctx context.Context, table, constraint string) error {
// ClickHouse does not support foreign keys
return fmt.Errorf("ClickHouse does not support foreign key constraints")
}
// HasForeignKey always returns false as ClickHouse doesn't support foreign keys.
func (m *Migration) HasForeignKey(ctx context.Context, table, constraint string) (bool, error) {
return false, nil
}
// CreateSchema creates a new database schema.
func (m *Migration) CreateSchema(ctx context.Context, schema string) error {
sql := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", database.QuoteIdentifier(schema))
return m.ExecuteSQL(ctx, sql)
}
// DropSchema drops an existing database schema.
func (m *Migration) DropSchema(ctx context.Context, schema string, cascade ...bool) error {
sql := fmt.Sprintf("DROP DATABASE IF EXISTS %s", database.QuoteIdentifier(schema))
return m.ExecuteSQL(ctx, sql)
}
// HasSchema checks if a schema exists.
func (m *Migration) HasSchema(ctx context.Context, schema string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM system.databases WHERE name = '%s'",
schema,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// formatDefaultValue formats the default value for SQL.
func formatDefaultValue(value any) string {
switch v := value.(type) {
case string:
return fmt.Sprintf("'%s'", escapeString(v))
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return fmt.Sprintf("%d", v)
case float32, float64:
return fmt.Sprintf("%f", v)
case bool:
if v {
return "1"
}
return "0"
case nil:
return "NULL"
default:
return fmt.Sprintf("'%v'", v)
}
}
// escapeString escapes special characters in strings for SQL.
func escapeString(s string) string {
s = strings.ReplaceAll(s, "'", "\\'")
return s
}
+10
View File
@@ -46,3 +46,13 @@ func (d *Driver) New(core *database.Core, node *database.ConfigNode) (database.D
func (d *Driver) GetChars() (charLeft string, charRight string) {
return quoteChar, quoteChar
}
// Migration returns a Migration instance for MSSQL database operations.
func (d *Driver) Migration() *Migration {
return NewMigration(d)
}
// GetMigration returns a Migration instance implementing the Migration interface.
func (d *Driver) GetMigration() database.Migration {
return d.Migration()
}
+340
View File
@@ -0,0 +1,340 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package mssql
import (
"context"
"fmt"
"strings"
"git.magicany.cc/black1552/gin-base/database"
)
// Migration implements database migration operations for SQL Server.
type Migration struct {
*database.MigrationCore
*database.AutoMigrateCore
}
// NewMigration creates a new SQL Server Migration instance.
func NewMigration(db database.DB) *Migration {
return &Migration{
MigrationCore: database.NewMigrationCore(db),
AutoMigrateCore: database.NewAutoMigrateCore(db),
}
}
// CreateTable creates a new table with the given name and column definitions.
func (m *Migration) CreateTable(ctx context.Context, table string, columns map[string]*database.ColumnDefinition, options ...database.TableOption) error {
if len(columns) == 0 {
return fmt.Errorf("cannot create table without columns")
}
var opts database.TableOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE TABLE ")
sql.WriteString(database.QuoteIdentifier(table))
sql.WriteString(" (\n")
// Add columns
var colDefs []string
var primaryKeys []string
for name, def := range columns {
colDef := m.buildColumnDefinition(name, def)
if def.PrimaryKey {
primaryKeys = append(primaryKeys, database.QuoteIdentifier(name))
}
colDefs = append(colDefs, " "+colDef)
}
// Add primary key constraint if needed
if len(primaryKeys) > 0 {
colDefs = append(colDefs, fmt.Sprintf(" PRIMARY KEY (%s)", strings.Join(primaryKeys, ", ")))
}
sql.WriteString(strings.Join(colDefs, ",\n"))
sql.WriteString("\n)")
return m.ExecuteSQL(ctx, sql.String())
}
// buildColumnDefinition builds column definition for SQL Server.
func (m *Migration) buildColumnDefinition(name string, def *database.ColumnDefinition) string {
var parts []string
parts = append(parts, database.QuoteIdentifier(name))
// Handle SQL Server-specific types
dbType := def.Type
if def.AutoIncrement {
if dbType == "INT" || dbType == "INTEGER" {
dbType = "INT IDENTITY(1,1)"
} else if dbType == "BIGINT" {
dbType = "BIGINT IDENTITY(1,1)"
}
}
parts = append(parts, dbType)
if !def.Null {
parts = append(parts, "NOT NULL")
} else {
parts = append(parts, "NULL")
}
if def.Default != nil {
defaultValue := formatDefaultValue(def.Default)
parts = append(parts, fmt.Sprintf("DEFAULT %s", defaultValue))
}
return strings.Join(parts, " ")
}
// DropTable drops an existing table from the database.
func (m *Migration) DropTable(ctx context.Context, table string, ifExists ...bool) error {
if len(ifExists) > 0 && ifExists[0] {
sql := fmt.Sprintf(
"IF OBJECT_ID('%s', 'U') IS NOT NULL DROP TABLE %s",
table,
database.QuoteIdentifier(table),
)
return m.ExecuteSQL(ctx, sql)
}
sql := fmt.Sprintf("DROP TABLE %s", database.QuoteIdentifier(table))
return m.ExecuteSQL(ctx, sql)
}
// HasTable checks if a table exists in the database.
func (m *Migration) HasTable(ctx context.Context, table string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '%s'",
table,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// RenameTable renames an existing table from oldName to newName.
func (m *Migration) RenameTable(ctx context.Context, oldName, newName string) error {
sql := fmt.Sprintf(
"EXEC sp_rename '%s', '%s'",
oldName,
newName,
)
return m.ExecuteSQL(ctx, sql)
}
// TruncateTable removes all records from a table but keeps the table structure.
func (m *Migration) TruncateTable(ctx context.Context, table string) error {
sql := fmt.Sprintf("TRUNCATE TABLE %s", database.QuoteIdentifier(table))
return m.ExecuteSQL(ctx, sql)
}
// AddColumn adds a new column to an existing table.
func (m *Migration) AddColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.buildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s ADD %s", database.QuoteIdentifier(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// DropColumn removes a column from an existing table.
func (m *Migration) DropColumn(ctx context.Context, table, column string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP COLUMN %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(column),
)
return m.ExecuteSQL(ctx, sql)
}
// RenameColumn renames a column in an existing table.
func (m *Migration) RenameColumn(ctx context.Context, table, oldName, newName string) error {
sql := fmt.Sprintf(
"EXEC sp_rename '%s.%s', '%s', 'COLUMN'",
table,
oldName,
newName,
)
return m.ExecuteSQL(ctx, sql)
}
// ModifyColumn modifies an existing column's definition.
func (m *Migration) ModifyColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.buildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s ALTER COLUMN %s", database.QuoteIdentifier(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// HasColumn checks if a column exists in a table.
func (m *Migration) HasColumn(ctx context.Context, table, column string) (bool, error) {
fields, err := m.GetDB().TableFields(ctx, table)
if err != nil {
return false, err
}
_, exists := fields[column]
return exists, nil
}
// CreateIndex creates a new index on the specified table and columns.
func (m *Migration) CreateIndex(ctx context.Context, table, index string, columns []string, options ...database.IndexOption) error {
var opts database.IndexOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE ")
if opts.Unique {
sql.WriteString("UNIQUE ")
}
sql.WriteString("INDEX ")
sql.WriteString(database.QuoteIdentifier(index))
sql.WriteString(" ON ")
sql.WriteString(database.QuoteIdentifier(table))
colList := m.BuildIndexColumns(columns)
sql.WriteString(fmt.Sprintf(" (%s)", colList))
return m.ExecuteSQL(ctx, sql.String())
}
// DropIndex drops an existing index from a table.
func (m *Migration) DropIndex(ctx context.Context, table, index string) error {
sql := fmt.Sprintf("DROP INDEX %s ON %s", database.QuoteIdentifier(index), database.QuoteIdentifier(table))
return m.ExecuteSQL(ctx, sql)
}
// HasIndex checks if an index exists on a table.
func (m *Migration) HasIndex(ctx context.Context, table, index string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM sys.indexes WHERE name = '%s' AND object_id = OBJECT_ID('%s')",
index,
table,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateForeignKey creates a foreign key constraint.
func (m *Migration) CreateForeignKey(ctx context.Context, table, constraint string, columns []string, refTable string, refColumns []string, options ...database.ForeignKeyOption) error {
var opts database.ForeignKeyOptions
for _, opt := range options {
opt(&opts)
}
sql := fmt.Sprintf(
"ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY %s REFERENCES %s %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(constraint),
m.BuildForeignKeyColumns(columns),
database.QuoteIdentifier(refTable),
m.BuildForeignKeyColumns(refColumns),
)
if opts.OnDelete != "" {
sql += fmt.Sprintf(" ON DELETE %s", opts.OnDelete)
}
if opts.OnUpdate != "" {
sql += fmt.Sprintf(" ON UPDATE %s", opts.OnUpdate)
}
return m.ExecuteSQL(ctx, sql)
}
// DropForeignKey drops a foreign key constraint.
func (m *Migration) DropForeignKey(ctx context.Context, table, constraint string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP CONSTRAINT %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(constraint),
)
return m.ExecuteSQL(ctx, sql)
}
// HasForeignKey checks if a foreign key constraint exists.
func (m *Migration) HasForeignKey(ctx context.Context, table, constraint string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_NAME = '%s' AND TABLE_NAME = '%s' AND CONSTRAINT_TYPE = 'FOREIGN KEY'",
constraint,
table,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateSchema creates a new database schema.
func (m *Migration) CreateSchema(ctx context.Context, schema string) error {
sql := fmt.Sprintf("CREATE SCHEMA %s", database.QuoteIdentifier(schema))
return m.ExecuteSQL(ctx, sql)
}
// DropSchema drops an existing database schema.
func (m *Migration) DropSchema(ctx context.Context, schema string, cascade ...bool) error {
sql := fmt.Sprintf("DROP SCHEMA %s", database.QuoteIdentifier(schema))
return m.ExecuteSQL(ctx, sql)
}
// HasSchema checks if a schema exists.
func (m *Migration) HasSchema(ctx context.Context, schema string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '%s'",
schema,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// formatDefaultValue formats the default value for SQL.
func formatDefaultValue(value any) string {
switch v := value.(type) {
case string:
return fmt.Sprintf("'%s'", escapeString(v))
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return fmt.Sprintf("%d", v)
case float32, float64:
return fmt.Sprintf("%f", v)
case bool:
if v {
return "1"
}
return "0"
case nil:
return "NULL"
default:
return fmt.Sprintf("'%v'", v)
}
}
// escapeString escapes special characters in strings for SQL.
func escapeString(s string) string {
s = strings.ReplaceAll(s, "'", "''")
return s
}
+10
View File
@@ -52,3 +52,13 @@ func (d *Driver) New(core *database.Core, node *database.ConfigNode) (database.D
func (d *Driver) GetChars() (charLeft string, charRight string) {
return quoteChar, quoteChar
}
// Migration returns a Migration instance for MySQL database operations.
func (d *Driver) Migration() *Migration {
return NewMigration(d)
}
// GetMigration returns a Migration instance implementing the Migration interface.
func (d *Driver) GetMigration() database.Migration {
return d.Migration()
}
+365
View File
@@ -0,0 +1,365 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package mysql
import (
"context"
"fmt"
"strings"
"git.magicany.cc/black1552/gin-base/database"
)
// Migration implements database migration operations for MySQL.
type Migration struct {
*database.MigrationCore
*database.AutoMigrateCore
}
// NewMigration creates a new MySQL Migration instance.
func NewMigration(db database.DB) *Migration {
return &Migration{
MigrationCore: database.NewMigrationCore(db),
AutoMigrateCore: database.NewAutoMigrateCore(db),
}
}
// CreateTable creates a new table with the given name and column definitions.
func (m *Migration) CreateTable(ctx context.Context, table string, columns map[string]*database.ColumnDefinition, options ...database.TableOption) error {
if len(columns) == 0 {
return fmt.Errorf("cannot create table without columns")
}
var opts database.TableOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE TABLE ")
if opts.IfNotExists {
sql.WriteString("IF NOT EXISTS ")
}
sql.WriteString(database.QuoteIdentifier(table))
sql.WriteString(" (\n")
// Add columns
var colDefs []string
var primaryKeys []string
for name, def := range columns {
colDef := m.BuildColumnDefinition(name, def)
if def.PrimaryKey {
primaryKeys = append(primaryKeys, database.QuoteIdentifier(name))
}
colDefs = append(colDefs, " "+colDef)
}
// Add primary key constraint if needed
if len(primaryKeys) > 0 {
colDefs = append(colDefs, fmt.Sprintf(" PRIMARY KEY (%s)", strings.Join(primaryKeys, ", ")))
}
sql.WriteString(strings.Join(colDefs, ",\n"))
sql.WriteString("\n)")
// Add table options
if opts.Engine != "" {
sql.WriteString(fmt.Sprintf(" ENGINE=%s", opts.Engine))
}
if opts.Charset != "" {
sql.WriteString(fmt.Sprintf(" DEFAULT CHARSET=%s", opts.Charset))
}
if opts.Collation != "" {
sql.WriteString(fmt.Sprintf(" COLLATE=%s", opts.Collation))
}
if opts.Comment != "" {
sql.WriteString(fmt.Sprintf(" COMMENT='%s'", escapeString(opts.Comment)))
}
return m.ExecuteSQL(ctx, sql.String())
}
// DropTable drops an existing table from the database.
func (m *Migration) DropTable(ctx context.Context, table string, ifExists ...bool) error {
sql := "DROP TABLE "
if len(ifExists) > 0 && ifExists[0] {
sql += "IF EXISTS "
}
sql += database.QuoteIdentifier(table)
return m.ExecuteSQL(ctx, sql)
}
// HasTable checks if a table exists in the database.
func (m *Migration) HasTable(ctx context.Context, table string) (bool, error) {
schema := m.GetDB().GetSchema()
if schema == "" {
schema = "DATABASE()"
} else {
schema = fmt.Sprintf("'%s'", schema)
}
query := fmt.Sprintf(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = %s AND table_name = '%s'",
schema,
table,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// RenameTable renames an existing table from oldName to newName.
func (m *Migration) RenameTable(ctx context.Context, oldName, newName string) error {
sql := fmt.Sprintf(
"RENAME TABLE %s TO %s",
database.QuoteIdentifier(oldName),
database.QuoteIdentifier(newName),
)
return m.ExecuteSQL(ctx, sql)
}
// TruncateTable removes all records from a table but keeps the table structure.
func (m *Migration) TruncateTable(ctx context.Context, table string) error {
sql := fmt.Sprintf("TRUNCATE TABLE %s", database.QuoteIdentifier(table))
return m.ExecuteSQL(ctx, sql)
}
// AddColumn adds a new column to an existing table.
func (m *Migration) AddColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.BuildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s", database.QuoteIdentifier(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// DropColumn removes a column from an existing table.
func (m *Migration) DropColumn(ctx context.Context, table, column string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP COLUMN %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(column),
)
return m.ExecuteSQL(ctx, sql)
}
// RenameColumn renames a column in an existing table.
func (m *Migration) RenameColumn(ctx context.Context, table, oldName, newName string) error {
// MySQL requires the full column definition when renaming
fields, err := m.GetDB().TableFields(ctx, table)
if err != nil {
return err
}
field, ok := fields[oldName]
if !ok {
return fmt.Errorf("column %s does not exist in table %s", oldName, table)
}
def := &database.ColumnDefinition{
Type: field.Type,
Null: field.Null,
Default: field.Default,
Comment: field.Comment,
AutoIncrement: strings.Contains(field.Extra, "auto_increment"),
}
colDef := m.BuildColumnDefinition(newName, def)
sql := fmt.Sprintf(
"ALTER TABLE %s CHANGE COLUMN %s %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(oldName),
colDef,
)
return m.ExecuteSQL(ctx, sql)
}
// ModifyColumn modifies an existing column's definition.
func (m *Migration) ModifyColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.BuildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s MODIFY COLUMN %s", database.QuoteIdentifier(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// HasColumn checks if a column exists in a table.
func (m *Migration) HasColumn(ctx context.Context, table, column string) (bool, error) {
fields, err := m.GetDB().TableFields(ctx, table)
if err != nil {
return false, err
}
_, exists := fields[column]
return exists, nil
}
// CreateIndex creates a new index on the specified table and columns.
func (m *Migration) CreateIndex(ctx context.Context, table, index string, columns []string, options ...database.IndexOption) error {
var opts database.IndexOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE ")
if opts.Unique {
sql.WriteString("UNIQUE ")
}
if opts.FullText {
sql.WriteString("FULLTEXT ")
}
if opts.Spatial {
sql.WriteString("SPATIAL ")
}
sql.WriteString("INDEX ")
sql.WriteString(database.QuoteIdentifier(index))
sql.WriteString(" ON ")
sql.WriteString(database.QuoteIdentifier(table))
colList := m.BuildIndexColumns(columns)
sql.WriteString(fmt.Sprintf(" (%s)", colList))
if opts.Using != "" {
sql.WriteString(fmt.Sprintf(" USING %s", opts.Using))
}
return m.ExecuteSQL(ctx, sql.String())
}
// DropIndex drops an existing index from a table.
func (m *Migration) DropIndex(ctx context.Context, table, index string) error {
sql := fmt.Sprintf(
"DROP INDEX %s ON %s",
database.QuoteIdentifier(index),
database.QuoteIdentifier(table),
)
return m.ExecuteSQL(ctx, sql)
}
// HasIndex checks if an index exists on a table.
func (m *Migration) HasIndex(ctx context.Context, table, index string) (bool, error) {
schema := m.GetDB().GetSchema()
if schema == "" {
schema = "DATABASE()"
} else {
schema = fmt.Sprintf("'%s'", schema)
}
query := fmt.Sprintf(
"SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = %s AND table_name = '%s' AND index_name = '%s'",
schema,
table,
index,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateForeignKey creates a foreign key constraint.
func (m *Migration) CreateForeignKey(ctx context.Context, table, constraint string, columns []string, refTable string, refColumns []string, options ...database.ForeignKeyOption) error {
var opts database.ForeignKeyOptions
for _, opt := range options {
opt(&opts)
}
sql := fmt.Sprintf(
"ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY %s REFERENCES %s %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(constraint),
m.BuildForeignKeyColumns(columns),
database.QuoteIdentifier(refTable),
m.BuildForeignKeyColumns(refColumns),
)
if opts.OnDelete != "" {
sql += fmt.Sprintf(" ON DELETE %s", opts.OnDelete)
}
if opts.OnUpdate != "" {
sql += fmt.Sprintf(" ON UPDATE %s", opts.OnUpdate)
}
return m.ExecuteSQL(ctx, sql)
}
// DropForeignKey drops a foreign key constraint.
func (m *Migration) DropForeignKey(ctx context.Context, table, constraint string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP FOREIGN KEY %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(constraint),
)
return m.ExecuteSQL(ctx, sql)
}
// HasForeignKey checks if a foreign key constraint exists.
func (m *Migration) HasForeignKey(ctx context.Context, table, constraint string) (bool, error) {
schema := m.GetDB().GetSchema()
if schema == "" {
schema = "DATABASE()"
} else {
schema = fmt.Sprintf("'%s'", schema)
}
query := fmt.Sprintf(
"SELECT COUNT(*) FROM information_schema.table_constraints WHERE constraint_schema = %s AND table_name = '%s' AND constraint_name = '%s' AND constraint_type = 'FOREIGN KEY'",
schema,
table,
constraint,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateSchema creates a new database schema.
func (m *Migration) CreateSchema(ctx context.Context, schema string) error {
sql := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", database.QuoteIdentifier(schema))
return m.ExecuteSQL(ctx, sql)
}
// DropSchema drops an existing database schema.
func (m *Migration) DropSchema(ctx context.Context, schema string, cascade ...bool) error {
sql := "DROP DATABASE "
if len(cascade) > 0 && cascade[0] {
sql += "IF EXISTS "
}
sql += database.QuoteIdentifier(schema)
return m.ExecuteSQL(ctx, sql)
}
// HasSchema checks if a schema exists.
func (m *Migration) HasSchema(ctx context.Context, schema string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = '%s'",
schema,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// escapeString escapes special characters in strings for SQL.
func escapeString(s string) string {
s = strings.ReplaceAll(s, "'", "''")
s = strings.ReplaceAll(s, "\\", "\\\\")
return s
}
+10
View File
@@ -44,3 +44,13 @@ func (d *Driver) New(core *database.Core, node *database.ConfigNode) (database.D
func (d *Driver) GetChars() (charLeft string, charRight string) {
return quoteChar, quoteChar
}
// Migration returns a Migration instance for Oracle database operations.
func (d *Driver) Migration() *Migration {
return NewMigration(d)
}
// GetMigration returns a Migration instance implementing the Migration interface.
func (d *Driver) GetMigration() database.Migration {
return d.Migration()
}
+351
View File
@@ -0,0 +1,351 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package oracle
import (
"context"
"fmt"
"strings"
"git.magicany.cc/black1552/gin-base/database"
)
// Migration implements database migration operations for Oracle.
type Migration struct {
*database.MigrationCore
*database.AutoMigrateCore
}
// NewMigration creates a new Oracle Migration instance.
func NewMigration(db database.DB) *Migration {
return &Migration{
MigrationCore: database.NewMigrationCore(db),
AutoMigrateCore: database.NewAutoMigrateCore(db),
}
}
// CreateTable creates a new table with the given name and column definitions.
func (m *Migration) CreateTable(ctx context.Context, table string, columns map[string]*database.ColumnDefinition, options ...database.TableOption) error {
if len(columns) == 0 {
return fmt.Errorf("cannot create table without columns")
}
var opts database.TableOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE TABLE ")
sql.WriteString(database.QuoteIdentifierDouble(table))
sql.WriteString(" (\n")
// Add columns
var colDefs []string
var primaryKeys []string
for name, def := range columns {
colDef := m.buildColumnDefinition(name, def)
if def.PrimaryKey {
primaryKeys = append(primaryKeys, database.QuoteIdentifierDouble(name))
}
colDefs = append(colDefs, " "+colDef)
}
// Add primary key constraint if needed
if len(primaryKeys) > 0 {
colDefs = append(colDefs, fmt.Sprintf(" PRIMARY KEY (%s)", strings.Join(primaryKeys, ", ")))
}
sql.WriteString(strings.Join(colDefs, ",\n"))
sql.WriteString("\n)")
return m.ExecuteSQL(ctx, sql.String())
}
// buildColumnDefinition builds column definition for Oracle.
func (m *Migration) buildColumnDefinition(name string, def *database.ColumnDefinition) string {
var parts []string
parts = append(parts, database.QuoteIdentifierDouble(name))
// Handle Oracle-specific types
dbType := def.Type
parts = append(parts, dbType)
if !def.Null {
parts = append(parts, "NOT NULL")
}
if def.Default != nil {
defaultValue := formatDefaultValue(def.Default)
parts = append(parts, fmt.Sprintf("DEFAULT %s", defaultValue))
}
return strings.Join(parts, " ")
}
// DropTable drops an existing table from the database.
func (m *Migration) DropTable(ctx context.Context, table string, ifExists ...bool) error {
if len(ifExists) > 0 && ifExists[0] {
sql := fmt.Sprintf(
"BEGIN EXECUTE IMMEDIATE 'DROP TABLE %s'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;",
database.QuoteIdentifierDouble(table),
)
return m.ExecuteSQL(ctx, sql)
}
sql := fmt.Sprintf("DROP TABLE %s", database.QuoteIdentifierDouble(table))
return m.ExecuteSQL(ctx, sql)
}
// HasTable checks if a table exists in the database.
func (m *Migration) HasTable(ctx context.Context, table string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME = '%s'",
strings.ToUpper(table),
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// RenameTable renames an existing table from oldName to newName.
func (m *Migration) RenameTable(ctx context.Context, oldName, newName string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s RENAME TO %s",
database.QuoteIdentifierDouble(oldName),
database.QuoteIdentifierDouble(newName),
)
return m.ExecuteSQL(ctx, sql)
}
// TruncateTable removes all records from a table but keeps the table structure.
func (m *Migration) TruncateTable(ctx context.Context, table string) error {
sql := fmt.Sprintf("TRUNCATE TABLE %s", database.QuoteIdentifierDouble(table))
return m.ExecuteSQL(ctx, sql)
}
// AddColumn adds a new column to an existing table.
func (m *Migration) AddColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.buildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s ADD %s", database.QuoteIdentifierDouble(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// DropColumn removes a column from an existing table.
func (m *Migration) DropColumn(ctx context.Context, table, column string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP COLUMN %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(column),
)
return m.ExecuteSQL(ctx, sql)
}
// RenameColumn renames a column in an existing table.
func (m *Migration) RenameColumn(ctx context.Context, table, oldName, newName string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s RENAME COLUMN %s TO %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(oldName),
database.QuoteIdentifierDouble(newName),
)
return m.ExecuteSQL(ctx, sql)
}
// ModifyColumn modifies an existing column's definition.
func (m *Migration) ModifyColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.buildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s MODIFY %s", database.QuoteIdentifierDouble(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// HasColumn checks if a column exists in a table.
func (m *Migration) HasColumn(ctx context.Context, table, column string) (bool, error) {
fields, err := m.GetDB().TableFields(ctx, table)
if err != nil {
return false, err
}
_, exists := fields[column]
return exists, nil
}
// CreateIndex creates a new index on the specified table and columns.
func (m *Migration) CreateIndex(ctx context.Context, table, index string, columns []string, options ...database.IndexOption) error {
var opts database.IndexOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE ")
if opts.Unique {
sql.WriteString("UNIQUE ")
}
sql.WriteString("INDEX ")
sql.WriteString(database.QuoteIdentifierDouble(index))
sql.WriteString(" ON ")
sql.WriteString(database.QuoteIdentifierDouble(table))
colList := m.BuildIndexColumnsWithDouble(columns)
sql.WriteString(fmt.Sprintf(" (%s)", colList))
return m.ExecuteSQL(ctx, sql.String())
}
// BuildIndexColumnsWithDouble builds the column list for index creation using double quotes.
func (m *Migration) BuildIndexColumnsWithDouble(columns []string) string {
quoted := make([]string, len(columns))
for i, col := range columns {
quoted[i] = database.QuoteIdentifierDouble(col)
}
return strings.Join(quoted, ", ")
}
// DropIndex drops an existing index from a table.
func (m *Migration) DropIndex(ctx context.Context, table, index string) error {
sql := fmt.Sprintf("DROP INDEX %s", database.QuoteIdentifierDouble(index))
return m.ExecuteSQL(ctx, sql)
}
// HasIndex checks if an index exists on a table.
func (m *Migration) HasIndex(ctx context.Context, table, index string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM USER_INDEXES WHERE INDEX_NAME = '%s' AND TABLE_NAME = '%s'",
strings.ToUpper(index),
strings.ToUpper(table),
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateForeignKey creates a foreign key constraint.
func (m *Migration) CreateForeignKey(ctx context.Context, table, constraint string, columns []string, refTable string, refColumns []string, options ...database.ForeignKeyOption) error {
var opts database.ForeignKeyOptions
for _, opt := range options {
opt(&opts)
}
sql := fmt.Sprintf(
"ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY %s REFERENCES %s %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(constraint),
m.BuildForeignKeyColumnsWithDouble(columns),
database.QuoteIdentifierDouble(refTable),
m.BuildForeignKeyColumnsWithDouble(refColumns),
)
if opts.OnDelete != "" {
sql += fmt.Sprintf(" ON DELETE %s", opts.OnDelete)
}
return m.ExecuteSQL(ctx, sql)
}
// BuildForeignKeyColumnsWithDouble builds the column list for foreign key using double quotes.
func (m *Migration) BuildForeignKeyColumnsWithDouble(columns []string) string {
quoted := make([]string, len(columns))
for i, col := range columns {
quoted[i] = database.QuoteIdentifierDouble(col)
}
return "(" + strings.Join(quoted, ", ") + ")"
}
// DropForeignKey drops a foreign key constraint.
func (m *Migration) DropForeignKey(ctx context.Context, table, constraint string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP CONSTRAINT %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(constraint),
)
return m.ExecuteSQL(ctx, sql)
}
// HasForeignKey checks if a foreign key constraint exists.
func (m *Migration) HasForeignKey(ctx context.Context, table, constraint string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM USER_CONSTRAINTS WHERE CONSTRAINT_NAME = '%s' AND TABLE_NAME = '%s' AND CONSTRAINT_TYPE = 'R'",
strings.ToUpper(constraint),
strings.ToUpper(table),
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateSchema creates a new database schema (user in Oracle).
func (m *Migration) CreateSchema(ctx context.Context, schema string) error {
// In Oracle, schema is equivalent to user
sql := fmt.Sprintf("CREATE USER %s IDENTIFIED BY password", database.QuoteIdentifierDouble(schema))
return m.ExecuteSQL(ctx, sql)
}
// DropSchema drops an existing database schema (user in Oracle).
func (m *Migration) DropSchema(ctx context.Context, schema string, cascade ...bool) error {
sql := "DROP USER "
if len(cascade) > 0 && cascade[0] {
sql += database.QuoteIdentifierDouble(schema) + " CASCADE"
} else {
sql += database.QuoteIdentifierDouble(schema)
}
return m.ExecuteSQL(ctx, sql)
}
// HasSchema checks if a schema exists.
func (m *Migration) HasSchema(ctx context.Context, schema string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM ALL_USERS WHERE USERNAME = '%s'",
strings.ToUpper(schema),
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// formatDefaultValue formats the default value for SQL.
func formatDefaultValue(value any) string {
switch v := value.(type) {
case string:
return fmt.Sprintf("'%s'", escapeString(v))
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return fmt.Sprintf("%d", v)
case float32, float64:
return fmt.Sprintf("%f", v)
case bool:
if v {
return "1"
}
return "0"
case nil:
return "NULL"
default:
return fmt.Sprintf("'%v'", v)
}
}
// escapeString escapes special characters in strings for SQL.
func escapeString(s string) string {
s = strings.ReplaceAll(s, "'", "''")
return s
}
+10
View File
@@ -48,3 +48,13 @@ func (d *Driver) New(core *database.Core, node *database.ConfigNode) (database.D
func (d *Driver) GetChars() (charLeft string, charRight string) {
return quoteChar, quoteChar
}
// Migration returns a Migration instance for PostgreSQL database operations.
func (d *Driver) Migration() *Migration {
return NewMigration(d)
}
// GetMigration returns a Migration instance implementing the Migration interface.
func (d *Driver) GetMigration() database.Migration {
return d.Migration()
}
+481
View File
@@ -0,0 +1,481 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package pgsql
import (
"context"
"fmt"
"strings"
"git.magicany.cc/black1552/gin-base/database"
)
// Migration implements database migration operations for PostgreSQL.
type Migration struct {
*database.MigrationCore
*database.AutoMigrateCore
}
// NewMigration creates a new PostgreSQL Migration instance.
func NewMigration(db database.DB) *Migration {
return &Migration{
MigrationCore: database.NewMigrationCore(db),
AutoMigrateCore: database.NewAutoMigrateCore(db),
}
}
// CreateTable creates a new table with the given name and column definitions.
func (m *Migration) CreateTable(ctx context.Context, table string, columns map[string]*database.ColumnDefinition, options ...database.TableOption) error {
if len(columns) == 0 {
return fmt.Errorf("cannot create table without columns")
}
var opts database.TableOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE TABLE ")
if opts.IfNotExists {
sql.WriteString("IF NOT EXISTS ")
}
sql.WriteString(database.QuoteIdentifierDouble(table))
sql.WriteString(" (\n")
// Add columns
var colDefs []string
var primaryKeys []string
for name, def := range columns {
colDef := m.buildColumnDefinition(name, def)
if def.PrimaryKey {
primaryKeys = append(primaryKeys, database.QuoteIdentifierDouble(name))
}
colDefs = append(colDefs, " "+colDef)
}
// Add primary key constraint if needed
if len(primaryKeys) > 0 {
colDefs = append(colDefs, fmt.Sprintf(" PRIMARY KEY (%s)", strings.Join(primaryKeys, ", ")))
}
sql.WriteString(strings.Join(colDefs, ",\n"))
sql.WriteString("\n)")
// Add table comment if provided
if opts.Comment != "" {
commentSQL := fmt.Sprintf(
"COMMENT ON TABLE %s IS '%s'",
database.QuoteIdentifierDouble(table),
escapeString(opts.Comment),
)
if err := m.ExecuteSQL(ctx, commentSQL); err != nil {
return err
}
}
return m.ExecuteSQL(ctx, sql.String())
}
// buildColumnDefinition builds column definition for PostgreSQL.
func (m *Migration) buildColumnDefinition(name string, def *database.ColumnDefinition) string {
var parts []string
parts = append(parts, database.QuoteIdentifierDouble(name))
// Handle PostgreSQL-specific types
dbType := def.Type
if def.AutoIncrement {
if dbType == "INT" || dbType == "INTEGER" {
dbType = "SERIAL"
} else if dbType == "BIGINT" {
dbType = "BIGSERIAL"
}
}
parts = append(parts, dbType)
if def.PrimaryKey {
// Primary key is handled separately
} else {
if !def.Null {
parts = append(parts, "NOT NULL")
}
if def.Unique {
parts = append(parts, "UNIQUE")
}
if def.Default != nil {
defaultValue := formatDefaultValue(def.Default)
parts = append(parts, fmt.Sprintf("DEFAULT %s", defaultValue))
}
}
return strings.Join(parts, " ")
}
// DropTable drops an existing table from the database.
func (m *Migration) DropTable(ctx context.Context, table string, ifExists ...bool) error {
sql := "DROP TABLE "
if len(ifExists) > 0 && ifExists[0] {
sql += "IF EXISTS "
}
sql += database.QuoteIdentifierDouble(table)
return m.ExecuteSQL(ctx, sql)
}
// HasTable checks if a table exists in the database.
func (m *Migration) HasTable(ctx context.Context, table string) (bool, error) {
schema := m.GetDB().GetSchema()
if schema == "" {
schema = "current_schema()"
} else {
schema = fmt.Sprintf("'%s'", schema)
}
query := fmt.Sprintf(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = %s AND table_name = '%s'",
schema,
table,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// RenameTable renames an existing table from oldName to newName.
func (m *Migration) RenameTable(ctx context.Context, oldName, newName string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s RENAME TO %s",
database.QuoteIdentifierDouble(oldName),
database.QuoteIdentifierDouble(newName),
)
return m.ExecuteSQL(ctx, sql)
}
// TruncateTable removes all records from a table but keeps the table structure.
func (m *Migration) TruncateTable(ctx context.Context, table string) error {
sql := fmt.Sprintf("TRUNCATE TABLE %s", database.QuoteIdentifierDouble(table))
return m.ExecuteSQL(ctx, sql)
}
// AddColumn adds a new column to an existing table.
func (m *Migration) AddColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.buildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s", database.QuoteIdentifierDouble(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// DropColumn removes a column from an existing table.
func (m *Migration) DropColumn(ctx context.Context, table, column string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP COLUMN %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(column),
)
return m.ExecuteSQL(ctx, sql)
}
// RenameColumn renames a column in an existing table.
func (m *Migration) RenameColumn(ctx context.Context, table, oldName, newName string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s RENAME COLUMN %s TO %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(oldName),
database.QuoteIdentifierDouble(newName),
)
return m.ExecuteSQL(ctx, sql)
}
// ModifyColumn modifies an existing column's definition.
func (m *Migration) ModifyColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
// PostgreSQL requires multiple ALTER statements for different modifications
var statements []string
if definition.Type != "" {
statements = append(statements, fmt.Sprintf(
"ALTER TABLE %s ALTER COLUMN %s TYPE %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(column),
definition.Type,
))
}
if definition.Default != nil {
defaultValue := formatDefaultValue(definition.Default)
statements = append(statements, fmt.Sprintf(
"ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(column),
defaultValue,
))
} else {
statements = append(statements, fmt.Sprintf(
"ALTER TABLE %s ALTER COLUMN %s DROP DEFAULT",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(column),
))
}
if !definition.Null {
statements = append(statements, fmt.Sprintf(
"ALTER TABLE %s ALTER COLUMN %s SET NOT NULL",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(column),
))
} else {
statements = append(statements, fmt.Sprintf(
"ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(column),
))
}
// Execute all statements
for _, stmt := range statements {
if err := m.ExecuteSQL(ctx, stmt); err != nil {
return err
}
}
return nil
}
// HasColumn checks if a column exists in a table.
func (m *Migration) HasColumn(ctx context.Context, table, column string) (bool, error) {
fields, err := m.GetDB().TableFields(ctx, table)
if err != nil {
return false, err
}
_, exists := fields[column]
return exists, nil
}
// CreateIndex creates a new index on the specified table and columns.
func (m *Migration) CreateIndex(ctx context.Context, table, index string, columns []string, options ...database.IndexOption) error {
var opts database.IndexOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE ")
if opts.Unique {
sql.WriteString("UNIQUE ")
}
sql.WriteString("INDEX ")
sql.WriteString(database.QuoteIdentifierDouble(index))
sql.WriteString(" ON ")
sql.WriteString(database.QuoteIdentifierDouble(table))
colList := m.BuildIndexColumnsWithDouble(columns)
sql.WriteString(fmt.Sprintf(" (%s)", colList))
if opts.Using != "" {
sql.WriteString(fmt.Sprintf(" USING %s", opts.Using))
}
if opts.Comment != "" {
// Comment will be added after index creation
}
err := m.ExecuteSQL(ctx, sql.String())
if err != nil {
return err
}
// Add comment if provided
if opts.Comment != "" {
commentSQL := fmt.Sprintf(
"COMMENT ON INDEX %s IS '%s'",
database.QuoteIdentifierDouble(index),
escapeString(opts.Comment),
)
return m.ExecuteSQL(ctx, commentSQL)
}
return nil
}
// BuildIndexColumnsWithDouble builds the column list for index creation using double quotes.
func (m *Migration) BuildIndexColumnsWithDouble(columns []string) string {
quoted := make([]string, len(columns))
for i, col := range columns {
quoted[i] = database.QuoteIdentifierDouble(col)
}
return strings.Join(quoted, ", ")
}
// DropIndex drops an existing index from a table.
func (m *Migration) DropIndex(ctx context.Context, table, index string) error {
sql := fmt.Sprintf("DROP INDEX %s", database.QuoteIdentifierDouble(index))
return m.ExecuteSQL(ctx, sql)
}
// HasIndex checks if an index exists on a table.
func (m *Migration) HasIndex(ctx context.Context, table, index string) (bool, error) {
schema := m.GetDB().GetSchema()
if schema == "" {
schema = "current_schema()"
} else {
schema = fmt.Sprintf("'%s'", schema)
}
query := fmt.Sprintf(
"SELECT COUNT(*) FROM pg_indexes WHERE schemaname = %s AND tablename = '%s' AND indexname = '%s'",
schema,
table,
index,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateForeignKey creates a foreign key constraint.
func (m *Migration) CreateForeignKey(ctx context.Context, table, constraint string, columns []string, refTable string, refColumns []string, options ...database.ForeignKeyOption) error {
var opts database.ForeignKeyOptions
for _, opt := range options {
opt(&opts)
}
sql := fmt.Sprintf(
"ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY %s REFERENCES %s %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(constraint),
m.BuildForeignKeyColumnsWithDouble(columns),
database.QuoteIdentifierDouble(refTable),
m.BuildForeignKeyColumnsWithDouble(refColumns),
)
if opts.OnDelete != "" {
sql += fmt.Sprintf(" ON DELETE %s", opts.OnDelete)
}
if opts.OnUpdate != "" {
sql += fmt.Sprintf(" ON UPDATE %s", opts.OnUpdate)
}
if opts.Deferrable {
sql += " DEFERRABLE"
if opts.InitiallyDeferred {
sql += " INITIALLY DEFERRED"
}
}
return m.ExecuteSQL(ctx, sql)
}
// BuildForeignKeyColumnsWithDouble builds the column list for foreign key using double quotes.
func (m *Migration) BuildForeignKeyColumnsWithDouble(columns []string) string {
quoted := make([]string, len(columns))
for i, col := range columns {
quoted[i] = database.QuoteIdentifierDouble(col)
}
return "(" + strings.Join(quoted, ", ") + ")"
}
// DropForeignKey drops a foreign key constraint.
func (m *Migration) DropForeignKey(ctx context.Context, table, constraint string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP CONSTRAINT %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(constraint),
)
return m.ExecuteSQL(ctx, sql)
}
// HasForeignKey checks if a foreign key constraint exists.
func (m *Migration) HasForeignKey(ctx context.Context, table, constraint string) (bool, error) {
schema := m.GetDB().GetSchema()
if schema == "" {
schema = "current_schema()"
} else {
schema = fmt.Sprintf("'%s'", schema)
}
query := fmt.Sprintf(
"SELECT COUNT(*) FROM information_schema.table_constraints WHERE constraint_schema = %s AND table_name = '%s' AND constraint_name = '%s' AND constraint_type = 'FOREIGN KEY'",
schema,
table,
constraint,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateSchema creates a new database schema.
func (m *Migration) CreateSchema(ctx context.Context, schema string) error {
sql := fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", database.QuoteIdentifierDouble(schema))
return m.ExecuteSQL(ctx, sql)
}
// DropSchema drops an existing database schema.
func (m *Migration) DropSchema(ctx context.Context, schema string, cascade ...bool) error {
sql := "DROP SCHEMA "
if len(cascade) > 0 && cascade[0] {
sql += "IF EXISTS "
sql += database.QuoteIdentifierDouble(schema) + " CASCADE"
} else {
sql += "IF EXISTS " + database.QuoteIdentifierDouble(schema)
}
return m.ExecuteSQL(ctx, sql)
}
// HasSchema checks if a schema exists.
func (m *Migration) HasSchema(ctx context.Context, schema string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = '%s'",
schema,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// formatDefaultValue formats the default value for SQL.
func formatDefaultValue(value any) string {
switch v := value.(type) {
case string:
return fmt.Sprintf("'%s'", escapeString(v))
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return fmt.Sprintf("%d", v)
case float32, float64:
return fmt.Sprintf("%f", v)
case bool:
if v {
return "TRUE"
}
return "FALSE"
case nil:
return "NULL"
default:
return fmt.Sprintf("'%v'", v)
}
}
// escapeString escapes special characters in strings for SQL.
func escapeString(s string) string {
s = strings.ReplaceAll(s, "'", "''")
return s
}
+10
View File
@@ -45,3 +45,13 @@ func (d *Driver) New(core *database.Core, node *database.ConfigNode) (database.D
func (d *Driver) GetChars() (charLeft string, charRight string) {
return quoteChar, quoteChar
}
// Migration returns a Migration instance for SQLite database operations.
func (d *Driver) Migration() *Migration {
return NewMigration(d)
}
// GetMigration returns a Migration instance implementing the Migration interface.
func (d *Driver) GetMigration() database.Migration {
return d.Migration()
}
+322
View File
@@ -0,0 +1,322 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package sqlite
import (
"context"
"fmt"
"strings"
"git.magicany.cc/black1552/gin-base/database"
)
// Migration implements database migration operations for SQLite.
type Migration struct {
*database.MigrationCore
*database.AutoMigrateCore
}
// NewMigration creates a new SQLite Migration instance.
func NewMigration(db database.DB) *Migration {
return &Migration{
MigrationCore: database.NewMigrationCore(db),
AutoMigrateCore: database.NewAutoMigrateCore(db),
}
}
// CreateTable creates a new table with the given name and column definitions.
func (m *Migration) CreateTable(ctx context.Context, table string, columns map[string]*database.ColumnDefinition, options ...database.TableOption) error {
if len(columns) == 0 {
return fmt.Errorf("cannot create table without columns")
}
var opts database.TableOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE TABLE ")
if opts.IfNotExists {
sql.WriteString("IF NOT EXISTS ")
}
sql.WriteString(database.QuoteIdentifier(table))
sql.WriteString(" (\n")
// Add columns
var colDefs []string
var primaryKeys []string
for name, def := range columns {
colDef := m.buildColumnDefinition(name, def)
if def.PrimaryKey {
primaryKeys = append(primaryKeys, database.QuoteIdentifier(name))
}
colDefs = append(colDefs, " "+colDef)
}
// Add primary key constraint if needed
if len(primaryKeys) > 0 {
colDefs = append(colDefs, fmt.Sprintf(" PRIMARY KEY (%s)", strings.Join(primaryKeys, ", ")))
}
sql.WriteString(strings.Join(colDefs, ",\n"))
sql.WriteString("\n)")
return m.ExecuteSQL(ctx, sql.String())
}
// buildColumnDefinition builds column definition for SQLite.
func (m *Migration) buildColumnDefinition(name string, def *database.ColumnDefinition) string {
var parts []string
parts = append(parts, database.QuoteIdentifier(name))
// Handle SQLite-specific types
dbType := def.Type
if def.AutoIncrement && def.PrimaryKey {
if dbType == "INT" || dbType == "INTEGER" {
dbType = "INTEGER"
}
}
parts = append(parts, dbType)
if def.PrimaryKey && def.AutoIncrement {
parts = append(parts, "PRIMARY KEY AUTOINCREMENT")
} else {
if !def.Null {
parts = append(parts, "NOT NULL")
}
if def.Unique && !def.PrimaryKey {
parts = append(parts, "UNIQUE")
}
if def.Default != nil {
defaultValue := formatDefaultValue(def.Default)
parts = append(parts, fmt.Sprintf("DEFAULT %s", defaultValue))
}
}
return strings.Join(parts, " ")
}
// DropTable drops an existing table from the database.
func (m *Migration) DropTable(ctx context.Context, table string, ifExists ...bool) error {
sql := "DROP TABLE "
if len(ifExists) > 0 && ifExists[0] {
sql += "IF EXISTS "
}
sql += database.QuoteIdentifier(table)
return m.ExecuteSQL(ctx, sql)
}
// HasTable checks if a table exists in the database.
func (m *Migration) HasTable(ctx context.Context, table string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='%s'",
table,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// RenameTable renames an existing table from oldName to newName.
func (m *Migration) RenameTable(ctx context.Context, oldName, newName string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s RENAME TO %s",
database.QuoteIdentifier(oldName),
database.QuoteIdentifier(newName),
)
return m.ExecuteSQL(ctx, sql)
}
// TruncateTable removes all records from a table but keeps the table structure.
func (m *Migration) TruncateTable(ctx context.Context, table string) error {
sql := fmt.Sprintf("DELETE FROM %s", database.QuoteIdentifier(table))
return m.ExecuteSQL(ctx, sql)
}
// AddColumn adds a new column to an existing table.
func (m *Migration) AddColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.buildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s", database.QuoteIdentifier(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// DropColumn removes a column from an existing table.
// Note: SQLite has limited ALTER TABLE support. This may require table recreation.
func (m *Migration) DropColumn(ctx context.Context, table, column string) error {
// SQLite 3.35.0+ supports DROP COLUMN
sql := fmt.Sprintf(
"ALTER TABLE %s DROP COLUMN %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(column),
)
return m.ExecuteSQL(ctx, sql)
}
// RenameColumn renames a column in an existing table.
func (m *Migration) RenameColumn(ctx context.Context, table, oldName, newName string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s RENAME COLUMN %s TO %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(oldName),
database.QuoteIdentifierDouble(newName),
)
return m.ExecuteSQL(ctx, sql)
}
// ModifyColumn modifies an existing column's definition.
// Note: SQLite requires table recreation for most column modifications.
func (m *Migration) ModifyColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
// SQLite has very limited ALTER TABLE support
// This would typically require recreating the table
return fmt.Errorf("SQLite does not support MODIFY COLUMN directly, table recreation required")
}
// HasColumn checks if a column exists in a table.
func (m *Migration) HasColumn(ctx context.Context, table, column string) (bool, error) {
fields, err := m.GetDB().TableFields(ctx, table)
if err != nil {
return false, err
}
_, exists := fields[column]
return exists, nil
}
// CreateIndex creates a new index on the specified table and columns.
func (m *Migration) CreateIndex(ctx context.Context, table, index string, columns []string, options ...database.IndexOption) error {
var opts database.IndexOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE ")
if opts.Unique {
sql.WriteString("UNIQUE ")
}
sql.WriteString("INDEX ")
if opts.IfNotExists {
sql.WriteString("IF NOT EXISTS ")
}
sql.WriteString(database.QuoteIdentifier(index))
sql.WriteString(" ON ")
sql.WriteString(database.QuoteIdentifier(table))
colList := m.BuildIndexColumns(columns)
sql.WriteString(fmt.Sprintf(" (%s)", colList))
return m.ExecuteSQL(ctx, sql.String())
}
// DropIndex drops an existing index from a table.
func (m *Migration) DropIndex(ctx context.Context, table, index string) error {
sql := fmt.Sprintf("DROP INDEX IF EXISTS %s", database.QuoteIdentifier(index))
return m.ExecuteSQL(ctx, sql)
}
// HasIndex checks if an index exists on a table.
func (m *Migration) HasIndex(ctx context.Context, table, index string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name='%s' AND tbl_name='%s'",
index,
table,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateForeignKey creates a foreign key constraint.
// Note: SQLite requires foreign keys to be enabled with PRAGMA foreign_keys = ON
func (m *Migration) CreateForeignKey(ctx context.Context, table, constraint string, columns []string, refTable string, refColumns []string, options ...database.ForeignKeyOption) error {
// SQLite doesn't support adding foreign keys to existing tables
// Foreign keys must be defined during table creation
return fmt.Errorf("SQLite does not support adding foreign keys to existing tables")
}
// DropForeignKey drops a foreign key constraint.
func (m *Migration) DropForeignKey(ctx context.Context, table, constraint string) error {
// SQLite doesn't support dropping foreign keys from existing tables
return fmt.Errorf("SQLite does not support dropping foreign keys from existing tables")
}
// HasForeignKey checks if a foreign key constraint exists.
func (m *Migration) HasForeignKey(ctx context.Context, table, constraint string) (bool, error) {
// Query pragma to check foreign keys
query := fmt.Sprintf("PRAGMA foreign_key_list(%s)", database.QuoteIdentifier(table))
result, err := m.GetDB().GetAll(ctx, query)
if err != nil {
return false, err
}
// Check if constraint exists in the result
for _, row := range result {
if id, ok := row["id"]; ok && id.String() == constraint {
return true, nil
}
}
return false, nil
}
// CreateSchema is not applicable for SQLite (single database file).
func (m *Migration) CreateSchema(ctx context.Context, schema string) error {
// SQLite doesn't support schemas
return fmt.Errorf("SQLite does not support schemas")
}
// DropSchema is not applicable for SQLite (single database file).
func (m *Migration) DropSchema(ctx context.Context, schema string, cascade ...bool) error {
// SQLite doesn't support schemas
return fmt.Errorf("SQLite does not support schemas")
}
// HasSchema checks if a schema exists (always returns false for SQLite).
func (m *Migration) HasSchema(ctx context.Context, schema string) (bool, error) {
// SQLite doesn't support schemas
return false, nil
}
// formatDefaultValue formats the default value for SQL.
func formatDefaultValue(value any) string {
switch v := value.(type) {
case string:
return fmt.Sprintf("'%s'", escapeString(v))
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return fmt.Sprintf("%d", v)
case float32, float64:
return fmt.Sprintf("%f", v)
case bool:
if v {
return "1"
}
return "0"
case nil:
return "NULL"
default:
return fmt.Sprintf("'%v'", v)
}
}
// escapeString escapes special characters in strings for SQL.
func escapeString(s string) string {
s = strings.ReplaceAll(s, "'", "''")
return s
}