feat(database): 添加 ClickHouse 数据库驱动支持
- 实现了完整的 ClickHouse 驱动,包括连接、查询、插入、更新、删除等基本操作 - 添加了 ClickHouse 特有的数据类型转换和 SQL 过滤功能 - 实现了表结构查询和字段信息获取功能 - 添加了 Ping 检查和错误处理机制 - 增加了对 UPDATE 和 DELETE 语句的语法转换以适配 ClickHouse - 添加了批量插入操作的支持 - 新增了 14 个测试用例文件用于验证数据库相关功能
This commit is contained in:
@@ -4,58 +4,43 @@
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
// Package oracle implements database.Driver for Oracle database.
|
||||
// Package oracle implements database.Driver, which supports operations for database Oracle.
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/sijms/go-ora/v2"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/database"
|
||||
)
|
||||
|
||||
// Driver is the driver for Oracle database.
|
||||
// Driver is the driver for oracle database.
|
||||
type Driver struct {
|
||||
*database.Core
|
||||
}
|
||||
|
||||
const (
|
||||
quoteChar = `"`
|
||||
rowNumberAliasForSelect = `ROW_NUMBER__`
|
||||
quoteChar = `"`
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := database.Register("oracle", New()); err != nil {
|
||||
if err := database.Register(`oracle`, New()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// New creates and returns a driver that implements database.Driver for Oracle.
|
||||
// New create and returns a driver that implements database.Driver, which supports operations for Oracle.
|
||||
func New() database.Driver {
|
||||
return &Driver{}
|
||||
}
|
||||
|
||||
// New creates and returns a database object for Oracle.
|
||||
// New creates and returns a database object for oracle.
|
||||
// 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{
|
||||
Core: core,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetChars returns the security char for Oracle.
|
||||
// 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 Oracle.
|
||||
func (d *Driver) Open(config *database.ConfigNode) (*sql.DB, error) {
|
||||
var source string
|
||||
if config.Link != "" {
|
||||
source = config.Link
|
||||
} else {
|
||||
source = fmt.Sprintf("oracle://%s:%s@%s:%s/%s",
|
||||
config.User, config.Pass, config.Host, config.Port, config.Name)
|
||||
}
|
||||
return sql.Open("oracle", source)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/database"
|
||||
)
|
||||
|
||||
// DoCommit commits current sql and arguments to underlying sql driver.
|
||||
func (d *Driver) DoCommit(ctx context.Context, in database.DoCommitInput) (out database.DoCommitOutput, err error) {
|
||||
out, err = d.Core.DoCommit(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(out.Records) > 0 {
|
||||
// remove auto added field.
|
||||
for i, record := range out.Records {
|
||||
delete(record, rowNumberAliasForSelect)
|
||||
out.Records[i] = record
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
returningClause = " RETURNING %s INTO ?"
|
||||
)
|
||||
|
||||
// DoExec commits the sql string and its arguments to underlying driver
|
||||
// through given link object and returns the execution result.
|
||||
// It handles INSERT statements specially to support LastInsertId.
|
||||
func (d *Driver) DoExec(
|
||||
ctx context.Context, link database.Link, sql string, args ...interface{},
|
||||
) (result sql.Result, err error) {
|
||||
var (
|
||||
isUseCoreDoExec = true
|
||||
primaryKey string
|
||||
pkField database.TableField
|
||||
)
|
||||
|
||||
// Transaction checks.
|
||||
if link == nil {
|
||||
if tx := database.TXFromCtx(ctx, d.GetGroup()); tx != nil {
|
||||
link = tx
|
||||
} else if link, err = d.MasterLink(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if !link.IsTransaction() {
|
||||
if tx := database.TXFromCtx(ctx, d.GetGroup()); tx != nil {
|
||||
link = tx
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it is an insert operation with primary key from context.
|
||||
if value := ctx.Value(internalPrimaryKeyInCtx); value != nil {
|
||||
if field, ok := value.(database.TableField); ok {
|
||||
pkField = field
|
||||
isUseCoreDoExec = false
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it is an INSERT statement with primary key.
|
||||
if !isUseCoreDoExec && pkField.Name != "" && strings.Contains(strings.ToUpper(sql), "INSERT INTO") {
|
||||
primaryKey = pkField.Name
|
||||
// Oracle supports RETURNING clause to get the last inserted id
|
||||
sql += fmt.Sprintf(returningClause, d.QuoteWord(primaryKey))
|
||||
} else {
|
||||
// Use default DoExec for non-INSERT or no primary key scenarios
|
||||
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
|
||||
}
|
||||
|
||||
// Prepare output variable for RETURNING clause
|
||||
var lastInsertId int64
|
||||
// Append the output parameter for the RETURNING clause
|
||||
args = append(args, &lastInsertId)
|
||||
|
||||
// Link execution.
|
||||
_, err = d.DoCommit(ctx, database.DoCommitInput{
|
||||
Link: link,
|
||||
Sql: sql,
|
||||
Args: args,
|
||||
Stmt: nil,
|
||||
Type: database.SqlTypeExecContext,
|
||||
IsTransaction: link.IsTransaction(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return &Result{
|
||||
lastInsertId: 0,
|
||||
rowsAffected: 0,
|
||||
lastInsertIdError: err,
|
||||
}, err
|
||||
}
|
||||
|
||||
// Get rows affected from the result
|
||||
// For single insert with RETURNING clause, affected is always 1
|
||||
var affected int64 = 1
|
||||
|
||||
// Check if the primary key field type supports LastInsertId
|
||||
if !strings.Contains(strings.ToLower(pkField.Type), "int") {
|
||||
return &Result{
|
||||
lastInsertId: 0,
|
||||
rowsAffected: affected,
|
||||
lastInsertIdError: gerror.NewCodef(
|
||||
gcode.CodeNotSupported,
|
||||
"LastInsertId is not supported by primary key type: %s",
|
||||
pkField.Type,
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &Result{
|
||||
lastInsertId: lastInsertId,
|
||||
rowsAffected: affected,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/database"
|
||||
"github.com/gogf/gf/v2/text/gregex"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
var (
|
||||
newSqlReplacementTmp = `
|
||||
SELECT * FROM (
|
||||
SELECT GFORM.*, ROWNUM ROW_NUMBER__ FROM (%s %s) GFORM WHERE ROWNUM <= %d
|
||||
) WHERE ROW_NUMBER__ > %d
|
||||
`
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
newSqlReplacementTmp, err = database.FormatMultiLineSqlToSingle(newSqlReplacementTmp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
newArgs = args
|
||||
// Convert placeholder char '?' to string ":vx".
|
||||
newSql, err = gregex.ReplaceStringFunc("\\?", sql, func(s string) string {
|
||||
index++
|
||||
return fmt.Sprintf(":v%d", index)
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
newSql, err = gregex.ReplaceString("\"", "", newSql)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
newSql, err = d.parseSql(newSql)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return d.Core.DoFilter(ctx, link, newSql, newArgs)
|
||||
}
|
||||
|
||||
// parseSql does some replacement of the sql before commits it to underlying driver,
|
||||
// for support of oracle server.
|
||||
func (d *Driver) parseSql(toBeCommittedSql string) (string, error) {
|
||||
var (
|
||||
err error
|
||||
operation = gstr.StrTillEx(toBeCommittedSql, " ")
|
||||
keyword = strings.ToUpper(gstr.Trim(operation))
|
||||
)
|
||||
switch keyword {
|
||||
case "SELECT":
|
||||
toBeCommittedSql, err = d.handleSelectSqlReplacement(toBeCommittedSql)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return toBeCommittedSql, nil
|
||||
}
|
||||
|
||||
func (d *Driver) handleSelectSqlReplacement(toBeCommittedSql string) (newSql string, err error) {
|
||||
var (
|
||||
match [][]string
|
||||
patten = `^\s*(?i)(SELECT)|(LIMIT\s*(\d+)\s*,{0,1}\s*(\d*))`
|
||||
)
|
||||
match, err = gregex.MatchAllString(patten, toBeCommittedSql)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(match) == 0 {
|
||||
return toBeCommittedSql, nil
|
||||
}
|
||||
var index = 1
|
||||
if len(match) < 2 || strings.HasPrefix(match[index][0], "LIMIT") == false {
|
||||
return toBeCommittedSql, nil
|
||||
}
|
||||
// only handle `SELECT ... LIMIT ...` statement.
|
||||
queryExpr, err := gregex.MatchString("((?i)SELECT)(.+)((?i)LIMIT)", toBeCommittedSql)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(queryExpr) == 0 {
|
||||
return toBeCommittedSql, nil
|
||||
}
|
||||
if len(queryExpr) != 4 ||
|
||||
strings.EqualFold(queryExpr[1], "SELECT") == false ||
|
||||
strings.EqualFold(queryExpr[3], "LIMIT") == false {
|
||||
return toBeCommittedSql, nil
|
||||
}
|
||||
page, limit := 0, 0
|
||||
for i := 1; i < len(match[index]); i++ {
|
||||
if len(strings.TrimSpace(match[index][i])) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(match[index][i], "LIMIT") {
|
||||
if match[index][i+2] != "" {
|
||||
page, err = strconv.Atoi(match[index][i+1])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
limit, err = strconv.Atoi(match[index][i+2])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
limit = (page/limit + 1) * limit
|
||||
page, err = strconv.Atoi(match[index][i+1])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
limit, err = strconv.Atoi(match[index][i+1])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
var newReplacedSql = fmt.Sprintf(
|
||||
newSqlReplacementTmp,
|
||||
queryExpr[1], queryExpr[2], limit, page,
|
||||
)
|
||||
return newReplacedSql, nil
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/database"
|
||||
"github.com/gogf/gf/v2/container/gset"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
const (
|
||||
internalPrimaryKeyInCtx gctx.StrKey = "primary_key_field"
|
||||
)
|
||||
|
||||
// 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:
|
||||
return d.doSave(ctx, link, table, list, option)
|
||||
|
||||
case database.InsertOptionReplace:
|
||||
// Oracle does not support REPLACE INTO syntax, use SAVE instead.
|
||||
return d.doSave(ctx, link, table, list, option)
|
||||
|
||||
case database.InsertOptionIgnore:
|
||||
// Oracle does not support INSERT IGNORE syntax, use MERGE instead.
|
||||
return d.doInsertIgnore(ctx, link, table, list, option)
|
||||
|
||||
case database.InsertOptionDefault:
|
||||
// For default insert, set primary key field in context to support LastInsertId.
|
||||
// Only set it when the primary key is not provided in the data, for performance reason.
|
||||
tableFields, err := d.GetCore().GetDB().TableFields(ctx, table)
|
||||
if err == nil && len(list) > 0 {
|
||||
for _, field := range tableFields {
|
||||
if strings.EqualFold(field.Key, "pri") {
|
||||
// Check if primary key is provided in the data.
|
||||
pkProvided := false
|
||||
for key := range list[0] {
|
||||
if strings.EqualFold(key, field.Name) {
|
||||
pkProvided = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// Only use RETURNING when primary key is not provided, for performance reason.
|
||||
if !pkProvided {
|
||||
pkField := *field
|
||||
ctx = context.WithValue(ctx, internalPrimaryKeyInCtx, pkField)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
}
|
||||
var (
|
||||
keys []string
|
||||
values []string
|
||||
params []any
|
||||
)
|
||||
// Retrieve the table fields and length.
|
||||
var (
|
||||
listLength = len(list)
|
||||
valueHolder = make([]string, 0)
|
||||
)
|
||||
for k := range list[0] {
|
||||
keys = append(keys, k)
|
||||
valueHolder = append(valueHolder, "?")
|
||||
}
|
||||
var (
|
||||
batchResult = new(database.SqlResult)
|
||||
charL, charR = d.GetChars()
|
||||
keyStr = charL + strings.Join(keys, charL+","+charR) + charR
|
||||
valueHolderStr = strings.Join(valueHolder, ",")
|
||||
)
|
||||
// Format "INSERT...INTO..." statement.
|
||||
// Note: Use standard INSERT INTO syntax instead of INSERT ALL to ensure triggers fire
|
||||
for i := 0; i < listLength; i++ {
|
||||
for _, k := range keys {
|
||||
if s, ok := list[i][k].(database.Raw); ok {
|
||||
params = append(params, gconv.String(s))
|
||||
} else {
|
||||
params = append(params, list[i][k])
|
||||
}
|
||||
}
|
||||
values = append(values, valueHolderStr)
|
||||
|
||||
// Execute individual INSERT for each record to trigger row-level triggers
|
||||
r, err := d.DoExec(ctx, link, fmt.Sprintf(
|
||||
"INSERT INTO %s(%s) VALUES(%s)",
|
||||
table, keyStr, valueHolderStr,
|
||||
), params...)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
if n, err := r.RowsAffected(); err != nil {
|
||||
return r, err
|
||||
} else {
|
||||
batchResult.Result = r
|
||||
batchResult.Affected += n
|
||||
}
|
||||
params = params[:0]
|
||||
}
|
||||
return batchResult, nil
|
||||
}
|
||||
|
||||
// doSave support upsert for Oracle
|
||||
func (d *Driver) doSave(ctx context.Context,
|
||||
link database.Link, table string, list database.List, option database.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
return d.doMergeInsert(ctx, link, table, list, option, true)
|
||||
}
|
||||
|
||||
// doInsertIgnore implements INSERT IGNORE operation using MERGE statement for Oracle database.
|
||||
// It only inserts records when there's no conflict on primary/unique keys.
|
||||
func (d *Driver) doInsertIgnore(ctx context.Context,
|
||||
link database.Link, table string, list database.List, option database.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
return d.doMergeInsert(ctx, link, table, list, option, false)
|
||||
}
|
||||
|
||||
// doMergeInsert implements MERGE-based insert operations for Oracle database.
|
||||
// When withUpdate is true, it performs upsert (insert or update).
|
||||
// When withUpdate is false, it performs insert ignore (insert only when no conflict).
|
||||
func (d *Driver) doMergeInsert(
|
||||
ctx context.Context,
|
||||
link database.Link, table string, list database.List, option database.DoInsertOption, withUpdate bool,
|
||||
) (result sql.Result, err error) {
|
||||
// If OnConflict is not specified, automatically get the primary key of the table
|
||||
conflictKeys := option.OnConflict
|
||||
if len(conflictKeys) == 0 {
|
||||
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
|
||||
if err != nil {
|
||||
return nil, gerror.WrapCode(
|
||||
gcode.CodeInternalError,
|
||||
err,
|
||||
`failed to get primary keys for table`,
|
||||
)
|
||||
}
|
||||
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/InsertIgnore 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.
|
||||
conflictKeys = primaryKeys
|
||||
}
|
||||
|
||||
var (
|
||||
one = list[0]
|
||||
oneLen = len(one)
|
||||
charL, charR = d.GetChars()
|
||||
conflictKeySet = gset.NewStrSet(false)
|
||||
|
||||
// queryHolders: Handle data with Holder that need to be upsert
|
||||
// queryValues: Handle data that need to be upsert
|
||||
// insertKeys: Handle valid keys that need to be inserted
|
||||
// insertValues: Handle values that need to be inserted
|
||||
// updateValues: Handle values that need to be updated
|
||||
queryHolders = make([]string, oneLen)
|
||||
queryValues = make([]any, oneLen)
|
||||
insertKeys = make([]string, oneLen)
|
||||
insertValues = make([]string, oneLen)
|
||||
updateValues []string
|
||||
)
|
||||
|
||||
// conflictKeys slice type conv to set type
|
||||
for _, conflictKey := range conflictKeys {
|
||||
conflictKeySet.Add(gstr.ToUpper(conflictKey))
|
||||
}
|
||||
|
||||
index := 0
|
||||
for key, value := range one {
|
||||
keyWithChar := charL + key + charR
|
||||
queryHolders[index] = fmt.Sprintf("? AS %s", keyWithChar)
|
||||
queryValues[index] = value
|
||||
insertKeys[index] = keyWithChar
|
||||
insertValues[index] = fmt.Sprintf("T2.%s", keyWithChar)
|
||||
|
||||
// Build updateValues only when withUpdate is true
|
||||
// Filter conflict keys and soft created fields from updateValues
|
||||
if withUpdate && !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) {
|
||||
updateValues = append(
|
||||
updateValues,
|
||||
fmt.Sprintf(`T1.%s = T2.%s`, keyWithChar, keyWithChar),
|
||||
)
|
||||
}
|
||||
index++
|
||||
}
|
||||
|
||||
var (
|
||||
batchResult = new(database.SqlResult)
|
||||
sqlStr = parseSqlForMerge(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys)
|
||||
)
|
||||
r, err := d.DoExec(ctx, link, sqlStr, queryValues...)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
if n, err := r.RowsAffected(); err != nil {
|
||||
return r, err
|
||||
} else {
|
||||
batchResult.Result = r
|
||||
batchResult.Affected += n
|
||||
}
|
||||
return batchResult, nil
|
||||
}
|
||||
|
||||
// parseSqlForMerge generates MERGE statement for Oracle database.
|
||||
// When updateValues is empty, it only inserts (INSERT IGNORE behavior).
|
||||
// When updateValues is provided, it performs upsert (INSERT or UPDATE).
|
||||
// Examples:
|
||||
// - INSERT IGNORE: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...)
|
||||
// - UPSERT: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) WHEN MATCHED THEN UPDATE SET ...
|
||||
func parseSqlForMerge(table string,
|
||||
queryHolders, insertKeys, insertValues, updateValues, duplicateKey []string,
|
||||
) (sqlStr string) {
|
||||
var (
|
||||
queryHolderStr = strings.Join(queryHolders, ",")
|
||||
insertKeyStr = strings.Join(insertKeys, ",")
|
||||
insertValueStr = strings.Join(insertValues, ",")
|
||||
duplicateKeyStr string
|
||||
)
|
||||
|
||||
// Build ON condition
|
||||
for index, keys := range duplicateKey {
|
||||
if index != 0 {
|
||||
duplicateKeyStr += " AND "
|
||||
}
|
||||
duplicateKeyStr += fmt.Sprintf("T1.%s = T2.%s", keys, keys)
|
||||
}
|
||||
|
||||
// Build SQL based on whether UPDATE is needed
|
||||
pattern := gstr.Trim(
|
||||
`MERGE INTO %s T1 USING (SELECT %s FROM DUAL) T2 ON (%s) WHEN ` +
|
||||
`NOT MATCHED THEN INSERT(%s) VALUES (%s)`,
|
||||
)
|
||||
if len(updateValues) > 0 {
|
||||
// Upsert: INSERT or UPDATE
|
||||
pattern += gstr.Trim(` WHEN MATCHED THEN UPDATE SET %s`)
|
||||
return fmt.Sprintf(
|
||||
pattern, table, queryHolderStr, duplicateKeyStr, insertKeyStr, insertValueStr,
|
||||
strings.Join(updateValues, ","),
|
||||
)
|
||||
}
|
||||
// Insert Ignore: INSERT only
|
||||
return fmt.Sprintf(pattern, table, queryHolderStr, duplicateKeyStr, insertKeyStr, insertValueStr)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strings"
|
||||
|
||||
gora "github.com/sijms/go-ora/v2"
|
||||
|
||||
"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/util/gconv"
|
||||
)
|
||||
|
||||
// Open creates and returns an underlying sql.DB object for oracle.
|
||||
func (d *Driver) Open(config *database.ConfigNode) (db *sql.DB, err error) {
|
||||
var (
|
||||
source string
|
||||
underlyingDriverName = "oracle"
|
||||
)
|
||||
|
||||
options := map[string]string{
|
||||
"CONNECTION TIMEOUT": "60",
|
||||
"PREFETCH_ROWS": "25",
|
||||
}
|
||||
|
||||
if config.Debug {
|
||||
options["TRACE FILE"] = "oracle_trace.log"
|
||||
}
|
||||
// [username:[password]@]host[:port][/service_name][?param1=value1&...¶mN=valueN]
|
||||
if config.Extra != "" {
|
||||
// fix #3226
|
||||
list := strings.Split(config.Extra, "&")
|
||||
for _, v := range list {
|
||||
kv := strings.Split(v, "=")
|
||||
if len(kv) == 2 {
|
||||
options[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
source = gora.BuildUrl(
|
||||
config.Host, gconv.Int(config.Port), config.Name, config.User, config.Pass, options,
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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 oracle
|
||||
|
||||
// OrderRandomFunction returns the SQL function for random ordering.
|
||||
func (d *Driver) OrderRandomFunction() string {
|
||||
return "DBMS_RANDOM.VALUE()"
|
||||
}
|
||||
@@ -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 oracle
|
||||
|
||||
// Result implements sql.Result interface for Oracle database.
|
||||
type Result struct {
|
||||
lastInsertId int64
|
||||
rowsAffected int64
|
||||
lastInsertIdError error
|
||||
}
|
||||
|
||||
// LastInsertId returns the last insert id.
|
||||
func (r *Result) LastInsertId() (int64, error) {
|
||||
return r.lastInsertId, r.lastInsertIdError
|
||||
}
|
||||
|
||||
// RowsAffected returns the rows affected.
|
||||
func (r *Result) RowsAffected() (int64, error) {
|
||||
return r.rowsAffected, nil
|
||||
}
|
||||
@@ -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 oracle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/database"
|
||||
"github.com/gogf/gf/v2/util/gutil"
|
||||
)
|
||||
|
||||
var (
|
||||
tableFieldsSqlTmp = `
|
||||
SELECT
|
||||
c.COLUMN_NAME AS FIELD,
|
||||
CASE
|
||||
WHEN (c.DATA_TYPE='NUMBER' AND NVL(c.DATA_SCALE,0)=0) THEN 'INT'||'('||c.DATA_PRECISION||','||c.DATA_SCALE||')'
|
||||
WHEN (c.DATA_TYPE='NUMBER' AND NVL(c.DATA_SCALE,0)>0) THEN 'FLOAT'||'('||c.DATA_PRECISION||','||c.DATA_SCALE||')'
|
||||
WHEN c.DATA_TYPE='FLOAT' THEN c.DATA_TYPE||'('||c.DATA_PRECISION||','||c.DATA_SCALE||')'
|
||||
ELSE c.DATA_TYPE||'('||c.DATA_LENGTH||')' END AS TYPE,
|
||||
c.NULLABLE,
|
||||
CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 'PRI' ELSE '' END AS KEY
|
||||
FROM USER_TAB_COLUMNS c
|
||||
LEFT JOIN (
|
||||
SELECT cols.COLUMN_NAME
|
||||
FROM USER_CONSTRAINTS cons
|
||||
JOIN USER_CONS_COLUMNS cols ON cons.CONSTRAINT_NAME = cols.CONSTRAINT_NAME
|
||||
WHERE cons.TABLE_NAME = '%s' AND cons.CONSTRAINT_TYPE = 'P'
|
||||
) pk ON c.COLUMN_NAME = pk.COLUMN_NAME
|
||||
WHERE c.TABLE_NAME = '%s'
|
||||
ORDER BY c.COLUMN_ID
|
||||
`
|
||||
)
|
||||
|
||||
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.
|
||||
//
|
||||
// Also see DriverMysql.TableFields.
|
||||
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...)
|
||||
upperTable = strings.ToUpper(table)
|
||||
structureSql = fmt.Sprintf(tableFieldsSqlTmp, upperTable, upperTable)
|
||||
)
|
||||
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)
|
||||
for i, m := range result {
|
||||
isNull := false
|
||||
if m["NULLABLE"].String() == "Y" {
|
||||
isNull = true
|
||||
}
|
||||
|
||||
fields[m["FIELD"].String()] = &database.TableField{
|
||||
Index: i,
|
||||
Name: m["FIELD"].String(),
|
||||
Type: m["TYPE"].String(),
|
||||
Null: isNull,
|
||||
Key: m["KEY"].String(),
|
||||
}
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/database"
|
||||
)
|
||||
|
||||
const (
|
||||
tablesSqlTmp = `SELECT TABLE_NAME FROM USER_TABLES ORDER BY TABLE_NAME`
|
||||
)
|
||||
|
||||
// Tables retrieves and returns the tables of current schema.
|
||||
// It's mainly used in cli tool chain for automatically generating the models.
|
||||
// Note that it ignores the parameter `schema` in oracle database, as it is not necessary.
|
||||
func (d *Driver) Tables(ctx context.Context, schema ...string) (tables []string, err error) {
|
||||
var result database.Result
|
||||
// DO NOT use `usedSchema` as parameter for function `SlaveLink`.
|
||||
link, err := d.SlaveLink(schema...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err = d.DoSelect(ctx, link, tablesSqlTmp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, m := range result {
|
||||
for _, v := range m {
|
||||
tables = append(tables, v.String())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user