diff --git a/contrib/drivers/mariadb/mariadb_z_unit_feature_duplicate_test.go b/contrib/drivers/mariadb/mariadb_z_unit_feature_duplicate_test.go new file mode 100644 index 000000000..79b444d5b --- /dev/null +++ b/contrib/drivers/mariadb/mariadb_z_unit_feature_duplicate_test.go @@ -0,0 +1,321 @@ +// 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 mariadb_test + +import ( + "context" + "fmt" + "testing" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/test/gtest" +) + +func createDuplicateTable(table ...string) string { + var name string + if len(table) > 0 { + name = table[0] + } else { + name = fmt.Sprintf(`duplicate_table_%d`, gtime.TimestampNano()) + } + dropTable(name) + if _, err := db.Exec(ctx, fmt.Sprintf(` + CREATE TABLE %s ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + email varchar(100) NOT NULL, + username varchar(45) NULL, + score int(10) unsigned DEFAULT 0, + login_count int(10) unsigned DEFAULT 0, + PRIMARY KEY (id), + UNIQUE KEY uk_email (email) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `, name)); err != nil { + gtest.Fatal(err) + } + return name +} + +func Test_OnDuplicateKeyUpdate_Basic(t *testing.T) { + table := createDuplicateTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // First insert + _, err := db.Exec(ctx, fmt.Sprintf( + "INSERT INTO %s (email, username, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score)", + table, + ), "user1@example.com", "user1", 100) + t.AssertNil(err) + + one, err := db.Model(table).Where("email", "user1@example.com").One() + t.AssertNil(err) + t.Assert(one["username"], "user1") + t.Assert(one["score"], 100) + + // Duplicate insert - should update + _, err = db.Exec(ctx, fmt.Sprintf( + "INSERT INTO %s (email, username, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score)", + table, + ), "user1@example.com", "user1_updated", 200) + t.AssertNil(err) + + one, err = db.Model(table).Where("email", "user1@example.com").One() + t.AssertNil(err) + t.Assert(one["username"], "user1_updated") + t.Assert(one["score"], 200) + + // Verify only one record exists + count, err := db.Model(table).Count() + t.AssertNil(err) + t.Assert(count, 1) + }) +} + +func Test_OnDuplicateKeyUpdate_Increment(t *testing.T) { + table := createDuplicateTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // First insert + _, err := db.Exec(ctx, fmt.Sprintf( + "INSERT INTO %s (email, username, login_count) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE login_count = login_count + 1", + table, + ), "user1@example.com", "user1", 1) + t.AssertNil(err) + + one, err := db.Model(table).Where("email", "user1@example.com").One() + t.AssertNil(err) + t.Assert(one["login_count"], 1) + + // Duplicate - increment login_count + _, err = db.Exec(ctx, fmt.Sprintf( + "INSERT INTO %s (email, username, login_count) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE login_count = login_count + 1", + table, + ), "user1@example.com", "user1", 1) + t.AssertNil(err) + + one, err = db.Model(table).Where("email", "user1@example.com").One() + t.AssertNil(err) + t.Assert(one["login_count"], 2) + + // Third time + _, err = db.Exec(ctx, fmt.Sprintf( + "INSERT INTO %s (email, username, login_count) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE login_count = login_count + 1", + table, + ), "user1@example.com", "user1", 1) + t.AssertNil(err) + + one, err = db.Model(table).Where("email", "user1@example.com").One() + t.AssertNil(err) + t.Assert(one["login_count"], 3) + }) +} + +func Test_OnDuplicateKeyUpdate_MultipleColumns(t *testing.T) { + table := createDuplicateTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // First insert + _, err := db.Exec(ctx, fmt.Sprintf( + "INSERT INTO %s (email, username, score, login_count) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score), login_count = login_count + 1", + table, + ), "user1@example.com", "user1", 100, 1) + t.AssertNil(err) + + one, err := db.Model(table).Where("email", "user1@example.com").One() + t.AssertNil(err) + t.Assert(one["username"], "user1") + t.Assert(one["score"], 100) + t.Assert(one["login_count"], 1) + + // Duplicate - update multiple columns + _, err = db.Exec(ctx, fmt.Sprintf( + "INSERT INTO %s (email, username, score, login_count) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score), login_count = login_count + 1", + table, + ), "user1@example.com", "user1_v2", 200, 1) + t.AssertNil(err) + + one, err = db.Model(table).Where("email", "user1@example.com").One() + t.AssertNil(err) + t.Assert(one["username"], "user1_v2") + t.Assert(one["score"], 200) + t.Assert(one["login_count"], 2) + }) +} + +func Test_OnDuplicateKeyUpdate_Batch(t *testing.T) { + table := createDuplicateTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert multiple records + _, err := db.Exec(ctx, fmt.Sprintf( + "INSERT INTO %s (email, username, score) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score)", + table, + ), "user1@example.com", "user1", 100, + "user2@example.com", "user2", 200, + "user3@example.com", "user3", 300) + t.AssertNil(err) + + count, err := db.Model(table).Count() + t.AssertNil(err) + t.Assert(count, 3) + + // Update with duplicate - should update specific records + _, err = db.Exec(ctx, fmt.Sprintf( + "INSERT INTO %s (email, username, score) VALUES (?, ?, ?), (?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score)", + table, + ), "user1@example.com", "user1_updated", 150, + "user2@example.com", "user2_updated", 250) + t.AssertNil(err) + + // Still 3 records + count, err = db.Model(table).Count() + t.AssertNil(err) + t.Assert(count, 3) + + // Verify updates + one, err := db.Model(table).Where("email", "user1@example.com").One() + t.AssertNil(err) + t.Assert(one["username"], "user1_updated") + t.Assert(one["score"], 150) + + one, err = db.Model(table).Where("email", "user2@example.com").One() + t.AssertNil(err) + t.Assert(one["username"], "user2_updated") + t.Assert(one["score"], 250) + + // user3 unchanged + one, err = db.Model(table).Where("email", "user3@example.com").One() + t.AssertNil(err) + t.Assert(one["username"], "user3") + t.Assert(one["score"], 300) + }) +} + +func Test_OnDuplicateKeyUpdate_ConditionalUpdate(t *testing.T) { + table := createDuplicateTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // First insert + _, err := db.Exec(ctx, fmt.Sprintf( + "INSERT INTO %s (email, username, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE score = IF(VALUES(score) > score, VALUES(score), score)", + table, + ), "user1@example.com", "user1", 100) + t.AssertNil(err) + + one, err := db.Model(table).Where("email", "user1@example.com").One() + t.AssertNil(err) + t.Assert(one["score"], 100) + + // Try to update with lower score - should not update + _, err = db.Exec(ctx, fmt.Sprintf( + "INSERT INTO %s (email, username, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE score = IF(VALUES(score) > score, VALUES(score), score)", + table, + ), "user1@example.com", "user1", 50) + t.AssertNil(err) + + one, err = db.Model(table).Where("email", "user1@example.com").One() + t.AssertNil(err) + t.Assert(one["score"], 100) // Still 100 + + // Update with higher score - should update + _, err = db.Exec(ctx, fmt.Sprintf( + "INSERT INTO %s (email, username, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE score = IF(VALUES(score) > score, VALUES(score), score)", + table, + ), "user1@example.com", "user1", 150) + t.AssertNil(err) + + one, err = db.Model(table).Where("email", "user1@example.com").One() + t.AssertNil(err) + t.Assert(one["score"], 150) // Updated to 150 + }) +} + +func Test_OnDuplicateKeyUpdate_WithTransaction(t *testing.T) { + table := createDuplicateTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Transaction with ON DUPLICATE KEY UPDATE + err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + // First insert + _, err := tx.Exec(fmt.Sprintf( + "INSERT INTO %s (email, username, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score)", + table, + ), "user1@example.com", "user1", 100) + if err != nil { + return err + } + + // Duplicate in same transaction + _, err = tx.Exec(fmt.Sprintf( + "INSERT INTO %s (email, username, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score)", + table, + ), "user1@example.com", "user1_updated", 200) + return err + }) + t.AssertNil(err) + + // Verify final state + one, err := db.Model(table).Where("email", "user1@example.com").One() + t.AssertNil(err) + t.Assert(one["username"], "user1_updated") + t.Assert(one["score"], 200) + + count, err := db.Model(table).Count() + t.AssertNil(err) + t.Assert(count, 1) + }) +} + +func Test_OnDuplicateKeyUpdate_MixedInsertUpdate(t *testing.T) { + table := createDuplicateTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // First batch insert + _, err := db.Exec(ctx, fmt.Sprintf( + "INSERT INTO %s (email, username, score) VALUES (?, ?, ?), (?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score)", + table, + ), "user1@example.com", "user1", 100, + "user2@example.com", "user2", 200) + t.AssertNil(err) + + count, err := db.Model(table).Count() + t.AssertNil(err) + t.Assert(count, 2) + + // Mixed batch: one duplicate, one new + _, err = db.Exec(ctx, fmt.Sprintf( + "INSERT INTO %s (email, username, score) VALUES (?, ?, ?), (?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score)", + table, + ), "user1@example.com", "user1_updated", 150, + "user3@example.com", "user3", 300) + t.AssertNil(err) + + // Should have 3 records now + count, err = db.Model(table).Count() + t.AssertNil(err) + t.Assert(count, 3) + + // Verify user1 was updated + one, err := db.Model(table).Where("email", "user1@example.com").One() + t.AssertNil(err) + t.Assert(one["username"], "user1_updated") + t.Assert(one["score"], 150) + + // Verify user3 was inserted + one, err = db.Model(table).Where("email", "user3@example.com").One() + t.AssertNil(err) + t.Assert(one["username"], "user3") + t.Assert(one["score"], 300) + }) +} diff --git a/contrib/drivers/mariadb/mariadb_z_unit_feature_json_test.go b/contrib/drivers/mariadb/mariadb_z_unit_feature_json_test.go new file mode 100644 index 000000000..e39ce260b --- /dev/null +++ b/contrib/drivers/mariadb/mariadb_z_unit_feature_json_test.go @@ -0,0 +1,394 @@ +// 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 mariadb_test + +import ( + "context" + "fmt" + "testing" + + "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" +) + +func createJSONTable(table ...string) string { + var name string + if len(table) > 0 { + name = table[0] + } else { + name = fmt.Sprintf(`json_table_%d`, gtime.TimestampNano()) + } + dropTable(name) + if _, err := db.Exec(ctx, fmt.Sprintf(` + CREATE TABLE %s ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + name varchar(45) NULL, + config json NULL, + metadata json NULL, + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `, name)); err != nil { + gtest.Fatal(err) + } + return name +} + +func Test_JSON_Insert_Map(t *testing.T) { + table := createJSONTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + data := g.Map{ + "name": "user1", + "config": g.Map{ + "theme": "dark", + "lang": "zh-CN", + }, + "metadata": g.Map{ + "tags": g.Slice{"admin", "developer"}, + "level": 5, + }, + } + result, err := db.Model(table).Data(data).Insert() + t.AssertNil(err) + n, _ := result.LastInsertId() + t.Assert(n, 1) + + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(one["name"], "user1") + t.AssertNE(one["config"], nil) + t.AssertNE(one["metadata"], nil) + }) +} + +func Test_JSON_Insert_String(t *testing.T) { + table := createJSONTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + data := g.Map{ + "name": "user2", + "config": `{"theme":"light","lang":"en-US"}`, + "metadata": `{"tags":["user"],"level":1}`, + } + result, err := db.Model(table).Data(data).Insert() + t.AssertNil(err) + n, _ := result.LastInsertId() + t.Assert(n, 1) + + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(one["name"], "user2") + t.AssertNE(one["config"], nil) + t.AssertNE(one["metadata"], nil) + }) +} + +func Test_JSON_Insert_Null(t *testing.T) { + table := createJSONTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + data := g.Map{ + "name": "user3", + "config": nil, + "metadata": nil, + } + result, err := db.Model(table).Data(data).Insert() + t.AssertNil(err) + n, _ := result.LastInsertId() + t.Assert(n, 1) + + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(one["name"], "user3") + t.Assert(one["config"], nil) + t.Assert(one["metadata"], nil) + }) +} + +func Test_JSON_Update(t *testing.T) { + table := createJSONTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert initial data + _, err := db.Model(table).Data(g.Map{ + "name": "user1", + "config": g.Map{ + "theme": "dark", + }, + }).Insert() + t.AssertNil(err) + + // Update JSON column + result, err := db.Model(table).Data(g.Map{ + "config": g.Map{ + "theme": "light", + "lang": "en-US", + }, + }).WherePri(1).Update() + t.AssertNil(err) + n, _ := result.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.AssertNE(one["config"], nil) + }) +} + +func Test_JSON_Extract_Where(t *testing.T) { + table := createJSONTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert test data + data := g.Slice{ + g.Map{ + "name": "user1", + "config": g.Map{ + "theme": "dark", + "lang": "zh-CN", + }, + }, + g.Map{ + "name": "user2", + "config": g.Map{ + "theme": "light", + "lang": "en-US", + }, + }, + g.Map{ + "name": "user3", + "config": g.Map{ + "theme": "dark", + "lang": "en-US", + }, + }, + } + _, err := db.Model(table).Data(data).Insert() + t.AssertNil(err) + + // Query by JSON field using JSON_EXTRACT + all, err := db.Model(table).Where("JSON_EXTRACT(config, '$.theme') = ?", "dark").All() + t.AssertNil(err) + t.Assert(len(all), 2) + + all, err = db.Model(table).Where("JSON_EXTRACT(config, '$.lang') = ?", "en-US").All() + t.AssertNil(err) + t.Assert(len(all), 2) + }) +} + +func Test_JSON_Extract_Select(t *testing.T) { + table := createJSONTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert test data + _, err := db.Model(table).Data(g.Map{ + "name": "user1", + "config": g.Map{ + "theme": "dark", + "lang": "zh-CN", + }, + "metadata": g.Map{ + "level": 5, + }, + }).Insert() + t.AssertNil(err) + + // Select with JSON_EXTRACT + one, err := db.Model(table).Fields("name, JSON_EXTRACT(config, '$.theme') as theme, JSON_EXTRACT(metadata, '$.level') as level").WherePri(1).One() + t.AssertNil(err) + t.Assert(one["name"], "user1") + t.AssertNE(one["theme"], nil) + t.AssertNE(one["level"], nil) + }) +} + +func Test_JSON_Array_Query(t *testing.T) { + table := createJSONTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert data with JSON array + data := g.Slice{ + g.Map{ + "name": "user1", + "metadata": g.Map{ + "tags": g.Slice{"admin", "developer"}, + }, + }, + g.Map{ + "name": "user2", + "metadata": g.Map{ + "tags": g.Slice{"user"}, + }, + }, + g.Map{ + "name": "user3", + "metadata": g.Map{ + "tags": g.Slice{"admin", "user"}, + }, + }, + } + _, err := db.Model(table).Data(data).Insert() + t.AssertNil(err) + + // Query by JSON array contains + all, err := db.Model(table).Where("JSON_CONTAINS(metadata, ?, '$.tags')", `"admin"`).All() + t.AssertNil(err) + t.Assert(len(all), 2) + }) +} + +func Test_JSON_Batch_Insert(t *testing.T) { + table := createJSONTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + data := g.Slice{ + g.Map{ + "name": "user1", + "config": g.Map{ + "theme": "dark", + }, + }, + g.Map{ + "name": "user2", + "config": g.Map{ + "theme": "light", + }, + }, + g.Map{ + "name": "user3", + "config": nil, + }, + } + result, err := db.Model(table).Data(data).Insert() + t.AssertNil(err) + n, _ := result.RowsAffected() + t.Assert(n, 3) + + all, err := db.Model(table).All() + t.AssertNil(err) + t.Assert(len(all), 3) + }) +} + +func Test_JSON_Scan_To_Struct(t *testing.T) { + table := createJSONTable() + defer dropTable(table) + + type Config struct { + Theme string `json:"theme"` + Lang string `json:"lang"` + } + type User struct { + Id int + Name string + Config *Config + } + + gtest.C(t, func(t *gtest.T) { + // Insert data + _, err := db.Model(table).Data(g.Map{ + "name": "user1", + "config": g.Map{ + "theme": "dark", + "lang": "zh-CN", + }, + }).Insert() + t.AssertNil(err) + + // Scan to struct + var user User + err = db.Model(table).WherePri(1).Scan(&user) + t.AssertNil(err) + t.Assert(user.Name, "user1") + t.AssertNE(user.Config, nil) + if user.Config != nil { + t.Assert(user.Config.Theme, "dark") + t.Assert(user.Config.Lang, "zh-CN") + } + }) +} + +func Test_JSON_Complex_Structure(t *testing.T) { + table := createJSONTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert complex nested JSON + data := g.Map{ + "name": "user1", + "config": g.Map{ + "ui": g.Map{ + "theme": "dark", + "fontSize": g.Map{ + "base": 14, + "code": 12, + }, + }, + "editor": g.Map{ + "tabSize": 4, + "wordWrap": true, + }, + }, + } + result, err := db.Model(table).Data(data).Insert() + t.AssertNil(err) + n, _ := result.LastInsertId() + t.Assert(n, 1) + + // Query nested JSON path + one, err := db.Model(table).Fields("JSON_EXTRACT(config, '$.ui.theme') as theme, JSON_EXTRACT(config, '$.ui.fontSize.base') as base_font").WherePri(1).One() + t.AssertNil(err) + t.AssertNE(one["theme"], nil) + t.AssertNE(one["base_font"], nil) + }) +} + +func Test_JSON_Transaction(t *testing.T) { + table := createJSONTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + // Insert in transaction + _, err := tx.Model(table).Ctx(ctx).Data(g.Map{ + "name": "user1", + "config": g.Map{ + "theme": "dark", + }, + }).Insert() + if err != nil { + return err + } + + // Update in transaction + _, err = tx.Model(table).Ctx(ctx).Data(g.Map{ + "config": g.Map{ + "theme": "light", + }, + }).WherePri(1).Update() + return err + }) + t.AssertNil(err) + + // Verify data + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(one["name"], "user1") + t.AssertNE(one["config"], nil) + }) +} diff --git a/contrib/drivers/mariadb/mariadb_z_unit_feature_lock_test.go b/contrib/drivers/mariadb/mariadb_z_unit_feature_lock_test.go new file mode 100644 index 000000000..f9109bcc1 --- /dev/null +++ b/contrib/drivers/mariadb/mariadb_z_unit_feature_lock_test.go @@ -0,0 +1,228 @@ +// 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 mariadb_test + +import ( + "context" + "testing" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/test/gtest" +) + +// Test_Model_Lock tests the Lock method with custom lock clause +func Test_Model_Lock(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Test basic Lock with FOR UPDATE + one, err := db.Model(table).Lock("FOR UPDATE").Where("id", 1).One() + t.AssertNil(err) + t.Assert(one["id"], 1) + + // Test Lock with legacy LOCK IN SHARE MODE (MariaDB/MySQL compatible) + one, err = db.Model(table).Lock("LOCK IN SHARE MODE").Where("id", 3).One() + t.AssertNil(err) + t.Assert(one["id"], 3) + + // Test Lock with predefined constants + one, err = db.Model(table).Lock(gdb.LockForUpdate).Where("id", 4).One() + t.AssertNil(err) + t.Assert(one["id"], 4) + }) +} + +// Test_Model_LockUpdate tests the LockUpdate convenience method +func Test_Model_LockUpdate(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Test LockUpdate is equivalent to Lock("FOR UPDATE") + one, err := db.Model(table).LockUpdate().Where("id", 1).One() + t.AssertNil(err) + t.Assert(one["id"], 1) + t.Assert(one["passport"], "user_1") + + // Test LockUpdate with All() + all, err := db.Model(table).LockUpdate().Where("id", 4).Order("id").All() + t.AssertNil(err) + t.Assert(len(all), 3) + t.Assert(all[0]["id"], 1) + t.Assert(all[2]["id"], 3) + + // Test LockUpdate with Count() + count, err := db.Model(table).LockUpdate().Where("id>?", 5).Count() + t.AssertNil(err) + t.Assert(count, 5) + }) +} + +// Test_Model_LockUpdateSkipLocked tests the LockUpdateSkipLocked convenience method +// Note: SKIP LOCKED requires MariaDB 10.6+, skipped for compatibility +// func Test_Model_LockUpdateSkipLocked(t *testing.T) { +// table := createInitTable() +// defer dropTable(table) +// +// gtest.C(t, func(t *gtest.T) { +// // Test LockUpdateSkipLocked basic usage +// one, err := db.Model(table).LockUpdateSkipLocked().Where("id", 1).One() +// t.AssertNil(err) +// t.Assert(one["id"], 1) +// +// // Test LockUpdateSkipLocked with All() +// all, err := db.Model(table).LockUpdateSkipLocked().Where("id>?", 7).Order("id").All() +// t.AssertNil(err) +// t.Assert(len(all), 3) +// }) +// } + +// Test_Model_LockShared tests the LockShared convenience method +func Test_Model_LockShared(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Test LockShared is equivalent to Lock("LOCK IN SHARE MODE") + one, err := db.Model(table).LockShared().Where("id", 1).One() + t.AssertNil(err) + t.Assert(one["id"], 1) + + // Test LockShared with All() + all, err := db.Model(table).LockShared().Where("id<=?", 5).Order("id").All() + t.AssertNil(err) + t.Assert(len(all), 5) + t.Assert(all[0]["id"], 1) + t.Assert(all[4]["id"], 5) + }) +} + +// Test_Model_Lock_WithTransaction tests Lock methods within transaction +func Test_Model_Lock_WithTransaction(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + // Lock row for update in transaction + one, err := tx.Model(table).LockUpdate().Where("id", 1).One() + t.AssertNil(err) + t.Assert(one["id"], 1) + + // Update the locked row + _, err = tx.Model(table).Data(g.Map{"nickname": "updated_name"}).Where("id", 1).Update() + t.AssertNil(err) + + // Verify update + updated, err := tx.Model(table).Where("id", 1).One() + t.AssertNil(err) + t.Assert(updated["nickname"], "updated_name") + + return nil + }) + t.AssertNil(err) + + // Verify transaction committed successfully + one, err := db.Model(table).Where("id", 1).One() + t.AssertNil(err) + t.Assert(one["nickname"], "updated_name") + }) +} + +// Test_Model_Lock_ReleaseAfterCommit tests lock is released after transaction commit +func Test_Model_Lock_ReleaseAfterCommit(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Start transaction and lock a row + tx, err := db.Begin(ctx) + t.AssertNil(err) + + one, err := tx.Model(table).LockUpdate().Where("id", 1).One() + t.AssertNil(err) + t.Assert(one["id"], 1) + + // Update within transaction + _, err = tx.Model(table).Data(g.Map{"nickname": "tx_update"}).Where("id", 1).Update() + t.AssertNil(err) + + // Commit transaction - this should release the lock + err = tx.Commit() + t.AssertNil(err) + + // Another query should succeed without blocking + one, err = db.Model(table).Where("id", 1).One() + t.AssertNil(err) + t.Assert(one["nickname"], "tx_update") + }) +} + +// Test_Model_Lock_ReleaseAfterRollback tests lock is released after transaction rollback +func Test_Model_Lock_ReleaseAfterRollback(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Start transaction and lock a row + tx, err := db.Begin(ctx) + t.AssertNil(err) + + one, err := tx.Model(table).LockUpdate().Where("id", 1).One() + t.AssertNil(err) + t.Assert(one["id"], 1) + + // Update within transaction + _, err = tx.Model(table).Data(g.Map{"nickname": "rollback_update"}).Where("id", 1).Update() + t.AssertNil(err) + + // Rollback transaction - this should release the lock and discard changes + err = tx.Rollback() + t.AssertNil(err) + + // Verify original value is preserved + one, err = db.Model(table).Where("id", 1).One() + t.AssertNil(err) + t.Assert(one["nickname"], "name_1") + }) +} + +// Test_Model_Lock_ChainedMethods tests Lock with other chained methods +func Test_Model_Lock_ChainedMethods(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Lock with Fields + one, err := db.Model(table).Fields("id,passport").LockUpdate().Where("id", 1).One() + t.AssertNil(err) + t.Assert(len(one), 2) + t.Assert(one["id"], 1) + t.Assert(one["passport"], "user_1") + + // Lock with Order and Limit + all, err := db.Model(table).LockShared().Where("id>?", 5).Order("id desc").Limit(3).All() + t.AssertNil(err) + t.Assert(len(all), 3) + t.Assert(all[0]["id"], 10) + t.Assert(all[2]["id"], 8) + + // Lock with Group and Having + all, err = db.Model(table).Fields("LEFT(passport,4) as prefix, COUNT(*) as cnt"). + LockUpdate(). + Group("prefix"). + Having("cnt>?", 0). + Order("prefix"). + All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["prefix"], "user") + t.Assert(all[0]["cnt"], 10) + }) +} diff --git a/contrib/drivers/mariadb/mariadb_z_unit_feature_master_slave_test.go b/contrib/drivers/mariadb/mariadb_z_unit_feature_master_slave_test.go new file mode 100644 index 000000000..08f41559c --- /dev/null +++ b/contrib/drivers/mariadb/mariadb_z_unit_feature_master_slave_test.go @@ -0,0 +1,324 @@ +// 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 mariadb_test + +import ( + "context" + "fmt" + "sync" + "testing" + + "github.com/gogf/gf/v2/container/garray" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/guid" +) + +func Test_Master_Slave(t *testing.T) { + var err error + + gtest.C(t, func(t *gtest.T) { + _, err = db.Exec(ctx, "CREATE DATABASE IF NOT EXISTS `master` CHARACTER SET UTF8") + t.AssertNil(err) + _, err = db.Exec(ctx, "CREATE DATABASE IF NOT EXISTS `slave` CHARACTER SET UTF8") + t.AssertNil(err) + }) + defer func() { + _, _ = db.Exec(ctx, "DROP DATABASE `master`") + _, _ = db.Exec(ctx, "DROP DATABASE `slave`") + }() + var ( + configKey = guid.S() + configGroup = gdb.ConfigGroup{ + gdb.ConfigNode{ + Host: "127.0.0.1", + Port: "3307", + User: "root", + Pass: "12345678", + Name: "master", + Type: "mariadb", + Role: "master", + Debug: true, + Weight: 100, + }, + gdb.ConfigNode{ + Host: "127.0.0.1", + Port: "3307", + User: "root", + Pass: "12345678", + Name: "slave", + Type: "mariadb", + Role: "slave", + Debug: true, + Weight: 100, + }, + } + ) + gdb.SetConfigGroup(configKey, configGroup) + masterSlaveDB := g.DB(configKey) + gtest.C(t, func(t *gtest.T) { + table := "table_" + guid.S() + createTableWithDb(masterSlaveDB.Schema("master"), table) + createTableWithDb(masterSlaveDB.Schema("slave"), table) + defer dropTableWithDb(masterSlaveDB.Schema("master"), table) + defer dropTableWithDb(masterSlaveDB.Schema("slave"), table) + + // Data insert to master. + array := garray.New(true) + for i := 1; i <= TableSize; i++ { + array.Append(g.Map{ + "id": i, + "passport": fmt.Sprintf(`user_%d`, i), + "password": fmt.Sprintf(`pass_%d`, i), + "nickname": fmt.Sprintf(`name_%d`, i), + "create_time": gtime.NewFromStr(CreateTime).String(), + }) + } + _, err = masterSlaveDB.Model(table).Data(array).Insert() + t.AssertNil(err) + + var count int + // Auto slave. + count, err = masterSlaveDB.Model(table).Count() + t.AssertNil(err) + t.Assert(count, int64(0)) + + // slave. + count, err = masterSlaveDB.Model(table).Slave().Count() + t.AssertNil(err) + t.Assert(count, int64(0)) + + // master. + count, err = masterSlaveDB.Model(table).Master().Count() + t.AssertNil(err) + t.Assert(count, int64(TableSize)) + }) +} + +// Test_Master_Slave_Concurrent_ReadWrite tests concurrent read/write routing +func Test_Master_Slave_Concurrent_ReadWrite(t *testing.T) { + var err error + + gtest.C(t, func(t *gtest.T) { + _, err = db.Exec(ctx, "CREATE DATABASE IF NOT EXISTS `master` CHARACTER SET UTF8") + t.AssertNil(err) + _, err = db.Exec(ctx, "CREATE DATABASE IF NOT EXISTS `slave` CHARACTER SET UTF8") + t.AssertNil(err) + }) + defer func() { + _, _ = db.Exec(ctx, "DROP DATABASE `master`") + _, _ = db.Exec(ctx, "DROP DATABASE `slave`") + }() + + var ( + configKey = guid.S() + configGroup = gdb.ConfigGroup{ + gdb.ConfigNode{ + Host: "127.0.0.1", + Port: "3307", + User: "root", + Pass: "12345678", + Name: "master", + Type: "mariadb", + Role: "master", + Weight: 100, + }, + gdb.ConfigNode{ + Host: "127.0.0.1", + Port: "3307", + User: "root", + Pass: "12345678", + Name: "slave", + Type: "mariadb", + Role: "slave", + Weight: 100, + }, + } + ) + gdb.SetConfigGroup(configKey, configGroup) + masterSlaveDB := g.DB(configKey) + + gtest.C(t, func(t *gtest.T) { + table := "table_" + guid.S() + createTableWithDb(masterSlaveDB.Schema("master"), table) + createTableWithDb(masterSlaveDB.Schema("slave"), table) + defer dropTableWithDb(masterSlaveDB.Schema("master"), table) + defer dropTableWithDb(masterSlaveDB.Schema("slave"), table) + + var wg sync.WaitGroup + concurrency := 10 + + // Concurrent writes to master + wg.Add(concurrency) + for i := 0; i < concurrency; i++ { + go func(id int) { + defer wg.Done() + _, err := masterSlaveDB.Model(table).Insert(g.Map{ + "passport": fmt.Sprintf("concurrent_%d", id), + "password": fmt.Sprintf("pass_%d", id), + "nickname": fmt.Sprintf("name_%d", id), + }) + t.AssertNil(err) + }(i) + } + wg.Wait() + + // Verify writes went to master + count, err := masterSlaveDB.Model(table).Master().Count() + t.AssertNil(err) + t.Assert(count, concurrency) + }) +} + +// Test_Master_Slave_Transaction_Routing tests transaction routing to master +func Test_Master_Slave_Transaction_Routing(t *testing.T) { + var err error + + gtest.C(t, func(t *gtest.T) { + _, err = db.Exec(ctx, "CREATE DATABASE IF NOT EXISTS `master` CHARACTER SET UTF8") + t.AssertNil(err) + _, err = db.Exec(ctx, "CREATE DATABASE IF NOT EXISTS `slave` CHARACTER SET UTF8") + t.AssertNil(err) + }) + defer func() { + _, _ = db.Exec(ctx, "DROP DATABASE `master`") + _, _ = db.Exec(ctx, "DROP DATABASE `slave`") + }() + + var ( + configKey = guid.S() + configGroup = gdb.ConfigGroup{ + gdb.ConfigNode{ + Host: "127.0.0.1", + Port: "3307", + User: "root", + Pass: "12345678", + Name: "master", + Type: "mariadb", + Role: "master", + Weight: 100, + }, + gdb.ConfigNode{ + Host: "127.0.0.1", + Port: "3307", + User: "root", + Pass: "12345678", + Name: "slave", + Type: "mariadb", + Role: "slave", + Weight: 100, + }, + } + ) + gdb.SetConfigGroup(configKey, configGroup) + masterSlaveDB := g.DB(configKey) + + gtest.C(t, func(t *gtest.T) { + table := "table_" + guid.S() + createTableWithDb(masterSlaveDB.Schema("master"), table) + createTableWithDb(masterSlaveDB.Schema("slave"), table) + defer dropTableWithDb(masterSlaveDB.Schema("master"), table) + defer dropTableWithDb(masterSlaveDB.Schema("slave"), table) + + // Transaction should route to master + err := masterSlaveDB.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + _, err := tx.Model(table).Insert(g.Map{ + "passport": "tx_user", + "password": "tx_pass", + "nickname": "tx_name", + }) + if err != nil { + return err + } + + // Read within transaction should also use master + count, err := tx.Model(table).Count() + t.AssertNil(err) + t.Assert(count, 1) + + return nil + }) + t.AssertNil(err) + + // Verify data is in master + count, err := masterSlaveDB.Model(table).Master().Count() + t.AssertNil(err) + t.Assert(count, 1) + }) +} + +// Test_Master_Slave_Explicit_Selection tests explicit master/slave selection +func Test_Master_Slave_Explicit_Selection(t *testing.T) { + var err error + + gtest.C(t, func(t *gtest.T) { + _, err = db.Exec(ctx, "CREATE DATABASE IF NOT EXISTS `master` CHARACTER SET UTF8") + t.AssertNil(err) + _, err = db.Exec(ctx, "CREATE DATABASE IF NOT EXISTS `slave` CHARACTER SET UTF8") + t.AssertNil(err) + }) + defer func() { + _, _ = db.Exec(ctx, "DROP DATABASE `master`") + _, _ = db.Exec(ctx, "DROP DATABASE `slave`") + }() + + var ( + configKey = guid.S() + configGroup = gdb.ConfigGroup{ + gdb.ConfigNode{ + Host: "127.0.0.1", + Port: "3307", + User: "root", + Pass: "12345678", + Name: "master", + Type: "mariadb", + Role: "master", + Weight: 100, + }, + gdb.ConfigNode{ + Host: "127.0.0.1", + Port: "3307", + User: "root", + Pass: "12345678", + Name: "slave", + Type: "mariadb", + Role: "slave", + Weight: 100, + }, + } + ) + gdb.SetConfigGroup(configKey, configGroup) + masterSlaveDB := g.DB(configKey) + + gtest.C(t, func(t *gtest.T) { + table := "table_" + guid.S() + createTableWithDb(masterSlaveDB.Schema("master"), table) + createTableWithDb(masterSlaveDB.Schema("slave"), table) + defer dropTableWithDb(masterSlaveDB.Schema("master"), table) + defer dropTableWithDb(masterSlaveDB.Schema("slave"), table) + + // Insert to master + _, err := masterSlaveDB.Model(table).Master().Insert(g.Map{ + "passport": "explicit_test", + "password": "pass", + "nickname": "name", + }) + t.AssertNil(err) + + // Explicitly read from slave (should be empty) + count, err := masterSlaveDB.Model(table).Slave().Count() + t.AssertNil(err) + t.Assert(count, 0) + + // Explicitly read from master (should have data) + count, err = masterSlaveDB.Model(table).Master().Count() + t.AssertNil(err) + t.Assert(count, 1) + }) +} diff --git a/contrib/drivers/mariadb/mariadb_z_unit_feature_metadata_test.go b/contrib/drivers/mariadb/mariadb_z_unit_feature_metadata_test.go new file mode 100644 index 000000000..2d6a020cb --- /dev/null +++ b/contrib/drivers/mariadb/mariadb_z_unit_feature_metadata_test.go @@ -0,0 +1,115 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mariadb_test + +import ( + "testing" + + "github.com/gogf/gf/v2/test/gtest" +) + +// Test_TableFields_Basic tests basic TableFields functionality +func Test_TableFields_Basic(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + fields, err := db.TableFields(ctx, table) + t.AssertNil(err) + t.AssertGT(len(fields), 0) + + // Verify common fields exist + _, ok := fields["id"] + t.Assert(ok, true) + _, ok = fields["passport"] + t.Assert(ok, true) + _, ok = fields["password"] + t.Assert(ok, true) + _, ok = fields["nickname"] + t.Assert(ok, true) + _, ok = fields["create_time"] + t.Assert(ok, true) + }) +} + +// Test_TableFields_Schema tests TableFields with explicit schema +func Test_TableFields_Schema(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + fields, err := db.TableFields(ctx, table, TestSchema1) + t.AssertNil(err) + t.AssertGT(len(fields), 0) + + // Verify field properties + idField, ok := fields["id"] + t.Assert(ok, true) + t.Assert(idField.Name, "id") + t.AssertGT(idField.Index, -1) + }) +} + +// Test_HasField_Positive tests HasField for existing field +func Test_HasField_Positive(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + has, err := db.GetCore().HasField(ctx, table, "id") + t.AssertNil(err) + t.Assert(has, true) + + has, err = db.GetCore().HasField(ctx, table, "passport") + t.AssertNil(err) + t.Assert(has, true) + }) +} + +// Test_HasField_Negative tests HasField for non-existent field +func Test_HasField_Negative(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + has, err := db.GetCore().HasField(ctx, table, "non_exist_field") + t.AssertNil(err) + t.Assert(has, false) + }) +} + +// Test_HasField_Schema tests HasField with explicit schema +func Test_HasField_Schema(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + has, err := db.GetCore().HasField(ctx, table, "id", TestSchema1) + t.AssertNil(err) + t.Assert(has, true) + }) +} + +// Test_QuoteWord_Basic tests basic QuoteWord functionality +func Test_QuoteWord_Basic(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + quoted := db.GetCore().QuoteWord("user") + t.Assert(quoted, "`user`") + + quoted = db.GetCore().QuoteWord("user_table") + t.Assert(quoted, "`user_table`") + }) +} + +// Test_QuoteWord_AlreadyQuoted tests QuoteWord with already quoted words +func Test_QuoteWord_AlreadyQuoted(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // If already quoted, should not double quote + quoted := db.GetCore().QuoteWord("`user`") + t.Assert(quoted, "`user`") + }) +} diff --git a/contrib/drivers/mariadb/mariadb_z_unit_feature_partition_test.go b/contrib/drivers/mariadb/mariadb_z_unit_feature_partition_test.go new file mode 100644 index 000000000..488491af1 --- /dev/null +++ b/contrib/drivers/mariadb/mariadb_z_unit_feature_partition_test.go @@ -0,0 +1,364 @@ +// 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 mariadb_test + +import ( + "context" + "fmt" + "testing" + + "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" +) + +func createRangePartitionTable(table ...string) string { + var name string + if len(table) > 0 { + name = table[0] + } else { + name = fmt.Sprintf(`partition_range_%d`, gtime.TimestampNano()) + } + if _, err := db3.Exec(ctx, fmt.Sprintf("DROP TABLE IF EXISTS `%s`", name)); err != nil { + gtest.Fatal(err) + } + if _, err := db3.Exec(ctx, fmt.Sprintf(` + CREATE TABLE %s ( + id int(11) NOT NULL, + sales_date date DEFAULT NULL, + amount decimal(10,2) DEFAULT NULL, + region varchar(50) DEFAULT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + PARTITION BY RANGE (YEAR(sales_date)) + (PARTITION p2020 VALUES LESS THAN (2021) ENGINE = InnoDB, + PARTITION p2021 VALUES LESS THAN (2022) ENGINE = InnoDB, + PARTITION p2022 VALUES LESS THAN (2023) ENGINE = InnoDB, + PARTITION p2023 VALUES LESS THAN (2024) ENGINE = InnoDB, + PARTITION p_future VALUES LESS THAN MAXVALUE ENGINE = InnoDB); + `, name)); err != nil { + gtest.Fatal(err) + } + return name +} + +func createHashPartitionTable(table ...string) string { + var name string + if len(table) > 0 { + name = table[0] + } else { + name = fmt.Sprintf(`partition_hash_%d`, gtime.TimestampNano()) + } + if _, err := db3.Exec(ctx, fmt.Sprintf("DROP TABLE IF EXISTS `%s`", name)); err != nil { + gtest.Fatal(err) + } + if _, err := db3.Exec(ctx, fmt.Sprintf(` + CREATE TABLE %s ( + id int(11) NOT NULL, + user_id int(11) NOT NULL, + username varchar(50) DEFAULT NULL, + email varchar(100) DEFAULT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + PARTITION BY HASH (user_id) + PARTITIONS 4; + `, name)); err != nil { + gtest.Fatal(err) + } + return name +} + +func createListPartitionTable(table ...string) string { + var name string + if len(table) > 0 { + name = table[0] + } else { + name = fmt.Sprintf(`partition_list_%d`, gtime.TimestampNano()) + } + if _, err := db3.Exec(ctx, fmt.Sprintf("DROP TABLE IF EXISTS `%s`", name)); err != nil { + gtest.Fatal(err) + } + if _, err := db3.Exec(ctx, fmt.Sprintf(` + CREATE TABLE %s ( + id int(11) NOT NULL, + region_code int(11) NOT NULL, + city varchar(50) DEFAULT NULL, + population int(11) DEFAULT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + PARTITION BY LIST (region_code) + (PARTITION p_north VALUES IN (1,2,3) ENGINE = InnoDB, + PARTITION p_south VALUES IN (4,5,6) ENGINE = InnoDB, + PARTITION p_east VALUES IN (7,8,9) ENGINE = InnoDB, + PARTITION p_west VALUES IN (10,11,12) ENGINE = InnoDB); + `, name)); err != nil { + gtest.Fatal(err) + } + return name +} + +func dropPartitionTable(table string) { + if _, err := db3.Exec(ctx, fmt.Sprintf("DROP TABLE IF EXISTS `%s`", table)); err != nil { + gtest.Error(err) + } +} + +func Test_Partition_Range_Insert_And_Query(t *testing.T) { + table := createRangePartitionTable() + defer dropPartitionTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert data across different partitions + data := g.Slice{ + g.Map{"id": 1, "sales_date": "2020-06-15", "amount": 1000.50, "region": "North"}, + g.Map{"id": 2, "sales_date": "2021-03-20", "amount": 2000.75, "region": "South"}, + g.Map{"id": 3, "sales_date": "2022-09-10", "amount": 3000.00, "region": "East"}, + g.Map{"id": 4, "sales_date": "2023-12-01", "amount": 4000.25, "region": "West"}, + g.Map{"id": 5, "sales_date": "2024-01-15", "amount": 5000.00, "region": "North"}, + } + _, err := db3.Model(table).Data(data).Insert() + t.AssertNil(err) + + // Query all data + all, err := db3.Model(table).All() + t.AssertNil(err) + t.Assert(len(all), 5) + + // Query specific year (should hit specific partition) + result, err := db3.Model(table).Where("YEAR(sales_date) = ?", 2022).All() + t.AssertNil(err) + t.Assert(len(result), 1) + t.Assert(result[0]["id"], 3) + }) +} + +func Test_Partition_Range_PartitionQuery(t *testing.T) { + // Known limitation: Model.Partition() sets m.partition field but it's not used in SQL generation + // See: database/gdb/gdb_model_select.go lines 735,755 - m.tables is used without PARTITION clause + // TODO: Add PARTITION clause support to GoFrame query builder + t.Skip("Partition clause in SELECT queries not yet supported in GoFrame query builder") + + table := createRangePartitionTable() + defer dropPartitionTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert data + data := g.Slice{ + g.Map{"id": 1, "sales_date": "2020-06-15", "amount": 1000.50}, + g.Map{"id": 2, "sales_date": "2021-03-20", "amount": 2000.75}, + g.Map{"id": 3, "sales_date": "2022-09-10", "amount": 3000.00}, + g.Map{"id": 4, "sales_date": "2023-12-01", "amount": 4000.25}, + } + _, err := db3.Model(table).Data(data).Insert() + t.AssertNil(err) + + // Query specific partition + result, err := db3.Model(table).Partition("p2022").All() + t.AssertNil(err) + t.Assert(len(result), 1) + t.Assert(result[0]["id"], 3) + + // Query multiple partitions + result, err = db3.Model(table).Partition("p2021", "p2022").All() + t.AssertNil(err) + t.Assert(len(result), 2) + }) +} + +func Test_Partition_Hash_Insert_And_Distribution(t *testing.T) { + table := createHashPartitionTable() + defer dropPartitionTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert data that will be distributed across hash partitions + data := g.Slice{} + for i := 1; i <= 20; i++ { + data = append(data, g.Map{ + "id": i, + "user_id": i * 10, + "username": fmt.Sprintf("user_%d", i), + "email": fmt.Sprintf("user%d@example.com", i), + }) + } + _, err := db3.Model(table).Data(data).Insert() + t.AssertNil(err) + + // Query all data + all, err := db3.Model(table).All() + t.AssertNil(err) + t.Assert(len(all), 20) + + // Query specific user_id (will hit specific partition based on hash) + result, err := db3.Model(table).Where("user_id", 100).One() + t.AssertNil(err) + t.Assert(result["username"], "user_10") + }) +} + +func Test_Partition_List_Insert_And_Query(t *testing.T) { + // Known limitation: Model.Partition() sets m.partition field but it's not used in SQL generation + // See: database/gdb/gdb_model_select.go lines 735,755 - m.tables is used without PARTITION clause + // TODO: Add PARTITION clause support to GoFrame query builder + t.Skip("Partition clause in SELECT queries not yet supported in GoFrame query builder") + + table := createListPartitionTable() + defer dropPartitionTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert data for different regions + data := g.Slice{ + g.Map{"id": 1, "region_code": 1, "city": "Beijing", "population": 2154}, + g.Map{"id": 2, "region_code": 2, "city": "Harbin", "population": 1063}, + g.Map{"id": 3, "region_code": 5, "city": "Guangzhou", "population": 1868}, + g.Map{"id": 4, "region_code": 7, "city": "Shanghai", "population": 2428}, + g.Map{"id": 5, "region_code": 10, "city": "Chengdu", "population": 2093}, + } + _, err := db3.Model(table).Data(data).Insert() + t.AssertNil(err) + + // Query all + all, err := db3.Model(table).All() + t.AssertNil(err) + t.Assert(len(all), 5) + + // Query specific partition (north region) + result, err := db3.Model(table).Partition("p_north").All() + t.AssertNil(err) + t.Assert(len(result), 2) + + // Query specific partition (south region) + result, err = db3.Model(table).Partition("p_south").All() + t.AssertNil(err) + t.Assert(len(result), 1) + t.Assert(result[0]["city"], "Guangzhou") + }) +} + +func Test_Partition_Range_Update(t *testing.T) { + table := createRangePartitionTable() + defer dropPartitionTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert data + _, err := db3.Model(table).Data(g.Map{ + "id": 1, + "sales_date": "2022-06-15", + "amount": 1000.00, + "region": "North", + }).Insert() + t.AssertNil(err) + + // Update data within same partition + result, err := db3.Model(table).Data(g.Map{ + "amount": 1500.00, + "region": "South", + }).Where("id", 1).Update() + t.AssertNil(err) + n, _ := result.RowsAffected() + t.Assert(n, 1) + + // Verify update + one, err := db3.Model(table).Where("id", 1).One() + t.AssertNil(err) + t.Assert(one["amount"], "1500.00") + t.Assert(one["region"], "South") + }) +} + +func Test_Partition_Range_Delete(t *testing.T) { + table := createRangePartitionTable() + defer dropPartitionTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert data + data := g.Slice{ + g.Map{"id": 1, "sales_date": "2020-06-15", "amount": 1000.50}, + g.Map{"id": 2, "sales_date": "2021-03-20", "amount": 2000.75}, + g.Map{"id": 3, "sales_date": "2022-09-10", "amount": 3000.00}, + } + _, err := db3.Model(table).Data(data).Insert() + t.AssertNil(err) + + // Delete from specific partition + result, err := db3.Model(table).Where("YEAR(sales_date) = ?", 2021).Delete() + t.AssertNil(err) + n, _ := result.RowsAffected() + t.Assert(n, 1) + + // Verify deletion + all, err := db3.Model(table).All() + t.AssertNil(err) + t.Assert(len(all), 2) + + // Verify remaining data + result2, err := db3.Model(table).Where("YEAR(sales_date) = ?", 2021).All() + t.AssertNil(err) + t.Assert(len(result2), 0) + }) +} + +func Test_Partition_Transaction(t *testing.T) { + table := createRangePartitionTable() + defer dropPartitionTable(table) + + gtest.C(t, func(t *gtest.T) { + // Transaction with partitioned table + err := db3.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + // Insert across multiple partitions + data := g.Slice{ + g.Map{"id": 1, "sales_date": "2020-06-15", "amount": 1000.50}, + g.Map{"id": 2, "sales_date": "2021-03-20", "amount": 2000.75}, + g.Map{"id": 3, "sales_date": "2022-09-10", "amount": 3000.00}, + } + _, err := tx.Model(table).Ctx(ctx).Data(data).Insert() + if err != nil { + return err + } + + // Update in transaction + _, err = tx.Model(table).Ctx(ctx).Data(g.Map{ + "amount": 1500.00, + }).Where("id", 1).Update() + return err + }) + t.AssertNil(err) + + // Verify transaction committed + all, err := db3.Model(table).All() + t.AssertNil(err) + t.Assert(len(all), 3) + + one, err := db3.Model(table).Where("id", 1).One() + t.AssertNil(err) + t.Assert(one["amount"], "1500.00") + }) +} + +func Test_Partition_Range_Count_And_Sum(t *testing.T) { + table := createRangePartitionTable() + defer dropPartitionTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert data + data := g.Slice{ + g.Map{"id": 1, "sales_date": "2020-06-15", "amount": 1000.00}, + g.Map{"id": 2, "sales_date": "2020-09-20", "amount": 1500.00}, + g.Map{"id": 3, "sales_date": "2021-03-20", "amount": 2000.00}, + g.Map{"id": 4, "sales_date": "2022-09-10", "amount": 3000.00}, + } + _, err := db3.Model(table).Data(data).Insert() + t.AssertNil(err) + + // Count by year (specific partition) + count, err := db3.Model(table).Where("YEAR(sales_date) = ?", 2020).Count() + t.AssertNil(err) + t.Assert(count, 2) + + // Sum across partitions + value, err := db3.Model(table).Fields("SUM(amount) as total").Value() + t.AssertNil(err) + t.AssertGT(value.Float64(), 7000.0) // 1000+1500+2000+3000 = 7500 + }) +} diff --git a/contrib/drivers/mariadb/mariadb_z_unit_issue_test.go b/contrib/drivers/mariadb/mariadb_z_unit_issue_test.go new file mode 100644 index 000000000..2f8134aed --- /dev/null +++ b/contrib/drivers/mariadb/mariadb_z_unit_issue_test.go @@ -0,0 +1,2075 @@ +// 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 mariadb_test + +import ( + "context" + "encoding/json" + "fmt" + "sync" + "testing" + "time" + + "github.com/gogf/gf/v2/container/gvar" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gmeta" + "github.com/gogf/gf/v2/util/guid" +) + +// https://github.com/gogf/gf/issues/1380 +func Test_Issue1380(t *testing.T) { + type GiftImage struct { + Uid string `json:"uid"` + Url string `json:"url"` + Status string `json:"status"` + Name string `json:"name"` + } + + type GiftComment struct { + Name string `json:"name"` + Field string `json:"field"` + Required bool `json:"required"` + } + + type Prop struct { + Name string `json:"name"` + Values []string `json:"values"` + } + + type Sku struct { + GiftId int64 `json:"gift_id"` + Name string `json:"name"` + ScorePrice int `json:"score_price"` + MarketPrice int `json:"market_price"` + CostPrice int `json:"cost_price"` + Stock int `json:"stock"` + } + + type Covers struct { + List []GiftImage `json:"list"` + } + + type GiftEntity struct { + Id int64 `json:"id"` + StoreId int64 `json:"store_id"` + GiftType int `json:"gift_type"` + GiftName string `json:"gift_name"` + Description string `json:"description"` + Covers Covers `json:"covers"` + Cover string `json:"cover"` + GiftCategoryId []int64 `json:"gift_category_id"` + HasProps bool `json:"has_props"` + OutSn string `json:"out_sn"` + IsLimitSell bool `json:"is_limit_sell"` + LimitSellType int `json:"limit_sell_type"` + LimitSellCycle string `json:"limit_sell_cycle"` + LimitSellCycleCount int `json:"limit_sell_cycle_count"` + LimitSellCustom bool `json:"limit_sell_custom"` // 只允许特定会员兑换 + LimitCustomerTags []int64 `json:"limit_customer_tags"` // 允许兑换的成员 + ScorePrice int `json:"score_price"` + MarketPrice float64 `json:"market_price"` + CostPrice int `json:"cost_price"` + Stock int `json:"stock"` + Props []Prop `json:"props"` + Skus []Sku `json:"skus"` + ExpressType []string `json:"express_type"` + Comments []GiftComment `json:"comments"` + Content string `json:"content"` + AtLeastRechargeCount int `json:"at_least_recharge_count"` + Status int `json:"status"` + } + + type User struct { + Id int + Passport string + } + + table := "jfy_gift" + array := gstr.SplitAndTrim(gtest.DataContent(`issues`, `1380.sql`), ";") + for _, v := range array { + if _, err := db.Exec(ctx, v); err != nil { + gtest.Error(err) + } + } + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + var ( + entity = new(GiftEntity) + err = db.Model(table).Where("id", 17).Scan(entity) + ) + t.AssertNil(err) + t.Assert(len(entity.Skus), 2) + + t.Assert(entity.Skus[0].Name, "red") + t.Assert(entity.Skus[0].Stock, 10) + t.Assert(entity.Skus[0].GiftId, 1) + t.Assert(entity.Skus[0].CostPrice, 80) + t.Assert(entity.Skus[0].ScorePrice, 188) + t.Assert(entity.Skus[0].MarketPrice, 388) + + t.Assert(entity.Skus[1].Name, "blue") + t.Assert(entity.Skus[1].Stock, 100) + t.Assert(entity.Skus[1].GiftId, 2) + t.Assert(entity.Skus[1].CostPrice, 81) + t.Assert(entity.Skus[1].ScorePrice, 200) + t.Assert(entity.Skus[1].MarketPrice, 288) + + t.Assert(entity.Id, 17) + t.Assert(entity.StoreId, 100004) + t.Assert(entity.GiftType, 1) + t.Assert(entity.GiftName, "GIFT") + t.Assert(entity.Description, "支持个性定制的父亲节老师长辈的专属礼物") + t.Assert(len(entity.Covers.List), 3) + t.Assert(entity.OutSn, "259402") + t.Assert(entity.LimitCustomerTags, "[]") + t.Assert(entity.ScorePrice, 10) + t.Assert(len(entity.Props), 1) + t.Assert(len(entity.Comments), 2) + t.Assert(entity.Status, 99) + t.Assert(entity.Content, `
礼品详情
`) + }) +} + +// https://github.com/gogf/gf/issues/1934 +func Test_Issue1934(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + one, err := db.Model(table).Where(" id ", 1).One() + t.AssertNil(err) + t.Assert(one["id"], 1) + }) +} + +// https://github.com/gogf/gf/issues/1570 +func Test_Issue1570(t *testing.T) { + var ( + tableUser = "user_" + gtime.TimestampMicroStr() + tableUserDetail = "user_detail_" + gtime.TimestampMicroStr() + tableUserScores = "user_scores_" + gtime.TimestampMicroStr() + ) + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + uid int(10) unsigned NOT NULL AUTO_INCREMENT, + name varchar(45) NOT NULL, + PRIMARY KEY (uid) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, tableUser)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUser) + + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + uid int(10) unsigned NOT NULL AUTO_INCREMENT, + address varchar(45) NOT NULL, + PRIMARY KEY (uid) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, tableUserDetail)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUserDetail) + + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + uid int(10) unsigned NOT NULL, + score int(10) unsigned NOT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, tableUserScores)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUserScores) + + type EntityUser struct { + Uid int `json:"uid"` + Name string `json:"name"` + } + type EntityUserDetail struct { + Uid int `json:"uid"` + Address string `json:"address"` + } + type EntityUserScores struct { + Id int `json:"id"` + Uid int `json:"uid"` + Score int `json:"score"` + } + type Entity struct { + User *EntityUser + UserDetail *EntityUserDetail + UserScores []*EntityUserScores + } + + // Initialize the data. + gtest.C(t, func(t *gtest.T) { + var err error + for i := 1; i <= 5; i++ { + // User. + _, err = db.Insert(ctx, tableUser, g.Map{ + "uid": i, + "name": fmt.Sprintf(`name_%d`, i), + }) + t.AssertNil(err) + // Detail. + _, err = db.Insert(ctx, tableUserDetail, g.Map{ + "uid": i, + "address": fmt.Sprintf(`address_%d`, i), + }) + t.AssertNil(err) + // Scores. + for j := 1; j <= 5; j++ { + _, err = db.Insert(ctx, tableUserScores, g.Map{ + "uid": i, + "score": j, + }) + t.AssertNil(err) + } + } + }) + + // Result ScanList with struct elements and pointer attributes. + gtest.C(t, func(t *gtest.T) { + var users []Entity + // User + err := db.Model(tableUser). + Where("uid", g.Slice{3, 4}). + Fields("uid"). + Order("uid asc"). + ScanList(&users, "User") + t.AssertNil(err) + t.AssertNil(err) + t.Assert(len(users), 2) + t.Assert(users[0].User, &EntityUser{3, ""}) + t.Assert(users[1].User, &EntityUser{4, ""}) + // Detail + err = db.Model(tableUserDetail). + Where("uid", gdb.ListItemValues(users, "User", "Uid")). + Order("uid asc"). + ScanList(&users, "UserDetail", "User", "uid:Uid") + t.AssertNil(err) + t.AssertNil(err) + t.Assert(users[0].UserDetail, &EntityUserDetail{3, "address_3"}) + t.Assert(users[1].UserDetail, &EntityUserDetail{4, "address_4"}) + // Scores + err = db.Model(tableUserScores). + Where("uid", gdb.ListItemValues(users, "User", "Uid")). + Order("id asc"). + ScanList(&users, "UserScores", "User", "uid:Uid") + t.AssertNil(err) + t.AssertNil(err) + t.Assert(len(users[0].UserScores), 5) + t.Assert(len(users[1].UserScores), 5) + t.Assert(users[0].UserScores[0].Uid, 3) + t.Assert(users[0].UserScores[0].Score, 1) + t.Assert(users[0].UserScores[4].Score, 5) + t.Assert(users[1].UserScores[0].Uid, 4) + t.Assert(users[1].UserScores[0].Score, 1) + t.Assert(users[1].UserScores[4].Score, 5) + }) +} + +// https://github.com/gogf/gf/issues/1401 +func Test_Issue1401(t *testing.T) { + var ( + table1 = "parcels" + table2 = "parcel_items" + ) + array := gstr.SplitAndTrim(gtest.DataContent(`issues`, `1401.sql`), ";") + for _, v := range array { + if _, err := db.Exec(ctx, v); err != nil { + gtest.Error(err) + } + } + defer dropTable(table1) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + type NItem struct { + Id int `json:"id"` + ParcelId int `json:"parcel_id"` + } + + type ParcelItem struct { + gmeta.Meta `orm:"table:parcel_items"` + NItem + } + + type ParcelRsp struct { + gmeta.Meta `orm:"table:parcels"` + Id int `json:"id"` + Items []*ParcelItem `json:"items" orm:"with:parcel_id=Id"` + } + + parcelDetail := &ParcelRsp{} + err := db.Model(table1).With(parcelDetail.Items).Where("id", 3).Scan(&parcelDetail) + t.AssertNil(err) + t.Assert(parcelDetail.Id, 3) + t.Assert(len(parcelDetail.Items), 1) + t.Assert(parcelDetail.Items[0].Id, 2) + t.Assert(parcelDetail.Items[0].ParcelId, 3) + }) +} + +// https://github.com/gogf/gf/issues/1412 +func Test_Issue1412(t *testing.T) { + var ( + table1 = "parcels" + table2 = "items" + ) + array := gstr.SplitAndTrim(gtest.DataContent(`issues`, `1412.sql`), ";") + for _, v := range array { + if _, err := db.Exec(ctx, v); err != nil { + gtest.Error(err) + } + } + defer dropTable(table1) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + type Items struct { + gmeta.Meta `orm:"table:items"` + Id int `json:"id"` + Name string `json:"name"` + } + + type ParcelRsp struct { + gmeta.Meta `orm:"table:parcels"` + Id int `json:"id"` + ItemId int `json:"item_id"` + Items Items `json:"items" orm:"with:Id=ItemId"` + } + + entity := &ParcelRsp{} + err := db.Model("parcels").With(Items{}).Where("id=3").Scan(&entity) + t.AssertNil(err) + t.Assert(entity.Id, 3) + t.Assert(entity.ItemId, 0) + t.Assert(entity.Items.Id, 0) + t.Assert(entity.Items.Name, "") + }) + + gtest.C(t, func(t *gtest.T) { + type Items struct { + gmeta.Meta `orm:"table:items"` + Id int `json:"id"` + Name string `json:"name"` + } + + type ParcelRsp struct { + gmeta.Meta `orm:"table:parcels"` + Id int `json:"id"` + ItemId int `json:"item_id"` + Items Items `json:"items" orm:"with:Id=ItemId"` + } + + entity := &ParcelRsp{} + err := db.Model("parcels").With(Items{}).Where("id=30000").Scan(&entity) + t.AssertNE(err, nil) + t.Assert(entity.Id, 0) + t.Assert(entity.ItemId, 0) + t.Assert(entity.Items.Id, 0) + t.Assert(entity.Items.Name, "") + }) +} + +// https://github.com/gogf/gf/issues/1002 +func Test_Issue1002(t *testing.T) { + table := createTable() + defer dropTable(table) + + result, err := db.Model(table).Data(g.Map{ + "id": 1, + "passport": "port_1", + "password": "pass_1", + "nickname": "name_2", + "create_time": "2020-10-27 19:03:33", + }).Insert() + gtest.AssertNil(err) + n, _ := result.RowsAffected() + gtest.Assert(n, 1) + + // where + string. + gtest.C(t, func(t *gtest.T) { + v, err := db.Model(table).Fields("id").Where("create_time>'2020-10-27 19:03:32' and create_time<'2020-10-27 19:03:34'").Value() + t.AssertNil(err) + t.Assert(v.Int(), 1) + }) + gtest.C(t, func(t *gtest.T) { + v, err := db.Model(table).Fields("id").Where("create_time>'2020-10-27 19:03:32' and create_time<'2020-10-27 19:03:34'").Value() + t.AssertNil(err) + t.Assert(v.Int(), 1) + }) + // where + string arguments. + gtest.C(t, func(t *gtest.T) { + v, err := db.Model(table).Fields("id").Where("create_time>? and create_time", "2020-10-27 19:03:32", "2020-10-27 19:03:34").Value() + t.AssertNil(err) + t.Assert(v.Int(), 1) + }) + // where + gtime.Time arguments. + gtest.C(t, func(t *gtest.T) { + v, err := db.Model(table).Fields("id").Where("create_time>? and create_time", gtime.New("2020-10-27 19:03:32"), gtime.New("2020-10-27 19:03:34")).Value() + t.AssertNil(err) + t.Assert(v.Int(), 1) + }) + // where + time.Time arguments, UTC. + gtest.C(t, func(t *gtest.T) { + t1, _ := time.Parse("2006-01-02 15:04:05", "2020-10-27 11:03:32") + t2, _ := time.Parse("2006-01-02 15:04:05", "2020-10-27 11:03:34") + { + v, err := db.Model(table).Fields("id").Where("create_time>? and create_time", t1, t2).Value() + t.AssertNil(err) + t.Assert(v.Int(), 1) + } + }) + // where + time.Time arguments, +8. + // gtest.C(t, func(t *gtest.T) { + // // Change current timezone to +8 zone. + // location, err := time.LoadLocation("Asia/Shanghai") + // t.AssertNil(err) + // t1, _ := time.ParseInLocation("2006-01-02 15:04:05", "2020-10-27 19:03:32", location) + // t2, _ := time.ParseInLocation("2006-01-02 15:04:05", "2020-10-27 19:03:34", location) + // { + // v, err := db.Model(table).Fields("id").Where("create_time>? and create_time", t1, t2).Value() + // t.AssertNil(err) + // t.Assert(v.Int(), 1) + // } + // { + // v, err := db.Model(table).Fields("id").Where("create_time>? and create_time", t1, t2).FindValue() + // t.AssertNil(err) + // t.Assert(v.Int(), 1) + // } + // { + // v, err := db.Model(table).Where("create_time>? and create_time", t1, t2).FindValue("id") + // t.AssertNil(err) + // t.Assert(v.Int(), 1) + // } + // }) +} + +// https://github.com/gogf/gf/issues/1700 +func Test_Issue1700(t *testing.T) { + table := "user_" + gtime.Now().TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` + CREATE TABLE %s ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + user_id int(10) unsigned NOT NULL, + UserId int(10) unsigned NOT NULL, + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table, + )); err != nil { + gtest.AssertNil(err) + } + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + type User struct { + Id int `orm:"id"` + Userid int `orm:"user_id"` + UserId int `orm:"UserId"` + } + _, err := db.Model(table).Data(User{ + Id: 1, + Userid: 2, + UserId: 3, + }).Insert() + t.AssertNil(err) + + one, err := db.Model(table).One() + t.AssertNil(err) + t.Assert(one, g.Map{ + "id": 1, + "user_id": 2, + "UserId": 3, + }) + + for i := 0; i < 1000; i++ { + var user *User + err = db.Model(table).Scan(&user) + t.AssertNil(err) + t.Assert(user.Id, 1) + t.Assert(user.Userid, 2) + t.Assert(user.UserId, 3) + } + }) +} + +// https://github.com/gogf/gf/issues/1701 +func Test_Issue1701(t *testing.T) { + table := createInitTable() + defer dropTable(table) + gtest.C(t, func(t *gtest.T) { + value, err := db.Model(table).Fields(gdb.Raw("if(id=1,100,null)")).WherePri(1).Value() + t.AssertNil(err) + t.Assert(value.String(), 100) + }) +} + +// https://github.com/gogf/gf/issues/1733 +func Test_Issue1733(t *testing.T) { + table := "user_" + guid.S() + if _, err := db.Exec(ctx, fmt.Sprintf(` + CREATE TABLE %s ( + id int(8) unsigned zerofill NOT NULL AUTO_INCREMENT, + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table, + )); err != nil { + gtest.AssertNil(err) + } + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + for i := 1; i <= 10; i++ { + _, err := db.Model(table).Data(g.Map{ + "id": i, + }).Insert() + t.AssertNil(err) + } + + all, err := db.Model(table).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(all), 10) + for i := 0; i < 10; i++ { + t.Assert(all[i]["id"].Int(), i+1) + } + }) +} + +// https://github.com/gogf/gf/issues/2012 +func Test_Issue2012(t *testing.T) { + table := "time_only_" + guid.S() + if _, err := db.Exec(ctx, fmt.Sprintf(` + CREATE TABLE %s( + id int(8) unsigned zerofill NOT NULL AUTO_INCREMENT, + time_only time, + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table, + )); err != nil { + gtest.AssertNil(err) + } + defer dropTable(table) + + type TimeOnly struct { + Id int `json:"id"` + TimeOnly *gtime.Time `json:"timeOnly"` + } + + gtest.C(t, func(t *gtest.T) { + timeOnly := gtime.New("15:04:05") + m := db.Model(table) + + _, err := m.Insert(TimeOnly{ + TimeOnly: gtime.New(timeOnly), + }) + t.AssertNil(err) + + _, err = m.Insert(g.Map{ + "time_only": timeOnly, + }) + t.AssertNil(err) + + _, err = m.Insert("time_only", timeOnly) + t.AssertNil(err) + }) +} + +// https://github.com/gogf/gf/issues/2105 +func Test_Issue2105(t *testing.T) { + table := "issue2105" + array := gstr.SplitAndTrim(gtest.DataContent(`issues`, `2105.sql`), ";") + for _, v := range array { + if _, err := db.Exec(ctx, v); err != nil { + gtest.Error(err) + } + } + defer dropTable(table) + + type JsonItem struct { + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` + } + type Test struct { + Id string `json:"id,omitempty"` + Json []*JsonItem `json:"json,omitempty"` + } + + gtest.C(t, func(t *gtest.T) { + var list []*Test + err := db.Model(table).Scan(&list) + t.AssertNil(err) + t.Assert(len(list), 2) + t.Assert(len(list[0].Json), 0) + t.Assert(len(list[1].Json), 3) + }) +} + +// https://github.com/gogf/gf/issues/2231 +func Test_Issue2231(t *testing.T) { + var ( + pattern = `(\w+):([\w\-]*):(.*?)@(\w+?)\((.+?)\)/{0,1}([^\?]*)\?{0,1}(.*)` + link = `mariadb:root:12345678@tcp(127.0.0.1:3307)/a正bc式?loc=Local&parseTime=true` + ) + gtest.C(t, func(t *gtest.T) { + match, err := gregex.MatchString(pattern, link) + t.AssertNil(err) + t.Assert(match[1], "mariadb") + t.Assert(match[2], "root") + t.Assert(match[3], "12345678") + t.Assert(match[4], "tcp") + t.Assert(match[5], "127.0.0.1:3307") + t.Assert(match[6], "a正bc式") + t.Assert(match[7], "loc=Local&parseTime=true") + }) +} + +// https://github.com/gogf/gf/issues/2339 +func Test_Issue2339(t *testing.T) { + table := createInitTable() + defer dropTable(table) + gtest.C(t, func(t *gtest.T) { + model1 := db.Model(table, "u1").Where("id between ? and ?", 1, 9) + model2 := db.Model("? as u2", model1) + model3 := db.Model("? as u3", model2) + all2, err := model2.WhereGT("id", 6).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(all2), 3) + t.Assert(all2[0]["id"], 7) + + all3, err := model3.WhereGT("id", 7).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(all3), 2) + t.Assert(all3[0]["id"], 8) + }) +} + +// https://github.com/gogf/gf/issues/2356 +func Test_Issue2356(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + table := "demo_" + guid.S() + if _, err := db.Exec(ctx, fmt.Sprintf(` + CREATE TABLE %s ( + id BIGINT(20) UNSIGNED NOT NULL DEFAULT '0', + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table, + )); err != nil { + t.AssertNil(err) + } + defer dropTable(table) + + if _, err := db.Exec(ctx, fmt.Sprintf(`INSERT INTO %s (id) VALUES (18446744073709551615);`, table)); err != nil { + t.AssertNil(err) + } + + one, err := db.Model(table).One() + t.AssertNil(err) + t.AssertEQ(one["id"].Val(), uint64(18446744073709551615)) + }) +} + +// https://github.com/gogf/gf/issues/2338 +// Known bug: cross-database JOIN with soft-delete generates unqualified +// `deleted_at` column causing ambiguity error in MariaDB driver. +// See https://github.com/gogf/gf/issues/4725 +func Test_Issue2338(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + table1 := "demo_" + guid.S() + table2 := "demo_" + guid.S() + if _, err := db.Schema(TestSchema1).Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'User ID', + nickname varchar(45) DEFAULT NULL COMMENT 'User Nickname', + create_at datetime(6) DEFAULT NULL COMMENT 'Created Time', + update_at datetime(6) DEFAULT NULL COMMENT 'Updated Time', + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table1, + )); err != nil { + t.AssertNil(err) + } + if _, err := db.Schema(TestSchema2).Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'User ID', + nickname varchar(45) DEFAULT NULL COMMENT 'User Nickname', + create_at datetime(6) DEFAULT NULL COMMENT 'Created Time', + update_at datetime(6) DEFAULT NULL COMMENT 'Updated Time', + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table2, + )); err != nil { + t.AssertNil(err) + } + defer dropTableWithDb(db.Schema(TestSchema1), table1) + defer dropTableWithDb(db.Schema(TestSchema2), table2) + + var err error + _, err = db.Schema(TestSchema1).Model(table1).Insert(g.Map{ + "id": 1, + "nickname": "name_1", + }) + t.AssertNil(err) + + _, err = db.Schema(TestSchema2).Model(table2).Insert(g.Map{ + "id": 1, + "nickname": "name_2", + }) + t.AssertNil(err) + + tableName1 := fmt.Sprintf(`%s.%s`, TestSchema1, table1) + tableName2 := fmt.Sprintf(`%s.%s`, TestSchema2, table2) + all, err := db.Model(tableName1).As(`a`). + LeftJoin(tableName2+" b", `a.id=b.id`). + Fields(`a.id`, `b.nickname`).All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["nickname"], "name_2") + }) + + gtest.C(t, func(t *gtest.T) { + table1 := "demo_" + guid.S() + table2 := "demo_" + guid.S() + if _, err := db.Schema(TestSchema1).Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'User ID', + nickname varchar(45) DEFAULT NULL COMMENT 'User Nickname', + create_at datetime(6) DEFAULT NULL COMMENT 'Created Time', + update_at datetime(6) DEFAULT NULL COMMENT 'Updated Time', + deleted_at datetime(6) DEFAULT NULL COMMENT 'Deleted Time', + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table1, + )); err != nil { + t.AssertNil(err) + } + if _, err := db.Schema(TestSchema2).Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'User ID', + nickname varchar(45) DEFAULT NULL COMMENT 'User Nickname', + create_at datetime(6) DEFAULT NULL COMMENT 'Created Time', + update_at datetime(6) DEFAULT NULL COMMENT 'Updated Time', + deleted_at datetime(6) DEFAULT NULL COMMENT 'Deleted Time', + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table2, + )); err != nil { + t.AssertNil(err) + } + defer dropTableWithDb(db.Schema(TestSchema1), table1) + defer dropTableWithDb(db.Schema(TestSchema2), table2) + + var err error + _, err = db.Schema(TestSchema1).Model(table1).Insert(g.Map{ + "id": 1, + "nickname": "name_1", + }) + t.AssertNil(err) + + _, err = db.Schema(TestSchema2).Model(table2).Insert(g.Map{ + "id": 1, + "nickname": "name_2", + }) + t.AssertNil(err) + + tableName1 := fmt.Sprintf(`%s.%s`, TestSchema1, table1) + tableName2 := fmt.Sprintf(`%s.%s`, TestSchema2, table2) + all, err := db.Model(tableName1).As(`a`). + LeftJoin(tableName2+" b", `a.id=b.id`). + Fields(`a.id`, `b.nickname`).All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["nickname"], "name_2") + }) +} + +// https://github.com/gogf/gf/issues/2427 +func Test_Issue2427(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + table := "demo_" + guid.S() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'User ID', + passport varchar(45) NOT NULL COMMENT 'User Passport', + password varchar(45) NOT NULL COMMENT 'User Password', + nickname varchar(45) NOT NULL COMMENT 'User Nickname', + create_at datetime(6) DEFAULT NULL COMMENT 'Created Time', + update_at datetime(6) DEFAULT NULL COMMENT 'Updated Time', + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table, + )); err != nil { + t.AssertNil(err) + } + defer dropTable(table) + + _, err1 := db.Model(table).Delete() + t.Assert(err1, `there should be WHERE condition statement for DELETE operation`) + + _, err2 := db.Model(table).Where(g.Map{}).Delete() + t.Assert(err2, `there should be WHERE condition statement for DELETE operation`) + + _, err3 := db.Model(table).Where(1).Delete() + t.AssertNil(err3) + }) +} + +// https://github.com/gogf/gf/issues/2561 +func Test_Issue2561(t *testing.T) { + table := createTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + type User struct { + g.Meta `orm:"do:true"` + Id any + Passport any + Password any + Nickname any + CreateTime any + } + data := g.Slice{ + User{ + Id: 1, + Passport: "user_1", + }, + User{ + Id: 2, + Password: "pass_2", + }, + User{ + Id: 3, + Password: "pass_3", + }, + } + result, err := db.Model(table).Data(data).Insert() + t.AssertNil(err) + // m, _ := result.LastInsertId() // TODO: The order of LastInsertId cannot be guaranteed + // t.Assert(m, 3) + + n, _ := result.RowsAffected() + t.Assert(n, 3) + + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(one[`id`], `1`) + t.Assert(one[`passport`], `user_1`) + t.Assert(one[`password`], ``) + t.Assert(one[`nickname`], ``) + t.Assert(one[`create_time`], ``) + + one, err = db.Model(table).WherePri(2).One() + t.AssertNil(err) + t.Assert(one[`id`], `2`) + t.Assert(one[`passport`], ``) + t.Assert(one[`password`], `pass_2`) + t.Assert(one[`nickname`], ``) + t.Assert(one[`create_time`], ``) + + one, err = db.Model(table).WherePri(3).One() + t.AssertNil(err) + t.Assert(one[`id`], `3`) + t.Assert(one[`passport`], ``) + t.Assert(one[`password`], `pass_3`) + t.Assert(one[`nickname`], ``) + t.Assert(one[`create_time`], ``) + }) +} + +// https://github.com/gogf/gf/issues/2439 +func Test_Issue2439(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + array := gstr.SplitAndTrim(gtest.DataContent(`issues`, `2439.sql`), ";") + for _, v := range array { + if _, err := db.Exec(ctx, v); err != nil { + gtest.Error(err) + } + } + defer dropTable("a") + defer dropTable("b") + defer dropTable("c") + + orm := db.Model("a") + orm = orm.InnerJoin( + "c", "a.id=c.id", + ) + orm = orm.InnerJoinOnField("b", "id") + whereFormat := fmt.Sprintf( + "(`%s`.`%s` LIKE ?) ", + "b", "name", + ) + orm = orm.WhereOrf( + whereFormat, + "%a%", + ) + r, err := orm.All() + t.AssertNil(err) + t.Assert(len(r), 1) + t.Assert(r[0]["id"], 2) + t.Assert(r[0]["name"], "a") + }) +} + +// https://github.com/gogf/gf/issues/2782 +func Test_Issue2787(t *testing.T) { + table := createTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + m := db.Model("user") + + condWhere, _ := m.Builder(). + Where("id", ""). + Where(m.Builder(). + Where("nickname", "foo"). + WhereOr("password", "abc123")). + Where("passport", "pp"). + Build() + t.Assert(condWhere, "(`id`=?) AND (((`nickname`=?) OR (`password`=?))) AND (`passport`=?)") + + condWhere, _ = m.OmitEmpty().Builder(). + Where("id", ""). + Where(m.Builder(). + Where("nickname", "foo"). + WhereOr("password", "abc123")). + Where("passport", "pp"). + Build() + t.Assert(condWhere, "((`nickname`=?) OR (`password`=?)) AND (`passport`=?)") + + condWhere, _ = m.OmitEmpty().Builder(). + Where(m.Builder(). + Where("nickname", "foo"). + WhereOr("password", "abc123")). + Where("id", ""). + Where("passport", "pp"). + Build() + t.Assert(condWhere, "((`nickname`=?) OR (`password`=?)) AND (`passport`=?)") + }) +} + +// https://github.com/gogf/gf/issues/2907 +func Test_Issue2907(t *testing.T) { + table := createInitTable() + defer dropTable(table) + gtest.C(t, func(t *gtest.T) { + var ( + orm = db.Model(table) + err error + ) + + orm = orm.WherePrefixNotIn( + table, + "id", + []int{ + 1, + 2, + }, + ) + all, err := orm.OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(all), TableSize-2) + t.Assert(all[0]["id"], 3) + }) +} + +// https://github.com/gogf/gf/issues/3086 +func Test_Issue3086(t *testing.T) { + table := "issue3086_user" + array := gstr.SplitAndTrim(gtest.DataContent(`issues`, `3086.sql`), ";") + for _, v := range array { + if _, err := db.Exec(ctx, v); err != nil { + gtest.Error(err) + } + } + defer dropTable(table) + gtest.C(t, func(t *gtest.T) { + type User struct { + g.Meta `orm:"do:true"` + Id any + Passport any + Password any + Nickname any + CreateTime any + } + data := g.Slice{ + User{ + Id: 1, + Passport: "user_1", + }, + User{ + Id: 1, + Passport: "user_2", + }, + } + _, err := db.Model(table).Data(data).Batch(10).Insert() + t.AssertNE(err, nil) + }) + gtest.C(t, func(t *gtest.T) { + type User struct { + g.Meta `orm:"do:true"` + Id any + Passport any + Password any + Nickname any + CreateTime any + } + data := g.Slice{ + User{ + Id: 3, + Passport: "user_1", + }, + User{ + Id: 4, + Passport: "user_2", + }, + } + result, err := db.Model(table).Data(data).Batch(10).Insert() + t.AssertNil(err) + n, err := result.RowsAffected() + t.AssertNil(err) + t.Assert(n, 2) + }) +} + +// https://github.com/gogf/gf/issues/3204 +func Test_Issue3204(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + // where + gtest.C(t, func(t *gtest.T) { + type User struct { + g.Meta `orm:"do:true"` + Id any `orm:"id,omitempty"` + Passport any `orm:"passport,omitempty"` + Password any `orm:"password,omitempty"` + Nickname any `orm:"nickname,omitempty"` + CreateTime any `orm:"create_time,omitempty"` + } + where := User{ + Id: 2, + Passport: "", + } + all, err := db.Model(table).Where(where).All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 2) + }) + // data + gtest.C(t, func(t *gtest.T) { + type User struct { + g.Meta `orm:"do:true"` + Id any `orm:"id,omitempty"` + Passport any `orm:"passport,omitempty"` + Password any `orm:"password,omitempty"` + Nickname any `orm:"nickname,omitempty"` + CreateTime any `orm:"create_time,omitempty"` + } + var ( + err error + sqlArray []string + insertId int64 + data = User{ + Id: 20, + Passport: "passport_20", + Password: "", + } + ) + sqlArray, err = gdb.CatchSQL(ctx, func(ctx context.Context) error { + insertId, err = db.Ctx(ctx).Model(table).Data(data).InsertAndGetId() + return err + }) + t.AssertNil(err) + t.Assert(insertId, 20) + t.Assert( + gstr.Contains(sqlArray[len(sqlArray)-1], "(`id`,`passport`) VALUES(20,'passport_20')"), + true, + ) + }) + // update data + gtest.C(t, func(t *gtest.T) { + type User struct { + g.Meta `orm:"do:true"` + Id any `orm:"id,omitempty"` + Passport any `orm:"passport,omitempty"` + Password any `orm:"password,omitempty"` + Nickname any `orm:"nickname,omitempty"` + CreateTime any `orm:"create_time,omitempty"` + } + var ( + err error + sqlArray []string + data = User{ + Passport: "passport_1", + Password: "", + Nickname: "", + } + ) + sqlArray, err = gdb.CatchSQL(ctx, func(ctx context.Context) error { + _, err = db.Ctx(ctx).Model(table).Data(data).WherePri(1).Update() + return err + }) + t.AssertNil(err) + t.Assert( + gstr.Contains(sqlArray[len(sqlArray)-1], "SET `passport`='passport_1' WHERE `id`=1"), + true, + ) + }) +} + +// https://github.com/gogf/gf/issues/3218 +func Test_Issue3218(t *testing.T) { + table := "issue3218_sys_config" + array := gstr.SplitAndTrim(gtest.DataContent(`issues`, `3218.sql`), ";") + for _, v := range array { + if _, err := db.Exec(ctx, v); err != nil { + gtest.Error(err) + } + } + defer dropTable(table) + gtest.C(t, func(t *gtest.T) { + type SysConfigInfo struct { + Name string `json:"name"` + Value map[string]string `json:"value"` + } + var configData *SysConfigInfo + err := db.Model(table).Scan(&configData) + t.AssertNil(err) + t.Assert(configData, &SysConfigInfo{ + Name: "site", + Value: map[string]string{ + "fixed_page": "", + "site_name": "22", + "version": "22", + "banned_ip": "22", + "filings": "2222", + }, + }) + }) +} + +// https://github.com/gogf/gf/issues/2552 +func Test_Issue2552_ClearTableFieldsAll(t *testing.T) { + table := createTable() + defer dropTable(table) + + // MariaDB driver queries table fields via information_schema.COLUMNS + // instead of MySQL's "SHOW FULL COLUMNS FROM". + showTableKey := `information_schema.COLUMNS` + gtest.C(t, func(t *gtest.T) { + ctx := context.Background() + sqlArray, err := gdb.CatchSQL(ctx, func(ctx context.Context) error { + _, err := db.Model(table).Ctx(ctx).Insert(g.Map{ + "passport": guid.S(), + "password": guid.S(), + "nickname": guid.S(), + "create_time": gtime.NewFromStr(CreateTime).String(), + }) + return err + }) + t.AssertNil(err) + t.Assert(gstr.Contains(gstr.Join(sqlArray, "|"), showTableKey), true) + + ctx = context.Background() + sqlArray, err = gdb.CatchSQL(ctx, func(ctx context.Context) error { + one, err := db.Model(table).Ctx(ctx).One() + t.Assert(len(one), 6) + return err + }) + t.AssertNil(err) + t.Assert(gstr.Contains(gstr.Join(sqlArray, "|"), showTableKey), false) + + _, err = db.Exec(ctx, fmt.Sprintf("alter table %s drop column `nickname`", table)) + t.AssertNil(err) + + err = db.GetCore().ClearTableFieldsAll(ctx) + t.AssertNil(err) + + ctx = context.Background() + sqlArray, err = gdb.CatchSQL(ctx, func(ctx context.Context) error { + one, err := db.Model(table).Ctx(ctx).One() + t.Assert(len(one), 5) + return err + }) + t.AssertNil(err) + t.Assert(gstr.Contains(gstr.Join(sqlArray, "|"), showTableKey), true) + }) +} + +// https://github.com/gogf/gf/issues/2552 +func Test_Issue2552_ClearTableFields(t *testing.T) { + table := createTable() + defer dropTable(table) + + // MariaDB driver queries table fields via information_schema.COLUMNS + // instead of MySQL's "SHOW FULL COLUMNS FROM". + showTableKey := `information_schema.COLUMNS` + gtest.C(t, func(t *gtest.T) { + ctx := context.Background() + sqlArray, err := gdb.CatchSQL(ctx, func(ctx context.Context) error { + _, err := db.Model(table).Ctx(ctx).Insert(g.Map{ + "passport": guid.S(), + "password": guid.S(), + "nickname": guid.S(), + "create_time": gtime.NewFromStr(CreateTime).String(), + }) + return err + }) + t.AssertNil(err) + t.Assert(gstr.Contains(gstr.Join(sqlArray, "|"), showTableKey), true) + + ctx = context.Background() + sqlArray, err = gdb.CatchSQL(ctx, func(ctx context.Context) error { + one, err := db.Model(table).Ctx(ctx).One() + t.Assert(len(one), 6) + return err + }) + t.AssertNil(err) + t.Assert(gstr.Contains(gstr.Join(sqlArray, "|"), showTableKey), false) + + _, err = db.Exec(ctx, fmt.Sprintf("alter table %s drop column `nickname`", table)) + t.AssertNil(err) + + err = db.GetCore().ClearTableFields(ctx, table) + t.AssertNil(err) + + ctx = context.Background() + sqlArray, err = gdb.CatchSQL(ctx, func(ctx context.Context) error { + one, err := db.Model(table).Ctx(ctx).One() + t.Assert(len(one), 5) + return err + }) + t.AssertNil(err) + t.Assert(gstr.Contains(gstr.Join(sqlArray, "|"), showTableKey), true) + }) +} + +// https://github.com/gogf/gf/issues/2643 +func Test_Issue2643(t *testing.T) { + table := "issue2643" + array := gstr.SplitAndTrim(gtest.DataContent(`issues`, `2643.sql`), ";") + for _, v := range array { + if _, err := db.Exec(ctx, v); err != nil { + gtest.Error(err) + } + } + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + var ( + expectKey1 = "SELECT s.name,replace(concat_ws(',',lpad(s.id, 6, '0'),s.name),',','') `code` FROM `issue2643` AS s" + expectKey2 = "SELECT CASE WHEN dept='物资部' THEN '物资部' ELSE '其他' END dept,sum(s.value) FROM `issue2643` AS s GROUP BY CASE WHEN dept='物资部' THEN '物资部' ELSE '其他' END" + ) + sqlArray, err := gdb.CatchSQL(ctx, func(ctx context.Context) error { + db.Ctx(ctx).Model(table).As("s").Fields( + "s.name", + "replace(concat_ws(',',lpad(s.id, 6, '0'),s.name),',','') `code`", + ).All() + db.Ctx(ctx).Model(table).As("s").Fields( + "CASE WHEN dept='物资部' THEN '物资部' ELSE '其他' END dept", + "sum(s.value)", + ).Group("CASE WHEN dept='物资部' THEN '物资部' ELSE '其他' END").All() + return nil + }) + t.AssertNil(err) + sqlContent := gstr.Join(sqlArray, "\n") + t.Assert(gstr.Contains(sqlContent, expectKey1), true) + t.Assert(gstr.Contains(sqlContent, expectKey2), true) + }) +} + +// https://github.com/gogf/gf/issues/3238 +func Test_Issue3238(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + for i := 0; i < 100; i++ { + _, err := db.Ctx(ctx).Model(table).Hook(gdb.HookHandler{ + Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) { + result, err = in.Next(ctx) + if err != nil { + return + } + var wg sync.WaitGroup + for _, record := range result { + wg.Add(1) + go func(record gdb.Record) { + defer wg.Done() + id, _ := db.Ctx(ctx).Model(table).WherePri(1).Value(`id`) + nickname, _ := db.Ctx(ctx).Model(table).WherePri(1).Value(`nickname`) + t.Assert(id.Int(), 1) + t.Assert(nickname.String(), "name_1") + }(record) + } + wg.Wait() + return + }, + }, + ).All() + t.AssertNil(err) + } + }) +} + +// https://github.com/gogf/gf/issues/3649 +func Test_Issue3649(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + sql, err := gdb.CatchSQL(context.Background(), func(ctx context.Context) (err error) { + user := db.Model(table).Ctx(ctx) + _, err = user.Where("create_time = ?", gdb.Raw("now()")).WhereLT("create_time", gdb.Raw("now()")).Count() + return + }) + t.AssertNil(err) + sqlStr := fmt.Sprintf("SELECT COUNT(1) FROM `%s` WHERE (create_time = now()) AND (`create_time` < now())", table) + t.Assert(sql[0], sqlStr) + }) +} + +// https://github.com/gogf/gf/issues/3754 +func Test_Issue3754(t *testing.T) { + table := "issue3754" + array := gstr.SplitAndTrim(gtest.DataContent(`issues`, `3754.sql`), ";") + for _, v := range array { + if _, err := db.Exec(ctx, v); err != nil { + gtest.Error(err) + } + } + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + fieldsEx := []string{"delete_at", "create_at", "update_at"} + // Insert. + dataInsert := g.Map{ + "id": 1, + "name": "name_1", + } + r, err := db.Model(table).Data(dataInsert).FieldsEx(fieldsEx).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(), "") + t.Assert(oneInsert["update_at"].String(), "") + + // Update. + dataUpdate := g.Map{ + "name": "name_1000", + } + r, err = db.Model(table).Data(dataUpdate).FieldsEx(fieldsEx).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(), "") + t.Assert(oneUpdate["update_at"].String(), "") + + // FieldsEx does not affect Delete operation. + r, err = db.Model(table).FieldsEx(fieldsEx).WherePri(1).Delete() + n, _ = r.RowsAffected() + t.Assert(n, 1) + oneDeleteUnscoped, err := db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(oneDeleteUnscoped["id"].Int(), 1) + t.Assert(oneDeleteUnscoped["name"].String(), "name_1000") + t.AssertNE(oneDeleteUnscoped["delete_at"].String(), "") + t.Assert(oneDeleteUnscoped["create_at"].String(), "") + t.Assert(oneDeleteUnscoped["update_at"].String(), "") + }) +} + +// https://github.com/gogf/gf/issues/3626 +func Test_Issue3626(t *testing.T) { + table := "issue3626" + array := gstr.SplitAndTrim(gtest.DataContent(`issues`, `3626.sql`), ";") + defer dropTable(table) + for _, v := range array { + if _, err := db.Exec(ctx, v); err != nil { + gtest.Error(err) + } + } + + // 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) + + oneInsert, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(oneInsert["id"].Int(), 1) + t.Assert(oneInsert["name"].String(), "name_1") + }) + + var ( + cacheKey = guid.S() + cacheFunc = func(duration time.Duration) gdb.HookHandler { + return gdb.HookHandler{ + Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) { + get, err := db.GetCache().Get(ctx, cacheKey) + if err == nil && !get.IsEmpty() { + err = get.Scan(&result) + if err == nil { + return result, nil + } + } + result, err = in.Next(ctx) + if err != nil { + return nil, err + } + if result == nil || result.Len() < 1 { + result = make(gdb.Result, 0) + } + _ = db.GetCache().Set(ctx, cacheKey, result, duration) + return + }, + } + } + ) + gtest.C(t, func(t *gtest.T) { + defer db.GetCache().Clear(ctx) + count, err := db.Model(table).Count() + t.AssertNil(err) + t.Assert(count, 1) + count, err = db.Model(table).Hook(cacheFunc(time.Hour)).Count() + t.AssertNil(err) + t.Assert(count, 1) + count, err = db.Model(table).Hook(cacheFunc(time.Hour)).Count() + t.AssertNil(err) + t.Assert(count, 1) + }) +} + +// https://github.com/gogf/gf/issues/3932 +func Test_Issue3932(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + one, err := db.Model(table).Order("id", "desc").One() + t.AssertNil(err) + t.Assert(one["id"], 10) + }) + gtest.C(t, func(t *gtest.T) { + one, err := db.Model(table).Order("id desc").One() + t.AssertNil(err) + t.Assert(one["id"], 10) + }) + gtest.C(t, func(t *gtest.T) { + one, err := db.Model(table).Order("id desc, nickname asc").One() + t.AssertNil(err) + t.Assert(one["id"], 10) + }) + gtest.C(t, func(t *gtest.T) { + one, err := db.Model(table).Order("id desc", "nickname asc").One() + t.AssertNil(err) + t.Assert(one["id"], 10) + }) + gtest.C(t, func(t *gtest.T) { + one, err := db.Model(table).Order("id desc").Order("nickname asc").One() + t.AssertNil(err) + t.Assert(one["id"], 10) + }) +} + +// https://github.com/gogf/gf/issues/3968 +func Test_Issue3968(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + var hook = gdb.HookHandler{ + Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) { + result, err = in.Next(ctx) + if err != nil { + return nil, err + } + if result != nil { + for i := range result { + result[i]["location"] = gvar.New("ny") + } + } + return + }, + } + var ( + count int + result gdb.Result + ) + err := db.Model(table).Hook(hook).ScanAndCount(&result, &count, false) + t.AssertNil(err) + t.Assert(count, 10) + t.Assert(len(result), 10) + }) +} + +// https://github.com/gogf/gf/issues/3915 +func Test_Issue3915(t *testing.T) { + table := "issue3915" + array := gstr.SplitAndTrim(gtest.DataContent(`issues`, `3915.sql`), ";") + for _, v := range array { + if _, err := db.Exec(ctx, v); err != nil { + gtest.Error(err) + } + } + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + //db.SetDebug(true) + all, err := db.Model(table).Where("a < b").All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 1) + + all, err = db.Model(table).Where(gdb.Raw("a < b")).All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 1) + + all, err = db.Model(table).WhereLT("a", gdb.Raw("`b`")).All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 1) + }) + + gtest.C(t, func(t *gtest.T) { + //db.SetDebug(true) + all, err := db.Model(table).Where("a > b").All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 2) + + all, err = db.Model(table).Where(gdb.Raw("a > b")).All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 2) + + all, err = db.Model(table).WhereGT("a", gdb.Raw("`b`")).All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 2) + }) +} + +type RoleBase struct { + gmeta.Meta `orm:"table:sys_role"` + Name string `json:"name" description:"角色名称" ` + Code string `json:"code" description:"角色 code" ` + Description string `json:"description" description:"描述信息" ` + Weight int `json:"weight" description:"排序" ` + StatusId int `json:"statusId" description:"发布状态" ` + CreatedAt *gtime.Time `json:"createdAt" description:"" ` + UpdatedAt *gtime.Time `json:"updatedAt" description:"" ` +} + +type Role struct { + gmeta.Meta `orm:"table:sys_role"` + RoleBase + Id uint `json:"id" description:""` + Status *Status `json:"status" description:"发布状态" orm:"with:id=status_id" ` +} + +type StatusBase struct { + gmeta.Meta `orm:"table:sys_status"` + En string `json:"en" description:"英文名称" ` + Cn string `json:"cn" description:"中文名称" ` + Weight int `json:"weight" description:"排序权重" ` +} + +type Status struct { + gmeta.Meta `orm:"table:sys_status"` + StatusBase + Id uint `json:"id" description:""` +} + +// https://github.com/gogf/gf/issues/2119 +func Test_Issue2119(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + tables := []string{ + "sys_role", + "sys_status", + } + + defer dropTable(tables[0]) + defer dropTable(tables[1]) + _ = tables + array := gstr.SplitAndTrim(gtest.DataContent(`issues`, `2119.sql`), ";") + for _, v := range array { + _, err := db.Exec(ctx, v) + t.AssertNil(err) + } + roles := make([]*Role, 0) + err := db.Ctx(context.Background()).Model(&Role{}).WithAll().Scan(&roles) + t.AssertNil(err) + expectStatus := []*Status{ + { + StatusBase: StatusBase{ + En: "undecided", + Cn: "未决定", + Weight: 800, + }, + Id: 2, + }, + { + StatusBase: StatusBase{ + En: "on line", + Cn: "上线", + Weight: 900, + }, + Id: 1, + }, + { + StatusBase: StatusBase{ + En: "on line", + Cn: "上线", + Weight: 900, + }, + Id: 1, + }, + { + StatusBase: StatusBase{ + En: "on line", + Cn: "上线", + Weight: 900, + }, + Id: 1, + }, + { + StatusBase: StatusBase{ + En: "on line", + Cn: "上线", + Weight: 900, + }, + Id: 1, + }, + } + + for i := 0; i < len(roles); i++ { + t.Assert(roles[i].Status, expectStatus[i]) + } + }) +} + +// https://github.com/gogf/gf/issues/4034 +func Test_Issue4034(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + table := "issue4034" + array := gstr.SplitAndTrim(gtest.DataContent(`issues`, `4034.sql`), ";") + for _, v := range array { + _, err := db.Exec(ctx, v) + t.AssertNil(err) + } + defer dropTable(table) + + err := issue4034SaveDeviceAndToken(ctx, table) + t.AssertNil(err) + }) +} + +func issue4034SaveDeviceAndToken(ctx context.Context, table string) error { + return db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + if err := issue4034SaveAppDevice(ctx, table, tx); err != nil { + return err + } + return nil + }) +} + +func issue4034SaveAppDevice(ctx context.Context, table string, tx gdb.TX) error { + _, err := db.Model(table).Safe().Ctx(ctx).TX(tx).Data(g.Map{ + "passport": "111", + "password": "222", + "nickname": "333", + }).Save() + return err +} + +// https://github.com/gogf/gf/issues/4086 +func Test_Issue4086(t *testing.T) { + table := "issue4086" + defer dropTable(table) + array := gstr.SplitAndTrim(gtest.DataContent(`issues`, `4086.sql`), ";") + for _, v := range array { + _, err := db.Exec(ctx, v) + gtest.AssertNil(err) + } + + gtest.C(t, func(t *gtest.T) { + type ProxyParam struct { + ProxyId int64 `json:"proxyId" orm:"proxy_id"` + RecommendIds []int64 `json:"recommendIds" orm:"recommend_ids"` + Photos []int64 `json:"photos" orm:"photos"` + } + + var proxyParamList []*ProxyParam + err := db.Model(table).Ctx(ctx).Scan(&proxyParamList) + t.AssertNil(err) + t.Assert(len(proxyParamList), 2) + t.Assert(proxyParamList, []*ProxyParam{ + { + ProxyId: 1, + RecommendIds: []int64{584, 585}, + Photos: nil, + }, + { + ProxyId: 2, + RecommendIds: []int64{}, + Photos: nil, + }, + }) + }) + + gtest.C(t, func(t *gtest.T) { + type ProxyParam struct { + ProxyId int64 `json:"proxyId" orm:"proxy_id"` + RecommendIds []int64 `json:"recommendIds" orm:"recommend_ids"` + Photos []float32 `json:"photos" orm:"photos"` + } + + var proxyParamList []*ProxyParam + err := db.Model(table).Ctx(ctx).Scan(&proxyParamList) + t.AssertNil(err) + t.Assert(len(proxyParamList), 2) + t.Assert(proxyParamList, []*ProxyParam{ + { + ProxyId: 1, + RecommendIds: []int64{584, 585}, + Photos: nil, + }, + { + ProxyId: 2, + RecommendIds: []int64{}, + Photos: nil, + }, + }) + }) + + gtest.C(t, func(t *gtest.T) { + type ProxyParam struct { + ProxyId int64 `json:"proxyId" orm:"proxy_id"` + RecommendIds []int64 `json:"recommendIds" orm:"recommend_ids"` + Photos []string `json:"photos" orm:"photos"` + } + + var proxyParamList []*ProxyParam + err := db.Model(table).Ctx(ctx).Scan(&proxyParamList) + t.AssertNil(err) + t.Assert(len(proxyParamList), 2) + t.Assert(proxyParamList, []*ProxyParam{ + { + ProxyId: 1, + RecommendIds: []int64{584, 585}, + Photos: nil, + }, + { + ProxyId: 2, + RecommendIds: []int64{}, + Photos: nil, + }, + }) + }) + + gtest.C(t, func(t *gtest.T) { + type ProxyParam struct { + ProxyId int64 `json:"proxyId" orm:"proxy_id"` + RecommendIds []int64 `json:"recommendIds" orm:"recommend_ids"` + Photos []any `json:"photos" orm:"photos"` + } + + var proxyParamList []*ProxyParam + err := db.Model(table).Ctx(ctx).Scan(&proxyParamList) + t.AssertNil(err) + t.Assert(len(proxyParamList), 2) + t.Assert(proxyParamList, []*ProxyParam{ + { + ProxyId: 1, + RecommendIds: []int64{584, 585}, + Photos: nil, + }, + { + ProxyId: 2, + RecommendIds: []int64{}, + Photos: nil, + }, + }) + }) + gtest.C(t, func(t *gtest.T) { + type ProxyParam struct { + ProxyId int64 `json:"proxyId" orm:"proxy_id"` + RecommendIds []int64 `json:"recommendIds" orm:"recommend_ids"` + Photos string `json:"photos" orm:"photos"` + } + + var proxyParamList []*ProxyParam + err := db.Model(table).Ctx(ctx).Scan(&proxyParamList) + t.AssertNil(err) + t.Assert(len(proxyParamList), 2) + t.Assert(proxyParamList, []*ProxyParam{ + { + ProxyId: 1, + RecommendIds: []int64{584, 585}, + Photos: "null", + }, + { + ProxyId: 2, + RecommendIds: []int64{}, + Photos: "", + }, + }) + }) + gtest.C(t, func(t *gtest.T) { + type ProxyParam struct { + ProxyId int64 `json:"proxyId" orm:"proxy_id"` + RecommendIds string `json:"recommendIds" orm:"recommend_ids"` + Photos json.RawMessage `json:"photos" orm:"photos"` + } + + var proxyParamList []*ProxyParam + err := db.Model(table).Ctx(ctx).Scan(&proxyParamList) + t.AssertNil(err) + t.Assert(len(proxyParamList), 2) + t.Assert(proxyParamList, []*ProxyParam{ + { + ProxyId: 1, + RecommendIds: "[584, 585]", + Photos: json.RawMessage("null"), + }, + { + ProxyId: 2, + RecommendIds: "[]", + Photos: json.RawMessage("null"), + }, + }) + }) +} + +// https://github.com/gogf/gf/issues/4500 +// Raw() Count ignores Where condition +func Test_Issue4500(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + // Test 1: Raw SQL with WHERE + external Where condition + Count + // This tests that formatCondition correctly uses AND when Raw SQL already has WHERE + gtest.C(t, func(t *gtest.T) { + count, err := db. + Raw(fmt.Sprintf("SELECT * FROM %s WHERE id IN (?)", table), g.Slice{1, 5, 7, 8, 9, 10}). + WhereLT("id", 8). + Count() + t.AssertNil(err) + // Raw SQL: id IN (1,5,7,8,9,10) = 6 records + // Where: id < 8 filters to {1,5,7} = 3 records + t.Assert(count, 3) + }) + + // Test 2: Raw SQL without WHERE + external Where condition + Count + // This tests that formatCondition correctly adds WHERE + gtest.C(t, func(t *gtest.T) { + count, err := db. + Raw(fmt.Sprintf("SELECT * FROM %s", table)). + WhereLT("id", 5). + Count() + t.AssertNil(err) + // Raw SQL: all 10 records + // Where: id < 5 = {1,2,3,4} = 4 records + t.Assert(count, 4) + }) + + // Test 3: Raw + Where + ScanAndCount + gtest.C(t, func(t *gtest.T) { + type User struct { + Id int + Passport string + } + var users []User + var total int + err := db. + Raw(fmt.Sprintf("SELECT * FROM %s WHERE id IN (?)", table), g.Slice{1, 5, 7, 8, 9, 10}). + WhereLT("id", 8). + ScanAndCount(&users, &total, false) + t.AssertNil(err) + // Both scan result and count should respect Where condition + t.Assert(len(users), 3) + t.Assert(total, 3) + }) + + // Test 4: Raw + multiple Where conditions + Count + gtest.C(t, func(t *gtest.T) { + count, err := db. + Raw(fmt.Sprintf("SELECT * FROM %s WHERE id > ?", table), 0). + WhereLT("id", 5). + WhereGTE("id", 2). + Count() + t.AssertNil(err) + // Raw: id > 0 (all 10 records) + // Where: id < 5 AND id >= 2 = {2, 3, 4} = 3 records + t.Assert(count, 3) + }) + + // Test 5: Raw SQL with no external Where + Count (baseline test) + gtest.C(t, func(t *gtest.T) { + count, err := db. + Raw(fmt.Sprintf("SELECT * FROM %s WHERE id IN (?)", table), g.Slice{1, 2, 3}). + Count() + t.AssertNil(err) + // Should count 3 records + t.Assert(count, 3) + }) + + // Test 6: Verify All() still works correctly with Raw + Where + gtest.C(t, func(t *gtest.T) { + all, err := db. + Raw(fmt.Sprintf("SELECT * FROM %s WHERE id IN (?)", table), g.Slice{1, 5, 7, 8, 9, 10}). + WhereLT("id", 8). + All() + t.AssertNil(err) + t.Assert(len(all), 3) + }) +} + +// https://github.com/gogf/gf/issues/4697 +func Test_Issue4697(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Fields("") should be treated as Fields() and select all fields + result, err := db.Model(table).Fields("").Limit(1).All() + t.AssertNil(err) + t.AssertGT(len(result), 0) + // Should have all fields (id, passport, password, nickname, create_time, create_date) + t.Assert(len(result[0]), 6) + }) + + gtest.C(t, func(t *gtest.T) { + // Fields("", "id") should ignore empty string and only select "id" + result, err := db.Model(table).Fields("", "id").Limit(1).All() + t.AssertNil(err) + t.AssertGT(len(result), 0) + t.Assert(len(result[0]), 1) + t.AssertNE(result[0]["id"], nil) + }) + + gtest.C(t, func(t *gtest.T) { + // Fields("id", "", "nickname") should ignore empty string + result, err := db.Model(table).Fields("id", "", "nickname").Limit(1).All() + t.AssertNil(err) + t.AssertGT(len(result), 0) + t.Assert(len(result[0]), 2) + t.AssertNE(result[0]["id"], nil) + t.AssertNE(result[0]["nickname"], nil) + }) +} + +// https://github.com/gogf/gf/issues/4698 +func Test_Issue4698(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + // Test 1: AllAndCount with multiple fields should generate valid COUNT SQL + gtest.C(t, func(t *gtest.T) { + result, count, err := db.Model(table).Fields("id, nickname").AllAndCount(true) + t.AssertNil(err) + t.Assert(count, TableSize) + t.Assert(len(result), TableSize) + t.AssertNE(result[0]["id"], nil) + t.AssertNE(result[0]["nickname"], nil) + t.Assert(result[0]["passport"], nil) + }) + + // Test 2: AllAndCount(false) with multiple fields + gtest.C(t, func(t *gtest.T) { + result, count, err := db.Model(table).Fields("id, nickname").AllAndCount(false) + t.AssertNil(err) + t.Assert(count, TableSize) + t.Assert(len(result), TableSize) + }) + + // Test 3: ScanAndCount with multiple fields + gtest.C(t, func(t *gtest.T) { + type User struct { + Id int + Nickname string + } + var users []User + var total int + err := db.Model(table).Fields("id, nickname").ScanAndCount(&users, &total, true) + t.AssertNil(err) + t.Assert(total, TableSize) + t.Assert(len(users), TableSize) + t.AssertGT(users[0].Id, 0) + t.AssertNE(users[0].Nickname, "") + }) + + // Test 4: AllAndCount with single field and useFieldForCount=true + gtest.C(t, func(t *gtest.T) { + result, count, err := db.Model(table).Fields("id").AllAndCount(true) + t.AssertNil(err) + t.Assert(count, TableSize) + t.Assert(len(result), TableSize) + t.Assert(len(result[0]), 1) + }) + + // Test 5: AllAndCount with Where condition + gtest.C(t, func(t *gtest.T) { + result, count, err := db.Model(table).Fields("id, nickname").Where("id", 5).AllAndCount(true) + t.AssertNil(err) + t.Assert(count, 4) + t.Assert(len(result), 4) + }) + + // Test 6: Distinct + AllAndCount(false) should use COUNT(1), not COUNT(DISTINCT 1) + gtest.C(t, func(t *gtest.T) { + result, count, err := db.Model(table).Fields("nickname").Distinct().AllAndCount(false) + t.AssertNil(err) + // COUNT(1) should return total rows, not distinct count + t.Assert(count, TableSize) + t.AssertGT(len(result), 0) + }) + + // Test 7: Distinct + AllAndCount(true) with single field should use COUNT(DISTINCT nickname) + gtest.C(t, func(t *gtest.T) { + _, count, err := db.Model(table).Fields("nickname").Distinct().AllAndCount(true) + t.AssertNil(err) + // COUNT(DISTINCT nickname) should return distinct count + t.Assert(count, TableSize) + }) + + // Test 8: Distinct + multiple fields + AllAndCount(true) should fallback to COUNT(1) + gtest.C(t, func(t *gtest.T) { + result, count, err := db.Model(table).Fields("id, nickname").Distinct().AllAndCount(true) + t.AssertNil(err) + t.Assert(count, TableSize) + t.Assert(len(result), TableSize) + }) +} diff --git a/contrib/drivers/mariadb/testdata/issues/1380.sql b/contrib/drivers/mariadb/testdata/issues/1380.sql new file mode 100644 index 000000000..59aefb551 --- /dev/null +++ b/contrib/drivers/mariadb/testdata/issues/1380.sql @@ -0,0 +1,35 @@ +CREATE TABLE `jfy_gift` ( +`id` int(0) UNSIGNED NOT NULL AUTO_INCREMENT, +`gift_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '礼品名称', +`at_least_recharge_count` int(0) UNSIGNED NOT NULL DEFAULT 1 COMMENT '最少兑换数量', +`comments` json NOT NULL COMMENT '礼品留言', +`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '礼品详情', +`cost_price` decimal(10, 2) NULL DEFAULT NULL COMMENT '成本价', +`cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '封面', +`covers` json NOT NULL COMMENT '礼品图片库', +`description` varchar(62) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '礼品备注', +`express_type` json NOT NULL COMMENT '配送方式', +`gift_type` int(0) NOT NULL COMMENT '礼品类型:1:实物;2:虚拟;3:优惠券;4:积分券', +`has_props` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否有多个属性', +`is_limit_sell` tinyint(0) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否限购', +`limit_customer_tags` json NOT NULL COMMENT '语序购买的会员标签', +`limit_sell_custom` tinyint(0) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否开启允许购买的会员标签', +`limit_sell_cycle` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '限购周期', +`limit_sell_cycle_count` int(0) NOT NULL COMMENT '限购期内允许购买的数量', +`limit_sell_type` tinyint(0) NOT NULL COMMENT '限购类型', +`market_price` decimal(10, 2) NOT NULL COMMENT '市场价', +`out_sn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '内部编码', +`props` json NOT NULL COMMENT '规格', +`skus` json NOT NULL COMMENT 'SKU', +`score_price` decimal(10, 2) NOT NULL COMMENT '兑换所需积分', +`stock` int(0) NOT NULL COMMENT '库存', +`create_at` datetime(0) NOT NULL COMMENT '创建日期', +`store_id` int(0) NOT NULL COMMENT '所属商城', +`status` int(0) UNSIGNED NULL DEFAULT 1 COMMENT '1:下架;20:审核中;30:复审中;99:上架', +`view_count` int(0) NOT NULL DEFAULT 0 COMMENT '访问量', +`sell_count` int(0) NULL DEFAULT 0 COMMENT '销量', +PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + + +INSERT INTO `jfy_gift` VALUES (17, 'GIFT', 1, '[{\"name\": \"身份证\", \"field\": \"idcard\", \"required\": false}, {\"name\": \"留言2\", \"field\": \"text\", \"required\": false}]', '礼品详情
', 0.00, '', '{\"list\": [{\"uid\": \"vc-upload-1629292486099-3\", \"url\": \"https://cdn.taobao.com/sULsYiwaOPjsKGoBXwKtuewPzACpBDfQ.jpg\", \"name\": \"O1CN01OH6PIP1Oc5ot06U17_!!922361725.jpg\", \"status\": \"done\"}, {\"uid\": \"vc-upload-1629292486099-4\", \"url\": \"https://cdn.taobao.com/lqLHDcrFTgNvlWyXfLYZwmsrODzIBtFH.jpg\", \"name\": \"O1CN018hBckI1Oc5ouc8ppl_!!922361725.jpg\", \"status\": \"done\"}, {\"uid\": \"vc-upload-1629292486099-5\", \"url\": \"https://cdn.taobao.com/pvqyutXckICmHhbPBQtrVLHuMlXuGxUg.jpg\", \"name\": \"O1CN0185Ubp91Oc5osQTTcc_!!922361725.jpg\", \"status\": \"done\"}]}', '支持个性定制的父亲节老师长辈的专属礼物', '[\"快递包邮\", \"同城配送\"]', 1, 0, 0, '[]', 0, 'day', 0, 1, 0.00, '259402', '[{\"name\": \"颜色\", \"values\": [\"红色\", \"蓝色\"]}]', '[{\"name\": \"red\", \"stock\": 10, \"gift_id\": 1, \"cost_price\": 80, \"score_price\": 188, \"market_price\": 388}, {\"name\": \"blue\", \"stock\": 100, \"gift_id\": 2, \"cost_price\": 81, \"score_price\": 200, \"market_price\": 288}]', 10.00, 0, '2021-08-18 21:26:13', 100004, 99, 0, 0); diff --git a/contrib/drivers/mariadb/testdata/issues/1401.sql b/contrib/drivers/mariadb/testdata/issues/1401.sql new file mode 100644 index 000000000..b09087326 --- /dev/null +++ b/contrib/drivers/mariadb/testdata/issues/1401.sql @@ -0,0 +1,32 @@ +-- ---------------------------- +-- Table structure for parcel_items +-- ---------------------------- +DROP TABLE IF EXISTS `parcel_items`; +CREATE TABLE `parcel_items` ( + `id` int(11) NOT NULL, + `parcel_id` int(11) NULL DEFAULT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of parcel_items +-- ---------------------------- +INSERT INTO `parcel_items` VALUES (1, 1, '新品'); +INSERT INTO `parcel_items` VALUES (2, 3, '新品2'); + +-- ---------------------------- +-- Table structure for parcels +-- ---------------------------- +DROP TABLE IF EXISTS `parcels`; +CREATE TABLE `parcels` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of parcels +-- ---------------------------- +INSERT INTO `parcels` VALUES (1); +INSERT INTO `parcels` VALUES (2); +INSERT INTO `parcels` VALUES (3); \ No newline at end of file diff --git a/contrib/drivers/mariadb/testdata/issues/1412.sql b/contrib/drivers/mariadb/testdata/issues/1412.sql new file mode 100644 index 000000000..453fca783 --- /dev/null +++ b/contrib/drivers/mariadb/testdata/issues/1412.sql @@ -0,0 +1,30 @@ +-- ---------------------------- +-- Table structure for items +-- ---------------------------- +CREATE TABLE `items` ( + `id` int(11) NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of items +-- ---------------------------- +INSERT INTO `items` VALUES (1, '金秋产品1'); +INSERT INTO `items` VALUES (2, '金秋产品2'); + +-- ---------------------------- +-- Table structure for parcels +-- ---------------------------- +CREATE TABLE `parcels` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `item_id` int(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of parcels +-- ---------------------------- +INSERT INTO `parcels` VALUES (1, 1); +INSERT INTO `parcels` VALUES (2, 2); +INSERT INTO `parcels` VALUES (3, 0); \ No newline at end of file diff --git a/contrib/drivers/mariadb/testdata/issues/2105.sql b/contrib/drivers/mariadb/testdata/issues/2105.sql new file mode 100644 index 000000000..55f09b8b0 --- /dev/null +++ b/contrib/drivers/mariadb/testdata/issues/2105.sql @@ -0,0 +1,9 @@ +CREATE TABLE `issue2105` ( + `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin, + PRIMARY KEY (`id`) +) ENGINE=InnoDB; + + +INSERT INTO `issue2105` VALUES ('1', NULL); +INSERT INTO `issue2105` VALUES ('2', '[{\"Name\": \"任务类型\", \"Value\": \"高价值\"}, {\"Name\": \"优先级\", \"Value\": \"高\"}, {\"Name\": \"是否亮点功能\", \"Value\": \"是\"}]'); diff --git a/contrib/drivers/mariadb/testdata/issues/2119.sql b/contrib/drivers/mariadb/testdata/issues/2119.sql new file mode 100644 index 000000000..89da7d17a --- /dev/null +++ b/contrib/drivers/mariadb/testdata/issues/2119.sql @@ -0,0 +1,47 @@ +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for sys_role +-- ---------------------------- +DROP TABLE IF EXISTS `sys_role`; +CREATE TABLE `sys_role` ( + `id` int(0) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '||s', + `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '角色名称||s,r', + `code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '角色 code||s,r', + `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '描述信息|text', + `weight` int(0) UNSIGNED NOT NULL DEFAULT 0 COMMENT '排序||r|min:0#发布状态不能小于 0', + `status_id` int(0) UNSIGNED NOT NULL DEFAULT 1 COMMENT '发布状态|hasOne|f:status,fk:id', + `created_at` datetime(0) NULL DEFAULT NULL, + `updated_at` datetime(0) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `code`(`code`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1091 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统角色表' ROW_FORMAT = Compact; + +-- ---------------------------- +-- Records of sys_role +-- ---------------------------- +INSERT INTO `sys_role` VALUES (1, '开发人员', 'developer', '123123', 900, 2, '2022-09-03 21:25:03', '2022-09-09 23:35:23'); +INSERT INTO `sys_role` VALUES (2, '管理员', 'admin', '', 800, 1, '2022-09-03 21:25:03', '2022-09-09 23:00:17'); +INSERT INTO `sys_role` VALUES (3, '运营', 'operator', '', 700, 1, '2022-09-03 21:25:03', '2022-09-03 21:25:03'); +INSERT INTO `sys_role` VALUES (4, '客服', 'service', '', 600, 1, '2022-09-03 21:25:03', '2022-09-03 21:25:03'); +INSERT INTO `sys_role` VALUES (5, '收银', 'account', '', 500, 1, '2022-09-03 21:25:03', '2022-09-03 21:25:03'); + +-- ---------------------------- +-- Table structure for sys_status +-- ---------------------------- +DROP TABLE IF EXISTS `sys_status`; +CREATE TABLE `sys_status` ( + `id` int(0) UNSIGNED NOT NULL AUTO_INCREMENT, + `en` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '英文名称', + `cn` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '中文名称', + `weight` int(0) UNSIGNED NOT NULL DEFAULT 0 COMMENT '排序权重', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '发布状态' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sys_status +-- ---------------------------- +INSERT INTO `sys_status` VALUES (1, 'on line', '上线', 900); +INSERT INTO `sys_status` VALUES (2, 'undecided', '未决定', 800); +INSERT INTO `sys_status` VALUES (3, 'off line', '下线', 700); \ No newline at end of file diff --git a/contrib/drivers/mariadb/testdata/issues/2439.sql b/contrib/drivers/mariadb/testdata/issues/2439.sql new file mode 100644 index 000000000..0949263cd --- /dev/null +++ b/contrib/drivers/mariadb/testdata/issues/2439.sql @@ -0,0 +1,19 @@ +CREATE TABLE `a` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + PRIMARY KEY (id) USING BTREE +) ENGINE = InnoDB; +INSERT INTO `a` (`id`) VALUES ('2'); + +CREATE TABLE `b` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL , + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB; +INSERT INTO `b` (`id`, `name`) VALUES ('2', 'a'); +INSERT INTO `b` (`id`, `name`) VALUES ('3', 'b'); + +CREATE TABLE `c` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB; +INSERT INTO `c` (`id`) VALUES ('2'); \ No newline at end of file diff --git a/contrib/drivers/mariadb/testdata/issues/2643.sql b/contrib/drivers/mariadb/testdata/issues/2643.sql new file mode 100644 index 000000000..e145460a1 --- /dev/null +++ b/contrib/drivers/mariadb/testdata/issues/2643.sql @@ -0,0 +1,7 @@ +CREATE TABLE `issue2643` ( + `id` INT(10) NULL DEFAULT NULL, + `name` VARCHAR(50) NULL DEFAULT NULL, + `value` INT(10) NULL DEFAULT NULL, + `dept` VARCHAR(50) NULL DEFAULT NULL +) +ENGINE=InnoDB \ No newline at end of file diff --git a/contrib/drivers/mariadb/testdata/issues/3086.sql b/contrib/drivers/mariadb/testdata/issues/3086.sql new file mode 100644 index 000000000..329ca847c --- /dev/null +++ b/contrib/drivers/mariadb/testdata/issues/3086.sql @@ -0,0 +1,10 @@ +CREATE TABLE `issue3086_user` +( + `id` int(10) unsigned NOT NULL COMMENT 'User ID', + `passport` varchar(45) NOT NULL COMMENT 'User Passport', + `password` varchar(45) DEFAULT NULL COMMENT 'User Password', + `nickname` varchar(45) DEFAULT NULL COMMENT 'User Nickname', + `create_at` datetime DEFAULT NULL COMMENT 'Created Time', + `update_at` datetime DEFAULT NULL COMMENT 'Updated Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/contrib/drivers/mariadb/testdata/issues/3218.sql b/contrib/drivers/mariadb/testdata/issues/3218.sql new file mode 100644 index 000000000..93b5f9dac --- /dev/null +++ b/contrib/drivers/mariadb/testdata/issues/3218.sql @@ -0,0 +1,14 @@ +CREATE TABLE `issue3218_sys_config` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '配置名称', + `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '配置值', + `created_at` timestamp NULL DEFAULT NULL COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `name`(`name`(191)) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci; + +-- ---------------------------- +-- Records of sys_config +-- ---------------------------- +INSERT INTO `issue3218_sys_config` VALUES (49, 'site', '{\"banned_ip\":\"22\",\"filings\":\"2222\",\"fixed_page\":\"\",\"site_name\":\"22\",\"version\":\"22\"}', '2023-12-19 14:08:25', '2023-12-19 14:08:25'); \ No newline at end of file diff --git a/contrib/drivers/mariadb/testdata/issues/3626.sql b/contrib/drivers/mariadb/testdata/issues/3626.sql new file mode 100644 index 000000000..e4ee53023 --- /dev/null +++ b/contrib/drivers/mariadb/testdata/issues/3626.sql @@ -0,0 +1,5 @@ +CREATE TABLE `issue3626` ( + id int(11) NOT NULL, + name varchar(45) DEFAULT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/contrib/drivers/mariadb/testdata/issues/3754.sql b/contrib/drivers/mariadb/testdata/issues/3754.sql new file mode 100644 index 000000000..e6cdac030 --- /dev/null +++ b/contrib/drivers/mariadb/testdata/issues/3754.sql @@ -0,0 +1,8 @@ +CREATE TABLE `issue3754` ( + id int(11) NOT NULL, + name varchar(45) DEFAULT NULL, + create_at datetime(0) DEFAULT NULL, + update_at datetime(0) DEFAULT NULL, + delete_at datetime(0) DEFAULT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/contrib/drivers/mariadb/testdata/issues/3915.sql b/contrib/drivers/mariadb/testdata/issues/3915.sql new file mode 100644 index 000000000..6fa6b86c8 --- /dev/null +++ b/contrib/drivers/mariadb/testdata/issues/3915.sql @@ -0,0 +1,9 @@ +CREATE TABLE `issue3915` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'user id', + `a` float DEFAULT NULL COMMENT 'user name', + `b` float DEFAULT NULL COMMENT 'user status', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO `issue3915` (`id`,`a`,`b`) VALUES (1,1,2); +INSERT INTO `issue3915` (`id`,`a`,`b`) VALUES (2,5,4); \ No newline at end of file diff --git a/contrib/drivers/mariadb/testdata/issues/4034.sql b/contrib/drivers/mariadb/testdata/issues/4034.sql new file mode 100644 index 000000000..abb99cedc --- /dev/null +++ b/contrib/drivers/mariadb/testdata/issues/4034.sql @@ -0,0 +1,8 @@ +CREATE TABLE issue4034 ( + id INT PRIMARY KEY AUTO_INCREMENT, + passport VARCHAR(255), + password VARCHAR(255), + nickname VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/contrib/drivers/mariadb/testdata/issues/4086.sql b/contrib/drivers/mariadb/testdata/issues/4086.sql new file mode 100644 index 000000000..5e7ba66e1 --- /dev/null +++ b/contrib/drivers/mariadb/testdata/issues/4086.sql @@ -0,0 +1,10 @@ +DROP TABLE IF EXISTS `issue4086`; +CREATE TABLE `issue4086` ( + `proxy_id` bigint NOT NULL, + `recommend_ids` json DEFAULT NULL, + `photos` json DEFAULT NULL, + PRIMARY KEY (`proxy_id`) +) ENGINE=InnoDB; + +INSERT INTO `issue4086` (`proxy_id`, `recommend_ids`, `photos`) VALUES (1, '[584, 585]', 'null'); +INSERT INTO `issue4086` (`proxy_id`, `recommend_ids`, `photos`) VALUES (2, '[]', NULL); \ No newline at end of file