feat(database): 添加 ClickHouse 数据库驱动支持
- 实现了完整的 ClickHouse 驱动,包括连接、查询、插入、更新、删除等基本操作 - 添加了 ClickHouse 特有的数据类型转换和 SQL 过滤功能 - 实现了表结构查询和字段信息获取功能 - 添加了 Ping 检查和错误处理机制 - 增加了对 UPDATE 和 DELETE 语句的语法转换以适配 ClickHouse - 添加了批量插入操作的支持 - 新增了 14 个测试用例文件用于验证数据库相关功能
This commit is contained in:
@@ -4,39 +4,39 @@
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
// Package pgsql implements database.Driver for PostgreSQL database.
|
||||
// Package pgsql implements database.Driver, which supports operations for database PostgreSQL.
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/database"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
// Driver is the driver for PostgreSQL database.
|
||||
// Driver is the driver for postgresql database.
|
||||
type Driver struct {
|
||||
*database.Core
|
||||
}
|
||||
|
||||
const (
|
||||
quoteChar = `"`
|
||||
internalPrimaryKeyInCtx gctx.StrKey = "primary_key"
|
||||
defaultSchema string = "public"
|
||||
quoteChar string = `"`
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := database.Register("pgsql", New()); err != nil {
|
||||
if err := database.Register(`pgsql`, New()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// New creates and returns a driver that implements database.Driver for PostgreSQL.
|
||||
// New create and returns a driver that implements database.Driver, which supports operations for PostgreSql.
|
||||
func New() database.Driver {
|
||||
return &Driver{}
|
||||
}
|
||||
|
||||
// New creates and returns a database object for PostgreSQL.
|
||||
// New creates and returns a database object for postgresql.
|
||||
// It implements the interface of database.Driver for extra database driver installation.
|
||||
func (d *Driver) New(core *database.Core, node *database.ConfigNode) (database.DB, error) {
|
||||
return &Driver{
|
||||
@@ -44,28 +44,7 @@ func (d *Driver) New(core *database.Core, node *database.ConfigNode) (database.D
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetChars returns the security char for PostgreSQL database.
|
||||
// GetChars returns the security char for this type of database.
|
||||
func (d *Driver) GetChars() (charLeft string, charRight string) {
|
||||
return quoteChar, quoteChar
|
||||
}
|
||||
|
||||
// Open creates and returns an underlying sql.DB object for PostgreSQL.
|
||||
func (d *Driver) Open(config *database.ConfigNode) (*sql.DB, error) {
|
||||
var (
|
||||
source string
|
||||
username = config.User
|
||||
password = config.Pass
|
||||
host = config.Host
|
||||
port = config.Port
|
||||
dbName = config.Name
|
||||
)
|
||||
|
||||
source = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||
host, port, username, password, dbName)
|
||||
|
||||
if config.Extra != "" {
|
||||
source += " " + config.Extra
|
||||
}
|
||||
|
||||
return sql.Open("postgres", source)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/lib/pq"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/database"
|
||||
"github.com/gogf/gf/v2/text/gregex"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// ConvertValueForField converts value to database acceptable value.
|
||||
func (d *Driver) ConvertValueForField(ctx context.Context, fieldType string, fieldValue any) (any, error) {
|
||||
if fieldValue == nil {
|
||||
return d.Core.ConvertValueForField(ctx, fieldType, fieldValue)
|
||||
}
|
||||
|
||||
var fieldValueKind = reflect.TypeOf(fieldValue).Kind()
|
||||
|
||||
if fieldValueKind == reflect.Slice {
|
||||
// For bytea type, pass []byte directly without any conversion.
|
||||
if _, ok := fieldValue.([]byte); ok && gstr.Contains(fieldType, "bytea") {
|
||||
return d.Core.ConvertValueForField(ctx, fieldType, fieldValue)
|
||||
}
|
||||
// For pgsql, json or jsonb require '[]'
|
||||
if !gstr.Contains(fieldType, "json") {
|
||||
fieldValue = gstr.ReplaceByMap(gconv.String(fieldValue),
|
||||
map[string]string{
|
||||
"[": "{",
|
||||
"]": "}",
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
return d.Core.ConvertValueForField(ctx, fieldType, fieldValue)
|
||||
}
|
||||
|
||||
// CheckLocalTypeForField checks and returns corresponding local golang type for given db type.
|
||||
// The parameter `fieldType` is in lower case, like:
|
||||
// `int2`, `int4`, `int8`, `_int2`, `_int4`, `_int8`, `_float4`, `_float8`, etc.
|
||||
//
|
||||
// PostgreSQL type mapping:
|
||||
//
|
||||
// | PostgreSQL Type | Local Go Type |
|
||||
// |------------------------------|---------------|
|
||||
// | int2, int4 | int |
|
||||
// | int8 | int64 |
|
||||
// | uuid | uuid.UUID |
|
||||
// | _int2, _int4 | []int32 | // Note: pq package does not provide Int16Array; int32 is used for compatibility
|
||||
// | _int8 | []int64 |
|
||||
// | _float4 | []float32 |
|
||||
// | _float8 | []float64 |
|
||||
// | _bool | []bool |
|
||||
// | _varchar, _text | []string |
|
||||
// | _char, _bpchar | []string |
|
||||
// | _numeric, _decimal, _money | []float64 |
|
||||
// | bytea | []byte |
|
||||
// | _bytea | [][]byte |
|
||||
// | _uuid | []uuid.UUID |
|
||||
func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, fieldValue any) (database.LocalType, error) {
|
||||
var typeName string
|
||||
match, _ := gregex.MatchString(`(.+?)\((.+)\)`, fieldType)
|
||||
if len(match) == 3 {
|
||||
typeName = gstr.Trim(match[1])
|
||||
} else {
|
||||
typeName = fieldType
|
||||
}
|
||||
typeName = strings.ToLower(typeName)
|
||||
switch typeName {
|
||||
case "int2", "int4":
|
||||
return database.LocalTypeInt, nil
|
||||
|
||||
case "int8":
|
||||
return database.LocalTypeInt64, nil
|
||||
|
||||
case "uuid":
|
||||
return database.LocalTypeUUID, nil
|
||||
|
||||
case "_int2", "_int4":
|
||||
return database.LocalTypeInt32Slice, nil
|
||||
|
||||
case "_int8":
|
||||
return database.LocalTypeInt64Slice, nil
|
||||
|
||||
case "_float4":
|
||||
return database.LocalTypeFloat32Slice, nil
|
||||
|
||||
case "_float8":
|
||||
return database.LocalTypeFloat64Slice, nil
|
||||
|
||||
case "_bool":
|
||||
return database.LocalTypeBoolSlice, nil
|
||||
|
||||
case "_varchar", "_text", "_char", "_bpchar":
|
||||
return database.LocalTypeStringSlice, nil
|
||||
|
||||
case "_uuid":
|
||||
return database.LocalTypeUUIDSlice, nil
|
||||
|
||||
case "_numeric", "_decimal", "_money":
|
||||
return database.LocalTypeFloat64Slice, nil
|
||||
|
||||
case "bytea":
|
||||
return database.LocalTypeBytes, nil
|
||||
|
||||
case "_bytea":
|
||||
return database.LocalTypeBytesSlice, nil
|
||||
|
||||
default:
|
||||
return d.Core.CheckLocalTypeForField(ctx, fieldType, fieldValue)
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertValueForLocal converts value to local Golang type of value according field type name from database.
|
||||
// The parameter `fieldType` is in lower case, like:
|
||||
// `int2`, `int4`, `int8`, `_int2`, `_int4`, `_int8`, `uuid`, `_uuid`, etc.
|
||||
//
|
||||
// See: https://www.postgresql.org/docs/current/datatype.html
|
||||
//
|
||||
// PostgreSQL type mapping:
|
||||
//
|
||||
// | PostgreSQL Type | SQL Type | pq Type | Go Type |
|
||||
// |-----------------|--------------------------------|-----------------|-------------|
|
||||
// | int2 | int2, smallint | - | int |
|
||||
// | int4 | int4, integer | - | int |
|
||||
// | int8 | int8, bigint, bigserial | - | int64 |
|
||||
// | uuid | uuid | - | uuid.UUID |
|
||||
// | _int2 | int2[], smallint[] | pq.Int32Array | []int32 |
|
||||
// | _int4 | int4[], integer[] | pq.Int32Array | []int32 |
|
||||
// | _int8 | int8[], bigint[] | pq.Int64Array | []int64 |
|
||||
// | _float4 | float4[], real[] | pq.Float32Array | []float32 |
|
||||
// | _float8 | float8[], double precision[] | pq.Float64Array | []float64 |
|
||||
// | _bool | boolean[], bool[] | pq.BoolArray | []bool |
|
||||
// | _varchar | varchar[], character varying[] | pq.StringArray | []string |
|
||||
// | _text | text[] | pq.StringArray | []string |
|
||||
// | _char, _bpchar | char[], character[] | pq.StringArray | []string |
|
||||
// | _numeric | numeric[] | pq.Float64Array | []float64 |
|
||||
// | _decimal | decimal[] | pq.Float64Array | []float64 |
|
||||
// | _money | money[] | pq.Float64Array | []float64 |
|
||||
// | bytea | bytea | - | []byte |
|
||||
// | _bytea | bytea[] | pq.ByteaArray | [][]byte |
|
||||
// | _uuid | uuid[] | pq.StringArray | []uuid.UUID |
|
||||
//
|
||||
// Note: PostgreSQL also supports these array types but they are not yet mapped:
|
||||
// - _date (date[]), _timestamp (timestamp[]), _timestamptz (timestamptz[])
|
||||
// - _jsonb (jsonb[]), _json (json[])
|
||||
func (d *Driver) ConvertValueForLocal(ctx context.Context, fieldType string, fieldValue any) (any, error) {
|
||||
typeName, _ := gregex.ReplaceString(`\(.+\)`, "", fieldType)
|
||||
typeName = strings.ToLower(typeName)
|
||||
|
||||
// Basic types are mostly handled by Core layer; handle array types and special-case bytea here.
|
||||
switch typeName {
|
||||
|
||||
// []byte
|
||||
case "bytea":
|
||||
if v, ok := fieldValue.([]byte); ok {
|
||||
return v, nil
|
||||
}
|
||||
return fieldValue, nil
|
||||
|
||||
// []int32
|
||||
case "_int2", "_int4":
|
||||
var result pq.Int32Array
|
||||
if err := result.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []int32(result), nil
|
||||
|
||||
// []int64
|
||||
case "_int8":
|
||||
var result pq.Int64Array
|
||||
if err := result.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []int64(result), nil
|
||||
|
||||
// []float32
|
||||
case "_float4":
|
||||
var result pq.Float32Array
|
||||
if err := result.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []float32(result), nil
|
||||
|
||||
// []float64
|
||||
case "_float8":
|
||||
var result pq.Float64Array
|
||||
if err := result.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []float64(result), nil
|
||||
|
||||
// []bool
|
||||
case "_bool":
|
||||
var result pq.BoolArray
|
||||
if err := result.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []bool(result), nil
|
||||
|
||||
// []string
|
||||
case "_varchar", "_text", "_char", "_bpchar":
|
||||
var result pq.StringArray
|
||||
if err := result.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []string(result), nil
|
||||
|
||||
// uuid.UUID
|
||||
case "uuid":
|
||||
var uuidStr string
|
||||
switch v := fieldValue.(type) {
|
||||
case []byte:
|
||||
uuidStr = string(v)
|
||||
case string:
|
||||
uuidStr = v
|
||||
default:
|
||||
uuidStr = gconv.String(fieldValue)
|
||||
}
|
||||
result, err := uuid.Parse(uuidStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
|
||||
// []uuid.UUID
|
||||
case "_uuid":
|
||||
var strArray pq.StringArray
|
||||
if err := strArray.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]uuid.UUID, len(strArray))
|
||||
for i, s := range strArray {
|
||||
parsed, err := uuid.Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[i] = parsed
|
||||
}
|
||||
return result, nil
|
||||
|
||||
// []float64
|
||||
case "_numeric", "_decimal", "_money":
|
||||
var result pq.Float64Array
|
||||
if err := result.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []float64(result), nil
|
||||
|
||||
// [][]byte
|
||||
case "_bytea":
|
||||
var result pq.ByteaArray
|
||||
if err := result.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return [][]byte(result), nil
|
||||
|
||||
default:
|
||||
return d.Core.ConvertValueForLocal(ctx, fieldType, fieldValue)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/database"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
// DoExec commits the sql string and its arguments to underlying driver
|
||||
// through given link object and returns the execution result.
|
||||
func (d *Driver) DoExec(ctx context.Context, link database.Link, sql string, args ...any) (result sql.Result, err error) {
|
||||
var (
|
||||
isUseCoreDoExec bool = false // Check whether the default method needs to be used
|
||||
primaryKey string = ""
|
||||
pkField database.TableField
|
||||
)
|
||||
|
||||
// Transaction checks.
|
||||
if link == nil {
|
||||
if tx := database.TXFromCtx(ctx, d.GetGroup()); tx != nil {
|
||||
// Firstly, check and retrieve transaction link from context.
|
||||
link = tx
|
||||
} else if link, err = d.MasterLink(); err != nil {
|
||||
// Or else it creates one from master node.
|
||||
return nil, err
|
||||
}
|
||||
} else if !link.IsTransaction() {
|
||||
// If current link is not transaction link, it checks and retrieves transaction from context.
|
||||
if tx := database.TXFromCtx(ctx, d.GetGroup()); tx != nil {
|
||||
link = tx
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it is an insert operation with primary key.
|
||||
if value := ctx.Value(internalPrimaryKeyInCtx); value != nil {
|
||||
var ok bool
|
||||
pkField, ok = value.(database.TableField)
|
||||
if !ok {
|
||||
isUseCoreDoExec = true
|
||||
}
|
||||
} else {
|
||||
isUseCoreDoExec = true
|
||||
}
|
||||
|
||||
// check if it is an insert operation.
|
||||
if !isUseCoreDoExec && pkField.Name != "" && strings.Contains(sql, "INSERT INTO") {
|
||||
primaryKey = pkField.Name
|
||||
sql += fmt.Sprintf(` RETURNING "%s"`, primaryKey)
|
||||
} else {
|
||||
// use default DoExec
|
||||
return d.Core.DoExec(ctx, link, sql, args...)
|
||||
}
|
||||
|
||||
// Only the insert operation with primary key can execute the following code
|
||||
|
||||
// Sql filtering.
|
||||
sql, args = d.FormatSqlBeforeExecuting(sql, args)
|
||||
sql, args, err = d.DoFilter(ctx, link, sql, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Link execution.
|
||||
var out database.DoCommitOutput
|
||||
out, err = d.DoCommit(ctx, database.DoCommitInput{
|
||||
Link: link,
|
||||
Sql: sql,
|
||||
Args: args,
|
||||
Stmt: nil,
|
||||
Type: database.SqlTypeQueryContext,
|
||||
IsTransaction: link.IsTransaction(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
affected := len(out.Records)
|
||||
if affected > 0 {
|
||||
if !strings.Contains(pkField.Type, "int") {
|
||||
return Result{
|
||||
affected: int64(affected),
|
||||
lastInsertId: 0,
|
||||
lastInsertIdError: gerror.NewCodef(
|
||||
gcode.CodeNotSupported,
|
||||
"LastInsertId is not supported by primary key type: %s", pkField.Type),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if out.Records[affected-1][primaryKey] != nil {
|
||||
lastInsertId := out.Records[affected-1][primaryKey].Int64()
|
||||
return Result{
|
||||
affected: int64(affected),
|
||||
lastInsertId: lastInsertId,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return Result{}, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/database"
|
||||
"github.com/gogf/gf/v2/text/gregex"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
// DoFilter deals with the sql string before commits it to underlying sql driver.
|
||||
func (d *Driver) DoFilter(
|
||||
ctx context.Context, link database.Link, sql string, args []any,
|
||||
) (newSql string, newArgs []any, err error) {
|
||||
var index int
|
||||
// Convert placeholder char '?' to string "$x".
|
||||
newSql, err = gregex.ReplaceStringFunc(`\?`, sql, func(s string) string {
|
||||
index++
|
||||
return fmt.Sprintf(`$%d`, index)
|
||||
})
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
// Handle pgsql jsonb feature support, which contains place-holder char '?'.
|
||||
// Refer:
|
||||
// https://github.com/gogf/gf/issues/1537
|
||||
// https://www.postgresql.org/docs/12/functions-json.html
|
||||
newSql, err = gregex.ReplaceStringFuncMatch(
|
||||
`(::jsonb([^\w\d]*)\$\d)`,
|
||||
newSql,
|
||||
func(match []string) string {
|
||||
return fmt.Sprintf(`::jsonb%s?`, match[2])
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
newSql, err = gregex.ReplaceString(` LIMIT (\d+),\s*(\d+)`, ` LIMIT $2 OFFSET $1`, newSql)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// Add support for pgsql INSERT OR IGNORE.
|
||||
if gstr.HasPrefix(newSql, database.InsertOperationIgnore) {
|
||||
newSql = "INSERT" + newSql[len(database.InsertOperationIgnore):] + " ON CONFLICT DO NOTHING"
|
||||
}
|
||||
|
||||
newArgs = args
|
||||
|
||||
return d.Core.DoFilter(ctx, link, newSql, newArgs)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/database"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
// DoInsert inserts or updates data for given table.
|
||||
// The list parameter must contain at least one record, which was previously validated.
|
||||
func (d *Driver) DoInsert(
|
||||
ctx context.Context,
|
||||
link database.Link, table string, list database.List, option database.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
switch option.InsertOption {
|
||||
case
|
||||
database.InsertOptionSave,
|
||||
database.InsertOptionReplace:
|
||||
// PostgreSQL does not support REPLACE INTO syntax, use Save (ON CONFLICT ... DO UPDATE) instead.
|
||||
// Automatically detect primary keys if OnConflict is not specified.
|
||||
if len(option.OnConflict) == 0 {
|
||||
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
|
||||
if err != nil {
|
||||
return nil, gerror.WrapCode(
|
||||
gcode.CodeInternalError,
|
||||
err,
|
||||
`failed to get primary keys for Save/Replace operation`,
|
||||
)
|
||||
}
|
||||
foundPrimaryKey := false
|
||||
for _, primaryKey := range primaryKeys {
|
||||
for dataKey := range list[0] {
|
||||
if strings.EqualFold(dataKey, primaryKey) {
|
||||
foundPrimaryKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundPrimaryKey {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundPrimaryKey {
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeMissingParameter,
|
||||
`Replace/Save operation requires conflict detection: `+
|
||||
`either specify OnConflict() columns or ensure table '%s' has a primary key in the data`,
|
||||
table,
|
||||
)
|
||||
}
|
||||
// TODO consider composite primary keys.
|
||||
option.OnConflict = primaryKeys
|
||||
}
|
||||
// Treat Replace as Save operation
|
||||
option.InsertOption = database.InsertOptionSave
|
||||
|
||||
// pgsql support InsertIgnore natively, so no need to set primary key in context.
|
||||
case database.InsertOptionIgnore, database.InsertOptionDefault:
|
||||
// Get table fields to retrieve the primary key TableField object (not just the name)
|
||||
// because DoExec needs the `TableField.Type` to determine if LastInsertId is supported.
|
||||
tableFields, err := d.GetCore().GetDB().TableFields(ctx, table)
|
||||
if err == nil {
|
||||
for _, field := range tableFields {
|
||||
if strings.EqualFold(field.Key, "pri") {
|
||||
pkField := *field
|
||||
ctx = context.WithValue(ctx, internalPrimaryKeyInCtx, pkField)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
}
|
||||
return d.Core.DoInsert(ctx, link, table, list, option)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/database"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// FormatUpsert returns SQL clause of type upsert for PgSQL.
|
||||
// For example: ON CONFLICT (id) DO UPDATE SET ...
|
||||
func (d *Driver) FormatUpsert(columns []string, list database.List, option database.DoInsertOption) (string, error) {
|
||||
if len(option.OnConflict) == 0 {
|
||||
return "", gerror.NewCode(
|
||||
gcode.CodeMissingParameter, `Please specify conflict columns`,
|
||||
)
|
||||
}
|
||||
|
||||
var onDuplicateStr string
|
||||
if option.OnDuplicateStr != "" {
|
||||
onDuplicateStr = option.OnDuplicateStr
|
||||
} else if len(option.OnDuplicateMap) > 0 {
|
||||
for k, v := range option.OnDuplicateMap {
|
||||
if len(onDuplicateStr) > 0 {
|
||||
onDuplicateStr += ","
|
||||
}
|
||||
switch v.(type) {
|
||||
case database.Raw, *database.Raw:
|
||||
onDuplicateStr += fmt.Sprintf(
|
||||
"%s=%s",
|
||||
d.Core.QuoteWord(k),
|
||||
v,
|
||||
)
|
||||
case database.Counter, *database.Counter:
|
||||
var counter database.Counter
|
||||
switch value := v.(type) {
|
||||
case database.Counter:
|
||||
counter = value
|
||||
case *database.Counter:
|
||||
counter = *value
|
||||
}
|
||||
operator, columnVal := "+", counter.Value
|
||||
if columnVal < 0 {
|
||||
operator, columnVal = "-", -columnVal
|
||||
}
|
||||
// Note: In PostgreSQL ON CONFLICT DO UPDATE, we use EXCLUDED to reference
|
||||
// the value that was proposed for insertion. This differs from MySQL's
|
||||
// ON DUPLICATE KEY UPDATE behavior where the column name without prefix
|
||||
// references the current row's value.
|
||||
onDuplicateStr += fmt.Sprintf(
|
||||
"%s=EXCLUDED.%s%s%s",
|
||||
d.QuoteWord(k),
|
||||
d.QuoteWord(counter.Field),
|
||||
operator,
|
||||
gconv.String(columnVal),
|
||||
)
|
||||
default:
|
||||
onDuplicateStr += fmt.Sprintf(
|
||||
"%s=EXCLUDED.%s",
|
||||
d.Core.QuoteWord(k),
|
||||
d.Core.QuoteWord(gconv.String(v)),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, column := range columns {
|
||||
// If it's SAVE operation, do not automatically update the creating time.
|
||||
if d.Core.IsSoftCreatedFieldName(column) {
|
||||
continue
|
||||
}
|
||||
if len(onDuplicateStr) > 0 {
|
||||
onDuplicateStr += ","
|
||||
}
|
||||
onDuplicateStr += fmt.Sprintf(
|
||||
"%s=EXCLUDED.%s",
|
||||
d.Core.QuoteWord(column),
|
||||
d.Core.QuoteWord(column),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
conflictKeys := gstr.Join(option.OnConflict, ",")
|
||||
|
||||
return fmt.Sprintf("ON CONFLICT (%s) DO UPDATE SET ", conflictKeys) + onDuplicateStr, nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/database"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
// Open creates and returns an underlying sql.DB object for pgsql.
|
||||
// https://pkg.go.dev/github.com/lib/pq
|
||||
func (d *Driver) Open(config *database.ConfigNode) (db *sql.DB, err error) {
|
||||
source, err := configNodeToSource(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
underlyingDriverName := "postgres"
|
||||
if db, err = sql.Open(underlyingDriverName, source); err != nil {
|
||||
err = gerror.WrapCodef(
|
||||
gcode.CodeDbOperationError, err,
|
||||
`sql.Open failed for driver "%s" by source "%s"`, underlyingDriverName, source,
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func configNodeToSource(config *database.ConfigNode) (string, error) {
|
||||
var source string
|
||||
source = fmt.Sprintf(
|
||||
"user=%s password='%s' host=%s sslmode=disable",
|
||||
config.User, config.Pass, config.Host,
|
||||
)
|
||||
if config.Port != "" {
|
||||
source = fmt.Sprintf("%s port=%s", source, config.Port)
|
||||
}
|
||||
if config.Name != "" {
|
||||
source = fmt.Sprintf("%s dbname=%s", source, config.Name)
|
||||
}
|
||||
if config.Namespace != "" {
|
||||
source = fmt.Sprintf("%s search_path=%s", source, config.Namespace)
|
||||
}
|
||||
if config.Timezone != "" {
|
||||
source = fmt.Sprintf("%s timezone=%s", source, config.Timezone)
|
||||
}
|
||||
if config.Extra != "" {
|
||||
extraMap, err := gstr.Parse(config.Extra)
|
||||
if err != nil {
|
||||
return "", gerror.WrapCodef(
|
||||
gcode.CodeInvalidParameter,
|
||||
err,
|
||||
`invalid extra configuration: %s`, config.Extra,
|
||||
)
|
||||
}
|
||||
for k, v := range extraMap {
|
||||
source += fmt.Sprintf(` %s=%s`, k, v)
|
||||
}
|
||||
}
|
||||
return source, nil
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package pgsql
|
||||
|
||||
// OrderRandomFunction returns the SQL function for random ordering.
|
||||
func (d *Driver) OrderRandomFunction() string {
|
||||
return "RANDOM()"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package pgsql
|
||||
|
||||
import "database/sql"
|
||||
|
||||
type Result struct {
|
||||
sql.Result
|
||||
affected int64
|
||||
lastInsertId int64
|
||||
lastInsertIdError error
|
||||
}
|
||||
|
||||
func (pgr Result) RowsAffected() (int64, error) {
|
||||
return pgr.affected, nil
|
||||
}
|
||||
|
||||
func (pgr Result) LastInsertId() (int64, error) {
|
||||
return pgr.lastInsertId, pgr.lastInsertIdError
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/database"
|
||||
"github.com/gogf/gf/v2/util/gutil"
|
||||
)
|
||||
|
||||
var (
|
||||
tableFieldsSqlTmp = `
|
||||
SELECT
|
||||
a.attname AS field,
|
||||
t.typname AS type,
|
||||
a.attnotnull AS null,
|
||||
(CASE WHEN d.contype = 'p' THEN 'pri' WHEN d.contype = 'u' THEN 'uni' ELSE '' END) AS key,
|
||||
ic.column_default AS default_value,
|
||||
b.description AS comment,
|
||||
COALESCE(character_maximum_length, numeric_precision, -1) AS length,
|
||||
numeric_scale AS scale
|
||||
FROM pg_attribute a
|
||||
LEFT JOIN pg_class c ON a.attrelid = c.oid
|
||||
LEFT JOIN pg_constraint d ON d.conrelid = c.oid AND a.attnum = d.conkey[1]
|
||||
LEFT JOIN pg_description b ON a.attrelid = b.objoid AND a.attnum = b.objsubid
|
||||
LEFT JOIN pg_type t ON a.atttypid = t.oid
|
||||
LEFT JOIN information_schema.columns ic ON ic.column_name = a.attname AND ic.table_name = c.relname
|
||||
WHERE c.oid = '%s'::regclass
|
||||
AND a.attisdropped IS FALSE
|
||||
AND a.attnum > 0
|
||||
ORDER BY a.attnum`
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
tableFieldsSqlTmp, err = database.FormatMultiLineSqlToSingle(tableFieldsSqlTmp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TableFields retrieves and returns the fields' information of specified table of current schema.
|
||||
func (d *Driver) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*database.TableField, err error) {
|
||||
var (
|
||||
result database.Result
|
||||
link database.Link
|
||||
usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...)
|
||||
// TODO duplicated `id` result?
|
||||
structureSql = fmt.Sprintf(tableFieldsSqlTmp, table)
|
||||
)
|
||||
if link, err = d.SlaveLink(usedSchema); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err = d.DoSelect(ctx, link, structureSql)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fields = make(map[string]*database.TableField)
|
||||
var (
|
||||
index = 0
|
||||
name string
|
||||
ok bool
|
||||
existingField *database.TableField
|
||||
)
|
||||
for _, m := range result {
|
||||
name = m["field"].String()
|
||||
// Merge duplicated fields, especially for key constraints.
|
||||
// Priority: pri > uni > others
|
||||
if existingField, ok = fields[name]; ok {
|
||||
currentKey := m["key"].String()
|
||||
// Merge key information with priority: pri > uni
|
||||
if currentKey == "pri" || (currentKey == "uni" && existingField.Key != "pri") {
|
||||
existingField.Key = currentKey
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
fieldType string
|
||||
dataType = m["type"].String()
|
||||
dataLength = m["length"].Int()
|
||||
)
|
||||
if dataLength > 0 {
|
||||
fieldType = fmt.Sprintf("%s(%d)", dataType, dataLength)
|
||||
} else {
|
||||
fieldType = dataType
|
||||
}
|
||||
|
||||
fields[name] = &database.TableField{
|
||||
Index: index,
|
||||
Name: name,
|
||||
Type: fieldType,
|
||||
Null: !m["null"].Bool(),
|
||||
Key: m["key"].String(),
|
||||
Default: m["default_value"].Val(),
|
||||
Comment: m["comment"].String(),
|
||||
}
|
||||
index++
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/database"
|
||||
"github.com/gogf/gf/v2/text/gregex"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gutil"
|
||||
)
|
||||
|
||||
var (
|
||||
tablesSqlTmp = `
|
||||
SELECT
|
||||
c.relname
|
||||
FROM
|
||||
pg_class c
|
||||
INNER JOIN pg_namespace n ON
|
||||
c.relnamespace = n.oid
|
||||
WHERE
|
||||
n.nspname = '%s'
|
||||
AND c.relkind IN ('r', 'p')
|
||||
%s
|
||||
ORDER BY
|
||||
c.relname
|
||||
`
|
||||
|
||||
versionRegex = regexp.MustCompile(`PostgreSQL (\d+\.\d+)`)
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
tablesSqlTmp, err = database.FormatMultiLineSqlToSingle(tablesSqlTmp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tables retrieves and returns the tables of current schema.
|
||||
// It's mainly used in cli tool chain for automatically generating the models.
|
||||
func (d *Driver) Tables(ctx context.Context, schema ...string) (tables []string, err error) {
|
||||
var (
|
||||
result database.Result
|
||||
usedSchema = gutil.GetOrDefaultStr(d.GetConfig().Namespace, schema...)
|
||||
)
|
||||
if usedSchema == "" {
|
||||
usedSchema = defaultSchema
|
||||
}
|
||||
// DO NOT use `usedSchema` as parameter for function `SlaveLink`.
|
||||
link, err := d.SlaveLink(schema...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
useRelpartbound := ""
|
||||
if gstr.CompareVersion(d.version(ctx, link), "10") >= 0 {
|
||||
useRelpartbound = "AND c.relpartbound IS NULL"
|
||||
}
|
||||
|
||||
var query = fmt.Sprintf(
|
||||
tablesSqlTmp,
|
||||
usedSchema,
|
||||
useRelpartbound,
|
||||
)
|
||||
|
||||
query, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(query))
|
||||
result, err = d.DoSelect(ctx, link, query)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, m := range result {
|
||||
for _, v := range m {
|
||||
tables = append(tables, v.String())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// version checks and returns the database version.
|
||||
func (d *Driver) version(ctx context.Context, link database.Link) string {
|
||||
result, err := d.DoSelect(ctx, link, "SELECT version();")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if v, ok := result[0]["version"]; ok {
|
||||
matches := versionRegex.FindStringSubmatch(v.String())
|
||||
if len(matches) >= 2 {
|
||||
return matches[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
CREATE TABLE "public"."%s" (
|
||||
"one" int8[] NOT NULL,
|
||||
"two" text[][] NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
CREATE TABLE "public"."%s" (
|
||||
"text" varchar(255) COLLATE "pg_catalog"."default",
|
||||
"number" int4
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE "public"."%s"
|
||||
(
|
||||
"one" int8[] NOT NULL,
|
||||
"two" text[][] NOT NULL,
|
||||
"three" jsonb,
|
||||
"four" json,
|
||||
"five" jsonb
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE test_enum (
|
||||
id int8 NOT NULL,
|
||||
status int2 DEFAULT 0 NOT NULL,
|
||||
CONSTRAINT test_enum_pk PRIMARY KEY (id)
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
DROP TABLE IF EXISTS instance;
|
||||
CREATE TABLE instance (
|
||||
f_id SERIAL NOT NULL PRIMARY KEY,
|
||||
name varchar(255) DEFAULT ''
|
||||
);
|
||||
INSERT INTO instance VALUES (1, 'john');
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
CREATE TABLE table_a (
|
||||
id SERIAL PRIMARY KEY,
|
||||
alias varchar(255) DEFAULT ''
|
||||
);
|
||||
|
||||
INSERT INTO table_a VALUES (1, 'table_a_test1');
|
||||
INSERT INTO table_a VALUES (2, 'table_a_test2');
|
||||
|
||||
CREATE TABLE table_b (
|
||||
id SERIAL PRIMARY KEY,
|
||||
table_a_id integer NOT NULL,
|
||||
alias varchar(255) DEFAULT ''
|
||||
);
|
||||
|
||||
INSERT INTO table_b VALUES (10, 1, 'table_b_test1');
|
||||
INSERT INTO table_b VALUES (20, 2, 'table_b_test2');
|
||||
INSERT INTO table_b VALUES (30, 1, 'table_b_test3');
|
||||
INSERT INTO table_b VALUES (40, 2, 'table_b_test4');
|
||||
|
||||
CREATE TABLE table_c (
|
||||
id SERIAL PRIMARY KEY,
|
||||
table_b_id integer NOT NULL,
|
||||
alias varchar(255) DEFAULT ''
|
||||
);
|
||||
|
||||
INSERT INTO table_c VALUES (100, 10, 'table_c_test1');
|
||||
INSERT INTO table_c VALUES (200, 10, 'table_c_test2');
|
||||
INSERT INTO table_c VALUES (300, 20, 'table_c_test3');
|
||||
INSERT INTO table_c VALUES (400, 30, 'table_c_test4');
|
||||
@@ -0,0 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name varchar(45) NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
uid SERIAL PRIMARY KEY,
|
||||
address varchar(45) NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
id SERIAL PRIMARY KEY,
|
||||
uid integer NOT NULL,
|
||||
score integer NOT NULL
|
||||
);
|
||||
Reference in New Issue
Block a user