Skip to content

Commit 768cf27

Browse files
committed
fix(dialect): support sqlite database version diagnostics
1 parent a847ed5 commit 768cf27

11 files changed

Lines changed: 219 additions & 10 deletions

File tree

apps/lina-core/internal/service/sysinfo/sysinfo.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"lina-core/internal/service/cachecoord"
1515
"lina-core/internal/service/config"
16+
"lina-core/pkg/dialect"
1617
"lina-core/pkg/logger"
1718
)
1819

@@ -227,9 +228,5 @@ func (s *serviceImpl) loadComponents(metadata *config.MetadataConfig, sectionKey
227228

228229
// getDbVersion retrieves the database version.
229230
func (s *serviceImpl) getDbVersion(ctx context.Context) (string, error) {
230-
result, err := g.DB().GetValue(ctx, "SELECT VERSION()")
231-
if err != nil {
232-
return "", err
233-
}
234-
return result.String(), nil
231+
return dialect.DatabaseVersion(ctx, g.DB())
235232
}

apps/lina-core/internal/service/sysinfo/sysinfo_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,28 @@ package sysinfo
55
import (
66
"context"
77
"errors"
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
"strings"
812
"testing"
913
"time"
1014

15+
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
16+
"github.com/gogf/gf/v2/database/gdb"
17+
"github.com/gogf/gf/v2/frame/g"
18+
1119
"lina-core/internal/service/cachecoord"
1220
)
1321

1422
const (
1523
// testRuntimeConfigDomain is the sysinfo test projection domain.
1624
testRuntimeConfigDomain cachecoord.Domain = "runtime-config"
25+
// sqliteSysInfoChildEnv marks the isolated child process that owns
26+
// GoFrame's global SQLite database configuration.
27+
sqliteSysInfoChildEnv = "LINA_SQLITE_SYSINFO_CHILD"
28+
// sqliteSysInfoDBEnv stores the temporary SQLite path for the child test.
29+
sqliteSysInfoDBEnv = "LINA_SQLITE_SYSINFO_DB"
1730
)
1831

1932
// fakeCacheCoordService provides deterministic cachecoord snapshots for
@@ -119,3 +132,60 @@ func TestLoadCacheCoordinationToleratesSnapshotFailure(t *testing.T) {
119132
t.Fatalf("expected empty diagnostics after snapshot failure, got %#v", items)
120133
}
121134
}
135+
136+
// TestGetDbVersionSupportsSQLite verifies sysinfo does not use MySQL-only
137+
// VERSION() diagnostics against SQLite databases.
138+
func TestGetDbVersionSupportsSQLite(t *testing.T) {
139+
if os.Getenv(sqliteSysInfoChildEnv) == "1" {
140+
t.Skip("parent test only launches the isolated SQLite child process")
141+
}
142+
143+
dbPath := filepath.Join(t.TempDir(), "sysinfo.db")
144+
cmd := exec.Command(os.Args[0], "-test.run=^TestGetDbVersionSupportsSQLiteChild$", "-test.count=1", "-test.v")
145+
cmd.Env = append(os.Environ(),
146+
sqliteSysInfoChildEnv+"=1",
147+
sqliteSysInfoDBEnv+"="+dbPath,
148+
)
149+
output, err := cmd.CombinedOutput()
150+
if err != nil {
151+
t.Fatalf("SQLite sysinfo child test failed: %v\n%s", err, string(output))
152+
}
153+
}
154+
155+
// TestGetDbVersionSupportsSQLiteChild runs the actual SQLite sysinfo
156+
// diagnostic check in an isolated process because GoFrame database config is
157+
// global.
158+
func TestGetDbVersionSupportsSQLiteChild(t *testing.T) {
159+
if os.Getenv(sqliteSysInfoChildEnv) != "1" {
160+
t.Skip("SQLite sysinfo child test is executed by TestGetDbVersionSupportsSQLite")
161+
}
162+
163+
ctx := context.Background()
164+
dbPath := os.Getenv(sqliteSysInfoDBEnv)
165+
if dbPath == "" {
166+
t.Fatalf("%s must be set", sqliteSysInfoDBEnv)
167+
}
168+
link := "sqlite::@file(" + dbPath + ")"
169+
if err := gdb.SetConfig(gdb.Config{
170+
gdb.DefaultGroupName: gdb.ConfigGroup{{Link: link}},
171+
}); err != nil {
172+
t.Fatalf("configure SQLite sysinfo database failed: %v", err)
173+
}
174+
t.Cleanup(func() {
175+
if closeErr := g.DB().Close(ctx); closeErr != nil {
176+
t.Errorf("close SQLite sysinfo database failed: %v", closeErr)
177+
}
178+
})
179+
180+
service := &serviceImpl{}
181+
version, err := service.getDbVersion(ctx)
182+
if err != nil {
183+
t.Fatalf("get SQLite sysinfo database version failed: %v", err)
184+
}
185+
if !strings.HasPrefix(version, "SQLite ") {
186+
t.Fatalf("expected SQLite version label, got %q", version)
187+
}
188+
if strings.TrimSpace(strings.TrimPrefix(version, "SQLite ")) == "" {
189+
t.Fatalf("expected SQLite version number to be non-empty, got %q", version)
190+
}
191+
}

