From baf30a0e992ab47b3f6ec84deeb95a502e494889 Mon Sep 17 00:00:00 2001 From: John Guo Date: Thu, 4 Dec 2025 20:12:12 +0800 Subject: [PATCH] feat(contrib/drivers/dm): add `Replace/InsertIgnore` support and field type/length enhancements for dm database (#4541) This pull request introduces significant improvements to the DM database driver, especially around insert operations, and refines documentation and tests to reflect these changes. The main focus is enabling support for "replace" and "insert ignore" operations using DM's `MERGE` statement, improving type reporting for table fields, and updating documentation for clarity and accuracy. ### DM Driver Insert Operations * Added support for `Replace` and `InsertIgnore` operations in the DM driver by internally mapping them to DM's `MERGE` statement. This enables upsert and insert-ignore behavior for DM databases, improving compatibility with other drivers. * Implemented helper methods (`doMergeInsert`, `doInsertIgnore`, and `getPrimaryKeys`) to generate correct `MERGE` SQL statements and automatically detect primary keys when needed. [[1]](diffhunk://#diff-f51b30e3f0b0f1284b905385a89992efd0de2fe9ff8c5a4062344dfab17d428eL31-R94) [[2]](diffhunk://#diff-f51b30e3f0b0f1284b905385a89992efd0de2fe9ff8c5a4062344dfab17d428eL115-R212) * Updated the logic for building update values and SQL generation to ensure correct behavior for both upsert and insert-ignore cases. [[1]](diffhunk://#diff-f51b30e3f0b0f1284b905385a89992efd0de2fe9ff8c5a4062344dfab17d428eL61-R109) [[2]](diffhunk://#diff-f51b30e3f0b0f1284b905385a89992efd0de2fe9ff8c5a4062344dfab17d428eL89-R132) [[3]](diffhunk://#diff-f51b30e3f0b0f1284b905385a89992efd0de2fe9ff8c5a4062344dfab17d428eL100-R144) [[4]](diffhunk://#diff-f51b30e3f0b0f1284b905385a89992efd0de2fe9ff8c5a4062344dfab17d428eL115-R212) ### Table Field Type Reporting * Improved the DM driver's `TableFields` method to report column types with length/precision (e.g., `VARCHAR(128)` instead of just `VARCHAR`), aligning with expectations and other drivers. [[1]](diffhunk://#diff-40a365112421ae1967bd960f8acefcc91ddb8180865b78bc49cd090fbf4883daL26-R26) [[2]](diffhunk://#diff-40a365112421ae1967bd960f8acefcc91ddb8180865b78bc49cd090fbf4883daR88-R105) * Updated related unit tests to expect the new type format for DM table fields. ### Documentation Updates * Removed outdated or redundant documentation in both English and Chinese driver README files, and clarified supported features and limitations for DM and other drivers. [[1]](diffhunk://#diff-d49f5bc3a34b11a6ccb82cc54675b06a7dea5f0a943ae91c4ca0d28bd5003299L1) [[2]](diffhunk://#diff-d49f5bc3a34b11a6ccb82cc54675b06a7dea5f0a943ae91c4ca0d28bd5003299L47-R46) [[3]](diffhunk://#diff-d49f5bc3a34b11a6ccb82cc54675b06a7dea5f0a943ae91c4ca0d28bd5003299L119-L122) [[4]](diffhunk://#diff-05411a14e9c7ca235f7f436bfde732853aa93b364361fe80d65ac768f4e4d613L1-L126) ### Test Suite Enhancements * Refactored and restored unit tests for DM driver insert operations, including tests for `Save`, `Insert`, and the new `InsertIgnore` functionality to ensure correct behavior and compatibility. [[1]](diffhunk://#diff-2b1a59b8b2adaa1ca3074629374ab122929e4d4fbb4cc794b8e1db60ebf8d4c2L143-L245) [[2]](diffhunk://#diff-2b1a59b8b2adaa1ca3074629374ab122929e4d4fbb4cc794b8e1db60ebf8d4c2R512-R632) * Minor adjustments to DM test initialization for improved clarity. ### Core Insert Logic Minor Refactoring * Minor variable renaming for clarity in the core insert logic (`gdb_core.go`), improving code readability. [[1]](diffhunk://#diff-b1bbe5e3995261813e4e0ac6ffee8a37c236eaa2759f2bd82e211711695a70bcL449-R452) [[2]](diffhunk://#diff-b1bbe5e3995261813e4e0ac6ffee8a37c236eaa2759f2bd82e211711695a70bcL466-R474) --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- contrib/drivers/README.MD | 7 +- contrib/drivers/README.zh_CN.MD | 126 -- contrib/drivers/dm/dm.go | 1 + contrib/drivers/dm/dm_do_insert.go | 145 +- contrib/drivers/dm/dm_table_fields.go | 18 +- contrib/drivers/dm/dm_z_unit_basic_test.go | 236 +-- .../dm/dm_z_unit_feature_soft_time_test.go | 1400 +++++++++++++++++ contrib/drivers/dm/dm_z_unit_init_test.go | 4 +- database/gdb/gdb_core.go | 14 +- database/gdb/gdb_model_soft_time.go | 1 + 10 files changed, 1659 insertions(+), 293 deletions(-) delete mode 100644 contrib/drivers/README.zh_CN.MD create mode 100644 contrib/drivers/dm/dm_z_unit_feature_soft_time_test.go diff --git a/contrib/drivers/README.MD b/contrib/drivers/README.MD index ef3bd361c..c10d58d66 100644 --- a/contrib/drivers/README.MD +++ b/contrib/drivers/README.MD @@ -1,4 +1,3 @@ -English | [简体中文](README.zh_CN.MD) # Database drivers @@ -44,7 +43,7 @@ func main() { ## Supported Drivers -### MySQL/MariaDB/TiDB +### MySQL/MariaDB/TiDB/OceanBase ```go import _ "github.com/gogf/gf/contrib/drivers/mysql/v2" @@ -116,10 +115,6 @@ Note: import _ "github.com/gogf/gf/contrib/drivers/dm/v2" ``` -Note: - -- It does not support `Replace` features. - ## Custom Drivers It's quick and easy, please refer to current driver source. diff --git a/contrib/drivers/README.zh_CN.MD b/contrib/drivers/README.zh_CN.MD deleted file mode 100644 index 0d5b1d214..000000000 --- a/contrib/drivers/README.zh_CN.MD +++ /dev/null @@ -1,126 +0,0 @@ -[English](README.MD) | 简体中文 - -# 数据库驱动程序 - -用于gdb包的数据库驱动程序。 - -## 安装 - -以 `mysql` 为例。 - -```shell -go get github.com/gogf/gf/contrib/drivers/mysql/v2@latest -# 方便复制 -go get github.com/gogf/gf/contrib/drivers/clickhouse/v2@latest -go get github.com/gogf/gf/contrib/drivers/dm/v2@latest -go get github.com/gogf/gf/contrib/drivers/mssql/v2@latest -go get github.com/gogf/gf/contrib/drivers/oracle/v2@latest -go get github.com/gogf/gf/contrib/drivers/pgsql/v2@latest -go get github.com/gogf/gf/contrib/drivers/sqlite/v2@latest -go get github.com/gogf/gf/contrib/drivers/sqlitecgo/v2@latest -``` - -选择并将驱动程序导入到您的项目中: - -```go -import _ "github.com/gogf/gf/contrib/drivers/mysql/v2" -``` - -通常在 `main.go` 的顶部导入: - -```go -package main - -import ( - _ "github.com/gogf/gf/contrib/drivers/mysql/v2" - - // 其他导入的包。 -) - -func main() { - // 主要逻辑。 -} -``` - -## 支持的驱动程序 - -### MySQL/MariaDB/TiDB - -```go -import _ "github.com/gogf/gf/contrib/drivers/mysql/v2" -``` - -### SQLite - -```go -import _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" -``` - -#### cgo 版本 - -32位Windows请使用cgo版本 - -```go -import _ "github.com/gogf/gf/contrib/drivers/sqlitecgo/v2" -``` - -### PostgreSQL - -```go -import _ "github.com/gogf/gf/contrib/drivers/pgsql/v2" -``` - -注意: - -- 不支持 `Replace` 功能。 - -### SQL Server - -```go -import _ "github.com/gogf/gf/contrib/drivers/mssql/v2" -``` - -注意: - -- 不支持 `Replace` 功能。 -- 仅支持服务器版本 >= `SQL Server2005` -- 仅支持 datetime2 和 datetimeoffset 类型来自动处理 created_at/updated_at/deleted_at 列,因为 datetime 类型在将列值作为字符串传递时不支持微秒精度。 - -### Oracle - -```go -import _ "github.com/gogf/gf/contrib/drivers/oracle/v2" -``` - -注意: - -- 不支持 `Replace` 功能。 -- 不支持 `LastInsertId`。 - -### ClickHouse - -```go -import _ "github.com/gogf/gf/contrib/drivers/clickhouse/v2" -``` - -注意: - -- 不支持 `InsertIgnore/InsertGetId` 功能。 -- 不支持 `Save/Replace` 功能。 -- 不支持 `Transaction` 功能。 -- 不支持 `RowsAffected` 功能。 - -### DM - -```go -import _ "github.com/gogf/gf/contrib/drivers/dm/v2" -``` - -注意: - -- 不支持 `Replace` 功能。 - -## 自定义驱动程序 - -自定义驱动程序非常快速和简单,您可以参考当前驱动程序的源代码来进行开发。 -如果您有关于支持新驱动程序的PR(Pull Request),我们将非常感激地接受您的提交到当前仓库。 \ No newline at end of file diff --git a/contrib/drivers/dm/dm.go b/contrib/drivers/dm/dm.go index 3bfb01cb9..6b45a51de 100644 --- a/contrib/drivers/dm/dm.go +++ b/contrib/drivers/dm/dm.go @@ -14,6 +14,7 @@ import ( "github.com/gogf/gf/v2/frame/g" ) +// Driver is the driver for dm database. type Driver struct { *gdb.Core } diff --git a/contrib/drivers/dm/dm_do_insert.go b/contrib/drivers/dm/dm_do_insert.go index 1218ab900..538340ffa 100644 --- a/contrib/drivers/dm/dm_do_insert.go +++ b/contrib/drivers/dm/dm_do_insert.go @@ -28,28 +28,70 @@ func (d *Driver) DoInsert( return d.doSave(ctx, link, table, list, option) case gdb.InsertOptionReplace: - // TODO:: Should be Supported - return nil, gerror.NewCode( - gcode.CodeNotSupported, `Replace operation is not supported by dm driver`, - ) - } + // dm does not support REPLACE INTO syntax, use SAVE instead. + return d.doSave(ctx, link, table, list, option) - return d.Core.DoInsert(ctx, link, table, list, option) + case gdb.InsertOptionIgnore: + // dm does not support INSERT IGNORE syntax, use MERGE instead. + return d.doInsertIgnore(ctx, link, table, list, option) + + default: + return d.Core.DoInsert(ctx, link, table, list, option) + } } // doSave support upsert for dm func (d *Driver) doSave(ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, ) (result sql.Result, err error) { - if len(option.OnConflict) == 0 { - return nil, gerror.NewCode( - gcode.CodeMissingParameter, `Please specify conflict columns`, - ) + return d.doMergeInsert(ctx, link, table, list, option, true) +} + +// doInsertIgnore implements INSERT IGNORE operation using MERGE statement for DM database. +// It only inserts records when there's no conflict on primary/unique keys. +func (d *Driver) doInsertIgnore(ctx context.Context, + link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, +) (result sql.Result, err error) { + return d.doMergeInsert(ctx, link, table, list, option, false) +} + +// doMergeInsert implements MERGE-based insert operations for DM database. +// When withUpdate is true, it performs upsert (insert or update). +// When withUpdate is false, it performs insert ignore (insert only when no conflict). +func (d *Driver) doMergeInsert( + ctx context.Context, + link gdb.Link, + table string, + list gdb.List, + option gdb.DoInsertOption, + withUpdate bool, +) (result sql.Result, err error) { + // If OnConflict is not specified, automatically get the primary key of the table + conflictKeys := option.OnConflict + if len(conflictKeys) == 0 { + conflictKeys, err = d.getPrimaryKeys(ctx, table) + if err != nil { + return nil, gerror.WrapCode( + gcode.CodeInternalError, + err, + `failed to get primary keys for table`, + ) + } + if len(conflictKeys) == 0 { + return nil, gerror.NewCode( + gcode.CodeMissingParameter, + `Please specify conflict columns or ensure the table has a primary key`, + ) + } } if len(list) == 0 { - return nil, gerror.NewCode( - gcode.CodeInvalidRequest, `Save operation list is empty by oracle driver`, + opName := "Save" + if !withUpdate { + opName = "InsertIgnore" + } + return nil, gerror.NewCodef( + gcode.CodeInvalidRequest, `%s operation list is empty by dm driver`, opName, ) } @@ -58,14 +100,13 @@ func (d *Driver) doSave(ctx context.Context, oneLen = len(one) charL, charR = d.GetChars() - conflictKeys = option.OnConflict conflictKeySet = gset.New(false) - // queryHolders: Handle data with Holder that need to be upsert - // queryValues: Handle data that need to be upsert + // queryHolders: Handle data with Holder that need to be merged + // queryValues: Handle data that need to be merged // insertKeys: Handle valid keys that need to be inserted // insertValues: Handle values that need to be inserted - // updateValues: Handle values that need to be updated + // updateValues: Handle values that need to be updated (only when withUpdate=true) queryHolders = make([]string, oneLen) queryValues = make([]any, oneLen) insertKeys = make([]string, oneLen) @@ -86,9 +127,9 @@ func (d *Driver) doSave(ctx context.Context, insertKeys[index] = keyWithChar insertValues[index] = fmt.Sprintf("T2.%s", keyWithChar) - // filter conflict keys in updateValues. - // And the key is not a soft created field. - if !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) { + // Build updateValues only when withUpdate is true + // Filter conflict keys and soft created fields from updateValues + if withUpdate && !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) { updateValues = append( updateValues, fmt.Sprintf(`T1.%s = T2.%s`, keyWithChar, keyWithChar), @@ -97,8 +138,10 @@ func (d *Driver) doSave(ctx context.Context, index++ } - batchResult := new(gdb.SqlResult) - sqlStr := parseSqlForUpsert(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys) + var ( + batchResult = new(gdb.SqlResult) + sqlStr = parseSqlForMerge(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys) + ) r, err := d.DoExec(ctx, link, sqlStr, queryValues...) if err != nil { return r, err @@ -112,40 +155,58 @@ func (d *Driver) doSave(ctx context.Context, return batchResult, nil } -// parseSqlForUpsert -// MERGE INTO {{table}} T1 -// USING ( SELECT {{queryHolders}} FROM DUAL T2 -// ON (T1.{{duplicateKey}} = T2.{{duplicateKey}} AND ...) -// WHEN NOT MATCHED THEN -// INSERT {{insertKeys}} VALUES {{insertValues}} -// WHEN MATCHED THEN -// UPDATE SET {{updateValues}} -func parseSqlForUpsert(table string, +// getPrimaryKeys retrieves the primary key field names of the table as a slice of strings. +// This method extracts primary key information from TableFields. +func (d *Driver) getPrimaryKeys(ctx context.Context, table string) ([]string, error) { + tableFields, err := d.TableFields(ctx, table) + if err != nil { + return nil, err + } + + var primaryKeys []string + for _, field := range tableFields { + if field.Key == "PRI" { + primaryKeys = append(primaryKeys, field.Name) + } + } + + return primaryKeys, nil +} + +// parseSqlForMerge generates MERGE statement for DM database. +// When updateValues is empty, it only inserts (INSERT IGNORE behavior). +// When updateValues is provided, it performs upsert (INSERT or UPDATE). +// Examples: +// - INSERT IGNORE: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) +// - UPSERT: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) WHEN MATCHED THEN UPDATE SET ... +func parseSqlForMerge(table string, queryHolders, insertKeys, insertValues, updateValues, duplicateKey []string, ) (sqlStr string) { var ( queryHolderStr = strings.Join(queryHolders, ",") insertKeyStr = strings.Join(insertKeys, ",") insertValueStr = strings.Join(insertValues, ",") - updateValueStr = strings.Join(updateValues, ",") duplicateKeyStr string - pattern = gstr.Trim(`MERGE INTO %s T1 USING (SELECT %s FROM DUAL) T2 ON (%s) WHEN NOT MATCHED THEN INSERT(%s) VALUES (%s) WHEN MATCHED THEN UPDATE SET %s;`) ) + // Build ON condition for index, keys := range duplicateKey { if index != 0 { duplicateKeyStr += " AND " } - duplicateTmp := fmt.Sprintf("T1.%s = T2.%s", keys, keys) - duplicateKeyStr += duplicateTmp + duplicateKeyStr += fmt.Sprintf("T1.%s = T2.%s", keys, keys) } - return fmt.Sprintf(pattern, - table, - queryHolderStr, - duplicateKeyStr, - insertKeyStr, - insertValueStr, - updateValueStr, - ) + // Build SQL based on whether UPDATE is needed + pattern := gstr.Trim(`MERGE INTO %s T1 USING (SELECT %s FROM DUAL) T2 ON (%s) WHEN NOT MATCHED THEN INSERT(%s) VALUES (%s)`) + if len(updateValues) > 0 { + // Upsert: INSERT or UPDATE + pattern += gstr.Trim(`WHEN MATCHED THEN UPDATE SET %s`) + return fmt.Sprintf( + pattern, table, queryHolderStr, duplicateKeyStr, insertKeyStr, insertValueStr, + strings.Join(updateValues, ","), + ) + } + // Insert Ignore: INSERT only + return fmt.Sprintf(pattern, table, queryHolderStr, duplicateKeyStr, insertKeyStr, insertValueStr) } diff --git a/contrib/drivers/dm/dm_table_fields.go b/contrib/drivers/dm/dm_table_fields.go index 9dc3c6c8c..137047366 100644 --- a/contrib/drivers/dm/dm_table_fields.go +++ b/contrib/drivers/dm/dm_table_fields.go @@ -23,7 +23,7 @@ func escapeSingleQuote(s string) string { } const ( - tableFieldsSqlTmp = `SELECT c.COLUMN_NAME, c.DATA_TYPE, c.DATA_DEFAULT, c.NULLABLE, cc.COMMENTS FROM ALL_TAB_COLUMNS c LEFT JOIN ALL_COL_COMMENTS cc ON c.COLUMN_NAME = cc.COLUMN_NAME AND c.TABLE_NAME = cc.TABLE_NAME AND c.OWNER = cc.OWNER WHERE c.TABLE_NAME = '%s' AND c.OWNER = '%s'` + tableFieldsSqlTmp = `SELECT c.COLUMN_NAME, c.DATA_TYPE, c.DATA_LENGTH, c.DATA_DEFAULT, c.NULLABLE, cc.COMMENTS FROM ALL_TAB_COLUMNS c LEFT JOIN ALL_COL_COMMENTS cc ON c.COLUMN_NAME = cc.COLUMN_NAME AND c.TABLE_NAME = cc.TABLE_NAME AND c.OWNER = cc.OWNER WHERE c.TABLE_NAME = '%s' AND c.OWNER = '%s'` tableFieldsPkSqlSchemaTmp = `SELECT COLS.COLUMN_NAME AS PRIMARY_KEY_COLUMN FROM USER_CONSTRAINTS CONS JOIN USER_CONS_COLUMNS COLS ON CONS.CONSTRAINT_NAME = COLS.CONSTRAINT_NAME WHERE CONS.TABLE_NAME = '%s' AND CONS.CONSTRAINT_TYPE = 'P'` tableFieldsPkSqlDBATmp = `SELECT COLS.COLUMN_NAME AS PRIMARY_KEY_COLUMN FROM DBA_CONSTRAINTS CONS JOIN DBA_CONS_COLUMNS COLS ON CONS.CONSTRAINT_NAME = COLS.CONSTRAINT_NAME WHERE CONS.TABLE_NAME = '%s' AND CONS.OWNER = '%s' AND CONS.CONSTRAINT_TYPE = 'P'` ) @@ -85,10 +85,24 @@ func (d *Driver) TableFields( if m["NULLABLE"].String() != "N" { nullable = true } + + // Build field type with length/precision + // For NUMBER(p,s): use DATA_PRECISION and DATA_SCALE + // For VARCHAR2/CHAR: use DATA_LENGTH + var ( + fieldType string + dataType = m["DATA_TYPE"].String() + dataLength = m["DATA_LENGTH"].Int() + ) + if dataLength > 0 { + fieldType = fmt.Sprintf("%s(%d)", dataType, dataLength) + } else { + fieldType = dataType + } fields[m["COLUMN_NAME"].String()] = &gdb.TableField{ Index: i, Name: m["COLUMN_NAME"].String(), - Type: m["DATA_TYPE"].String(), + Type: fieldType, Null: nullable, Default: m["DATA_DEFAULT"].Val(), Key: pkFields.Get(m["COLUMN_NAME"].String()), diff --git a/contrib/drivers/dm/dm_z_unit_basic_test.go b/contrib/drivers/dm/dm_z_unit_basic_test.go index aeb83a70a..da9ab215b 100644 --- a/contrib/drivers/dm/dm_z_unit_basic_test.go +++ b/contrib/drivers/dm/dm_z_unit_basic_test.go @@ -80,12 +80,12 @@ func TestTableFields(t *testing.T) { createInitTable(tables) gtest.C(t, func(t *gtest.T) { var expect = map[string][]any{ - "ID": {"BIGINT", false}, - "ACCOUNT_NAME": {"VARCHAR", false}, - "PWD_RESET": {"TINYINT", false}, - "ATTR_INDEX": {"INT", true}, - "DELETED": {"INT", false}, - "CREATED_TIME": {"TIMESTAMP", false}, + "ID": {"BIGINT(8)", false}, + "ACCOUNT_NAME": {"VARCHAR(128)", false}, + "PWD_RESET": {"TINYINT(1)", false}, + "ATTR_INDEX": {"INT(4)", true}, + "DELETED": {"INT(4)", false}, + "CREATED_TIME": {"TIMESTAMP(8)", false}, } res, err := db.TableFields(ctx, tables) @@ -140,109 +140,6 @@ func Test_DB_Query(t *testing.T) { }) } -func TestModelSave(t *testing.T) { - table := createTable() - defer dropTable(table) - gtest.C(t, func(t *gtest.T) { - type User struct { - Id int - AccountName string - AttrIndex int - } - var ( - user User - count int - result sql.Result - err error - ) - - result, err = db.Model(table).Data(g.Map{ - "id": 1, - "accountName": "ac1", - "attrIndex": 100, - }).OnConflict("id").Save() - - t.AssertNil(err) - n, _ := result.RowsAffected() - t.Assert(n, 1) - - err = db.Model(table).Scan(&user) - t.AssertNil(err) - t.Assert(user.Id, 1) - t.Assert(user.AccountName, "ac1") - t.Assert(user.AttrIndex, 100) - - _, err = db.Model(table).Data(g.Map{ - "id": 1, - "accountName": "ac2", - "attrIndex": 200, - }).OnConflict("id").Save() - t.AssertNil(err) - - err = db.Model(table).Scan(&user) - t.AssertNil(err) - t.Assert(user.AccountName, "ac2") - t.Assert(user.AttrIndex, 200) - - count, err = db.Model(table).Count() - t.AssertNil(err) - t.Assert(count, 1) - }) -} - -func TestModelInsert(t *testing.T) { - // g.Model.insert not lost default not null coloumn - table := "A_tables" - createInitTable(table) - gtest.C(t, func(t *gtest.T) { - i := 200 - data := User{ - ID: int64(i), - AccountName: fmt.Sprintf(`A%dtwo`, i), - PwdReset: 0, - AttrIndex: 99, - CreatedTime: time.Now(), - UpdatedTime: time.Now(), - } - // _, err := db.Schema(TestDBName).Model(table).Data(data).Insert() - _, err := db.Model(table).Insert(&data) - gtest.AssertNil(err) - }) - - gtest.C(t, func(t *gtest.T) { - i := 201 - data := User{ - ID: int64(i), - AccountName: fmt.Sprintf(`A%dtwoONE`, i), - PwdReset: 1, - CreatedTime: time.Now(), - AttrIndex: 98, - UpdatedTime: time.Now(), - } - // _, err := db.Schema(TestDBName).Model(table).Data(data).Insert() - _, err := db.Model(table).Data(&data).Insert() - gtest.AssertNil(err) - }) -} - -func TestDBInsert(t *testing.T) { - table := "A_tables" - createInitTable("A_tables") - gtest.C(t, func(t *gtest.T) { - i := 300 - data := g.Map{ - "ID": i, - "ACCOUNT_NAME": fmt.Sprintf(`A%dthress`, i), - "PWD_RESET": 3, - "ATTR_INDEX": 98, - "CREATED_TIME": gtime.Now(), - "UPDATED_TIME": gtime.Now(), - } - _, err := db.Insert(ctx, table, &data) - gtest.AssertNil(err) - }) -} - func Test_DB_Exec(t *testing.T) { createInitTable("A_tables") gtest.C(t, func(t *gtest.T) { @@ -612,3 +509,124 @@ func Test_Empty_Slice_Argument(t *testing.T) { t.Assert(len(result), 0) }) } + +func TestModelSave(t *testing.T) { + table := createTable() + defer dropTable(table) + gtest.C(t, func(t *gtest.T) { + type User struct { + Id int + AccountName string + AttrIndex int + } + var ( + user User + count int + result sql.Result + err error + ) + + result, err = db.Model(table).Data(g.Map{ + "id": 1, + "accountName": "ac1", + "attrIndex": 100, + }).OnConflict("id").Save() + + t.AssertNil(err) + n, _ := result.RowsAffected() + t.Assert(n, 1) + + err = db.Model(table).Scan(&user) + t.AssertNil(err) + t.Assert(user.Id, 1) + t.Assert(user.AccountName, "ac1") + t.Assert(user.AttrIndex, 100) + + _, err = db.Model(table).Data(g.Map{ + "id": 1, + "accountName": "ac2", + "attrIndex": 200, + }).OnConflict("id").Save() + t.AssertNil(err) + + err = db.Model(table).Scan(&user) + t.AssertNil(err) + t.Assert(user.AccountName, "ac2") + t.Assert(user.AttrIndex, 200) + + count, err = db.Model(table).Count() + t.AssertNil(err) + t.Assert(count, 1) + }) +} + +func TestModelInsert(t *testing.T) { + // g.Model.insert not lost default not null column + table := "A_tables" + createInitTable(table) + gtest.C(t, func(t *gtest.T) { + i := 200 + data := User{ + ID: int64(i), + AccountName: fmt.Sprintf(`A%dtwo`, i), + PwdReset: 0, + AttrIndex: 99, + CreatedTime: time.Now(), + UpdatedTime: time.Now(), + } + // _, err := db.Schema(TestDBName).Model(table).Data(data).Insert() + _, err := db.Model(table).Insert(&data) + gtest.AssertNil(err) + }) + + gtest.C(t, func(t *gtest.T) { + i := 201 + data := User{ + ID: int64(i), + AccountName: fmt.Sprintf(`A%dtwoONE`, i), + PwdReset: 1, + CreatedTime: time.Now(), + AttrIndex: 98, + UpdatedTime: time.Now(), + } + // _, err := db.Schema(TestDBName).Model(table).Data(data).Insert() + _, err := db.Model(table).Data(&data).Insert() + gtest.AssertNil(err) + }) +} + +func Test_Model_InsertIgnore(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + // db.SetDebug(true) + + gtest.C(t, func(t *gtest.T) { + data := User{ + ID: int64(666), + AccountName: fmt.Sprintf(`name_%d`, 666), + PwdReset: 0, + AttrIndex: 99, + CreatedTime: time.Now(), + UpdatedTime: time.Now(), + } + _, err := db.Model(table).Data(data).Insert() + t.AssertNil(err) + }) + gtest.C(t, func(t *gtest.T) { + data := User{ + ID: int64(666), + AccountName: fmt.Sprintf(`name_%d`, 777), + PwdReset: 0, + AttrIndex: 99, + CreatedTime: time.Now(), + UpdatedTime: time.Now(), + } + _, err := db.Model(table).Data(data).InsertIgnore() + t.AssertNil(err) + + one, err := db.Model(table).Where("id", 666).One() + t.AssertNil(err) + t.Assert(one["ACCOUNT_NAME"].String(), "name_666") + }) +} diff --git a/contrib/drivers/dm/dm_z_unit_feature_soft_time_test.go b/contrib/drivers/dm/dm_z_unit_feature_soft_time_test.go new file mode 100644 index 000000000..d66630f7f --- /dev/null +++ b/contrib/drivers/dm/dm_z_unit_feature_soft_time_test.go @@ -0,0 +1,1400 @@ +// 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_test + +import ( + "fmt" + "testing" + "time" + + "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" +) + +// CreateAt/UpdateAt/DeleteAt. +func Test_SoftTime_CreateUpdateDelete1(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id INT NOT NULL, + name VARCHAR(45) DEFAULT NULL, + create_at TIMESTAMP(6) DEFAULT NULL, + update_at TIMESTAMP(6) DEFAULT NULL, + delete_at TIMESTAMP(6) DEFAULT NULL, + PRIMARY KEY (id) +); + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert + dataInsert := g.Map{ + "id": 1, + "name": "name_1", + } + r, err := db.Model(table).Data(dataInsert).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + oneInsert, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneInsert["ID"].Int(), 1) + t.Assert(oneInsert["NAME"].String(), "name_1") + t.Assert(oneInsert["DELETE_AT"].String(), "") + t.AssertGE(oneInsert["CREATE_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + t.AssertGE(oneInsert["UPDATE_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + + // For time asserting purpose. + time.Sleep(2 * time.Second) + + // Save + dataSave := g.Map{ + "id": 1, + "name": "name_10", + } + r, err = db.Model(table).Data(dataSave).Save() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneSave, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneSave["ID"].Int(), 1) + t.Assert(oneSave["NAME"].String(), "name_10") + t.Assert(oneSave["DELETE_AT"].String(), "") + t.Assert(oneSave["CREATE_AT"].GTime().Timestamp(), oneInsert["CREATE_AT"].GTime().Timestamp()) + t.AssertNE(oneSave["UPDATE_AT"].GTime().Timestamp(), oneInsert["UPDATE_AT"].GTime().Timestamp()) + t.AssertGE(oneSave["UPDATE_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + + // For time asserting purpose. + time.Sleep(2 * time.Second) + + // Update + dataUpdate := g.Map{ + "name": "name_1000", + } + r, err = db.Model(table).Data(dataUpdate).WherePri(1).Update() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneUpdate, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneUpdate["ID"].Int(), 1) + t.Assert(oneUpdate["NAME"].String(), "name_1000") + t.Assert(oneUpdate["DELETE_AT"].String(), "") + t.Assert(oneUpdate["CREATE_AT"].GTime().Timestamp(), oneInsert["CREATE_AT"].GTime().Timestamp()) + t.AssertGE(oneUpdate["UPDATE_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + + // Replace + dataReplace := g.Map{ + "id": 1, + "name": "name_100", + } + r, err = db.Model(table).Data(dataReplace).Replace() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneReplace, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneReplace["ID"].Int(), 1) + t.Assert(oneReplace["NAME"].String(), "name_100") + t.Assert(oneReplace["DELETE_AT"].String(), "") + t.AssertGE(oneReplace["CREATE_AT"].GTime().Timestamp(), oneInsert["CREATE_AT"].GTime().Timestamp()) + t.AssertGE(oneReplace["UPDATE_AT"].GTime().Timestamp(), oneInsert["UPDATE_AT"].GTime().Timestamp()) + + // For time asserting purpose. + time.Sleep(2 * time.Second) + + // Delete + r, err = db.Model(table).Delete("id", 1) + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + // Delete Select + one4, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one4), 0) + one5, err := db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(one5["ID"].Int(), 1) + t.AssertGE(one5["DELETE_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + // Delete Count + i, err := db.Model(table).Count() + t.AssertNil(err) + t.Assert(i, 0) + i, err = db.Model(table).Unscoped().Count() + t.AssertNil(err) + t.Assert(i, 1) + + // Delete Unscoped + r, err = db.Model(table).Unscoped().Delete("id", 1) + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + one6, err := db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one6), 0) + i, err = db.Model(table).Unscoped().Count() + t.AssertNil(err) + t.Assert(i, 0) + }) +} + +// CreateAt/UpdateAt/DeleteAt. +func Test_SoftTime_CreateUpdateDelete2(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id INT NOT NULL, + name VARCHAR(45) DEFAULT NULL, + create_at TIMESTAMP(0) DEFAULT NULL, + update_at TIMESTAMP(0) DEFAULT NULL, + delete_at TIMESTAMP(0) DEFAULT NULL, + PRIMARY KEY (id) +); + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert + dataInsert := g.Map{ + "id": 1, + "name": "name_1", + } + r, err := db.Model(table).Data(dataInsert).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + oneInsert, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneInsert["ID"].Int(), 1) + t.Assert(oneInsert["NAME"].String(), "name_1") + t.Assert(oneInsert["DELETE_AT"].String(), "") + t.AssertGE(oneInsert["CREATE_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + t.AssertGE(oneInsert["UPDATE_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + + // For time asserting purpose. + time.Sleep(2 * time.Second) + + // Save + dataSave := g.Map{ + "id": 1, + "name": "name_10", + } + r, err = db.Model(table).Data(dataSave).Save() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneSave, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneSave["ID"].Int(), 1) + t.Assert(oneSave["NAME"].String(), "name_10") + t.Assert(oneSave["DELETE_AT"].String(), "") + t.Assert(oneSave["CREATE_AT"].GTime().Timestamp(), oneInsert["CREATE_AT"].GTime().Timestamp()) + t.AssertNE(oneSave["UPDATE_AT"].GTime().Timestamp(), oneInsert["UPDATE_AT"].GTime().Timestamp()) + t.AssertGE(oneSave["UPDATE_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + + // For time asserting purpose. + time.Sleep(2 * time.Second) + + // Update + dataUpdate := g.Map{ + "name": "name_1000", + } + r, err = db.Model(table).Data(dataUpdate).WherePri(1).Update() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneUpdate, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneUpdate["ID"].Int(), 1) + t.Assert(oneUpdate["NAME"].String(), "name_1000") + t.Assert(oneUpdate["DELETE_AT"].String(), "") + t.Assert(oneUpdate["CREATE_AT"].GTime().Timestamp(), oneInsert["CREATE_AT"].GTime().Timestamp()) + t.AssertGE(oneUpdate["UPDATE_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + + // Replace + dataReplace := g.Map{ + "id": 1, + "name": "name_100", + } + r, err = db.Model(table).Data(dataReplace).Replace() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneReplace, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneReplace["ID"].Int(), 1) + t.Assert(oneReplace["NAME"].String(), "name_100") + t.Assert(oneReplace["DELETE_AT"].String(), "") + t.AssertGE(oneReplace["CREATE_AT"].GTime().Timestamp(), oneInsert["CREATE_AT"].GTime().Timestamp()) + t.AssertGE(oneReplace["UPDATE_AT"].GTime().Timestamp(), oneInsert["UPDATE_AT"].GTime().Timestamp()) + + // For time asserting purpose. + time.Sleep(2 * time.Second) + + // Delete + r, err = db.Model(table).Delete("id", 1) + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + // Delete Select + one4, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one4), 0) + one5, err := db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(one5["ID"].Int(), 1) + t.AssertGE(one5["DELETE_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + // Delete Count + i, err := db.Model(table).Count() + t.AssertNil(err) + t.Assert(i, 0) + i, err = db.Model(table).Unscoped().Count() + t.AssertNil(err) + t.Assert(i, 1) + + // Delete Unscoped + r, err = db.Model(table).Unscoped().Delete("id", 1) + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + one6, err := db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one6), 0) + i, err = db.Model(table).Unscoped().Count() + t.AssertNil(err) + t.Assert(i, 0) + }) +} + +// CreatedAt/UpdatedAt/DeletedAt. +func Test_SoftTime_CreatedUpdatedDeleted_Map(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id INT NOT NULL, + name VARCHAR(45) DEFAULT NULL, + created_at TIMESTAMP(6) DEFAULT NULL, + updated_at TIMESTAMP(6) DEFAULT NULL, + deleted_at TIMESTAMP(6) DEFAULT NULL, + PRIMARY KEY (id) +); + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert + dataInsert := g.Map{ + "id": 1, + "name": "name_1", + } + r, err := db.Model(table).Data(dataInsert).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + oneInsert, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneInsert["ID"].Int(), 1) + t.Assert(oneInsert["NAME"].String(), "name_1") + t.Assert(oneInsert["DELETED_AT"].String(), "") + t.AssertGE(oneInsert["CREATED_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + t.AssertGE(oneInsert["UPDATED_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + + // For time asserting purpose. + time.Sleep(2 * time.Second) + + // Save + dataSave := g.Map{ + "id": 1, + "name": "name_10", + } + r, err = db.Model(table).Data(dataSave).Save() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneSave, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneSave["ID"].Int(), 1) + t.Assert(oneSave["NAME"].String(), "name_10") + t.Assert(oneSave["DELETED_AT"].String(), "") + t.Assert(oneSave["CREATED_AT"].GTime().Timestamp(), oneInsert["CREATED_AT"].GTime().Timestamp()) + t.AssertNE(oneSave["UPDATED_AT"].GTime().Timestamp(), oneInsert["UPDATED_AT"].GTime().Timestamp()) + t.AssertGE(oneSave["UPDATED_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + + // For time asserting purpose. + time.Sleep(2 * time.Second) + + // Update + dataUpdate := g.Map{ + "name": "name_1000", + } + r, err = db.Model(table).Data(dataUpdate).WherePri(1).Update() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneUpdate, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneUpdate["ID"].Int(), 1) + t.Assert(oneUpdate["NAME"].String(), "name_1000") + t.Assert(oneUpdate["DELETED_AT"].String(), "") + t.Assert(oneUpdate["CREATED_AT"].GTime().Timestamp(), oneInsert["CREATED_AT"].GTime().Timestamp()) + t.AssertGE(oneUpdate["UPDATED_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + + // Replace + dataReplace := g.Map{ + "id": 1, + "name": "name_100", + } + r, err = db.Model(table).Data(dataReplace).Replace() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneReplace, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneReplace["ID"].Int(), 1) + t.Assert(oneReplace["NAME"].String(), "name_100") + t.Assert(oneReplace["DELETED_AT"].String(), "") + t.AssertGE(oneReplace["CREATED_AT"].GTime().Timestamp(), oneInsert["CREATED_AT"].GTime().Timestamp()) + t.AssertGE(oneReplace["UPDATED_AT"].GTime().Timestamp(), oneInsert["UPDATED_AT"].GTime().Timestamp()) + + // For time asserting purpose. + time.Sleep(2 * time.Second) + + // Delete + r, err = db.Model(table).Delete("id", 1) + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + // Delete Select + one4, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one4), 0) + one5, err := db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(one5["ID"].Int(), 1) + t.AssertGE(one5["DELETED_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + // Delete Count + i, err := db.Model(table).Count() + t.AssertNil(err) + t.Assert(i, 0) + i, err = db.Model(table).Unscoped().Count() + t.AssertNil(err) + t.Assert(i, 1) + + // Delete Unscoped + r, err = db.Model(table).Unscoped().Delete("id", 1) + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + one6, err := db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one6), 0) + i, err = db.Model(table).Unscoped().Count() + t.AssertNil(err) + t.Assert(i, 0) + }) +} + +// CreatedAt/UpdatedAt/DeletedAt. +func Test_SoftTime_CreatedUpdatedDeleted_Struct(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id INT NOT NULL, + name VARCHAR(45) DEFAULT NULL, + created_at TIMESTAMP(6) DEFAULT NULL, + updated_at TIMESTAMP(6) DEFAULT NULL, + deleted_at TIMESTAMP(6) DEFAULT NULL, + PRIMARY KEY (id) +); + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + type User struct { + Id int + Name string + CreatedAT *gtime.Time + UpdatedAT *gtime.Time + DeletedAT *gtime.Time + } + gtest.C(t, func(t *gtest.T) { + // Insert + dataInsert := User{ + Id: 1, + Name: "name_1", + } + r, err := db.Model(table).Data(dataInsert).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + oneInsert, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneInsert["ID"].Int(), 1) + t.Assert(oneInsert["NAME"].String(), "name_1") + t.Assert(oneInsert["DELETED_AT"].String(), "") + t.AssertGE(oneInsert["CREATED_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + t.AssertGE(oneInsert["UPDATED_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + + // For time asserting purpose. + time.Sleep(2 * time.Second) + + // Save + dataSave := User{ + Id: 1, + Name: "name_10", + } + r, err = db.Model(table).Data(dataSave).OmitEmpty().Save() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneSave, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneSave["ID"].Int(), 1) + t.Assert(oneSave["NAME"].String(), "name_10") + t.Assert(oneSave["DELETED_AT"].String(), "") + t.Assert(oneSave["CREATED_AT"].GTime().Timestamp(), oneInsert["CREATED_AT"].GTime().Timestamp()) + t.AssertNE(oneSave["UPDATED_AT"].GTime().Timestamp(), oneInsert["UPDATED_AT"].GTime().Timestamp()) + t.AssertGE(oneSave["UPDATED_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + + // For time asserting purpose. + time.Sleep(2 * time.Second) + + // Update + dataUpdate := User{ + Name: "name_1000", + } + r, err = db.Model(table).Data(dataUpdate).OmitEmpty().WherePri(1).Update() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneUpdate, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneUpdate["ID"].Int(), 1) + t.Assert(oneUpdate["NAME"].String(), "name_1000") + t.Assert(oneUpdate["DELETED_AT"].String(), "") + t.Assert(oneUpdate["CREATED_AT"].GTime().Timestamp(), oneInsert["CREATED_AT"].GTime().Timestamp()) + t.AssertGE(oneUpdate["UPDATED_AT"].GTime().Timestamp(), gtime.Timestamp()-4) + + // Replace + dataReplace := User{ + Id: 1, + Name: "name_100", + } + r, err = db.Model(table).Data(dataReplace).OmitEmpty().Replace() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneReplace, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneReplace["ID"].Int(), 1) + t.Assert(oneReplace["NAME"].String(), "name_100") + t.Assert(oneReplace["DELETED_AT"].String(), "") + t.AssertGE(oneReplace["CREATED_AT"].GTime().Timestamp(), oneInsert["CREATED_AT"].GTime().Timestamp()) + t.AssertGE(oneReplace["UPDATED_AT"].GTime().Timestamp(), oneInsert["UPDATED_AT"].GTime().Timestamp()) + + // For time asserting purpose. + time.Sleep(2 * time.Second) + + // Delete + r, err = db.Model(table).Delete("id", 1) + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + // Delete Select + one4, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one4), 0) + one5, err := db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(one5["ID"].Int(), 1) + t.AssertGE(one5["DELETED_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + // Delete Count + i, err := db.Model(table).Count() + t.AssertNil(err) + t.Assert(i, 0) + i, err = db.Model(table).Unscoped().Count() + t.AssertNil(err) + t.Assert(i, 1) + + // Delete Unscoped + r, err = db.Model(table).Unscoped().Delete("id", 1) + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + one6, err := db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one6), 0) + i, err = db.Model(table).Unscoped().Count() + t.AssertNil(err) + t.Assert(i, 0) + }) +} + +func Test_SoftUpdateTime(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id INT NOT NULL, + num INT DEFAULT NULL, + create_at TIMESTAMP(6) DEFAULT NULL, + update_at TIMESTAMP(6) DEFAULT NULL, + delete_at TIMESTAMP(6) DEFAULT NULL, + PRIMARY KEY (id) +); + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert + dataInsert := g.Map{ + "id": 1, + "num": 10, + } + r, err := db.Model(table).Data(dataInsert).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + oneInsert, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneInsert["ID"].Int(), 1) + t.Assert(oneInsert["NUM"].Int(), 10) + + // Update. + r, err = db.Model(table).Data("num=num+1").Where("id=?", 1).Update() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + }) +} + +func Test_SoftUpdateTime_WithDO(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id INT NOT NULL, + num INT DEFAULT NULL, + created_at TIMESTAMP(6) DEFAULT NULL, + updated_at TIMESTAMP(6) DEFAULT NULL, + deleted_at TIMESTAMP(6) DEFAULT NULL, + PRIMARY KEY (id) +); + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert + dataInsert := g.Map{ + "id": 1, + "num": 10, + } + r, err := db.Model(table).Data(dataInsert).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + oneInserted, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneInserted["ID"].Int(), 1) + t.Assert(oneInserted["NUM"].Int(), 10) + + // Update. + time.Sleep(2 * time.Second) + type User struct { + g.Meta `orm:"do:true"` + Id any + Num any + CreatedAt any + UpdatedAt any + DeletedAt any + } + r, err = db.Model(table).Data(User{ + Num: 100, + }).Where("id=?", 1).Update() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneUpdated, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneUpdated["NUM"].Int(), 100) + t.Assert(oneUpdated["CREATED_AT"].String(), oneInserted["CREATED_AT"].String()) + t.AssertNE(oneUpdated["UPDATED_AT"].String(), oneInserted["UPDATED_AT"].String()) + }) +} + +func Test_SoftDelete(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id INT NOT NULL, + name VARCHAR(45) DEFAULT NULL, + create_at TIMESTAMP(6) DEFAULT NULL, + update_at TIMESTAMP(6) DEFAULT NULL, + delete_at TIMESTAMP(6) DEFAULT NULL, + PRIMARY KEY (id) +); + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + // db.SetDebug(true) + gtest.C(t, func(t *gtest.T) { + for i := 1; i <= 10; i++ { + data := g.Map{ + "id": i, + "name": fmt.Sprintf("name_%d", i), + } + r, err := db.Model(table).Data(data).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + } + }) + gtest.C(t, func(t *gtest.T) { + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.AssertNE(one["CREATE_AT"].String(), "") + t.AssertNE(one["UPDATE_AT"].String(), "") + t.Assert(one["DELETE_AT"].String(), "") + }) + gtest.C(t, func(t *gtest.T) { + one, err := db.Model(table).WherePri(10).One() + t.AssertNil(err) + t.AssertNE(one["CREATE_AT"].String(), "") + t.AssertNE(one["UPDATE_AT"].String(), "") + t.Assert(one["DELETE_AT"].String(), "") + }) + gtest.C(t, func(t *gtest.T) { + ids := g.SliceInt{1, 3, 5} + r, err := db.Model(table).Where("id", ids).Delete() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 3) + + count, err := db.Model(table).Where("id", ids).Count() + t.AssertNil(err) + t.Assert(count, 0) + + all, err := db.Model(table).Unscoped().Where("id", ids).All() + t.AssertNil(err) + t.Assert(len(all), 3) + t.AssertNE(all[0]["CREATE_AT"].String(), "") + t.AssertNE(all[0]["UPDATE_AT"].String(), "") + t.AssertNE(all[0]["DELETE_AT"].String(), "") + t.AssertNE(all[1]["CREATE_AT"].String(), "") + t.AssertNE(all[1]["UPDATE_AT"].String(), "") + t.AssertNE(all[1]["DELETE_AT"].String(), "") + t.AssertNE(all[2]["CREATE_AT"].String(), "") + t.AssertNE(all[2]["UPDATE_AT"].String(), "") + t.AssertNE(all[2]["DELETE_AT"].String(), "") + }) +} + +func Test_SoftDelete_Join(t *testing.T) { + table1 := "time_test_table1" + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id INT NOT NULL, + name VARCHAR(45) DEFAULT NULL, + create_at TIMESTAMP(6) DEFAULT NULL, + update_at TIMESTAMP(6) DEFAULT NULL, + delete_at TIMESTAMP(6) DEFAULT NULL, + PRIMARY KEY (id) +); + `, table1)); err != nil { + gtest.Error(err) + } + defer dropTable(table1) + + table2 := "time_test_table2" + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id INT NOT NULL, + name VARCHAR(45) DEFAULT NULL, + createat TIMESTAMP(6) DEFAULT NULL, + updateat TIMESTAMP(6) DEFAULT NULL, + deleteat TIMESTAMP(6) DEFAULT NULL, + PRIMARY KEY (id) +); + `, 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.Model(table1).Data(dataInsert1).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + dataInsert2 := g.Map{ + "id": 1, + "name": "name_2", + } + r, err = db.Model(table2).Data(dataInsert2).Insert() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table1, "t1").LeftJoin(table2, "t2", "t2.id=t1.id").Fields("t1.name").One() + t.AssertNil(err) + t.Assert(one["NAME"], "name_1") + + // Soft deleting. + r, err = db.Model(table1).Where(1).Delete() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + one, err = db.Model(table1, "t1").LeftJoin(table2, "t2", "t2.id=t1.id").Fields("t1.name").One() + t.AssertNil(err) + t.Assert(one.IsEmpty(), true) + + one, err = db.Model(table2, "t2").LeftJoin(table1, "t1", "t2.id=t1.id").Fields("t2.name").One() + t.AssertNil(err) + t.Assert(one.IsEmpty(), true) + }) +} + +func Test_SoftDelete_WhereAndOr(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id INT NOT NULL, + name VARCHAR(45) DEFAULT NULL, + create_at TIMESTAMP(6) DEFAULT NULL, + update_at TIMESTAMP(6) DEFAULT NULL, + delete_at TIMESTAMP(6) DEFAULT NULL, + PRIMARY KEY (id) +); + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + // db.SetDebug(true) + // Add datas. + gtest.C(t, func(t *gtest.T) { + for i := 1; i <= 10; i++ { + data := g.Map{ + "id": i, + "name": fmt.Sprintf("name_%d", i), + } + r, err := db.Model(table).Data(data).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + } + }) + gtest.C(t, func(t *gtest.T) { + ids := g.SliceInt{1, 3, 5} + r, err := db.Model(table).Where("id", ids).Delete() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 3) + + count, err := db.Model(table).Where("id", 1).WhereOr("id", 3).Count() + t.AssertNil(err) + t.Assert(count, 0) + }) +} + +func Test_CreateUpdateTime_Struct(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id INT NOT NULL, + name VARCHAR(45) DEFAULT NULL, + create_at TIMESTAMP(6) DEFAULT NULL, + update_at TIMESTAMP(6) DEFAULT NULL, + delete_at TIMESTAMP(6) DEFAULT NULL, + PRIMARY KEY (id) +); + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + // db.SetDebug(true) + // defer db.SetDebug(false) + + type Entity struct { + Id uint64 `orm:"id,primary" json:"id"` + Name string `orm:"name" json:"name"` + CreateAt *gtime.Time `orm:"create_at" json:"create_at"` + UpdateAt *gtime.Time `orm:"update_at" json:"update_at"` + DeleteAt *gtime.Time `orm:"delete_at" json:"delete_at"` + } + gtest.C(t, func(t *gtest.T) { + // Insert + dataInsert := &Entity{ + Id: 1, + Name: "name_1", + CreateAt: nil, + UpdateAt: nil, + DeleteAt: nil, + } + r, err := db.Model(table).Data(dataInsert).OmitEmpty().Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + oneInsert, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneInsert["ID"].Int(), 1) + t.Assert(oneInsert["NAME"].String(), "name_1") + t.Assert(oneInsert["DELETE_AT"].String(), "") + t.AssertGE(oneInsert["CREATE_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + t.AssertGE(oneInsert["UPDATE_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + + time.Sleep(2 * time.Second) + + // Save + dataSave := &Entity{ + Id: 1, + Name: "name_10", + CreateAt: nil, + UpdateAt: nil, + DeleteAt: nil, + } + r, err = db.Model(table).Data(dataSave).OmitEmpty().Save() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneSave, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneSave["ID"].Int(), 1) + t.Assert(oneSave["NAME"].String(), "name_10") + t.Assert(oneSave["DELETE_AT"].String(), "") + t.Assert(oneSave["CREATE_AT"].GTime().Timestamp(), oneInsert["CREATE_AT"].GTime().Timestamp()) + t.AssertNE(oneSave["UPDATE_AT"].GTime().Timestamp(), oneInsert["UPDATE_AT"].GTime().Timestamp()) + t.AssertGE(oneSave["UPDATE_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + + time.Sleep(2 * time.Second) + + // Update + dataUpdate := &Entity{ + Id: 1, + Name: "name_1000", + CreateAt: nil, + UpdateAt: nil, + DeleteAt: nil, + } + r, err = db.Model(table).Data(dataUpdate).WherePri(1).OmitEmpty().Update() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneUpdate, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneUpdate["ID"].Int(), 1) + t.Assert(oneUpdate["NAME"].String(), "name_1000") + t.Assert(oneUpdate["DELETE_AT"].String(), "") + t.Assert(oneUpdate["CREATE_AT"].GTime().Timestamp(), oneInsert["CREATE_AT"].GTime().Timestamp()) + t.AssertGE(oneUpdate["UPDATE_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + + // Replace + dataReplace := &Entity{ + Id: 1, + Name: "name_100", + CreateAt: nil, + UpdateAt: nil, + DeleteAt: nil, + } + r, err = db.Model(table).Data(dataReplace).OmitEmpty().Replace() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneReplace, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneReplace["ID"].Int(), 1) + t.Assert(oneReplace["NAME"].String(), "name_100") + t.Assert(oneReplace["DELETE_AT"].String(), "") + t.AssertGE(oneReplace["CREATE_AT"].GTime().Timestamp(), oneInsert["CREATE_AT"].GTime().Timestamp()) + t.AssertGE(oneReplace["UPDATE_AT"].GTime().Timestamp(), oneInsert["UPDATE_AT"].GTime().Timestamp()) + + time.Sleep(2 * time.Second) + + // Delete + r, err = db.Model(table).Delete("id", 1) + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + // Delete Select + one4, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one4), 0) + one5, err := db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(one5["ID"].Int(), 1) + t.AssertGE(one5["DELETE_AT"].GTime().Timestamp(), gtime.Timestamp()-2) + // Delete Count + i, err := db.Model(table).Count() + t.AssertNil(err) + t.Assert(i, 0) + i, err = db.Model(table).Unscoped().Count() + t.AssertNil(err) + t.Assert(i, 1) + + // Delete Unscoped + r, err = db.Model(table).Unscoped().Delete("id", 1) + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + one6, err := db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one6), 0) + i, err = db.Model(table).Unscoped().Count() + t.AssertNil(err) + t.Assert(i, 0) + }) +} + +func Test_SoftTime_CreateUpdateDelete_UnixTimestamp(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id INT NOT NULL, + name VARCHAR(45) DEFAULT NULL, + create_at INT DEFAULT NULL, + update_at INT DEFAULT NULL, + delete_at INT DEFAULT NULL, + PRIMARY KEY (id) +); + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + // insert + gtest.C(t, func(t *gtest.T) { + dataInsert := g.Map{ + "id": 1, + "name": "name_1", + } + r, err := db.Model(table).Data(dataInsert).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(one["NAME"].String(), "name_1") + t.AssertGT(one["CREATE_AT"].Int64(), 0) + t.AssertGT(one["UPDATE_AT"].Int64(), 0) + t.Assert(one["DELETE_AT"].Int64(), 0) + t.Assert(len(one["CREATE_AT"].String()), 10) + t.Assert(len(one["UPDATE_AT"].String()), 10) + }) + + // sleep some seconds to make update time greater than create time. + time.Sleep(2 * time.Second) + + // update + gtest.C(t, func(t *gtest.T) { + // update: map + dataInsert := g.Map{ + "name": "name_11", + } + r, err := db.Model(table).Data(dataInsert).WherePri(1).Update() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(one["NAME"].String(), "name_11") + t.AssertGT(one["CREATE_AT"].Int64(), 0) + t.AssertGT(one["UPDATE_AT"].Int64(), 0) + t.Assert(one["DELETE_AT"].Int64(), 0) + t.Assert(len(one["CREATE_AT"].String()), 10) + t.Assert(len(one["UPDATE_AT"].String()), 10) + + var ( + lastCreateTime = one["CREATE_AT"].Int64() + lastUpdateTime = one["UPDATE_AT"].Int64() + ) + + time.Sleep(2 * time.Second) + + // update: string + r, err = db.Model(table).Data("name='name_111'").WherePri(1).Update() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + one, err = db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(one["NAME"].String(), "name_111") + t.Assert(one["CREATE_AT"].Int64(), lastCreateTime) + t.AssertGT(one["UPDATE_AT"].Int64(), lastUpdateTime) + t.Assert(one["DELETE_AT"].Int64(), 0) + }) + + // delete + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table).WherePri(1).Delete() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one), 0) + + one, err = db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(one["NAME"].String(), "name_111") + t.AssertGT(one["CREATE_AT"].Int64(), 0) + t.AssertGT(one["UPDATE_AT"].Int64(), 0) + t.AssertGT(one["DELETE_AT"].Int64(), 0) + }) +} + +func Test_SoftTime_CreateUpdateDelete_Bool_Deleted(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + // do not use BIT(1) but use BIT in dm database as bool type. + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id INT NOT NULL, + name VARCHAR(45) DEFAULT NULL, + create_at INT DEFAULT NULL, + update_at INT DEFAULT NULL, + delete_at BIT DEFAULT NULL, + PRIMARY KEY (id) +); + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + // db.SetDebug(true) + // insert + gtest.C(t, func(t *gtest.T) { + dataInsert := g.Map{ + "id": 1, + "name": "name_1", + } + r, err := db.Model(table).Data(dataInsert).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(one["NAME"].String(), "name_1") + t.AssertGT(one["CREATE_AT"].Int64(), 0) + t.AssertGT(one["UPDATE_AT"].Int64(), 0) + t.Assert(one["DELETE_AT"].Int64(), 0) + t.Assert(len(one["CREATE_AT"].String()), 10) + t.Assert(len(one["UPDATE_AT"].String()), 10) + }) + + // delete + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table).WherePri(1).Delete() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one), 0) + + one, err = db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(one["NAME"].String(), "name_1") + t.AssertGT(one["CREATE_AT"].Int64(), 0) + t.AssertGT(one["UPDATE_AT"].Int64(), 0) + t.Assert(one["DELETE_AT"].Int64(), 1) + }) +} + +func Test_SoftTime_CreateUpdateDelete_Option_SoftTimeTypeTimestampMilli(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id INT NOT NULL, + name VARCHAR(45) DEFAULT NULL, + create_at BIGINT DEFAULT NULL, + update_at BIGINT DEFAULT NULL, + delete_at BIT DEFAULT NULL, + PRIMARY KEY (id) +); + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + var softTimeOption = gdb.SoftTimeOption{ + SoftTimeType: gdb.SoftTimeTypeTimestampMilli, + } + + // insert + gtest.C(t, func(t *gtest.T) { + dataInsert := g.Map{ + "id": 1, + "name": "name_1", + } + r, err := db.Model(table).SoftTime(softTimeOption).Data(dataInsert).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).One() + t.AssertNil(err) + t.Assert(one["NAME"].String(), "name_1") + t.Assert(len(one["CREATE_AT"].String()), 13) + t.Assert(len(one["UPDATE_AT"].String()), 13) + t.Assert(one["DELETE_AT"].Int64(), 0) + }) + + // delete + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).Delete() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one), 0) + + one, err = db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(one["NAME"].String(), "name_1") + t.AssertGT(one["CREATE_AT"].Int64(), 0) + t.AssertGT(one["UPDATE_AT"].Int64(), 0) + t.Assert(one["DELETE_AT"].Int64(), 1) + }) +} + +func Test_SoftTime_CreateUpdateDelete_Option_SoftTimeTypeTimestampNano(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id INT NOT NULL, + name VARCHAR(45) DEFAULT NULL, + create_at BIGINT DEFAULT NULL, + update_at BIGINT DEFAULT NULL, + delete_at BIT DEFAULT NULL, + PRIMARY KEY (id) +); + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + var softTimeOption = gdb.SoftTimeOption{ + SoftTimeType: gdb.SoftTimeTypeTimestampNano, + } + + // insert + gtest.C(t, func(t *gtest.T) { + dataInsert := g.Map{ + "id": 1, + "name": "name_1", + } + r, err := db.Model(table).SoftTime(softTimeOption).Data(dataInsert).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).One() + t.AssertNil(err) + t.Assert(one["NAME"].String(), "name_1") + t.Assert(len(one["CREATE_AT"].String()), 19) + t.Assert(len(one["UPDATE_AT"].String()), 19) + t.Assert(one["DELETE_AT"].Int64(), 0) + }) + + // delete + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).Delete() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one), 0) + + one, err = db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(one["NAME"].String(), "name_1") + t.AssertGT(one["CREATE_AT"].Int64(), 0) + t.AssertGT(one["UPDATE_AT"].Int64(), 0) + t.Assert(one["DELETE_AT"].Int64(), 1) + }) +} + +func Test_SoftTime_CreateUpdateDelete_Specified(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id INT NOT NULL, + name VARCHAR(45) DEFAULT NULL, + create_at TIMESTAMP(0) DEFAULT NULL, + update_at TIMESTAMP(0) DEFAULT NULL, + delete_at TIMESTAMP(0) DEFAULT NULL, + PRIMARY KEY (id) +); + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert + dataInsert := g.Map{ + "id": 1, + "name": "name_1", + "create_at": gtime.NewFromStr("2024-05-30 20:00:00"), + "update_at": gtime.NewFromStr("2024-05-30 20:00:00"), + } + r, err := db.Model(table).Data(dataInsert).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + oneInsert, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneInsert["ID"].Int(), 1) + t.Assert(oneInsert["NAME"].String(), "name_1") + t.Assert(oneInsert["DELETE_AT"].String(), "") + t.Assert(oneInsert["CREATE_AT"].String(), "2024-05-30 20:00:00") + t.Assert(oneInsert["UPDATE_AT"].String(), "2024-05-30 20:00:00") + + // For time asserting purpose. + time.Sleep(2 * time.Second) + + // Save + dataSave := g.Map{ + "id": 1, + "name": "name_10", + "update_at": gtime.NewFromStr("2024-05-30 20:15:00"), + } + r, err = db.Model(table).Data(dataSave).Save() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneSave, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneSave["ID"].Int(), 1) + t.Assert(oneSave["NAME"].String(), "name_10") + t.Assert(oneSave["DELETE_AT"].String(), "") + t.Assert(oneSave["CREATE_AT"].String(), "2024-05-30 20:00:00") + t.Assert(oneSave["UPDATE_AT"].String(), "2024-05-30 20:15:00") + + // For time asserting purpose. + time.Sleep(2 * time.Second) + + // Update + dataUpdate := g.Map{ + "name": "name_1000", + "update_at": gtime.NewFromStr("2024-05-30 20:30:00"), + } + r, err = db.Model(table).Data(dataUpdate).WherePri(1).Update() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneUpdate, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneUpdate["ID"].Int(), 1) + t.Assert(oneUpdate["NAME"].String(), "name_1000") + t.Assert(oneUpdate["DELETE_AT"].String(), "") + t.Assert(oneUpdate["CREATE_AT"].String(), "2024-05-30 20:00:00") + t.Assert(oneUpdate["UPDATE_AT"].String(), "2024-05-30 20:30:00") + + // Replace + dataReplace := g.Map{ + "id": 1, + "name": "name_100", + "create_at": gtime.NewFromStr("2024-05-30 21:00:00"), + "update_at": gtime.NewFromStr("2024-05-30 21:00:00"), + } + r, err = db.Model(table).Data(dataReplace).Replace() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + oneReplace, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneReplace["ID"].Int(), 1) + t.Assert(oneReplace["NAME"].String(), "name_100") + t.Assert(oneReplace["DELETE_AT"].String(), "") + t.AssertGE(oneReplace["CREATE_AT"].GTime().Timestamp(), oneInsert["CREATE_AT"].GTime().Timestamp()) + t.AssertGE(oneReplace["UPDATE_AT"].GTime().Timestamp(), oneInsert["UPDATE_AT"].GTime().Timestamp()) + + // For time asserting purpose. + time.Sleep(2 * time.Second) + + // Insert with delete_at + dataInsertDelete := g.Map{ + "id": 2, + "name": "name_2", + "create_at": gtime.NewFromStr("2024-05-30 20:00:00"), + "update_at": gtime.NewFromStr("2024-05-30 20:00:00"), + "delete_at": gtime.NewFromStr("2024-05-30 20:00:00"), + } + r, err = db.Model(table).Data(dataInsertDelete).Insert() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + // Delete Select + oneDelete, err := db.Model(table).WherePri(2).One() + t.AssertNil(err) + t.Assert(len(oneDelete), 0) + oneDeleteUnscoped, err := db.Model(table).Unscoped().WherePri(2).One() + t.AssertNil(err) + t.Assert(oneDeleteUnscoped["ID"].Int(), 2) + t.Assert(oneDeleteUnscoped["NAME"].String(), "name_2") + t.Assert(oneDeleteUnscoped["DELETE_AT"].String(), "2024-05-30 20:00:00") + t.Assert(oneDeleteUnscoped["CREATE_AT"].String(), "2024-05-30 20:00:00") + t.Assert(oneDeleteUnscoped["UPDATE_AT"].String(), "2024-05-30 20:00:00") + }) +} diff --git a/contrib/drivers/dm/dm_z_unit_init_test.go b/contrib/drivers/dm/dm_z_unit_init_test.go index 94d056716..100329a70 100644 --- a/contrib/drivers/dm/dm_z_unit_init_test.go +++ b/contrib/drivers/dm/dm_z_unit_init_test.go @@ -63,8 +63,8 @@ func init() { Weight: 1, MaxIdleConnCount: 10, MaxOpenConnCount: 10, - CreatedAt: "created_time", - UpdatedAt: "updated_time", + // CreatedAt: "created_time", + // UpdatedAt: "updated_time", } nodeLink := gdb.ConfigNode{ diff --git a/database/gdb/gdb_core.go b/database/gdb/gdb_core.go index d34c58ed3..56d10bd35 100644 --- a/database/gdb/gdb_core.go +++ b/database/gdb/gdb_core.go @@ -446,8 +446,10 @@ func (c *Core) DoInsert(ctx context.Context, link Link, table string, list List, // Group the list by fields. Different fields to different list. // It here uses ListMap to keep sequence for data inserting. // ============================================================================================ - var keyListMap = gmap.NewListMap() - var tmpkeyListMap = make(map[string]List) + var ( + keyListMap = gmap.NewListMap() + tmpKeyListMap = make(map[string]List) + ) for _, item := range list { mapLen := len(item) if mapLen == 0 { @@ -463,13 +465,13 @@ func (c *Core) DoInsert(ctx context.Context, link Link, table string, list List, keys = tmpKeys // for fieldsToSequence tmpKeysInSequenceStr := gstr.Join(tmpKeys, ",") - if tmpkeyListMapItem, ok := tmpkeyListMap[tmpKeysInSequenceStr]; ok { - tmpkeyListMap[tmpKeysInSequenceStr] = append(tmpkeyListMapItem, item) + if tmpKeyListMapItem, ok := tmpKeyListMap[tmpKeysInSequenceStr]; ok { + tmpKeyListMap[tmpKeysInSequenceStr] = append(tmpKeyListMapItem, item) } else { - tmpkeyListMap[tmpKeysInSequenceStr] = List{item} + tmpKeyListMap[tmpKeysInSequenceStr] = List{item} } } - for tmpKeysInSequenceStr, itemList := range tmpkeyListMap { + for tmpKeysInSequenceStr, itemList := range tmpKeyListMap { keyListMap.Set(tmpKeysInSequenceStr, itemList) } if keyListMap.Size() > 1 { diff --git a/database/gdb/gdb_model_soft_time.go b/database/gdb/gdb_model_soft_time.go index 3972bf647..1ddcdcb12 100644 --- a/database/gdb/gdb_model_soft_time.go +++ b/database/gdb/gdb_model_soft_time.go @@ -380,6 +380,7 @@ func (m *softTimeMaintainer) GetValueByFieldTypeForCreateOrUpdate( ctx context.Context, fieldType LocalType, isDeletedField bool, ) any { var value any + // for create or update procedure, the deleted field is always set to non-deleted value. if isDeletedField { switch fieldType { case LocalTypeDate, LocalTypeTime, LocalTypeDatetime: