mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-26 00:11:43 +08:00
- 提交 internal/db 多驱动用户可见错误与状态文案多语言化 - 补齐数据库驱动多语言测试与六语言 catalog - 修复 frontend i18n catalog 的 4 个失效 guard
451 lines
13 KiB
Go
451 lines
13 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"database/sql/driver"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"GoNavi-Wails/shared/i18n"
|
|
)
|
|
|
|
func TestExecParameterizedInsertBatchesGroupsRowsByColumnSet(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var queries []string
|
|
var args [][]interface{}
|
|
err := execParameterizedInsertBatches(parameterizedInsertConfig{
|
|
Table: "\"users\"",
|
|
Rows: []map[string]interface{}{
|
|
{"id": 1, "name": "Alice"},
|
|
{"name": "Bob", "id": 2},
|
|
{"id": 3},
|
|
},
|
|
QuoteColumn: func(column string) string { return `"` + column + `"` },
|
|
Placeholder: func(idx int) string {
|
|
return fmt.Sprintf("$%d", idx)
|
|
},
|
|
Exec: func(query string, values ...interface{}) (sql.Result, error) {
|
|
queries = append(queries, query)
|
|
args = append(args, append([]interface{}(nil), values...))
|
|
return driver.RowsAffected(1), nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("execParameterizedInsertBatches() error = %v", err)
|
|
}
|
|
|
|
if len(queries) != 2 {
|
|
t.Fatalf("expected 2 insert statements, got %d: %v", len(queries), queries)
|
|
}
|
|
if queries[0] != `INSERT INTO "users" ("id", "name") VALUES ($1, $2), ($3, $4)` {
|
|
t.Fatalf("unexpected first query: %s", queries[0])
|
|
}
|
|
if queries[1] != `INSERT INTO "users" ("id") VALUES ($1)` {
|
|
t.Fatalf("unexpected second query: %s", queries[1])
|
|
}
|
|
if got := fmt.Sprint(args[0]); got != "[1 Alice 2 Bob]" {
|
|
t.Fatalf("unexpected first args: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestExecParameterizedInsertBatchesUsesNamedSQLServerArgs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var query string
|
|
var args []interface{}
|
|
err := execParameterizedInsertBatches(parameterizedInsertConfig{
|
|
Table: "[dbo].[users]",
|
|
Rows: []map[string]interface{}{{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}},
|
|
QuoteColumn: func(column string) string { return "[" + column + "]" },
|
|
Placeholder: func(idx int) string {
|
|
return fmt.Sprintf("@p%d", idx)
|
|
},
|
|
Arg: func(idx int, _ string, value interface{}) interface{} {
|
|
return sql.Named(fmt.Sprintf("p%d", idx), value)
|
|
},
|
|
Exec: func(q string, values ...interface{}) (sql.Result, error) {
|
|
query = q
|
|
args = append([]interface{}(nil), values...)
|
|
return driver.RowsAffected(1), nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("execParameterizedInsertBatches() error = %v", err)
|
|
}
|
|
|
|
if query != `INSERT INTO [dbo].[users] ([id], [name]) VALUES (@p1, @p2), (@p3, @p4)` {
|
|
t.Fatalf("unexpected query: %s", query)
|
|
}
|
|
if len(args) != 4 {
|
|
t.Fatalf("expected 4 args, got %d", len(args))
|
|
}
|
|
first, ok := args[0].(sql.NamedArg)
|
|
if !ok || first.Name != "p1" || first.Value != 1 {
|
|
t.Fatalf("unexpected first arg: %#v", args[0])
|
|
}
|
|
}
|
|
|
|
func TestExecLiteralInsertBatchesBuildsMultiRowValues(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var query string
|
|
err := execLiteralInsertBatches(literalInsertConfig{
|
|
Table: "`metrics`",
|
|
Rows: []map[string]interface{}{{"ts": 1, "value": "a"}, {"ts": 2, "value": "b"}},
|
|
QuoteColumn: func(column string) string { return "`" + column + "`" },
|
|
Literal: func(value interface{}) string {
|
|
return fmt.Sprintf("'%v'", value)
|
|
},
|
|
Exec: func(q string) (sql.Result, error) {
|
|
query = q
|
|
return driver.RowsAffected(1), nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("execLiteralInsertBatches() error = %v", err)
|
|
}
|
|
|
|
if query != "INSERT INTO `metrics` (`ts`, `value`) VALUES ('1', 'a'), ('2', 'b')" {
|
|
t.Fatalf("unexpected query: %s", query)
|
|
}
|
|
}
|
|
|
|
func TestBatchInsertRowLimitRespectsArgumentLimit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if got := batchInsertRowLimit(2, 1000, 60000); got != 1000 {
|
|
t.Fatalf("2 columns limit=%d, want 1000", got)
|
|
}
|
|
if got := batchInsertRowLimit(100, 1000, 60000); got != 600 {
|
|
t.Fatalf("100 columns limit=%d, want 600", got)
|
|
}
|
|
if got := batchInsertRowLimit(70000, 1000, 60000); got != 1 {
|
|
t.Fatalf("wide table limit=%d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestExecParameterizedInsertBatchesSplitsByArgumentLimit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var queries []string
|
|
rows := []map[string]interface{}{
|
|
{"a": 1, "b": 2},
|
|
{"a": 3, "b": 4},
|
|
{"a": 5, "b": 6},
|
|
}
|
|
err := execParameterizedInsertBatches(parameterizedInsertConfig{
|
|
Table: "`t`",
|
|
Rows: rows,
|
|
QuoteColumn: func(column string) string { return "`" + column + "`" },
|
|
Placeholder: func(int) string {
|
|
return "?"
|
|
},
|
|
Exec: func(query string, _ ...interface{}) (sql.Result, error) {
|
|
queries = append(queries, query)
|
|
return driver.RowsAffected(1), nil
|
|
},
|
|
MaxRows: 1000,
|
|
MaxArgs: 4,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("execParameterizedInsertBatches() error = %v", err)
|
|
}
|
|
if len(queries) != 2 {
|
|
t.Fatalf("expected 2 queries, got %d: %v", len(queries), queries)
|
|
}
|
|
if strings.Count(queries[0], "(?, ?)") != 2 || strings.Count(queries[1], "(?, ?)") != 1 {
|
|
t.Fatalf("unexpected split queries: %v", queries)
|
|
}
|
|
}
|
|
|
|
func TestExecParameterizedInsertBatchesOmitsColumnsPerRow(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var queries []string
|
|
err := execParameterizedInsertBatches(parameterizedInsertConfig{
|
|
Table: "`events`",
|
|
Rows: []map[string]interface{}{
|
|
{"id": 1, "created_at": ""},
|
|
{"id": 2, "created_at": "2026-05-25 10:00:00"},
|
|
},
|
|
QuoteColumn: func(column string) string { return "`" + column + "`" },
|
|
Placeholder: func(int) string {
|
|
return "?"
|
|
},
|
|
Value: func(column string, value interface{}) (interface{}, bool) {
|
|
return value, column == "created_at" && value == ""
|
|
},
|
|
Exec: func(query string, _ ...interface{}) (sql.Result, error) {
|
|
queries = append(queries, query)
|
|
return driver.RowsAffected(1), nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("execParameterizedInsertBatches() error = %v", err)
|
|
}
|
|
|
|
if len(queries) != 2 {
|
|
t.Fatalf("expected rows with different effective columns to split into 2 statements, got %d: %v", len(queries), queries)
|
|
}
|
|
if queries[0] != "INSERT INTO `events` (`id`) VALUES (?)" {
|
|
t.Fatalf("unexpected omitted-column query: %s", queries[0])
|
|
}
|
|
if queries[1] != "INSERT INTO `events` (`created_at`, `id`) VALUES (?, ?)" {
|
|
t.Fatalf("unexpected full-column query: %s", queries[1])
|
|
}
|
|
}
|
|
|
|
func TestExecParameterizedInsertBatchesRunsEmptyInsertSQLWhenAllColumnsOmitted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var queries []string
|
|
err := execParameterizedInsertBatches(parameterizedInsertConfig{
|
|
Table: "`events`",
|
|
Rows: []map[string]interface{}{{"created_at": ""}, {"created_at": ""}},
|
|
QuoteColumn: func(column string) string { return "`" + column + "`" },
|
|
Placeholder: func(int) string { return "?" },
|
|
Value: func(_ string, value interface{}) (interface{}, bool) {
|
|
return value, true
|
|
},
|
|
EmptyInsertSQL: func(table string) string {
|
|
return fmt.Sprintf("INSERT INTO %s () VALUES ()", table)
|
|
},
|
|
Exec: func(query string, _ ...interface{}) (sql.Result, error) {
|
|
queries = append(queries, query)
|
|
return driver.RowsAffected(1), nil
|
|
},
|
|
RequireAffected: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("execParameterizedInsertBatches() error = %v", err)
|
|
}
|
|
|
|
if len(queries) != 2 {
|
|
t.Fatalf("expected 2 empty insert statements, got %d: %v", len(queries), queries)
|
|
}
|
|
for _, query := range queries {
|
|
if query != "INSERT INTO `events` () VALUES ()" {
|
|
t.Fatalf("unexpected empty insert query: %s", query)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBatchInsertErrorsUseCurrentLanguage(t *testing.T) {
|
|
SetBackendLanguage(i18n.LanguageEnUS)
|
|
t.Cleanup(func() {
|
|
SetBackendLanguage(i18n.LanguageZhCN)
|
|
})
|
|
|
|
baseErr := errors.New("driver insert failed")
|
|
rows := []map[string]interface{}{{"id": 1}}
|
|
quoteColumn := func(column string) string { return `"` + column + `"` }
|
|
placeholder := func(int) string { return "?" }
|
|
|
|
cases := []struct {
|
|
name string
|
|
call func() error
|
|
want string
|
|
}{
|
|
{
|
|
name: "parameterized table name required",
|
|
call: func() error {
|
|
return execParameterizedInsertBatches(parameterizedInsertConfig{
|
|
Table: " ",
|
|
Rows: rows,
|
|
QuoteColumn: quoteColumn,
|
|
Placeholder: placeholder,
|
|
Exec: func(string, ...interface{}) (sql.Result, error) {
|
|
return driver.RowsAffected(1), nil
|
|
},
|
|
})
|
|
},
|
|
want: "Table name is required",
|
|
},
|
|
{
|
|
name: "parameterized quote function required",
|
|
call: func() error {
|
|
return execParameterizedInsertBatches(parameterizedInsertConfig{
|
|
Table: `"users"`,
|
|
Rows: rows,
|
|
Placeholder: placeholder,
|
|
Exec: func(string, ...interface{}) (sql.Result, error) {
|
|
return driver.RowsAffected(1), nil
|
|
},
|
|
})
|
|
},
|
|
want: "Column quoting function is required",
|
|
},
|
|
{
|
|
name: "parameterized placeholder function required",
|
|
call: func() error {
|
|
return execParameterizedInsertBatches(parameterizedInsertConfig{
|
|
Table: `"users"`,
|
|
Rows: rows,
|
|
QuoteColumn: quoteColumn,
|
|
Exec: func(string, ...interface{}) (sql.Result, error) {
|
|
return driver.RowsAffected(1), nil
|
|
},
|
|
})
|
|
},
|
|
want: "Placeholder function is required",
|
|
},
|
|
{
|
|
name: "parameterized exec function required",
|
|
call: func() error {
|
|
return execParameterizedInsertBatches(parameterizedInsertConfig{
|
|
Table: `"users"`,
|
|
Rows: rows,
|
|
QuoteColumn: quoteColumn,
|
|
Placeholder: placeholder,
|
|
})
|
|
},
|
|
want: "Execution function is required",
|
|
},
|
|
{
|
|
name: "parameterized insert failed keeps raw detail",
|
|
call: func() error {
|
|
return execParameterizedInsertBatches(parameterizedInsertConfig{
|
|
Table: `"users"`,
|
|
Rows: rows,
|
|
QuoteColumn: quoteColumn,
|
|
Placeholder: placeholder,
|
|
Exec: func(string, ...interface{}) (sql.Result, error) {
|
|
return nil, baseErr
|
|
},
|
|
})
|
|
},
|
|
want: "Insert failed: driver insert failed",
|
|
},
|
|
{
|
|
name: "parameterized insert no rows affected",
|
|
call: func() error {
|
|
return execParameterizedInsertBatches(parameterizedInsertConfig{
|
|
Table: `"users"`,
|
|
Rows: rows,
|
|
QuoteColumn: quoteColumn,
|
|
Placeholder: placeholder,
|
|
RequireAffected: true,
|
|
Exec: func(string, ...interface{}) (sql.Result, error) {
|
|
return driver.RowsAffected(0), nil
|
|
},
|
|
})
|
|
},
|
|
want: "Insert did not take effect: no rows were affected",
|
|
},
|
|
{
|
|
name: "literal function required",
|
|
call: func() error {
|
|
return execLiteralInsertBatches(literalInsertConfig{
|
|
Table: `"users"`,
|
|
Rows: rows,
|
|
QuoteColumn: quoteColumn,
|
|
Exec: func(string) (sql.Result, error) {
|
|
return driver.RowsAffected(1), nil
|
|
},
|
|
})
|
|
},
|
|
want: "Literal function is required",
|
|
},
|
|
{
|
|
name: "literal insert failed keeps raw detail and sql",
|
|
call: func() error {
|
|
return execLiteralInsertBatches(literalInsertConfig{
|
|
Table: `"users"`,
|
|
Rows: rows,
|
|
QuoteColumn: quoteColumn,
|
|
Literal: func(value interface{}) string { return fmt.Sprintf("%v", value) },
|
|
Exec: func(string) (sql.Result, error) {
|
|
return nil, baseErr
|
|
},
|
|
})
|
|
},
|
|
want: `Insert failed: driver insert failed; SQL=INSERT INTO "users" ("id") VALUES (1)`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := tc.call()
|
|
if err == nil {
|
|
t.Fatal("expected batch insert error")
|
|
}
|
|
if err.Error() != tc.want {
|
|
t.Fatalf("expected %q, got %q", tc.want, err.Error())
|
|
}
|
|
for _, raw := range []string{"表名不能为空", "列名引用函数不能为空", "占位符函数不能为空", "执行函数不能为空", "字面量函数不能为空", "插入失败", "插入未生效"} {
|
|
if strings.Contains(err.Error(), raw) {
|
|
t.Fatalf("expected no raw Chinese batch insert text %q in %q", raw, err.Error())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBatchInsertErrorSourcesUseI18nKeys(t *testing.T) {
|
|
sourceBytes, err := os.ReadFile("batch_insert.go")
|
|
if err != nil {
|
|
t.Fatalf("read batch_insert.go: %v", err)
|
|
}
|
|
source := string(sourceBytes)
|
|
|
|
for _, rawMessage := range []string{
|
|
`fmt.Errorf("表名不能为空")`,
|
|
`fmt.Errorf("列名引用函数不能为空")`,
|
|
`fmt.Errorf("占位符函数不能为空")`,
|
|
`fmt.Errorf("执行函数不能为空")`,
|
|
`fmt.Errorf("字面量函数不能为空")`,
|
|
`fmt.Errorf("插入失败:%v", err)`,
|
|
`fmt.Errorf("插入失败:%v; sql=%s", err, query)`,
|
|
`fmt.Errorf("插入未生效:未影响任何行")`,
|
|
} {
|
|
if strings.Contains(source, rawMessage) {
|
|
t.Fatalf("batch_insert.go still contains raw batch insert text %q", rawMessage)
|
|
}
|
|
}
|
|
|
|
for _, key := range []string{
|
|
"db.backend.error.table_name_required",
|
|
"db.backend.error.batch_insert_quote_column_required",
|
|
"db.backend.error.batch_insert_placeholder_required",
|
|
"db.backend.error.batch_insert_exec_required",
|
|
"db.backend.error.batch_insert_literal_required",
|
|
"db.backend.error.batch_insert_failed",
|
|
"db.backend.error.batch_insert_failed_with_sql",
|
|
"db.backend.error.batch_insert_no_rows_affected",
|
|
} {
|
|
if !strings.Contains(source, key) {
|
|
t.Fatalf("batch_insert.go does not reference i18n key %q", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBatchInsertErrorCatalogKeysExist(t *testing.T) {
|
|
catalogs, err := i18n.LoadCatalogs()
|
|
if err != nil {
|
|
t.Fatalf("LoadCatalogs() error = %v", err)
|
|
}
|
|
|
|
keys := []string{
|
|
"db.backend.error.table_name_required",
|
|
"db.backend.error.batch_insert_quote_column_required",
|
|
"db.backend.error.batch_insert_placeholder_required",
|
|
"db.backend.error.batch_insert_exec_required",
|
|
"db.backend.error.batch_insert_literal_required",
|
|
"db.backend.error.batch_insert_failed",
|
|
"db.backend.error.batch_insert_failed_with_sql",
|
|
"db.backend.error.batch_insert_no_rows_affected",
|
|
}
|
|
for _, language := range i18n.SupportedLanguages() {
|
|
catalog := catalogs[language]
|
|
for _, key := range keys {
|
|
if strings.TrimSpace(catalog[key]) == "" {
|
|
t.Fatalf("%s catalog missing batch insert key %q", language, key)
|
|
}
|
|
}
|
|
}
|
|
}
|