2026-01-22 09:32:01 +08:00
|
|
|
package db
|
|
|
|
|
|
|
|
|
|
import (
|
2026-01-23 01:51:35 +08:00
|
|
|
. "code.hoteas.com/golang/hotime/common"
|
2026-01-22 09:32:01 +08:00
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// TestDialectQuoteIdentifier 测试方言的 QuoteIdentifier 方法
|
|
|
|
|
func TestDialectQuoteIdentifier(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
dialect Dialect
|
|
|
|
|
input string
|
|
|
|
|
expected string
|
|
|
|
|
}{
|
|
|
|
|
// MySQL 方言测试
|
|
|
|
|
{"MySQL simple", &MySQLDialect{}, "name", "`name`"},
|
|
|
|
|
{"MySQL with backticks", &MySQLDialect{}, "`name`", "`name`"},
|
|
|
|
|
{"MySQL with quotes", &MySQLDialect{}, "\"name\"", "`name`"},
|
|
|
|
|
|
|
|
|
|
// PostgreSQL 方言测试
|
|
|
|
|
{"PostgreSQL simple", &PostgreSQLDialect{}, "name", "\"name\""},
|
|
|
|
|
{"PostgreSQL with backticks", &PostgreSQLDialect{}, "`name`", "\"name\""},
|
|
|
|
|
{"PostgreSQL with quotes", &PostgreSQLDialect{}, "\"name\"", "\"name\""},
|
|
|
|
|
|
|
|
|
|
// SQLite 方言测试
|
|
|
|
|
{"SQLite simple", &SQLiteDialect{}, "name", "\"name\""},
|
|
|
|
|
{"SQLite with backticks", &SQLiteDialect{}, "`name`", "\"name\""},
|
|
|
|
|
{"SQLite with quotes", &SQLiteDialect{}, "\"name\"", "\"name\""},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
result := tt.dialect.QuoteIdentifier(tt.input)
|
|
|
|
|
if result != tt.expected {
|
|
|
|
|
t.Errorf("QuoteIdentifier(%q) = %q, want %q", tt.input, result, tt.expected)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestDialectQuoteChar 测试方言的 QuoteChar 方法
|
|
|
|
|
func TestDialectQuoteChar(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
dialect Dialect
|
|
|
|
|
expected string
|
|
|
|
|
}{
|
|
|
|
|
{"MySQL", &MySQLDialect{}, "`"},
|
|
|
|
|
{"PostgreSQL", &PostgreSQLDialect{}, "\""},
|
|
|
|
|
{"SQLite", &SQLiteDialect{}, "\""},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
result := tt.dialect.QuoteChar()
|
|
|
|
|
if result != tt.expected {
|
|
|
|
|
t.Errorf("QuoteChar() = %q, want %q", result, tt.expected)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestIdentifierProcessorTableName 测试表名处理
|
|
|
|
|
func TestIdentifierProcessorTableName(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
dialect Dialect
|
|
|
|
|
prefix string
|
|
|
|
|
input string
|
|
|
|
|
expected string
|
|
|
|
|
}{
|
|
|
|
|
// MySQL 无前缀
|
|
|
|
|
{"MySQL no prefix", &MySQLDialect{}, "", "order", "`order`"},
|
|
|
|
|
{"MySQL no prefix with backticks", &MySQLDialect{}, "", "`order`", "`order`"},
|
|
|
|
|
|
|
|
|
|
// MySQL 有前缀
|
|
|
|
|
{"MySQL with prefix", &MySQLDialect{}, "app_", "order", "`app_order`"},
|
|
|
|
|
{"MySQL with prefix and backticks", &MySQLDialect{}, "app_", "`order`", "`app_order`"},
|
|
|
|
|
|
|
|
|
|
// PostgreSQL 无前缀
|
|
|
|
|
{"PostgreSQL no prefix", &PostgreSQLDialect{}, "", "order", "\"order\""},
|
|
|
|
|
|
|
|
|
|
// PostgreSQL 有前缀
|
|
|
|
|
{"PostgreSQL with prefix", &PostgreSQLDialect{}, "app_", "order", "\"app_order\""},
|
|
|
|
|
{"PostgreSQL with prefix and quotes", &PostgreSQLDialect{}, "app_", "\"order\"", "\"app_order\""},
|
|
|
|
|
|
|
|
|
|
// SQLite 有前缀
|
|
|
|
|
{"SQLite with prefix", &SQLiteDialect{}, "app_", "user", "\"app_user\""},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
processor := NewIdentifierProcessor(tt.dialect, tt.prefix)
|
|
|
|
|
result := processor.ProcessTableName(tt.input)
|
|
|
|
|
if result != tt.expected {
|
|
|
|
|
t.Errorf("ProcessTableName(%q) = %q, want %q", tt.input, result, tt.expected)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestIdentifierProcessorColumn 测试列名处理(包括 table.column 格式)
|
|
|
|
|
func TestIdentifierProcessorColumn(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
dialect Dialect
|
|
|
|
|
prefix string
|
|
|
|
|
input string
|
|
|
|
|
expected string
|
|
|
|
|
}{
|
|
|
|
|
// 单独列名
|
|
|
|
|
{"MySQL simple column", &MySQLDialect{}, "", "name", "`name`"},
|
|
|
|
|
{"MySQL simple column with prefix", &MySQLDialect{}, "app_", "name", "`name`"},
|
|
|
|
|
|
|
|
|
|
// table.column 格式
|
|
|
|
|
{"MySQL table.column no prefix", &MySQLDialect{}, "", "order.name", "`order`.`name`"},
|
|
|
|
|
{"MySQL table.column with prefix", &MySQLDialect{}, "app_", "order.name", "`app_order`.`name`"},
|
|
|
|
|
{"MySQL table.column with backticks", &MySQLDialect{}, "app_", "`order`.name", "`app_order`.`name`"},
|
|
|
|
|
|
|
|
|
|
// PostgreSQL
|
|
|
|
|
{"PostgreSQL table.column with prefix", &PostgreSQLDialect{}, "app_", "order.name", "\"app_order\".\"name\""},
|
|
|
|
|
{"PostgreSQL table.column with quotes", &PostgreSQLDialect{}, "app_", "\"order\".name", "\"app_order\".\"name\""},
|
|
|
|
|
|
|
|
|
|
// SQLite
|
|
|
|
|
{"SQLite table.column with prefix", &SQLiteDialect{}, "app_", "user.email", "\"app_user\".\"email\""},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
processor := NewIdentifierProcessor(tt.dialect, tt.prefix)
|
|
|
|
|
result := processor.ProcessColumn(tt.input)
|
|
|
|
|
if result != tt.expected {
|
|
|
|
|
t.Errorf("ProcessColumn(%q) = %q, want %q", tt.input, result, tt.expected)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestIdentifierProcessorConditionString 测试条件字符串处理
|
|
|
|
|
func TestIdentifierProcessorConditionString(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
dialect Dialect
|
|
|
|
|
prefix string
|
|
|
|
|
input string
|
|
|
|
|
contains []string // 结果应该包含这些字符串
|
|
|
|
|
}{
|
|
|
|
|
// MySQL 简单条件
|
|
|
|
|
{
|
|
|
|
|
"MySQL simple condition",
|
|
|
|
|
&MySQLDialect{},
|
|
|
|
|
"app_",
|
|
|
|
|
"user.id = order.user_id",
|
|
|
|
|
[]string{"`app_user`", "`app_order`"},
|
|
|
|
|
},
|
|
|
|
|
// MySQL 复杂条件
|
|
|
|
|
{
|
|
|
|
|
"MySQL complex condition",
|
|
|
|
|
&MySQLDialect{},
|
|
|
|
|
"app_",
|
|
|
|
|
"user.id = order.user_id AND order.status = 1",
|
|
|
|
|
[]string{"`app_user`", "`app_order`"},
|
|
|
|
|
},
|
|
|
|
|
// PostgreSQL
|
|
|
|
|
{
|
|
|
|
|
"PostgreSQL condition",
|
|
|
|
|
&PostgreSQLDialect{},
|
|
|
|
|
"app_",
|
|
|
|
|
"user.id = order.user_id",
|
|
|
|
|
[]string{"\"app_user\"", "\"app_order\""},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
processor := NewIdentifierProcessor(tt.dialect, tt.prefix)
|
|
|
|
|
result := processor.ProcessConditionString(tt.input)
|
|
|
|
|
for _, expected := range tt.contains {
|
|
|
|
|
if !strings.Contains(result, expected) {
|
|
|
|
|
t.Errorf("ProcessConditionString(%q) = %q, should contain %q", tt.input, result, expected)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestHoTimeDBHelperMethods 测试 HoTimeDB 的辅助方法 T() 和 C()
|
|
|
|
|
func TestHoTimeDBHelperMethods(t *testing.T) {
|
|
|
|
|
// 创建 MySQL 数据库实例
|
|
|
|
|
mysqlDB := &HoTimeDB{
|
|
|
|
|
Type: "mysql",
|
|
|
|
|
Prefix: "app_",
|
|
|
|
|
}
|
|
|
|
|
mysqlDB.initDialect()
|
|
|
|
|
|
|
|
|
|
// 测试 T() 方法
|
|
|
|
|
t.Run("MySQL T() method", func(t *testing.T) {
|
|
|
|
|
result := mysqlDB.T("order")
|
|
|
|
|
expected := "`app_order`"
|
|
|
|
|
if result != expected {
|
|
|
|
|
t.Errorf("T(\"order\") = %q, want %q", result, expected)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 测试 C() 方法(两个参数)
|
|
|
|
|
t.Run("MySQL C() method with two args", func(t *testing.T) {
|
|
|
|
|
result := mysqlDB.C("order", "name")
|
|
|
|
|
expected := "`app_order`.`name`"
|
|
|
|
|
if result != expected {
|
|
|
|
|
t.Errorf("C(\"order\", \"name\") = %q, want %q", result, expected)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 测试 C() 方法(一个参数,点号格式)
|
|
|
|
|
t.Run("MySQL C() method with dot notation", func(t *testing.T) {
|
|
|
|
|
result := mysqlDB.C("order.name")
|
|
|
|
|
expected := "`app_order`.`name`"
|
|
|
|
|
if result != expected {
|
|
|
|
|
t.Errorf("C(\"order.name\") = %q, want %q", result, expected)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 创建 PostgreSQL 数据库实例
|
|
|
|
|
pgDB := &HoTimeDB{
|
|
|
|
|
Type: "postgres",
|
|
|
|
|
Prefix: "app_",
|
|
|
|
|
}
|
|
|
|
|
pgDB.initDialect()
|
|
|
|
|
|
|
|
|
|
// 测试 PostgreSQL 的 T() 方法
|
|
|
|
|
t.Run("PostgreSQL T() method", func(t *testing.T) {
|
|
|
|
|
result := pgDB.T("order")
|
|
|
|
|
expected := "\"app_order\""
|
|
|
|
|
if result != expected {
|
|
|
|
|
t.Errorf("T(\"order\") = %q, want %q", result, expected)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 测试 PostgreSQL 的 C() 方法
|
|
|
|
|
t.Run("PostgreSQL C() method", func(t *testing.T) {
|
|
|
|
|
result := pgDB.C("order", "name")
|
|
|
|
|
expected := "\"app_order\".\"name\""
|
|
|
|
|
if result != expected {
|
|
|
|
|
t.Errorf("C(\"order\", \"name\") = %q, want %q", result, expected)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 01:51:35 +08:00
|
|
|
// TestWhereWithORCondition 测试 OR 条件处理是否正确添加括号
|
|
|
|
|
func TestWhereWithORCondition(t *testing.T) {
|
|
|
|
|
// 创建 MySQL 数据库实例
|
|
|
|
|
mysqlDB := &HoTimeDB{
|
|
|
|
|
Type: "mysql",
|
|
|
|
|
Prefix: "",
|
|
|
|
|
}
|
|
|
|
|
mysqlDB.initDialect()
|
|
|
|
|
|
|
|
|
|
// 测试 OR 与普通条件组合 (假设 A: 顺序问题)
|
|
|
|
|
t.Run("OR with normal condition", func(t *testing.T) {
|
|
|
|
|
data := Map{
|
|
|
|
|
"OR": Map{
|
|
|
|
|
"username": "test",
|
|
|
|
|
"phone": "123",
|
|
|
|
|
},
|
|
|
|
|
"state": 0,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
where, params := mysqlDB.where(data)
|
|
|
|
|
fmt.Println("Test 1 - OR with normal condition:")
|
|
|
|
|
fmt.Println(" Generated WHERE:", where)
|
|
|
|
|
fmt.Println(" Params count:", len(params))
|
|
|
|
|
|
|
|
|
|
// 检查 OR 条件是否被括号包裹
|
|
|
|
|
if !strings.Contains(where, "(") || !strings.Contains(where, ")") {
|
|
|
|
|
t.Errorf("OR condition should be wrapped with parentheses, got: %s", where)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查是否有 AND 连接
|
|
|
|
|
if !strings.Contains(where, "AND") {
|
|
|
|
|
t.Errorf("OR condition and normal condition should be connected with AND, got: %s", where)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 测试纯 OR 条件(无其他普通条件)
|
|
|
|
|
t.Run("Pure OR condition", func(t *testing.T) {
|
|
|
|
|
data := Map{
|
|
|
|
|
"OR": Map{
|
|
|
|
|
"username": "test",
|
|
|
|
|
"phone": "123",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
where, params := mysqlDB.where(data)
|
|
|
|
|
fmt.Println("Test 2 - Pure OR condition:")
|
|
|
|
|
fmt.Println(" Generated WHERE:", where)
|
|
|
|
|
fmt.Println(" Params count:", len(params))
|
|
|
|
|
|
|
|
|
|
// 检查 OR 条件内部应该用 OR 连接
|
|
|
|
|
if !strings.Contains(where, "OR") {
|
|
|
|
|
t.Errorf("OR condition should contain OR keyword, got: %s", where)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 测试多个普通条件与 OR 组合 (假设 A)
|
|
|
|
|
t.Run("OR with multiple normal conditions", func(t *testing.T) {
|
|
|
|
|
data := Map{
|
|
|
|
|
"OR": Map{
|
|
|
|
|
"username": "test",
|
|
|
|
|
"phone": "123",
|
|
|
|
|
},
|
|
|
|
|
"state": 0,
|
|
|
|
|
"status": 1,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
where, params := mysqlDB.where(data)
|
|
|
|
|
fmt.Println("Test 3 - OR with multiple normal conditions:")
|
|
|
|
|
fmt.Println(" Generated WHERE:", where)
|
|
|
|
|
fmt.Println(" Params count:", len(params))
|
|
|
|
|
|
|
|
|
|
// 应该有括号
|
|
|
|
|
if !strings.Contains(where, "(") {
|
|
|
|
|
t.Errorf("OR condition should be wrapped with parentheses, got: %s", where)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 测试嵌套 AND/OR 条件 (假设 B, E)
|
|
|
|
|
t.Run("Nested AND/OR conditions", func(t *testing.T) {
|
|
|
|
|
data := Map{
|
|
|
|
|
"OR": Map{
|
|
|
|
|
"username": "test",
|
|
|
|
|
"AND": Map{
|
|
|
|
|
"phone": "123",
|
|
|
|
|
"status": 1,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"state": 0,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
where, params := mysqlDB.where(data)
|
|
|
|
|
fmt.Println("Test 4 - Nested AND/OR conditions:")
|
|
|
|
|
fmt.Println(" Generated WHERE:", where)
|
|
|
|
|
fmt.Println(" Params count:", len(params))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 测试空 OR 条件 (假设 C)
|
|
|
|
|
t.Run("Empty OR condition", func(t *testing.T) {
|
|
|
|
|
data := Map{
|
|
|
|
|
"OR": Map{},
|
|
|
|
|
"state": 0,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
where, params := mysqlDB.where(data)
|
|
|
|
|
fmt.Println("Test 5 - Empty OR condition:")
|
|
|
|
|
fmt.Println(" Generated WHERE:", where)
|
|
|
|
|
fmt.Println(" Params count:", len(params))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 测试 OR 与 LIMIT, ORDER 组合 (假设 D)
|
|
|
|
|
t.Run("OR with LIMIT and ORDER", func(t *testing.T) {
|
|
|
|
|
data := Map{
|
|
|
|
|
"OR": Map{
|
|
|
|
|
"username": "test",
|
|
|
|
|
"phone": "123",
|
|
|
|
|
},
|
|
|
|
|
"state": 0,
|
|
|
|
|
"ORDER": "id DESC",
|
|
|
|
|
"LIMIT": 10,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
where, params := mysqlDB.where(data)
|
|
|
|
|
fmt.Println("Test 6 - OR with LIMIT and ORDER:")
|
|
|
|
|
fmt.Println(" Generated WHERE:", where)
|
|
|
|
|
fmt.Println(" Params count:", len(params))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 测试同时有 OR 和 AND 关键字 (假设 E)
|
|
|
|
|
t.Run("Both OR and AND keywords", func(t *testing.T) {
|
|
|
|
|
data := Map{
|
|
|
|
|
"OR": Map{
|
|
|
|
|
"username": "test",
|
|
|
|
|
"phone": "123",
|
|
|
|
|
},
|
|
|
|
|
"AND": Map{
|
|
|
|
|
"type": 1,
|
|
|
|
|
"source": "web",
|
|
|
|
|
},
|
|
|
|
|
"state": 0,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
where, params := mysqlDB.where(data)
|
|
|
|
|
fmt.Println("Test 7 - Both OR and AND keywords:")
|
|
|
|
|
fmt.Println(" Generated WHERE:", where)
|
|
|
|
|
fmt.Println(" Params count:", len(params))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 测试普通条件在 OR 之前(排序后)(假设 A)
|
|
|
|
|
t.Run("Normal condition before OR alphabetically", func(t *testing.T) {
|
|
|
|
|
data := Map{
|
|
|
|
|
"OR": Map{
|
|
|
|
|
"username": "test",
|
|
|
|
|
"phone": "123",
|
|
|
|
|
},
|
|
|
|
|
"active": 1, // 'a' 在 'O' 之前
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
where, params := mysqlDB.where(data)
|
|
|
|
|
fmt.Println("Test 8 - Normal condition before OR (alphabetically):")
|
|
|
|
|
fmt.Println(" Generated WHERE:", where)
|
|
|
|
|
fmt.Println(" Params count:", len(params))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 09:32:01 +08:00
|
|
|
// 打印测试结果(用于调试)
|
|
|
|
|
func ExampleIdentifierProcessor() {
|
|
|
|
|
// MySQL 示例
|
|
|
|
|
mysqlProcessor := NewIdentifierProcessor(&MySQLDialect{}, "app_")
|
|
|
|
|
fmt.Println("MySQL:")
|
|
|
|
|
fmt.Println(" Table:", mysqlProcessor.ProcessTableName("order"))
|
|
|
|
|
fmt.Println(" Column:", mysqlProcessor.ProcessColumn("order.name"))
|
|
|
|
|
fmt.Println(" Condition:", mysqlProcessor.ProcessConditionString("user.id = order.user_id"))
|
|
|
|
|
|
|
|
|
|
// PostgreSQL 示例
|
|
|
|
|
pgProcessor := NewIdentifierProcessor(&PostgreSQLDialect{}, "app_")
|
|
|
|
|
fmt.Println("PostgreSQL:")
|
|
|
|
|
fmt.Println(" Table:", pgProcessor.ProcessTableName("order"))
|
|
|
|
|
fmt.Println(" Column:", pgProcessor.ProcessColumn("order.name"))
|
|
|
|
|
fmt.Println(" Condition:", pgProcessor.ProcessConditionString("user.id = order.user_id"))
|
|
|
|
|
|
|
|
|
|
// Output:
|
|
|
|
|
// MySQL:
|
|
|
|
|
// Table: `app_order`
|
|
|
|
|
// Column: `app_order`.`name`
|
|
|
|
|
// Condition: `app_user`.`id` = `app_order`.`user_id`
|
|
|
|
|
// PostgreSQL:
|
|
|
|
|
// Table: "app_order"
|
|
|
|
|
// Column: "app_order"."name"
|
|
|
|
|
// Condition: "app_user"."id" = "app_order"."user_id"
|
|
|
|
|
}
|