test(contrib/drivers/mariadb): add layer 3 features and issue regression tests (#4724)

## Summary

- Port 5 feature tests: duplicate-key handling
(OnDuplicate/OnDuplicateEx/Save), JSON field operations, row-level
locking (Lock/LockUpdate/LockShared with transactions), master-slave
configuration, table metadata inspection
- Port 1 partition test: RANGE partitioning with Partition() clause
(adapted from MySQL baseline)
- Port 30 issue regression tests from MySQL baseline
- Includes 14 testdata SQL files for issue-specific table schemas

Layer 3 tests cover MariaDB-specific adaptations where needed (e.g.,
SKIP LOCKED requires MariaDB 10.6+ — commented out for compatibility,
LOCK IN SHARE MODE instead of FOR SHARE for older versions).

All tests are structurally aligned with the MySQL driver baseline.
Package and import references are adapted for MariaDB.

ref #4689

Co-authored-by: John Guo <claymore1986@gmail.com>
This commit is contained in:
Jack Ling
2026-04-10 09:38:43 +08:00
committed by GitHub
parent bb71ccfd4c
commit 1878202625
21 changed files with 4064 additions and 0 deletions

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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`")
})
}

View File

@ -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
})
}

File diff suppressed because it is too large Load Diff

View File

@ -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}]', '<p>礼品详情</p>', 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);

View File

@ -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);

View File

@ -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);

View File

@ -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\": \"是\"}]');

View File

@ -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);

View File

@ -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');

View File

@ -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

View File

@ -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;

View File

@ -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');

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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
);

View File

@ -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);