apps/lina-core/pkg/dialect/dialect.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"context"
77
"strings"
88

9+
"github.com/gogf/gf/v2/database/gdb"
910
"github.com/gogf/gf/v2/errors/gcode"
1011

1112
"lina-core/pkg/bizerr"
@@ -48,6 +49,8 @@ type Dialect interface {
4849
// SupportsCluster reports whether this database can back multi-node
4950
// coordination state.
5051
SupportsCluster() bool
52+
// DatabaseVersion returns a display-ready database engine and version label.
53+
DatabaseVersion(ctx context.Context, db gdb.DB) (string, error)
5154
// OnStartup applies dialect-specific runtime bootstrap behavior before
5255
// cluster services start.
5356
OnStartup(ctx context.Context, runtime RuntimeConfig) error
@@ -81,6 +84,30 @@ func From(link string) (Dialect, error) {
8184
}
8285
}
8386

87+
// FromDatabase resolves one database dialect from the active GoFrame database
88+
// configuration.
89+
func FromDatabase(db gdb.DB) (Dialect, error) {
90+
link := ""
91+
if db != nil && db.GetConfig() != nil {
92+
configNode := db.GetConfig()
93+
link = strings.TrimSpace(configNode.Link)
94+
if link == "" && strings.TrimSpace(configNode.Type) != "" {
95+
link = strings.TrimSpace(configNode.Type) + ":"
96+
}
97+
}
98+
return From(link)
99+
}
100+
101+
// DatabaseVersion returns a display-ready database engine and version label for
102+
// the active GoFrame database.
103+
func DatabaseVersion(ctx context.Context, db gdb.DB) (string, error) {
104+
dbDialect, err := FromDatabase(db)
105+
if err != nil {
106+
return "", err
107+
}
108+
return dbDialect.DatabaseVersion(ctx, db)
109+
}
110+
84111
// mysqlDialect is the public package wrapper for the internal MySQL dialect.
85112
type mysqlDialect struct{}
86113

@@ -104,6 +131,11 @@ func (mysqlDialect) SupportsCluster() bool {
104131
return internalmysql.SupportsCluster()
105132
}
106133

