diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index cc15c639b..8afc1585a 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -67,9 +67,13 @@ jobs: - 3306:3306 # PostgreSQL backend server. - # docker run -d --name postgres -p 5432:5432 \ - # -e POSTGRES_PASSWORD=12345678 -e POSTGRES_USER=postgres -e POSTGRES_DB=test \ - # -v postgres:/Users/john/Temp/postgresql/data loads/postgres:13 + # docker run -d --name postgres \ + # -p 5432:5432 \ + # -e POSTGRES_PASSWORD=12345678 \ + # -e POSTGRES_USER=postgres \ + # -e POSTGRES_DB=test \ + # -v postgres:/Users/john/Temp/postgresql/data \ + # loads/postgres:13 postgres: image: loads/postgres:13 env: @@ -87,7 +91,8 @@ jobs: --health-retries 5 # MSSQL backend server. - # docker run -d --name mssql -p 1433:1433 \ + # docker run \ + # -p 1433:1433 \ # -e ACCEPT_EULA=Y \ # -e SA_PASSWORD=LoremIpsum86 \ # -e MSSQL_DB=test \ @@ -123,8 +128,13 @@ jobs: - 9001:9001 # Polaris backend server. - # docker run -d --name polaris -p 8090:8090 -p 8091:8091 -p 8093:8093 -p 9090:9090 -p 9091:9091 loads/polaris-server-standalone:1.11.2 - # docker run -d --name polaris -p 8090:8090 -p 8091:8091 -p 8093:8093 -p 9090:9090 -p 9091:9091 loads/polaris-standalone:v1.16.3 + # docker run -d --name polaris \ + # -p 8090:8090 -p 8091:8091 -p 8093:8093 -p 9090:9090 -p 9091:9091 \ + # loads/polaris-server-standalone:1.11.2 + # + # docker run -d --name polaris \ + # -p 8090:8090 -p 8091:8091 -p 8093:8093 -p 9090:9090 -p 9091:9091 \ + # loads/polaris-standalone:v1.16.3 polaris: image: loads/polaris-standalone:v1.17.2 ports: @@ -134,7 +144,14 @@ jobs: - 9090:9090 - 9091:9091 - # Oracle 11g server + # Oracle 11g server. + # docker run \ + # -e ORACLE_ALLOW_REMOTE=true \ + # -e ORACLE_SID=XE \ + # -e ORACLE_DB_USER_NAME=system \ + # -e ORACLE_DB_PASSWORD=oracle \ + # -p 1521:1521 \ + # loads/oracle-xe-11g-r2:11.2.0 oracle-server: image: loads/oracle-xe-11g-r2:11.2.0 env: diff --git a/contrib/drivers/clickhouse/clickhouse.go b/contrib/drivers/clickhouse/clickhouse.go index 33665c0eb..e0dbc24fa 100644 --- a/contrib/drivers/clickhouse/clickhouse.go +++ b/contrib/drivers/clickhouse/clickhouse.go @@ -9,25 +9,10 @@ package clickhouse import ( "context" - "database/sql" - "database/sql/driver" "errors" - "fmt" - "net/url" - "strings" - "time" - - "github.com/ClickHouse/clickhouse-go/v2" - "github.com/google/uuid" - "github.com/shopspring/decimal" "github.com/gogf/gf/v2/database/gdb" - "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/os/gtime" - "github.com/gogf/gf/v2/text/gregex" - "github.com/gogf/gf/v2/util/gutil" ) // Driver is the driver for postgresql database. @@ -71,386 +56,9 @@ func (d *Driver) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) { }, nil } -// Open creates and returns an underlying sql.DB object for clickhouse. -func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { - source := config.Link - // clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 - if config.Link != "" { - // ============================================================================ - // Deprecated from v2.2.0. - // ============================================================================ - // Custom changing the schema in runtime. - if config.Name != "" { - source, _ = gregex.ReplaceString(replaceSchemaPattern, "@$1/"+config.Name, config.Link) - } else { - // If no schema, the link is matched for replacement - dbName, _ := gregex.MatchString(replaceSchemaPattern, config.Link) - if len(dbName) > 0 { - config.Name = dbName[len(dbName)-1] - } - } - } else { - if config.Pass != "" { - source = fmt.Sprintf( - "clickhouse://%s:%s@%s:%s/%s?debug=%t", - config.User, url.PathEscape(config.Pass), - config.Host, config.Port, config.Name, config.Debug, - ) - } else { - source = fmt.Sprintf( - "clickhouse://%s@%s:%s/%s?debug=%t", - config.User, config.Host, config.Port, config.Name, config.Debug, - ) - } - if config.Extra != "" { - source = fmt.Sprintf("%s&%s", source, config.Extra) - } - } - if db, err = sql.Open(driverName, source); err != nil { - err = gerror.WrapCodef( - gcode.CodeDbOperationError, err, - `sql.Open failed for driver "%s" by source "%s"`, driverName, source, - ) - return nil, err - } - return -} - -// 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 gdb.Result - link, err := d.SlaveLink(schema...) - if err != nil { - return nil, err - } - query := fmt.Sprintf("select name from `system`.tables where database = '%s'", d.GetConfig().Name) - result, err = d.DoSelect(ctx, link, query) - if err != nil { - return - } - for _, m := range result { - tables = append(tables, m["name"].String()) - } - return -} - -// 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]*gdb.TableField, err error) { - var ( - result gdb.Result - link gdb.Link - usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) - ) - if link, err = d.SlaveLink(usedSchema); err != nil { - return nil, err - } - var ( - columns = "name,position,default_expression,comment,type,is_in_partition_key,is_in_sorting_key,is_in_primary_key,is_in_sampling_key" - getColumnsSql = fmt.Sprintf( - "select %s from `system`.columns c where `table` = '%s'", - columns, table, - ) - ) - result, err = d.DoSelect(ctx, link, getColumnsSql) - if err != nil { - return nil, err - } - fields = make(map[string]*gdb.TableField) - for _, m := range result { - var ( - isNull = false - fieldType = m["type"].String() - ) - // in clickhouse , field type like is Nullable(int) - fieldsResult, _ := gregex.MatchString(`^Nullable\((.*?)\)`, fieldType) - if len(fieldsResult) == 2 { - isNull = true - fieldType = fieldsResult[1] - } - position := m["position"].Int() - if result[0]["position"].Int() != 0 { - position -= 1 - } - fields[m["name"].String()] = &gdb.TableField{ - Index: position, - Name: m["name"].String(), - Default: m["default_expression"].Val(), - Comment: m["comment"].String(), - // Key: m["Key"].String(), - Type: fieldType, - Null: isNull, - } - } - return fields, nil -} - -// PingMaster pings the master node to check authentication or keeps the connection alive. -func (d *Driver) PingMaster() error { - conn, err := d.Master() - if err != nil { - return err - } - return d.ping(conn) -} - -// PingSlave pings the slave node to check authentication or keeps the connection alive. -func (d *Driver) PingSlave() error { - conn, err := d.Slave() - if err != nil { - return err - } - return d.ping(conn) -} - -// ping Returns the Clickhouse specific error. -func (d *Driver) ping(conn *sql.DB) error { - err := conn.Ping() - if exception, ok := err.(*clickhouse.Exception); ok { - return fmt.Errorf("[%d]%s", exception.Code, exception.Message) - } - return err -} - -// DoFilter handles the sql before posts it to database. -func (d *Driver) DoFilter( - ctx context.Context, link gdb.Link, originSql string, args []interface{}, -) (newSql string, newArgs []interface{}, err error) { - if len(args) == 0 { - return originSql, args, nil - } - // Convert placeholder char '?' to string "$x". - var index int - originSql, _ = gregex.ReplaceStringFunc(`\?`, originSql, func(s string) string { - index++ - return fmt.Sprintf(`$%d`, index) - }) - - // Only SQL generated through the framework is processed. - if !d.getNeedParsedSqlFromCtx(ctx) { - return originSql, args, nil - } - - // replace STD SQL to Clickhouse SQL grammar - modeRes, err := gregex.MatchString(filterTypePattern, strings.TrimSpace(originSql)) - if err != nil { - return "", nil, err - } - if len(modeRes) == 0 { - return originSql, args, nil - } - - // Only delete/ UPDATE statements require filter - switch strings.ToUpper(modeRes[0]) { - case "UPDATE": - // MySQL eg: UPDATE table_name SET field1=new-value1, field2=new-value2 [WHERE Clause] - // Clickhouse eg: ALTER TABLE [db.]table UPDATE column1 = expr1 [, ...] WHERE filter_expr - newSql, err = gregex.ReplaceStringFuncMatch( - updateFilterPattern, originSql, - func(s []string) string { - return fmt.Sprintf("ALTER TABLE %s UPDATE", s[1]) - }, - ) - if err != nil { - return "", nil, err - } - return newSql, args, nil - - case "DELETE": - // MySQL eg: DELETE FROM table_name [WHERE Clause] - // Clickhouse eg: ALTER TABLE [db.]table [ON CLUSTER cluster] DELETE WHERE filter_expr - newSql, err = gregex.ReplaceStringFuncMatch( - deleteFilterPattern, originSql, - func(s []string) string { - return fmt.Sprintf("ALTER TABLE %s DELETE", s[1]) - }, - ) - if err != nil { - return "", nil, err - } - return newSql, args, nil - - } - return originSql, args, nil -} - -// DoCommit commits current sql and arguments to underlying sql driver. -func (d *Driver) DoCommit(ctx context.Context, in gdb.DoCommitInput) (out gdb.DoCommitOutput, err error) { - ctx = d.InjectIgnoreResult(ctx) - return d.Core.DoCommit(ctx, in) -} - -// DoInsert inserts or updates data forF given table. -func (d *Driver) DoInsert( - ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, -) (result sql.Result, err error) { - var ( - keys []string // Field names. - valueHolder = make([]string, 0) - ) - // Handle the field names and placeholders. - for k := range list[0] { - keys = append(keys, k) - valueHolder = append(valueHolder, "?") - } - // Prepare the batch result pointer. - var ( - charL, charR = d.Core.GetChars() - keysStr = charL + strings.Join(keys, charR+","+charL) + charR - holderStr = strings.Join(valueHolder, ",") - tx gdb.TX - stmt *gdb.Stmt - ) - tx, err = d.Core.Begin(ctx) - if err != nil { - return - } - // It here uses defer to guarantee transaction be committed or roll-backed. - defer func() { - if err == nil { - _ = tx.Commit() - } else { - _ = tx.Rollback() - } - }() - stmt, err = tx.Prepare(fmt.Sprintf( - "INSERT INTO %s(%s) VALUES (%s)", - d.QuotePrefixTableName(table), keysStr, - holderStr, - )) - if err != nil { - return - } - for i := 0; i < len(list); i++ { - // Values that will be committed to underlying database driver. - params := make([]interface{}, 0) - for _, k := range keys { - params = append(params, list[i][k]) - } - // Prepare is allowed to execute only once in a transaction opened by clickhouse - result, err = stmt.ExecContext(ctx, params...) - if err != nil { - return - } - } - return -} - -// ConvertValueForField converts value to the type of the record field. -func (d *Driver) ConvertValueForField(ctx context.Context, fieldType string, fieldValue interface{}) (interface{}, error) { - switch itemValue := fieldValue.(type) { - case time.Time: - // If the time is zero, it then updates it to nil, - // which will insert/update the value to database as "null". - if itemValue.IsZero() { - return nil, nil - } - - case uuid.UUID: - return itemValue, nil - - case *time.Time: - // If the time is zero, it then updates it to nil, - // which will insert/update the value to database as "null". - if itemValue == nil || itemValue.IsZero() { - return nil, nil - } - return itemValue, nil - - case gtime.Time: - // If the time is zero, it then updates it to nil, - // which will insert/update the value to database as "null". - if itemValue.IsZero() { - return nil, nil - } - // for gtime type, needs to get time.Time - return itemValue.Time, nil - - case *gtime.Time: - // for gtime type, needs to get time.Time - if itemValue != nil { - return itemValue.Time, nil - } - // If the time is zero, it then updates it to nil, - // which will insert/update the value to database as "null". - if itemValue == nil || itemValue.IsZero() { - return nil, nil - } - - case decimal.Decimal: - return itemValue, nil - - case *decimal.Decimal: - if itemValue != nil { - return *itemValue, nil - } - return nil, nil - - default: - // if the other type implements valuer for the driver package - // the converted result is used - // otherwise the interface data is committed - valuer, ok := itemValue.(driver.Valuer) - if !ok { - return itemValue, nil - } - convertedValue, err := valuer.Value() - if err != nil { - return nil, err - } - return convertedValue, nil - } - return fieldValue, nil -} - -// DoDelete does "DELETE FROM ... " statement for the table. -func (d *Driver) DoDelete(ctx context.Context, link gdb.Link, table string, condition string, args ...interface{}) (result sql.Result, err error) { - ctx = d.injectNeedParsedSql(ctx) - return d.Core.DoDelete(ctx, link, table, condition, args...) -} - -// DoUpdate does "UPDATE ... " statement for the table. -func (d *Driver) DoUpdate(ctx context.Context, link gdb.Link, table string, data interface{}, condition string, args ...interface{}) (result sql.Result, err error) { - ctx = d.injectNeedParsedSql(ctx) - return d.Core.DoUpdate(ctx, link, table, data, condition, args...) -} - -// InsertIgnore Other queries for modifying data parts are not supported: REPLACE, MERGE, UPSERT, INSERT UPDATE. -func (d *Driver) InsertIgnore(ctx context.Context, table string, data interface{}, batch ...int) (sql.Result, error) { - return nil, errUnsupportedInsertIgnore -} - -// InsertAndGetId Other queries for modifying data parts are not supported: REPLACE, MERGE, UPSERT, INSERT UPDATE. -func (d *Driver) InsertAndGetId(ctx context.Context, table string, data interface{}, batch ...int) (int64, error) { - return 0, errUnsupportedInsertGetId -} - -// Replace Other queries for modifying data parts are not supported: REPLACE, MERGE, UPSERT, INSERT UPDATE. -func (d *Driver) Replace(ctx context.Context, table string, data interface{}, batch ...int) (sql.Result, error) { - return nil, errUnsupportedReplace -} - -// Begin starts and returns the transaction object. -func (d *Driver) Begin(ctx context.Context) (tx gdb.TX, err error) { - return nil, errUnsupportedBegin -} - -// Transaction wraps the transaction logic using function `f`. -func (d *Driver) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) error { - return errUnsupportedTransaction -} - func (d *Driver) injectNeedParsedSql(ctx context.Context) context.Context { if ctx.Value(needParsedSqlInCtx) != nil { return ctx } return context.WithValue(ctx, needParsedSqlInCtx, true) } - -func (d *Driver) getNeedParsedSqlFromCtx(ctx context.Context) bool { - if ctx.Value(needParsedSqlInCtx) != nil { - return true - } - return false -} diff --git a/contrib/drivers/clickhouse/clickhouse_convert.go b/contrib/drivers/clickhouse/clickhouse_convert.go new file mode 100644 index 000000000..7540bb042 --- /dev/null +++ b/contrib/drivers/clickhouse/clickhouse_convert.go @@ -0,0 +1,85 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + "database/sql/driver" + "time" + + "github.com/google/uuid" + "github.com/shopspring/decimal" + + "github.com/gogf/gf/v2/os/gtime" +) + +// ConvertValueForField converts value to the type of the record field. +func (d *Driver) ConvertValueForField(ctx context.Context, fieldType string, fieldValue interface{}) (interface{}, error) { + switch itemValue := fieldValue.(type) { + case time.Time: + // If the time is zero, it then updates it to nil, + // which will insert/update the value to database as "null". + if itemValue.IsZero() { + return nil, nil + } + + case uuid.UUID: + return itemValue, nil + + case *time.Time: + // If the time is zero, it then updates it to nil, + // which will insert/update the value to database as "null". + if itemValue == nil || itemValue.IsZero() { + return nil, nil + } + return itemValue, nil + + case gtime.Time: + // If the time is zero, it then updates it to nil, + // which will insert/update the value to database as "null". + if itemValue.IsZero() { + return nil, nil + } + // for gtime type, needs to get time.Time + return itemValue.Time, nil + + case *gtime.Time: + // for gtime type, needs to get time.Time + if itemValue != nil { + return itemValue.Time, nil + } + // If the time is zero, it then updates it to nil, + // which will insert/update the value to database as "null". + if itemValue == nil || itemValue.IsZero() { + return nil, nil + } + + case decimal.Decimal: + return itemValue, nil + + case *decimal.Decimal: + if itemValue != nil { + return *itemValue, nil + } + return nil, nil + + default: + // if the other type implements valuer for the driver package + // the converted result is used + // otherwise the interface data is committed + valuer, ok := itemValue.(driver.Valuer) + if !ok { + return itemValue, nil + } + convertedValue, err := valuer.Value() + if err != nil { + return nil, err + } + return convertedValue, nil + } + return fieldValue, nil +} diff --git a/contrib/drivers/clickhouse/clickhouse_do_commit.go b/contrib/drivers/clickhouse/clickhouse_do_commit.go new file mode 100644 index 000000000..6bd20b454 --- /dev/null +++ b/contrib/drivers/clickhouse/clickhouse_do_commit.go @@ -0,0 +1,19 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" +) + +// DoCommit commits current sql and arguments to underlying sql driver. +func (d *Driver) DoCommit(ctx context.Context, in gdb.DoCommitInput) (out gdb.DoCommitOutput, err error) { + ctx = d.InjectIgnoreResult(ctx) + return d.Core.DoCommit(ctx, in) +} diff --git a/contrib/drivers/clickhouse/clickhouse_do_delete.go b/contrib/drivers/clickhouse/clickhouse_do_delete.go new file mode 100644 index 000000000..e75d213fc --- /dev/null +++ b/contrib/drivers/clickhouse/clickhouse_do_delete.go @@ -0,0 +1,20 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + "database/sql" + + "github.com/gogf/gf/v2/database/gdb" +) + +// DoDelete does "DELETE FROM ... " statement for the table. +func (d *Driver) DoDelete(ctx context.Context, link gdb.Link, table string, condition string, args ...interface{}) (result sql.Result, err error) { + ctx = d.injectNeedParsedSql(ctx) + return d.Core.DoDelete(ctx, link, table, condition, args...) +} diff --git a/contrib/drivers/clickhouse/clickhouse_do_filter.go b/contrib/drivers/clickhouse/clickhouse_do_filter.go new file mode 100644 index 000000000..bb2ca2a17 --- /dev/null +++ b/contrib/drivers/clickhouse/clickhouse_do_filter.go @@ -0,0 +1,85 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + "fmt" + "strings" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/text/gregex" +) + +// DoFilter handles the sql before posts it to database. +func (d *Driver) DoFilter( + ctx context.Context, link gdb.Link, originSql string, args []interface{}, +) (newSql string, newArgs []interface{}, err error) { + if len(args) == 0 { + return originSql, args, nil + } + // Convert placeholder char '?' to string "$x". + var index int + originSql, _ = gregex.ReplaceStringFunc(`\?`, originSql, func(s string) string { + index++ + return fmt.Sprintf(`$%d`, index) + }) + + // Only SQL generated through the framework is processed. + if !d.getNeedParsedSqlFromCtx(ctx) { + return originSql, args, nil + } + + // replace STD SQL to Clickhouse SQL grammar + modeRes, err := gregex.MatchString(filterTypePattern, strings.TrimSpace(originSql)) + if err != nil { + return "", nil, err + } + if len(modeRes) == 0 { + return originSql, args, nil + } + + // Only delete/ UPDATE statements require filter + switch strings.ToUpper(modeRes[0]) { + case "UPDATE": + // MySQL eg: UPDATE table_name SET field1=new-value1, field2=new-value2 [WHERE Clause] + // Clickhouse eg: ALTER TABLE [db.]table UPDATE column1 = expr1 [, ...] WHERE filter_expr + newSql, err = gregex.ReplaceStringFuncMatch( + updateFilterPattern, originSql, + func(s []string) string { + return fmt.Sprintf("ALTER TABLE %s UPDATE", s[1]) + }, + ) + if err != nil { + return "", nil, err + } + return newSql, args, nil + + case "DELETE": + // MySQL eg: DELETE FROM table_name [WHERE Clause] + // Clickhouse eg: ALTER TABLE [db.]table [ON CLUSTER cluster] DELETE WHERE filter_expr + newSql, err = gregex.ReplaceStringFuncMatch( + deleteFilterPattern, originSql, + func(s []string) string { + return fmt.Sprintf("ALTER TABLE %s DELETE", s[1]) + }, + ) + if err != nil { + return "", nil, err + } + return newSql, args, nil + + } + return originSql, args, nil +} + +func (d *Driver) getNeedParsedSqlFromCtx(ctx context.Context) bool { + if ctx.Value(needParsedSqlInCtx) != nil { + return true + } + return false +} diff --git a/contrib/drivers/clickhouse/clickhouse_do_insert.go b/contrib/drivers/clickhouse/clickhouse_do_insert.go new file mode 100644 index 000000000..36625b1f7 --- /dev/null +++ b/contrib/drivers/clickhouse/clickhouse_do_insert.go @@ -0,0 +1,72 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/gogf/gf/v2/database/gdb" +) + +// DoInsert inserts or updates data forF given table. +func (d *Driver) DoInsert( + ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, +) (result sql.Result, err error) { + var ( + keys []string // Field names. + valueHolder = make([]string, 0) + ) + // Handle the field names and placeholders. + for k := range list[0] { + keys = append(keys, k) + valueHolder = append(valueHolder, "?") + } + // Prepare the batch result pointer. + var ( + charL, charR = d.Core.GetChars() + keysStr = charL + strings.Join(keys, charR+","+charL) + charR + holderStr = strings.Join(valueHolder, ",") + tx gdb.TX + stmt *gdb.Stmt + ) + tx, err = d.Core.Begin(ctx) + if err != nil { + return + } + // It here uses defer to guarantee transaction be committed or roll-backed. + defer func() { + if err == nil { + _ = tx.Commit() + } else { + _ = tx.Rollback() + } + }() + stmt, err = tx.Prepare(fmt.Sprintf( + "INSERT INTO %s(%s) VALUES (%s)", + d.QuotePrefixTableName(table), keysStr, + holderStr, + )) + if err != nil { + return + } + for i := 0; i < len(list); i++ { + // Values that will be committed to underlying database driver. + params := make([]interface{}, 0) + for _, k := range keys { + params = append(params, list[i][k]) + } + // Prepare is allowed to execute only once in a transaction opened by clickhouse + result, err = stmt.ExecContext(ctx, params...) + if err != nil { + return + } + } + return +} diff --git a/contrib/drivers/clickhouse/clickhouse_do_update.go b/contrib/drivers/clickhouse/clickhouse_do_update.go new file mode 100644 index 000000000..b1d82e66c --- /dev/null +++ b/contrib/drivers/clickhouse/clickhouse_do_update.go @@ -0,0 +1,20 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + "database/sql" + + "github.com/gogf/gf/v2/database/gdb" +) + +// DoUpdate does "UPDATE ... " statement for the table. +func (d *Driver) DoUpdate(ctx context.Context, link gdb.Link, table string, data interface{}, condition string, args ...interface{}) (result sql.Result, err error) { + ctx = d.injectNeedParsedSql(ctx) + return d.Core.DoUpdate(ctx, link, table, data, condition, args...) +} diff --git a/contrib/drivers/clickhouse/clickhouse_insert.go b/contrib/drivers/clickhouse/clickhouse_insert.go new file mode 100644 index 000000000..f7b38a2a0 --- /dev/null +++ b/contrib/drivers/clickhouse/clickhouse_insert.go @@ -0,0 +1,27 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + "database/sql" +) + +// InsertIgnore Other queries for modifying data parts are not supported: REPLACE, MERGE, UPSERT, INSERT UPDATE. +func (d *Driver) InsertIgnore(ctx context.Context, table string, data interface{}, batch ...int) (sql.Result, error) { + return nil, errUnsupportedInsertIgnore +} + +// InsertAndGetId Other queries for modifying data parts are not supported: REPLACE, MERGE, UPSERT, INSERT UPDATE. +func (d *Driver) InsertAndGetId(ctx context.Context, table string, data interface{}, batch ...int) (int64, error) { + return 0, errUnsupportedInsertGetId +} + +// Replace Other queries for modifying data parts are not supported: REPLACE, MERGE, UPSERT, INSERT UPDATE. +func (d *Driver) Replace(ctx context.Context, table string, data interface{}, batch ...int) (sql.Result, error) { + return nil, errUnsupportedReplace +} diff --git a/contrib/drivers/clickhouse/clickhouse_open.go b/contrib/drivers/clickhouse/clickhouse_open.go new file mode 100644 index 000000000..15eaf147b --- /dev/null +++ b/contrib/drivers/clickhouse/clickhouse_open.go @@ -0,0 +1,63 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "database/sql" + "fmt" + "net/url" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gregex" +) + +// Open creates and returns an underlying sql.DB object for clickhouse. +func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { + source := config.Link + // clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 + if config.Link != "" { + // ============================================================================ + // Deprecated from v2.2.0. + // ============================================================================ + // Custom changing the schema in runtime. + if config.Name != "" { + source, _ = gregex.ReplaceString(replaceSchemaPattern, "@$1/"+config.Name, config.Link) + } else { + // If no schema, the link is matched for replacement + dbName, _ := gregex.MatchString(replaceSchemaPattern, config.Link) + if len(dbName) > 0 { + config.Name = dbName[len(dbName)-1] + } + } + } else { + if config.Pass != "" { + source = fmt.Sprintf( + "clickhouse://%s:%s@%s:%s/%s?debug=%t", + config.User, url.PathEscape(config.Pass), + config.Host, config.Port, config.Name, config.Debug, + ) + } else { + source = fmt.Sprintf( + "clickhouse://%s@%s:%s/%s?debug=%t", + config.User, config.Host, config.Port, config.Name, config.Debug, + ) + } + if config.Extra != "" { + source = fmt.Sprintf("%s&%s", source, config.Extra) + } + } + if db, err = sql.Open(driverName, source); err != nil { + err = gerror.WrapCodef( + gcode.CodeDbOperationError, err, + `sql.Open failed for driver "%s" by source "%s"`, driverName, source, + ) + return nil, err + } + return +} diff --git a/contrib/drivers/clickhouse/clickhouse_ping.go b/contrib/drivers/clickhouse/clickhouse_ping.go new file mode 100644 index 000000000..4bb2fbddf --- /dev/null +++ b/contrib/drivers/clickhouse/clickhouse_ping.go @@ -0,0 +1,41 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "database/sql" + "fmt" + + "github.com/ClickHouse/clickhouse-go/v2" +) + +// PingMaster pings the master node to check authentication or keeps the connection alive. +func (d *Driver) PingMaster() error { + conn, err := d.Master() + if err != nil { + return err + } + return d.ping(conn) +} + +// PingSlave pings the slave node to check authentication or keeps the connection alive. +func (d *Driver) PingSlave() error { + conn, err := d.Slave() + if err != nil { + return err + } + return d.ping(conn) +} + +// ping Returns the Clickhouse specific error. +func (d *Driver) ping(conn *sql.DB) error { + err := conn.Ping() + if exception, ok := err.(*clickhouse.Exception); ok { + return fmt.Errorf("[%d]%s", exception.Code, exception.Message) + } + return err +} diff --git a/contrib/drivers/clickhouse/clickhouse_table_fields.go b/contrib/drivers/clickhouse/clickhouse_table_fields.go new file mode 100644 index 000000000..9617fd9ca --- /dev/null +++ b/contrib/drivers/clickhouse/clickhouse_table_fields.go @@ -0,0 +1,70 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + "fmt" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/util/gutil" +) + +const ( + tableFieldsColumns = `name,position,default_expression,comment,type,is_in_partition_key,is_in_sorting_key,is_in_primary_key,is_in_sampling_key` +) + +// 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]*gdb.TableField, err error) { + var ( + result gdb.Result + link gdb.Link + usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) + ) + if link, err = d.SlaveLink(usedSchema); err != nil { + return nil, err + } + var ( + getColumnsSql = fmt.Sprintf( + "select %s from `system`.columns c where `table` = '%s'", + tableFieldsColumns, table, + ) + ) + result, err = d.DoSelect(ctx, link, getColumnsSql) + if err != nil { + return nil, err + } + fields = make(map[string]*gdb.TableField) + for _, m := range result { + var ( + isNull = false + fieldType = m["type"].String() + ) + // in clickhouse , field type like is Nullable(int) + fieldsResult, _ := gregex.MatchString(`^Nullable\((.*?)\)`, fieldType) + if len(fieldsResult) == 2 { + isNull = true + fieldType = fieldsResult[1] + } + position := m["position"].Int() + if result[0]["position"].Int() != 0 { + position -= 1 + } + fields[m["name"].String()] = &gdb.TableField{ + Index: position, + Name: m["name"].String(), + Default: m["default_expression"].Val(), + Comment: m["comment"].String(), + // Key: m["Key"].String(), + Type: fieldType, + Null: isNull, + } + } + return fields, nil +} diff --git a/contrib/drivers/clickhouse/clickhouse_tables.go b/contrib/drivers/clickhouse/clickhouse_tables.go new file mode 100644 index 000000000..ecb8b18b2 --- /dev/null +++ b/contrib/drivers/clickhouse/clickhouse_tables.go @@ -0,0 +1,36 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + "fmt" + + "github.com/gogf/gf/v2/database/gdb" +) + +const ( + tablesSqlTmp = "select name from `system`.tables where database = '%s'" +) + +// 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 gdb.Result + link, err := d.SlaveLink(schema...) + if err != nil { + return nil, err + } + result, err = d.DoSelect(ctx, link, fmt.Sprintf(tablesSqlTmp, d.GetConfig().Name)) + if err != nil { + return + } + for _, m := range result { + tables = append(tables, m["name"].String()) + } + return +} diff --git a/contrib/drivers/clickhouse/clickhouse_transaction.go b/contrib/drivers/clickhouse/clickhouse_transaction.go new file mode 100644 index 000000000..f3f3820a4 --- /dev/null +++ b/contrib/drivers/clickhouse/clickhouse_transaction.go @@ -0,0 +1,23 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" +) + +// Begin starts and returns the transaction object. +func (d *Driver) Begin(ctx context.Context) (tx gdb.TX, err error) { + return nil, errUnsupportedBegin +} + +// Transaction wraps the transaction logic using function `f`. +func (d *Driver) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) error { + return errUnsupportedTransaction +} diff --git a/contrib/drivers/clickhouse/clickhouse_db_test.go b/contrib/drivers/clickhouse/clickhouse_z_unit_db_test.go similarity index 100% rename from contrib/drivers/clickhouse/clickhouse_db_test.go rename to contrib/drivers/clickhouse/clickhouse_z_unit_db_test.go diff --git a/contrib/drivers/clickhouse/clickhouse_init_test.go b/contrib/drivers/clickhouse/clickhouse_z_unit_init_test.go similarity index 100% rename from contrib/drivers/clickhouse/clickhouse_init_test.go rename to contrib/drivers/clickhouse/clickhouse_z_unit_init_test.go diff --git a/contrib/drivers/clickhouse/clickhouse_issue_test.go b/contrib/drivers/clickhouse/clickhouse_z_unit_issue_test.go similarity index 100% rename from contrib/drivers/clickhouse/clickhouse_issue_test.go rename to contrib/drivers/clickhouse/clickhouse_z_unit_issue_test.go diff --git a/contrib/drivers/clickhouse/clickhouse_model_test.go b/contrib/drivers/clickhouse/clickhouse_z_unit_model_test.go similarity index 100% rename from contrib/drivers/clickhouse/clickhouse_model_test.go rename to contrib/drivers/clickhouse/clickhouse_z_unit_model_test.go diff --git a/contrib/drivers/clickhouse/clickhouse_test.go b/contrib/drivers/clickhouse/clickhouse_z_unit_test.go similarity index 100% rename from contrib/drivers/clickhouse/clickhouse_test.go rename to contrib/drivers/clickhouse/clickhouse_z_unit_test.go diff --git a/contrib/drivers/dm/dm.go b/contrib/drivers/dm/dm.go index 3855003b1..3bfb01cb9 100644 --- a/contrib/drivers/dm/dm.go +++ b/contrib/drivers/dm/dm.go @@ -8,24 +8,10 @@ package dm import ( - "context" - "database/sql" - "fmt" - "net/url" - "strings" - "time" - _ "gitee.com/chunanyong/dm" "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/errors/gcode" - "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gtime" - "github.com/gogf/gf/v2/text/gregex" - "github.com/gogf/gf/v2/text/gstr" - "github.com/gogf/gf/v2/util/gconv" - "github.com/gogf/gf/v2/util/gutil" ) type Driver struct { @@ -61,390 +47,7 @@ func (d *Driver) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) { }, nil } -// Open creates and returns an underlying sql.DB object for pgsql. -func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { - var ( - source string - underlyingDriverName = "dm" - ) - if config.Name == "" { - return nil, fmt.Errorf( - `dm.Open failed for driver "%s" without DB Name`, underlyingDriverName, - ) - } - // Data Source Name of DM8: - // dm://userName:password@ip:port/dbname - // dm://userName:password@DW/dbname?DW=(192.168.1.1:5236,192.168.1.2:5236) - var domain string - if config.Port != "" { - domain = fmt.Sprintf("%s:%s", config.Host, config.Port) - } else { - domain = config.Host - } - source = fmt.Sprintf( - "dm://%s:%s@%s/%s?charset=%s&schema=%s", - config.User, config.Pass, domain, config.Name, config.Charset, config.Name, - ) - // Demo of timezone setting: - // &loc=Asia/Shanghai - if config.Timezone != "" { - if strings.Contains(config.Timezone, "/") { - config.Timezone = url.QueryEscape(config.Timezone) - } - source = fmt.Sprintf("%s&loc%s", source, config.Timezone) - } - if config.Extra != "" { - source = fmt.Sprintf("%s&%s", source, config.Extra) - } - - if db, err = sql.Open(underlyingDriverName, source); err != nil { - err = gerror.WrapCodef( - gcode.CodeDbOperationError, err, - `dm.Open failed for driver "%s" by source "%s"`, underlyingDriverName, source, - ) - return nil, err - } - return -} - // GetChars returns the security char for this type of database. func (d *Driver) GetChars() (charLeft string, charRight string) { return quoteChar, quoteChar } - -// 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 gdb.Result - // When schema is empty, return the default link - link, err := d.SlaveLink(schema...) - if err != nil { - return nil, err - } - // The link has been distinguished and no longer needs to judge the owner - result, err = d.DoSelect( - ctx, link, `SELECT * FROM ALL_TABLES`, - ) - if err != nil { - return - } - for _, m := range result { - if v, ok := m["IOT_NAME"]; ok { - tables = append(tables, v.String()) - } - } - return -} - -// 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]*gdb.TableField, err error) { - var ( - result gdb.Result - link gdb.Link - // When no schema is specified, the configuration item is returned by default - usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) - ) - // When usedSchema is empty, return the default link - if link, err = d.SlaveLink(usedSchema); err != nil { - return nil, err - } - // The link has been distinguished and no longer needs to judge the owner - result, err = d.DoSelect( - ctx, link, - fmt.Sprintf( - `SELECT * FROM ALL_TAB_COLUMNS WHERE Table_Name= '%s' AND OWNER = '%s'`, - strings.ToUpper(table), - strings.ToUpper(d.GetSchema()), - ), - ) - if err != nil { - return nil, err - } - fields = make(map[string]*gdb.TableField) - for i, m := range result { - // m[NULLABLE] returns "N" "Y" - // "N" means not null - // "Y" means could be null - var nullable bool - if m["NULLABLE"].String() != "N" { - nullable = true - } - fields[m["COLUMN_NAME"].String()] = &gdb.TableField{ - Index: i, - Name: m["COLUMN_NAME"].String(), - Type: m["DATA_TYPE"].String(), - Null: nullable, - Default: m["DATA_DEFAULT"].Val(), - // Key: m["Key"].String(), - // Extra: m["Extra"].String(), - // Comment: m["Comment"].String(), - } - } - return fields, nil -} - -// ConvertValueForField converts value to the type of the record field. -func (d *Driver) ConvertValueForField(ctx context.Context, fieldType string, fieldValue interface{}) (interface{}, error) { - switch itemValue := fieldValue.(type) { - // dm does not support time.Time, it so here converts it to time string that it supports. - case time.Time: - // If the time is zero, it then updates it to nil, - // which will insert/update the value to database as "null". - if itemValue.IsZero() { - return nil, nil - } - return gtime.New(itemValue).String(), nil - - // dm does not support time.Time, it so here converts it to time string that it supports. - case *time.Time: - // If the time is zero, it then updates it to nil, - // which will insert/update the value to database as "null". - if itemValue == nil || itemValue.IsZero() { - return nil, nil - } - return gtime.New(itemValue).String(), nil - } - - return fieldValue, nil -} - -// DoFilter deals with the sql string before commits it to underlying sql driver. -func (d *Driver) DoFilter(ctx context.Context, link gdb.Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, err error) { - // There should be no need to capitalize, because it has been done from field processing before - newSql, _ = gregex.ReplaceString(`["\n\t]`, "", sql) - newSql = gstr.ReplaceI(gstr.ReplaceI(newSql, "GROUP_CONCAT", "LISTAGG"), "SEPARATOR", ",") - - // TODO The current approach is too rough. We should deal with the GROUP_CONCAT function and the parsing of the index field from within the select from match. - // (GROUP_CONCAT DM does not approve; index cannot be used as a query column name, and security characters need to be added, such as "index") - l, r := d.GetChars() - if strings.Contains(newSql, "INDEX") || strings.Contains(newSql, "index") { - if !(strings.Contains(newSql, "_INDEX") || strings.Contains(newSql, "_index")) { - newSql = gstr.ReplaceI(newSql, "INDEX", l+"INDEX"+r) - } - } - - // TODO i tried to do but it never work: - // array, err := gregex.MatchAllString(`SELECT (.*INDEX.*) FROM .*`, newSql) - // g.Dump("err:", err) - // g.Dump("array:", array) - // g.Dump("array:", array[0][1]) - - // newSql, err = gregex.ReplaceString(`SELECT (.*INDEX.*) FROM .*`, l+"INDEX"+r, newSql) - // g.Dump("err:", err) - // g.Dump("newSql:", newSql) - - // re, err := regexp.Compile(`.*SELECT (.*INDEX.*) FROM .*`) - // newSql = re.ReplaceAllStringFunc(newSql, func(data string) string { - // fmt.Println("data:", data) - // return data - // }) - - return d.Core.DoFilter( - ctx, - link, - newSql, - args, - ) -} - -// TODO I originally wanted to only convert keywords in select -// 但是我发现 DoQuery 中会对 sql 会对 " " 达梦的安全字符 进行 / 转义,最后还是导致达梦无法正常解析 -// However, I found that DoQuery() will perform / escape on sql with " " Dameng's safe characters, which ultimately caused Dameng to be unable to parse normally. -// But processing in DoFilter() is OK -// func (d *Driver) DoQuery(ctx context.Context, link gdb.Link, sql string, args ...interface{}) (gdb.Result, error) { -// l, r := d.GetChars() -// new := gstr.ReplaceI(sql, "INDEX", l+"INDEX"+r) -// g.Dump("new:", new) -// return d.Core.DoQuery( -// ctx, -// link, -// new, -// args, -// ) -// } - -// DoInsert inserts or updates data forF given table. -func (d *Driver) DoInsert( - ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, -) (result sql.Result, err error) { - switch option.InsertOption { - case gdb.InsertOptionReplace: - // TODO:: Should be Supported - return nil, gerror.NewCode( - gcode.CodeNotSupported, `Replace operation is not supported by dm driver`, - ) - - case gdb.InsertOptionSave: - // This syntax currently only supports design tables whose primary key is ID. - listLength := len(list) - if listLength == 0 { - return nil, gerror.NewCode( - gcode.CodeInvalidRequest, `Save operation list is empty by dm driver`, - ) - } - var ( - keysSort []string - charL, charR = d.GetChars() - ) - // Column names need to be aligned in the syntax - for k := range list[0] { - keysSort = append(keysSort, k) - } - var char = struct { - charL string - charR string - valueCharL string - valueCharR string - duplicateKey string - keys []string - }{ - charL: charL, - charR: charR, - valueCharL: "'", - valueCharR: "'", - // TODO:: Need to dynamically set the primary key of the table - duplicateKey: "ID", - keys: keysSort, - } - - // insertKeys: Handle valid keys that need to be inserted and updated - // insertValues: Handle values that need to be inserted - // updateValues: Handle values that need to be updated - // queryValues: Handle only one insert with column name - insertKeys, insertValues, updateValues, queryValues := parseValue(list[0], char) - // unionValues: Handling values that need to be inserted and updated - unionValues := parseUnion(list[1:], char) - - batchResult := new(gdb.SqlResult) - // parseSql(): - // MERGE INTO {{table}} T1 - // USING ( SELECT {{queryValues}} FROM DUAL - // {{unionValues}} ) T2 - // ON (T1.{{duplicateKey}} = T2.{{duplicateKey}}) - // WHEN NOT MATCHED THEN - // INSERT {{insertKeys}} VALUES {{insertValues}} - // WHEN MATCHED THEN - // UPDATE SET {{updateValues}} - sqlStr := parseSql( - insertKeys, insertValues, updateValues, queryValues, unionValues, table, char.duplicateKey, - ) - r, err := d.DoExec(ctx, link, sqlStr) - 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 - } - return d.Core.DoInsert(ctx, link, table, list, option) -} - -func parseValue(listOne gdb.Map, char struct { - charL string - charR string - valueCharL string - valueCharR string - duplicateKey string - keys []string -}) (insertKeys []string, insertValues []string, updateValues []string, queryValues []string) { - for _, column := range char.keys { - if listOne[column] == nil { - // remove unassigned struct object - continue - } - insertKeys = append(insertKeys, char.charL+column+char.charR) - insertValues = append(insertValues, "T2."+char.charL+column+char.charR) - if column != char.duplicateKey { - updateValues = append( - updateValues, - fmt.Sprintf(`T1.%s = T2.%s`, char.charL+column+char.charR, char.charL+column+char.charR), - ) - } - - saveValue := gconv.String(listOne[column]) - queryValues = append( - queryValues, - fmt.Sprintf( - char.valueCharL+"%s"+char.valueCharR+" AS "+char.charL+"%s"+char.charR, - saveValue, column, - ), - ) - } - return -} - -func parseUnion(list gdb.List, char struct { - charL string - charR string - valueCharL string - valueCharR string - duplicateKey string - keys []string -}) (unionValues []string) { - for _, mapper := range list { - var saveValue []string - for _, column := range char.keys { - if mapper[column] == nil { - continue - } - // va := reflect.ValueOf(mapper[column]) - // ty := reflect.TypeOf(mapper[column]) - // switch ty.Kind() { - // case reflect.String: - // saveValue = append(saveValue, char.valueCharL+va.String()+char.valueCharR) - - // case reflect.Int: - // saveValue = append(saveValue, strconv.FormatInt(va.Int(), 10)) - - // case reflect.Int64: - // saveValue = append(saveValue, strconv.FormatInt(va.Int(), 10)) - - // default: - // // The fish has no chance getting here. - // // Nothing to do. - // } - saveValue = append(saveValue, - fmt.Sprintf( - char.valueCharL+"%s"+char.valueCharR, - gconv.String(mapper[column]), - )) - } - unionValues = append( - unionValues, - fmt.Sprintf(`UNION ALL SELECT %s FROM DUAL`, strings.Join(saveValue, ",")), - ) - } - return -} - -func parseSql( - insertKeys, insertValues, updateValues, queryValues, unionValues []string, table, duplicateKey string, -) (sqlStr string) { - var ( - queryValueStr = strings.Join(queryValues, ",") - unionValueStr = strings.Join(unionValues, " ") - insertKeyStr = strings.Join(insertKeys, ",") - insertValueStr = strings.Join(insertValues, ",") - updateValueStr = strings.Join(updateValues, ",") - pattern = gstr.Trim(` -MERGE INTO %s T1 USING (SELECT %s FROM DUAL %s) T2 ON %s -WHEN NOT MATCHED -THEN -INSERT(%s) VALUES (%s) -WHEN MATCHED -THEN -UPDATE SET %s; -COMMIT; -`) - ) - return fmt.Sprintf( - pattern, - table, queryValueStr, unionValueStr, - fmt.Sprintf("(T1.%s = T2.%s)", duplicateKey, duplicateKey), - insertKeyStr, insertValueStr, updateValueStr, - ) -} diff --git a/contrib/drivers/dm/dm_convert.go b/contrib/drivers/dm/dm_convert.go new file mode 100644 index 000000000..3bbc81647 --- /dev/null +++ b/contrib/drivers/dm/dm_convert.go @@ -0,0 +1,40 @@ +// 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 dm + +import ( + "context" + + "time" + + "github.com/gogf/gf/v2/os/gtime" +) + +// ConvertValueForField converts value to the type of the record field. +func (d *Driver) ConvertValueForField(ctx context.Context, fieldType string, fieldValue interface{}) (interface{}, error) { + switch itemValue := fieldValue.(type) { + // dm does not support time.Time, it so here converts it to time string that it supports. + case time.Time: + // If the time is zero, it then updates it to nil, + // which will insert/update the value to database as "null". + if itemValue.IsZero() { + return nil, nil + } + return gtime.New(itemValue).String(), nil + + // dm does not support time.Time, it so here converts it to time string that it supports. + case *time.Time: + // If the time is zero, it then updates it to nil, + // which will insert/update the value to database as "null". + if itemValue == nil || itemValue.IsZero() { + return nil, nil + } + return gtime.New(itemValue).String(), nil + } + + return fieldValue, nil +} diff --git a/contrib/drivers/dm/dm_do_filter.go b/contrib/drivers/dm/dm_do_filter.go new file mode 100644 index 000000000..2a91e3734 --- /dev/null +++ b/contrib/drivers/dm/dm_do_filter.go @@ -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 dm + +import ( + "context" + + "strings" + + "github.com/gogf/gf/v2/database/gdb" + "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 gdb.Link, sql string, args []interface{}, +) (newSql string, newArgs []interface{}, err error) { + // There should be no need to capitalize, because it has been done from field processing before + newSql, _ = gregex.ReplaceString(`["\n\t]`, "", sql) + newSql = gstr.ReplaceI(gstr.ReplaceI(newSql, "GROUP_CONCAT", "LISTAGG"), "SEPARATOR", ",") + + // TODO The current approach is too rough. We should deal with the GROUP_CONCAT function and the + // parsing of the index field from within the select from match. + // (GROUP_CONCAT DM does not approve; index cannot be used as a query column name, and security characters need to be added, such as "index") + l, r := d.GetChars() + if strings.Contains(newSql, "INDEX") || strings.Contains(newSql, "index") { + if !(strings.Contains(newSql, "_INDEX") || strings.Contains(newSql, "_index")) { + newSql = gstr.ReplaceI(newSql, "INDEX", l+"INDEX"+r) + } + } + + // TODO i tried to do but it never work: + // array, err := gregex.MatchAllString(`SELECT (.*INDEX.*) FROM .*`, newSql) + // g.Dump("err:", err) + // g.Dump("array:", array) + // g.Dump("array:", array[0][1]) + + // newSql, err = gregex.ReplaceString(`SELECT (.*INDEX.*) FROM .*`, l+"INDEX"+r, newSql) + // g.Dump("err:", err) + // g.Dump("newSql:", newSql) + + // re, err := regexp.Compile(`.*SELECT (.*INDEX.*) FROM .*`) + // newSql = re.ReplaceAllStringFunc(newSql, func(data string) string { + // fmt.Println("data:", data) + // return data + // }) + + return d.Core.DoFilter( + ctx, + link, + newSql, + args, + ) +} diff --git a/contrib/drivers/dm/dm_do_insert.go b/contrib/drivers/dm/dm_do_insert.go new file mode 100644 index 000000000..02d7eba64 --- /dev/null +++ b/contrib/drivers/dm/dm_do_insert.go @@ -0,0 +1,207 @@ +// 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 dm + +import ( + "context" + "database/sql" + "fmt" + + "strings" + + "github.com/gogf/gf/v2/database/gdb" + "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" +) + +// DoInsert inserts or updates data forF given table. +func (d *Driver) DoInsert( + ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, +) (result sql.Result, err error) { + switch option.InsertOption { + case gdb.InsertOptionReplace: + // TODO:: Should be Supported + return nil, gerror.NewCode( + gcode.CodeNotSupported, `Replace operation is not supported by dm driver`, + ) + + case gdb.InsertOptionSave: + // This syntax currently only supports design tables whose primary key is ID. + listLength := len(list) + if listLength == 0 { + return nil, gerror.NewCode( + gcode.CodeInvalidRequest, `Save operation list is empty by dm driver`, + ) + } + var ( + keysSort []string + charL, charR = d.GetChars() + ) + // Column names need to be aligned in the syntax + for k := range list[0] { + keysSort = append(keysSort, k) + } + var char = struct { + charL string + charR string + valueCharL string + valueCharR string + duplicateKey string + keys []string + }{ + charL: charL, + charR: charR, + valueCharL: "'", + valueCharR: "'", + // TODO:: Need to dynamically set the primary key of the table + duplicateKey: "ID", + keys: keysSort, + } + + // insertKeys: Handle valid keys that need to be inserted and updated + // insertValues: Handle values that need to be inserted + // updateValues: Handle values that need to be updated + // queryValues: Handle only one insert with column name + insertKeys, insertValues, updateValues, queryValues := parseValue(list[0], char) + // unionValues: Handling values that need to be inserted and updated + unionValues := parseUnion(list[1:], char) + + batchResult := new(gdb.SqlResult) + // parseSql(): + // MERGE INTO {{table}} T1 + // USING ( SELECT {{queryValues}} FROM DUAL + // {{unionValues}} ) T2 + // ON (T1.{{duplicateKey}} = T2.{{duplicateKey}}) + // WHEN NOT MATCHED THEN + // INSERT {{insertKeys}} VALUES {{insertValues}} + // WHEN MATCHED THEN + // UPDATE SET {{updateValues}} + sqlStr := parseSql( + insertKeys, insertValues, updateValues, queryValues, unionValues, table, char.duplicateKey, + ) + r, err := d.DoExec(ctx, link, sqlStr) + 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 + } + return d.Core.DoInsert(ctx, link, table, list, option) +} + +func parseValue(listOne gdb.Map, char struct { + charL string + charR string + valueCharL string + valueCharR string + duplicateKey string + keys []string +}) (insertKeys []string, insertValues []string, updateValues []string, queryValues []string) { + for _, column := range char.keys { + if listOne[column] == nil { + // remove unassigned struct object + continue + } + insertKeys = append(insertKeys, char.charL+column+char.charR) + insertValues = append(insertValues, "T2."+char.charL+column+char.charR) + if column != char.duplicateKey { + updateValues = append( + updateValues, + fmt.Sprintf(`T1.%s = T2.%s`, char.charL+column+char.charR, char.charL+column+char.charR), + ) + } + + saveValue := gconv.String(listOne[column]) + queryValues = append( + queryValues, + fmt.Sprintf( + char.valueCharL+"%s"+char.valueCharR+" AS "+char.charL+"%s"+char.charR, + saveValue, column, + ), + ) + } + return +} + +func parseUnion(list gdb.List, char struct { + charL string + charR string + valueCharL string + valueCharR string + duplicateKey string + keys []string +}) (unionValues []string) { + for _, mapper := range list { + var saveValue []string + for _, column := range char.keys { + if mapper[column] == nil { + continue + } + // va := reflect.ValueOf(mapper[column]) + // ty := reflect.TypeOf(mapper[column]) + // switch ty.Kind() { + // case reflect.String: + // saveValue = append(saveValue, char.valueCharL+va.String()+char.valueCharR) + + // case reflect.Int: + // saveValue = append(saveValue, strconv.FormatInt(va.Int(), 10)) + + // case reflect.Int64: + // saveValue = append(saveValue, strconv.FormatInt(va.Int(), 10)) + + // default: + // // The fish has no chance getting here. + // // Nothing to do. + // } + saveValue = append(saveValue, + fmt.Sprintf( + char.valueCharL+"%s"+char.valueCharR, + gconv.String(mapper[column]), + )) + } + unionValues = append( + unionValues, + fmt.Sprintf(`UNION ALL SELECT %s FROM DUAL`, strings.Join(saveValue, ",")), + ) + } + return +} + +func parseSql( + insertKeys, insertValues, updateValues, queryValues, unionValues []string, table, duplicateKey string, +) (sqlStr string) { + var ( + queryValueStr = strings.Join(queryValues, ",") + unionValueStr = strings.Join(unionValues, " ") + insertKeyStr = strings.Join(insertKeys, ",") + insertValueStr = strings.Join(insertValues, ",") + updateValueStr = strings.Join(updateValues, ",") + pattern = gstr.Trim(` +MERGE INTO %s T1 USING (SELECT %s FROM DUAL %s) T2 ON %s +WHEN NOT MATCHED +THEN +INSERT(%s) VALUES (%s) +WHEN MATCHED +THEN +UPDATE SET %s; +COMMIT; +`) + ) + return fmt.Sprintf( + pattern, + table, queryValueStr, unionValueStr, + fmt.Sprintf("(T1.%s = T2.%s)", duplicateKey, duplicateKey), + insertKeyStr, insertValueStr, updateValueStr, + ) +} diff --git a/contrib/drivers/dm/dm_do_query.go b/contrib/drivers/dm/dm_do_query.go new file mode 100644 index 000000000..e06c412a6 --- /dev/null +++ b/contrib/drivers/dm/dm_do_query.go @@ -0,0 +1,23 @@ +// 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 dm + +// TODO I originally wanted to only convert keywords in select +// 但是我发现 DoQuery 中会对 sql 会对 " " 达梦的安全字符 进行 / 转义,最后还是导致达梦无法正常解析 +// However, I found that DoQuery() will perform / escape on sql with " " Dameng's safe characters, which ultimately caused Dameng to be unable to parse normally. +// But processing in DoFilter() is OK +// func (d *Driver) DoQuery(ctx context.Context, link gdb.Link, sql string, args ...interface{}) (gdb.Result, error) { +// l, r := d.GetChars() +// new := gstr.ReplaceI(sql, "INDEX", l+"INDEX"+r) +// g.Dump("new:", new) +// return d.Core.DoQuery( +// ctx, +// link, +// new, +// args, +// ) +// } diff --git a/contrib/drivers/dm/dm_open.go b/contrib/drivers/dm/dm_open.go new file mode 100644 index 000000000..8c51b5ff1 --- /dev/null +++ b/contrib/drivers/dm/dm_open.go @@ -0,0 +1,65 @@ +// 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 dm + +import ( + "database/sql" + "fmt" + + "net/url" + "strings" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +// Open creates and returns an underlying sql.DB object for pgsql. +func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { + var ( + source string + underlyingDriverName = "dm" + ) + if config.Name == "" { + return nil, fmt.Errorf( + `dm.Open failed for driver "%s" without DB Name`, underlyingDriverName, + ) + } + // Data Source Name of DM8: + // dm://userName:password@ip:port/dbname + // dm://userName:password@DW/dbname?DW=(192.168.1.1:5236,192.168.1.2:5236) + var domain string + if config.Port != "" { + domain = fmt.Sprintf("%s:%s", config.Host, config.Port) + } else { + domain = config.Host + } + source = fmt.Sprintf( + "dm://%s:%s@%s/%s?charset=%s&schema=%s", + config.User, config.Pass, domain, config.Name, config.Charset, config.Name, + ) + // Demo of timezone setting: + // &loc=Asia/Shanghai + if config.Timezone != "" { + if strings.Contains(config.Timezone, "/") { + config.Timezone = url.QueryEscape(config.Timezone) + } + source = fmt.Sprintf("%s&loc%s", source, config.Timezone) + } + if config.Extra != "" { + source = fmt.Sprintf("%s&%s", source, config.Extra) + } + + if db, err = sql.Open(underlyingDriverName, source); err != nil { + err = gerror.WrapCodef( + gcode.CodeDbOperationError, err, + `dm.Open failed for driver "%s" by source "%s"`, underlyingDriverName, source, + ) + return nil, err + } + return +} diff --git a/contrib/drivers/dm/dm_table_fields.go b/contrib/drivers/dm/dm_table_fields.go new file mode 100644 index 000000000..f2c715870 --- /dev/null +++ b/contrib/drivers/dm/dm_table_fields.go @@ -0,0 +1,70 @@ +// 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 dm + +import ( + "context" + "fmt" + + "strings" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/util/gutil" +) + +const ( + tableFieldsSqlTmp = `SELECT * FROM ALL_TAB_COLUMNS WHERE Table_Name= '%s' AND OWNER = '%s'` +) + +// 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]*gdb.TableField, err error) { + var ( + result gdb.Result + link gdb.Link + // When no schema is specified, the configuration item is returned by default + usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) + ) + // When usedSchema is empty, return the default link + if link, err = d.SlaveLink(usedSchema); err != nil { + return nil, err + } + // The link has been distinguished and no longer needs to judge the owner + result, err = d.DoSelect( + ctx, link, + fmt.Sprintf( + tableFieldsSqlTmp, + strings.ToUpper(table), + strings.ToUpper(d.GetSchema()), + ), + ) + if err != nil { + return nil, err + } + fields = make(map[string]*gdb.TableField) + for i, m := range result { + // m[NULLABLE] returns "N" "Y" + // "N" means not null + // "Y" means could be null + var nullable bool + if m["NULLABLE"].String() != "N" { + nullable = true + } + fields[m["COLUMN_NAME"].String()] = &gdb.TableField{ + Index: i, + Name: m["COLUMN_NAME"].String(), + Type: m["DATA_TYPE"].String(), + Null: nullable, + Default: m["DATA_DEFAULT"].Val(), + // Key: m["Key"].String(), + // Extra: m["Extra"].String(), + // Comment: m["Comment"].String(), + } + } + return fields, nil +} diff --git a/contrib/drivers/dm/dm_tables.go b/contrib/drivers/dm/dm_tables.go new file mode 100644 index 000000000..7c7935e54 --- /dev/null +++ b/contrib/drivers/dm/dm_tables.go @@ -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 dm + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" +) + +const ( + tablesSqlTmp = `SELECT * FROM ALL_TABLES` +) + +// 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 gdb.Result + // When schema is empty, return the default link + link, err := d.SlaveLink(schema...) + if err != nil { + return nil, err + } + // The link has been distinguished and no longer needs to judge the owner + result, err = d.DoSelect(ctx, link, tablesSqlTmp) + if err != nil { + return + } + for _, m := range result { + if v, ok := m["IOT_NAME"]; ok { + tables = append(tables, v.String()) + } + } + return +} diff --git a/contrib/drivers/dm/dm_z_basic_test.go b/contrib/drivers/dm/dm_z_unit_basic_test.go similarity index 100% rename from contrib/drivers/dm/dm_z_basic_test.go rename to contrib/drivers/dm/dm_z_unit_basic_test.go diff --git a/contrib/drivers/dm/dm_init_test.go b/contrib/drivers/dm/dm_z_unit_init_test.go similarity index 99% rename from contrib/drivers/dm/dm_init_test.go rename to contrib/drivers/dm/dm_z_unit_init_test.go index bfecf1ecf..ad71189da 100644 --- a/contrib/drivers/dm/dm_init_test.go +++ b/contrib/drivers/dm/dm_z_unit_init_test.go @@ -12,7 +12,6 @@ import ( "strings" "time" - _ "gitee.com/chunanyong/dm" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" diff --git a/contrib/drivers/mssql/mssql.go b/contrib/drivers/mssql/mssql.go index b183e6002..045add1ed 100644 --- a/contrib/drivers/mssql/mssql.go +++ b/contrib/drivers/mssql/mssql.go @@ -12,20 +12,11 @@ package mssql import ( - "context" - "database/sql" - "fmt" - "strconv" - "strings" - _ "github.com/denisenkom/go-mssqldb" "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/errors/gcode" - "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/text/gregex" "github.com/gogf/gf/v2/text/gstr" - "github.com/gogf/gf/v2/util/gutil" ) // Driver is the driver for SQL server database. @@ -43,6 +34,21 @@ func init() { } } +// formatSqlTmp formats sql template string into one line. +func formatSqlTmp(sqlTmp string) string { + var err error + // format sql template string. + sqlTmp, err = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(sqlTmp)) + if err != nil { + panic(err) + } + sqlTmp, err = gregex.ReplaceString(`\s{2,}`, " ", gstr.Trim(sqlTmp)) + if err != nil { + panic(err) + } + return sqlTmp +} + // New create and returns a driver that implements gdb.Driver, which supports operations for Mssql. func New() gdb.Driver { return &Driver{} @@ -56,262 +62,7 @@ func (d *Driver) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) { }, nil } -// Open creates and returns an underlying sql.DB object for mssql. -func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { - var ( - source string - underlyingDriverName = "sqlserver" - ) - if config.Link != "" { - // ============================================================================ - // Deprecated from v2.2.0. - // ============================================================================ - source = config.Link - // Custom changing the schema in runtime. - if config.Name != "" { - source, _ = gregex.ReplaceString(`database=([\w\.\-]+)+`, "database="+config.Name, source) - } - } else { - source = fmt.Sprintf( - "user id=%s;password=%s;server=%s;port=%s;database=%s;encrypt=disable", - config.User, config.Pass, config.Host, config.Port, config.Name, - ) - if config.Extra != "" { - var extraMap map[string]interface{} - if extraMap, err = gstr.Parse(config.Extra); err != nil { - return nil, err - } - for k, v := range extraMap { - source += fmt.Sprintf(`;%s=%s`, k, v) - } - } - } - - 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 -} - // GetChars returns the security char for this type of database. func (d *Driver) GetChars() (charLeft string, charRight string) { return quoteChar, quoteChar } - -// DoFilter deals with the sql string before commits it to underlying sql driver. -func (d *Driver) DoFilter(ctx context.Context, link gdb.Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, err error) { - var index int - // Convert placeholder char '?' to string "@px". - newSql, _ = gregex.ReplaceStringFunc("\\?", sql, func(s string) string { - index++ - return fmt.Sprintf("@p%d", index) - }) - newSql, _ = gregex.ReplaceString("\"", "", newSql) - return d.Core.DoFilter(ctx, link, d.parseSql(newSql), args) -} - -// parseSql does some replacement of the sql before commits it to underlying driver, -// for support of microsoft sql server. -func (d *Driver) parseSql(sql string) string { - // SELECT * FROM USER WHERE ID=1 LIMIT 1 - if m, _ := gregex.MatchString(`^SELECT(.+)LIMIT 1$`, sql); len(m) > 1 { - return fmt.Sprintf(`SELECT TOP 1 %s`, m[1]) - } - // SELECT * FROM USER WHERE AGE>18 ORDER BY ID DESC LIMIT 100, 200 - patten := `^\s*(?i)(SELECT)|(LIMIT\s*(\d+)\s*,\s*(\d+))` - if gregex.IsMatchString(patten, sql) == false { - return sql - } - res, err := gregex.MatchAllString(patten, sql) - if err != nil { - return "" - } - var ( - index = 0 - keyword = strings.TrimSpace(res[index][0]) - ) - index++ - switch strings.ToUpper(keyword) { - case "SELECT": - // LIMIT statement checks. - if len(res) < 2 || - (strings.HasPrefix(res[index][0], "LIMIT") == false && - strings.HasPrefix(res[index][0], "limit") == false) { - break - } - if gregex.IsMatchString("((?i)SELECT)(.+)((?i)LIMIT)", sql) == false { - break - } - // ORDER BY statement checks. - var ( - selectStr = "" - orderStr = "" - haveOrder = gregex.IsMatchString("((?i)SELECT)(.+)((?i)ORDER BY)", sql) - ) - if haveOrder { - queryExpr, _ := gregex.MatchString("((?i)SELECT)(.+)((?i)ORDER BY)", sql) - if len(queryExpr) != 4 || - strings.EqualFold(queryExpr[1], "SELECT") == false || - strings.EqualFold(queryExpr[3], "ORDER BY") == false { - break - } - selectStr = queryExpr[2] - orderExpr, _ := gregex.MatchString("((?i)ORDER BY)(.+)((?i)LIMIT)", sql) - if len(orderExpr) != 4 || - strings.EqualFold(orderExpr[1], "ORDER BY") == false || - strings.EqualFold(orderExpr[3], "LIMIT") == false { - break - } - orderStr = orderExpr[2] - } else { - queryExpr, _ := gregex.MatchString("((?i)SELECT)(.+)((?i)LIMIT)", sql) - if len(queryExpr) != 4 || - strings.EqualFold(queryExpr[1], "SELECT") == false || - strings.EqualFold(queryExpr[3], "LIMIT") == false { - break - } - selectStr = queryExpr[2] - } - first, limit := 0, 0 - for i := 1; i < len(res[index]); i++ { - if len(strings.TrimSpace(res[index][i])) == 0 { - continue - } - - if strings.HasPrefix(res[index][i], "LIMIT") || - strings.HasPrefix(res[index][i], "limit") { - first, _ = strconv.Atoi(res[index][i+1]) - limit, _ = strconv.Atoi(res[index][i+2]) - break - } - } - if haveOrder { - sql = fmt.Sprintf( - "SELECT * FROM "+ - "(SELECT ROW_NUMBER() OVER (ORDER BY %s) as ROWNUMBER_, %s ) as TMP_ "+ - "WHERE TMP_.ROWNUMBER_ > %d AND TMP_.ROWNUMBER_ <= %d", - orderStr, selectStr, first, first+limit, - ) - } else { - if first == 0 { - first = limit - } - sql = fmt.Sprintf( - "SELECT * FROM (SELECT TOP %d * FROM (SELECT TOP %d %s) as TMP1_ ) as TMP2_ ", - limit, first+limit, selectStr, - ) - } - default: - } - return sql -} - -// 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 gdb.Result - link, err := d.SlaveLink(schema...) - if err != nil { - return nil, err - } - - result, err = d.DoSelect( - ctx, link, `SELECT NAME FROM SYSOBJECTS WHERE XTYPE='U' AND STATUS >= 0 ORDER BY NAME`, - ) - if err != nil { - return - } - for _, m := range result { - for _, v := range m { - tables = append(tables, v.String()) - } - } - return -} - -// 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]*gdb.TableField, err error) { - var ( - result gdb.Result - link gdb.Link - usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) - ) - if link, err = d.SlaveLink(usedSchema); err != nil { - return nil, err - } - structureSql := fmt.Sprintf(` -SELECT - a.name Field, - CASE b.name - WHEN 'datetime' THEN 'datetime' - WHEN 'numeric' THEN b.name + '(' + convert(varchar(20), a.xprec) + ',' + convert(varchar(20), a.xscale) + ')' - WHEN 'char' THEN b.name + '(' + convert(varchar(20), a.length)+ ')' - WHEN 'varchar' THEN b.name + '(' + convert(varchar(20), a.length)+ ')' - ELSE b.name + '(' + convert(varchar(20),a.length)+ ')' END AS Type, - CASE WHEN a.isnullable=1 THEN 'YES' ELSE 'NO' end AS [Null], - CASE WHEN exists ( - SELECT 1 FROM sysobjects WHERE xtype='PK' AND name IN ( - SELECT name FROM sysindexes WHERE indid IN ( - SELECT indid FROM sysindexkeys WHERE id = a.id AND colid=a.colid - ) - ) - ) THEN 'PRI' ELSE '' END AS [Key], - CASE WHEN COLUMNPROPERTY(a.id,a.name,'IsIdentity')=1 THEN 'auto_increment' ELSE '' END Extra, - isnull(e.text,'') AS [Default], - isnull(g.[value],'') AS [Comment] -FROM syscolumns a -LEFT JOIN systypes b ON a.xtype=b.xtype AND a.xusertype=b.xusertype -INNER JOIN sysobjects d ON a.id=d.id AND d.xtype='U' AND d.name<>'dtproperties' -LEFT JOIN syscomments e ON a.cdefault=e.id -LEFT JOIN sys.extended_properties g ON a.id=g.major_id AND a.colid=g.minor_id -LEFT JOIN sys.extended_properties f ON d.id=f.major_id AND f.minor_id =0 -WHERE d.name='%s' -ORDER BY a.id,a.colorder`, - table, - ) - structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql)) - result, err = d.DoSelect(ctx, link, structureSql) - if err != nil { - return nil, err - } - fields = make(map[string]*gdb.TableField) - for i, m := range result { - fields[m["Field"].String()] = &gdb.TableField{ - Index: i, - Name: m["Field"].String(), - Type: m["Type"].String(), - Null: m["Null"].Bool(), - Key: m["Key"].String(), - Default: m["Default"].Val(), - Extra: m["Extra"].String(), - Comment: m["Comment"].String(), - } - } - return fields, nil -} - -// DoInsert inserts or updates data forF given table. -func (d *Driver) DoInsert(ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption) (result sql.Result, err error) { - switch option.InsertOption { - case gdb.InsertOptionSave: - return nil, gerror.NewCode( - gcode.CodeNotSupported, - `Save operation is not supported by mssql driver`, - ) - - case gdb.InsertOptionReplace: - return nil, gerror.NewCode( - gcode.CodeNotSupported, - `Replace operation is not supported by mssql driver`, - ) - - default: - return d.Core.DoInsert(ctx, link, table, list, option) - } -} diff --git a/contrib/drivers/mssql/mssql_do_filter.go b/contrib/drivers/mssql/mssql_do_filter.go new file mode 100644 index 000000000..aa97d4584 --- /dev/null +++ b/contrib/drivers/mssql/mssql_do_filter.go @@ -0,0 +1,163 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mssql + +import ( + "context" + "fmt" + + "strconv" + "strings" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" +) + +var ( + selectSqlTmp = `SELECT * FROM (SELECT TOP %d * FROM (SELECT TOP %d %s) as TMP1_ ) as TMP2_ ` + selectWithOrderSqlTmp = ` +SELECT * FROM (SELECT ROW_NUMBER() OVER (ORDER BY %s) as ROWNUMBER_, %s ) as TMP_ +WHERE TMP_.ROWNUMBER_ > %d AND TMP_.ROWNUMBER_ <= %d +` +) + +func init() { + selectWithOrderSqlTmp = formatSqlTmp(selectWithOrderSqlTmp) +} + +// DoFilter deals with the sql string before commits it to underlying sql driver. +func (d *Driver) DoFilter( + ctx context.Context, link gdb.Link, sql string, args []interface{}, +) (newSql string, newArgs []interface{}, err error) { + var index int + // Convert placeholder char '?' to string "@px". + newSql, err = gregex.ReplaceStringFunc("\\?", sql, func(s string) string { + index++ + return fmt.Sprintf("@p%d", index) + }) + if err != nil { + return "", nil, err + } + newSql, err = gregex.ReplaceString("\"", "", newSql) + if err != nil { + return "", nil, err + } + newSql, err = d.parseSql(newSql) + if err != nil { + return "", nil, err + } + newArgs = args + return d.Core.DoFilter(ctx, link, newSql, newArgs) +} + +// parseSql does some replacement of the sql before commits it to underlying driver, +// for support of microsoft sql 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) { + // SELECT * FROM USER WHERE ID=1 LIMIT 1 + match, err := gregex.MatchString(`^SELECT(.+)LIMIT 1$`, toBeCommittedSql) + if err != nil { + return "", err + } + if len(match) > 1 { + return fmt.Sprintf(`SELECT TOP 1 %s`, match[1]), nil + } + + // SELECT * FROM USER WHERE AGE>18 ORDER BY ID DESC LIMIT 100, 200 + patten := `^\s*(?i)(SELECT)|(LIMIT\s*(\d+)\s*,\s*(\d+))` + if gregex.IsMatchString(patten, toBeCommittedSql) == false { + return toBeCommittedSql, nil + } + allMatch, err := gregex.MatchAllString(patten, toBeCommittedSql) + if err != nil { + return "", err + } + var index = 1 + // LIMIT statement checks. + if len(allMatch) < 2 || + (strings.HasPrefix(allMatch[index][0], "LIMIT") == false && + strings.HasPrefix(allMatch[index][0], "limit") == false) { + return toBeCommittedSql, nil + } + if gregex.IsMatchString("((?i)SELECT)(.+)((?i)LIMIT)", toBeCommittedSql) == false { + return toBeCommittedSql, nil + } + // ORDER BY statement checks. + var ( + selectStr = "" + orderStr = "" + haveOrder = gregex.IsMatchString("((?i)SELECT)(.+)((?i)ORDER BY)", toBeCommittedSql) + ) + if haveOrder { + queryExpr, _ := gregex.MatchString("((?i)SELECT)(.+)((?i)ORDER BY)", toBeCommittedSql) + if len(queryExpr) != 4 || + strings.EqualFold(queryExpr[1], "SELECT") == false || + strings.EqualFold(queryExpr[3], "ORDER BY") == false { + return toBeCommittedSql, nil + } + selectStr = queryExpr[2] + orderExpr, _ := gregex.MatchString("((?i)ORDER BY)(.+)((?i)LIMIT)", toBeCommittedSql) + if len(orderExpr) != 4 || + strings.EqualFold(orderExpr[1], "ORDER BY") == false || + strings.EqualFold(orderExpr[3], "LIMIT") == false { + return toBeCommittedSql, nil + } + orderStr = orderExpr[2] + } else { + queryExpr, _ := gregex.MatchString("((?i)SELECT)(.+)((?i)LIMIT)", toBeCommittedSql) + if len(queryExpr) != 4 || + strings.EqualFold(queryExpr[1], "SELECT") == false || + strings.EqualFold(queryExpr[3], "LIMIT") == false { + return toBeCommittedSql, nil + } + selectStr = queryExpr[2] + } + first, limit := 0, 0 + for i := 1; i < len(allMatch[index]); i++ { + if len(strings.TrimSpace(allMatch[index][i])) == 0 { + continue + } + if strings.HasPrefix(allMatch[index][i], "LIMIT") || + strings.HasPrefix(allMatch[index][i], "limit") { + first, _ = strconv.Atoi(allMatch[index][i+1]) + limit, _ = strconv.Atoi(allMatch[index][i+2]) + break + } + } + if haveOrder { + toBeCommittedSql = fmt.Sprintf( + selectWithOrderSqlTmp, + orderStr, selectStr, first, first+limit, + ) + return toBeCommittedSql, nil + } + + if first == 0 { + first = limit + } + toBeCommittedSql = fmt.Sprintf( + selectSqlTmp, + limit, first+limit, selectStr, + ) + return toBeCommittedSql, nil +} diff --git a/contrib/drivers/mssql/mssql_do_insert.go b/contrib/drivers/mssql/mssql_do_insert.go new file mode 100644 index 000000000..e0e221d50 --- /dev/null +++ b/contrib/drivers/mssql/mssql_do_insert.go @@ -0,0 +1,36 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mssql + +import ( + "context" + "database/sql" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +// DoInsert inserts or updates data forF given table. +func (d *Driver) DoInsert(ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption) (result sql.Result, err error) { + switch option.InsertOption { + case gdb.InsertOptionSave: + return nil, gerror.NewCode( + gcode.CodeNotSupported, + `Save operation is not supported by mssql driver`, + ) + + case gdb.InsertOptionReplace: + return nil, gerror.NewCode( + gcode.CodeNotSupported, + `Replace operation is not supported by mssql driver`, + ) + + default: + return d.Core.DoInsert(ctx, link, table, list, option) + } +} diff --git a/contrib/drivers/mssql/mssql_open.go b/contrib/drivers/mssql/mssql_open.go new file mode 100644 index 000000000..9555dc855 --- /dev/null +++ b/contrib/drivers/mssql/mssql_open.go @@ -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 mssql + +import ( + "database/sql" + "fmt" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" +) + +// Open creates and returns an underlying sql.DB object for mssql. +func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { + var ( + source string + underlyingDriverName = "sqlserver" + ) + if config.Link != "" { + // ============================================================================ + // Deprecated from v2.2.0. + // ============================================================================ + source = config.Link + // Custom changing the schema in runtime. + if config.Name != "" { + source, _ = gregex.ReplaceString(`database=([\w\.\-]+)+`, "database="+config.Name, source) + } + } else { + source = fmt.Sprintf( + "user id=%s;password=%s;server=%s;port=%s;database=%s;encrypt=disable", + config.User, config.Pass, config.Host, config.Port, config.Name, + ) + if config.Extra != "" { + var extraMap map[string]interface{} + if extraMap, err = gstr.Parse(config.Extra); err != nil { + return nil, err + } + for k, v := range extraMap { + source += fmt.Sprintf(`;%s=%s`, k, v) + } + } + } + + 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 +} diff --git a/contrib/drivers/mssql/mssql_table_fields.go b/contrib/drivers/mssql/mssql_table_fields.go new file mode 100644 index 000000000..cce605287 --- /dev/null +++ b/contrib/drivers/mssql/mssql_table_fields.go @@ -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 mssql + +import ( + "context" + "fmt" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/util/gutil" +) + +var ( + tableFieldsSqlTmp = ` +SELECT + a.name Field, + CASE b.name + WHEN 'datetime' THEN 'datetime' + WHEN 'numeric' THEN b.name + '(' + convert(varchar(20), a.xprec) + ',' + convert(varchar(20), a.xscale) + ')' + WHEN 'char' THEN b.name + '(' + convert(varchar(20), a.length)+ ')' + WHEN 'varchar' THEN b.name + '(' + convert(varchar(20), a.length)+ ')' + ELSE b.name + '(' + convert(varchar(20),a.length)+ ')' END AS Type, + CASE WHEN a.isnullable=1 THEN 'YES' ELSE 'NO' end AS [Null], + CASE WHEN exists ( + SELECT 1 FROM sysobjects WHERE xtype='PK' AND name IN ( + SELECT name FROM sysindexes WHERE indid IN ( + SELECT indid FROM sysindexkeys WHERE id = a.id AND colid=a.colid + ) + ) + ) THEN 'PRI' ELSE '' END AS [Key], + CASE WHEN COLUMNPROPERTY(a.id,a.name,'IsIdentity')=1 THEN 'auto_increment' ELSE '' END Extra, + isnull(e.text,'') AS [Default], + isnull(g.[value],'') AS [Comment] +FROM syscolumns a +LEFT JOIN systypes b ON a.xtype=b.xtype AND a.xusertype=b.xusertype +INNER JOIN sysobjects d ON a.id=d.id AND d.xtype='U' AND d.name<>'dtproperties' +LEFT JOIN syscomments e ON a.cdefault=e.id +LEFT JOIN sys.extended_properties g ON a.id=g.major_id AND a.colid=g.minor_id +LEFT JOIN sys.extended_properties f ON d.id=f.major_id AND f.minor_id =0 +WHERE d.name='%s' +ORDER BY a.id,a.colorder +` +) + +func init() { + tableFieldsSqlTmp = formatSqlTmp(tableFieldsSqlTmp) +} + +// 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]*gdb.TableField, err error) { + var ( + result gdb.Result + link gdb.Link + usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) + ) + if link, err = d.SlaveLink(usedSchema); err != nil { + return nil, err + } + structureSql := fmt.Sprintf(tableFieldsSqlTmp, table) + result, err = d.DoSelect(ctx, link, structureSql) + if err != nil { + return nil, err + } + fields = make(map[string]*gdb.TableField) + for i, m := range result { + fields[m["Field"].String()] = &gdb.TableField{ + Index: i, + Name: m["Field"].String(), + Type: m["Type"].String(), + Null: m["Null"].Bool(), + Key: m["Key"].String(), + Default: m["Default"].Val(), + Extra: m["Extra"].String(), + Comment: m["Comment"].String(), + } + } + return fields, nil +} diff --git a/contrib/drivers/mssql/mssql_tables.go b/contrib/drivers/mssql/mssql_tables.go new file mode 100644 index 000000000..4cd2d2da2 --- /dev/null +++ b/contrib/drivers/mssql/mssql_tables.go @@ -0,0 +1,38 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mssql + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" +) + +const ( + tablesSqlTmp = `SELECT NAME FROM SYSOBJECTS WHERE XTYPE='U' AND STATUS >= 0 ORDER BY NAME` +) + +// 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 gdb.Result + 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 +} diff --git a/contrib/drivers/mssql/mssql_z_basic_test.go b/contrib/drivers/mssql/mssql_z_unit_basic_test.go similarity index 100% rename from contrib/drivers/mssql/mssql_z_basic_test.go rename to contrib/drivers/mssql/mssql_z_unit_basic_test.go diff --git a/contrib/drivers/mssql/mssql_init_test.go b/contrib/drivers/mssql/mssql_z_unit_init_test.go similarity index 98% rename from contrib/drivers/mssql/mssql_init_test.go rename to contrib/drivers/mssql/mssql_z_unit_init_test.go index fdf383ae0..a5db58e4d 100644 --- a/contrib/drivers/mssql/mssql_init_test.go +++ b/contrib/drivers/mssql/mssql_z_unit_init_test.go @@ -10,7 +10,6 @@ import ( "context" "fmt" - _ "github.com/denisenkom/go-mssqldb" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" diff --git a/contrib/drivers/mssql/mssql_z_model_test.go b/contrib/drivers/mssql/mssql_z_unit_model_test.go similarity index 100% rename from contrib/drivers/mssql/mssql_z_model_test.go rename to contrib/drivers/mssql/mssql_z_unit_model_test.go diff --git a/contrib/drivers/mysql/mysql.go b/contrib/drivers/mysql/mysql.go index edb3798f1..ce142f513 100644 --- a/contrib/drivers/mysql/mysql.go +++ b/contrib/drivers/mysql/mysql.go @@ -8,20 +8,10 @@ package mysql import ( - "context" - "database/sql" - "fmt" - "net/url" - "strings" - _ "github.com/go-sql-driver/mysql" "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/errors/gcode" - "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/text/gregex" - "github.com/gogf/gf/v2/util/gutil" ) // Driver is the driver for mysql database. @@ -59,119 +49,7 @@ func (d *Driver) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) { }, nil } -// Open creates and returns an underlying sql.DB object for mysql. -// Note that it converts time.Time argument to local timezone in default. -func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { - var ( - source string - underlyingDriverName = "mysql" - ) - // [username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN] - if config.Link != "" { - // ============================================================================ - // Deprecated from v2.2.0. - // ============================================================================ - source = config.Link - // Custom changing the schema in runtime. - if config.Name != "" { - source, _ = gregex.ReplaceString(`/([\w\.\-]+)+`, "/"+config.Name, source) - } - } else { - // TODO: Do not set charset when charset is not specified (in v2.5.0) - source = fmt.Sprintf( - "%s:%s@%s(%s:%s)/%s?charset=%s", - config.User, config.Pass, config.Protocol, config.Host, config.Port, config.Name, config.Charset, - ) - if config.Timezone != "" { - if strings.Contains(config.Timezone, "/") { - config.Timezone = url.QueryEscape(config.Timezone) - } - source = fmt.Sprintf("%s&loc=%s", source, config.Timezone) - } - if config.Extra != "" { - source = fmt.Sprintf("%s&%s", source, config.Extra) - } - } - 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 -} - // GetChars returns the security char for this type of database. func (d *Driver) GetChars() (charLeft string, charRight string) { return quoteChar, quoteChar } - -// DoFilter handles the sql before posts it to database. -func (d *Driver) DoFilter(ctx context.Context, link gdb.Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, err error) { - return d.Core.DoFilter(ctx, link, sql, args) -} - -// 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 gdb.Result - link, err := d.SlaveLink(schema...) - if err != nil { - return nil, err - } - result, err = d.DoSelect(ctx, link, `SHOW TABLES`) - if err != nil { - return - } - for _, m := range result { - for _, v := range m { - tables = append(tables, v.String()) - } - } - return -} - -// TableFields retrieves and returns the fields' information of specified table of current -// schema. -// -// The parameter `link` is optional, if given nil it automatically retrieves a raw sql connection -// as its link to proceed necessary sql query. -// -// Note that it returns a map containing the field name and its corresponding fields. -// As a map is unsorted, the TableField struct has a "Index" field marks its sequence in -// the fields. -// -// It's using cache feature to enhance the performance, which is never expired util the -// process restarts. -func (d *Driver) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*gdb.TableField, err error) { - var ( - result gdb.Result - link gdb.Link - usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) - ) - if link, err = d.SlaveLink(usedSchema); err != nil { - return nil, err - } - result, err = d.DoSelect( - ctx, link, - fmt.Sprintf(`SHOW FULL COLUMNS FROM %s`, d.QuoteWord(table)), - ) - if err != nil { - return nil, err - } - fields = make(map[string]*gdb.TableField) - for i, m := range result { - fields[m["Field"].String()] = &gdb.TableField{ - Index: i, - Name: m["Field"].String(), - Type: m["Type"].String(), - Null: m["Null"].Bool(), - Key: m["Key"].String(), - Default: m["Default"].Val(), - Extra: m["Extra"].String(), - Comment: m["Comment"].String(), - } - } - return fields, nil -} diff --git a/contrib/drivers/mysql/mysql_do_filter.go b/contrib/drivers/mysql/mysql_do_filter.go new file mode 100644 index 000000000..96bc7224c --- /dev/null +++ b/contrib/drivers/mysql/mysql_do_filter.go @@ -0,0 +1,18 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mysql + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" +) + +// DoFilter handles the sql before posts it to database. +func (d *Driver) DoFilter(ctx context.Context, link gdb.Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, err error) { + return d.Core.DoFilter(ctx, link, sql, args) +} diff --git a/contrib/drivers/mysql/mysql_open.go b/contrib/drivers/mysql/mysql_open.go new file mode 100644 index 000000000..4c53fcf39 --- /dev/null +++ b/contrib/drivers/mysql/mysql_open.go @@ -0,0 +1,63 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mysql + +import ( + "database/sql" + "fmt" + + "net/url" + "strings" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gregex" +) + +// Open creates and returns an underlying sql.DB object for mysql. +// Note that it converts time.Time argument to local timezone in default. +func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { + var ( + source string + underlyingDriverName = "mysql" + ) + // [username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN] + if config.Link != "" { + // ============================================================================ + // Deprecated from v2.2.0. + // ============================================================================ + source = config.Link + // Custom changing the schema in runtime. + if config.Name != "" { + source, _ = gregex.ReplaceString(`/([\w\.\-]+)+`, "/"+config.Name, source) + } + } else { + // TODO: Do not set charset when charset is not specified (in v2.5.0) + source = fmt.Sprintf( + "%s:%s@%s(%s:%s)/%s?charset=%s", + config.User, config.Pass, config.Protocol, config.Host, config.Port, config.Name, config.Charset, + ) + if config.Timezone != "" { + if strings.Contains(config.Timezone, "/") { + config.Timezone = url.QueryEscape(config.Timezone) + } + source = fmt.Sprintf("%s&loc=%s", source, config.Timezone) + } + if config.Extra != "" { + source = fmt.Sprintf("%s&%s", source, config.Extra) + } + } + 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 +} diff --git a/contrib/drivers/mysql/mysql_table_fields.go b/contrib/drivers/mysql/mysql_table_fields.go new file mode 100644 index 000000000..bbd1532c2 --- /dev/null +++ b/contrib/drivers/mysql/mysql_table_fields.go @@ -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 mysql + +import ( + "context" + "fmt" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/util/gutil" +) + +// TableFields retrieves and returns the fields' information of specified table of current +// schema. +// +// The parameter `link` is optional, if given nil it automatically retrieves a raw sql connection +// as its link to proceed necessary sql query. +// +// Note that it returns a map containing the field name and its corresponding fields. +// As a map is unsorted, the TableField struct has a "Index" field marks its sequence in +// the fields. +// +// It's using cache feature to enhance the performance, which is never expired util the +// process restarts. +func (d *Driver) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*gdb.TableField, err error) { + var ( + result gdb.Result + link gdb.Link + usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) + ) + if link, err = d.SlaveLink(usedSchema); err != nil { + return nil, err + } + result, err = d.DoSelect( + ctx, link, + fmt.Sprintf(`SHOW FULL COLUMNS FROM %s`, d.QuoteWord(table)), + ) + if err != nil { + return nil, err + } + fields = make(map[string]*gdb.TableField) + for i, m := range result { + fields[m["Field"].String()] = &gdb.TableField{ + Index: i, + Name: m["Field"].String(), + Type: m["Type"].String(), + Null: m["Null"].Bool(), + Key: m["Key"].String(), + Default: m["Default"].Val(), + Extra: m["Extra"].String(), + Comment: m["Comment"].String(), + } + } + return fields, nil +} diff --git a/contrib/drivers/mysql/mysql_tables.go b/contrib/drivers/mysql/mysql_tables.go new file mode 100644 index 000000000..b30c99105 --- /dev/null +++ b/contrib/drivers/mysql/mysql_tables.go @@ -0,0 +1,33 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mysql + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" +) + +// 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 gdb.Result + link, err := d.SlaveLink(schema...) + if err != nil { + return nil, err + } + result, err = d.DoSelect(ctx, link, `SHOW TABLES`) + if err != nil { + return + } + for _, m := range result { + for _, v := range m { + tables = append(tables, v.String()) + } + } + return +} diff --git a/contrib/drivers/mysql/mysql_basic_test.go b/contrib/drivers/mysql/mysql_z_unit_basic_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_basic_test.go rename to contrib/drivers/mysql/mysql_z_unit_basic_test.go diff --git a/contrib/drivers/mysql/mysql_core_test.go b/contrib/drivers/mysql/mysql_z_unit_core_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_core_test.go rename to contrib/drivers/mysql/mysql_z_unit_core_test.go diff --git a/contrib/drivers/mysql/mysql_feature_ctx_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_ctx_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_feature_ctx_test.go rename to contrib/drivers/mysql/mysql_z_unit_feature_ctx_test.go diff --git a/contrib/drivers/mysql/mysql_feature_hook_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_hook_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_feature_hook_test.go rename to contrib/drivers/mysql/mysql_z_unit_feature_hook_test.go diff --git a/contrib/drivers/mysql/mysql_feature_master_slave_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_master_slave_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_feature_master_slave_test.go rename to contrib/drivers/mysql/mysql_z_unit_feature_master_slave_test.go diff --git a/contrib/drivers/mysql/mysql_feature_model_builder_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_model_builder_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_feature_model_builder_test.go rename to contrib/drivers/mysql/mysql_z_unit_feature_model_builder_test.go diff --git a/contrib/drivers/mysql/mysql_feature_model_do_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_model_do_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_feature_model_do_test.go rename to contrib/drivers/mysql/mysql_z_unit_feature_model_do_test.go diff --git a/contrib/drivers/mysql/mysql_feature_model_join_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_model_join_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_feature_model_join_test.go rename to contrib/drivers/mysql/mysql_z_unit_feature_model_join_test.go diff --git a/contrib/drivers/mysql/mysql_feature_model_sharding_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_model_sharding_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_feature_model_sharding_test.go rename to contrib/drivers/mysql/mysql_z_unit_feature_model_sharding_test.go diff --git a/contrib/drivers/mysql/mysql_feature_model_struct_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_model_struct_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_feature_model_struct_test.go rename to contrib/drivers/mysql/mysql_z_unit_feature_model_struct_test.go diff --git a/contrib/drivers/mysql/mysql_feature_model_subquery_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_model_subquery_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_feature_model_subquery_test.go rename to contrib/drivers/mysql/mysql_z_unit_feature_model_subquery_test.go diff --git a/contrib/drivers/mysql/mysql_feature_raw_type_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_raw_type_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_feature_raw_type_test.go rename to contrib/drivers/mysql/mysql_z_unit_feature_raw_type_test.go diff --git a/contrib/drivers/mysql/mysql_feature_scanlist_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_scanlist_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_feature_scanlist_test.go rename to contrib/drivers/mysql/mysql_z_unit_feature_scanlist_test.go diff --git a/contrib/drivers/mysql/mysql_feature_time_maintain_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_time_maintain_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_feature_time_maintain_test.go rename to contrib/drivers/mysql/mysql_z_unit_feature_time_maintain_test.go diff --git a/contrib/drivers/mysql/mysql_feature_union_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_union_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_feature_union_test.go rename to contrib/drivers/mysql/mysql_z_unit_feature_union_test.go diff --git a/contrib/drivers/mysql/mysql_feature_with_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_with_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_feature_with_test.go rename to contrib/drivers/mysql/mysql_z_unit_feature_with_test.go diff --git a/contrib/drivers/mysql/mysql__test.go b/contrib/drivers/mysql/mysql_z_unit_init_test.go similarity index 100% rename from contrib/drivers/mysql/mysql__test.go rename to contrib/drivers/mysql/mysql_z_unit_init_test.go diff --git a/contrib/drivers/mysql/mysql_issue_test.go b/contrib/drivers/mysql/mysql_z_unit_issue_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_issue_test.go rename to contrib/drivers/mysql/mysql_z_unit_issue_test.go diff --git a/contrib/drivers/mysql/mysql_model_test.go b/contrib/drivers/mysql/mysql_z_unit_model_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_model_test.go rename to contrib/drivers/mysql/mysql_z_unit_model_test.go diff --git a/contrib/drivers/mysql/mysql_transaction_test.go b/contrib/drivers/mysql/mysql_z_unit_transaction_test.go similarity index 100% rename from contrib/drivers/mysql/mysql_transaction_test.go rename to contrib/drivers/mysql/mysql_z_unit_transaction_test.go diff --git a/contrib/drivers/oracle/oracle.go b/contrib/drivers/oracle/oracle.go index 9604dadd7..7b174f421 100644 --- a/contrib/drivers/oracle/oracle.go +++ b/contrib/drivers/oracle/oracle.go @@ -12,21 +12,9 @@ package oracle import ( - "context" - "database/sql" - "fmt" - "strconv" - "strings" - - gora "github.com/sijms/go-ora/v2" - "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/errors/gcode" - "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/text/gregex" "github.com/gogf/gf/v2/text/gstr" - "github.com/gogf/gf/v2/util/gconv" - "github.com/gogf/gf/v2/util/gutil" ) // Driver is the driver for oracle database. @@ -44,6 +32,21 @@ func init() { } } +// formatSqlTmp formats sql template string into one line. +func formatSqlTmp(sqlTmp string) string { + var err error + // format sql template string. + sqlTmp, err = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(sqlTmp)) + if err != nil { + panic(err) + } + sqlTmp, err = gregex.ReplaceString(`\s{2,}`, " ", gstr.Trim(sqlTmp)) + if err != nil { + panic(err) + } + return sqlTmp +} + // New create and returns a driver that implements gdb.Driver, which supports operations for Oracle. func New() gdb.Driver { return &Driver{} @@ -57,255 +60,7 @@ func (d *Driver) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) { }, nil } -// Open creates and returns an underlying sql.DB object for oracle. -func (d *Driver) Open(config *gdb.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.Link != "" { - // ============================================================================ - // Deprecated from v2.2.0. - // ============================================================================ - source = config.Link - // Custom changing the schema in runtime. - if config.Name != "" { - source, _ = gregex.ReplaceString(`@(.+?)/([\w\.\-]+)+`, "@$1/"+config.Name, source) - } - } else { - if config.Extra != "" { - var extraMap map[string]interface{} - if extraMap, err = gstr.Parse(config.Extra); err != nil { - return nil, err - } - for k, v := range extraMap { - options[k] = gconv.String(v) - } - } - 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 -} - // GetChars returns the security char for this type of database. func (d *Driver) GetChars() (charLeft string, charRight string) { return quoteChar, quoteChar } - -// DoFilter deals with the sql string before commits it to underlying sql driver. -func (d *Driver) DoFilter(ctx context.Context, link gdb.Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, err error) { - var index int - // Convert placeholder char '?' to string ":vx". - newSql, _ = gregex.ReplaceStringFunc("\\?", sql, func(s string) string { - index++ - return fmt.Sprintf(":v%d", index) - }) - newSql, _ = gregex.ReplaceString("\"", "", newSql) - return d.Core.DoFilter(ctx, link, d.parseSql(newSql), args) -} - -// parseSql does some replacement of the sql before commits it to underlying driver, -// for support of oracle server. -func (d *Driver) parseSql(sql string) string { - var ( - patten = `^\s*(?i)(SELECT)|(LIMIT\s*(\d+)\s*,{0,1}\s*(\d*))` - allMatch, _ = gregex.MatchAllString(patten, sql) - ) - if len(allMatch) == 0 { - return sql - } - var ( - index = 0 - keyword = strings.ToUpper(strings.TrimSpace(allMatch[index][0])) - ) - index++ - switch keyword { - case "SELECT": - if len(allMatch) < 2 || strings.HasPrefix(allMatch[index][0], "LIMIT") == false { - break - } - if gregex.IsMatchString("((?i)SELECT)(.+)((?i)LIMIT)", sql) == false { - break - } - queryExpr, _ := gregex.MatchString("((?i)SELECT)(.+)((?i)LIMIT)", sql) - if len(queryExpr) != 4 || - strings.EqualFold(queryExpr[1], "SELECT") == false || - strings.EqualFold(queryExpr[3], "LIMIT") == false { - break - } - page, limit := 0, 0 - for i := 1; i < len(allMatch[index]); i++ { - if len(strings.TrimSpace(allMatch[index][i])) == 0 { - continue - } - - if strings.HasPrefix(allMatch[index][i], "LIMIT") { - if allMatch[index][i+2] != "" { - page, _ = strconv.Atoi(allMatch[index][i+1]) - limit, _ = strconv.Atoi(allMatch[index][i+2]) - - if page <= 0 { - page = 1 - } - - limit = (page/limit + 1) * limit - - page, _ = strconv.Atoi(allMatch[index][i+1]) - } else { - limit, _ = strconv.Atoi(allMatch[index][i+1]) - } - break - } - } - sql = fmt.Sprintf( - "SELECT * FROM "+ - "(SELECT GFORM.*, ROWNUM ROWNUM_ FROM (%s %s) GFORM WHERE ROWNUM <= %d)"+ - " WHERE ROWNUM_ > %d", - queryExpr[1], queryExpr[2], limit, page, - ) - } - return sql -} - -// 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 gdb.Result - result, err = d.DoSelect(ctx, nil, "SELECT TABLE_NAME FROM USER_TABLES ORDER BY TABLE_NAME") - if err != nil { - return - } - for _, m := range result { - for _, v := range m { - tables = append(tables, v.String()) - } - } - return -} - -// 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]*gdb.TableField, err error) { - var ( - result gdb.Result - link gdb.Link - usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) - structureSql = fmt.Sprintf(` -SELECT - COLUMN_NAME AS FIELD, - CASE - WHEN (DATA_TYPE='NUMBER' AND NVL(DATA_SCALE,0)=0) THEN 'INT'||'('||DATA_PRECISION||','||DATA_SCALE||')' - WHEN (DATA_TYPE='NUMBER' AND NVL(DATA_SCALE,0)>0) THEN 'FLOAT'||'('||DATA_PRECISION||','||DATA_SCALE||')' - WHEN DATA_TYPE='FLOAT' THEN DATA_TYPE||'('||DATA_PRECISION||','||DATA_SCALE||')' - ELSE DATA_TYPE||'('||DATA_LENGTH||')' END AS TYPE,NULLABLE -FROM USER_TAB_COLUMNS WHERE TABLE_NAME = '%s' ORDER BY COLUMN_ID`, - strings.ToUpper(table), - ) - ) - if link, err = d.SlaveLink(usedSchema); err != nil { - return nil, err - } - structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql)) - result, err = d.DoSelect(ctx, link, structureSql) - if err != nil { - return nil, err - } - fields = make(map[string]*gdb.TableField) - for i, m := range result { - isNull := false - if m["NULLABLE"].String() == "Y" { - isNull = true - } - - fields[m["FIELD"].String()] = &gdb.TableField{ - Index: i, - Name: m["FIELD"].String(), - Type: m["TYPE"].String(), - Null: isNull, - } - } - return fields, nil -} - -// DoInsert inserts or updates data for given table. -func (d *Driver) DoInsert( - ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, -) (result sql.Result, err error) { - switch option.InsertOption { - case gdb.InsertOptionSave: - return nil, gerror.NewCode(gcode.CodeNotSupported, `Save operation is not supported by oracle driver`) - - case gdb.InsertOptionReplace: - return nil, gerror.NewCode(gcode.CodeNotSupported, `Replace operation is not supported by oracle driver`) - } - - var ( - keys []string - values []string - params []interface{} - ) - // 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(gdb.SqlResult) - charL, charR = d.GetChars() - keyStr = charL + strings.Join(keys, charL+","+charR) + charR - valueHolderStr = strings.Join(valueHolder, ",") - ) - // Format "INSERT...INTO..." statement. - intoStr := make([]string, 0) - for i := 0; i < len(list); i++ { - for _, k := range keys { - params = append(params, list[i][k]) - } - values = append(values, valueHolderStr) - intoStr = append(intoStr, fmt.Sprintf("INTO %s(%s) VALUES(%s)", table, keyStr, valueHolderStr)) - if len(intoStr) == option.BatchCount || (i == listLength-1 && len(valueHolder) > 0) { - r, err := d.DoExec(ctx, link, fmt.Sprintf( - "INSERT ALL %s SELECT * FROM DUAL", - strings.Join(intoStr, " "), - ), 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] - intoStr = intoStr[:0] - } - } - return batchResult, nil -} diff --git a/contrib/drivers/oracle/oracle_do_filter.go b/contrib/drivers/oracle/oracle_do_filter.go new file mode 100644 index 000000000..7c668dfce --- /dev/null +++ b/contrib/drivers/oracle/oracle_do_filter.go @@ -0,0 +1,140 @@ +// 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" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" +) + +var ( + newSqlReplacementTmp = ` +SELECT * FROM ( + SELECT GFORM.*, ROWNUM ROWNUM_ FROM (%s %s) GFORM WHERE ROWNUM <= %d +) + WHERE ROWNUM_ > %d +` +) + +func init() { + newSqlReplacementTmp = formatSqlTmp(newSqlReplacementTmp) +} + +// DoFilter deals with the sql string before commits it to underlying sql driver. +func (d *Driver) DoFilter(ctx context.Context, link gdb.Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, 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 +} diff --git a/contrib/drivers/oracle/oracle_do_insert.go b/contrib/drivers/oracle/oracle_do_insert.go new file mode 100644 index 000000000..d52496166 --- /dev/null +++ b/contrib/drivers/oracle/oracle_do_insert.go @@ -0,0 +1,95 @@ +// 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" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/util/gconv" +) + +// DoInsert inserts or updates data for given table. +func (d *Driver) DoInsert( + ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, +) (result sql.Result, err error) { + switch option.InsertOption { + case gdb.InsertOptionSave: + return nil, gerror.NewCode( + gcode.CodeNotSupported, + `Save operation is not supported by oracle driver`, + ) + + case gdb.InsertOptionReplace: + return nil, gerror.NewCode( + gcode.CodeNotSupported, + `Replace operation is not supported by oracle driver`, + ) + } + var ( + keys []string + values []string + params []interface{} + ) + // 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(gdb.SqlResult) + charL, charR = d.GetChars() + keyStr = charL + strings.Join(keys, charL+","+charR) + charR + valueHolderStr = strings.Join(valueHolder, ",") + ) + // Format "INSERT...INTO..." statement. + intoStrArray := make([]string, 0) + for i := 0; i < len(list); i++ { + for _, k := range keys { + if s, ok := list[i][k].(gdb.Raw); ok { + params = append(params, gconv.String(s)) + } else { + params = append(params, list[i][k]) + } + } + values = append(values, valueHolderStr) + intoStrArray = append( + intoStrArray, + fmt.Sprintf( + "INTO %s(%s) VALUES(%s)", + table, keyStr, valueHolderStr, + ), + ) + if len(intoStrArray) == option.BatchCount || (i == listLength-1 && len(valueHolder) > 0) { + r, err := d.DoExec(ctx, link, fmt.Sprintf( + "INSERT ALL %s SELECT * FROM DUAL", + strings.Join(intoStrArray, " "), + ), 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] + intoStrArray = intoStrArray[:0] + } + } + return batchResult, nil +} diff --git a/contrib/drivers/oracle/oracle_open.go b/contrib/drivers/oracle/oracle_open.go new file mode 100644 index 000000000..a13c00ea2 --- /dev/null +++ b/contrib/drivers/oracle/oracle_open.go @@ -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 oracle + +import ( + "database/sql" + gora "github.com/sijms/go-ora/v2" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" +) + +// Open creates and returns an underlying sql.DB object for oracle. +func (d *Driver) Open(config *gdb.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.Link != "" { + // ============================================================================ + // Deprecated from v2.2.0. + // ============================================================================ + source = config.Link + // Custom changing the schema in runtime. + if config.Name != "" { + source, _ = gregex.ReplaceString(`@(.+?)/([\w\.\-]+)+`, "@$1/"+config.Name, source) + } + } else { + if config.Extra != "" { + var extraMap map[string]interface{} + if extraMap, err = gstr.Parse(config.Extra); err != nil { + return nil, err + } + for k, v := range extraMap { + options[k] = gconv.String(v) + } + } + 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 +} diff --git a/contrib/drivers/oracle/oracle_table_fields.go b/contrib/drivers/oracle/oracle_table_fields.go new file mode 100644 index 000000000..ca8027707 --- /dev/null +++ b/contrib/drivers/oracle/oracle_table_fields.go @@ -0,0 +1,67 @@ +// 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" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/util/gutil" +) + +var ( + tableFieldsSqlTmp = ` +SELECT + COLUMN_NAME AS FIELD, + CASE + WHEN (DATA_TYPE='NUMBER' AND NVL(DATA_SCALE,0)=0) THEN 'INT'||'('||DATA_PRECISION||','||DATA_SCALE||')' + WHEN (DATA_TYPE='NUMBER' AND NVL(DATA_SCALE,0)>0) THEN 'FLOAT'||'('||DATA_PRECISION||','||DATA_SCALE||')' + WHEN DATA_TYPE='FLOAT' THEN DATA_TYPE||'('||DATA_PRECISION||','||DATA_SCALE||')' + ELSE DATA_TYPE||'('||DATA_LENGTH||')' END AS TYPE,NULLABLE +FROM USER_TAB_COLUMNS WHERE TABLE_NAME = '%s' ORDER BY COLUMN_ID +` +) + +func init() { + tableFieldsSqlTmp = formatSqlTmp(tableFieldsSqlTmp) +} + +// 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]*gdb.TableField, err error) { + var ( + result gdb.Result + link gdb.Link + usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) + structureSql = fmt.Sprintf(tableFieldsSqlTmp, strings.ToUpper(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]*gdb.TableField) + for i, m := range result { + isNull := false + if m["NULLABLE"].String() == "Y" { + isNull = true + } + + fields[m["FIELD"].String()] = &gdb.TableField{ + Index: i, + Name: m["FIELD"].String(), + Type: m["TYPE"].String(), + Null: isNull, + } + } + return fields, nil +} diff --git a/contrib/drivers/oracle/oracle_tables.go b/contrib/drivers/oracle/oracle_tables.go new file mode 100644 index 000000000..c3031ce7e --- /dev/null +++ b/contrib/drivers/oracle/oracle_tables.go @@ -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" + + "github.com/gogf/gf/v2/database/gdb" +) + +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 gdb.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 +} diff --git a/contrib/drivers/oracle/oracle_z_basic_test.go b/contrib/drivers/oracle/oracle_z_unit_basic_test.go similarity index 99% rename from contrib/drivers/oracle/oracle_z_basic_test.go rename to contrib/drivers/oracle/oracle_z_unit_basic_test.go index 9cd47c1f9..074ea008e 100644 --- a/contrib/drivers/oracle/oracle_z_basic_test.go +++ b/contrib/drivers/oracle/oracle_z_unit_basic_test.go @@ -8,10 +8,11 @@ package oracle_test import ( "fmt" - "github.com/gogf/gf/v2/util/gconv" "strings" "testing" + "github.com/gogf/gf/v2/util/gconv" + "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/test/gtest" diff --git a/contrib/drivers/oracle/oracle_init_test.go b/contrib/drivers/oracle/oracle_z_unit_init_test.go similarity index 99% rename from contrib/drivers/oracle/oracle_init_test.go rename to contrib/drivers/oracle/oracle_z_unit_init_test.go index 74a987f5d..d58b44eb0 100644 --- a/contrib/drivers/oracle/oracle_init_test.go +++ b/contrib/drivers/oracle/oracle_z_unit_init_test.go @@ -11,12 +11,13 @@ import ( "fmt" "strings" + _ "github.com/sijms/go-ora/v2" + "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/test/gtest" - _ "github.com/sijms/go-ora/v2" ) var ( diff --git a/contrib/drivers/oracle/oracle_z_model_test.go b/contrib/drivers/oracle/oracle_z_unit_model_test.go similarity index 98% rename from contrib/drivers/oracle/oracle_z_model_test.go rename to contrib/drivers/oracle/oracle_z_unit_model_test.go index d50d46cfe..712939cfa 100644 --- a/contrib/drivers/oracle/oracle_z_model_test.go +++ b/contrib/drivers/oracle/oracle_z_unit_model_test.go @@ -131,7 +131,6 @@ func Test_Model_RightJoin(t *testing.T) { func TestPage(t *testing.T) { table := createInitTable() defer dropTable(table) - db.SetDebug(true) result, err := db.Model(table).Page(1, 2).Order("ID").All() gtest.Assert(err, nil) fmt.Println("page:1--------", result) @@ -235,6 +234,28 @@ func Test_Model_Insert(t *testing.T) { }) } +// https://github.com/gogf/gf/issues/3286 +func Test_Model_Insert_Raw(t *testing.T) { + table := createTable() + defer dropTable(table) + gtest.C(t, func(t *gtest.T) { + _, err := db.Model(table).Data(g.Map{ + "ID": 1, + "UID": 1, + "PASSPORT": "t1", + "PASSWORD": "25d55ad283aa400af464c76d713c07ad", + "NICKNAME": gdb.Raw("name_1"), + "SALARY": 2675.11, + "CREATE_TIME": gtime.Now().String(), + }).Insert() + t.AssertNil(err) + + value, err := db.Model(table).Fields("PASSPORT").Where("id=1").Value() + t.AssertNil(err) + t.Assert(value.String(), "t1") + }) +} + func Test_Model_Insert_Time(t *testing.T) { table := createTable() defer dropTable(table) diff --git a/contrib/drivers/pgsql/pgsql.go b/contrib/drivers/pgsql/pgsql.go index 28a80651d..492d679c2 100644 --- a/contrib/drivers/pgsql/pgsql.go +++ b/contrib/drivers/pgsql/pgsql.go @@ -12,22 +12,12 @@ package pgsql import ( - "context" - "database/sql" - "fmt" - "regexp" - "strings" - _ "github.com/lib/pq" "github.com/gogf/gf/v2/database/gdb" - "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/gregex" "github.com/gogf/gf/v2/text/gstr" - "github.com/gogf/gf/v2/util/gconv" - "github.com/gogf/gf/v2/util/gutil" ) // Driver is the driver for postgresql database. @@ -47,6 +37,21 @@ func init() { } } +// formatSqlTmp formats sql template string into one line. +func formatSqlTmp(sqlTmp string) string { + var err error + // format sql template string. + sqlTmp, err = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(sqlTmp)) + if err != nil { + panic(err) + } + sqlTmp, err = gregex.ReplaceString(`\s{2,}`, " ", gstr.Trim(sqlTmp)) + if err != nil { + panic(err) + } + return sqlTmp +} + // New create and returns a driver that implements gdb.Driver, which supports operations for PostgreSql. func New() gdb.Driver { return &Driver{} @@ -60,425 +65,7 @@ func (d *Driver) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) { }, nil } -// Open creates and returns an underlying sql.DB object for pgsql. -// https://pkg.go.dev/github.com/lib/pq -func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { - var ( - source string - underlyingDriverName = "postgres" - ) - if config.Link != "" { - // ============================================================================ - // Deprecated from v2.2.0. - // ============================================================================ - source = config.Link - // Custom changing the schema in runtime. - if config.Name != "" { - source, _ = gregex.ReplaceString(`dbname=([\w\.\-]+)+`, "dbname="+config.Name, source) - } - } else { - if config.Name != "" { - source = fmt.Sprintf( - "user=%s password=%s host=%s port=%s dbname=%s sslmode=disable", - config.User, config.Pass, config.Host, config.Port, config.Name, - ) - } else { - source = fmt.Sprintf( - "user=%s password=%s host=%s port=%s sslmode=disable", - config.User, config.Pass, config.Host, config.Port, - ) - } - - 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 != "" { - var extraMap map[string]interface{} - if extraMap, err = gstr.Parse(config.Extra); err != nil { - return nil, err - } - for k, v := range extraMap { - source += fmt.Sprintf(` %s=%s`, k, v) - } - } - } - - 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 -} - // GetChars returns the security char for this type of database. func (d *Driver) GetChars() (charLeft string, charRight string) { return quoteChar, quoteChar } - -// CheckLocalTypeForField checks and returns corresponding local golang type for given db type. -func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, fieldValue interface{}) (gdb.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 - // For pgsql, int2 = smallint. - "int2", - // For pgsql, int4 = integer - "int4": - return gdb.LocalTypeInt, nil - - case - // For pgsql, int8 = bigint - "int8": - return gdb.LocalTypeInt64, nil - - case - "_int2", - "_int4": - return gdb.LocalTypeIntSlice, nil - - case - "_int8": - return gdb.LocalTypeInt64Slice, 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: -// `float(5,2)`, `unsigned double(5,2)`, `decimal(10,2)`, `char(45)`, `varchar(100)`, etc. -func (d *Driver) ConvertValueForLocal(ctx context.Context, fieldType string, fieldValue interface{}) (interface{}, error) { - typeName, _ := gregex.ReplaceString(`\(.+\)`, "", fieldType) - typeName = strings.ToLower(typeName) - switch typeName { - // For pgsql, int2 = smallint and int4 = integer. - case "int2", "int4": - return gconv.Int(gconv.String(fieldValue)), nil - - // For pgsql, int8 = bigint. - case "int8": - return gconv.Int64(gconv.String(fieldValue)), nil - - // Int32 slice. - case - "_int2", "_int4": - return gconv.Ints( - gstr.ReplaceByMap(gconv.String(fieldValue), - map[string]string{ - "{": "[", - "}": "]", - }, - ), - ), nil - - // Int64 slice. - case - "_int8": - return gconv.Int64s( - gstr.ReplaceByMap(gconv.String(fieldValue), - map[string]string{ - "{": "[", - "}": "]", - }, - ), - ), nil - - default: - return d.Core.ConvertValueForLocal(ctx, fieldType, fieldValue) - } -} - -// DoFilter deals with the sql string before commits it to underlying sql driver. -func (d *Driver) DoFilter(ctx context.Context, link gdb.Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, err error) { - var index int - // Convert placeholder char '?' to string "$x". - newSql, _ = gregex.ReplaceStringFunc(`\?`, sql, func(s string) string { - index++ - return fmt.Sprintf(`$%d`, index) - }) - // 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, _ = gregex.ReplaceStringFuncMatch(`(::jsonb([^\w\d]*)\$\d)`, newSql, func(match []string) string { - return fmt.Sprintf(`::jsonb%s?`, match[2]) - }) - newSql, _ = gregex.ReplaceString(` LIMIT (\d+),\s*(\d+)`, ` LIMIT $2 OFFSET $1`, newSql) - return d.Core.DoFilter(ctx, link, newSql, args) -} - -// 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 gdb.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(` -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`, - 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 gdb.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 := regexp.MustCompile(`PostgreSQL (\d+\.\d+)`).FindStringSubmatch(v.String()) - if len(matches) >= 2 { - return matches[1] - } - } - } - return "" -} - -// 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]*gdb.TableField, err error) { - var ( - result gdb.Result - link gdb.Link - usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) - // TODO duplicated `id` result? - structureSql = fmt.Sprintf(` -SELECT a.attname AS field, t.typname AS type,a.attnotnull as null, - (case when d.contype is not null then 'pri' 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.relname = '%s' and a.attisdropped is false and a.attnum > 0 -ORDER BY a.attnum`, - table, - ) - ) - if link, err = d.SlaveLink(usedSchema); err != nil { - return nil, err - } - structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql)) - result, err = d.DoSelect(ctx, link, structureSql) - if err != nil { - return nil, err - } - fields = make(map[string]*gdb.TableField) - var ( - index = 0 - name string - ok bool - ) - for _, m := range result { - name = m["field"].String() - // Filter duplicated fields. - if _, ok = fields[name]; ok { - continue - } - fields[name] = &gdb.TableField{ - Index: index, - Name: name, - Type: m["type"].String(), - Null: !m["null"].Bool(), - Key: m["key"].String(), - Default: m["default_value"].Val(), - Comment: m["comment"].String(), - } - index++ - } - return fields, nil -} - -// DoInsert inserts or updates data forF given table. -func (d *Driver) DoInsert(ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption) (result sql.Result, err error) { - switch option.InsertOption { - case gdb.InsertOptionSave: - return nil, gerror.NewCode( - gcode.CodeNotSupported, - `Save operation is not supported by pgsql driver`, - ) - - case gdb.InsertOptionReplace: - return nil, gerror.NewCode( - gcode.CodeNotSupported, - `Replace operation is not supported by pgsql driver`, - ) - - case gdb.InsertOptionIgnore: - return nil, gerror.NewCode( - gcode.CodeNotSupported, - `Insert ignore operation is not supported by pgsql driver`, - ) - - case gdb.InsertOptionDefault: - tableFields, err := d.GetCore().GetDB().TableFields(ctx, table) - if err == nil { - for _, field := range tableFields { - if field.Key == "pri" { - pkField := *field - ctx = context.WithValue(ctx, internalPrimaryKeyInCtx, pkField) - break - } - } - } - } - return d.Core.DoInsert(ctx, link, table, list, option) -} - -// 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 gdb.Link, sql string, args ...interface{}) (result sql.Result, err error) { - var ( - isUseCoreDoExec bool = false // Check whether the default method needs to be used - primaryKey string = "" - pkField gdb.TableField - ) - - // Transaction checks. - if link == nil { - if tx := gdb.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 := gdb.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.(gdb.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 += " RETURNING " + 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 - - if d.GetConfig().ExecTimeout > 0 { - var cancelFunc context.CancelFunc - ctx, cancelFunc = context.WithTimeout(ctx, d.GetConfig().ExecTimeout) - defer cancelFunc() - } - - // 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 gdb.DoCommitOutput - out, err = d.DoCommit(ctx, gdb.DoCommitInput{ - Link: link, - Sql: sql, - Args: args, - Stmt: nil, - Type: gdb.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 -} diff --git a/contrib/drivers/pgsql/pgsql_convert.go b/contrib/drivers/pgsql/pgsql_convert.go new file mode 100644 index 000000000..7f1ea76a8 --- /dev/null +++ b/contrib/drivers/pgsql/pgsql_convert.go @@ -0,0 +1,98 @@ +// 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" + "strings" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" +) + +// CheckLocalTypeForField checks and returns corresponding local golang type for given db type. +func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, fieldValue interface{}) (gdb.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 + // For pgsql, int2 = smallint. + "int2", + // For pgsql, int4 = integer + "int4": + return gdb.LocalTypeInt, nil + + case + // For pgsql, int8 = bigint + "int8": + return gdb.LocalTypeInt64, nil + + case + "_int2", + "_int4": + return gdb.LocalTypeIntSlice, nil + + case + "_int8": + return gdb.LocalTypeInt64Slice, 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: +// `float(5,2)`, `unsigned double(5,2)`, `decimal(10,2)`, `char(45)`, `varchar(100)`, etc. +func (d *Driver) ConvertValueForLocal(ctx context.Context, fieldType string, fieldValue interface{}) (interface{}, error) { + typeName, _ := gregex.ReplaceString(`\(.+\)`, "", fieldType) + typeName = strings.ToLower(typeName) + switch typeName { + // For pgsql, int2 = smallint and int4 = integer. + case "int2", "int4": + return gconv.Int(gconv.String(fieldValue)), nil + + // For pgsql, int8 = bigint. + case "int8": + return gconv.Int64(gconv.String(fieldValue)), nil + + // Int32 slice. + case + "_int2", "_int4": + return gconv.Ints( + gstr.ReplaceByMap(gconv.String(fieldValue), + map[string]string{ + "{": "[", + "}": "]", + }, + ), + ), nil + + // Int64 slice. + case + "_int8": + return gconv.Int64s( + gstr.ReplaceByMap(gconv.String(fieldValue), + map[string]string{ + "{": "[", + "}": "]", + }, + ), + ), nil + + default: + return d.Core.ConvertValueForLocal(ctx, fieldType, fieldValue) + } +} diff --git a/contrib/drivers/pgsql/pgsql_do_exec.go b/contrib/drivers/pgsql/pgsql_do_exec.go new file mode 100644 index 000000000..4bb233b53 --- /dev/null +++ b/contrib/drivers/pgsql/pgsql_do_exec.go @@ -0,0 +1,115 @@ +// 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" + + "github.com/gogf/gf/v2/database/gdb" + "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 gdb.Link, sql string, args ...interface{}) (result sql.Result, err error) { + var ( + isUseCoreDoExec bool = false // Check whether the default method needs to be used + primaryKey string = "" + pkField gdb.TableField + ) + + // Transaction checks. + if link == nil { + if tx := gdb.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 := gdb.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.(gdb.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 += " RETURNING " + 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 + + if d.GetConfig().ExecTimeout > 0 { + var cancelFunc context.CancelFunc + ctx, cancelFunc = context.WithTimeout(ctx, d.GetConfig().ExecTimeout) + defer cancelFunc() + } + + // 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 gdb.DoCommitOutput + out, err = d.DoCommit(ctx, gdb.DoCommitInput{ + Link: link, + Sql: sql, + Args: args, + Stmt: nil, + Type: gdb.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 +} diff --git a/contrib/drivers/pgsql/pgsql_do_filter.go b/contrib/drivers/pgsql/pgsql_do_filter.go new file mode 100644 index 000000000..fa0b1c4a5 --- /dev/null +++ b/contrib/drivers/pgsql/pgsql_do_filter.go @@ -0,0 +1,46 @@ +// 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" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/text/gregex" +) + +// DoFilter deals with the sql string before commits it to underlying sql driver. +func (d *Driver) DoFilter( + ctx context.Context, link gdb.Link, sql string, args []interface{}, +) (newSql string, newArgs []interface{}, 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 + } + newArgs = args + return d.Core.DoFilter(ctx, link, newSql, newArgs) +} diff --git a/contrib/drivers/pgsql/pgsql_do_insert.go b/contrib/drivers/pgsql/pgsql_do_insert.go new file mode 100644 index 000000000..b7586e3d7 --- /dev/null +++ b/contrib/drivers/pgsql/pgsql_do_insert.go @@ -0,0 +1,52 @@ +// 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" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +// DoInsert inserts or updates data forF given table. +func (d *Driver) DoInsert(ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption) (result sql.Result, err error) { + switch option.InsertOption { + case gdb.InsertOptionSave: + return nil, gerror.NewCode( + gcode.CodeNotSupported, + `Save operation is not supported by pgsql driver`, + ) + + case gdb.InsertOptionReplace: + return nil, gerror.NewCode( + gcode.CodeNotSupported, + `Replace operation is not supported by pgsql driver`, + ) + + case gdb.InsertOptionIgnore: + return nil, gerror.NewCode( + gcode.CodeNotSupported, + `Insert ignore operation is not supported by pgsql driver`, + ) + + case gdb.InsertOptionDefault: + tableFields, err := d.GetCore().GetDB().TableFields(ctx, table) + if err == nil { + for _, field := range tableFields { + if field.Key == "pri" { + pkField := *field + ctx = context.WithValue(ctx, internalPrimaryKeyInCtx, pkField) + break + } + } + } + } + return d.Core.DoInsert(ctx, link, table, list, option) +} diff --git a/contrib/drivers/pgsql/pgsql_open.go b/contrib/drivers/pgsql/pgsql_open.go new file mode 100644 index 000000000..7bbd98462 --- /dev/null +++ b/contrib/drivers/pgsql/pgsql_open.go @@ -0,0 +1,76 @@ +// 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" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gregex" + "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 *gdb.ConfigNode) (db *sql.DB, err error) { + var ( + source string + underlyingDriverName = "postgres" + ) + if config.Link != "" { + // ============================================================================ + // Deprecated from v2.2.0. + // ============================================================================ + source = config.Link + // Custom changing the schema in runtime. + if config.Name != "" { + source, _ = gregex.ReplaceString(`dbname=([\w\.\-]+)+`, "dbname="+config.Name, source) + } + } else { + if config.Name != "" { + source = fmt.Sprintf( + "user=%s password=%s host=%s port=%s dbname=%s sslmode=disable", + config.User, config.Pass, config.Host, config.Port, config.Name, + ) + } else { + source = fmt.Sprintf( + "user=%s password=%s host=%s port=%s sslmode=disable", + config.User, config.Pass, config.Host, config.Port, + ) + } + + 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 != "" { + var extraMap map[string]interface{} + if extraMap, err = gstr.Parse(config.Extra); err != nil { + return nil, err + } + for k, v := range extraMap { + source += fmt.Sprintf(` %s=%s`, k, v) + } + } + } + + 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 +} diff --git a/contrib/drivers/pgsql/pgsql_table_fields.go b/contrib/drivers/pgsql/pgsql_table_fields.go new file mode 100644 index 000000000..ed64b825c --- /dev/null +++ b/contrib/drivers/pgsql/pgsql_table_fields.go @@ -0,0 +1,78 @@ +// 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" + + "github.com/gogf/gf/v2/database/gdb" + "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 is not null then 'pri' 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.relname = '%s' and a.attisdropped is false and a.attnum > 0 +ORDER BY a.attnum` +) + +func init() { + tableFieldsSqlTmp = formatSqlTmp(tableFieldsSqlTmp) +} + +// 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]*gdb.TableField, err error) { + var ( + result gdb.Result + link gdb.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]*gdb.TableField) + var ( + index = 0 + name string + ok bool + ) + for _, m := range result { + name = m["field"].String() + // Filter duplicated fields. + if _, ok = fields[name]; ok { + continue + } + fields[name] = &gdb.TableField{ + Index: index, + Name: name, + Type: m["type"].String(), + Null: !m["null"].Bool(), + Key: m["key"].String(), + Default: m["default_value"].Val(), + Comment: m["comment"].String(), + } + index++ + } + return fields, nil +} diff --git a/contrib/drivers/pgsql/pgsql_tables.go b/contrib/drivers/pgsql/pgsql_tables.go new file mode 100644 index 000000000..297f7c4cc --- /dev/null +++ b/contrib/drivers/pgsql/pgsql_tables.go @@ -0,0 +1,96 @@ +// 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" + + "github.com/gogf/gf/v2/database/gdb" + "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 +` +) + +func init() { + tablesSqlTmp = formatSqlTmp(tablesSqlTmp) +} + +// 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 gdb.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 gdb.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 := regexp.MustCompile(`PostgreSQL (\d+\.\d+)`).FindStringSubmatch(v.String()) + if len(matches) >= 2 { + return matches[1] + } + } + } + return "" +} diff --git a/contrib/drivers/pgsql/pgsql_z_db_test.go b/contrib/drivers/pgsql/pgsql_z_unit_db_test.go similarity index 100% rename from contrib/drivers/pgsql/pgsql_z_db_test.go rename to contrib/drivers/pgsql/pgsql_z_unit_db_test.go diff --git a/contrib/drivers/pgsql/pgsql_z_init_test.go b/contrib/drivers/pgsql/pgsql_z_unit_init_test.go similarity index 100% rename from contrib/drivers/pgsql/pgsql_z_init_test.go rename to contrib/drivers/pgsql/pgsql_z_unit_init_test.go diff --git a/contrib/drivers/pgsql/pgsql_z_model_test.go b/contrib/drivers/pgsql/pgsql_z_unit_model_test.go similarity index 100% rename from contrib/drivers/pgsql/pgsql_z_model_test.go rename to contrib/drivers/pgsql/pgsql_z_unit_model_test.go diff --git a/contrib/drivers/pgsql/pgsql_z_raw_test.go b/contrib/drivers/pgsql/pgsql_z_unit_raw_test.go similarity index 100% rename from contrib/drivers/pgsql/pgsql_z_raw_test.go rename to contrib/drivers/pgsql/pgsql_z_unit_raw_test.go diff --git a/contrib/drivers/pgsql/pgsql_z_test.go b/contrib/drivers/pgsql/pgsql_z_unit_test.go similarity index 100% rename from contrib/drivers/pgsql/pgsql_z_test.go rename to contrib/drivers/pgsql/pgsql_z_unit_test.go diff --git a/contrib/drivers/sqlite/sqlite.go b/contrib/drivers/sqlite/sqlite.go index 088f8e760..65554930b 100644 --- a/contrib/drivers/sqlite/sqlite.go +++ b/contrib/drivers/sqlite/sqlite.go @@ -11,20 +11,9 @@ package sqlite import ( - "context" - "database/sql" - "fmt" - _ "github.com/glebarez/go-sqlite" "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/encoding/gurl" - "github.com/gogf/gf/v2/errors/gcode" - "github.com/gogf/gf/v2/errors/gerror" - "github.com/gogf/gf/v2/os/gfile" - "github.com/gogf/gf/v2/text/gstr" - "github.com/gogf/gf/v2/util/gconv" - "github.com/gogf/gf/v2/util/gutil" ) // Driver is the driver for sqlite database. @@ -55,138 +44,7 @@ func (d *Driver) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) { }, nil } -// Open creates and returns an underlying sql.DB object for sqlite. -// https://github.com/glebarez/go-sqlite -func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { - var ( - source string - underlyingDriverName = "sqlite" - ) - if config.Link != "" { - // ============================================================================ - // Deprecated from v2.2.0. - // ============================================================================ - source = config.Link - } else { - source = config.Name - } - // It searches the source file to locate its absolute path.. - if absolutePath, _ := gfile.Search(source); absolutePath != "" { - source = absolutePath - } - - // Multiple PRAGMAs can be specified, e.g.: - // path/to/some.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL) - if config.Extra != "" { - var ( - options string - extraMap map[string]interface{} - ) - if extraMap, err = gstr.Parse(config.Extra); err != nil { - return nil, err - } - for k, v := range extraMap { - if options != "" { - options += "&" - } - options += fmt.Sprintf(`_pragma=%s(%s)`, k, gurl.Encode(gconv.String(v))) - } - if len(options) > 1 { - source += "?" + 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 -} - // GetChars returns the security char for this type of database. func (d *Driver) GetChars() (charLeft string, charRight string) { return quoteChar, quoteChar } - -// DoFilter deals with the sql string before commits it to underlying sql driver. -func (d *Driver) DoFilter(ctx context.Context, link gdb.Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, err error) { - // Special insert/ignore operation for sqlite. - switch { - case gstr.HasPrefix(sql, gdb.InsertOperationIgnore): - sql = "INSERT OR IGNORE" + sql[len(gdb.InsertOperationIgnore):] - - case gstr.HasPrefix(sql, gdb.InsertOperationReplace): - sql = "INSERT OR REPLACE" + sql[len(gdb.InsertOperationReplace):] - - default: - if gstr.Contains(sql, gdb.InsertOnDuplicateKeyUpdate) { - return sql, args, gerror.NewCode( - gcode.CodeNotSupported, - `Save operation is not supported by sqlite driver`, - ) - } - } - return d.Core.DoFilter(ctx, link, sql, args) -} - -// 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 gdb.Result - link, err := d.SlaveLink(schema...) - if err != nil { - return nil, err - } - - result, err = d.DoSelect( - ctx, - link, - `SELECT NAME FROM SQLITE_MASTER WHERE TYPE='table' ORDER BY NAME`, - ) - if err != nil { - return - } - for _, m := range result { - for _, v := range m { - tables = append(tables, v.String()) - } - } - return -} - -// 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]*gdb.TableField, err error) { - var ( - result gdb.Result - link gdb.Link - usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) - ) - if link, err = d.SlaveLink(usedSchema); err != nil { - return nil, err - } - result, err = d.DoSelect(ctx, link, fmt.Sprintf(`PRAGMA TABLE_INFO(%s)`, d.QuoteWord(table))) - if err != nil { - return nil, err - } - fields = make(map[string]*gdb.TableField) - for i, m := range result { - mKey := "" - if m["pk"].Bool() { - mKey = "pri" - } - fields[m["name"].String()] = &gdb.TableField{ - Index: i, - Name: m["name"].String(), - Type: m["type"].String(), - Key: mKey, - Default: m["dflt_value"].Val(), - Null: !m["notnull"].Bool(), - } - } - return fields, nil -} diff --git a/contrib/drivers/sqlite/sqlite_do_filter.go b/contrib/drivers/sqlite/sqlite_do_filter.go new file mode 100644 index 000000000..1fa344962 --- /dev/null +++ b/contrib/drivers/sqlite/sqlite_do_filter.go @@ -0,0 +1,37 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package sqlite + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "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 gdb.Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, err error) { + // Special insert/ignore operation for sqlite. + switch { + case gstr.HasPrefix(sql, gdb.InsertOperationIgnore): + sql = "INSERT OR IGNORE" + sql[len(gdb.InsertOperationIgnore):] + + case gstr.HasPrefix(sql, gdb.InsertOperationReplace): + sql = "INSERT OR REPLACE" + sql[len(gdb.InsertOperationReplace):] + + default: + if gstr.Contains(sql, gdb.InsertOnDuplicateKeyUpdate) { + return sql, args, gerror.NewCode( + gcode.CodeNotSupported, + `Save operation is not supported by sqlite driver`, + ) + } + } + return d.Core.DoFilter(ctx, link, sql, args) +} diff --git a/contrib/drivers/sqlite/sqlite_open.go b/contrib/drivers/sqlite/sqlite_open.go new file mode 100644 index 000000000..401b4b694 --- /dev/null +++ b/contrib/drivers/sqlite/sqlite_open.go @@ -0,0 +1,71 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package sqlite + +import ( + "database/sql" + "fmt" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/encoding/gurl" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" +) + +// Open creates and returns an underlying sql.DB object for sqlite. +// https://github.com/glebarez/go-sqlite +func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { + var ( + source string + underlyingDriverName = "sqlite" + ) + if config.Link != "" { + // ============================================================================ + // Deprecated from v2.2.0. + // ============================================================================ + source = config.Link + } else { + source = config.Name + } + // It searches the source file to locate its absolute path.. + if absolutePath, _ := gfile.Search(source); absolutePath != "" { + source = absolutePath + } + + // Multiple PRAGMAs can be specified, e.g.: + // path/to/some.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL) + if config.Extra != "" { + var ( + options string + extraMap map[string]interface{} + ) + if extraMap, err = gstr.Parse(config.Extra); err != nil { + return nil, err + } + for k, v := range extraMap { + if options != "" { + options += "&" + } + options += fmt.Sprintf(`_pragma=%s(%s)`, k, gurl.Encode(gconv.String(v))) + } + if len(options) > 1 { + source += "?" + 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 +} diff --git a/contrib/drivers/sqlite/sqlite_table_fields.go b/contrib/drivers/sqlite/sqlite_table_fields.go new file mode 100644 index 000000000..d4ae1650c --- /dev/null +++ b/contrib/drivers/sqlite/sqlite_table_fields.go @@ -0,0 +1,49 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package sqlite + +import ( + "context" + "fmt" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/util/gutil" +) + +// 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]*gdb.TableField, err error) { + var ( + result gdb.Result + link gdb.Link + usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) + ) + if link, err = d.SlaveLink(usedSchema); err != nil { + return nil, err + } + result, err = d.DoSelect(ctx, link, fmt.Sprintf(`PRAGMA TABLE_INFO(%s)`, d.QuoteWord(table))) + if err != nil { + return nil, err + } + fields = make(map[string]*gdb.TableField) + for i, m := range result { + mKey := "" + if m["pk"].Bool() { + mKey = "pri" + } + fields[m["name"].String()] = &gdb.TableField{ + Index: i, + Name: m["name"].String(), + Type: m["type"].String(), + Key: mKey, + Default: m["dflt_value"].Val(), + Null: !m["notnull"].Bool(), + } + } + return fields, nil +} diff --git a/contrib/drivers/sqlite/sqlite_tables.go b/contrib/drivers/sqlite/sqlite_tables.go new file mode 100644 index 000000000..f8999e5c4 --- /dev/null +++ b/contrib/drivers/sqlite/sqlite_tables.go @@ -0,0 +1,38 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package sqlite + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" +) + +const ( + tablesSqlTmp = `SELECT NAME FROM SQLITE_MASTER WHERE TYPE='table' ORDER BY NAME` +) + +// 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 gdb.Result + 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 +} diff --git a/contrib/drivers/sqlite/sqlite_core_test.go b/contrib/drivers/sqlite/sqlite_z_unit_core_test.go similarity index 100% rename from contrib/drivers/sqlite/sqlite_core_test.go rename to contrib/drivers/sqlite/sqlite_z_unit_core_test.go diff --git a/contrib/drivers/sqlite/sqlite_0_test.go b/contrib/drivers/sqlite/sqlite_z_unit_init_test.go similarity index 100% rename from contrib/drivers/sqlite/sqlite_0_test.go rename to contrib/drivers/sqlite/sqlite_z_unit_init_test.go diff --git a/contrib/drivers/sqlite/sqlite_model_test.go b/contrib/drivers/sqlite/sqlite_z_unit_model_test.go similarity index 100% rename from contrib/drivers/sqlite/sqlite_model_test.go rename to contrib/drivers/sqlite/sqlite_z_unit_model_test.go diff --git a/contrib/drivers/sqlitecgo/go.mod b/contrib/drivers/sqlitecgo/go.mod index 61980469e..ca2d66d62 100644 --- a/contrib/drivers/sqlitecgo/go.mod +++ b/contrib/drivers/sqlitecgo/go.mod @@ -3,6 +3,7 @@ module github.com/gogf/gf/contrib/drivers/sqlitecgo/v2 go 1.18 require ( + github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.6.2 github.com/gogf/gf/v2 v2.6.2 github.com/mattn/go-sqlite3 v1.14.17 ) @@ -10,10 +11,13 @@ require ( require ( github.com/BurntSushi/toml v1.2.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/color v1.15.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grokify/html-strip-tags-go v0.0.1 // indirect github.com/magiconair/properties v1.8.6 // indirect @@ -21,6 +25,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.4 // indirect go.opentelemetry.io/otel v1.14.0 // indirect go.opentelemetry.io/otel/sdk v1.14.0 // indirect @@ -29,6 +34,13 @@ require ( golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect ) -replace github.com/gogf/gf/v2 => ../../../ +replace ( + github.com/gogf/gf/contrib/drivers/sqlite/v2 => ../sqlite/ + github.com/gogf/gf/v2 => ../../../ +) diff --git a/contrib/drivers/sqlitecgo/go.sum b/contrib/drivers/sqlitecgo/go.sum index 22f425241..110342772 100644 --- a/contrib/drivers/sqlitecgo/go.sum +++ b/contrib/drivers/sqlitecgo/go.sum @@ -3,16 +3,23 @@ github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0= @@ -32,6 +39,9 @@ github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -54,3 +64,11 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= diff --git a/contrib/drivers/sqlitecgo/sqlite.go b/contrib/drivers/sqlitecgo/sqlitecgo.go similarity index 53% rename from contrib/drivers/sqlitecgo/sqlite.go rename to contrib/drivers/sqlitecgo/sqlitecgo.go index de257bfe2..e77b3f259 100644 --- a/contrib/drivers/sqlitecgo/sqlite.go +++ b/contrib/drivers/sqlitecgo/sqlitecgo.go @@ -13,12 +13,12 @@ package sqlitecgo import ( - "context" "database/sql" "fmt" _ "github.com/mattn/go-sqlite3" + "github.com/gogf/gf/contrib/drivers/sqlite/v2" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/encoding/gurl" "github.com/gogf/gf/v2/errors/gcode" @@ -26,16 +26,15 @@ import ( "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/text/gstr" "github.com/gogf/gf/v2/util/gconv" - "github.com/gogf/gf/v2/util/gutil" ) // Driver is the driver for sqlite database. type Driver struct { - *gdb.Core + gdb.DB } -const ( - quoteChar = "`" +var ( + sqliteDriver = sqlite.New() ) func init() { @@ -52,8 +51,12 @@ func New() gdb.Driver { // New creates and returns a database object for sqlite. // It implements the interface of gdb.Driver for extra database driver installation. func (d *Driver) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) { + db, err := sqliteDriver.New(core, node) + if err != nil { + return nil, err + } return &Driver{ - Core: core, + DB: db, }, nil } @@ -107,88 +110,3 @@ func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { } return } - -// GetChars returns the security char for this type of database. -func (d *Driver) GetChars() (charLeft string, charRight string) { - return quoteChar, quoteChar -} - -// DoFilter deals with the sql string before commits it to underlying sql driver. -func (d *Driver) DoFilter(ctx context.Context, link gdb.Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, err error) { - // Special insert/ignore operation for sqlite. - switch { - case gstr.HasPrefix(sql, gdb.InsertOperationIgnore): - sql = "INSERT OR IGNORE" + sql[len(gdb.InsertOperationIgnore):] - - case gstr.HasPrefix(sql, gdb.InsertOperationReplace): - sql = "INSERT OR REPLACE" + sql[len(gdb.InsertOperationReplace):] - - default: - if gstr.Contains(sql, gdb.InsertOnDuplicateKeyUpdate) { - return sql, args, gerror.NewCode( - gcode.CodeNotSupported, - `Save operation is not supported by sqlite driver`, - ) - } - } - return d.Core.DoFilter(ctx, link, sql, args) -} - -// 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 gdb.Result - link, err := d.SlaveLink(schema...) - if err != nil { - return nil, err - } - - result, err = d.DoSelect( - ctx, - link, - `SELECT NAME FROM SQLITE_MASTER WHERE TYPE='table' ORDER BY NAME`, - ) - if err != nil { - return - } - for _, m := range result { - for _, v := range m { - tables = append(tables, v.String()) - } - } - return -} - -// 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]*gdb.TableField, err error) { - var ( - result gdb.Result - link gdb.Link - usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) - ) - if link, err = d.SlaveLink(usedSchema); err != nil { - return nil, err - } - result, err = d.DoSelect(ctx, link, fmt.Sprintf(`PRAGMA TABLE_INFO(%s)`, d.QuoteWord(table))) - if err != nil { - return nil, err - } - fields = make(map[string]*gdb.TableField) - for i, m := range result { - mKey := "" - if m["pk"].Bool() { - mKey = "pri" - } - fields[m["name"].String()] = &gdb.TableField{ - Index: i, - Name: m["name"].String(), - Type: m["type"].String(), - Key: mKey, - Default: m["dflt_value"].Val(), - Null: !m["notnull"].Bool(), - } - } - return fields, nil -} diff --git a/contrib/drivers/sqlitecgo/sqlite_core_test.go b/contrib/drivers/sqlitecgo/sqlitecgo_z_unit_core_test.go similarity index 100% rename from contrib/drivers/sqlitecgo/sqlite_core_test.go rename to contrib/drivers/sqlitecgo/sqlitecgo_z_unit_core_test.go diff --git a/contrib/drivers/sqlitecgo/sqlite_0_test.go b/contrib/drivers/sqlitecgo/sqlitecgo_z_unit_init_test.go similarity index 100% rename from contrib/drivers/sqlitecgo/sqlite_0_test.go rename to contrib/drivers/sqlitecgo/sqlitecgo_z_unit_init_test.go diff --git a/contrib/drivers/sqlitecgo/sqlite_model_test.go b/contrib/drivers/sqlitecgo/sqlitecgo_z_unit_model_test.go similarity index 100% rename from contrib/drivers/sqlitecgo/sqlite_model_test.go rename to contrib/drivers/sqlitecgo/sqlitecgo_z_unit_model_test.go diff --git a/database/gdb/gdb_core.go b/database/gdb/gdb_core.go index 482caf80b..556318d6a 100644 --- a/database/gdb/gdb_core.go +++ b/database/gdb/gdb_core.go @@ -487,12 +487,13 @@ func (c *Core) DoInsert(ctx context.Context, link Link, table string, list List, keysStr = charL + strings.Join(keys, charR+","+charL) + charR operation = GetInsertOperationByOption(option.InsertOption) ) + // `ON DUPLICATED...` statement only takes effect on Save operation. if option.InsertOption == InsertOptionSave { onDuplicateStr = c.formatOnDuplicate(keys, option) } var ( - listLength = len(list) - valueHolder = make([]string, 0) + listLength = len(list) + valueHolders = make([]string, 0) ) for i := 0; i < listLength; i++ { values = values[:0] @@ -506,9 +507,9 @@ func (c *Core) DoInsert(ctx context.Context, link Link, table string, list List, params = append(params, list[i][k]) } } - valueHolder = append(valueHolder, "("+gstr.Join(values, ",")+")") + valueHolders = append(valueHolders, "("+gstr.Join(values, ",")+")") // Batch package checks: It meets the batch number, or it is the last element. - if len(valueHolder) == option.BatchCount || (i == listLength-1 && len(valueHolder) > 0) { + if len(valueHolders) == option.BatchCount || (i == listLength-1 && len(valueHolders) > 0) { var ( stdSqlResult sql.Result affectedRows int64 @@ -516,7 +517,7 @@ func (c *Core) DoInsert(ctx context.Context, link Link, table string, list List, stdSqlResult, err = c.db.DoExec(ctx, link, fmt.Sprintf( "%s INTO %s(%s) VALUES%s %s", operation, c.QuotePrefixTableName(table), keysStr, - gstr.Join(valueHolder, ","), + gstr.Join(valueHolders, ","), onDuplicateStr, ), params...) if err != nil { @@ -530,7 +531,7 @@ func (c *Core) DoInsert(ctx context.Context, link Link, table string, list List, batchResult.Affected += affectedRows } params = params[:0] - valueHolder = valueHolder[:0] + valueHolders = valueHolders[:0] } } return batchResult, nil