diff --git a/.example/other/test.go b/.example/other/test.go index 460bdfbba..34747c58e 100644 --- a/.example/other/test.go +++ b/.example/other/test.go @@ -1,47 +1,23 @@ package main import ( - "encoding/json" "fmt" - "github.com/gogf/gf/encoding/gjson" + "github.com/gogf/gf/text/gregex" + "github.com/gogf/gf/text/gstr" ) -type A struct { - F string - G string -} - -type B struct { - A - H string -} - -type C struct { - A A - F string -} - -type D struct { - I A - F string -} - -func SystemJsonEncode(a interface{}) string { - js, err := json.Marshal(a) - if err != nil { - return "{}" - } else { - return fmt.Sprintf("%s", js) - } -} - func main() { - fmt.Println("encoding/json", SystemJsonEncode(B{})) - fmt.Println("gjson", gjson.New(B{}).MustToJsonString()) - fmt.Println() - fmt.Println("encoding/json", SystemJsonEncode(C{})) - fmt.Println("gjson", gjson.New(C{}).MustToJsonString()) - fmt.Println() - fmt.Println("encoding/json", SystemJsonEncode(D{})) - fmt.Println("gjson", gjson.New(D{}).MustToJsonString()) + s := `user u LEFT JOIN user_detail ud ON(ud.id=u.id) LEFT JOIN user_stats us ON(us.id=u.id)` + split := " JOIN " + as := "my-as" + if gstr.Contains(s, split) { + // For join table. + array := gstr.Split(s, split) + array[len(array)-1], _ = gregex.ReplaceString(`(.+) ON`, fmt.Sprintf(`$1 AS %s ON`, as), array[len(array)-1]) + s = gstr.Join(array, split) + } else { + // For base table. + s = gstr.TrimRight(s) + " AS " + as + } + fmt.Println(s) } diff --git a/database/gdb/gdb.go b/database/gdb/gdb.go index 6272caa74..efa029572 100644 --- a/database/gdb/gdb.go +++ b/database/gdb/gdb.go @@ -78,8 +78,8 @@ type DB interface { Delete(table string, condition interface{}, args ...interface{}) (sql.Result, error) // Model creation. - Table(tables string) *Model - Model(tables string) *Model + Table(table ...string) *Model + Model(table ...string) *Model Schema(schema string) *Schema // Configuration methods. diff --git a/database/gdb/gdb_core.go b/database/gdb/gdb_core.go index ffa79bfac..ecf0833b4 100644 --- a/database/gdb/gdb_core.go +++ b/database/gdb/gdb_core.go @@ -11,6 +11,7 @@ import ( "database/sql" "errors" "fmt" + "github.com/gogf/gf/internal/utils" "reflect" "regexp" "strings" @@ -417,7 +418,7 @@ func (c *Core) DoInsert(link Link, table string, data interface{}, option int, b for k, _ := range dataMap { // If it's SAVE operation, // do not automatically update the creating time. - if k == gSOFT_FIELD_NAME_CREATE { + if utils.EqualFoldWithoutChars(k, gSOFT_FIELD_NAME_CREATE) { continue } if len(updateStr) > 0 { @@ -538,7 +539,7 @@ func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option i for _, k := range keys { // If it's SAVE operation, // do not automatically update the creating time. - if k == gSOFT_FIELD_NAME_CREATE { + if utils.EqualFoldWithoutChars(k, gSOFT_FIELD_NAME_CREATE) { continue } if len(updateStr) > 0 { diff --git a/database/gdb/gdb_func.go b/database/gdb/gdb_func.go index b00fefd95..54a611421 100644 --- a/database/gdb/gdb_func.go +++ b/database/gdb/gdb_func.go @@ -147,8 +147,13 @@ func doQuoteWord(s, charLeft, charRight string) string { } // doQuoteString quotes string with quote chars. It handles strings like: -// "user", "user u", "user,user_detail", "user u, user_detail ut", -// "user.user u, user.user_detail ut", "u.id asc". +// "user", +// "user u", +// "user,user_detail", +// "user u, user_detail ut", +// "user.user u, user.user_detail ut", +// "u.id, u.name, u.age", +// "u.id asc". func doQuoteString(s, charLeft, charRight string) string { array1 := gstr.SplitAndTrim(s, ",") for k1, v1 := range array1 { diff --git a/database/gdb/gdb_model.go b/database/gdb/gdb_model.go index dd1a9078c..de5eb5caa 100644 --- a/database/gdb/gdb_model.go +++ b/database/gdb/gdb_model.go @@ -7,6 +7,8 @@ package gdb import ( + "fmt" + "github.com/gogf/gf/text/gregex" "time" "github.com/gogf/gf/text/gstr" @@ -59,14 +61,28 @@ const ( ) // Table creates and returns a new ORM model from given schema. -// The parameter can be more than one table names, like : -// "user", "user u", "user, user_detail", "user u, user_detail ud" -func (c *Core) Table(table string) *Model { - table = c.DB.QuotePrefixTableName(table) +// The parameter can be more than one table names, and also alias name, like: +// 1. Table names: +// Table("user") +// Table("user u") +// Table("user, user_detail") +// Table("user u, user_detail ud") +// 2. Table name with alias: Table("user", "u") +func (c *Core) Table(table ...string) *Model { + tables := "" + if len(table) > 1 { + tables = fmt.Sprintf( + `%s AS %s`, c.DB.QuotePrefixTableName(table[0]), c.DB.QuoteWord(table[1]), + ) + } else if len(table) == 1 { + tables = c.DB.QuotePrefixTableName(table[0]) + } else { + panic("table cannot be empty") + } return &Model{ db: c.DB, - tablesInit: table, - tables: table, + tablesInit: tables, + tables: tables, fields: "*", start: -1, offset: -1, @@ -76,37 +92,39 @@ func (c *Core) Table(table string) *Model { // Model is alias of Core.Table. // See Core.Table. -func (c *Core) Model(table string) *Model { - return c.DB.Table(table) +func (c *Core) Model(table ...string) *Model { + return c.DB.Table(table...) } // Table acts like Core.Table except it operates on transaction. // See Core.Table. -func (tx *TX) Table(table string) *Model { - table = tx.db.QuotePrefixTableName(table) - return &Model{ - db: tx.db, - tx: tx, - tablesInit: table, - tables: table, - fields: "*", - start: -1, - offset: -1, - option: OPTION_ALLOWEMPTY, - } +func (tx *TX) Table(table ...string) *Model { + model := tx.db.Table(table...) + model.db = tx.db + model.tx = tx + return model } // Model is alias of tx.Table. // See tx.Table. -func (tx *TX) Model(table string) *Model { - return tx.Table(table) +func (tx *TX) Model(table ...string) *Model { + return tx.Table(table...) } // As sets an alias name for current table. func (m *Model) As(as string) *Model { if m.tables != "" { model := m.getModel() - model.tables = gstr.TrimRight(model.tables) + " AS " + as + split := " JOIN " + if gstr.Contains(model.tables, split) { + // For join table. + array := gstr.Split(model.tables, split) + array[len(array)-1], _ = gregex.ReplaceString(`(.+) ON`, fmt.Sprintf(`$1 AS %s ON`, as), array[len(array)-1]) + model.tables = gstr.Join(array, split) + } else { + // For base table. + model.tables = gstr.TrimRight(model.tables) + " AS " + as + } return model } return m diff --git a/database/gdb/gdb_model_delete.go b/database/gdb/gdb_model_delete.go index 7974c7c7d..3ae07bf41 100644 --- a/database/gdb/gdb_model_delete.go +++ b/database/gdb/gdb_model_delete.go @@ -44,7 +44,7 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) { return m.db.DoUpdate( m.getLink(true), m.tables, - fmt.Sprintf(`%s='%s'`, m.db.QuoteWord(fieldNameDelete), gtime.Now().String()), + fmt.Sprintf(`%s='%s'`, m.db.QuoteString(fieldNameDelete), gtime.Now().String()), conditionWhere+conditionExtra, conditionArgs..., ) diff --git a/database/gdb/gdb_model_join.go b/database/gdb/gdb_model_join.go index d3d156c93..e6d818387 100644 --- a/database/gdb/gdb_model_join.go +++ b/database/gdb/gdb_model_join.go @@ -9,22 +9,70 @@ package gdb import "fmt" // LeftJoin does "LEFT JOIN ... ON ..." statement on the model. -func (m *Model) LeftJoin(table string, on string) *Model { +// The parameter
can be joined table and its joined condition, +// and also with its alias name, like: +// Table("user").LeftJoin("user_detail", "user_detail.uid=user.uid") +// Table("user", "u").LeftJoin("user_detail", "ud", "ud.uid=u.uid") +func (m *Model) LeftJoin(table ...string) *Model { model := m.getModel() - model.tables += fmt.Sprintf(" LEFT JOIN %s ON (%s)", m.db.QuotePrefixTableName(table), on) + if len(table) > 2 { + model.tables += fmt.Sprintf( + " LEFT JOIN %s AS %s ON (%s)", + m.db.QuotePrefixTableName(table[0]), m.db.QuoteWord(table[1]), table[2], + ) + } else if len(table) == 2 { + model.tables += fmt.Sprintf( + " LEFT JOIN %s ON (%s)", + m.db.QuotePrefixTableName(table[0]), table[1], + ) + } else { + panic("invalid join table parameter") + } return model } // RightJoin does "RIGHT JOIN ... ON ..." statement on the model. -func (m *Model) RightJoin(table string, on string) *Model { +// The parameter
can be joined table and its joined condition, +// and also with its alias name, like: +// Table("user").RightJoin("user_detail", "user_detail.uid=user.uid") +// Table("user", "u").RightJoin("user_detail", "ud", "ud.uid=u.uid") +func (m *Model) RightJoin(table ...string) *Model { model := m.getModel() - model.tables += fmt.Sprintf(" RIGHT JOIN %s ON (%s)", m.db.QuotePrefixTableName(table), on) + if len(table) > 2 { + model.tables += fmt.Sprintf( + " RIGHT JOIN %s AS %s ON (%s)", + m.db.QuotePrefixTableName(table[0]), m.db.QuoteWord(table[1]), table[2], + ) + } else if len(table) == 2 { + model.tables += fmt.Sprintf( + " RIGHT JOIN %s ON (%s)", + m.db.QuotePrefixTableName(table[0]), table[1], + ) + } else { + panic("invalid join table parameter") + } return model } // InnerJoin does "INNER JOIN ... ON ..." statement on the model. -func (m *Model) InnerJoin(table string, on string) *Model { +// The parameter
can be joined table and its joined condition, +// and also with its alias name, like: +// Table("user").InnerJoin("user_detail", "user_detail.uid=user.uid") +// Table("user", "u").InnerJoin("user_detail", "ud", "ud.uid=u.uid") +func (m *Model) InnerJoin(table ...string) *Model { model := m.getModel() - model.tables += fmt.Sprintf(" INNER JOIN %s ON (%s)", m.db.QuotePrefixTableName(table), on) + if len(table) > 2 { + model.tables += fmt.Sprintf( + " INNER JOIN %s AS %s ON (%s)", + m.db.QuotePrefixTableName(table[0]), m.db.QuoteWord(table[1]), table[2], + ) + } else if len(table) == 2 { + model.tables += fmt.Sprintf( + " INNER JOIN %s ON (%s)", + m.db.QuotePrefixTableName(table[0]), table[1], + ) + } else { + panic("invalid join table parameter") + } return model } diff --git a/database/gdb/gdb_model_select.go b/database/gdb/gdb_model_select.go index aefd83197..e99d66cad 100644 --- a/database/gdb/gdb_model_select.go +++ b/database/gdb/gdb_model_select.go @@ -31,19 +31,24 @@ func (m *Model) All(where ...interface{}) (Result, error) { return m.Where(where[0], where[1:]...).All() } var ( - fieldNameDelete = m.getSoftFieldNameDelete() + softDeletingCondition = m.getConditionForSoftDeleting() conditionWhere, conditionExtra, conditionArgs = m.formatCondition(false) ) - if !m.unscoped && fieldNameDelete != "" { + if !m.unscoped && softDeletingCondition != "" { if conditionWhere == "" { - conditionWhere = " WHERE" + conditionWhere = " WHERE " } else { - conditionWhere += " AND" + conditionWhere += " AND " } - conditionWhere += fmt.Sprintf(` %s IS NULL`, m.db.QuoteWord(fieldNameDelete)) + conditionWhere += softDeletingCondition } return m.getAll( - fmt.Sprintf("SELECT %s FROM %s%s", m.fields, m.tables, conditionWhere+conditionExtra), + fmt.Sprintf( + "SELECT %s FROM %s%s", + m.db.QuoteString(m.fields), + m.tables, + conditionWhere+conditionExtra, + ), conditionArgs..., ) } @@ -244,19 +249,19 @@ func (m *Model) Count(where ...interface{}) (int, error) { } countFields := "COUNT(1)" if m.fields != "" && m.fields != "*" { - countFields = fmt.Sprintf(`COUNT(%s)`, m.fields) + countFields = fmt.Sprintf(`COUNT(%s)`, m.db.QuoteString(m.fields)) } var ( - fieldNameDelete = m.getSoftFieldNameDelete() + softDeletingCondition = m.getConditionForSoftDeleting() conditionWhere, conditionExtra, conditionArgs = m.formatCondition(false) ) - if !m.unscoped && fieldNameDelete != "" { + if !m.unscoped && softDeletingCondition != "" { if conditionWhere == "" { - conditionWhere = " WHERE" + conditionWhere = " WHERE " } else { - conditionWhere += " AND" + conditionWhere += " AND " } - conditionWhere += fmt.Sprintf(` %s IS NULL`, m.db.QuoteWord(fieldNameDelete)) + conditionWhere += softDeletingCondition } s := fmt.Sprintf("SELECT %s FROM %s %s", countFields, m.tables, conditionWhere+conditionExtra) diff --git a/database/gdb/gdb_model_time.go b/database/gdb/gdb_model_time.go index c3351a328..870f8ec62 100644 --- a/database/gdb/gdb_model_time.go +++ b/database/gdb/gdb_model_time.go @@ -1,4 +1,4 @@ -// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved. +// Copyright 2020 gf Author(https://github.com/gogf/gf). 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, @@ -7,6 +7,10 @@ package gdb import ( + "fmt" + "github.com/gogf/gf/container/garray" + "github.com/gogf/gf/text/gregex" + "github.com/gogf/gf/text/gstr" "github.com/gogf/gf/util/gconv" "github.com/gogf/gf/util/gutil" ) @@ -20,38 +24,121 @@ const ( // getSoftFieldNameCreate checks and returns the field name for record creating time. // If there's no field name for storing creating time, it returns an empty string. // It checks the key with or without cases or chars '-'/'_'/'.'/' '. -func (m *Model) getSoftFieldNameCreate() (field string) { - fieldsMap, _ := m.db.TableFields(m.tables) - if len(fieldsMap) > 0 { - field, _ = gutil.MapPossibleItemByKey( - gconv.Map(fieldsMap), gSOFT_FIELD_NAME_CREATE, - ) +func (m *Model) getSoftFieldNameCreate(table ...string) string { + name := "" + if len(table) > 0 { + name = table[0] + } else { + name = m.getPrimaryTableName() } - return + return m.getSoftFieldName(name, gSOFT_FIELD_NAME_CREATE) } // getSoftFieldNameUpdate checks and returns the field name for record updating time. // If there's no field name for storing updating time, it returns an empty string. // It checks the key with or without cases or chars '-'/'_'/'.'/' '. -func (m *Model) getSoftFieldNameUpdate() (field string) { - fieldsMap, _ := m.db.TableFields(m.tables) - if len(fieldsMap) > 0 { - field, _ = gutil.MapPossibleItemByKey( - gconv.Map(fieldsMap), gSOFT_FIELD_NAME_UPDATE, - ) +func (m *Model) getSoftFieldNameUpdate(table ...string) (field string) { + name := "" + if len(table) > 0 { + name = table[0] + } else { + name = m.getPrimaryTableName() } - return + return m.getSoftFieldName(name, gSOFT_FIELD_NAME_UPDATE) } // getSoftFieldNameDelete checks and returns the field name for record deleting time. // If there's no field name for storing deleting time, it returns an empty string. // It checks the key with or without cases or chars '-'/'_'/'.'/' '. -func (m *Model) getSoftFieldNameDelete() (field string) { - fieldsMap, _ := m.db.TableFields(m.tables) +func (m *Model) getSoftFieldNameDelete(table ...string) (field string) { + name := "" + if len(table) > 0 { + name = table[0] + } else { + name = m.getPrimaryTableName() + } + return m.getSoftFieldName(name, gSOFT_FIELD_NAME_DELETE) +} + +// getSoftFieldName retrieves and returns the field name of the table for possible key. +func (m *Model) getSoftFieldName(table string, key string) (field string) { + fieldsMap, _ := m.db.TableFields(table) if len(fieldsMap) > 0 { field, _ = gutil.MapPossibleItemByKey( - gconv.Map(fieldsMap), gSOFT_FIELD_NAME_DELETE, + gconv.Map(fieldsMap), key, ) } return } + +// getConditionForSoftDeleting retrieves and returns the condition string for soft deleting. +// It supports multiple tables string like: +// "user u, user_detail ud" +// "user u LEFT JOIN user_detail ud ON(ud.uid=u.uid)" +// "user LEFT JOIN user_detail ON(user_detail.uid=user.uid)" +// "user u LEFT JOIN user_detail ud ON(ud.uid=u.uid) LEFT JOIN user_stats us ON(us.uid=u.uid)" +func (m *Model) getConditionForSoftDeleting() string { + conditionArray := garray.NewStrArray() + if gstr.Contains(m.tables, " JOIN ") { + // Base table. + match, _ := gregex.MatchString(`(.+?) [A-Z]+ JOIN`, m.tables) + conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(match[1])) + // Multiple joined tables. + matches, _ := gregex.MatchAllString(`JOIN (.+?) ON`, m.tables) + for _, match := range matches { + conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(match[1])) + } + } + if conditionArray.Len() == 0 && gstr.Contains(m.tables, ",") { + // Multiple base tables. + for _, s := range gstr.SplitAndTrim(m.tables, ",") { + conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(s)) + } + } + conditionArray.FilterEmpty() + if conditionArray.Len() > 0 { + return conditionArray.Join(" AND ") + } + // Only one table. + if fieldName := m.getSoftFieldNameDelete(); fieldName != "" { + return fmt.Sprintf(`%s IS NULL`, m.db.QuoteWord(fieldName)) + } + return "" +} + +// getConditionOfTableStringForSoftDeleting does something as its name describes. +func (m *Model) getConditionOfTableStringForSoftDeleting(s string) string { + var ( + field = "" + table = "" + array1 = gstr.SplitAndTrim(s, " ") + array2 = gstr.SplitAndTrim(array1[0], ".") + ) + if len(array2) >= 2 { + table = array2[1] + } else { + table = array2[0] + } + field = m.getSoftFieldNameDelete(table) + if field == "" { + return "" + } + if len(array1) >= 3 { + return fmt.Sprintf(`%s.%s IS NULL`, m.db.QuoteWord(array1[2]), m.db.QuoteWord(field)) + } + if len(array1) >= 2 { + return fmt.Sprintf(`%s.%s IS NULL`, m.db.QuoteWord(array1[1]), m.db.QuoteWord(field)) + } + return fmt.Sprintf(`%s.%s IS NULL`, m.db.QuoteWord(table), m.db.QuoteWord(field)) +} + +// getPrimaryTableName parses and returns the primary table name. +func (m *Model) getPrimaryTableName() string { + array1 := gstr.SplitAndTrim(m.tables, ",") + array2 := gstr.SplitAndTrim(array1[0], " ") + array3 := gstr.SplitAndTrim(array2[0], ".") + if len(array3) >= 2 { + return array3[1] + } + return array3[0] +} diff --git a/database/gdb/gdb_model_utility.go b/database/gdb/gdb_model_utility.go index 71398af80..b5e75e7f2 100644 --- a/database/gdb/gdb_model_utility.go +++ b/database/gdb/gdb_model_utility.go @@ -74,8 +74,13 @@ func (m *Model) doFilterDataMapForInsertOrUpdate(data Map, allowOmitEmpty bool) if len(m.fields) > 0 && m.fields != "*" { // Keep specified fields. - set := gset.NewStrSetFrom(gstr.SplitAndTrim(m.fields, ",")) + var ( + set = gset.NewStrSetFrom(gstr.SplitAndTrim(m.fields, ",")) + charL, charR = m.db.GetChars() + chars = charL + charR + ) for k := range data { + k = gstr.Trim(k, chars) if !set.Contains(k) { delete(data, k) } diff --git a/database/gdb/gdb_unit_z_func_test.go b/database/gdb/gdb_unit_z_func_test.go deleted file mode 100644 index 3e6941c97..000000000 --- a/database/gdb/gdb_unit_z_func_test.go +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2019 gf Author(https://github.com/gogf/gf). 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 gdb - -import ( - "github.com/gogf/gf/test/gtest" - "testing" -) - -func Test_Func_FormatSqlWithArgs(t *testing.T) { - // mysql - gtest.C(t, func(t *gtest.T) { - var s string - s = FormatSqlWithArgs("select * from table where id>=? and sex=?", []interface{}{100, 1}) - t.Assert(s, "select * from table where id>=100 and sex=1") - }) - // mssql - gtest.C(t, func(t *gtest.T) { - var s string - s = FormatSqlWithArgs("select * from table where id>=@p1 and sex=@p2", []interface{}{100, 1}) - t.Assert(s, "select * from table where id>=100 and sex=1") - }) - // pgsql - gtest.C(t, func(t *gtest.T) { - var s string - s = FormatSqlWithArgs("select * from table where id>=$1 and sex=$2", []interface{}{100, 1}) - t.Assert(s, "select * from table where id>=100 and sex=1") - }) - // oracle - gtest.C(t, func(t *gtest.T) { - var s string - s = FormatSqlWithArgs("select * from table where id>=:1 and sex=:2", []interface{}{100, 1}) - t.Assert(s, "select * from table where id>=100 and sex=1") - }) -} - -func Test_Func_doQuoteWord(t *testing.T) { - gtest.C(t, func(t *gtest.T) { - array := map[string]string{ - "user": "`user`", - "user u": "user u", - "user_detail": "`user_detail`", - "user,user_detail": "user,user_detail", - "user u, user_detail ut": "user u, user_detail ut", - "u.id asc": "u.id asc", - "u.id asc, ut.uid desc": "u.id asc, ut.uid desc", - } - for k, v := range array { - t.Assert(doQuoteWord(k, "`", "`"), v) - } - }) -} - -func Test_Func_doQuoteString(t *testing.T) { - gtest.C(t, func(t *gtest.T) { - // "user", "user u", "user,user_detail", "user u, user_detail ut", "u.id asc". - array := map[string]string{ - "user": "`user`", - "user u": "`user` u", - "user,user_detail": "`user`,`user_detail`", - "user u, user_detail ut": "`user` u,`user_detail` ut", - "u.id asc": "`u`.`id` asc", - "u.id asc, ut.uid desc": "`u`.`id` asc,`ut`.`uid` desc", - "user.user u, user.user_detail ut": "`user`.`user` u,`user`.`user_detail` ut", - // mssql global schema access with double dots. - "user..user u, user.user_detail ut": "`user`..`user` u,`user`.`user_detail` ut", - } - for k, v := range array { - t.Assert(doQuoteString(k, "`", "`"), v) - } - }) -} - -func Test_Func_addTablePrefix(t *testing.T) { - gtest.C(t, func(t *gtest.T) { - prefix := "" - array := map[string]string{ - "user": "`user`", - "user u": "`user` u", - "user as u": "`user` as u", - "user,user_detail": "`user`,`user_detail`", - "user u, user_detail ut": "`user` u,`user_detail` ut", - "`user`.user_detail": "`user`.`user_detail`", - "`user`.`user_detail`": "`user`.`user_detail`", - "user as u, user_detail as ut": "`user` as u,`user_detail` as ut", - "UserCenter.user as u, UserCenter.user_detail as ut": "`UserCenter`.`user` as u,`UserCenter`.`user_detail` as ut", - // mssql global schema access with double dots. - "UserCenter..user as u, user_detail as ut": "`UserCenter`..`user` as u,`user_detail` as ut", - } - for k, v := range array { - t.Assert(doHandleTableName(k, prefix, "`", "`"), v) - } - }) - gtest.C(t, func(t *gtest.T) { - prefix := "gf_" - array := map[string]string{ - "user": "`gf_user`", - "user u": "`gf_user` u", - "user as u": "`gf_user` as u", - "user,user_detail": "`gf_user`,`gf_user_detail`", - "user u, user_detail ut": "`gf_user` u,`gf_user_detail` ut", - "`user`.user_detail": "`user`.`gf_user_detail`", - "`user`.`user_detail`": "`user`.`gf_user_detail`", - "user as u, user_detail as ut": "`gf_user` as u,`gf_user_detail` as ut", - "UserCenter.user as u, UserCenter.user_detail as ut": "`UserCenter`.`gf_user` as u,`UserCenter`.`gf_user_detail` as ut", - // mssql global schema access with double dots. - "UserCenter..user as u, user_detail as ut": "`UserCenter`..`gf_user` as u,`gf_user_detail` as ut", - } - for k, v := range array { - t.Assert(doHandleTableName(k, prefix, "`", "`"), v) - } - }) -} diff --git a/database/gdb/gdb_unit_z_mysql_internal_test.go b/database/gdb/gdb_unit_z_mysql_internal_test.go new file mode 100644 index 000000000..a25d5391c --- /dev/null +++ b/database/gdb/gdb_unit_z_mysql_internal_test.go @@ -0,0 +1,289 @@ +// Copyright 2019 gf Author(https://github.com/gogf/gf). 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 gdb + +import ( + "fmt" + "github.com/gogf/gf/os/gcmd" + "github.com/gogf/gf/os/gtime" + "github.com/gogf/gf/test/gtest" + "testing" +) + +const ( + SCHEMA = "test_internal" +) + +var ( + db DB + configNode ConfigNode +) + +func init() { + parser, err := gcmd.Parse(map[string]bool{ + "name": true, + "type": true, + }, false) + gtest.Assert(err, nil) + configNode = ConfigNode{ + Host: "127.0.0.1", + Port: "3306", + User: "root", + Pass: "12345678", + Name: parser.GetOpt("name", ""), + Type: parser.GetOpt("type", "mysql"), + Role: "master", + Charset: "utf8", + Weight: 1, + MaxIdleConnCount: 10, + MaxOpenConnCount: 10, + MaxConnLifetime: 600, + } + AddConfigNode(DEFAULT_GROUP_NAME, configNode) + // Default db. + if r, err := New(); err != nil { + gtest.Error(err) + } else { + db = r + } + schemaTemplate := "CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET UTF8" + if _, err := db.Exec(fmt.Sprintf(schemaTemplate, SCHEMA)); err != nil { + gtest.Error(err) + } + db.SetSchema(SCHEMA) +} + +func dropTable(table string) { + if _, err := db.Exec(fmt.Sprintf("DROP TABLE IF EXISTS `%s`", table)); err != nil { + gtest.Error(err) + } +} + +func Test_Func_FormatSqlWithArgs(t *testing.T) { + // mysql + gtest.C(t, func(t *gtest.T) { + var s string + s = FormatSqlWithArgs("select * from table where id>=? and sex=?", []interface{}{100, 1}) + t.Assert(s, "select * from table where id>=100 and sex=1") + }) + // mssql + gtest.C(t, func(t *gtest.T) { + var s string + s = FormatSqlWithArgs("select * from table where id>=@p1 and sex=@p2", []interface{}{100, 1}) + t.Assert(s, "select * from table where id>=100 and sex=1") + }) + // pgsql + gtest.C(t, func(t *gtest.T) { + var s string + s = FormatSqlWithArgs("select * from table where id>=$1 and sex=$2", []interface{}{100, 1}) + t.Assert(s, "select * from table where id>=100 and sex=1") + }) + // oracle + gtest.C(t, func(t *gtest.T) { + var s string + s = FormatSqlWithArgs("select * from table where id>=:1 and sex=:2", []interface{}{100, 1}) + t.Assert(s, "select * from table where id>=100 and sex=1") + }) +} + +func Test_Func_doQuoteWord(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + array := map[string]string{ + "user": "`user`", + "user u": "user u", + "user_detail": "`user_detail`", + "user,user_detail": "user,user_detail", + "user u, user_detail ut": "user u, user_detail ut", + "u.id asc": "u.id asc", + "u.id asc, ut.uid desc": "u.id asc, ut.uid desc", + } + for k, v := range array { + t.Assert(doQuoteWord(k, "`", "`"), v) + } + }) +} + +func Test_Func_doQuoteString(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + array := map[string]string{ + "user": "`user`", + "user u": "`user` u", + "user,user_detail": "`user`,`user_detail`", + "user u, user_detail ut": "`user` u,`user_detail` ut", + "u.id, u.name, u.age": "`u`.`id`,`u`.`name`,`u`.`age`", + "u.id asc": "`u`.`id` asc", + "u.id asc, ut.uid desc": "`u`.`id` asc,`ut`.`uid` desc", + "user.user u, user.user_detail ut": "`user`.`user` u,`user`.`user_detail` ut", + // mssql global schema access with double dots. + "user..user u, user.user_detail ut": "`user`..`user` u,`user`.`user_detail` ut", + } + for k, v := range array { + t.Assert(doQuoteString(k, "`", "`"), v) + } + }) +} + +func Test_Func_addTablePrefix(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + prefix := "" + array := map[string]string{ + "user": "`user`", + "user u": "`user` u", + "user as u": "`user` as u", + "user,user_detail": "`user`,`user_detail`", + "user u, user_detail ut": "`user` u,`user_detail` ut", + "`user`.user_detail": "`user`.`user_detail`", + "`user`.`user_detail`": "`user`.`user_detail`", + "user as u, user_detail as ut": "`user` as u,`user_detail` as ut", + "UserCenter.user as u, UserCenter.user_detail as ut": "`UserCenter`.`user` as u,`UserCenter`.`user_detail` as ut", + // mssql global schema access with double dots. + "UserCenter..user as u, user_detail as ut": "`UserCenter`..`user` as u,`user_detail` as ut", + } + for k, v := range array { + t.Assert(doHandleTableName(k, prefix, "`", "`"), v) + } + }) + gtest.C(t, func(t *gtest.T) { + prefix := "gf_" + array := map[string]string{ + "user": "`gf_user`", + "user u": "`gf_user` u", + "user as u": "`gf_user` as u", + "user,user_detail": "`gf_user`,`gf_user_detail`", + "user u, user_detail ut": "`gf_user` u,`gf_user_detail` ut", + "`user`.user_detail": "`user`.`gf_user_detail`", + "`user`.`user_detail`": "`user`.`gf_user_detail`", + "user as u, user_detail as ut": "`gf_user` as u,`gf_user_detail` as ut", + "UserCenter.user as u, UserCenter.user_detail as ut": "`UserCenter`.`gf_user` as u,`UserCenter`.`gf_user_detail` as ut", + // mssql global schema access with double dots. + "UserCenter..user as u, user_detail as ut": "`UserCenter`..`gf_user` as u,`gf_user_detail` as ut", + } + for k, v := range array { + t.Assert(doHandleTableName(k, prefix, "`", "`"), v) + } + }) +} + +func Test_Model_getSoftFieldName(t *testing.T) { + table1 := "soft_deleting_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE %s ( + id int(11) NOT NULL, + name varchar(45) DEFAULT NULL, + create_at datetime DEFAULT NULL, + update_at datetime DEFAULT NULL, + delete_at datetime DEFAULT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table1)); err != nil { + gtest.Error(err) + } + defer dropTable(table1) + + table2 := "soft_deleting_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE %s ( + id int(11) NOT NULL, + name varchar(45) DEFAULT NULL, + createat datetime DEFAULT NULL, + updateat datetime DEFAULT NULL, + deleteat datetime DEFAULT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table2)); err != nil { + gtest.Error(err) + } + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + model := db.Table(table1) + gtest.Assert(model.getSoftFieldNameCreate(table2), "createat") + gtest.Assert(model.getSoftFieldNameUpdate(table2), "updateat") + gtest.Assert(model.getSoftFieldNameDelete(table2), "deleteat") + }) +} + +func Test_Model_getConditionForSoftDeleting(t *testing.T) { + table1 := "soft_deleting_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE %s ( + id1 int(11) NOT NULL, + name1 varchar(45) DEFAULT NULL, + create_at datetime DEFAULT NULL, + update_at datetime DEFAULT NULL, + delete_at datetime DEFAULT NULL, + PRIMARY KEY (id1) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table1)); err != nil { + gtest.Error(err) + } + defer dropTable(table1) + + table2 := "soft_deleting_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE %s ( + id2 int(11) NOT NULL, + name2 varchar(45) DEFAULT NULL, + createat datetime DEFAULT NULL, + updateat datetime DEFAULT NULL, + deleteat datetime DEFAULT NULL, + PRIMARY KEY (id2) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table2)); err != nil { + gtest.Error(err) + } + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + model := db.Table(table1) + t.Assert(model.getConditionForSoftDeleting(), "`delete_at` IS NULL") + }) + gtest.C(t, func(t *gtest.T) { + model := db.Table(fmt.Sprintf(`%s as t`, table1)) + t.Assert(model.getConditionForSoftDeleting(), "`delete_at` IS NULL") + }) + gtest.C(t, func(t *gtest.T) { + model := db.Table(fmt.Sprintf(`%s, %s`, table1, table2)) + t.Assert(model.getConditionForSoftDeleting(), fmt.Sprintf( + "`%s`.`delete_at` IS NULL AND `%s`.`deleteat` IS NULL", + table1, table2, + )) + }) + gtest.C(t, func(t *gtest.T) { + model := db.Table(fmt.Sprintf(`%s t1, %s as t2`, table1, table2)) + t.Assert(model.getConditionForSoftDeleting(), "`t1`.`delete_at` IS NULL AND `t2`.`deleteat` IS NULL") + }) + gtest.C(t, func(t *gtest.T) { + model := db.Table(fmt.Sprintf(`%s as t1, %s as t2`, table1, table2)) + t.Assert(model.getConditionForSoftDeleting(), "`t1`.`delete_at` IS NULL AND `t2`.`deleteat` IS NULL") + }) + gtest.C(t, func(t *gtest.T) { + model := db.Table(fmt.Sprintf(`%s as t1`, table1)).LeftJoin(table2+" t2", "t2.id2=t1.id1") + t.Assert(model.getConditionForSoftDeleting(), "`t1`.`delete_at` IS NULL AND `t2`.`deleteat` IS NULL") + }) + gtest.C(t, func(t *gtest.T) { + model := db.Table(fmt.Sprintf(`%s`, table1)).LeftJoin(table2, "t2.id2=t1.id1") + t.Assert(model.getConditionForSoftDeleting(), fmt.Sprintf( + "`%s`.`delete_at` IS NULL AND `%s`.`deleteat` IS NULL", + table1, table2, + )) + }) + gtest.C(t, func(t *gtest.T) { + model := db.Table(fmt.Sprintf(`%s`, table1)).LeftJoin(table2, "t2.id2=t1.id1").RightJoin(table2, "t2.id2=t1.id1") + t.Assert(model.getConditionForSoftDeleting(), fmt.Sprintf( + "`%s`.`delete_at` IS NULL AND `%s`.`deleteat` IS NULL AND `%s`.`deleteat` IS NULL", + table1, table2, table2, + )) + }) + gtest.C(t, func(t *gtest.T) { + model := db.Table(table1+" as t1").LeftJoin(table2+" as t2", "t2.id2=t1.id1").RightJoin(table2+" as t3 ", "t2.id2=t1.id1") + t.Assert( + model.getConditionForSoftDeleting(), + "`t1`.`delete_at` IS NULL AND `t2`.`deleteat` IS NULL AND `t3`.`deleteat` IS NULL", + ) + }) +} diff --git a/database/gdb/gdb_unit_z_mysql_time_test.go b/database/gdb/gdb_unit_z_mysql_time_test.go index 4396beab5..f32070b0d 100644 --- a/database/gdb/gdb_unit_z_mysql_time_test.go +++ b/database/gdb/gdb_unit_z_mysql_time_test.go @@ -147,3 +147,74 @@ CREATE TABLE %s ( t.Assert(i, 0) }) } + +func Test_SoftDelete_Join(t *testing.T) { + table1 := "time_test_table1" + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE %s ( + id int(11) NOT NULL, + name varchar(45) DEFAULT NULL, + create_at datetime DEFAULT NULL, + update_at datetime DEFAULT NULL, + delete_at datetime DEFAULT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table1)); err != nil { + gtest.Error(err) + } + defer dropTable(table1) + + table2 := "time_test_table2" + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE %s ( + id int(11) NOT NULL, + name varchar(45) DEFAULT NULL, + createat datetime DEFAULT NULL, + updateat datetime DEFAULT NULL, + deleteat datetime DEFAULT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table2)); err != nil { + gtest.Error(err) + } + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + //db.SetDebug(true) + dataInsert1 := g.Map{ + "id": 1, + "name": "name_1", + } + r, err := db.Table(table1).Data(dataInsert1).Insert() + t.Assert(err, nil) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + dataInsert2 := g.Map{ + "id": 1, + "name": "name_2", + } + r, err = db.Table(table2).Data(dataInsert2).Insert() + t.Assert(err, nil) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Table(table1, "t1").LeftJoin(table2, "t2", "t2.id=t1.id").Fields("t1.name").FindOne() + t.Assert(err, nil) + t.Assert(one["name"], "name_1") + + // Soft deleting. + r, err = db.Table(table1).Delete() + t.Assert(err, nil) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + one, err = db.Table(table1, "t1").LeftJoin(table2, "t2", "t2.id=t1.id").Fields("t1.name").FindOne() + t.Assert(err, nil) + t.Assert(one.IsEmpty(), true) + + one, err = db.Table(table2, "t2").LeftJoin(table1, "t1", "t2.id=t1.id").Fields("t2.name").FindOne() + t.Assert(err, nil) + t.Assert(one.IsEmpty(), true) + }) +} diff --git a/internal/utils/utils_str.go b/internal/utils/utils_str.go index 5f6a878a9..b14e13afb 100644 --- a/internal/utils/utils_str.go +++ b/internal/utils/utils_str.go @@ -6,7 +6,16 @@ package utils -import "strings" +import ( + "regexp" + "strings" +) + +var ( + // replaceCharReg is the regular expression object for replacing chars in key. + // It is used for function EqualFoldWithoutChars. + replaceCharReg, _ = regexp.Compile(`[\-\.\_\s]+`) +) // IsLetterUpper checks whether the given byte b is in upper case. func IsLetterUpper(b byte) bool { @@ -73,3 +82,12 @@ func ReplaceByMap(origin string, replaces map[string]string) string { } return origin } + +// EqualFoldWithoutChars checks string and equal case-insensitively, +// with/without chars '-'/'_'/'.'/' '. +func EqualFoldWithoutChars(s1, s2 string) bool { + return strings.EqualFold( + replaceCharReg.ReplaceAllString(s1, ""), + replaceCharReg.ReplaceAllString(s2, ""), + ) +} diff --git a/util/gutil/gutil_map.go b/util/gutil/gutil_map.go index 64fb8448c..c787075ef 100644 --- a/util/gutil/gutil_map.go +++ b/util/gutil/gutil_map.go @@ -7,8 +7,8 @@ package gutil import ( + "github.com/gogf/gf/internal/utils" "regexp" - "strings" ) var ( @@ -40,14 +40,9 @@ func MapPossibleItemByKey(data map[string]interface{}, key string) (foundKey str if v, ok := data[key]; ok { return key, v } - replacedKey := replaceCharReg.ReplaceAllString(key, "") - if v, ok := data[replacedKey]; ok { - return replacedKey, v - } - // Loop for check. + // Loop checking. for k, v := range data { - // Remove all special chars and compare with case insensitive. - if strings.EqualFold(replaceCharReg.ReplaceAllString(k, ""), replacedKey) { + if utils.EqualFoldWithoutChars(k, key) { return k, v } }