134+
// DatabaseVersion returns the MySQL server version label.
135+
func (mysqlDialect) DatabaseVersion(ctx context.Context, db gdb.DB) (string, error) {
136+
return internalmysql.DatabaseVersion(ctx, db)
137+
}
138+
107139
// OnStartup has no MySQL-specific startup side effects.
108140
func (mysqlDialect) OnStartup(ctx context.Context, runtime RuntimeConfig) error {
109141
return nil
@@ -134,6 +166,11 @@ func (sqliteDialect) SupportsCluster() bool {
134166
return internalsqlite.SupportsCluster()
135167
}
136168

169+
// DatabaseVersion returns the SQLite library version label.
170+
func (sqliteDialect) DatabaseVersion(ctx context.Context, db gdb.DB) (string, error) {
171+
return internalsqlite.DatabaseVersion(ctx, db)
172+
}
173+
137174
// OnStartup applies SQLite-specific startup behavior before cluster services start.
138175
func (d sqliteDialect) OnStartup(ctx context.Context, runtime RuntimeConfig) error {
139176
return internalsqlite.OnStartup(ctx, d.link, runtime)

apps/lina-core/pkg/dialect/dialect_sqlite_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ package dialect
44

55
import (
66
"context"
7+
"path/filepath"
78
"strings"
89
"testing"
910

11+
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
12+
"github.com/gogf/gf/v2/database/gdb"
1013
"github.com/gogf/gf/v2/os/glog"
1114

1215
"lina-core/pkg/logger"
@@ -80,6 +83,33 @@ func TestSQLiteOnStartupOverridesCluster(t *testing.T) {
8083
}
8184
}
8285

86+
// TestSQLiteDatabaseVersionUsesSQLiteFunction verifies version diagnostics do
87+
// not call the MySQL-only VERSION() function on SQLite connections.
88+
func TestSQLiteDatabaseVersionUsesSQLiteFunction(t *testing.T) {
89+
ctx := context.Background()
90+
dbPath := filepath.Join(t.TempDir(), "version.db")
91+
db, err := gdb.New(gdb.ConfigNode{Link: "sqlite::@file(" + dbPath + ")"})
92+
if err != nil {
93+
t.Fatalf("open SQLite database failed: %v", err)
94+
}
95+
t.Cleanup(func() {
96+
if closeErr := db.Close(ctx); closeErr != nil {
97+
t.Errorf("close SQLite database failed: %v", closeErr)
98+
}
99+
})
100+
101+
version, err := DatabaseVersion(ctx, db)
102+
if err != nil {
103+
t.Fatalf("query SQLite database version failed: %v", err)
104+
}
105+
if !strings.HasPrefix(version, "SQLite ") {
106+
t.Fatalf("expected SQLite version label, got %q", version)
107+
}
108+
if strings.TrimSpace(strings.TrimPrefix(version, "SQLite ")) == "" {
109+
t.Fatalf("expected SQLite version number to be non-empty, got %q", version)
110+
}
111+
}
112+
83113
// containsAnyMessage reports whether one captured message contains a substring.
84114
func containsAnyMessage(messages []string, needle string) bool {
85115
for _, message := range messages {

apps/lina-core/pkg/dialect/internal/mysql/dialect.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ func SupportsCluster() bool {
6666
return true
6767
}
6868

69+
// DatabaseVersion returns the MySQL server version label.
70+
func DatabaseVersion(ctx context.Context, db gdb.DB) (string, error) {
71+
if db == nil {
72+
return "", gerror.New("database connection is required")
73+
}
74+
result, err := db.GetValue(ctx, "SELECT VERSION()")
75+
if err != nil {
76+
return "", err
77+
}
78+
return "MySQL " + strings.TrimSpace(result.String()), nil
79+
}
80+
6981
// ConfigNodeFromLink returns the GoFrame-parsed MySQL configuration node.
7082
func ConfigNodeFromLink(link string) (*gdb.ConfigNode, error) {
7183
db, err := gdb.New(gdb.ConfigNode{Link: link})

apps/lina-core/pkg/dialect/internal/sqlite/dialect.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"strings"
88

9+
"github.com/gogf/gf/v2/database/gdb"
910
"github.com/gogf/gf/v2/errors/gerror"
1011
"github.com/gogf/gf/v2/os/gfile"
1112

@@ -64,6 +65,18 @@ func SupportsCluster() bool {
6465
return false
6566
}
6667

68+
// DatabaseVersion returns the SQLite library version label.
69+
func DatabaseVersion(ctx context.Context, db gdb.DB) (string, error) {
70+
if db == nil {
71+
return "", gerror.New("database connection is required")
72+
}
73+
result, err := db.GetValue(ctx, "SELECT sqlite_version()")
74+
if err != nil {
75+
return "", err
76+
}
77+
return "SQLite " + strings.TrimSpace(result.String()), nil
78+
}
79+
6780
// OnStartup locks cluster mode off and prints prominent warnings for SQLite.
6881
func OnStartup(ctx context.Context, link string, runtime StartupRuntime) error {
6982
if runtime != nil {

apps/lina-plugins/monitor-server/backend/internal/service/monitor/monitor.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
netutil "github.com/shirou/gopsutil/v4/net"
2323
"github.com/shirou/gopsutil/v4/process"
2424

25+
"lina-core/pkg/dialect"
2526
"lina-core/pkg/logger"
2627
"lina-plugin-monitor-server/backend/internal/dao"
2728
"lina-plugin-monitor-server/backend/internal/model/do"
@@ -200,8 +201,12 @@ func (s *serviceImpl) Collect(ctx context.Context) *MonitorData {
200201
// GetDBInfo collects database metrics on demand.
201202
func (s *serviceImpl) GetDBInfo(ctx context.Context) *DBInfo {
202203
info := &DBInfo{}
203-
if value, err := g.DB().GetValue(ctx, "SELECT VERSION()"); err == nil {
204-
info.Version = value.String()
204+
dbVersion, err := dialect.DatabaseVersion(ctx, g.DB())
205+
if err != nil {
206+
logger.Warningf(ctx, "collect database version failed: %v", err)
207+
info.Version = "unknown"
208+
} else {
209+
info.Version = dbVersion
205210
}
206211
statsItems := g.DB().GetCore().Stats(ctx)
207212
if len(statsItems) > 0 {

apps/lina-plugins/monitor-server/backend/internal/service/monitor/monitor_upsert_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"context"
77
"os"
88
"path/filepath"
9+
"strings"
910
"testing"
1011

1112
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
@@ -56,6 +57,25 @@ func TestUpsertMonitorSnapshotWorksOnSQLite(t *testing.T) {
5657
}
5758
}
5859

60+
// TestGetDBInfoReturnsSQLiteVersion verifies monitor-server database
61+
// diagnostics return a non-empty SQLite version label instead of silently
62+
// swallowing the MySQL-only VERSION() failure.
63+
func TestGetDBInfoReturnsSQLiteVersion(t *testing.T) {
64+
ctx := context.Background()
65+
setupSQLiteMonitorServerDB(t, ctx)
66+
67+
info := New().GetDBInfo(ctx)
68+
if info == nil {
69+
t.Fatal("expected SQLite DB info to be returned")
70+
}
71+
if !strings.HasPrefix(info.Version, "SQLite ") {
72+
t.Fatalf("expected SQLite database version label, got %q", info.Version)
73+
}
74+
if strings.TrimSpace(strings.TrimPrefix(info.Version, "SQLite ")) == "" {
75+
t.Fatalf("expected SQLite database version number to be non-empty, got %q", info.Version)
76+
}
77+
}
78+
5979
// setupSQLiteMonitorServerDB points the generated DAO at a temporary SQLite
6080
// database and creates the monitor-server table.
6181
func setupSQLiteMonitorServerDB(t *testing.T, ctx context.Context) {

hack/tests/e2e/dialect/TC0165-sqlite-mode-business-zero-impact.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,16 @@ type UserCreateResult = {
2424
id: number;
2525
};
2626

27+
type ServerMonitorResult = {
28+
dbInfo?: {
29+
version?: string;
30+
};
31+
};
32+
2733
test.describe("TC-165 SQLite mode business zero impact", () => {
2834
requireSQLiteE2E();
2935

30-
test("TC-165a~c: user CRUD, execution log list, and source plugin lifecycle work on SQLite", async () => {
36+
test("TC-165a~d: user CRUD, execution log list, source plugin lifecycle, and monitor database version work on SQLite", async () => {
3137
const api = await createAdminApiContext();
3238
const username = `sqlite_e2e_${Date.now()}`;
3339
let createdUserId = 0;
@@ -99,6 +105,12 @@ test.describe("TC-165 SQLite mode business zero impact", () => {
99105
expect(plugin?.installed).toBe(1);
100106
expect(plugin?.enabled).toBe(1);
101107

108+
const monitor = await expectApiSuccess<ServerMonitorResult>(
109+
await api.get("monitor/server"),
110+
"query server monitor in SQLite mode",
111+
);
112+
expect(monitor.dbInfo?.version ?? "").toMatch(/^SQLite\s+\S+/);
113+
102114
await updatePluginStatus(api, sqliteSourcePluginId, false);
103115
plugin = await findPlugin(api, sqliteSourcePluginId);
104116
expect(plugin?.enabled).toBe(0);

openspec/changes/sqlite-database-support/specs/database-dialect-abstraction/spec.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
### Requirement: 宿主必须通过统一的方言抽象层收敛数据库引擎差异
44

5-
系统 SHALL 在 `apps/lina-core/pkg/dialect/` 提供公共稳定的 `Dialect` 接口与方言辅助能力作为数据库引擎差异的唯一收敛点。所有数据库引擎相关的差异化行为(DDL 转译、数据库准备、集群能力查询、启动期钩子、驱动错误分类)必须通过该包暴露,业务模块(`controller` / `service` / `model` / `dao`)不得在自身代码路径中出现 `if isMySQL / if isSQLite` 等数据库引擎判断。`pkg/dialect` 的公开签名 SHALL 只依赖稳定窄接口,不得暴露宿主 `internal` 包中的具体服务类型,也不得导出 MySQL / SQLite 方言具体实现类型;具体方言实现应收敛在 `pkg/dialect/internal/mysql``pkg/dialect/internal/sqlite` 等内部子包中,由公共工厂与公共门面能力统一委托。
5+
系统 SHALL 在 `apps/lina-core/pkg/dialect/` 提供公共稳定的 `Dialect` 接口与方言辅助能力作为数据库引擎差异的唯一收敛点。所有数据库引擎相关的差异化行为(DDL 转译、数据库准备、集群能力查询、启动期钩子、驱动错误分类、数据库版本查询)必须通过该包暴露,业务模块(`controller` / `service` / `model` / `dao`)不得在自身代码路径中出现 `if isMySQL / if isSQLite` 等数据库引擎判断。`pkg/dialect` 的公开签名 SHALL 只依赖稳定窄接口,不得暴露宿主 `internal` 包中的具体服务类型,也不得导出 MySQL / SQLite 方言具体实现类型;具体方言实现应收敛在 `pkg/dialect/internal/mysql``pkg/dialect/internal/sqlite` 等内部子包中,由公共工厂与公共门面能力统一委托。
66

77
#### Scenario: 业务模块不感知数据库引擎差异
88
- **** 业务模块(如 `user` / `role` / `dict` / `kvcache` / `locker`)通过 DAO 层执行查询、写入、更新、删除操作时
99
- **** 业务代码不包含针对数据库引擎的分支判断
1010
- **** 同一份业务代码在 MySQL 和 SQLite 两种引擎下行为一致
1111

1212
#### Scenario: 所有方言相关行为通过 Dialect 接口暴露
13-
- **** 宿主需要执行"DDL 转译 / 数据库准备 / 集群能力查询 / 启动期钩子"中的任一行为时
13+
- **** 宿主需要执行"DDL 转译 / 数据库准备 / 集群能力查询 / 启动期钩子 / 数据库版本查询"中的任一行为时
1414
- **** 调用方通过 `dialect.From(link)` 获取当前方言实例
1515
- **** 调用方仅依赖 `Dialect` 接口的方法签名,不依赖具体实现的内部细节
1616

@@ -26,6 +26,14 @@
2626
- **** 调用方不得硬编码 MySQL / SQLite 错误文案、错误码或具体驱动错误类型
2727
- **** `pkg/dialect` 使用驱动暴露的结构化错误码进行分类,错误文案匹配最多只能作为方言包内部的显式兜底
2828

29+
#### Scenario: 数据库版本查询由 dialect 公共包提供
30+
- **** 宿主系统信息或 `monitor-server` 服务监控需要展示数据库版本时
31+
- **** 调用方通过 `dialect.DatabaseVersion(ctx, db)` 或等价 `Dialect` 方法查询
32+
- **** MySQL 方言使用 MySQL 可执行的版本查询语句
33+
- **** SQLite 方言使用 SQLite 可执行的版本查询语句
34+
- **** SQLite 模式下不得执行 `SELECT VERSION()` 或因为缺少 `VERSION()` 函数而返回空版本
35+
- **** 返回给页面的版本文本必须包含数据库引擎名称与非空版本号
36+
2937
#### Scenario: dialect 公共包不暴露宿主 internal 具体类型
3038
- **** 插件生命周期、初始化命令或工具链代码导入 `apps/lina-core/pkg/dialect`
3139
- **** 公开接口不要求调用方引用 `apps/lina-core/internal/...` 下的具体服务类型

0 commit comments

Comments
 (0)