fix(go.sum): update ResolveSpec dependency to v1.0.87
This commit is contained in:
+1
@@ -0,0 +1 @@
|
||||
.idea
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-NOW Jinzhu <wosmvp@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# GORM PostgreSQL Driver
|
||||
|
||||
## Quick Start
|
||||
|
||||
```go
|
||||
import (
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// https://github.com/jackc/pgx
|
||||
dsn := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai"
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```go
|
||||
import (
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{
|
||||
DSN: "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai", // data source name, refer https://github.com/jackc/pgx
|
||||
PreferSimpleProtocol: true, // disables implicit prepared statement usage. By default pgx automatically uses the extended protocol
|
||||
}), &gorm.Config{})
|
||||
```
|
||||
|
||||
|
||||
Checkout [https://gorm.io](https://gorm.io) for details.
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
// The error codes to map PostgreSQL errors to gorm errors, here is the PostgreSQL error codes reference https://www.postgresql.org/docs/current/errcodes-appendix.html.
|
||||
var errCodes = map[string]error{
|
||||
"23505": gorm.ErrDuplicatedKey,
|
||||
"23503": gorm.ErrForeignKeyViolated,
|
||||
"42703": gorm.ErrInvalidField,
|
||||
"23514": gorm.ErrCheckConstraintViolated,
|
||||
}
|
||||
|
||||
type ErrMessage struct {
|
||||
Code string
|
||||
Severity string
|
||||
Message string
|
||||
}
|
||||
|
||||
// Translate it will translate the error to native gorm errors.
|
||||
// Since currently gorm supporting both pgx and pg drivers, only checking for pgx PgError types is not enough for translating errors, so we have additional error json marshal fallback.
|
||||
func (dialector Dialector) Translate(err error) error {
|
||||
if pgErr, ok := err.(*pgconn.PgError); ok {
|
||||
if translatedErr, found := errCodes[pgErr.Code]; found {
|
||||
return translatedErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
parsedErr, marshalErr := json.Marshal(err)
|
||||
if marshalErr != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errMsg ErrMessage
|
||||
unmarshalErr := json.Unmarshal(parsedErr, &errMsg)
|
||||
if unmarshalErr != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if translatedErr, found := errCodes[errMsg.Code]; found {
|
||||
return translatedErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
+821
@@ -0,0 +1,821 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/migrator"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
// See https://stackoverflow.com/questions/2204058/list-columns-with-indexes-in-postgresql
|
||||
// Here are some changes:
|
||||
// - use `LEFT JOIN` instead of `CROSS JOIN`
|
||||
// - exclude indexes used to support constraints (they are auto-generated)
|
||||
const indexSql = `
|
||||
SELECT
|
||||
ct.relname AS table_name,
|
||||
ci.relname AS index_name,
|
||||
i.indisunique AS non_unique,
|
||||
i.indisprimary AS primary,
|
||||
a.attname AS column_name
|
||||
FROM
|
||||
pg_index i
|
||||
LEFT JOIN pg_class ct ON ct.oid = i.indrelid
|
||||
LEFT JOIN pg_class ci ON ci.oid = i.indexrelid
|
||||
LEFT JOIN pg_attribute a ON a.attrelid = ct.oid
|
||||
LEFT JOIN pg_constraint con ON con.conindid = i.indexrelid
|
||||
WHERE
|
||||
a.attnum = ANY(i.indkey)
|
||||
AND con.oid IS NULL
|
||||
AND ct.relkind = 'r'
|
||||
AND ct.relname = ?
|
||||
`
|
||||
|
||||
var typeAliasMap = map[string][]string{
|
||||
"int": {"integer"},
|
||||
"int2": {"smallint"},
|
||||
"int4": {"integer"},
|
||||
"int8": {"bigint"},
|
||||
"smallint": {"int2"},
|
||||
"integer": {"int4"},
|
||||
"bigint": {"int8"},
|
||||
"date": {"date"},
|
||||
"decimal": {"numeric"},
|
||||
"numeric": {"decimal"},
|
||||
"timestamp": {"timestamp"},
|
||||
"timestamptz": {"timestamp with time zone"},
|
||||
"timestamp without time zone": {"timestamp"},
|
||||
"timestamp with time zone": {"timestamptz"},
|
||||
"bool": {"boolean"},
|
||||
"boolean": {"bool"},
|
||||
"serial2": {"smallserial"},
|
||||
"serial4": {"serial"},
|
||||
"serial8": {"bigserial"},
|
||||
"varbit": {"bit varying"},
|
||||
"char": {"character"},
|
||||
"varchar": {"character varying"},
|
||||
"float4": {"real"},
|
||||
"float8": {"double precision"},
|
||||
"time": {"time"},
|
||||
"timetz": {"time with time zone"},
|
||||
"time without time zone": {"time"},
|
||||
"time with time zone": {"timetz"},
|
||||
}
|
||||
|
||||
type Migrator struct {
|
||||
migrator.Migrator
|
||||
}
|
||||
|
||||
// select querys ignore dryrun
|
||||
func (m Migrator) queryRaw(sql string, values ...interface{}) (tx *gorm.DB) {
|
||||
queryTx := m.DB
|
||||
if m.DB.DryRun {
|
||||
queryTx = m.DB.Session(&gorm.Session{})
|
||||
queryTx.DryRun = false
|
||||
}
|
||||
return queryTx.Raw(sql, values...)
|
||||
}
|
||||
|
||||
func (m Migrator) CurrentDatabase() (name string) {
|
||||
m.queryRaw("SELECT CURRENT_DATABASE()").Scan(&name)
|
||||
return
|
||||
}
|
||||
|
||||
func (m Migrator) BuildIndexOptions(opts []schema.IndexOption, stmt *gorm.Statement) (results []interface{}) {
|
||||
for _, opt := range opts {
|
||||
str := stmt.Quote(opt.DBName)
|
||||
if opt.Expression != "" {
|
||||
str = opt.Expression
|
||||
}
|
||||
|
||||
if opt.Collate != "" {
|
||||
str += " COLLATE " + opt.Collate
|
||||
}
|
||||
|
||||
if opt.Sort != "" {
|
||||
str += " " + opt.Sort
|
||||
}
|
||||
results = append(results, clause.Expr{SQL: str})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m Migrator) HasIndex(value interface{}, name string) bool {
|
||||
var count int64
|
||||
m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
if idx := stmt.Schema.LookIndex(name); idx != nil {
|
||||
name = idx.Name
|
||||
}
|
||||
}
|
||||
currentSchema, curTable := m.CurrentSchema(stmt, stmt.Table)
|
||||
return m.queryRaw(
|
||||
"SELECT count(*) FROM pg_indexes WHERE tablename = ? AND indexname = ? AND schemaname = ?", curTable, name, currentSchema,
|
||||
).Scan(&count).Error
|
||||
})
|
||||
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (m Migrator) CreateIndex(value interface{}, name string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
if idx := stmt.Schema.LookIndex(name); idx != nil {
|
||||
opts := m.BuildIndexOptions(idx.Fields, stmt)
|
||||
values := []interface{}{clause.Column{Name: idx.Name}, m.CurrentTable(stmt), opts}
|
||||
|
||||
createIndexSQL := "CREATE "
|
||||
if idx.Class != "" {
|
||||
createIndexSQL += idx.Class + " "
|
||||
}
|
||||
createIndexSQL += "INDEX "
|
||||
|
||||
hasConcurrentOption := strings.TrimSpace(strings.ToUpper(idx.Option)) == "CONCURRENTLY"
|
||||
if hasConcurrentOption {
|
||||
createIndexSQL += "CONCURRENTLY "
|
||||
}
|
||||
|
||||
createIndexSQL += "IF NOT EXISTS ? ON ?"
|
||||
|
||||
if idx.Type != "" {
|
||||
createIndexSQL += " USING " + idx.Type + "(?)"
|
||||
} else {
|
||||
createIndexSQL += " ?"
|
||||
}
|
||||
|
||||
if idx.Option != "" && !hasConcurrentOption {
|
||||
createIndexSQL += " " + idx.Option
|
||||
}
|
||||
|
||||
if idx.Where != "" {
|
||||
createIndexSQL += " WHERE " + idx.Where
|
||||
}
|
||||
|
||||
return m.DB.Exec(createIndexSQL, values...).Error
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to create index with name %v", name)
|
||||
})
|
||||
}
|
||||
|
||||
func (m Migrator) RenameIndex(value interface{}, oldName, newName string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
return m.DB.Exec(
|
||||
"ALTER INDEX ? RENAME TO ?",
|
||||
clause.Column{Name: oldName}, clause.Column{Name: newName},
|
||||
).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (m Migrator) DropIndex(value interface{}, name string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
if idx := stmt.Schema.LookIndex(name); idx != nil {
|
||||
name = idx.Name
|
||||
}
|
||||
}
|
||||
|
||||
return m.DB.Exec("DROP INDEX ?", clause.Column{Name: name}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (m Migrator) GetTables() (tableList []string, err error) {
|
||||
currentSchema, _ := m.CurrentSchema(m.DB.Statement, "")
|
||||
return tableList, m.queryRaw("SELECT table_name FROM information_schema.tables WHERE table_schema = ? AND table_type = ?", currentSchema, "BASE TABLE").Scan(&tableList).Error
|
||||
}
|
||||
|
||||
func (m Migrator) CreateTable(values ...interface{}) (err error) {
|
||||
if err = m.Migrator.CreateTable(values...); err != nil {
|
||||
return
|
||||
}
|
||||
for _, value := range m.ReorderModels(values, false) {
|
||||
if err = m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
for _, fieldName := range stmt.Schema.DBNames {
|
||||
field := stmt.Schema.FieldsByDBName[fieldName]
|
||||
if field.Comment != "" {
|
||||
if err := m.DB.Exec(
|
||||
"COMMENT ON COLUMN ?.? IS ?",
|
||||
m.CurrentTable(stmt), clause.Column{Name: field.DBName}, gorm.Expr(m.Migrator.Dialector.Explain("$1", field.Comment)),
|
||||
).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m Migrator) HasTable(value interface{}) bool {
|
||||
var count int64
|
||||
m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
currentSchema, curTable := m.CurrentSchema(stmt, stmt.Table)
|
||||
return m.queryRaw("SELECT count(*) FROM information_schema.tables WHERE table_schema = ? AND table_name = ? AND table_type = ?", currentSchema, curTable, "BASE TABLE").Scan(&count).Error
|
||||
})
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (m Migrator) DropTable(values ...interface{}) error {
|
||||
values = m.ReorderModels(values, false)
|
||||
tx := m.DB.Session(&gorm.Session{})
|
||||
for i := len(values) - 1; i >= 0; i-- {
|
||||
if err := m.RunWithValue(values[i], func(stmt *gorm.Statement) error {
|
||||
return tx.Exec("DROP TABLE IF EXISTS ? CASCADE", m.CurrentTable(stmt)).Error
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Migrator) AddColumn(value interface{}, field string) error {
|
||||
if err := m.Migrator.AddColumn(value, field); err != nil {
|
||||
return err
|
||||
}
|
||||
m.resetPreparedStmts()
|
||||
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
if field := stmt.Schema.LookUpField(field); field != nil {
|
||||
if field.Comment != "" {
|
||||
if err := m.DB.Exec(
|
||||
"COMMENT ON COLUMN ?.? IS ?",
|
||||
m.CurrentTable(stmt), clause.Column{Name: field.DBName}, gorm.Expr(m.Migrator.Dialector.Explain("$1", field.Comment)),
|
||||
).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (m Migrator) HasColumn(value interface{}, field string) bool {
|
||||
var count int64
|
||||
m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
name := field
|
||||
if stmt.Schema != nil {
|
||||
if field := stmt.Schema.LookUpField(field); field != nil {
|
||||
name = field.DBName
|
||||
}
|
||||
}
|
||||
|
||||
currentSchema, curTable := m.CurrentSchema(stmt, stmt.Table)
|
||||
return m.queryRaw(
|
||||
"SELECT count(*) FROM INFORMATION_SCHEMA.columns WHERE table_schema = ? AND table_name = ? AND column_name = ?",
|
||||
currentSchema, curTable, name,
|
||||
).Scan(&count).Error
|
||||
})
|
||||
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (m Migrator) MigrateColumn(value interface{}, field *schema.Field, columnType gorm.ColumnType) error {
|
||||
// skip primary field
|
||||
if !field.PrimaryKey {
|
||||
if err := m.Migrator.MigrateColumn(value, field, columnType); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
var description string
|
||||
currentSchema, curTable := m.CurrentSchema(stmt, stmt.Table)
|
||||
values := []interface{}{currentSchema, curTable, field.DBName, stmt.Table, currentSchema}
|
||||
checkSQL := "SELECT description FROM pg_catalog.pg_description "
|
||||
checkSQL += "WHERE objsubid = (SELECT ordinal_position FROM information_schema.columns WHERE table_schema = ? AND table_name = ? AND column_name = ?) "
|
||||
checkSQL += "AND objoid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = ? AND relnamespace = "
|
||||
checkSQL += "(SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = ?))"
|
||||
m.queryRaw(checkSQL, values...).Scan(&description)
|
||||
|
||||
comment := strings.Trim(field.Comment, "'")
|
||||
comment = strings.Trim(comment, `"`)
|
||||
if field.Comment != "" && comment != description {
|
||||
if err := m.DB.Exec(
|
||||
"COMMENT ON COLUMN ?.? IS ?",
|
||||
m.CurrentTable(stmt), clause.Column{Name: field.DBName}, gorm.Expr(m.Migrator.Dialector.Explain("$1", field.Comment)),
|
||||
).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// AlterColumn alter value's `field` column' type based on schema definition
|
||||
func (m Migrator) AlterColumn(value interface{}, field string) error {
|
||||
err := m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
if field := stmt.Schema.LookUpField(field); field != nil {
|
||||
var (
|
||||
columnTypes, _ = m.DB.Migrator().ColumnTypes(value)
|
||||
fieldColumnType *migrator.ColumnType
|
||||
)
|
||||
for _, columnType := range columnTypes {
|
||||
if columnType.Name() == field.DBName {
|
||||
fieldColumnType, _ = columnType.(*migrator.ColumnType)
|
||||
}
|
||||
}
|
||||
|
||||
fileType := clause.Expr{SQL: m.DataTypeOf(field)}
|
||||
// check for typeName and SQL name
|
||||
isSameType := true
|
||||
if !strings.EqualFold(fieldColumnType.DatabaseTypeName(), fileType.SQL) {
|
||||
isSameType = false
|
||||
// if different, also check for aliases
|
||||
aliases := m.GetTypeAliases(fieldColumnType.DatabaseTypeName())
|
||||
for _, alias := range aliases {
|
||||
if strings.HasPrefix(fileType.SQL, alias) {
|
||||
isSameType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// not same, migrate
|
||||
if !isSameType {
|
||||
filedColumnAutoIncrement, _ := fieldColumnType.AutoIncrement()
|
||||
if field.AutoIncrement && filedColumnAutoIncrement { // update
|
||||
serialDatabaseType, _ := getSerialDatabaseType(fileType.SQL)
|
||||
if t, _ := fieldColumnType.ColumnType(); t != serialDatabaseType {
|
||||
if err := m.UpdateSequence(m.DB, stmt, field, serialDatabaseType); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if field.AutoIncrement && !filedColumnAutoIncrement { // create
|
||||
serialDatabaseType, _ := getSerialDatabaseType(fileType.SQL)
|
||||
if err := m.CreateSequence(m.DB, stmt, field, serialDatabaseType); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !field.AutoIncrement && filedColumnAutoIncrement { // delete
|
||||
if err := m.DeleteSequence(m.DB, stmt, field, fileType); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := m.modifyColumn(stmt, field, fileType, fieldColumnType); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if null, _ := fieldColumnType.Nullable(); null == field.NotNull {
|
||||
if field.NotNull {
|
||||
if err := m.DB.Exec("ALTER TABLE ? ALTER COLUMN ? SET NOT NULL", m.CurrentTable(stmt), clause.Column{Name: field.DBName}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := m.DB.Exec("ALTER TABLE ? ALTER COLUMN ? DROP NOT NULL", m.CurrentTable(stmt), clause.Column{Name: field.DBName}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if v, ok := fieldColumnType.DefaultValue(); (field.DefaultValueInterface == nil && ok) || v != field.DefaultValue {
|
||||
if field.HasDefaultValue && (field.DefaultValueInterface != nil || field.DefaultValue != "") {
|
||||
if field.DefaultValueInterface != nil {
|
||||
defaultStmt := &gorm.Statement{Vars: []interface{}{field.DefaultValueInterface}}
|
||||
m.Dialector.BindVarTo(defaultStmt, defaultStmt, field.DefaultValueInterface)
|
||||
if err := m.DB.Exec("ALTER TABLE ? ALTER COLUMN ? SET DEFAULT ?", m.CurrentTable(stmt), clause.Column{Name: field.DBName}, clause.Expr{SQL: m.Dialector.Explain(defaultStmt.SQL.String(), field.DefaultValueInterface)}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if field.DefaultValue != "(-)" {
|
||||
if err := m.DB.Exec("ALTER TABLE ? ALTER COLUMN ? SET DEFAULT ?", m.CurrentTable(stmt), clause.Column{Name: field.DBName}, clause.Expr{SQL: field.DefaultValue}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := m.DB.Exec("ALTER TABLE ? ALTER COLUMN ? DROP DEFAULT", m.CurrentTable(stmt), clause.Column{Name: field.DBName}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if !field.HasDefaultValue {
|
||||
// case - as-is column has default value and to-be column has no default value
|
||||
// need to drop default
|
||||
if err := m.DB.Exec("ALTER TABLE ? ALTER COLUMN ? DROP DEFAULT", m.CurrentTable(stmt), clause.Column{Name: field.DBName}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to look up field with name: %s", field)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.resetPreparedStmts()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Migrator) modifyColumn(stmt *gorm.Statement, field *schema.Field, targetType clause.Expr, existingColumn *migrator.ColumnType) error {
|
||||
alterSQL := "ALTER TABLE ? ALTER COLUMN ? TYPE ? USING ?::?"
|
||||
isUncastableDefaultValue := false
|
||||
|
||||
if targetType.SQL == "boolean" {
|
||||
switch existingColumn.DatabaseTypeName() {
|
||||
case "int2", "int8", "numeric":
|
||||
alterSQL = "ALTER TABLE ? ALTER COLUMN ? TYPE ? USING ?::int::?"
|
||||
}
|
||||
isUncastableDefaultValue = true
|
||||
}
|
||||
|
||||
if dv, _ := existingColumn.DefaultValue(); dv != "" && isUncastableDefaultValue {
|
||||
if err := m.DB.Exec("ALTER TABLE ? ALTER COLUMN ? DROP DEFAULT", m.CurrentTable(stmt), clause.Column{Name: field.DBName}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := m.DB.Exec(alterSQL, m.CurrentTable(stmt), clause.Column{Name: field.DBName}, targetType, clause.Column{Name: field.DBName}, targetType).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Migrator) HasConstraint(value interface{}, name string) bool {
|
||||
var count int64
|
||||
m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
|
||||
if constraint != nil {
|
||||
name = constraint.GetName()
|
||||
}
|
||||
currentSchema, curTable := m.CurrentSchema(stmt, table)
|
||||
|
||||
return m.queryRaw(
|
||||
"SELECT count(*) FROM INFORMATION_SCHEMA.table_constraints WHERE table_schema = ? AND table_name = ? AND constraint_name = ?",
|
||||
currentSchema, curTable, name,
|
||||
).Scan(&count).Error
|
||||
})
|
||||
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (m Migrator) ColumnTypes(value interface{}) (columnTypes []gorm.ColumnType, err error) {
|
||||
columnTypes = make([]gorm.ColumnType, 0)
|
||||
err = m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
var (
|
||||
currentDatabase = m.DB.Migrator().CurrentDatabase()
|
||||
currentSchema, table = m.CurrentSchema(stmt, stmt.Table)
|
||||
columns, err = m.queryRaw(
|
||||
"SELECT c.column_name, c.is_nullable = 'YES', c.udt_name, c.character_maximum_length, c.numeric_precision, c.numeric_precision_radix, c.numeric_scale, c.datetime_precision, 8 * typlen, c.column_default, pd.description, c.identity_increment FROM information_schema.columns AS c JOIN pg_type AS pgt ON c.udt_name = pgt.typname LEFT JOIN pg_catalog.pg_description as pd ON pd.objsubid = c.ordinal_position AND pd.objoid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = c.table_name AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = c.table_schema)) where table_catalog = ? AND table_schema = ? AND table_name = ?",
|
||||
currentDatabase, currentSchema, table).Rows()
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for columns.Next() {
|
||||
var (
|
||||
column = &migrator.ColumnType{
|
||||
PrimaryKeyValue: sql.NullBool{Valid: true},
|
||||
UniqueValue: sql.NullBool{Valid: true},
|
||||
}
|
||||
datetimePrecision sql.NullInt64
|
||||
radixValue sql.NullInt64
|
||||
typeLenValue sql.NullInt64
|
||||
identityIncrement sql.NullString
|
||||
)
|
||||
|
||||
err = columns.Scan(
|
||||
&column.NameValue, &column.NullableValue, &column.DataTypeValue, &column.LengthValue, &column.DecimalSizeValue,
|
||||
&radixValue, &column.ScaleValue, &datetimePrecision, &typeLenValue, &column.DefaultValueValue, &column.CommentValue, &identityIncrement,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if typeLenValue.Valid && typeLenValue.Int64 > 0 {
|
||||
column.LengthValue = typeLenValue
|
||||
}
|
||||
|
||||
autoIncrementValuePattern := regexp.MustCompile(`^nextval\('"?[^']+seq"?'::regclass\)$`)
|
||||
if autoIncrementValuePattern.MatchString(column.DefaultValueValue.String) || (identityIncrement.Valid && identityIncrement.String != "") {
|
||||
column.AutoIncrementValue = sql.NullBool{Bool: true, Valid: true}
|
||||
column.DefaultValueValue = sql.NullString{}
|
||||
}
|
||||
|
||||
if column.DefaultValueValue.Valid {
|
||||
column.DefaultValueValue.String = parseDefaultValueValue(column.DefaultValueValue.String)
|
||||
}
|
||||
|
||||
if datetimePrecision.Valid {
|
||||
column.DecimalSizeValue = datetimePrecision
|
||||
}
|
||||
|
||||
columnTypes = append(columnTypes, column)
|
||||
}
|
||||
columns.Close()
|
||||
|
||||
// assign sql column type
|
||||
{
|
||||
rows, rowsErr := m.GetRows(currentSchema, table)
|
||||
if rowsErr != nil {
|
||||
return rowsErr
|
||||
}
|
||||
rawColumnTypes, err := rows.ColumnTypes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, columnType := range columnTypes {
|
||||
for _, c := range rawColumnTypes {
|
||||
if c.Name() == columnType.Name() {
|
||||
columnType.(*migrator.ColumnType).SQLColumnType = c
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
|
||||
// check primary, unique field
|
||||
{
|
||||
columnTypeRows, err := m.queryRaw("SELECT constraint_name FROM information_schema.table_constraints tc JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_catalog, table_name, constraint_name) JOIN information_schema.columns AS c ON c.table_schema = tc.constraint_schema AND tc.table_name = c.table_name AND ccu.column_name = c.column_name WHERE constraint_type IN ('PRIMARY KEY', 'UNIQUE') AND c.table_catalog = ? AND c.table_schema = ? AND c.table_name = ? AND constraint_type = ?", currentDatabase, currentSchema, table, "UNIQUE").Rows()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uniqueContraints := map[string]int{}
|
||||
for columnTypeRows.Next() {
|
||||
var constraintName string
|
||||
columnTypeRows.Scan(&constraintName)
|
||||
uniqueContraints[constraintName]++
|
||||
}
|
||||
columnTypeRows.Close()
|
||||
|
||||
columnTypeRows, err = m.queryRaw("SELECT c.column_name, constraint_name, constraint_type FROM information_schema.table_constraints tc JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_catalog, table_name, constraint_name) JOIN information_schema.columns AS c ON c.table_schema = tc.constraint_schema AND tc.table_name = c.table_name AND ccu.column_name = c.column_name WHERE constraint_type IN ('PRIMARY KEY', 'UNIQUE') AND c.table_catalog = ? AND c.table_schema = ? AND c.table_name = ?", currentDatabase, currentSchema, table).Rows()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for columnTypeRows.Next() {
|
||||
var name, constraintName, columnType string
|
||||
columnTypeRows.Scan(&name, &constraintName, &columnType)
|
||||
for _, c := range columnTypes {
|
||||
mc := c.(*migrator.ColumnType)
|
||||
if mc.NameValue.String == name {
|
||||
switch columnType {
|
||||
case "PRIMARY KEY":
|
||||
mc.PrimaryKeyValue = sql.NullBool{Bool: true, Valid: true}
|
||||
case "UNIQUE":
|
||||
if uniqueContraints[constraintName] == 1 {
|
||||
mc.UniqueValue = sql.NullBool{Bool: true, Valid: true}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
columnTypeRows.Close()
|
||||
}
|
||||
|
||||
// check column type
|
||||
{
|
||||
dataTypeRows, err := m.queryRaw(`SELECT a.attname as column_name, format_type(a.atttypid, a.atttypmod) AS data_type
|
||||
FROM pg_attribute a JOIN pg_class b ON a.attrelid = b.oid AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = ?)
|
||||
WHERE a.attnum > 0 -- hide internal columns
|
||||
AND NOT a.attisdropped -- hide deleted columns
|
||||
AND b.relname = ?`, currentSchema, table).Rows()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for dataTypeRows.Next() {
|
||||
var name, dataType string
|
||||
dataTypeRows.Scan(&name, &dataType)
|
||||
for _, c := range columnTypes {
|
||||
mc := c.(*migrator.ColumnType)
|
||||
if mc.NameValue.String == name {
|
||||
mc.ColumnTypeValue = sql.NullString{String: dataType, Valid: true}
|
||||
// Handle array type: _text -> text[] , _int4 -> integer[]
|
||||
// Not support array size limits and array size limits because:
|
||||
// https://www.postgresql.org/docs/current/arrays.html#ARRAYS-DECLARATION
|
||||
if strings.HasPrefix(mc.DataTypeValue.String, "_") {
|
||||
mc.DataTypeValue = sql.NullString{String: dataType, Valid: true}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
dataTypeRows.Close()
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (m Migrator) GetRows(currentSchema interface{}, table interface{}) (*sql.Rows, error) {
|
||||
name := table.(string)
|
||||
if _, ok := currentSchema.(string); ok {
|
||||
name = fmt.Sprintf("%v.%v", currentSchema, table)
|
||||
}
|
||||
|
||||
return m.DB.Session(&gorm.Session{}).Table(name).Limit(1).Scopes(func(d *gorm.DB) *gorm.DB {
|
||||
dialector, _ := m.Dialector.(Dialector)
|
||||
// use simple protocol
|
||||
if !m.DB.PrepareStmt && (dialector.Config != nil && (dialector.Config.DriverName == "" || dialector.Config.DriverName == "pgx")) {
|
||||
d.Statement.Vars = append([]interface{}{pgx.QueryExecModeSimpleProtocol}, d.Statement.Vars...)
|
||||
}
|
||||
return d
|
||||
}).Rows()
|
||||
}
|
||||
|
||||
func (m Migrator) CurrentSchema(stmt *gorm.Statement, table string) (interface{}, interface{}) {
|
||||
if strings.Contains(table, ".") {
|
||||
if tables := strings.Split(table, `.`); len(tables) == 2 {
|
||||
return tables[0], tables[1]
|
||||
}
|
||||
}
|
||||
|
||||
if stmt.TableExpr != nil {
|
||||
if tables := strings.Split(stmt.TableExpr.SQL, `"."`); len(tables) == 2 {
|
||||
return strings.TrimPrefix(tables[0], `"`), table
|
||||
}
|
||||
}
|
||||
return clause.Expr{SQL: "CURRENT_SCHEMA()"}, table
|
||||
}
|
||||
|
||||
func (m Migrator) CreateSequence(tx *gorm.DB, stmt *gorm.Statement, field *schema.Field,
|
||||
serialDatabaseType string) (err error) {
|
||||
|
||||
_, table := m.CurrentSchema(stmt, stmt.Table)
|
||||
tableName := table.(string)
|
||||
|
||||
sequenceName := strings.Join([]string{tableName, field.DBName, "seq"}, "_")
|
||||
if err = tx.Exec(`CREATE SEQUENCE IF NOT EXISTS ? AS ?`, clause.Expr{SQL: sequenceName},
|
||||
clause.Expr{SQL: serialDatabaseType}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Exec("ALTER TABLE ? ALTER COLUMN ? SET DEFAULT nextval('?')",
|
||||
clause.Expr{SQL: tableName}, clause.Expr{SQL: field.DBName}, clause.Expr{SQL: sequenceName}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Exec("ALTER SEQUENCE ? OWNED BY ?.?",
|
||||
clause.Expr{SQL: sequenceName}, clause.Expr{SQL: tableName}, clause.Expr{SQL: field.DBName}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m Migrator) UpdateSequence(tx *gorm.DB, stmt *gorm.Statement, field *schema.Field,
|
||||
serialDatabaseType string) (err error) {
|
||||
|
||||
sequenceName, err := m.getColumnSequenceName(tx, stmt, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tx.Exec(`ALTER SEQUENCE IF EXISTS ? AS ?`, clause.Expr{SQL: sequenceName}, clause.Expr{SQL: serialDatabaseType}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Exec("ALTER TABLE ? ALTER COLUMN ? TYPE ?",
|
||||
m.CurrentTable(stmt), clause.Expr{SQL: field.DBName}, clause.Expr{SQL: serialDatabaseType}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m Migrator) DeleteSequence(tx *gorm.DB, stmt *gorm.Statement, field *schema.Field,
|
||||
fileType clause.Expr) (err error) {
|
||||
|
||||
sequenceName, err := m.getColumnSequenceName(tx, stmt, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Exec("ALTER TABLE ? ALTER COLUMN ? TYPE ?", m.CurrentTable(stmt), clause.Column{Name: field.DBName}, fileType).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Exec("ALTER TABLE ? ALTER COLUMN ? DROP DEFAULT",
|
||||
m.CurrentTable(stmt), clause.Expr{SQL: field.DBName}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tx.Exec(`DROP SEQUENCE IF EXISTS ?`, clause.Expr{SQL: sequenceName}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (m Migrator) getColumnSequenceName(tx *gorm.DB, stmt *gorm.Statement, field *schema.Field) (
|
||||
sequenceName string, err error) {
|
||||
_, table := m.CurrentSchema(stmt, stmt.Table)
|
||||
|
||||
// DefaultValueValue is reset by ColumnTypes, search again.
|
||||
var columnDefault string
|
||||
err = tx.Raw(
|
||||
`SELECT column_default FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,
|
||||
table, field.DBName).Scan(&columnDefault).Error
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sequenceName = strings.TrimSuffix(
|
||||
strings.TrimPrefix(columnDefault, `nextval('`),
|
||||
`'::regclass)`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (m Migrator) GetIndexes(value interface{}) ([]gorm.Index, error) {
|
||||
indexes := make([]gorm.Index, 0)
|
||||
|
||||
err := m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
result := make([]*Index, 0)
|
||||
scanErr := m.queryRaw(indexSql, stmt.Table).Scan(&result).Error
|
||||
if scanErr != nil {
|
||||
return scanErr
|
||||
}
|
||||
indexMap := groupByIndexName(result)
|
||||
for _, idx := range indexMap {
|
||||
tempIdx := &migrator.Index{
|
||||
TableName: idx[0].TableName,
|
||||
NameValue: idx[0].IndexName,
|
||||
PrimaryKeyValue: sql.NullBool{
|
||||
Bool: idx[0].Primary,
|
||||
Valid: true,
|
||||
},
|
||||
UniqueValue: sql.NullBool{
|
||||
Bool: idx[0].NonUnique,
|
||||
Valid: true,
|
||||
},
|
||||
}
|
||||
for _, x := range idx {
|
||||
tempIdx.ColumnList = append(tempIdx.ColumnList, x.ColumnName)
|
||||
}
|
||||
indexes = append(indexes, tempIdx)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return indexes, err
|
||||
}
|
||||
|
||||
// Index table index info
|
||||
type Index struct {
|
||||
TableName string `gorm:"column:table_name"`
|
||||
ColumnName string `gorm:"column:column_name"`
|
||||
IndexName string `gorm:"column:index_name"`
|
||||
NonUnique bool `gorm:"column:non_unique"`
|
||||
Primary bool `gorm:"column:primary"`
|
||||
}
|
||||
|
||||
func groupByIndexName(indexList []*Index) map[string][]*Index {
|
||||
columnIndexMap := make(map[string][]*Index, len(indexList))
|
||||
for _, idx := range indexList {
|
||||
columnIndexMap[idx.IndexName] = append(columnIndexMap[idx.IndexName], idx)
|
||||
}
|
||||
return columnIndexMap
|
||||
}
|
||||
|
||||
func (m Migrator) GetTypeAliases(databaseTypeName string) []string {
|
||||
return typeAliasMap[databaseTypeName]
|
||||
}
|
||||
|
||||
// should reset prepared stmts when table changed
|
||||
func (m Migrator) resetPreparedStmts() {
|
||||
if m.DB.PrepareStmt {
|
||||
if pdb, ok := m.DB.ConnPool.(*gorm.PreparedStmtDB); ok {
|
||||
pdb.Reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Migrator) DropColumn(dst interface{}, field string) error {
|
||||
if err := m.Migrator.DropColumn(dst, field); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.resetPreparedStmts()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Migrator) RenameColumn(dst interface{}, oldName, field string) error {
|
||||
if err := m.Migrator.RenameColumn(dst, oldName, field); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.resetPreparedStmts()
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseDefaultValueValue(defaultValue string) string {
|
||||
value := regexp.MustCompile(`^(.*?)(?:::.*)?$`).ReplaceAllString(defaultValue, "$1")
|
||||
return strings.Trim(value, "'")
|
||||
}
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/callbacks"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/migrator"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
type Dialector struct {
|
||||
*Config
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
DriverName string
|
||||
DSN string
|
||||
WithoutQuotingCheck bool
|
||||
PreferSimpleProtocol bool
|
||||
WithoutReturning bool
|
||||
Conn gorm.ConnPool
|
||||
}
|
||||
|
||||
var (
|
||||
timeZoneMatcher = regexp.MustCompile("(time_zone|TimeZone|timezone)=(.*?)($|&| )")
|
||||
defaultIdentifierLength = 63 //maximum identifier length for postgres
|
||||
)
|
||||
|
||||
func Open(dsn string) gorm.Dialector {
|
||||
return &Dialector{&Config{DSN: dsn}}
|
||||
}
|
||||
|
||||
func New(config Config) gorm.Dialector {
|
||||
return &Dialector{Config: &config}
|
||||
}
|
||||
|
||||
func (dialector Dialector) Name() string {
|
||||
return "postgres"
|
||||
}
|
||||
|
||||
func (dialector Dialector) Apply(config *gorm.Config) error {
|
||||
if config.NamingStrategy == nil {
|
||||
config.NamingStrategy = schema.NamingStrategy{
|
||||
IdentifierMaxLength: defaultIdentifierLength,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v := config.NamingStrategy.(type) {
|
||||
case *schema.NamingStrategy:
|
||||
if v.IdentifierMaxLength <= 0 {
|
||||
v.IdentifierMaxLength = defaultIdentifierLength
|
||||
}
|
||||
case schema.NamingStrategy:
|
||||
if v.IdentifierMaxLength <= 0 {
|
||||
v.IdentifierMaxLength = defaultIdentifierLength
|
||||
config.NamingStrategy = v
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dialector Dialector) Initialize(db *gorm.DB) (err error) {
|
||||
callbackConfig := &callbacks.Config{
|
||||
CreateClauses: []string{"INSERT", "VALUES", "ON CONFLICT"},
|
||||
UpdateClauses: []string{"UPDATE", "SET", "FROM", "WHERE"},
|
||||
DeleteClauses: []string{"DELETE", "FROM", "WHERE"},
|
||||
}
|
||||
// register callbacks
|
||||
if !dialector.WithoutReturning {
|
||||
callbackConfig.CreateClauses = append(callbackConfig.CreateClauses, "RETURNING")
|
||||
callbackConfig.UpdateClauses = append(callbackConfig.UpdateClauses, "RETURNING")
|
||||
callbackConfig.DeleteClauses = append(callbackConfig.DeleteClauses, "RETURNING")
|
||||
}
|
||||
callbacks.RegisterDefaultCallbacks(db, callbackConfig)
|
||||
|
||||
if dialector.Conn != nil {
|
||||
db.ConnPool = dialector.Conn
|
||||
} else if dialector.DriverName != "" {
|
||||
db.ConnPool, err = sql.Open(dialector.DriverName, dialector.Config.DSN)
|
||||
} else {
|
||||
var config *pgx.ConnConfig
|
||||
|
||||
config, err = pgx.ParseConfig(dialector.Config.DSN)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if dialector.Config.PreferSimpleProtocol {
|
||||
config.DefaultQueryExecMode = pgx.QueryExecModeSimpleProtocol
|
||||
}
|
||||
result := timeZoneMatcher.FindStringSubmatch(dialector.Config.DSN)
|
||||
var options []stdlib.OptionOpenDB
|
||||
if len(result) > 2 {
|
||||
config.RuntimeParams["timezone"] = result[2]
|
||||
options = append(options, stdlib.OptionAfterConnect(func(ctx context.Context, conn *pgx.Conn) error {
|
||||
loc, tzErr := time.LoadLocation(result[2])
|
||||
if tzErr != nil {
|
||||
return tzErr
|
||||
}
|
||||
conn.TypeMap().RegisterType(&pgtype.Type{
|
||||
Name: "timestamp",
|
||||
OID: pgtype.TimestampOID,
|
||||
Codec: &pgtype.TimestampCodec{ScanLocation: loc},
|
||||
})
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
db.ConnPool = stdlib.OpenDB(*config, options...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (dialector Dialector) Migrator(db *gorm.DB) gorm.Migrator {
|
||||
return Migrator{migrator.Migrator{Config: migrator.Config{
|
||||
DB: db,
|
||||
Dialector: dialector,
|
||||
CreateIndexAfterCreateTable: true,
|
||||
}}}
|
||||
}
|
||||
|
||||
func (dialector Dialector) DefaultValueOf(field *schema.Field) clause.Expression {
|
||||
return clause.Expr{SQL: "DEFAULT"}
|
||||
}
|
||||
|
||||
func (dialector Dialector) BindVarTo(writer clause.Writer, stmt *gorm.Statement, v interface{}) {
|
||||
writer.WriteByte('$')
|
||||
index := 0
|
||||
varLen := len(stmt.Vars)
|
||||
if varLen > 0 {
|
||||
switch stmt.Vars[0].(type) {
|
||||
case pgx.QueryExecMode:
|
||||
index++
|
||||
}
|
||||
}
|
||||
writer.WriteString(strconv.Itoa(varLen - index))
|
||||
}
|
||||
|
||||
func (dialector Dialector) QuoteTo(writer clause.Writer, str string) {
|
||||
if dialector.WithoutQuotingCheck {
|
||||
writer.WriteString(str)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
underQuoted, selfQuoted bool
|
||||
continuousBacktick int8
|
||||
shiftDelimiter int8
|
||||
)
|
||||
|
||||
for _, v := range []byte(str) {
|
||||
switch v {
|
||||
case '"':
|
||||
continuousBacktick++
|
||||
if continuousBacktick == 2 {
|
||||
writer.WriteString(`""`)
|
||||
continuousBacktick = 0
|
||||
}
|
||||
case '.':
|
||||
if continuousBacktick > 0 || !selfQuoted {
|
||||
shiftDelimiter = 0
|
||||
underQuoted = false
|
||||
continuousBacktick = 0
|
||||
writer.WriteByte('"')
|
||||
}
|
||||
writer.WriteByte(v)
|
||||
continue
|
||||
default:
|
||||
if shiftDelimiter-continuousBacktick <= 0 && !underQuoted {
|
||||
writer.WriteByte('"')
|
||||
underQuoted = true
|
||||
if selfQuoted = continuousBacktick > 0; selfQuoted {
|
||||
continuousBacktick -= 1
|
||||
}
|
||||
}
|
||||
|
||||
for ; continuousBacktick > 0; continuousBacktick -= 1 {
|
||||
writer.WriteString(`""`)
|
||||
}
|
||||
|
||||
writer.WriteByte(v)
|
||||
}
|
||||
shiftDelimiter++
|
||||
}
|
||||
|
||||
if continuousBacktick > 0 && !selfQuoted {
|
||||
writer.WriteString(`""`)
|
||||
}
|
||||
writer.WriteByte('"')
|
||||
}
|
||||
|
||||
var numericPlaceholder = regexp.MustCompile(`\$(\d+)`)
|
||||
|
||||
func (dialector Dialector) Explain(sql string, vars ...interface{}) string {
|
||||
return logger.ExplainSQL(sql, numericPlaceholder, `'`, vars...)
|
||||
}
|
||||
|
||||
func (dialector Dialector) DataTypeOf(field *schema.Field) string {
|
||||
switch field.DataType {
|
||||
case schema.Bool:
|
||||
return "boolean"
|
||||
case schema.Int, schema.Uint:
|
||||
size := field.Size
|
||||
if field.DataType == schema.Uint {
|
||||
size++
|
||||
}
|
||||
if field.AutoIncrement {
|
||||
switch {
|
||||
case size <= 16:
|
||||
return "smallserial"
|
||||
case size <= 32:
|
||||
return "serial"
|
||||
default:
|
||||
return "bigserial"
|
||||
}
|
||||
} else {
|
||||
switch {
|
||||
case size <= 16:
|
||||
return "smallint"
|
||||
case size <= 32:
|
||||
return "integer"
|
||||
default:
|
||||
return "bigint"
|
||||
}
|
||||
}
|
||||
case schema.Float:
|
||||
if field.Precision > 0 {
|
||||
if field.Scale > 0 {
|
||||
return fmt.Sprintf("numeric(%d, %d)", field.Precision, field.Scale)
|
||||
}
|
||||
return fmt.Sprintf("numeric(%d)", field.Precision)
|
||||
}
|
||||
return "decimal"
|
||||
case schema.String:
|
||||
if field.Size > 0 && field.Size <= 10485760 {
|
||||
return fmt.Sprintf("varchar(%d)", field.Size)
|
||||
}
|
||||
return "text"
|
||||
case schema.Time:
|
||||
if field.Precision > 0 {
|
||||
return fmt.Sprintf("timestamptz(%d)", field.Precision)
|
||||
}
|
||||
return "timestamptz"
|
||||
case schema.Bytes:
|
||||
return "bytea"
|
||||
default:
|
||||
return dialector.getSchemaCustomType(field)
|
||||
}
|
||||
}
|
||||
|
||||
func (dialector Dialector) getSchemaCustomType(field *schema.Field) string {
|
||||
sqlType := string(field.DataType)
|
||||
|
||||
if field.AutoIncrement && !strings.Contains(strings.ToLower(sqlType), "serial") {
|
||||
size := field.Size
|
||||
if field.GORMDataType == schema.Uint {
|
||||
size++
|
||||
}
|
||||
switch {
|
||||
case size <= 16:
|
||||
sqlType = "smallserial"
|
||||
case size <= 32:
|
||||
sqlType = "serial"
|
||||
default:
|
||||
sqlType = "bigserial"
|
||||
}
|
||||
}
|
||||
|
||||
return sqlType
|
||||
}
|
||||
|
||||
func (dialector Dialector) SavePoint(tx *gorm.DB, name string) error {
|
||||
tx.Exec("SAVEPOINT " + name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dialector Dialector) RollbackTo(tx *gorm.DB, name string) error {
|
||||
tx.Exec("ROLLBACK TO SAVEPOINT " + name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSerialDatabaseType(s string) (dbType string, ok bool) {
|
||||
switch s {
|
||||
case "smallserial":
|
||||
return "smallint", true
|
||||
case "serial":
|
||||
return "integer", true
|
||||
case "bigserial":
|
||||
return "bigint", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
Generated
+1
@@ -0,0 +1 @@
|
||||
.idea/
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-NOW Jinzhu <wosmvp@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# GORM Sqlite Driver
|
||||
|
||||

|
||||
|
||||
## USAGE
|
||||
|
||||
```go
|
||||
import (
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// github.com/mattn/go-sqlite3
|
||||
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
|
||||
```
|
||||
|
||||
Checkout [https://gorm.io](https://gorm.io) for details.
|
||||
|
||||
### Pure go Sqlite Driver
|
||||
|
||||
checkout [https://github.com/glebarez/sqlite](https://github.com/glebarez/sqlite) for details
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
|
||||
```
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm/migrator"
|
||||
)
|
||||
|
||||
var (
|
||||
sqliteSeparator = "`|\"|'|\t"
|
||||
uniqueRegexp = regexp.MustCompile(fmt.Sprintf(`^CONSTRAINT [%v]?[\w-]+[%v]? UNIQUE (.*)$`, sqliteSeparator, sqliteSeparator))
|
||||
indexRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)CREATE(?: UNIQUE)? INDEX [%v]?[\w\d-]+[%v]?(?s:.*?)ON (.*)$`, sqliteSeparator, sqliteSeparator))
|
||||
tableRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)(CREATE TABLE [%v]?[\w\d-]+[%v]?)(?:\s*\((.*)\))?`, sqliteSeparator, sqliteSeparator))
|
||||
separatorRegexp = regexp.MustCompile(fmt.Sprintf("[%v]", sqliteSeparator))
|
||||
columnRegexp = regexp.MustCompile(fmt.Sprintf(`^[%v]?([\w\d]+)[%v]?\s+([\w\(\)\d]+)(.*)$`, sqliteSeparator, sqliteSeparator))
|
||||
defaultValueRegexp = regexp.MustCompile(`(?i) DEFAULT \(?(.+)?\)?( |COLLATE|GENERATED|$)`)
|
||||
regRealDataType = regexp.MustCompile(`[^\d](\d+)[^\d]?`)
|
||||
)
|
||||
|
||||
type ddl struct {
|
||||
head string
|
||||
fields []string
|
||||
columns []migrator.ColumnType
|
||||
}
|
||||
|
||||
func parseDDL(strs ...string) (*ddl, error) {
|
||||
var result ddl
|
||||
for _, str := range strs {
|
||||
if sections := tableRegexp.FindStringSubmatch(str); len(sections) > 0 {
|
||||
var (
|
||||
ddlBody = sections[2]
|
||||
ddlBodyRunes = []rune(ddlBody)
|
||||
bracketLevel int
|
||||
quote rune
|
||||
buf string
|
||||
)
|
||||
ddlBodyRunesLen := len(ddlBodyRunes)
|
||||
|
||||
result.head = sections[1]
|
||||
|
||||
for idx := 0; idx < ddlBodyRunesLen; idx++ {
|
||||
var (
|
||||
next rune = 0
|
||||
c = ddlBodyRunes[idx]
|
||||
)
|
||||
if idx+1 < ddlBodyRunesLen {
|
||||
next = ddlBodyRunes[idx+1]
|
||||
}
|
||||
|
||||
if sc := string(c); separatorRegexp.MatchString(sc) {
|
||||
if c == next {
|
||||
buf += sc // Skip escaped quote
|
||||
idx++
|
||||
} else if quote > 0 {
|
||||
quote = 0
|
||||
} else {
|
||||
quote = c
|
||||
}
|
||||
} else if quote == 0 {
|
||||
if c == '(' {
|
||||
bracketLevel++
|
||||
} else if c == ')' {
|
||||
bracketLevel--
|
||||
} else if bracketLevel == 0 {
|
||||
if c == ',' {
|
||||
result.fields = append(result.fields, strings.TrimSpace(buf))
|
||||
buf = ""
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bracketLevel < 0 {
|
||||
return nil, errors.New("invalid DDL, unbalanced brackets")
|
||||
}
|
||||
|
||||
buf += string(c)
|
||||
}
|
||||
|
||||
if bracketLevel != 0 {
|
||||
return nil, errors.New("invalid DDL, unbalanced brackets")
|
||||
}
|
||||
|
||||
if buf != "" {
|
||||
result.fields = append(result.fields, strings.TrimSpace(buf))
|
||||
}
|
||||
|
||||
for _, f := range result.fields {
|
||||
fUpper := strings.ToUpper(f)
|
||||
if strings.HasPrefix(fUpper, "CHECK") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(fUpper, "CONSTRAINT") {
|
||||
matches := uniqueRegexp.FindStringSubmatch(f)
|
||||
if len(matches) > 0 {
|
||||
cols, err := parseAllColumns(matches[1])
|
||||
if err == nil && len(cols) == 1 {
|
||||
for idx, column := range result.columns {
|
||||
if column.NameValue.String == cols[0] {
|
||||
column.UniqueValue = sql.NullBool{Bool: true, Valid: true}
|
||||
result.columns[idx] = column
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(fUpper, "PRIMARY KEY") {
|
||||
cols, err := parseAllColumns(f)
|
||||
if err == nil {
|
||||
for _, name := range cols {
|
||||
for idx, column := range result.columns {
|
||||
if column.NameValue.String == name {
|
||||
column.PrimaryKeyValue = sql.NullBool{Bool: true, Valid: true}
|
||||
result.columns[idx] = column
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if matches := columnRegexp.FindStringSubmatch(f); len(matches) > 0 {
|
||||
columnType := migrator.ColumnType{
|
||||
NameValue: sql.NullString{String: matches[1], Valid: true},
|
||||
DataTypeValue: sql.NullString{String: matches[2], Valid: true},
|
||||
ColumnTypeValue: sql.NullString{String: matches[2], Valid: true},
|
||||
PrimaryKeyValue: sql.NullBool{Valid: true},
|
||||
UniqueValue: sql.NullBool{Valid: true},
|
||||
NullableValue: sql.NullBool{Bool: true, Valid: true},
|
||||
DefaultValueValue: sql.NullString{Valid: false},
|
||||
}
|
||||
|
||||
matchUpper := strings.ToUpper(matches[3])
|
||||
if strings.Contains(matchUpper, " NOT NULL") {
|
||||
columnType.NullableValue = sql.NullBool{Bool: false, Valid: true}
|
||||
} else if strings.Contains(matchUpper, " NULL") {
|
||||
columnType.NullableValue = sql.NullBool{Bool: true, Valid: true}
|
||||
}
|
||||
if strings.Contains(matchUpper, " UNIQUE") {
|
||||
columnType.UniqueValue = sql.NullBool{Bool: true, Valid: true}
|
||||
}
|
||||
if strings.Contains(matchUpper, " PRIMARY") {
|
||||
columnType.PrimaryKeyValue = sql.NullBool{Bool: true, Valid: true}
|
||||
}
|
||||
if defaultMatches := defaultValueRegexp.FindStringSubmatch(matches[3]); len(defaultMatches) > 1 {
|
||||
if strings.ToLower(defaultMatches[1]) != "null" {
|
||||
columnType.DefaultValueValue = sql.NullString{String: strings.Trim(defaultMatches[1], `"`), Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
// data type length
|
||||
matches := regRealDataType.FindAllStringSubmatch(columnType.DataTypeValue.String, -1)
|
||||
if len(matches) == 1 && len(matches[0]) == 2 {
|
||||
size, _ := strconv.Atoi(matches[0][1])
|
||||
columnType.LengthValue = sql.NullInt64{Valid: true, Int64: int64(size)}
|
||||
columnType.DataTypeValue.String = strings.TrimSuffix(columnType.DataTypeValue.String, matches[0][0])
|
||||
}
|
||||
|
||||
result.columns = append(result.columns, columnType)
|
||||
}
|
||||
}
|
||||
} else if matches := indexRegexp.FindStringSubmatch(str); len(matches) > 0 {
|
||||
// don't report Unique by UniqueIndex
|
||||
} else {
|
||||
return nil, errors.New("invalid DDL")
|
||||
}
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (d *ddl) clone() *ddl {
|
||||
copied := new(ddl)
|
||||
*copied = *d
|
||||
|
||||
copied.fields = make([]string, len(d.fields))
|
||||
copy(copied.fields, d.fields)
|
||||
copied.columns = make([]migrator.ColumnType, len(d.columns))
|
||||
copy(copied.columns, d.columns)
|
||||
|
||||
return copied
|
||||
}
|
||||
|
||||
func (d *ddl) compile() string {
|
||||
if len(d.fields) == 0 {
|
||||
return d.head
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s (%s)", d.head, strings.Join(d.fields, ","))
|
||||
}
|
||||
|
||||
func (d *ddl) renameTable(dst, src string) error {
|
||||
tableReg, err := regexp.Compile("\\s*('|`|\")?\\b" + regexp.QuoteMeta(src) + "\\b('|`|\")?\\s*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
replaced := tableReg.ReplaceAllString(d.head, fmt.Sprintf(" `%s` ", dst))
|
||||
if replaced == d.head {
|
||||
return fmt.Errorf("failed to look up tablename `%s` from DDL head '%s'", src, d.head)
|
||||
}
|
||||
|
||||
d.head = replaced
|
||||
return nil
|
||||
}
|
||||
|
||||
func compileConstraintRegexp(name string) *regexp.Regexp {
|
||||
return regexp.MustCompile("^(?i:CONSTRAINT)\\s+[\"`]?" + regexp.QuoteMeta(name) + "[\"`\\s]")
|
||||
}
|
||||
|
||||
func (d *ddl) addConstraint(name string, sql string) {
|
||||
reg := compileConstraintRegexp(name)
|
||||
|
||||
for i := 0; i < len(d.fields); i++ {
|
||||
if reg.MatchString(d.fields[i]) {
|
||||
d.fields[i] = sql
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
d.fields = append(d.fields, sql)
|
||||
}
|
||||
|
||||
func (d *ddl) removeConstraint(name string) bool {
|
||||
reg := compileConstraintRegexp(name)
|
||||
|
||||
for i := 0; i < len(d.fields); i++ {
|
||||
if reg.MatchString(d.fields[i]) {
|
||||
d.fields = append(d.fields[:i], d.fields[i+1:]...)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *ddl) hasConstraint(name string) bool {
|
||||
reg := compileConstraintRegexp(name)
|
||||
|
||||
for _, f := range d.fields {
|
||||
if reg.MatchString(f) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *ddl) getColumns() []string {
|
||||
res := []string{}
|
||||
|
||||
for _, f := range d.fields {
|
||||
fUpper := strings.ToUpper(f)
|
||||
if strings.HasPrefix(fUpper, "PRIMARY KEY") ||
|
||||
strings.HasPrefix(fUpper, "CHECK") ||
|
||||
strings.HasPrefix(fUpper, "CONSTRAINT") ||
|
||||
strings.Contains(fUpper, "GENERATED ALWAYS AS") {
|
||||
continue
|
||||
}
|
||||
|
||||
reg := regexp.MustCompile("^[\"`']?([\\w\\d]+)[\"`']?")
|
||||
match := reg.FindStringSubmatch(f)
|
||||
|
||||
if match != nil {
|
||||
res = append(res, "`"+match[1]+"`")
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (d *ddl) removeColumn(name string) bool {
|
||||
reg := regexp.MustCompile("^(`|'|\"| )" + regexp.QuoteMeta(name) + "(`|'|\"| ) .*?$")
|
||||
|
||||
for i := 0; i < len(d.fields); i++ {
|
||||
if reg.MatchString(d.fields[i]) {
|
||||
d.fields = append(d.fields[:i], d.fields[i+1:]...)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type parseAllColumnsState int
|
||||
|
||||
const (
|
||||
parseAllColumnsState_NONE parseAllColumnsState = iota
|
||||
parseAllColumnsState_Beginning
|
||||
parseAllColumnsState_ReadingRawName
|
||||
parseAllColumnsState_ReadingQuotedName
|
||||
parseAllColumnsState_EndOfName
|
||||
parseAllColumnsState_State_End
|
||||
)
|
||||
|
||||
func parseAllColumns(in string) ([]string, error) {
|
||||
s := []rune(in)
|
||||
columns := make([]string, 0)
|
||||
state := parseAllColumnsState_NONE
|
||||
quote := rune(0)
|
||||
name := make([]rune, 0)
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch state {
|
||||
case parseAllColumnsState_NONE:
|
||||
if s[i] == '(' {
|
||||
state = parseAllColumnsState_Beginning
|
||||
}
|
||||
case parseAllColumnsState_Beginning:
|
||||
if isSpace(s[i]) {
|
||||
continue
|
||||
}
|
||||
if isQuote(s[i]) {
|
||||
state = parseAllColumnsState_ReadingQuotedName
|
||||
quote = s[i]
|
||||
continue
|
||||
}
|
||||
if s[i] == '[' {
|
||||
state = parseAllColumnsState_ReadingQuotedName
|
||||
quote = ']'
|
||||
continue
|
||||
} else if s[i] == ')' {
|
||||
return columns, fmt.Errorf("unexpected token: %s", string(s[i]))
|
||||
}
|
||||
state = parseAllColumnsState_ReadingRawName
|
||||
name = append(name, s[i])
|
||||
case parseAllColumnsState_ReadingRawName:
|
||||
if isSeparator(s[i]) {
|
||||
state = parseAllColumnsState_Beginning
|
||||
columns = append(columns, string(name))
|
||||
name = make([]rune, 0)
|
||||
continue
|
||||
}
|
||||
if s[i] == ')' {
|
||||
state = parseAllColumnsState_State_End
|
||||
columns = append(columns, string(name))
|
||||
}
|
||||
if isQuote(s[i]) {
|
||||
return nil, fmt.Errorf("unexpected token: %s", string(s[i]))
|
||||
}
|
||||
if isSpace(s[i]) {
|
||||
state = parseAllColumnsState_EndOfName
|
||||
columns = append(columns, string(name))
|
||||
name = make([]rune, 0)
|
||||
continue
|
||||
}
|
||||
name = append(name, s[i])
|
||||
case parseAllColumnsState_ReadingQuotedName:
|
||||
if s[i] == quote {
|
||||
// check if quote character is escaped
|
||||
if i+1 < len(s) && s[i+1] == quote {
|
||||
name = append(name, quote)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
state = parseAllColumnsState_EndOfName
|
||||
columns = append(columns, string(name))
|
||||
name = make([]rune, 0)
|
||||
continue
|
||||
}
|
||||
name = append(name, s[i])
|
||||
case parseAllColumnsState_EndOfName:
|
||||
if isSpace(s[i]) {
|
||||
continue
|
||||
}
|
||||
if isSeparator(s[i]) {
|
||||
state = parseAllColumnsState_Beginning
|
||||
continue
|
||||
}
|
||||
if s[i] == ')' {
|
||||
state = parseAllColumnsState_State_End
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected token: %s", string(s[i]))
|
||||
case parseAllColumnsState_State_End:
|
||||
break
|
||||
}
|
||||
}
|
||||
if state != parseAllColumnsState_State_End {
|
||||
return nil, errors.New("unexpected end")
|
||||
}
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
func isSpace(r rune) bool {
|
||||
return r == ' ' || r == '\t'
|
||||
}
|
||||
|
||||
func isQuote(r rune) bool {
|
||||
return r == '`' || r == '"' || r == '\''
|
||||
}
|
||||
|
||||
func isSeparator(r rune) bool {
|
||||
return r == ','
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// The error codes to map sqlite errors to gorm errors, here is a reference about error codes for sqlite https://www.sqlite.org/rescode.html.
|
||||
var errCodes = map[int]error{
|
||||
1555: gorm.ErrDuplicatedKey,
|
||||
2067: gorm.ErrDuplicatedKey,
|
||||
787: gorm.ErrForeignKeyViolated,
|
||||
}
|
||||
|
||||
type ErrMessage struct {
|
||||
Code int `json:"Code"`
|
||||
ExtendedCode int `json:"ExtendedCode"`
|
||||
SystemErrno int `json:"SystemErrno"`
|
||||
}
|
||||
|
||||
// Translate it will translate the error to native gorm errors.
|
||||
// We are not using go-sqlite3 error type intentionally here because it will need the CGO_ENABLED=1 and cross-C-compiler.
|
||||
func (dialector Dialector) Translate(err error) error {
|
||||
parsedErr, marshalErr := json.Marshal(err)
|
||||
if marshalErr != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errMsg ErrMessage
|
||||
unmarshalErr := json.Unmarshal(parsedErr, &errMsg)
|
||||
if unmarshalErr != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if translatedErr, found := errCodes[errMsg.ExtendedCode]; found {
|
||||
return translatedErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package sqlite
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrConstraintsNotImplemented = errors.New("constraints not implemented on sqlite, consider using DisableForeignKeyConstraintWhenMigrating, more details https://github.com/go-gorm/gorm/wiki/GORM-V2-Release-Note-Draft#all-new-migrator")
|
||||
)
|
||||
+430
@@ -0,0 +1,430 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/migrator"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
type Migrator struct {
|
||||
migrator.Migrator
|
||||
}
|
||||
|
||||
func (m *Migrator) RunWithoutForeignKey(fc func() error) error {
|
||||
var enabled int
|
||||
m.DB.Raw("PRAGMA foreign_keys").Scan(&enabled)
|
||||
if enabled == 1 {
|
||||
m.DB.Exec("PRAGMA foreign_keys = OFF")
|
||||
defer m.DB.Exec("PRAGMA foreign_keys = ON")
|
||||
}
|
||||
|
||||
return fc()
|
||||
}
|
||||
|
||||
func (m Migrator) HasTable(value interface{}) bool {
|
||||
var count int
|
||||
m.Migrator.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
return m.DB.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?", stmt.Table).Row().Scan(&count)
|
||||
})
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (m Migrator) DropTable(values ...interface{}) error {
|
||||
return m.RunWithoutForeignKey(func() error {
|
||||
values = m.ReorderModels(values, false)
|
||||
tx := m.DB.Session(&gorm.Session{})
|
||||
|
||||
for i := len(values) - 1; i >= 0; i-- {
|
||||
if err := m.RunWithValue(values[i], func(stmt *gorm.Statement) error {
|
||||
return tx.Exec("DROP TABLE IF EXISTS ?", clause.Table{Name: stmt.Table}).Error
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (m Migrator) GetTables() (tableList []string, err error) {
|
||||
return tableList, m.DB.Raw("SELECT name FROM sqlite_master where type=?", "table").Scan(&tableList).Error
|
||||
}
|
||||
|
||||
func (m Migrator) HasColumn(value interface{}, name string) bool {
|
||||
var count int
|
||||
m.Migrator.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
if field := stmt.Schema.LookUpField(name); field != nil {
|
||||
name = field.DBName
|
||||
}
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
m.DB.Raw(
|
||||
"SELECT count(*) FROM sqlite_master WHERE type = ? AND tbl_name = ? AND (sql LIKE ? OR sql LIKE ? OR sql LIKE ? OR sql LIKE ? OR sql LIKE ?)",
|
||||
"table", stmt.Table, `%"`+name+`" %`, `%`+name+` %`, "%`"+name+"`%", "%["+name+"]%", "%\t"+name+"\t%",
|
||||
).Row().Scan(&count)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (m Migrator) AlterColumn(value interface{}, name string) error {
|
||||
return m.RunWithoutForeignKey(func() error {
|
||||
return m.recreateTable(value, nil, func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
|
||||
if field := stmt.Schema.LookUpField(name); field != nil {
|
||||
var sqlArgs []interface{}
|
||||
for i, f := range ddl.fields {
|
||||
if matches := columnRegexp.FindStringSubmatch(f); len(matches) > 1 && matches[1] == field.DBName {
|
||||
ddl.fields[i] = fmt.Sprintf("`%v` ?", field.DBName)
|
||||
sqlArgs = []interface{}{m.FullDataTypeOf(field)}
|
||||
// table created by old version might look like `CREATE TABLE ? (? varchar(10) UNIQUE)`.
|
||||
// FullDataTypeOf doesn't contain UNIQUE, so we need to add unique constraint.
|
||||
if strings.Contains(strings.ToUpper(matches[3]), " UNIQUE") {
|
||||
uniName := m.DB.NamingStrategy.UniqueName(stmt.Table, field.DBName)
|
||||
uni, _ := m.GuessConstraintInterfaceAndTable(stmt, uniName)
|
||||
if uni != nil {
|
||||
uniSQL, uniArgs := uni.Build()
|
||||
ddl.addConstraint(uniName, uniSQL)
|
||||
sqlArgs = append(sqlArgs, uniArgs...)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return ddl, sqlArgs, nil
|
||||
}
|
||||
return nil, nil, fmt.Errorf("failed to alter field with name %v", name)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ColumnTypes return columnTypes []gorm.ColumnType and execErr error
|
||||
func (m Migrator) ColumnTypes(value interface{}) ([]gorm.ColumnType, error) {
|
||||
columnTypes := make([]gorm.ColumnType, 0)
|
||||
execErr := m.RunWithValue(value, func(stmt *gorm.Statement) (err error) {
|
||||
var (
|
||||
sqls []string
|
||||
sqlDDL *ddl
|
||||
)
|
||||
|
||||
if err := m.DB.Raw("SELECT sql FROM sqlite_master WHERE type IN ? AND tbl_name = ? AND sql IS NOT NULL order by type = ? desc", []string{"table", "index"}, stmt.Table, "table").Scan(&sqls).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sqlDDL, err = parseDDL(sqls...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err := m.DB.Session(&gorm.Session{}).Table(stmt.Table).Limit(1).Rows()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err = rows.Close()
|
||||
}()
|
||||
|
||||
var rawColumnTypes []*sql.ColumnType
|
||||
rawColumnTypes, err = rows.ColumnTypes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, c := range rawColumnTypes {
|
||||
columnType := migrator.ColumnType{SQLColumnType: c}
|
||||
for _, column := range sqlDDL.columns {
|
||||
if column.NameValue.String == c.Name() {
|
||||
column.SQLColumnType = c
|
||||
columnType = column
|
||||
break
|
||||
}
|
||||
}
|
||||
columnTypes = append(columnTypes, columnType)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return columnTypes, execErr
|
||||
}
|
||||
|
||||
func (m Migrator) DropColumn(value interface{}, name string) error {
|
||||
return m.recreateTable(value, nil, func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
|
||||
if field := stmt.Schema.LookUpField(name); field != nil {
|
||||
name = field.DBName
|
||||
}
|
||||
|
||||
ddl.removeColumn(name)
|
||||
return ddl, nil, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (m Migrator) CreateConstraint(value interface{}, name string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
|
||||
|
||||
return m.recreateTable(value, &table,
|
||||
func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
|
||||
var (
|
||||
constraintName string
|
||||
constraintSql string
|
||||
constraintValues []interface{}
|
||||
)
|
||||
|
||||
if constraint != nil {
|
||||
constraintName = constraint.GetName()
|
||||
constraintSql, constraintValues = constraint.Build()
|
||||
} else {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
ddl.addConstraint(constraintName, constraintSql)
|
||||
return ddl, constraintValues, nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (m Migrator) DropConstraint(value interface{}, name string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
|
||||
if constraint != nil {
|
||||
name = constraint.GetName()
|
||||
}
|
||||
|
||||
return m.recreateTable(value, &table,
|
||||
func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
|
||||
ddl.removeConstraint(name)
|
||||
return ddl, nil, nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (m Migrator) HasConstraint(value interface{}, name string) bool {
|
||||
var count int64
|
||||
m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
|
||||
if constraint != nil {
|
||||
name = constraint.GetName()
|
||||
}
|
||||
|
||||
m.DB.Raw(
|
||||
"SELECT count(*) FROM sqlite_master WHERE type = ? AND tbl_name = ? AND (sql LIKE ? OR sql LIKE ? OR sql LIKE ? OR sql LIKE ? OR sql LIKE ?)",
|
||||
"table", table, `%CONSTRAINT "`+name+`" %`, `%CONSTRAINT `+name+` %`, "%CONSTRAINT `"+name+"`%", "%CONSTRAINT ["+name+"]%", "%CONSTRAINT \t"+name+"\t%",
|
||||
).Row().Scan(&count)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (m Migrator) CurrentDatabase() (name string) {
|
||||
var null interface{}
|
||||
m.DB.Raw("PRAGMA database_list").Row().Scan(&null, &name, &null)
|
||||
return
|
||||
}
|
||||
|
||||
func (m Migrator) BuildIndexOptions(opts []schema.IndexOption, stmt *gorm.Statement) (results []interface{}) {
|
||||
for _, opt := range opts {
|
||||
str := stmt.Quote(opt.DBName)
|
||||
if opt.Expression != "" {
|
||||
str = opt.Expression
|
||||
}
|
||||
|
||||
if opt.Collate != "" {
|
||||
str += " COLLATE " + opt.Collate
|
||||
}
|
||||
|
||||
if opt.Sort != "" {
|
||||
str += " " + opt.Sort
|
||||
}
|
||||
results = append(results, clause.Expr{SQL: str})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m Migrator) CreateIndex(value interface{}, name string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
if idx := stmt.Schema.LookIndex(name); idx != nil {
|
||||
opts := m.BuildIndexOptions(idx.Fields, stmt)
|
||||
values := []interface{}{clause.Column{Name: idx.Name}, clause.Table{Name: stmt.Table}, opts}
|
||||
|
||||
createIndexSQL := "CREATE "
|
||||
if idx.Class != "" {
|
||||
createIndexSQL += idx.Class + " "
|
||||
}
|
||||
createIndexSQL += "INDEX ?"
|
||||
|
||||
if idx.Type != "" {
|
||||
createIndexSQL += " USING " + idx.Type
|
||||
}
|
||||
createIndexSQL += " ON ??"
|
||||
|
||||
if idx.Where != "" {
|
||||
createIndexSQL += " WHERE " + idx.Where
|
||||
}
|
||||
|
||||
return m.DB.Exec(createIndexSQL, values...).Error
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to create index with name %v", name)
|
||||
})
|
||||
}
|
||||
|
||||
func (m Migrator) HasIndex(value interface{}, name string) bool {
|
||||
var count int
|
||||
m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
if idx := stmt.Schema.LookIndex(name); idx != nil {
|
||||
name = idx.Name
|
||||
}
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
m.DB.Raw(
|
||||
"SELECT count(*) FROM sqlite_master WHERE type = ? AND tbl_name = ? AND name = ?", "index", stmt.Table, name,
|
||||
).Row().Scan(&count)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (m Migrator) RenameIndex(value interface{}, oldName, newName string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
var sql string
|
||||
m.DB.Raw("SELECT sql FROM sqlite_master WHERE type = ? AND tbl_name = ? AND name = ?", "index", stmt.Table, oldName).Row().Scan(&sql)
|
||||
if sql != "" {
|
||||
if err := m.DropIndex(value, oldName); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.DB.Exec(strings.Replace(sql, oldName, newName, 1)).Error
|
||||
}
|
||||
return fmt.Errorf("failed to find index with name %v", oldName)
|
||||
})
|
||||
}
|
||||
|
||||
func (m Migrator) DropIndex(value interface{}, name string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
if idx := stmt.Schema.LookIndex(name); idx != nil {
|
||||
name = idx.Name
|
||||
}
|
||||
}
|
||||
|
||||
return m.DB.Exec("DROP INDEX ?", clause.Column{Name: name}).Error
|
||||
})
|
||||
}
|
||||
|
||||
type Index struct {
|
||||
Seq int
|
||||
Name string
|
||||
Unique bool
|
||||
Origin string
|
||||
Partial bool
|
||||
}
|
||||
|
||||
// GetIndexes return Indexes []gorm.Index and execErr error,
|
||||
// See the [doc]
|
||||
//
|
||||
// [doc]: https://www.sqlite.org/pragma.html#pragma_index_list
|
||||
func (m Migrator) GetIndexes(value interface{}) ([]gorm.Index, error) {
|
||||
indexes := make([]gorm.Index, 0)
|
||||
err := m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
rst := make([]*Index, 0)
|
||||
if err := m.DB.Debug().Raw("SELECT * FROM PRAGMA_index_list(?)", stmt.Table).Scan(&rst).Error; err != nil { // alias `PRAGMA index_list(?)`
|
||||
return err
|
||||
}
|
||||
for _, index := range rst {
|
||||
if index.Origin == "u" { // skip the index was created by a UNIQUE constraint
|
||||
continue
|
||||
}
|
||||
var columns []string
|
||||
if err := m.DB.Raw("SELECT name FROM PRAGMA_index_info(?)", index.Name).Scan(&columns).Error; err != nil { // alias `PRAGMA index_info(?)`
|
||||
return err
|
||||
}
|
||||
indexes = append(indexes, &migrator.Index{
|
||||
TableName: stmt.Table,
|
||||
NameValue: index.Name,
|
||||
ColumnList: columns,
|
||||
PrimaryKeyValue: sql.NullBool{Bool: index.Origin == "pk", Valid: true}, // The exceptions are INTEGER PRIMARY KEY
|
||||
UniqueValue: sql.NullBool{Bool: index.Unique, Valid: true},
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return indexes, err
|
||||
}
|
||||
|
||||
func (m Migrator) getRawDDL(table string) (string, error) {
|
||||
var createSQL string
|
||||
m.DB.Raw("SELECT sql FROM sqlite_master WHERE type = ? AND tbl_name = ? AND name = ?", "table", table, table).Row().Scan(&createSQL)
|
||||
|
||||
if m.DB.Error != nil {
|
||||
return "", m.DB.Error
|
||||
}
|
||||
return createSQL, nil
|
||||
}
|
||||
|
||||
func (m Migrator) recreateTable(
|
||||
value interface{}, tablePtr *string,
|
||||
getCreateSQL func(ddl *ddl, stmt *gorm.Statement) (sql *ddl, sqlArgs []interface{}, err error),
|
||||
) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
table := stmt.Table
|
||||
if tablePtr != nil {
|
||||
table = *tablePtr
|
||||
}
|
||||
|
||||
rawDDL, err := m.getRawDDL(table)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
originDDL, err := parseDDL(rawDDL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createDDL, sqlArgs, err := getCreateSQL(originDDL.clone(), stmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if createDDL == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
newTableName := table + "__temp"
|
||||
if err := createDDL.renameTable(newTableName, table); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
columns := createDDL.getColumns()
|
||||
createSQL := createDDL.compile()
|
||||
|
||||
return m.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Exec(createSQL, sqlArgs...).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
queries := []string{
|
||||
fmt.Sprintf("INSERT INTO `%v`(%v) SELECT %v FROM `%v`", newTableName, strings.Join(columns, ","), strings.Join(columns, ","), table),
|
||||
fmt.Sprintf("DROP TABLE `%v`", table),
|
||||
fmt.Sprintf("ALTER TABLE `%v` RENAME TO `%v`", newTableName, table),
|
||||
}
|
||||
for _, query := range queries {
|
||||
if err := tx.Exec(query).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
|
||||
"gorm.io/gorm/callbacks"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/migrator"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
// DriverName is the default driver name for SQLite.
|
||||
const DriverName = "sqlite3"
|
||||
|
||||
type Dialector struct {
|
||||
DriverName string
|
||||
DSN string
|
||||
Conn gorm.ConnPool
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
DriverName string
|
||||
DSN string
|
||||
Conn gorm.ConnPool
|
||||
}
|
||||
|
||||
func Open(dsn string) gorm.Dialector {
|
||||
return &Dialector{DSN: dsn}
|
||||
}
|
||||
|
||||
func New(config Config) gorm.Dialector {
|
||||
return &Dialector{DSN: config.DSN, DriverName: config.DriverName, Conn: config.Conn}
|
||||
}
|
||||
|
||||
func (dialector Dialector) Name() string {
|
||||
return "sqlite"
|
||||
}
|
||||
|
||||
func (dialector Dialector) Initialize(db *gorm.DB) (err error) {
|
||||
if dialector.DriverName == "" {
|
||||
dialector.DriverName = DriverName
|
||||
}
|
||||
|
||||
if dialector.Conn != nil {
|
||||
db.ConnPool = dialector.Conn
|
||||
} else {
|
||||
conn, err := sql.Open(dialector.DriverName, dialector.DSN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.ConnPool = conn
|
||||
}
|
||||
|
||||
var version string
|
||||
if err := db.ConnPool.QueryRowContext(context.Background(), "select sqlite_version()").Scan(&version); err != nil {
|
||||
return err
|
||||
}
|
||||
// https://www.sqlite.org/releaselog/3_35_0.html
|
||||
if compareVersion(version, "3.35.0") >= 0 {
|
||||
callbacks.RegisterDefaultCallbacks(db, &callbacks.Config{
|
||||
CreateClauses: []string{"INSERT", "VALUES", "ON CONFLICT", "RETURNING"},
|
||||
UpdateClauses: []string{"UPDATE", "SET", "FROM", "WHERE", "RETURNING"},
|
||||
DeleteClauses: []string{"DELETE", "FROM", "WHERE", "RETURNING"},
|
||||
LastInsertIDReversed: true,
|
||||
})
|
||||
} else {
|
||||
callbacks.RegisterDefaultCallbacks(db, &callbacks.Config{
|
||||
LastInsertIDReversed: true,
|
||||
})
|
||||
}
|
||||
|
||||
for k, v := range dialector.ClauseBuilders() {
|
||||
if _, ok := db.ClauseBuilders[k]; !ok {
|
||||
db.ClauseBuilders[k] = v
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (dialector Dialector) ClauseBuilders() map[string]clause.ClauseBuilder {
|
||||
return map[string]clause.ClauseBuilder{
|
||||
"INSERT": func(c clause.Clause, builder clause.Builder) {
|
||||
if insert, ok := c.Expression.(clause.Insert); ok {
|
||||
if stmt, ok := builder.(*gorm.Statement); ok {
|
||||
stmt.WriteString("INSERT ")
|
||||
if insert.Modifier != "" {
|
||||
stmt.WriteString(insert.Modifier)
|
||||
stmt.WriteByte(' ')
|
||||
}
|
||||
|
||||
stmt.WriteString("INTO ")
|
||||
if insert.Table.Name == "" {
|
||||
stmt.WriteQuoted(stmt.Table)
|
||||
} else {
|
||||
stmt.WriteQuoted(insert.Table)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Build(builder)
|
||||
},
|
||||
"LIMIT": func(c clause.Clause, builder clause.Builder) {
|
||||
if limit, ok := c.Expression.(clause.Limit); ok {
|
||||
var lmt = -1
|
||||
if limit.Limit != nil && *limit.Limit >= 0 {
|
||||
lmt = *limit.Limit
|
||||
}
|
||||
if lmt >= 0 || limit.Offset > 0 {
|
||||
builder.WriteString("LIMIT ")
|
||||
builder.WriteString(strconv.Itoa(lmt))
|
||||
}
|
||||
if limit.Offset > 0 {
|
||||
builder.WriteString(" OFFSET ")
|
||||
builder.WriteString(strconv.Itoa(limit.Offset))
|
||||
}
|
||||
}
|
||||
},
|
||||
"FOR": func(c clause.Clause, builder clause.Builder) {
|
||||
if _, ok := c.Expression.(clause.Locking); ok {
|
||||
// SQLite3 does not support row-level locking.
|
||||
return
|
||||
}
|
||||
c.Build(builder)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (dialector Dialector) DefaultValueOf(field *schema.Field) clause.Expression {
|
||||
if field.AutoIncrement {
|
||||
return clause.Expr{SQL: "NULL"}
|
||||
}
|
||||
|
||||
// doesn't work, will raise error
|
||||
return clause.Expr{SQL: "DEFAULT"}
|
||||
}
|
||||
|
||||
func (dialector Dialector) Migrator(db *gorm.DB) gorm.Migrator {
|
||||
return Migrator{migrator.Migrator{Config: migrator.Config{
|
||||
DB: db,
|
||||
Dialector: dialector,
|
||||
CreateIndexAfterCreateTable: true,
|
||||
}}}
|
||||
}
|
||||
|
||||
func (dialector Dialector) BindVarTo(writer clause.Writer, stmt *gorm.Statement, v interface{}) {
|
||||
writer.WriteByte('?')
|
||||
}
|
||||
|
||||
func (dialector Dialector) QuoteTo(writer clause.Writer, str string) {
|
||||
var (
|
||||
underQuoted, selfQuoted bool
|
||||
continuousBacktick int8
|
||||
shiftDelimiter int8
|
||||
)
|
||||
|
||||
for _, v := range []byte(str) {
|
||||
switch v {
|
||||
case '`':
|
||||
continuousBacktick++
|
||||
if continuousBacktick == 2 {
|
||||
writer.WriteString("``")
|
||||
continuousBacktick = 0
|
||||
}
|
||||
case '.':
|
||||
if continuousBacktick > 0 || !selfQuoted {
|
||||
shiftDelimiter = 0
|
||||
underQuoted = false
|
||||
continuousBacktick = 0
|
||||
writer.WriteString("`")
|
||||
}
|
||||
writer.WriteByte(v)
|
||||
continue
|
||||
default:
|
||||
if shiftDelimiter-continuousBacktick <= 0 && !underQuoted {
|
||||
writer.WriteString("`")
|
||||
underQuoted = true
|
||||
if selfQuoted = continuousBacktick > 0; selfQuoted {
|
||||
continuousBacktick -= 1
|
||||
}
|
||||
}
|
||||
|
||||
for ; continuousBacktick > 0; continuousBacktick -= 1 {
|
||||
writer.WriteString("``")
|
||||
}
|
||||
|
||||
writer.WriteByte(v)
|
||||
}
|
||||
shiftDelimiter++
|
||||
}
|
||||
|
||||
if continuousBacktick > 0 && !selfQuoted {
|
||||
writer.WriteString("``")
|
||||
}
|
||||
writer.WriteString("`")
|
||||
}
|
||||
|
||||
func (dialector Dialector) Explain(sql string, vars ...interface{}) string {
|
||||
return logger.ExplainSQL(sql, nil, `"`, vars...)
|
||||
}
|
||||
|
||||
func (dialector Dialector) DataTypeOf(field *schema.Field) string {
|
||||
switch field.DataType {
|
||||
case schema.Bool:
|
||||
return "numeric"
|
||||
case schema.Int, schema.Uint:
|
||||
if field.AutoIncrement {
|
||||
// doesn't check `PrimaryKey`, to keep backward compatibility
|
||||
// https://www.sqlite.org/autoinc.html
|
||||
return "integer PRIMARY KEY AUTOINCREMENT"
|
||||
} else {
|
||||
return "integer"
|
||||
}
|
||||
case schema.Float:
|
||||
return "real"
|
||||
case schema.String:
|
||||
return "text"
|
||||
case schema.Time:
|
||||
// Distinguish between schema.Time and tag time
|
||||
if val, ok := field.TagSettings["TYPE"]; ok {
|
||||
return val
|
||||
} else {
|
||||
return "datetime"
|
||||
}
|
||||
case schema.Bytes:
|
||||
return "blob"
|
||||
}
|
||||
|
||||
return string(field.DataType)
|
||||
}
|
||||
|
||||
func (dialectopr Dialector) SavePoint(tx *gorm.DB, name string) error {
|
||||
tx.Exec("SAVEPOINT " + name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dialectopr Dialector) RollbackTo(tx *gorm.DB, name string) error {
|
||||
tx.Exec("ROLLBACK TO SAVEPOINT " + name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func compareVersion(version1, version2 string) int {
|
||||
n, m := len(version1), len(version2)
|
||||
i, j := 0, 0
|
||||
for i < n || j < m {
|
||||
x := 0
|
||||
for ; i < n && version1[i] != '.'; i++ {
|
||||
x = x*10 + int(version1[i]-'0')
|
||||
}
|
||||
i++
|
||||
y := 0
|
||||
for ; j < m && version2[j] != '.'; j++ {
|
||||
y = y*10 + int(version2[j]-'0')
|
||||
}
|
||||
j++
|
||||
if x > y {
|
||||
return 1
|
||||
}
|
||||
if x < y {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-NOW Jinzhu <wosmvp@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# GORM SQL Server Driver
|
||||
|
||||
## USAGE
|
||||
|
||||
```go
|
||||
import (
|
||||
"gorm.io/driver/sqlserver"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// github.com/microsoft/go-mssqldb
|
||||
dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"
|
||||
db, err := gorm.Open(sqlserver.Open(dsn), &gorm.Config{})
|
||||
```
|
||||
|
||||
## Azure AD Auth
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/microsoft/go-mssqldb/azuread"
|
||||
"gorm.io/driver/sqlserver"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// github.com/microsoft/go-mssqldb
|
||||
dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"
|
||||
dialector := &sqlserver.Dialector{Config: &sqlserver.Config{DSN: dsn, DriverName: azuread.DriverName}}
|
||||
db, err := gorm.Open(dialector, &gorm.Config{})
|
||||
```
|
||||
|
||||
Checkout [https://gorm.io](https://gorm.io) for details.
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
package sqlserver
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/callbacks"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func Create(db *gorm.DB) {
|
||||
if db.Error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if db.Statement.Schema != nil && !db.Statement.Unscoped {
|
||||
for _, c := range db.Statement.Schema.CreateClauses {
|
||||
db.Statement.AddClause(c)
|
||||
}
|
||||
}
|
||||
|
||||
hasOutput := false
|
||||
if db.Statement.SQL.String() == "" {
|
||||
var (
|
||||
values = callbacks.ConvertToCreateValues(db.Statement)
|
||||
c = db.Statement.Clauses["ON CONFLICT"]
|
||||
onConflict, hasConflict = c.Expression.(clause.OnConflict)
|
||||
)
|
||||
|
||||
if hasConflict {
|
||||
if len(db.Statement.Schema.PrimaryFields) > 0 {
|
||||
columnsMap := map[string]bool{}
|
||||
for _, column := range values.Columns {
|
||||
columnsMap[column.Name] = true
|
||||
}
|
||||
|
||||
for _, field := range db.Statement.Schema.PrimaryFields {
|
||||
if _, ok := columnsMap[field.DBName]; !ok {
|
||||
hasConflict = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hasConflict = false
|
||||
}
|
||||
}
|
||||
|
||||
if hasConflict {
|
||||
hasOutput = MergeCreate(db, onConflict, values)
|
||||
} else {
|
||||
setIdentityInsert := false
|
||||
|
||||
if db.Statement.Schema != nil {
|
||||
if field := db.Statement.Schema.PrioritizedPrimaryField; field != nil && field.AutoIncrement {
|
||||
switch db.Statement.ReflectValue.Kind() {
|
||||
case reflect.Struct:
|
||||
_, isZero := field.ValueOf(db.Statement.Context, db.Statement.ReflectValue)
|
||||
setIdentityInsert = !isZero
|
||||
case reflect.Slice, reflect.Array:
|
||||
for i := 0; i < db.Statement.ReflectValue.Len(); i++ {
|
||||
obj := db.Statement.ReflectValue.Index(i)
|
||||
if reflect.Indirect(obj).Kind() == reflect.Struct {
|
||||
_, isZero := field.ValueOf(db.Statement.Context, db.Statement.ReflectValue.Index(i))
|
||||
setIdentityInsert = !isZero
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if setIdentityInsert {
|
||||
db.Statement.WriteString("SET IDENTITY_INSERT ")
|
||||
db.Statement.WriteQuoted(db.Statement.Table)
|
||||
db.Statement.WriteString(" ON;")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.Statement.AddClauseIfNotExists(clause.Insert{})
|
||||
db.Statement.Build("INSERT")
|
||||
db.Statement.WriteByte(' ')
|
||||
|
||||
db.Statement.AddClause(values)
|
||||
if values, ok := db.Statement.Clauses["VALUES"].Expression.(clause.Values); ok {
|
||||
if len(values.Columns) > 0 {
|
||||
db.Statement.WriteByte('(')
|
||||
for idx, column := range values.Columns {
|
||||
if idx > 0 {
|
||||
db.Statement.WriteByte(',')
|
||||
}
|
||||
db.Statement.WriteQuoted(column)
|
||||
}
|
||||
db.Statement.WriteByte(')')
|
||||
|
||||
hasOutput = outputInserted(db)
|
||||
|
||||
db.Statement.WriteString(" VALUES ")
|
||||
|
||||
for idx, value := range values.Values {
|
||||
if idx > 0 {
|
||||
db.Statement.WriteByte(',')
|
||||
}
|
||||
|
||||
db.Statement.WriteByte('(')
|
||||
db.Statement.AddVar(db.Statement, value...)
|
||||
db.Statement.WriteByte(')')
|
||||
}
|
||||
|
||||
db.Statement.WriteString(";")
|
||||
} else {
|
||||
db.Statement.WriteString("DEFAULT VALUES;")
|
||||
}
|
||||
}
|
||||
|
||||
if setIdentityInsert {
|
||||
db.Statement.WriteString("SET IDENTITY_INSERT ")
|
||||
db.Statement.WriteQuoted(db.Statement.Table)
|
||||
db.Statement.WriteString(" OFF;")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !db.DryRun && db.Error == nil {
|
||||
if db.Statement.Schema != nil && hasOutput {
|
||||
rows, err := db.Statement.ConnPool.QueryContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...)
|
||||
if db.AddError(err) == nil {
|
||||
defer rows.Close()
|
||||
gorm.Scan(rows, db, gorm.ScanUpdate|gorm.ScanOnConflictDoNothing)
|
||||
if db.Statement.Result != nil {
|
||||
db.Statement.Result.RowsAffected = db.RowsAffected
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...)
|
||||
if db.AddError(err) == nil {
|
||||
db.RowsAffected, _ = result.RowsAffected()
|
||||
if db.Statement.Result != nil {
|
||||
db.Statement.Result.Result = result
|
||||
db.Statement.Result.RowsAffected = db.RowsAffected
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MergeCreate(db *gorm.DB, onConflict clause.OnConflict, values clause.Values) bool {
|
||||
db.Statement.WriteString("MERGE INTO ")
|
||||
db.Statement.WriteQuoted(db.Statement.Table)
|
||||
db.Statement.WriteString(" USING (VALUES")
|
||||
for idx, value := range values.Values {
|
||||
if idx > 0 {
|
||||
db.Statement.WriteByte(',')
|
||||
}
|
||||
|
||||
db.Statement.WriteByte('(')
|
||||
db.Statement.AddVar(db.Statement, value...)
|
||||
db.Statement.WriteByte(')')
|
||||
}
|
||||
|
||||
db.Statement.WriteString(") AS excluded (")
|
||||
for idx, column := range values.Columns {
|
||||
if idx > 0 {
|
||||
db.Statement.WriteByte(',')
|
||||
}
|
||||
db.Statement.WriteQuoted(column.Name)
|
||||
}
|
||||
db.Statement.WriteString(") ON ")
|
||||
|
||||
var where clause.Where
|
||||
for _, field := range db.Statement.Schema.PrimaryFields {
|
||||
where.Exprs = append(where.Exprs, clause.Eq{
|
||||
Column: clause.Column{Table: db.Statement.Table, Name: field.DBName},
|
||||
Value: clause.Column{Table: "excluded", Name: field.DBName},
|
||||
})
|
||||
}
|
||||
where.Build(db.Statement)
|
||||
|
||||
if len(onConflict.DoUpdates) > 0 {
|
||||
db.Statement.WriteString(" WHEN MATCHED THEN UPDATE SET ")
|
||||
onConflict.DoUpdates.Build(db.Statement)
|
||||
}
|
||||
|
||||
db.Statement.WriteString(" WHEN NOT MATCHED THEN INSERT (")
|
||||
|
||||
written := false
|
||||
for _, column := range values.Columns {
|
||||
if db.Statement.Schema.PrioritizedPrimaryField == nil || !db.Statement.Schema.PrioritizedPrimaryField.AutoIncrement || db.Statement.Schema.PrioritizedPrimaryField.DBName != column.Name {
|
||||
if written {
|
||||
db.Statement.WriteByte(',')
|
||||
}
|
||||
written = true
|
||||
db.Statement.WriteQuoted(column.Name)
|
||||
}
|
||||
}
|
||||
|
||||
db.Statement.WriteString(") VALUES (")
|
||||
|
||||
written = false
|
||||
for _, column := range values.Columns {
|
||||
if db.Statement.Schema.PrioritizedPrimaryField == nil || !db.Statement.Schema.PrioritizedPrimaryField.AutoIncrement || db.Statement.Schema.PrioritizedPrimaryField.DBName != column.Name {
|
||||
if written {
|
||||
db.Statement.WriteByte(',')
|
||||
}
|
||||
written = true
|
||||
db.Statement.WriteQuoted(clause.Column{
|
||||
Table: "excluded",
|
||||
Name: column.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
db.Statement.WriteString(")")
|
||||
hasOutput := outputInserted(db)
|
||||
db.Statement.WriteString(";")
|
||||
return hasOutput
|
||||
}
|
||||
|
||||
func outputInserted(db *gorm.DB) (hasOutput bool) {
|
||||
if db.Statement.Schema != nil && len(db.Statement.Schema.FieldsWithDefaultDBValue) > 0 {
|
||||
for _, field := range db.Statement.Schema.FieldsWithDefaultDBValue {
|
||||
if hasOutput {
|
||||
db.Statement.WriteString(",")
|
||||
}
|
||||
if field.Readable {
|
||||
if !hasOutput {
|
||||
db.Statement.WriteString(" OUTPUT INSERTED.")
|
||||
hasOutput = true
|
||||
} else {
|
||||
db.Statement.WriteString(" INSERTED.")
|
||||
}
|
||||
db.Statement.AddVar(db.Statement, clause.Column{Name: field.DBName})
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasOutput
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package sqlserver
|
||||
|
||||
import (
|
||||
"github.com/microsoft/go-mssqldb"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// The error codes to map mssql errors to gorm errors, here is a reference about error codes for mssql https://learn.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors?view=sql-server-ver16
|
||||
var errCodes = map[int32]error{
|
||||
2627: gorm.ErrDuplicatedKey,
|
||||
2601: gorm.ErrDuplicatedKey,
|
||||
547: gorm.ErrForeignKeyViolated,
|
||||
}
|
||||
|
||||
type ErrMessage struct {
|
||||
Number int32 `json:"Number"`
|
||||
Message string `json:"Message"`
|
||||
}
|
||||
|
||||
// Translate it will translate the error to native gorm errors.
|
||||
func (dialector Dialector) Translate(err error) error {
|
||||
if mssqlErr, ok := err.(mssql.Error); ok {
|
||||
if translatedErr, found := errCodes[mssqlErr.Number]; found {
|
||||
return translatedErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
+601
@@ -0,0 +1,601 @@
|
||||
package sqlserver
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/migrator"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
const indexSQL = `
|
||||
SELECT
|
||||
col.name AS column_name,
|
||||
i.name AS index_name,
|
||||
i.is_unique,
|
||||
i.is_primary_key
|
||||
FROM
|
||||
sys.indexes i
|
||||
LEFT JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
|
||||
LEFT JOIN sys.all_columns col ON col.column_id = ic.column_id AND col.object_id = ic.object_id
|
||||
WHERE
|
||||
i.name IS NOT NULL
|
||||
AND i.is_unique_constraint = 0
|
||||
AND i.object_id = OBJECT_ID(?)
|
||||
`
|
||||
|
||||
type Migrator struct {
|
||||
migrator.Migrator
|
||||
}
|
||||
|
||||
func (m Migrator) GetTables() (tableList []string, err error) {
|
||||
return tableList, m.DB.Raw("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_CATALOG = ?", m.CurrentDatabase()).Scan(&tableList).Error
|
||||
}
|
||||
|
||||
func (m Migrator) CreateTable(values ...interface{}) (err error) {
|
||||
if err = m.Migrator.CreateTable(values...); err != nil {
|
||||
return
|
||||
}
|
||||
for _, value := range m.ReorderModels(values, false) {
|
||||
if err = m.RunWithValue(value, func(stmt *gorm.Statement) (err error) {
|
||||
if stmt.Schema == nil {
|
||||
return
|
||||
}
|
||||
for _, fieldName := range stmt.Schema.DBNames {
|
||||
field := stmt.Schema.FieldsByDBName[fieldName]
|
||||
if _, ok := field.TagSettings["COMMENT"]; !ok {
|
||||
continue
|
||||
}
|
||||
if err = m.setColumnComment(stmt, field, true); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m Migrator) setColumnComment(stmt *gorm.Statement, field *schema.Field, add bool) error {
|
||||
schemaName := m.getTableSchemaName(stmt.Schema)
|
||||
commentExpr := gorm.Expr(strings.ReplaceAll(field.Comment, "'", "''"))
|
||||
// add field comment
|
||||
if add {
|
||||
return m.DB.Exec(
|
||||
"EXEC sp_addextendedproperty 'MS_Description', N'?', 'SCHEMA', ?, 'TABLE', ?, 'COLUMN', ?",
|
||||
commentExpr, schemaName, stmt.Table, field.DBName,
|
||||
).Error
|
||||
}
|
||||
// update field comment
|
||||
return m.DB.Exec(
|
||||
"EXEC sp_updateextendedproperty 'MS_Description', N'?', 'SCHEMA', ?, 'TABLE', ?, 'COLUMN', ?",
|
||||
commentExpr, schemaName, stmt.Table, field.DBName,
|
||||
).Error
|
||||
}
|
||||
|
||||
func (m Migrator) getTableSchemaName(schema *schema.Schema) string {
|
||||
// return the schema name if it is explicitly provided in the table name
|
||||
// otherwise return default schema name
|
||||
schemaName := getTableSchemaName(schema)
|
||||
if schemaName == "" {
|
||||
schemaName = m.DefaultSchema()
|
||||
}
|
||||
return schemaName
|
||||
}
|
||||
|
||||
func getTableSchemaName(schema *schema.Schema) string {
|
||||
// return the schema name if it is explicitly provided in the table name
|
||||
// otherwise return a sql wildcard -> use any table_schema
|
||||
if schema == nil || !strings.Contains(schema.Table, ".") {
|
||||
return ""
|
||||
}
|
||||
_, schemaName, _ := splitFullQualifiedName(schema.Table)
|
||||
return schemaName
|
||||
}
|
||||
|
||||
func splitFullQualifiedName(name string) (string, string, string) {
|
||||
nameParts := strings.Split(name, ".")
|
||||
if len(nameParts) == 1 { // [table_name]
|
||||
return "", "", nameParts[0]
|
||||
} else if len(nameParts) == 2 { // [table_schema].[table_name]
|
||||
return "", nameParts[0], nameParts[1]
|
||||
} else if len(nameParts) == 3 { // [table_catalog].[table_schema].[table_name]
|
||||
return nameParts[0], nameParts[1], nameParts[2]
|
||||
}
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
func getFullQualifiedTableName(stmt *gorm.Statement) string {
|
||||
fullQualifiedTableName := stmt.Table
|
||||
if schemaName := getTableSchemaName(stmt.Schema); schemaName != "" {
|
||||
fullQualifiedTableName = schemaName + "." + fullQualifiedTableName
|
||||
}
|
||||
return fullQualifiedTableName
|
||||
}
|
||||
|
||||
func (m Migrator) HasTable(value interface{}) bool {
|
||||
var count int
|
||||
_ = m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
schemaName := getTableSchemaName(stmt.Schema)
|
||||
if schemaName == "" {
|
||||
schemaName = "%"
|
||||
}
|
||||
return m.DB.Raw(
|
||||
"SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = ? AND TABLE_CATALOG = ? and TABLE_SCHEMA like ? AND TABLE_TYPE = ?",
|
||||
stmt.Table, m.CurrentDatabase(), schemaName, "BASE TABLE",
|
||||
).Row().Scan(&count)
|
||||
})
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (m Migrator) DropTable(values ...interface{}) error {
|
||||
values = m.ReorderModels(values, false)
|
||||
for i := len(values) - 1; i >= 0; i-- {
|
||||
tx := m.DB.Session(&gorm.Session{})
|
||||
if err := m.RunWithValue(values[i], func(stmt *gorm.Statement) error {
|
||||
type constraint struct {
|
||||
Name string
|
||||
Parent string
|
||||
}
|
||||
var constraints []constraint
|
||||
err := tx.Raw("SELECT name, OBJECT_NAME(parent_object_id) as parent FROM sys.foreign_keys WHERE referenced_object_id = object_id(?)", getFullQualifiedTableName(stmt)).Scan(&constraints).Error
|
||||
|
||||
for _, c := range constraints {
|
||||
if err == nil {
|
||||
err = tx.Exec("ALTER TABLE ? DROP CONSTRAINT ?;", gorm.Expr(c.Parent), gorm.Expr(c.Name)).Error
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
err = tx.Exec("DROP TABLE IF EXISTS ?", clause.Table{Name: stmt.Table}).Error
|
||||
}
|
||||
|
||||
return err
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Migrator) RenameTable(oldName, newName interface{}) error {
|
||||
var oldTable, newTable string
|
||||
if v, ok := oldName.(string); ok {
|
||||
oldTable = v
|
||||
} else {
|
||||
stmt := &gorm.Statement{DB: m.DB}
|
||||
if err := stmt.Parse(oldName); err == nil {
|
||||
oldTable = stmt.Table
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if v, ok := newName.(string); ok {
|
||||
newTable = v
|
||||
} else {
|
||||
stmt := &gorm.Statement{DB: m.DB}
|
||||
if err := stmt.Parse(newName); err == nil {
|
||||
newTable = stmt.Table
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.DB.Exec(
|
||||
"sp_rename @objname = ?, @newname = ?;",
|
||||
clause.Table{Name: oldTable}, clause.Table{Name: newTable},
|
||||
).Error
|
||||
}
|
||||
|
||||
func (m Migrator) AddColumn(value interface{}, name string) error {
|
||||
if err := m.Migrator.AddColumn(value, name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) (err error) {
|
||||
if stmt.Schema != nil {
|
||||
if field := stmt.Schema.LookUpField(name); field != nil {
|
||||
if _, ok := field.TagSettings["COMMENT"]; !ok {
|
||||
return
|
||||
}
|
||||
if err = m.setColumnComment(stmt, field, true); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func (m Migrator) HasColumn(value interface{}, field string) bool {
|
||||
var count int64
|
||||
_ = m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
currentDatabase := m.DB.Migrator().CurrentDatabase()
|
||||
name := field
|
||||
if stmt.Schema != nil {
|
||||
if field := stmt.Schema.LookUpField(field); field != nil {
|
||||
name = field.DBName
|
||||
}
|
||||
}
|
||||
|
||||
return m.DB.Raw(
|
||||
"SELECT count(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_CATALOG = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?",
|
||||
currentDatabase, stmt.Table, name,
|
||||
).Row().Scan(&count)
|
||||
})
|
||||
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (m Migrator) AlterColumn(value interface{}, field string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
if field := stmt.Schema.LookUpField(field); field != nil {
|
||||
fieldType := clause.Expr{SQL: m.DataTypeOf(field)}
|
||||
if field.NotNull {
|
||||
fieldType.SQL += " NOT NULL"
|
||||
} else {
|
||||
fieldType.SQL += " NULL"
|
||||
}
|
||||
|
||||
return m.DB.Exec(
|
||||
"ALTER TABLE ? ALTER COLUMN ? ?",
|
||||
clause.Table{Name: getFullQualifiedTableName(stmt)}, clause.Column{Name: field.DBName}, fieldType,
|
||||
).Error
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to look up field with name: %s", field)
|
||||
})
|
||||
}
|
||||
|
||||
func (m Migrator) RenameColumn(value interface{}, oldName, newName string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
if field := stmt.Schema.LookUpField(oldName); field != nil {
|
||||
oldName = field.DBName
|
||||
}
|
||||
if field := stmt.Schema.LookUpField(newName); field != nil {
|
||||
newName = field.DBName
|
||||
}
|
||||
}
|
||||
|
||||
return m.DB.Exec(
|
||||
"sp_rename @objname = ?, @newname = ?, @objtype = 'COLUMN';",
|
||||
fmt.Sprintf("%s.%s", stmt.Table, oldName), clause.Column{Name: newName},
|
||||
).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (m Migrator) GetColumnComment(stmt *gorm.Statement, fieldDBName string) (comment sql.NullString) {
|
||||
queryTx := m.DB.Session(&gorm.Session{Logger: m.DB.Logger.LogMode(logger.Warn)})
|
||||
if m.DB.DryRun {
|
||||
queryTx.DryRun = false
|
||||
}
|
||||
queryTx.Raw("SELECT value FROM [?].sys.fn_listextendedproperty('MS_Description', 'SCHEMA', ?, 'TABLE', ?, 'COLUMN', ?)",
|
||||
gorm.Expr(m.CurrentDatabase()), m.getTableSchemaName(stmt.Schema), stmt.Table, fieldDBName).Scan(&comment)
|
||||
return
|
||||
}
|
||||
|
||||
func (m Migrator) MigrateColumn(value interface{}, field *schema.Field, columnType gorm.ColumnType) error {
|
||||
if err := m.Migrator.MigrateColumn(value, field, columnType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) (err error) {
|
||||
comment := m.GetColumnComment(stmt, field.DBName)
|
||||
if field.Comment != comment.String {
|
||||
if comment.Valid {
|
||||
err = m.setColumnComment(stmt, field, false)
|
||||
} else {
|
||||
err = m.setColumnComment(stmt, field, true)
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
var defaultValueTrimRegexp = regexp.MustCompile("^\\('?([^']*)'?\\)$")
|
||||
|
||||
// ColumnTypes return columnTypes []gorm.ColumnType and execErr error
|
||||
func (m Migrator) ColumnTypes(value interface{}) ([]gorm.ColumnType, error) {
|
||||
columnTypes := make([]gorm.ColumnType, 0)
|
||||
execErr := m.RunWithValue(value, func(stmt *gorm.Statement) (err error) {
|
||||
rows, err := m.DB.Session(&gorm.Session{}).Table(getFullQualifiedTableName(stmt)).Limit(1).Rows()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rawColumnTypes, _ := rows.ColumnTypes()
|
||||
_ = rows.Close()
|
||||
|
||||
{
|
||||
_, schemaName, tableName := splitFullQualifiedName(stmt.Table)
|
||||
|
||||
query := strings.TrimSpace(`
|
||||
SELECT COLUMN_NAME, DATA_TYPE, COLUMN_DEFAULT, c.IS_NULLABLE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_PRECISION_RADIX, NUMERIC_SCALE, DATETIME_PRECISION, AUTO_INCREMENT = c2.is_identity
|
||||
FROM INFORMATION_SCHEMA.COLUMNS c
|
||||
LEFT JOIN sys.tables t ON c.TABLE_NAME = t.[name]
|
||||
LEFT JOIN sys.columns c2 ON t.object_id = c2.object_id AND c2.[name] = c.COLUMN_NAME
|
||||
WHERE TABLE_CATALOG = ? AND TABLE_NAME = ?`)
|
||||
|
||||
queryParameters := []interface{}{m.CurrentDatabase(), tableName}
|
||||
|
||||
if schemaName != "" {
|
||||
query += " AND TABLE_SCHEMA = ?"
|
||||
queryParameters = append(queryParameters, schemaName)
|
||||
}
|
||||
|
||||
var (
|
||||
columnTypeSQL = query
|
||||
columns, rowErr = m.DB.Raw(columnTypeSQL, queryParameters...).Rows()
|
||||
)
|
||||
|
||||
if rowErr != nil {
|
||||
return rowErr
|
||||
}
|
||||
|
||||
for columns.Next() {
|
||||
var (
|
||||
column = migrator.ColumnType{
|
||||
PrimaryKeyValue: sql.NullBool{Valid: true},
|
||||
UniqueValue: sql.NullBool{Valid: true},
|
||||
}
|
||||
datetimePrecision sql.NullInt64
|
||||
radixValue sql.NullInt64
|
||||
nullableValue sql.NullString
|
||||
autoIncrementValue sql.NullBool
|
||||
values = []interface{}{
|
||||
&column.NameValue, &column.ColumnTypeValue, &column.DefaultValueValue, &nullableValue, &column.LengthValue, &column.DecimalSizeValue, &radixValue, &column.ScaleValue, &datetimePrecision, &autoIncrementValue,
|
||||
}
|
||||
)
|
||||
|
||||
if scanErr := columns.Scan(values...); scanErr != nil {
|
||||
return scanErr
|
||||
}
|
||||
|
||||
if nullableValue.Valid {
|
||||
column.NullableValue = sql.NullBool{Bool: strings.EqualFold(nullableValue.String, "YES"), Valid: true}
|
||||
}
|
||||
|
||||
if datetimePrecision.Valid {
|
||||
column.DecimalSizeValue = datetimePrecision
|
||||
}
|
||||
|
||||
if autoIncrementValue.Valid && autoIncrementValue.Bool {
|
||||
column.AutoIncrementValue = autoIncrementValue
|
||||
}
|
||||
|
||||
if column.DefaultValueValue.Valid {
|
||||
matches := defaultValueTrimRegexp.FindStringSubmatch(column.DefaultValueValue.String)
|
||||
for len(matches) > 1 {
|
||||
column.DefaultValueValue.String = matches[1]
|
||||
matches = defaultValueTrimRegexp.FindStringSubmatch(column.DefaultValueValue.String)
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range rawColumnTypes {
|
||||
if c.Name() == column.NameValue.String {
|
||||
column.SQLColumnType = c
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
columnTypes = append(columnTypes, column)
|
||||
}
|
||||
|
||||
_ = columns.Close()
|
||||
}
|
||||
|
||||
{
|
||||
_, schemaName, tableName := splitFullQualifiedName(stmt.Table)
|
||||
query := "SELECT c.COLUMN_NAME, t.CONSTRAINT_TYPE FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS t JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE c ON c.CONSTRAINT_NAME=t.CONSTRAINT_NAME WHERE t.CONSTRAINT_TYPE IN ('PRIMARY KEY', 'UNIQUE') AND c.TABLE_CATALOG = ? AND c.TABLE_NAME = ?"
|
||||
|
||||
queryParameters := []interface{}{m.CurrentDatabase(), tableName}
|
||||
|
||||
if schemaName != "" {
|
||||
query += " AND c.TABLE_SCHEMA = ?"
|
||||
queryParameters = append(queryParameters, schemaName)
|
||||
}
|
||||
|
||||
columnTypeRows, err := m.DB.Raw(query, queryParameters...).Rows()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for columnTypeRows.Next() {
|
||||
var name, columnType string
|
||||
_ = columnTypeRows.Scan(&name, &columnType)
|
||||
for idx, c := range columnTypes {
|
||||
mc := c.(migrator.ColumnType)
|
||||
if mc.NameValue.String == name {
|
||||
switch columnType {
|
||||
case "PRIMARY KEY":
|
||||
mc.PrimaryKeyValue = sql.NullBool{Bool: true, Valid: true}
|
||||
case "UNIQUE":
|
||||
mc.UniqueValue = sql.NullBool{Bool: true, Valid: true}
|
||||
}
|
||||
columnTypes[idx] = mc
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = columnTypeRows.Close()
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
|
||||
return columnTypes, execErr
|
||||
}
|
||||
|
||||
func (m Migrator) CreateView(name string, option gorm.ViewOption) error {
|
||||
if option.Query == nil {
|
||||
return gorm.ErrSubQueryRequired
|
||||
}
|
||||
|
||||
sql := new(strings.Builder)
|
||||
sql.WriteString("CREATE ")
|
||||
if option.Replace {
|
||||
sql.WriteString("OR ALTER ")
|
||||
}
|
||||
sql.WriteString("VIEW ")
|
||||
m.QuoteTo(sql, name)
|
||||
sql.WriteString(" AS ")
|
||||
|
||||
m.DB.Statement.AddVar(sql, option.Query)
|
||||
|
||||
if option.CheckOption != "" {
|
||||
sql.WriteString(" ")
|
||||
sql.WriteString(option.CheckOption)
|
||||
}
|
||||
return m.DB.Exec(m.Explain(sql.String(), m.DB.Statement.Vars...)).Error
|
||||
}
|
||||
|
||||
func (m Migrator) CreateIndex(value interface{}, name string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
var idx *schema.Index
|
||||
if stmt.Schema != nil {
|
||||
idx = stmt.Schema.LookIndex(name)
|
||||
}
|
||||
if idx == nil {
|
||||
return fmt.Errorf("failed to create index with name %s", name)
|
||||
}
|
||||
|
||||
opts := m.BuildIndexOptions(idx.Fields, stmt)
|
||||
values := []interface{}{clause.Column{Name: idx.Name}, m.CurrentTable(stmt), opts}
|
||||
|
||||
createIndexSQL := "CREATE "
|
||||
if idx.Class != "" {
|
||||
createIndexSQL += idx.Class + " "
|
||||
}
|
||||
createIndexSQL += "INDEX ? ON ??"
|
||||
|
||||
if idx.Where != "" {
|
||||
createIndexSQL += " WHERE " + idx.Where
|
||||
}
|
||||
|
||||
if idx.Option != "" {
|
||||
createIndexSQL += " " + idx.Option
|
||||
}
|
||||
|
||||
return m.DB.Exec(createIndexSQL, values...).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (m Migrator) HasIndex(value interface{}, name string) bool {
|
||||
var count int
|
||||
_ = m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
if idx := stmt.Schema.LookIndex(name); idx != nil {
|
||||
name = idx.Name
|
||||
}
|
||||
}
|
||||
|
||||
return m.DB.Raw(
|
||||
"SELECT count(*) FROM sys.indexes WHERE name=? AND object_id=OBJECT_ID(?)",
|
||||
name, getFullQualifiedTableName(stmt),
|
||||
).Row().Scan(&count)
|
||||
})
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (m Migrator) RenameIndex(value interface{}, oldName, newName string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
|
||||
return m.DB.Exec(
|
||||
"sp_rename @objname = ?, @newname = ?, @objtype = 'INDEX';",
|
||||
fmt.Sprintf("%s.%s", stmt.Table, oldName), clause.Column{Name: newName},
|
||||
).Error
|
||||
})
|
||||
}
|
||||
|
||||
type Index struct {
|
||||
ColumnName string `gorm:"column:column_name"`
|
||||
IndexName string `gorm:"column:index_name"`
|
||||
IsUnique sql.NullBool `gorm:"column:is_unique"`
|
||||
IsPrimaryKey sql.NullBool `gorm:"column:is_primary_key"`
|
||||
}
|
||||
|
||||
func (m Migrator) GetIndexes(value interface{}) ([]gorm.Index, error) {
|
||||
indexes := make([]gorm.Index, 0)
|
||||
err := m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
result := make([]*Index, 0)
|
||||
if err := m.DB.Raw(indexSQL, stmt.Table).Scan(&result).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
indexMap := make(map[string]*migrator.Index)
|
||||
for _, r := range result {
|
||||
idx, ok := indexMap[r.IndexName]
|
||||
if !ok {
|
||||
idx = &migrator.Index{
|
||||
TableName: stmt.Table,
|
||||
NameValue: r.IndexName,
|
||||
ColumnList: nil,
|
||||
PrimaryKeyValue: r.IsPrimaryKey,
|
||||
UniqueValue: r.IsUnique,
|
||||
}
|
||||
}
|
||||
idx.ColumnList = append(idx.ColumnList, r.ColumnName)
|
||||
indexMap[r.IndexName] = idx
|
||||
}
|
||||
for _, idx := range indexMap {
|
||||
indexes = append(indexes, idx)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return indexes, err
|
||||
}
|
||||
|
||||
func (m Migrator) HasConstraint(value interface{}, name string) bool {
|
||||
var count int64
|
||||
_ = m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
|
||||
if constraint != nil {
|
||||
name = constraint.GetName()
|
||||
}
|
||||
|
||||
tableCatalog, tableSchema, tableName := splitFullQualifiedName(table)
|
||||
if tableCatalog == "" {
|
||||
tableCatalog = m.CurrentDatabase()
|
||||
}
|
||||
if tableSchema == "" {
|
||||
tableSchema = "%"
|
||||
}
|
||||
|
||||
return m.DB.Raw(
|
||||
`SELECT count(*) FROM (
|
||||
SELECT C.name, T.name as table_name FROM sys.check_constraints as C
|
||||
INNER JOIN sys.tables as T on C.parent_object_id=T.object_id
|
||||
INNER JOIN INFORMATION_SCHEMA.TABLES as I on I.TABLE_NAME = T.name
|
||||
WHERE C.name = ? AND I.TABLE_NAME = ? AND I.TABLE_SCHEMA like ? AND I.TABLE_CATALOG = ?
|
||||
UNION
|
||||
SELECT FK.name, T.name as table_name FROM sys.foreign_keys as FK
|
||||
INNER JOIN sys.tables as T on FK.parent_object_id=T.object_id
|
||||
INNER JOIN INFORMATION_SCHEMA.TABLES as I on I.TABLE_NAME = T.name
|
||||
WHERE FK.name = ? AND I.TABLE_NAME = ? AND I.TABLE_SCHEMA like ? AND I.TABLE_CATALOG = ?
|
||||
) as constraints;`,
|
||||
name, tableName, tableSchema, tableCatalog,
|
||||
name, tableName, tableSchema, tableCatalog,
|
||||
).Row().Scan(&count)
|
||||
})
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (m Migrator) CurrentDatabase() (name string) {
|
||||
_ = m.DB.Raw("SELECT DB_NAME() AS [Current Database]").Row().Scan(&name)
|
||||
return
|
||||
}
|
||||
|
||||
func (m Migrator) DefaultSchema() (name string) {
|
||||
_ = m.DB.Raw("SELECT SCHEMA_NAME() AS [Default Schema]").Row().Scan(&name)
|
||||
return
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
package sqlserver
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
_ "github.com/microsoft/go-mssqldb"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/callbacks"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/migrator"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DriverName string
|
||||
DSN string
|
||||
DefaultStringSize int
|
||||
Conn gorm.ConnPool
|
||||
}
|
||||
|
||||
type Dialector struct {
|
||||
*Config
|
||||
}
|
||||
|
||||
func (dialector Dialector) Name() string {
|
||||
return "sqlserver"
|
||||
}
|
||||
|
||||
func Open(dsn string) gorm.Dialector {
|
||||
return &Dialector{Config: &Config{DSN: dsn}}
|
||||
}
|
||||
|
||||
func New(config Config) gorm.Dialector {
|
||||
return &Dialector{Config: &config}
|
||||
}
|
||||
|
||||
func (dialector Dialector) Initialize(db *gorm.DB) (err error) {
|
||||
// register callbacks
|
||||
callbacks.RegisterDefaultCallbacks(db, &callbacks.Config{
|
||||
CreateClauses: []string{"INSERT", "VALUES", "ON CONFLICT"},
|
||||
QueryClauses: []string{"SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "LIMIT", "FOR"},
|
||||
UpdateClauses: []string{"UPDATE", "SET", "RETURNING", "FROM", "WHERE"},
|
||||
DeleteClauses: []string{"DELETE", "FROM", "RETURNING", "WHERE"},
|
||||
})
|
||||
db.Callback().Create().Replace("gorm:create", Create)
|
||||
db.Callback().Update().Replace("gorm:update", Update)
|
||||
|
||||
if dialector.DriverName == "" {
|
||||
dialector.DriverName = "sqlserver"
|
||||
}
|
||||
|
||||
if dialector.Conn != nil {
|
||||
db.ConnPool = dialector.Conn
|
||||
} else {
|
||||
db.ConnPool, err = sql.Open(dialector.DriverName, dialector.DSN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range dialector.ClauseBuilders() {
|
||||
db.ClauseBuilders[k] = v
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (dialector Dialector) ClauseBuilders() map[string]clause.ClauseBuilder {
|
||||
return map[string]clause.ClauseBuilder{
|
||||
"LIMIT": func(c clause.Clause, builder clause.Builder) {
|
||||
if limit, ok := c.Expression.(clause.Limit); ok {
|
||||
if stmt, ok := builder.(*gorm.Statement); ok {
|
||||
if _, ok := stmt.Clauses["ORDER BY"]; !ok {
|
||||
if stmt.Schema != nil && stmt.Schema.PrioritizedPrimaryField != nil {
|
||||
builder.WriteString("ORDER BY ")
|
||||
builder.WriteQuoted(stmt.Schema.PrioritizedPrimaryField.DBName)
|
||||
builder.WriteByte(' ')
|
||||
} else {
|
||||
builder.WriteString("ORDER BY (SELECT NULL) ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if limit.Offset > 0 {
|
||||
builder.WriteString("OFFSET ")
|
||||
builder.WriteString(strconv.Itoa(limit.Offset))
|
||||
builder.WriteString(" ROWS")
|
||||
}
|
||||
|
||||
if limit.Limit != nil && *limit.Limit >= 0 {
|
||||
if limit.Offset == 0 {
|
||||
builder.WriteString("OFFSET 0 ROW")
|
||||
}
|
||||
builder.WriteString(" FETCH NEXT ")
|
||||
builder.WriteString(strconv.Itoa(*limit.Limit))
|
||||
builder.WriteString(" ROWS ONLY")
|
||||
}
|
||||
}
|
||||
},
|
||||
"RETURNING": func(c clause.Clause, builder clause.Builder) {
|
||||
if returning, ok := c.Expression.(clause.Returning); ok {
|
||||
if stmt, ok := builder.(*gorm.Statement); ok {
|
||||
var outputTable string
|
||||
if _, ok := stmt.Clauses["UPDATE"]; ok {
|
||||
outputTable = "INSERTED"
|
||||
} else if _, ok := stmt.Clauses["DELETE"]; ok {
|
||||
outputTable = "DELETED"
|
||||
}
|
||||
|
||||
if outputTable != "" {
|
||||
stmt.WriteString("OUTPUT ")
|
||||
|
||||
if len(returning.Columns) > 0 {
|
||||
columns := []clause.Column{}
|
||||
for _, column := range returning.Columns {
|
||||
column.Table = outputTable
|
||||
columns = append(columns, column)
|
||||
}
|
||||
returning.Columns = columns
|
||||
returning.Build(stmt)
|
||||
} else {
|
||||
stmt.WriteString(outputTable + ".*")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (dialector Dialector) DefaultValueOf(field *schema.Field) clause.Expression {
|
||||
return clause.Expr{SQL: "NULL"}
|
||||
}
|
||||
|
||||
func (dialector Dialector) Migrator(db *gorm.DB) gorm.Migrator {
|
||||
return Migrator{migrator.Migrator{Config: migrator.Config{
|
||||
DB: db,
|
||||
Dialector: dialector,
|
||||
CreateIndexAfterCreateTable: true,
|
||||
}}}
|
||||
}
|
||||
|
||||
func (dialector Dialector) BindVarTo(writer clause.Writer, stmt *gorm.Statement, v interface{}) {
|
||||
writer.WriteString("@p")
|
||||
writer.WriteString(strconv.Itoa(len(stmt.Vars)))
|
||||
}
|
||||
|
||||
func (dialector Dialector) QuoteTo(writer clause.Writer, str string) {
|
||||
writer.WriteByte('"')
|
||||
if strings.Contains(str, ".") {
|
||||
for idx, str := range strings.Split(str, ".") {
|
||||
if idx > 0 {
|
||||
writer.WriteString(`."`)
|
||||
}
|
||||
writer.WriteString(str)
|
||||
writer.WriteByte('"')
|
||||
}
|
||||
} else {
|
||||
writer.WriteString(str)
|
||||
writer.WriteByte('"')
|
||||
}
|
||||
}
|
||||
|
||||
var numericPlaceholder = regexp.MustCompile("@p(\\d+)")
|
||||
|
||||
func (dialector Dialector) Explain(sql string, vars ...interface{}) string {
|
||||
for idx, v := range vars {
|
||||
if b, ok := v.(bool); ok {
|
||||
if b {
|
||||
vars[idx] = 1
|
||||
} else {
|
||||
vars[idx] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return logger.ExplainSQL(sql, numericPlaceholder, `'`, vars...)
|
||||
}
|
||||
|
||||
func (dialector Dialector) DataTypeOf(field *schema.Field) string {
|
||||
switch field.DataType {
|
||||
case schema.Bool:
|
||||
return "bit"
|
||||
case schema.Int, schema.Uint:
|
||||
var sqlType string
|
||||
switch {
|
||||
case field.Size < 16:
|
||||
sqlType = "smallint"
|
||||
case field.Size < 31:
|
||||
sqlType = "int"
|
||||
default:
|
||||
sqlType = "bigint"
|
||||
}
|
||||
|
||||
if field.AutoIncrement {
|
||||
return sqlType + " IDENTITY(1,1)"
|
||||
}
|
||||
return sqlType
|
||||
case schema.Float:
|
||||
if field.Precision > 0 {
|
||||
if field.Scale > 0 {
|
||||
return fmt.Sprintf("decimal(%d, %d)", field.Precision, field.Scale)
|
||||
}
|
||||
return fmt.Sprintf("decimal(%d)", field.Precision)
|
||||
}
|
||||
return "float"
|
||||
case schema.String:
|
||||
size := field.Size
|
||||
hasIndex := field.TagSettings["INDEX"] != "" || field.TagSettings["UNIQUE"] != ""
|
||||
if (field.PrimaryKey || hasIndex) && size == 0 {
|
||||
if dialector.DefaultStringSize > 0 {
|
||||
size = dialector.DefaultStringSize
|
||||
} else {
|
||||
size = 256
|
||||
}
|
||||
}
|
||||
if size > 0 && size <= 4000 {
|
||||
return fmt.Sprintf("nvarchar(%d)", size)
|
||||
}
|
||||
return "nvarchar(MAX)"
|
||||
case schema.Time:
|
||||
if field.Precision > 0 {
|
||||
return fmt.Sprintf("datetimeoffset(%d)", field.Precision)
|
||||
}
|
||||
return "datetimeoffset"
|
||||
case schema.Bytes:
|
||||
return "varbinary(MAX)"
|
||||
}
|
||||
|
||||
return string(field.DataType)
|
||||
}
|
||||
|
||||
func (dialectopr Dialector) SavePoint(tx *gorm.DB, name string) error {
|
||||
tx.Exec("SAVE TRANSACTION " + name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dialectopr Dialector) RollbackTo(tx *gorm.DB, name string) error {
|
||||
tx.Exec("ROLLBACK TRANSACTION " + name)
|
||||
return nil
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package sqlserver
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/callbacks"
|
||||
)
|
||||
|
||||
var updateFunc = callbacks.Update(&callbacks.Config{
|
||||
UpdateClauses: []string{"UPDATE", "SET", "RETURNING", "FROM", "WHERE"},
|
||||
})
|
||||
|
||||
func Update(db *gorm.DB) {
|
||||
if db.Statement.Schema != nil && db.Statement.Schema.PrioritizedPrimaryField != nil && db.Statement.Schema.PrioritizedPrimaryField.AutoIncrement {
|
||||
db.Statement.Omits = append(db.Statement.Omits, db.Statement.Schema.PrioritizedPrimaryField.DBName)
|
||||
}
|
||||
|
||||
updateFunc(db)
|
||||
}
|
||||
Reference in New Issue
Block a user