From daa5948ed0c4f098dcd3bb5a7188dc471569529b Mon Sep 17 00:00:00 2001 From: tc6-01 Date: Sat, 15 Nov 2025 18:44:46 +0800 Subject: [PATCH] =?UTF-8?q?feature(sqlite):=20egorm=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=20sqlite=20=E6=96=87=E4=BB=B6=E6=A8=A1=E5=BC=8F=E4=B8=8E?= =?UTF-8?q?=E5=86=85=E5=AD=98=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + examples/sqlite/main.go | 56 +++++++++++++++++++++++ go.mod | 2 + internal/dsn/sqlite.go | 91 +++++++++++++++++++++++++++++++++++++ internal/dsn/sqlite_test.go | 90 ++++++++++++++++++++++++++++++++++++ 5 files changed, 241 insertions(+) create mode 100644 examples/sqlite/main.go create mode 100644 internal/dsn/sqlite.go create mode 100644 internal/dsn/sqlite_test.go diff --git a/README.md b/README.md index 1085a31..0afd030 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ type Config struct { insecureSkipVerify=true ``` + ## 5.2 优雅的Debug 通过开启``debug``配置和命令行的``export EGO_DEBUG=true``,我们就可以在测试环境里看到请求里的配置名、地址、耗时、请求数据、响应数据 ![image](./docs/images/ego_debug.png) @@ -125,6 +126,7 @@ func testDB() error { return err } ``` + ## 6 GORM的日志 任何gorm的请求都会记录gorm的错误access日志,如果需要对gorm的日志做定制化处理,可参考以下使用方式。 diff --git a/examples/sqlite/main.go b/examples/sqlite/main.go new file mode 100644 index 0000000..527c860 --- /dev/null +++ b/examples/sqlite/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "context" + + "github.com/gotomicro/ego" + "github.com/gotomicro/ego/core/elog" + + "github.com/ego-component/egorm" +) + +// 运行方式: +// export EGO_DEBUG=true && go run main.go --config=config.toml +type KV struct { + ID int `gorm:"primaryKey" json:"id"` + K string `gorm:"uniqueIndex;not null" json:"k"` + V string `gorm:"not null" json:"v"` +} + +func (KV) TableName() string { return "kv" } + +var dbs []*egorm.Component + +func main() { + if err := ego.New().Invoker( + openDB, + run, + ).Run(); err != nil { + elog.Error("startup", elog.Any("err", err)) + } +} + +func openDB() error { + dbs = []*egorm.Component{ + egorm.Load("sqlite.test").Build(), + egorm.Load("sqlite.share.memory").Build(), + egorm.Load("sqlite.nonshared.memory").Build(), + } + for _, db := range dbs { + if err := db.AutoMigrate(&KV{}); err != nil { + return err + } + } + return nil +} + +func run() error { + ctx := context.Background() + for _, db := range dbs { + _ = db.WithContext(ctx).Create(&KV{K: "hello", V: "world"}).Error + var out KV + err := db.WithContext(ctx).Where("k = ?", "hello").First(&out).Error + elog.Info("kv", elog.String("k", out.K), elog.String("v", out.V), elog.FieldErr(err)) + } + return nil +} diff --git a/go.mod b/go.mod index 8bb31ea..f363faa 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( gorm.io/driver/clickhouse v0.3.2 gorm.io/driver/mysql v1.5.7 gorm.io/driver/postgres v1.3.5 + gorm.io/driver/sqlite v1.5.0 gorm.io/driver/sqlserver v1.5.1 gorm.io/gorm v1.25.7 gorm.io/hints v1.1.2 @@ -50,6 +51,7 @@ require ( github.com/jackc/pgx/v4 v4.16.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-sqlite3 v1.14.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/microsoft/go-mssqldb v1.1.0 // indirect github.com/mitchellh/mapstructure v1.3.2 // indirect diff --git a/internal/dsn/sqlite.go b/internal/dsn/sqlite.go new file mode 100644 index 0000000..f8a598f --- /dev/null +++ b/internal/dsn/sqlite.go @@ -0,0 +1,91 @@ +package dsn + +import ( + "net/url" + "path/filepath" + "strings" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/schema" + + "github.com/ego-component/egorm/manager" +) + +var ( + _ manager.DSNParser = (*SqliteDSNParser)(nil) +) + +type SqliteDSNParser struct { +} + +func init() { + manager.Register(&SqliteDSNParser{}) +} + +func (p *SqliteDSNParser) Scheme() string { + return "sqlite" +} + +func (p *SqliteDSNParser) NamingStrategy() schema.Namer { + return nil +} + +func (p *SqliteDSNParser) GetDialector(dsn string) gorm.Dialector { + return sqlite.Open(dsn) +} + +// ParseDSN supports typical gorm sqlite DSN strings: +// - file based: "test.db", "/abs/path/to/test.db", "file:test.db?cache=shared&_fk=1" +// - memory: ":memory:", "file::memory:?cache=shared" +func (p *SqliteDSNParser) ParseDSN(dsn string) (cfg *manager.DSN, err error) { + cfg = new(manager.DSN) + cfg.Params = map[string]string{} + + // Normalize for parsing query parameters + raw := dsn + if strings.HasPrefix(raw, "file:") { + if idx := strings.IndexByte(raw, '?'); idx >= 0 { + query := raw[idx+1:] + for _, kv := range strings.Split(query, "&") { + parts := strings.SplitN(kv, "=", 2) + if len(parts) != 2 { + continue + } + val, decodeErr := url.QueryUnescape(parts[1]) + if decodeErr != nil { + continue + } + cfg.Params[parts[0]] = val + } + } + } + + // Determine DBName for logging/metrics only + switch { + // memory mode + case dsn == ":memory:": + case strings.Contains(dsn, "::memory:"): + cfg.DBName = "memory" + default: + trimmed := dsn + // strip "file:" prefix for name extraction + if strings.HasPrefix(trimmed, "file:") { + if idx := strings.IndexByte(trimmed, '?'); idx >= 0 { + trimmed = trimmed[:idx] + } + trimmed = strings.TrimPrefix(trimmed, "file:") + } else { + // if path like "dir/db.sqlite" keep base + if idx := strings.IndexByte(trimmed, '?'); idx >= 0 { + trimmed = trimmed[:idx] + } + } + base := filepath.Base(trimmed) + if base == "" || base == "." || base == "/" { + base = "sqlite" + } + cfg.DBName = base + } + return +} diff --git a/internal/dsn/sqlite_test.go b/internal/dsn/sqlite_test.go new file mode 100644 index 0000000..1b8c80c --- /dev/null +++ b/internal/dsn/sqlite_test.go @@ -0,0 +1,90 @@ +package dsn + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSqliteDSNParser_ParseDSN(t *testing.T) { + tests := []struct { + name string + dsn string + expected *struct { + DBName string + Addr string + Params map[string]string + } + }{ + { + name: "memory database", + dsn: ":memory:", + expected: &struct { + DBName string + Addr string + Params map[string]string + }{ + DBName: "memory", + Addr: "", + Params: map[string]string{}, + }, + }, + { + name: "file memory with cache", + dsn: "file::memory:?cache=shared", + expected: &struct { + DBName string + Addr string + Params map[string]string + }{ + DBName: "memory", + Addr: "", + Params: map[string]string{ + "cache": "shared", + }, + }, + }, + { + name: "file based database", + dsn: "test.db", + expected: &struct { + DBName string + Addr string + Params map[string]string + }{ + DBName: "test.db", + Addr: "", + Params: map[string]string{}, + }, + }, + { + name: "file URI with parameters", + dsn: "file:./data/app.db?cache=shared&_journal_mode=WAL&_busy_timeout=5000&_fk=1", + expected: &struct { + DBName string + Addr string + Params map[string]string + }{ + DBName: "app.db", + Addr: "", + Params: map[string]string{ + "cache": "shared", + "_journal_mode": "WAL", + "_busy_timeout": "5000", + "_fk": "1", + }, + }, + }, + } + + dsnParser := SqliteDSNParser{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg, err := dsnParser.ParseDSN(tt.dsn) + assert.NoError(t, err) + assert.Equal(t, tt.expected.DBName, cfg.DBName) + assert.Equal(t, tt.expected.Addr, cfg.Addr) + assert.Equal(t, tt.expected.Params, cfg.Params) + }) + } +}