mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-27 17:02:04 +08:00
- 提交 internal/db 多驱动用户可见错误与状态文案多语言化 - 补齐数据库驱动多语言测试与六语言 catalog - 修复 frontend i18n catalog 的 4 个失效 guard
291 lines
7.9 KiB
Go
291 lines
7.9 KiB
Go
//go:build gonavi_full_drivers || gonavi_duckdb_driver
|
|
|
|
package db
|
|
|
|
import (
|
|
"errors"
|
|
"database/sql"
|
|
"database/sql/driver"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"GoNavi-Wails/internal/connection"
|
|
"GoNavi-Wails/shared/i18n"
|
|
)
|
|
|
|
type duckDBI18nEmptyRowsDriver struct{}
|
|
|
|
type duckDBI18nEmptyRowsConn struct{}
|
|
|
|
type duckDBI18nEmptyRowsStmt struct{}
|
|
|
|
type duckDBI18nEmptyRowsRows struct{}
|
|
|
|
var registerDuckDBI18nEmptyRowsDriverOnce sync.Once
|
|
|
|
func (duckDBI18nEmptyRowsDriver) Open(name string) (driver.Conn, error) {
|
|
return duckDBI18nEmptyRowsConn{}, nil
|
|
}
|
|
|
|
func (duckDBI18nEmptyRowsConn) Prepare(query string) (driver.Stmt, error) {
|
|
return duckDBI18nEmptyRowsStmt{}, nil
|
|
}
|
|
|
|
func (duckDBI18nEmptyRowsConn) Close() error { return nil }
|
|
|
|
func (duckDBI18nEmptyRowsConn) Begin() (driver.Tx, error) {
|
|
return nil, fmt.Errorf("transactions are not supported in duckdb i18n empty rows test driver")
|
|
}
|
|
|
|
func (duckDBI18nEmptyRowsStmt) Close() error { return nil }
|
|
|
|
func (duckDBI18nEmptyRowsStmt) NumInput() int { return -1 }
|
|
|
|
func (duckDBI18nEmptyRowsStmt) Exec(args []driver.Value) (driver.Result, error) {
|
|
return nil, fmt.Errorf("exec is not supported in duckdb i18n empty rows test driver")
|
|
}
|
|
|
|
func (duckDBI18nEmptyRowsStmt) Query(args []driver.Value) (driver.Rows, error) {
|
|
return duckDBI18nEmptyRowsRows{}, nil
|
|
}
|
|
|
|
func (duckDBI18nEmptyRowsRows) Columns() []string {
|
|
return []string{"sql"}
|
|
}
|
|
|
|
func (duckDBI18nEmptyRowsRows) Close() error { return nil }
|
|
|
|
func (duckDBI18nEmptyRowsRows) Next(dest []driver.Value) error {
|
|
return io.EOF
|
|
}
|
|
|
|
func openDuckDBI18nEmptyRowsDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
|
|
registerDuckDBI18nEmptyRowsDriverOnce.Do(func() {
|
|
sql.Register("duckdb_i18n_empty_rows", duckDBI18nEmptyRowsDriver{})
|
|
})
|
|
|
|
conn, err := sql.Open("duckdb_i18n_empty_rows", "")
|
|
if err != nil {
|
|
t.Fatalf("open duckdb_i18n_empty_rows test DB failed: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = conn.Close()
|
|
})
|
|
return conn
|
|
}
|
|
|
|
func TestDuckDBRuntimeUsesCurrentLanguageForConnectionNotOpen(t *testing.T) {
|
|
SetBackendLanguage(i18n.LanguageEnUS)
|
|
t.Cleanup(func() {
|
|
SetBackendLanguage(i18n.LanguageZhCN)
|
|
})
|
|
|
|
err := (&DuckDB{}).Ping()
|
|
if err == nil {
|
|
t.Fatal("expected Ping to fail when DuckDB connection is not open")
|
|
}
|
|
if err.Error() != "Connection is not open" {
|
|
t.Fatalf("expected English not-open error, got %q", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestDuckDBBuildSupportStatusUsesCurrentLanguageWhenDriverIsUnavailable(t *testing.T) {
|
|
if supported, _ := duckDBBuildSupportStatus(); supported {
|
|
t.Skip("current build already includes DuckDB runtime support")
|
|
}
|
|
|
|
SetBackendLanguage(i18n.LanguageEnUS)
|
|
t.Cleanup(func() {
|
|
SetBackendLanguage(i18n.LanguageZhCN)
|
|
})
|
|
|
|
wantReason := fmt.Sprintf(
|
|
"The current build does not include the DuckDB driver (platform=%s/%s). Enable CGO and use a supported platform (darwin/linux amd64|arm64, windows/amd64), or provide a custom library via -tags duckdb_use_lib / duckdb_use_static_lib",
|
|
runtime.GOOS,
|
|
runtime.GOARCH,
|
|
)
|
|
|
|
supported, reason := duckDBBuildSupportStatus()
|
|
if supported {
|
|
t.Fatal("expected DuckDB build support to stay unavailable in this test environment")
|
|
}
|
|
if reason != wantReason {
|
|
t.Fatalf("expected English DuckDB-unavailable reason %q, got %q", wantReason, reason)
|
|
}
|
|
|
|
err := (&DuckDB{}).Connect(connection.ConnectionConfig{Type: "duckdb"})
|
|
if err == nil {
|
|
t.Fatal("expected DuckDB connect to fail when runtime support is unavailable")
|
|
}
|
|
|
|
wantConnectError := "DuckDB driver is unavailable: " + wantReason
|
|
if err.Error() != wantConnectError {
|
|
t.Fatalf("expected English DuckDB connect error %q, got %q", wantConnectError, err.Error())
|
|
}
|
|
}
|
|
|
|
func TestDuckDBDDLMetadataErrorsUseCurrentLanguage(t *testing.T) {
|
|
SetBackendLanguage(i18n.LanguageEnUS)
|
|
t.Cleanup(func() {
|
|
SetBackendLanguage(i18n.LanguageZhCN)
|
|
})
|
|
|
|
duck := &DuckDB{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
call func() error
|
|
want string
|
|
}{
|
|
{
|
|
name: "create statement table name required",
|
|
call: func() error {
|
|
_, err := duck.GetCreateStatement("", " ")
|
|
return err
|
|
},
|
|
want: "Table name is required",
|
|
},
|
|
{
|
|
name: "create statement not found",
|
|
call: func() error {
|
|
_, err := (&DuckDB{conn: openDuckDBI18nEmptyRowsDB(t)}).GetCreateStatement("main", "orders")
|
|
return err
|
|
},
|
|
want: "The CREATE TABLE statement was not found",
|
|
},
|
|
{
|
|
name: "columns table name required",
|
|
call: func() error {
|
|
_, err := duck.GetColumns("", " ")
|
|
return err
|
|
},
|
|
want: "Table name is required",
|
|
},
|
|
{
|
|
name: "indexes table name required",
|
|
call: func() error {
|
|
_, err := duck.GetIndexes("", " ")
|
|
return err
|
|
},
|
|
want: "Table name is required",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := tc.call()
|
|
if err == nil {
|
|
t.Fatal("expected DuckDB DDL metadata call to fail")
|
|
}
|
|
if err.Error() != tc.want {
|
|
t.Fatalf("expected %q, got %q", tc.want, err.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDuckDBApplyChangesErrorsUseCurrentLanguage(t *testing.T) {
|
|
SetBackendLanguage(i18n.LanguageEnUS)
|
|
t.Cleanup(func() {
|
|
SetBackendLanguage(i18n.LanguageZhCN)
|
|
})
|
|
|
|
t.Run("delete failure", func(t *testing.T) {
|
|
dbConn, state := openDuckDBRecordingDB(t)
|
|
state.failDelete = errors.New("delete blocked")
|
|
duckdb := &DuckDB{conn: dbConn}
|
|
|
|
err := duckdb.ApplyChanges("main.events", connection.ChangeSet{
|
|
Deletes: []map[string]interface{}{
|
|
{"id": 1},
|
|
},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected delete failure to bubble up")
|
|
}
|
|
if err.Error() != "Delete failed: delete blocked" {
|
|
t.Fatalf("expected English delete failure, got %q", err.Error())
|
|
}
|
|
})
|
|
|
|
t.Run("update key condition required", func(t *testing.T) {
|
|
dbConn, _ := openDuckDBRecordingDB(t)
|
|
duckdb := &DuckDB{conn: dbConn}
|
|
|
|
err := duckdb.ApplyChanges("main.events", connection.ChangeSet{
|
|
Updates: []connection.UpdateRow{{
|
|
Values: map[string]interface{}{
|
|
"name": "renamed",
|
|
},
|
|
}},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected update without keys to fail")
|
|
}
|
|
if err.Error() != "Update operation requires key conditions" {
|
|
t.Fatalf("expected English update-key failure, got %q", err.Error())
|
|
}
|
|
})
|
|
|
|
t.Run("update failure", func(t *testing.T) {
|
|
dbConn, state := openDuckDBRecordingDB(t)
|
|
state.failUpdate = errors.New("update blocked")
|
|
duckdb := &DuckDB{conn: dbConn}
|
|
|
|
err := duckdb.ApplyChanges("main.events", connection.ChangeSet{
|
|
Updates: []connection.UpdateRow{{
|
|
Keys: map[string]interface{}{
|
|
"id": 1,
|
|
},
|
|
Values: map[string]interface{}{
|
|
"name": "renamed",
|
|
},
|
|
}},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected update failure to bubble up")
|
|
}
|
|
if err.Error() != "Update failed: update blocked" {
|
|
t.Fatalf("expected English update failure, got %q", err.Error())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestDuckDBUserVisibleRuntimeErrorsDoNotReintroduceInlineChinese(t *testing.T) {
|
|
t.Helper()
|
|
|
|
files := []string{"duckdb_impl.go", "duckdb_platform_unsupported.go"}
|
|
disallowed := []string{
|
|
string([]rune{0x44, 0x75, 0x63, 0x6b, 0x44, 0x42, 0x20, 0x9a71, 0x52a8, 0x4e0d, 0x53ef, 0x7528}),
|
|
string([]rune{0x6253, 0x5f00, 0x6570, 0x636e, 0x5e93, 0x8fde, 0x63a5, 0x5931, 0x8d25}),
|
|
string([]rune{0x8fde, 0x63a5, 0x5efa, 0x7acb, 0x540e, 0x9a8c, 0x8bc1, 0x5931, 0x8d25}),
|
|
string([]rune{0x8fde, 0x63a5, 0x672a, 0x6253, 0x5f00}),
|
|
string([]rune{0x5f53, 0x524d, 0x6784, 0x5efa, 0x4e0d, 0x5305, 0x542b, 0x20, 0x44, 0x75, 0x63, 0x6b, 0x44, 0x42, 0x20, 0x9a71, 0x52a8}),
|
|
string([]rune{0x8868, 0x540d, 0x4e0d, 0x80fd, 0x4e3a, 0x7a7a}),
|
|
string([]rune{0x672a, 0x627e, 0x5230, 0x5efa, 0x8868, 0x8bed, 0x53e5}),
|
|
string([]rune{0x5220, 0x9664, 0x5931, 0x8d25}),
|
|
string([]rune{0x66f4, 0x65b0, 0x64cd, 0x4f5c, 0x9700, 0x8981, 0x4e3b, 0x952e, 0x6761, 0x4ef6}),
|
|
string([]rune{0x66f4, 0x65b0, 0x5931, 0x8d25}),
|
|
}
|
|
|
|
for _, file := range files {
|
|
content, err := os.ReadFile(file)
|
|
if err != nil {
|
|
t.Fatalf("read %s failed: %v", file, err)
|
|
}
|
|
source := string(content)
|
|
for _, raw := range disallowed {
|
|
if strings.Contains(source, raw) {
|
|
t.Fatalf("%s still contains inline user-visible Chinese raw: %s", file, raw)
|
|
}
|
|
}
|
|
}
|
|
}
|