From 8361ba934409ef1cbffe59a49d07c808477ca563 Mon Sep 17 00:00:00 2001 From: homerpan Date: Fri, 30 May 2025 11:20:54 +0800 Subject: [PATCH] Merge gorm to tag mongodb/v0.2.1 --- gorm/README.md | 238 +++++++++++-- gorm/client.go | 40 ++- gorm/client_test.go | 170 +++++++++- gorm/docs/architecture.md | 100 ++++++ gorm/docs/resource/architecture.png | Bin 0 -> 33285 bytes gorm/docs/resource/introduce.png | Bin 0 -> 11096 bytes gorm/examples/clickhouse/README.md | 13 + gorm/examples/clickhouse/init.sql | 10 + gorm/examples/clickhouse/main.go | 45 +++ gorm/examples/clickhouse/run-docker.sh | 12 + gorm/examples/clickhouse/trpc_go.yaml | 29 ++ gorm/examples/go.mod | 72 ++++ gorm/examples/go.sum | 361 ++++++++++++++++++++ gorm/examples/mysql/README.md | 13 + gorm/examples/mysql/init.sql | 9 + gorm/examples/mysql/main.go | 44 +++ gorm/examples/mysql/run-docker.sh | 11 + gorm/examples/mysql/trpc_go.yaml | 36 ++ gorm/examples/sqlite/README.md | 10 + gorm/examples/sqlite/main.go | 48 +++ gorm/examples/sqlite/run-docker.sh | 10 + gorm/examples/sqlite/trpc_go.yaml | 32 ++ gorm/go.mod | 20 +- gorm/go.sum | 36 +- gorm/log.go | 27 +- gorm/log_mock.go | 2 +- gorm/log_test.go | 3 +- gorm/mock/client_mock.go | 229 +++++++++++++ gorm/plugin.go | 63 +++- gorm/plugin_test.go | 277 ++++++++++++++- gorm/transport.go | 149 ++++++--- gorm/transport_test.go | 445 +++++++++++++++---------- 32 files changed, 2230 insertions(+), 324 deletions(-) create mode 100644 gorm/docs/architecture.md create mode 100644 gorm/docs/resource/architecture.png create mode 100644 gorm/docs/resource/introduce.png create mode 100755 gorm/examples/clickhouse/README.md create mode 100644 gorm/examples/clickhouse/init.sql create mode 100644 gorm/examples/clickhouse/main.go create mode 100755 gorm/examples/clickhouse/run-docker.sh create mode 100644 gorm/examples/clickhouse/trpc_go.yaml create mode 100644 gorm/examples/go.mod create mode 100644 gorm/examples/go.sum create mode 100755 gorm/examples/mysql/README.md create mode 100644 gorm/examples/mysql/init.sql create mode 100644 gorm/examples/mysql/main.go create mode 100755 gorm/examples/mysql/run-docker.sh create mode 100644 gorm/examples/mysql/trpc_go.yaml create mode 100755 gorm/examples/sqlite/README.md create mode 100644 gorm/examples/sqlite/main.go create mode 100755 gorm/examples/sqlite/run-docker.sh create mode 100644 gorm/examples/sqlite/trpc_go.yaml create mode 100644 gorm/mock/client_mock.go diff --git a/gorm/README.md b/gorm/README.md index 7c6d5c1..444e2a6 100644 --- a/gorm/README.md +++ b/gorm/README.md @@ -23,7 +23,66 @@ Currently, the most popular ORM framework in Go is Gorm, which has a high level ## Quick Start -Currently, it supports MySQL/ClickHouse and has made adjustments in the code to quickly iterate and support other types of databases. +Add trpc_go.yaml framework configuration: + +```yaml +client: + service: + - name: trpc.mysql.server.service + # Reference: https://github.com/go-sql-driver/mysql?tab=readme-ov-file#dsn-data-source-name + target: dsn://root:123456@tcp(127.0.0.1:3306)/mydb?charset=utf8mb4&parseTime=True +``` + +Code implementation: + +```go +package main + +import ( + "github.com/trpc-group/trpc-database/gorm" + "trpc.group/trpc-go/trpc-go" + "trpc.group/trpc-go/trpc-go/log" +) + +type User struct { + ID int + Username string +} + +func main() { + _ = trpc.NewServer() + + cli, err := gorm.NewClientProxy("trpc.mysql.server.service") + if err != nil { + panic(err) + } + + // Create record + insertUser := User{Username: "gorm-client"} + result := cli.Create(&insertUser) + log.Infof("inserted data's primary key: %d, err: %v", insertUser.ID, result.Error) + + // Query record + var queryUser User + if err := cli.First(&queryUser).Error; err != nil { + panic(err) + } + log.Infof("query user: %+v", queryUser) + + // Delete record + deleteUser := User{ID: insertUser.ID} + if err := cli.Delete(&deleteUser).Error; err != nil { + panic(err) + } + log.Info("delete record succeed") + + // For more use cases, see https://gorm.io/docs/create.html +} +``` + +Complete example: [mysql-example](examples/mysql/main.go) + +## Complete Configuration ### tRPC-Go Framework Configuration @@ -52,43 +111,109 @@ plugins: # Plugin configuration database: gorm: # Default connection pool configuration for all database connections - max_idle: 20 # Maximum number of idle connections - max_open: 100 # Maximum number of open connections - max_lifetime: 180000 # Maximum connection lifetime (in milliseconds) + max_idle: 20 # Maximum number of idle connections (default 10 if not set or set to 0); if negative, no idle connections are retained + max_open: 100 # Maximum number of open connections (default 10000 if not set or set to 0); if negative, no limit on open connections + max_lifetime: 180000 # Maximum connection lifetime in milliseconds (default 3min); if negative, connections are not closed due to age + driver_name: mysql # Driver used for the connection (empty by default, import the corresponding driver if specifying) + logger: # this feature is supported in versions >= v0.2.2 + slow_threshold: 200 # Slow query threshold in milliseconds, 0 means no slow query logging (default 0) + colorful: false # Whether to colorize the logs (default false) + ignore_record_not_found_error: false # Whether to ignore errors when records are not found (default false) + log_level: 4 # Log level: 1:Silent, 2:Error, 3:Warn, 4:Info (default no logging) + max_sql_size: 100 # Maximum SQL statement length for truncation, 0 means no limit (default 0) # Individual connection pool configuration for specific database connections service: - - name: trpc.mysql.xxxx.xxxx + - name: trpc.mysql.server.service max_idle: 10 max_open: 50 max_lifetime: 180000 - driver_name: xxx # Driver used for the connection (empty by default, import the corresponding driver if specifying) + driver_name: mysql # Driver used for the connection (empty by default, import the corresponding driver if specifying) + logger: + slow_threshold: 1000 + colorful: true + ignore_record_not_found_error: true + log_level: 4 +``` + +## Advanced Features + +### Polaris Routing and Addressing +If you need to use Polaris routing and addressing, first add: + +`import _ "github.com/trpc-group/trpc-naming-polaris"` + +Then use the `gorm+polaris` scheme in the framework configuration target, and write the Polaris service name in the original `host:port` position. + +```yaml +client: + service: + - name: trpc.mysql.xxxx.xxxx + namespace: Production + # Use gorm+polaris scheme, write Polaris service name in original host:port position + target: gorm+polaris://root:123456@tcp(${polaris_name})/mydb?parseTime=True ``` -### gorm Logger Configuration (Optional) +### ClickHouse Integration -You can configure logging parameters using plugins. Here is an example of configuring logging parameters using YAML syntax: +Add trpc_go.yaml framework configuration: +Note that service.name uses the four-segment format `trpc.${app}.${server}.${service}`, where `${app}` should be `clickhouse`. ```yaml -plugins: # Plugin configuration +client: + service: + - name: trpc.clickhouse.server.service + # Reference: https://github.com/ClickHouse/clickhouse-go?tab=readme-ov-file#dsn + target: dsn://clickhouse://default:@127.0.0.1:9000/mydb?dial_timeout=200ms&max_execution_time=60 + # In gorm/v0.2.2 and earlier, using this DSN format would prompt username or password errors, + # you should use the pre-change DSN format: + # target: dsn://tcp://localhost:9000?username=user&password=qwerty&database=clicks&read_timeout=10&write_timeout=20 +``` + +Complete example: [clickhouse-example](examples/clickhouse/main.go) + +### SQLite Integration + +Add trpc_go.yaml framework configuration: + +Note that service.name uses the four-segment format `trpc.${app}.${server}.${service}`, where `${app}` should be `sqlite`. + +```yaml +client: + service: + - name: trpc.sqlite.server.service + # Reference: https://github.com/mattn/go-sqlite3?tab=readme-ov-file#dsn-examples + # Execute "sqlite3 mysqlite.db" in the current directory to create the database + target: dsn://file:mysqlite.db +``` + +Add plugin configuration, keep the service name consistent, and configure `driver_name: sqlite3`. + +```yaml +plugins: database: gorm: - # Default logging configuration for all database connections - logger: - slow_threshold: 1000 # Slow query threshold in milliseconds - colorful: true # Whether to colorize the logs - ignore_record_not_found_error: true # Whether to ignore errors when records are not found - log_level: 4 # 1: Silent, 2: Error, 3: Warn, 4: Info - # Individual logging configuration for specific database connections service: - - name: trpc.mysql.xxxx.xxxx - logger: - slow_threshold: 1000 - colorful: true - ignore_record_not_found_error: true - log_level: 4 + # Configuration effective for trpc.sqlite.server.service client + - name: trpc.sqlite.server.service + driver_name: sqlite3 # Requires import "github.com/mattn/go-sqlite3" +``` + +Complete example: [sqlite-example](examples/sqlite/main.go) + +### PostgreSQL Integration + +Add trpc_go.yaml framework configuration. +Note that service.name uses the four-segment format `trpc.${app}.${server}.${service}`, where `${app}` should be `postgres`. + +```yaml +client: + service: + - name: trpc.postgres.server.service + # Reference: https://github.com/jackc/pgx?tab=readme-ov-file#example-usage + target: dsn://postgres://username:password@localhost:5432/database_name ``` ### Code Implementation @@ -136,6 +261,7 @@ Example of logging: gormDB := gorm.NewClientProxy("trpc.mysql.test.test") gormDB.Debug().Where("current_owners = ?", "xxxx").Where("id < ?", xxxx).Find(&owners) ``` + ### Context When using the database plugin, you may need to report trace information and pass a context with the request. Gorm provides the WithContext method to include a context. @@ -166,6 +292,9 @@ Example: gormDB.Debug().Where("current_owners = ?", "xxxx").Where("id < ?", xxxx).Find(&owners) ``` +### Implementation Details + +See the specific [implementation details](docs/architecture.md) of the gorm plugin. ## Implementation Approach @@ -197,14 +326,39 @@ Thus, the Client becomes a custom connection that satisfies gorm's requirements, Additionally, there is a TxClient used for transaction handling, which implements both the ConnPool and TxCommitter interfaces defined by gorm. +## Notes + +### Timeout Settings Not Effective + +If users add timeout in the framework configuration, the tRPC-Go framework will create a new context when making calls and cancel the context after the call ends. This feature will cause "Context Canceled" errors when reading data from the `Row` interface, so the current plugin will set timeout to zero. + +```yaml +client: + service: + - name: trpc.mysql.xxxx.xxxx # initialized as mysql + timeout: 1000 # timeout configuration will not take effect +``` + +If you need to configure request timeout, you can use context.WithTimeout() to control the context yourself. +If you want to configure connection establishment timeout, connection read/write timeout, you can consider configuring related parameters in the DSN, for example, for MySQL you can configure [`readTimeout`, `writeTimeout` and `timeout`](https://github.com/go-sql-driver/mysql?tab=readme-ov-file#connection-pool-and-timeouts) in the DSN. + +```yaml +client: + service: + - name: trpc.mysql.server.service + # Add readTimeout to DSN parameters, reference: https://github.com/go-sql-driver/mysql?tab=readme-ov-file#dsn-data-source-name + target: dsn://root:123456@tcp(127.0.0.1:3306)/mydb?readTimeout=100ms +``` ## Related Links: * Custom Connection in GORM:https://gorm.io/zh_CN/docs/connecting_to_the_database.html ## FAQ + ### How to print specific SQL statements and results? -This plugin has implemented the TRPC Logger for GORM, as mentioned in the "Logging" section above. If you are using the default NewClientProxy, you can use the Debug() method before the request to output the SQL statements to the TRPC log with the Info level. +This plugin has implemented the TRPC Logger for GORM. You only need to configure `plugin.database.gorm.logger` configuration (requires plugin version >= v0.2.2) to print specific SQL statements to tRPC-Go logs. +Or you can add `Debug()` before the request to output to logs at Info level. Example: ``` @@ -222,15 +376,49 @@ gormDB.WithContext(ctx).Where("current_owners = ?", "xxxx").Where("id < ?", xxxx ### How to set the isolation level for transactions? When starting a transaction, you can provide sql.TxOptions in the Begin method to set the isolation level. Alternatively, you can set it manually after starting the transaction using tx.Exec. +### Soft delete not working after switching from native gorm to tRPC gorm plugin + +This is because gorm changed the soft delete method after the major version upgrade from jinzhu/gorm to gorm.io/gorm. + +For the new soft delete method, see the documentation https://gorm.io/docs/delete.html#Soft-Delete + +Using gorm.DeleteAt can directly be compatible with jinzhu/gorm soft delete. + +### How to handle Context Canceled errors + +**Update to the latest version to resolve this issue. The latest version will force timeout to 0.** + +This problem is generally caused by setting a global timeout in the trpc framework client. A common trpc framework configuration example is as follows: + +```yaml +client: + timeout: 1000 # Need to remove this + service: + - name: xxxxx + protocol: trpc + timeout: 1000 # Set timeout separately for other services +``` + +The client-level timeout is a global timeout. All trpc clients that don't have a separate timeout setting will use this setting. The old version of this plugin would report errors due to context being canceled as long as timeout was set. + ### Currently, only MySQL, ClickHouse, and PostgreSQL are supported. +Parse the `client.service.name` name, which needs to use the standard four-segment name format. For example, `trpc.mysql.xxx.xxx` initializes the `mysql driver`; `trpc.clickhouse.xxx.xxx` initializes the `clickhouse driver`; `trpc.postgres.xxx.xxx` initializes the `postgres driver(pgx)`. + +For non-standard four-segment names, it defaults to initializing the `mysql driver`. + ### When using GORM transactions, only the Begin method goes through the tRPC call chain. + +**Update to the latest version to resolve this issue. v0.1.3 version has resolved this issue.** + This is a design compromise made to reduce complexity. In this plugin, when calling BeginTx, it directly returns the result of sql.DB.BeginTx(), which is an already opened transaction. Subsequent transaction operations are handled by that transaction. Considering that this plugin is mainly designed for connecting to MySQL instances, this approach can reduce some complexity while ensuring normal operation. However, considering that there may be services that require all database requests to go through the tRPC filter, the mechanism will be modified in the future to make all requests within the transaction go through the tRPC request flow. If inaccurate reporting of data is caused by this behavior, you can disable the GORM transaction optimization to ensure that all requests that do not explicitly use transactions go through the basic reporting methods. +> To ensure data consistency, GORM performs write operations (create, update, delete) within transactions. If you don't have this requirement, you can disable it during initialization. + Example: ```go connPool := gormplugin.NewConnPool("trpc.mysql.test.test") @@ -244,6 +432,4 @@ gormDB, err := gorm.Open( SkipDefaultTransaction: true, // Disable GORM transaction optimization }, ) -``` - - +``` \ No newline at end of file diff --git a/gorm/client.go b/gorm/client.go index 0d1d6d1..2ac26bb 100644 --- a/gorm/client.go +++ b/gorm/client.go @@ -14,7 +14,6 @@ import ( "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" - "trpc.group/trpc-go/trpc-go" "trpc.group/trpc-go/trpc-go/client" "trpc.group/trpc-go/trpc-go/codec" @@ -55,6 +54,11 @@ func (op OpEnum) String() string { // ConnPool implements the gorm.ConnPool interface as well as transaction and Ping functionality. type ConnPool interface { + Prepare(query string) (*sql.Stmt, error) + Exec(query string, args ...interface{}) (sql.Result, error) + Query(query string, args ...interface{}) (*sql.Rows, error) + QueryRow(query string, args ...interface{}) *sql.Row + PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) @@ -73,8 +77,8 @@ type Request struct { Op OpEnum Query string Args []interface{} - Tx *sql.Tx - TxOptions *sql.TxOptions + Tx *sql.Tx `json:"-"` + TxOptions *sql.TxOptions `json:"-"` } // Response is the result returned by the tRPC framework. @@ -83,8 +87,8 @@ type Response struct { Stmt *sql.Stmt Row *sql.Row Rows *sql.Rows - Tx *sql.Tx - DB *sql.DB + Tx *sql.Tx `json:"-"` + DB *sql.DB `json:"-"` } // Client encapsulates the tRPC client and implements the ConnPoolBeginner interface. @@ -170,6 +174,7 @@ var NewClientProxy = func(name string, opts ...client.Option) (*gorm.DB, error) } // Support for other DBs. // Compatibility logic, defaulting to MySQL. + // internal repository issues/235 dbEngineType := splitServiceName[1] // Support postgresql. switch dbEngineType { @@ -233,6 +238,10 @@ func handleReqArgs(mreq *Request) error { // so here the value needs to be passed. for k, arg := range mreq.Args { if valuer, ok := arg.(driver.Valuer); ok { + // cannot call Value on nil Valuer + if reflect.ValueOf(valuer).Kind() == reflect.Ptr && reflect.ValueOf(valuer).IsNil() { + continue + } v, err := valuer.Value() if err != nil { return err @@ -251,6 +260,11 @@ func (gc *Client) PrepareContext(ctx context.Context, query string) (*sql.Stmt, return prepareContext(gc, ctx, query) } +// Prepare implements the ConnPool.Prepare method. +func (gc *Client) Prepare(query string) (*sql.Stmt, error) { + return prepareContext(gc, context.Background(), query) +} + func prepareContext(cp gorm.ConnPool, ctx context.Context, query string) (*sql.Stmt, error) { mreq := &Request{ Op: OpPrepareContext, @@ -269,6 +283,11 @@ func (gc *Client) ExecContext(ctx context.Context, query string, args ...interfa return execContext(gc, ctx, query, args...) } +// Exec implements the ConnPool.Exec method. +func (gc *Client) Exec(query string, args ...interface{}) (sql.Result, error) { + return execContext(gc, context.Background(), query, args...) +} + func execContext(cp gorm.ConnPool, ctx context.Context, query string, args ...interface{}) (sql.Result, error) { mreq := &Request{ Op: OpExecContext, @@ -288,6 +307,11 @@ func (gc *Client) QueryContext(ctx context.Context, query string, args ...interf return queryContext(gc, ctx, query, args...) } +// Query implements the ConnPool.Query method. +func (gc *Client) Query(query string, args ...interface{}) (*sql.Rows, error) { + return queryContext(gc, context.Background(), query, args...) +} + func queryContext(cp gorm.ConnPool, ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { mreq := &Request{ Op: OpQueryContext, @@ -307,6 +331,11 @@ func (gc *Client) QueryRowContext(ctx context.Context, query string, args ...int return queryRowContext(gc, ctx, query, args...) } +// QueryRow Context implements the ConnPool.QueryRow method. +func (gc *Client) QueryRow(query string, args ...interface{}) *sql.Row { + return queryRowContext(gc, context.Background(), query, args...) +} + func queryRowContext(cp gorm.ConnPool, ctx context.Context, query string, args ...interface{}) *sql.Row { mreq := &Request{ Op: OpQueryRowContext, @@ -317,6 +346,7 @@ func queryRowContext(cp gorm.ConnPool, ctx context.Context, query string, args . if err := handleReq(ctx, cp, mreq, mrsp); err != nil { // An error occurred during execution, // and sql.Row.err needs to be assigned a value to avoid panic during chain calls to QueryRowContent().Scan(). + // internal repository issues/182 row := &sql.Row{} v := reflect.ValueOf(row) errField := v.Elem().FieldByName("err") diff --git a/gorm/client_test.go b/gorm/client_test.go index 2a31e19..2e4c1d6 100644 --- a/gorm/client_test.go +++ b/gorm/client_test.go @@ -9,17 +9,15 @@ import ( "testing" "time" + "github.com/DATA-DOG/go-sqlmock" + "github.com/golang/mock/gomock" + . "github.com/smartystreets/goconvey/convey" "gorm.io/driver/mysql" "gorm.io/gorm" - "trpc.group/trpc-go/trpc-go/client" "trpc.group/trpc-go/trpc-go/client/mockclient" "trpc.group/trpc-go/trpc-go/codec" "trpc.group/trpc-go/trpc-go/transport" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/golang/mock/gomock" - . "github.com/smartystreets/goconvey/convey" ) const ( @@ -37,12 +35,6 @@ func TestUnit_NewConnPool_P0(t *testing.T) { }) } -func setMockError(mockClient *mockclient.MockClient) { - mockClient.EXPECT(). - Invoke(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - Return(fmt.Errorf("fake error")) -} - func TestUnit_gormCli_PrepareContext_P0(t *testing.T) { Convey("TestUnit_gormCli_PrepareContext_P0", t, func() { mockCtrl := gomock.NewController(t) @@ -53,7 +45,9 @@ func TestUnit_gormCli_PrepareContext_P0(t *testing.T) { gormClient.(*Client).Client = mockClient Convey("Invoke error", func() { - setMockError(mockClient) + mockClient.EXPECT(). + Invoke(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(fmt.Errorf("fake error")) _, err := gormClient.PrepareContext(context.Background(), "select * from table limit 1") So(err, ShouldNotBeNil) @@ -78,6 +72,43 @@ func TestUnit_gormCli_PrepareContext_P0(t *testing.T) { }) } +func TestUnit_gormCli_Prepare_P0(t *testing.T) { + Convey("TestUnit_gormCli_Prepare_P0", t, func() { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockClient := mockclient.NewMockClient(mockCtrl) + + gormClient := NewConnPool(SQLName) + gormClient.(*Client).Client = mockClient + + Convey("Invoke error", func() { + mockClient.EXPECT(). + Invoke(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(fmt.Errorf("fake error")) + + _, err := gormClient.Prepare("select * from table limit 1") + So(err, ShouldNotBeNil) + So(err.Error(), ShouldEqual, "fake error") + }) + Convey("Invoke success", func() { + var mockSQLStmt = &sql.Stmt{} + mockClient.EXPECT(). + Invoke(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Do(func(ctx context.Context, reqbody interface{}, rspbody interface{}, opt ...client.Option) error { + msg := codec.Message(ctx) + rsp, ok := msg.ClientRspHead().(*Response) + So(ok, ShouldBeTrue) + rsp.Stmt = mockSQLStmt + return nil + }) + + result, err := gormClient.Prepare("select * from table limit 1") + So(err, ShouldBeNil) + So(result, ShouldEqual, mockSQLStmt) + }) + }) +} + func TestUnit_gormCli_ExecContext_P0(t *testing.T) { Convey("TestUnit_gormCli_ExecContext_P0", t, func() { mockCtrl := gomock.NewController(t) @@ -114,6 +145,42 @@ func TestUnit_gormCli_ExecContext_P0(t *testing.T) { }) } +func TestUnit_gormCli_Exec_P0(t *testing.T) { + Convey("TestUnit_gormCli_Exec_P0", t, func() { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockClient := mockclient.NewMockClient(mockCtrl) + + gormClient := NewConnPool(SQLName) + gormClient.(*Client).Client = mockClient + + Convey("Invoke error", func() { + mockClient.EXPECT(). + Invoke(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(fmt.Errorf("fake error")) + + _, err := gormClient.Exec("select * from table limit 1") + So(err, ShouldNotBeNil) + So(err.Error(), ShouldEqual, "fake error") + }) + Convey("Invoke success", func() { + mockClient.EXPECT(). + Invoke(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Do(func(ctx context.Context, reqbody interface{}, rspbody interface{}, opt ...client.Option) error { + msg := codec.Message(ctx) + rsp, ok := msg.ClientRspHead().(*Response) + So(ok, ShouldBeTrue) + rsp.Result = driver.RowsAffected(1) + return nil + }) + + result, err := gormClient.Exec("select * from table limit 1") + So(err, ShouldBeNil) + So(result, ShouldResemble, driver.RowsAffected(1)) + }) + }) +} + func TestUnit_gormCli_QueryContext_P0(t *testing.T) { Convey("TestUnit_gormCli_ExecContext_P0", t, func() { mockCtrl := gomock.NewController(t) @@ -124,7 +191,9 @@ func TestUnit_gormCli_QueryContext_P0(t *testing.T) { gormClient.(*Client).Client = mockClient Convey("Invoke error", func() { - setMockError(mockClient) + mockClient.EXPECT(). + Invoke(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(fmt.Errorf("fake error")) _, err := gormClient.QueryContext(context.Background(), "select * from table limit 1") So(err, ShouldNotBeNil) @@ -149,6 +218,43 @@ func TestUnit_gormCli_QueryContext_P0(t *testing.T) { }) } +func TestUnit_gormCli_Query_P0(t *testing.T) { + Convey("TestUnit_gormCli_ExecContext_P0", t, func() { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockClient := mockclient.NewMockClient(mockCtrl) + + gormClient := NewConnPool(SQLName) + gormClient.(*Client).Client = mockClient + + Convey("Invoke error", func() { + mockClient.EXPECT(). + Invoke(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(fmt.Errorf("fake error")) + + _, err := gormClient.Query("select * from table limit 1") + So(err, ShouldNotBeNil) + So(err.Error(), ShouldEqual, "fake error") + }) + Convey("Invoke success", func() { + var mockSQLRows = &sql.Rows{} + mockClient.EXPECT(). + Invoke(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Do(func(ctx context.Context, reqbody interface{}, rspbody interface{}, opt ...client.Option) error { + msg := codec.Message(ctx) + rsp, ok := msg.ClientRspHead().(*Response) + So(ok, ShouldBeTrue) + rsp.Rows = mockSQLRows + return nil + }) + + result, err := gormClient.Query("select * from table limit 1") + So(err, ShouldBeNil) + So(result, ShouldEqual, mockSQLRows) + }) + }) +} + func TestUnit_gormCli_BeginTx_P0(t *testing.T) { Convey("TestUnit_gormCli_BeginTx_P0", t, func() { mockCtrl := gomock.NewController(t) @@ -159,7 +265,9 @@ func TestUnit_gormCli_BeginTx_P0(t *testing.T) { gormClient.(*Client).Client = mockClient Convey("Invoke error", func() { - setMockError(mockClient) + mockClient.EXPECT(). + Invoke(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(fmt.Errorf("fake error")) _, err := gormClient.BeginTx(context.Background(), &sql.TxOptions{}) So(err, ShouldNotBeNil) @@ -196,7 +304,9 @@ func TestUnit_gormCli_GetDB_P0(t *testing.T) { gormClient.(*Client).Client = mockClient Convey("Invoke error", func() { - setMockError(mockClient) + mockClient.EXPECT(). + Invoke(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(fmt.Errorf("fake error")) _, err := gormClient.GetDBConn() So(err, ShouldNotBeNil) @@ -236,7 +346,9 @@ func TestUnit_gormTxCli_Commit_P0(t *testing.T) { } Convey("Invoke error", func() { - setMockError(mockClient) + mockClient.EXPECT(). + Invoke(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(fmt.Errorf("fake error")) err := gormTxClient.Commit() So(err.Error(), ShouldEqual, "fake error") @@ -267,7 +379,9 @@ func TestUnit_gormTxCli_Rollback_P0(t *testing.T) { } Convey("Invoke error", func() { - setMockError(mockClient) + mockClient.EXPECT(). + Invoke(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(fmt.Errorf("fake error")) err := gormTxClient.Rollback() So(err.Error(), ShouldEqual, "fake error") @@ -293,7 +407,9 @@ func TestUnit_gormCli_Ping_P0(t *testing.T) { gormClient.(*Client).Client = mockClient Convey("Invoke error", func() { - setMockError(mockClient) + mockClient.EXPECT(). + Invoke(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(fmt.Errorf("fake error")) err := gormClient.Ping() So(err, ShouldNotBeNil) @@ -321,6 +437,17 @@ func TestUnit_gormCli_QueryRowContext_P0(t *testing.T) { }) } +func TestUnit_gormCli_QueryRow_P0(t *testing.T) { + // Invalid DSN address. + Convey("TestUnit_gormCli_QueryRow_P0", t, func() { + gormClient := NewConnPool(SQLName, client.WithTarget("dns://xxxx")) + Convey("Invoke error", func() { + err := gormClient.QueryRow("id=?", "1").Scan() + So(err, ShouldNotBeNil) + }) + }) +} + func TestUnit_CompleteCall(t *testing.T) { // Mock the database directly to create a complete call chain. Convey("TestUnit_CompleteCall", t, func() { @@ -496,5 +623,12 @@ func TestUnit_handleReqArgs(t *testing.T) { } err = handleReqArgs(req2) So(err, ShouldNotBeNil) + var t3 *testValuer + req3 := &Request{ + Op: OpExecContext, + Args: []interface{}{3, "test3", t3}, + } + err = handleReqArgs(req3) + So(err, ShouldBeNil) }) } diff --git a/gorm/docs/architecture.md b/gorm/docs/architecture.md new file mode 100644 index 0000000..25d0ace --- /dev/null +++ b/gorm/docs/architecture.md @@ -0,0 +1,100 @@ +# Implementation Details + +## Implementation Approach + +The overall implementation of gorm is as follows: + +![Gorm Introduction](resource/introduce.png) + +**gorm interacts with the database through the `*sql.DB` library.** +gorm also supports custom database connection extensions, as mentioned in the official documentation: + +[GORM allows initializing *gorm.DB with an existing database connection](https://gorm.io/docs/connecting_to_the_database.html) + +So the implementation logic is as follows: + +![Architecture Diagram](resource/architecture.png) + +The plugin acts as a bridge between gorm and the tRPC framework, implementing the ConnPool interface that gorm expects while routing all database operations through tRPC's client infrastructure. + +## Specific Implementation + +### Package Structure +``` +gorm/ +├── client.go # Entry point for using this plugin +├── codec.go # Encoding and decoding module +├── plugin.go # Implementation of plugin configuration +├── transport.go # Actual data sending and receiving +└── docs/ + └── architecture.md # This documentation +``` + +### client.go Logic Explanation + +The Client struct corresponding to ConnPool is defined as follows: + +```go +// Client encapsulates trpc client +type Client struct { + ServiceName string + Client client.Client + opts []client.Option +} +``` + +Client implements the following methods: + +```go +//======== ConnPool interface related methods +func (gc *Client) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) +func (gc *Client) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) +func (gc *Client) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) +func (gc *Client) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row +func (gc *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) + +//======== ConnPoolBeginner interface related methods +func (gc *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (gorm.ConnPool, error) + +//======== Ping method +func (gc *Client) Ping() + +// ======== GetDBConnector interface related methods +func (gc *Client) GetDBConn() (*sql.DB, error) +``` + +In the gorm framework, all interactions with the DB are done through the DB.ConnPool field. ConnPool is an interface type, so as long as our Client implements all methods of ConnPool, gorm can use the Client to interact with the DB. + +The definition of ConnPool can be found here: [gorm ConnPool](https://github.com/go-gorm/gorm/blob/master/interfaces.go) + +After practical usage, it was found that implementing only the ConnPool methods in gormCli is not sufficient. For example, it was not possible to interact with the database using transactions. Therefore, a comprehensive code search was performed to identify all the methods in gorm that call ConnPool, and they were implemented accordingly. Search method: using `type assertion` .ConnPool.( to search the code. + +At this point, the Client becomes a `custom connection` that satisfies gorm's requirements, similar to sql.DB, but implementing only a subset of sql.DB functionality. + +Additionally, there is a TxClient used for transaction handling, which implements both the ConnPool and TxCommitter interfaces defined by gorm. + +### transport.go Logic Explanation + +This is implemented using a method similar to trpc-database/sql, encapsulating the final step of sending requests outward. Here, db is the sql driver from `go-sql-driver`. + +```go +func (ct *ClientTransport) RoundTrip(ctx context.Context, reqBuf []byte, + // The front part is basically the same as the trpc-go mysql client RoundTrip method + ... + // Call the corresponding method of *sql.DB based on the Op in the request parameters, then return the result + switch sqlReq.Op { + case OpPrepareContext: + stmt, err := db.PrepareContext(ctx, sqlReq.Query) + if err != nil { + return nil, err + } + sqlRsp.Stmt = stmt + + case OpExecContext: + result, err := db.ExecContext(ctx, sqlReq.Query, sqlReq.Args...) + if err != nil { + return nil, err + } + sqlRsp.Result = result + ... +``` \ No newline at end of file diff --git a/gorm/docs/resource/architecture.png b/gorm/docs/resource/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..88ec3549bed819da490e52c7ec00d78304ec140c GIT binary patch literal 33285 zcmbrm1yt2tyDz#B6%_#y5otw2q(Qn=LQ)izkW#w4Yl%gJgmj3MgdiXwjdZ89bS!$& z&Eh=E_xsL1d!N1Uy<=S7@eXyxzvi6J^Q(#9lgEz;E>T=UAP@wyGLp&&#F-rg0w?Uk zd3Z%%Q*0CdVP`5K@x)YLAAz_N`YBY2Me^y5Mx=8fGKTIV{^Io(J?XdS-gewDY<~E* zcC9nP{HriehVY|(gcBcY{fBkZ`o>!2>#PyV;a#88)9JMn-1Sw%jP1S9qwa}&i&Ph6 zBrXVFnBDA^Ra9DPW65^&^kaPZgIn6?)paQkFRx(T=dRK!C>2T9ZygoY3M*C#Is+Xv z&LJ)9=sgi$!<;V{U*Ntp@;4&9M|mNxHuN(!PAD7mwFw-{&dt`lCJYT4NfXyHLFKxl%7aRYO4a684z{mU z_sL|_yUB0K1zxZ!VD<81a^0-^(|*momp zgKT>bPH;Td%Ga7om%ViED_*o(<)C*qovav{kA^|GsV}3fh(Ne8A`q|NArOb~((45T z!jTh!SbB*-2*GE_Y~stFiohG^_2eH(B2KaYeXh(1hq23Wd#3K7{L1;Jjh(fTsfFQ9 z2Ui=zn|}vWAn2QlX4s#bmyc1`1RpLxXrpR6Uv7p*db8CeZWu5=AAWX-*JnoG!@Zb2 zu$}$3jNZXSfZ@u^_eAF{{?thny8XPn878ko>Z^)lfw+6~($FOt`b%uAlVPD#q^Gan z236iJ^)(vF?UdnoWZJM4cDprSaKW6pV3Z=)`dlATfVIOs=g82-$i55m+m8!75M6RF zOuf6KgJlJYOad+tX?cjcZd?ZHW@iDY-FKF_^0iJzw=wpJm! zFFEsYsy)X`JGshbLvo`*`_-%OUpi>t9%c?B>ra*H8z~M}@2V|-igE88)-lbzUs5OP zxQv7U**s^P7?-_A^hGBZuFVXw@731Y$*Z~AyvXq!8SR?uz9PX{s_)6x?u{LlWd52R zUYO|^vr!*7A|ZYyDt6H|M?zo9SXy%lT{cBr^gU10V#X&TEjQ_ER@8~bD@wNk(fb$E zJYr+1-s!%n4$9#74mEPJmU`m3We?jVn#2{Q-3<4kG`L%%szaYiH|Fcu^cQmAKW?8cQOA{PwXbUgH1RT4KNGDPN> zm0YoZ^%p}=oBRI#kylaS`1{rQ-@gN}uly9`-T8YktH+9!e?JoUKYye%BFqlclJQ1j zB{hMytV;Lq(f{{}bb3&x(i;Ch>ED;TI=1c|OH0B}eMe>{88Q6W+M6v&VPE+ZAM1vF zftWudUFZMpBgS%F&5qX|1pmFl8)!rA$|wn|+h;IfVihAsB@7n%~DTJTXKF$YWt$K` zNsBNUA0Iz^?wsw?Z)QZh5l>zRI@*2BzeUeCwWG4>Fd-RaBmzg=WTTgqt|D{l?q%o^n@Ij-nAI$ zEp#WRW@HF&6%-V>EPpGxqgl*)>((uigW2%olf4PlSXGD7Oy%Bqip6lj?>yb4QhQWy zp04tJ1ma3XCMq6Q2C$gSnNd?NfC}8|^%&Ml|AH@jWSzRI0r^ zA3l8O8xSy4npUOn_xD{8<6cLWPHTT!syvSt#w&#b$T;}i_DHxs%$(`v zv7>$;8m9cU|KIbUC9)!h$5;q~YWh|(z9gfhd4%A<0o zF-RD77(kZiWME_@q^GZ+;AjMYF&oTX^{B8MEw=YsRnzr!?2PTo5fv33+n9Vyblt^k zy58>t8VN;%*i{HyDQGh6QJoIU0|c)P+amKVhPf*$D&oo7z4oUvR)+H57F&)AapY3* znqA|I3#!H(;=g{6Pqw)?5jRv|h(F=6MZm?ywK7@~5+6?$%WvJ8Ea5X$>CO*R5@xrc zcrdMaX9TkfLcPe0l+fqx!4`__t94r>n?j;UOmnl0*XfD#ijy$9`cw?zdANOxho`XS z@O{hi?x?k_fLitn#~Z&@ltLpT8{@32T4VUj&3}I-ZfR*@eet7NP=uM0 z@dKM?aK80~7~SpL325&zE9WYBoaDxpgNuuc3UPcvlanuvE%PdODGZE|Oyx?hb!lsGr|EY!XClCh^83T>RK^YIG|G?=LJTp1|}@FQWmNN9g+C)>$<7WUMGj`N#aS$Q#k6Z*IBRQi=S+h z_z+Ul!OgktEa7zLXz=Li>BWzZlsU2>R!53v(&MaZ4Y+&KG$JmY z?F$P?9+In95esAG{BvzlOq`q_;pjLv``x!zoXYLT9?`hIyZb!r3MHlHWNCM|ja(GF z;Zn7?_#;y1Snkk5B%3n?GzJa=cDLcA_(<%X&0wUCm`T zE0eC2L|6k4=lhFOPufJ6>5FG@-(DafNRtktusMthgs1-I%^So8eEdFYo7DQ=_XTux z%G0wW_8dQt+wleL?d;-{gsBLjK)}?3`Ev#rx23i9?t0OIaeZN7A-+hG_}WRkAqIni zYZCB$klCQe{9UK&SbC>AW^plNUC7goy&7kCYs;R!Is*R@%kwOU-J^&&&4oPit)kN- zHwpZcB5}-qN}((*=S#$+5E?d{^0wo>;laTuO6!UqLFh@F`vwLh96xItB5}|9+?Sr0 zQu}vb%^TV*D6l}JCbZJs?hEcWYqIh=uHq5zGHP?zYv8E9>R#6oFJK$bAdftA^Je$s zwAW^cW6Ti+A&t-j8JRnx?z=|wfl4l?Cn2TftD^KLMY!QDl2%si4<0#Te~cEj zTGB+;Z2yE)97(@kj((ilRu^KkGW67_BlbbS=;+I~9p)$YI-c>*m7IbJd~kao8|oKd z%jr_tHJ)Qef6=o2T>Wo+q82*{eF4=F4!!B~4D`KDe7-$e0lUr}A%}%Y-@rfzL~tyx zxzO%fhm+1i46?^GMLH;c89iQk6u%QhBYOCTtTi1PkyY6uBWerknxhv6bA(RX+DYWt z(jtqOmX^N$)&BW0h0jc2z+(OZ$LM0e>k%ZA^CW!wkIeAO%d;Co8@?v|}`}Yyhd(vUqA)u^X zc#Ul|Og^*A)m-@4HZ?Xso3>~;)Y^V!|wO%Ih_-xGP zC7se>1{Zo#$i+Ma5O6oth;SyQbk{9DRJ-t&Z8)RizS;Og#JDSpIxH;A<#?m%V1FiL zWAm^?QE>1How&F-6dqe!+fEebNJvudvf8nt#J>Pw#h2#7VT)iArpE$yc6Oa{QOE&j z=t7-4T~Sf7u`ga8lx}kK@K9q%=Q6d>?|hE~J8bV-`BU15Bk(0fp9<>nxhg&umgO7@ zSehIWML=yop6b9dY~_Q1Uv3CmdFW zc5=wAPL7Y+U(CGcE_V_Y=DB%u@{6Mm!NF){x49rK+;wejZM<_q=`*M_A1aK9)so96 ze0^p7oM4QmmzU)id(#5wy9~IwY^NpR$v1ISAJ_hRi)Y%GPLLq%obI@sD;4WHsGYFb zzr4vURbTiOt~B)OHvj}uiR8q@zq*qp$b}qCyir6ntRwAj@utKj`lV9e^{!L?PvAH` zB7$e$0TVKLMc9-nAQ!FnpTK^E+O{jB&&=^Ztl%`j=O0o~ap(WWvj4vzvqvENeM!mG zJAZ8I2((@%O;OtpQsp z`oyB9oTnoS3z1p3s-kDk*Z0OlEOGhyElSrb28$4}&=vaY)at6GgDl;@Io6Y^vc5{L6uqV`>Hi2bt0EtHZOpdr>r7^gt#|cvx7N#V|Mlg)R!V4R_+;z` z$NL+9?7KwYx3-;5<-o;UT2>CG%=HrK$IdKU=0f8xMp4lOz|2>#TzTmr2v6YwS*yRV zue{4*S`@(74EZSbic`#y*@BcLyEm4raXowZ@Jz{=6J;E~H6t%?|AHNiaoFE?+JBg$ zdp|$G#Xg}wrT>f#gP-qD1}xPH)r*RcZ-0%0D_~3aw!U5xFzDZ*u~IY!DwwLSm?V}k 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 Y5squwJrCyLXHc@Ylx}8RfB5SE0EwFvvj6}9 literal 0 HcmV?d00001 diff --git a/gorm/docs/resource/introduce.png b/gorm/docs/resource/introduce.png new file mode 100644 index 0000000000000000000000000000000000000000..d3d2461a6f3376328354402625a971a7f974abe8 GIT binary patch literal 11096 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 MMGb|jXBMIV4+6!rg#Z8m literal 0 HcmV?d00001 diff --git a/gorm/examples/clickhouse/README.md b/gorm/examples/clickhouse/README.md new file mode 100755 index 0000000..f0707b3 --- /dev/null +++ b/gorm/examples/clickhouse/README.md @@ -0,0 +1,13 @@ +# ClickHouse Example + +## Start Docker Container and Run ClickHouse Service + +Execute the `run-docker.sh` script, where `init.sql` will create the database and table. + +## Execute SQL Commands + +Run `go run .`. If executed correctly, the program will not panic and will print related execution logs to stdio. + +## Add Other Examples or Tests + +You can modify `init.sql` to change the database configuration. \ No newline at end of file diff --git a/gorm/examples/clickhouse/init.sql b/gorm/examples/clickhouse/init.sql new file mode 100644 index 0000000..958594f --- /dev/null +++ b/gorm/examples/clickhouse/init.sql @@ -0,0 +1,10 @@ +CREATE DATABASE IF NOT EXISTS my_database; +USE my_database; + +CREATE TABLE IF NOT EXISTS users ( + id UInt64, + username String, + created_at DateTime DEFAULT now(), + updated_at DateTime DEFAULT now() +) ENGINE = MergeTree() +ORDER BY id; diff --git a/gorm/examples/clickhouse/main.go b/gorm/examples/clickhouse/main.go new file mode 100644 index 0000000..317ab89 --- /dev/null +++ b/gorm/examples/clickhouse/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "trpc.group/trpc-go/trpc-database/gorm" + "trpc.group/trpc-go/trpc-go" + "trpc.group/trpc-go/trpc-go/log" +) + +// User is the model struct. +type User struct { + ID int + Username string +} + +func main() { + _ = trpc.NewServer() + + cli, err := gorm.NewClientProxy("trpc.clickhouse.server.service") + if err != nil { + panic(err) + } + + // Create record + insertUser := User{Username: "gorm-client"} + if result := cli.Create(&insertUser); result.Error != nil { + panic(result.Error) + } + log.Infof("inserted data succeed") + + // Query record + var queryUser User + if err := cli.First(&queryUser).Error; err != nil { + panic(err) + } + log.Infof("query user: %+v", queryUser) + + // Delete record + deleteUser := User{} + if err := cli.Where("username = ?", "gorm-client").Delete(&deleteUser).Error; err != nil { + panic(err) + } + log.Info("delete record succeed") + + // For more use cases, see https://gorm.io/docs/create.html +} diff --git a/gorm/examples/clickhouse/run-docker.sh b/gorm/examples/clickhouse/run-docker.sh new file mode 100755 index 0000000..50cfd64 --- /dev/null +++ b/gorm/examples/clickhouse/run-docker.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +docker pull clickhouse:25.4.1 +docker stop clickhouse-container || true +docker rm clickhouse-container || true + +# https://hub.docker.com/_/clickhouse +docker run --name clickhouse-container \ + -e CLICKHOUSE_USER=my-username \ + -e CLICKHOUSE_PASSWORD=my-secret-pw\ + -d -p 9000:9000 \ + -v "$(pwd)"/init.sql:/docker-entrypoint-initdb.d/init.sql \ + clickhouse:25.4.1 \ No newline at end of file diff --git a/gorm/examples/clickhouse/trpc_go.yaml b/gorm/examples/clickhouse/trpc_go.yaml new file mode 100644 index 0000000..5ec8408 --- /dev/null +++ b/gorm/examples/clickhouse/trpc_go.yaml @@ -0,0 +1,29 @@ +global: # Global configuration + namespace: Development # Environment type: Production for formal, Development for non-formal + env_name: test # Environment name for non-formal environments + +client: + service: + - name: trpc.clickhouse.server.service + # Reference: https://github.com/ClickHouse/clickhouse-go?tab=readme-ov-file#dsn + target: dsn://clickhouse://my-username:my-secret-pw@127.0.0.1:9000/my_database?dial_timeout=200ms&max_execution_time=60 + +plugins: # Plugin configuration + log: # Log configuration + default: # Default log configuration, supports multiple outputs + - writer: console # Console standard output (default) + level: debug # Log level for standard output + + database: + gorm: + # Configuration effective for all gorm clients + max_idle: 20 # Maximum idle connections (default 10) + max_open: 100 # Maximum active connections (default 10000) + max_lifetime: 180000 # Maximum connection lifetime in milliseconds (default 3min) + driver_name: clickhouse # Driver used for connection, empty by default, import corresponding driver if needed + logger: + slow_threshold: 200 # Slow query threshold in milliseconds, 0 means no slow query logging (default 0) + colorful: false # Whether to print colorful logs (default false) + ignore_record_not_found_error: false # Whether to ignore record not found errors (default false) + log_level: 4 # Log level: 1:Silent, 2:Error, 3:Warn, 4:Info (default no logging) + max_sql_size: 100 # Maximum SQL statement length for truncation, 0 means no limit (default 0) diff --git a/gorm/examples/go.mod b/gorm/examples/go.mod new file mode 100644 index 0000000..ba1cf6b --- /dev/null +++ b/gorm/examples/go.mod @@ -0,0 +1,72 @@ +module example + +go 1.21.6 + +replace github.com/trpc-group/trpc-database/gorm => ../ + +require ( + github.com/mattn/go-sqlite3 v1.14.22 + github.com/trpc-group/trpc-database/gorm v0.2.7 + gorm.io/driver/sqlite v1.5.5 + gorm.io/gorm v1.25.7 + trpc.group/trpc-go/trpc-database/gorm v1.0.0 + trpc.group/trpc-go/trpc-go v1.0.3 +) + +require ( + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/ClickHouse/ch-go v0.52.1 // indirect + github.com/ClickHouse/clickhouse-go/v2 v2.9.1 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-faster/city v1.0.1 // indirect + github.com/go-faster/errors v0.6.1 // indirect + github.com/go-sql-driver/mysql v1.6.0 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/golang/snappy v0.0.3 // indirect + github.com/google/flatbuffers v2.0.0+incompatible // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgconn v1.12.1 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect + github.com/jackc/pgtype v1.11.0 // indirect + github.com/jackc/pgx/v4 v4.16.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.15.15 // indirect + github.com/lestrrat-go/strftime v1.0.6 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/panjf2000/ants/v2 v2.4.6 // indirect + github.com/paulmach/orb v0.9.0 // indirect + github.com/pierrec/lz4/v4 v4.1.18 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/spf13/cast v1.3.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.43.0 // indirect + go.opentelemetry.io/otel v1.13.0 // indirect + go.opentelemetry.io/otel/trace v1.13.0 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/automaxprocs v1.3.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/mysql v1.3.4 // indirect + gorm.io/driver/postgres v1.3.7 // indirect + trpc.group/trpc-go/tnet v1.0.1 // indirect + trpc.group/trpc-go/trpc-selector-dsn v1.1.0 // indirect + trpc.group/trpc/trpc-protocol/pb/go/trpc v1.0.0 // indirect +) diff --git a/gorm/examples/go.sum b/gorm/examples/go.sum new file mode 100644 index 0000000..2e2b249 --- /dev/null +++ b/gorm/examples/go.sum @@ -0,0 +1,361 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/ClickHouse/ch-go v0.52.1 h1:nucdgfD1BDSHjbNaG3VNebonxJzD8fX8jbuBpfo5VY0= +github.com/ClickHouse/ch-go v0.52.1/go.mod h1:B9htMJ0hii/zrC2hljUKdnagRBuLqtRG/GrU3jqCwRk= +github.com/ClickHouse/clickhouse-go/v2 v2.9.1 h1:IeE2bwVvAba7Yw5ZKu98bKI4NpDmykEy6jUaQdJJCk8= +github.com/ClickHouse/clickhouse-go/v2 v2.9.1/go.mod h1:teXfZNM90iQ99Jnuht+dxQXCuhDZ8nvvMoTJOFrcmcg= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= +github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= +github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI= +github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-playground/form/v4 v4.2.0 h1:N1wh+Goz61e6w66vo8vJkQt+uwZSoLz50kZPJWR8eic= +github.com/go-playground/form/v4 v4.2.0/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v2.0.0+incompatible h1:dicJ2oXwypfwUGnB2/TYWYEKiuk9eYQlQO/AnOHl5mI= +github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8= +github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y= +github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs= +github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y= +github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= +github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= +github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ= +github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/panjf2000/ants/v2 v2.4.6 h1:drmj9mcygn2gawZ155dRbo+NfXEfAssjZNU1qoIb4gQ= +github.com/panjf2000/ants/v2 v2.4.6/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A= +github.com/paulmach/orb v0.9.0 h1:MwA1DqOKtvCgm7u9RZ/pnYejTeDJPnr0+0oFajBbJqk= +github.com/paulmach/orb v0.9.0/go.mod h1:SudmOk85SXtmXAB3sLGyJ6tZy/8pdfrV0o6ef98Xc30= +github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= +github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= +github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.43.0 h1:Gy4sb32C98fbzVWZlTM1oTMdLWGyvxR03VhM6cBIU4g= +github.com/valyala/fasthttp v1.43.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= +go.opentelemetry.io/otel v1.13.0 h1:1ZAKnNQKwBBxFtww/GwxNUyTf0AxkZzrukO8MeXqe4Y= +go.opentelemetry.io/otel v1.13.0/go.mod h1:FH3RtdZCzRkJYFTCsAKDy9l/XYjMdNv6QrkFFB8DvVg= +go.opentelemetry.io/otel/trace v1.13.0 h1:CBgRZ6ntv+Amuj1jDsMhZtlAPT6gbyIRdaIzFhfBSdY= +go.opentelemetry.io/otel/trace v1.13.0/go.mod h1:muCvmmO9KKpvuXSf3KKAXXB2ygNYHQ+ZfI5X08d3tds= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/automaxprocs v1.3.0 h1:II28aZoGdaglS5vVNnspf28lnZpXScxtIozx1lAjdb0= +go.uber.org/automaxprocs v1.3.0/go.mod h1:9CWT6lKIep8U41DDaPiH6eFscnTyjfTANNQNx6LrIcA= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.3.4 h1:/KoBMgsUHC3bExsekDcmNYaBnfH2WNeFuXqqrqMc98Q= +gorm.io/driver/mysql v1.3.4/go.mod h1:s4Tq0KmD0yhPGHbZEwg1VPlH0vT/GBHJZorPzhcxBUE= +gorm.io/driver/postgres v1.3.7 h1:FKF6sIMDHDEvvMF/XJvbnCl0nu6KSKUaPXevJ4r+VYQ= +gorm.io/driver/postgres v1.3.7/go.mod h1:f02ympjIcgtHEGFMZvdgTxODZ9snAHDb4hXfigBVuNI= +gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E= +gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= +gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +trpc.group/trpc-go/tnet v1.0.1 h1:Yzqyrgyfm+W742FzGr39c4+OeQmLi7PWotJxrOBtV9o= +trpc.group/trpc-go/tnet v1.0.1/go.mod h1:s/webUFYWEFBHErKyFmj7LYC7XfC2LTLCcwfSnJ04M0= +trpc.group/trpc-go/trpc-database/gorm v1.0.0 h1:SBXZw7MH3sPxZCWYOXReeB53PNzKVvXvmyDDdGLIOwI= +trpc.group/trpc-go/trpc-database/gorm v1.0.0/go.mod h1:Q4EGjKETLi2xPi1m9uG3qMJTYRYFYUT/QmJ0YgT57Kw= +trpc.group/trpc-go/trpc-go v1.0.3 h1:X4RhPmJOkVoK6EGKoV241dvEpB6EagBeyu3ZrqkYZQY= +trpc.group/trpc-go/trpc-go v1.0.3/go.mod h1:82O+G2rD5ST+JAPuPPSqvsr6UI59UxV27iAILSkAIlQ= +trpc.group/trpc-go/trpc-selector-dsn v1.1.0 h1:z3VqiboZq60MBu0cHVlRe5q7VydGbBdrX9xAfzsTVIQ= +trpc.group/trpc-go/trpc-selector-dsn v1.1.0/go.mod h1:78NOrldaWxLJd2M+VCm4OABphAYzx98dZWTLDFSzeQg= +trpc.group/trpc/trpc-protocol/pb/go/trpc v1.0.0 h1:rMtHYzI0ElMJRxHtT5cD99SigFE6XzKK4PFtjcwokI0= +trpc.group/trpc/trpc-protocol/pb/go/trpc v1.0.0/go.mod h1:K+a1K/Gnlcg9BFHWx30vLBIEDhxODhl25gi1JjA54CQ= diff --git a/gorm/examples/mysql/README.md b/gorm/examples/mysql/README.md new file mode 100755 index 0000000..69c2bf1 --- /dev/null +++ b/gorm/examples/mysql/README.md @@ -0,0 +1,13 @@ +# MySQL Example + +## Start Docker Container and Run MySQL Service + +Execute the `run-docker.sh` script, where `init.sql` will create the database and table. + +## Execute SQL Commands + +Run `go run .`. If executed correctly, the program will not panic and will print related execution logs to stdio. + +## Add Other Examples or Tests + +You can modify `init.sql` to change the database configuration. \ No newline at end of file diff --git a/gorm/examples/mysql/init.sql b/gorm/examples/mysql/init.sql new file mode 100644 index 0000000..c5d2a38 --- /dev/null +++ b/gorm/examples/mysql/init.sql @@ -0,0 +1,9 @@ +CREATE DATABASE IF NOT EXISTS my_database; +USE my_database; + +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) diff --git a/gorm/examples/mysql/main.go b/gorm/examples/mysql/main.go new file mode 100644 index 0000000..19a1d3d --- /dev/null +++ b/gorm/examples/mysql/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "github.com/trpc-group/trpc-database/gorm" + + "trpc.group/trpc-go/trpc-go" + "trpc.group/trpc-go/trpc-go/log" +) + +// User is the model struct. +type User struct { + ID int + Username string +} + +func main() { + _ = trpc.NewServer() + + cli, err := gorm.NewClientProxy("trpc.mysql.server.service") + if err != nil { + panic(err) + } + + // Create record + insertUser := User{Username: "gorm-client"} + result := cli.Create(&insertUser) + log.Infof("inserted data's primary key: %d, err: %v", insertUser.ID, result.Error) + + // Query record + var queryUser User + if err := cli.First(&queryUser).Error; err != nil { + panic(err) + } + log.Infof("query user: %+v", queryUser) + + // Delete record + deleteUser := User{ID: insertUser.ID} + if err := cli.Delete(&deleteUser).Error; err != nil { + panic(err) + } + log.Info("delete record succeed") + + // For more use cases, see https://gorm.io/docs/create.html +} diff --git a/gorm/examples/mysql/run-docker.sh b/gorm/examples/mysql/run-docker.sh new file mode 100755 index 0000000..69b076c --- /dev/null +++ b/gorm/examples/mysql/run-docker.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +docker pull mysql:9.3.0 +docker stop mysql-container || true +docker rm mysql-container || true + +# https://hub.docker.com/_/mysql/ +docker run --name mysql-container \ + -e MYSQL_ROOT_PASSWORD=my-secret-pw \ + -d -p 3306:3306 \ + -v "$(pwd)"/init.sql:/docker-entrypoint-initdb.d/init.sql \ + mysql:9.3.0 \ No newline at end of file diff --git a/gorm/examples/mysql/trpc_go.yaml b/gorm/examples/mysql/trpc_go.yaml new file mode 100644 index 0000000..ae02495 --- /dev/null +++ b/gorm/examples/mysql/trpc_go.yaml @@ -0,0 +1,36 @@ +global: # Global configuration + namespace: Development # Environment type: Production for formal, Development for non-formal + env_name: test # Environment name for non-formal environments + +client: + service: + - name: trpc.mysql.server.service + # Reference: https://github.com/go-sql-driver/mysql?tab=readme-ov-file#dsn-data-source-name + target: dsn://root:my-secret-pw@tcp(127.0.0.1:3306)/my_database?charset=utf8mb4&parseTime=True + +plugins: # Plugin configuration + log: # Log configuration + default: # Default log configuration, supports multiple outputs + - writer: console # Console standard output (default) + level: debug # Log level for standard output + + database: + gorm: + # Configuration effective for all gorm clients + max_idle: 20 # Maximum idle connections (default 10) + max_open: 100 # Maximum active connections (default 10000) + max_lifetime: 180000 # Maximum connection lifetime in milliseconds (default 3min) + driver_name: mysql # Driver used for connection, empty by default, import corresponding driver if needed + logger: + slow_threshold: 200 # Slow query threshold in milliseconds, 0 means no slow query logging (default 0) + colorful: false # Whether to print colorful logs (default false) + ignore_record_not_found_error: false # Whether to ignore record not found errors (default false) + log_level: 4 # Log level: 1:Silent, 2:Error, 3:Warn, 4:Info (default no logging) + max_sql_size: 100 # Maximum SQL statement length for truncation, 0 means no limit (default 0) + service: + # Configuration effective for trpc.mysql.server.service client + - name: trpc.mysql.server.service + max_idle: 10 # Maximum idle connections (default 10) + max_open: 50 # Maximum active connections (default 10000) + max_lifetime: 180000 # Maximum connection lifetime in milliseconds (default 3min) + driver_name: mysql # Driver used for connection, empty by default, import corresponding driver if needed diff --git a/gorm/examples/sqlite/README.md b/gorm/examples/sqlite/README.md new file mode 100755 index 0000000..d707a74 --- /dev/null +++ b/gorm/examples/sqlite/README.md @@ -0,0 +1,10 @@ +# SQLite Example + +## Start Docker Container and Run SQLite Service + +Execute the `run-docker.sh` script. + +## Execute SQL Commands + +Run `go run .`. If executed correctly, the program will not panic and will print related execution logs to stdio. + diff --git a/gorm/examples/sqlite/main.go b/gorm/examples/sqlite/main.go new file mode 100644 index 0000000..d01e7c0 --- /dev/null +++ b/gorm/examples/sqlite/main.go @@ -0,0 +1,48 @@ +package main + +import ( + tgorm "github.com/trpc-group/trpc-database/gorm" + _ "github.com/mattn/go-sqlite3" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + + "trpc.group/trpc-go/trpc-go" + "trpc.group/trpc-go/trpc-go/log" +) + +// User is the model struct. +type User struct { + ID int + Username string +} + +func main() { + _ = trpc.NewServer() + + connPool := tgorm.NewConnPool("trpc.sqlite.server.service") + cli, err := gorm.Open(&sqlite.Dialector{Conn: connPool}, &gorm.Config{}) + if err != nil { + panic(err) + } + + // Create record + insertUser := User{ID: 10, Username: "gorm-client"} + result := cli.Table("Users").Create(&insertUser) + log.Infof("inserted data's primary key: %d, err: %v", insertUser.ID, result.Error) + + // Query record + var queryUser User + if err := cli.First(&queryUser).Error; err != nil { + panic(err) + } + log.Infof("query user: %+v", queryUser) + + // Delete record + deleteUser := User{ID: insertUser.ID} + if err := cli.Delete(&deleteUser).Error; err != nil { + panic(err) + } + log.Info("delete record succeed") + + // For more use cases, see https://gorm.io/docs/create.html +} diff --git a/gorm/examples/sqlite/run-docker.sh b/gorm/examples/sqlite/run-docker.sh new file mode 100755 index 0000000..210445b --- /dev/null +++ b/gorm/examples/sqlite/run-docker.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +rm mysqlite.db || true +docker pull alpine/sqlite:3.48.0 +docker stop sqlite-container || true +docker rm sqlite-container || true + +# https://hub.docker.com/r/alpine/sqlite +docker run --name sqlite-container \ + -v "$(pwd)":/apps \ + -w /apps alpine/sqlite:3.48.0 mysqlite.db "CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT);" \ diff --git a/gorm/examples/sqlite/trpc_go.yaml b/gorm/examples/sqlite/trpc_go.yaml new file mode 100644 index 0000000..c8e4b84 --- /dev/null +++ b/gorm/examples/sqlite/trpc_go.yaml @@ -0,0 +1,32 @@ +global: # Global configuration + namespace: Development # Environment type: Production for formal, Development for non-formal + env_name: test # Environment name for non-formal environments + +client: + service: + - name: trpc.sqlite.server.service + # Reference: https://github.com/mattn/go-sqlite3?tab=readme-ov-file#dsn-examples + # Execute "sqlite3 mysqlite.db" in the current directory to create the database + target: dsn://file:mysqlite.db + +plugins: # Plugin configuration + log: # Log configuration + default: # Default log configuration, supports multiple outputs + - writer: console # Console standard output (default) + level: debug # Log level for standard output + + database: + gorm: + logger: + slow_threshold: 200 # Slow query threshold in milliseconds, 0 means no slow query logging (default 0) + colorful: false # Whether to print colorful logs (default false) + ignore_record_not_found_error: false # Whether to ignore record not found errors (default false) + log_level: 4 # Log level: 1:Silent, 2:Error, 3:Warn, 4:Info (default no logging) + max_sql_size: 100 # Maximum SQL statement length for truncation, 0 means no limit (default 0) + service: + # Configuration effective for trpc.sqlite.server.service client + - name: trpc.sqlite.server.service + max_idle: 10 # Maximum idle connections (default 10) + max_open: 50 # Maximum active connections (default 10000) + max_lifetime: 180000 # Maximum connection lifetime in milliseconds (default 3min) + driver_name: sqlite3 # Driver used for connection, empty by default, import corresponding driver if needed diff --git a/gorm/go.mod b/gorm/go.mod index 15d6740..12611e7 100644 --- a/gorm/go.mod +++ b/gorm/go.mod @@ -10,12 +10,13 @@ require ( github.com/google/go-cmp v0.5.9 github.com/smartystreets/goconvey v1.6.4 github.com/stretchr/testify v1.8.2 + golang.org/x/sync v0.1.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.3.4 gorm.io/driver/postgres v1.3.7 - gorm.io/gorm v1.23.5 - trpc.group/trpc-go/trpc-go v1.0.0 - trpc.group/trpc-go/trpc-selector-dsn v1.0.0 + gorm.io/gorm v1.25.1 + trpc.group/trpc-go/trpc-go v1.0.3 + trpc.group/trpc-go/trpc-selector-dsn v1.1.0 ) require ( @@ -41,7 +42,7 @@ require ( github.com/jackc/pgtype v1.11.0 // indirect github.com/jackc/pgx/v4 v4.16.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.4 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect github.com/klauspost/compress v1.15.15 // indirect @@ -66,10 +67,9 @@ require ( go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/text v0.6.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect - trpc.group/trpc-go/tnet v0.0.0-20230810071536-9d05338021cf // indirect - trpc.group/trpc/trpc-protocol/pb/go/trpc v0.0.0-20230803031059-de4168eb5952 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + trpc.group/trpc-go/tnet v1.0.1 // indirect + trpc.group/trpc/trpc-protocol/pb/go/trpc v1.0.0 // indirect ) diff --git a/gorm/go.sum b/gorm/go.sum index a8cbd4f..c19531a 100644 --- a/gorm/go.sum +++ b/gorm/go.sum @@ -106,8 +106,9 @@ github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -261,7 +262,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -286,8 +287,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -297,8 +298,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -321,8 +322,9 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -338,14 +340,14 @@ gorm.io/driver/mysql v1.3.4/go.mod h1:s4Tq0KmD0yhPGHbZEwg1VPlH0vT/GBHJZorPzhcxBU gorm.io/driver/postgres v1.3.7 h1:FKF6sIMDHDEvvMF/XJvbnCl0nu6KSKUaPXevJ4r+VYQ= gorm.io/driver/postgres v1.3.7/go.mod h1:f02ympjIcgtHEGFMZvdgTxODZ9snAHDb4hXfigBVuNI= gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= -gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM= -gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64= +gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -trpc.group/trpc-go/tnet v0.0.0-20230810071536-9d05338021cf h1:Qo0p6ZJV60Qd5XajiIDidVgx1NDM9UHL7DzDKc2gqns= -trpc.group/trpc-go/tnet v0.0.0-20230810071536-9d05338021cf/go.mod h1:s/webUFYWEFBHErKyFmj7LYC7XfC2LTLCcwfSnJ04M0= -trpc.group/trpc-go/trpc-go v1.0.0 h1:bSbcNpRFEXJONkwMVs8Xd+Mw/mFPUeE9Lcxk7KAaSoU= -trpc.group/trpc-go/trpc-go v1.0.0/go.mod h1:ve2YyZleGVbnKr0RLUJcu35dXw2zZmsi3RdKVPgL4+4= -trpc.group/trpc-go/trpc-selector-dsn v1.0.0 h1:erxsILXM6/9s2gHgfaqCetG3YcVaAvKSvdG815zc1SA= -trpc.group/trpc-go/trpc-selector-dsn v1.0.0/go.mod h1:7h1YQkaSvlOdlNUzXRByruPSAuCMQzLCvLge8BFa/eI= -trpc.group/trpc/trpc-protocol/pb/go/trpc v0.0.0-20230803031059-de4168eb5952 h1:AhjP72IKa1YKnSIayk1X5xSzKrem0EanjZ7oMc2HYOw= -trpc.group/trpc/trpc-protocol/pb/go/trpc v0.0.0-20230803031059-de4168eb5952/go.mod h1:K+a1K/Gnlcg9BFHWx30vLBIEDhxODhl25gi1JjA54CQ= +trpc.group/trpc-go/tnet v1.0.1 h1:Yzqyrgyfm+W742FzGr39c4+OeQmLi7PWotJxrOBtV9o= +trpc.group/trpc-go/tnet v1.0.1/go.mod h1:s/webUFYWEFBHErKyFmj7LYC7XfC2LTLCcwfSnJ04M0= +trpc.group/trpc-go/trpc-go v1.0.3 h1:X4RhPmJOkVoK6EGKoV241dvEpB6EagBeyu3ZrqkYZQY= +trpc.group/trpc-go/trpc-go v1.0.3/go.mod h1:82O+G2rD5ST+JAPuPPSqvsr6UI59UxV27iAILSkAIlQ= +trpc.group/trpc-go/trpc-selector-dsn v1.1.0 h1:z3VqiboZq60MBu0cHVlRe5q7VydGbBdrX9xAfzsTVIQ= +trpc.group/trpc-go/trpc-selector-dsn v1.1.0/go.mod h1:78NOrldaWxLJd2M+VCm4OABphAYzx98dZWTLDFSzeQg= +trpc.group/trpc/trpc-protocol/pb/go/trpc v1.0.0 h1:rMtHYzI0ElMJRxHtT5cD99SigFE6XzKK4PFtjcwokI0= +trpc.group/trpc/trpc-protocol/pb/go/trpc v1.0.0/go.mod h1:K+a1K/Gnlcg9BFHWx30vLBIEDhxODhl25gi1JjA54CQ= diff --git a/gorm/log.go b/gorm/log.go index 0a92775..bba0fa8 100644 --- a/gorm/log.go +++ b/gorm/log.go @@ -31,6 +31,7 @@ const ( // TRPCLogger implements the Gorm logger.Interface. type TRPCLogger struct { + maxSqlSize int config logger.Config infoStr, warnStr, errStr string traceStr, traceErrStr, traceWarnStr string @@ -70,6 +71,12 @@ func NewTRPCLogger(config logger.Config) *TRPCLogger { } } +// SetMaxSqlLength set the max length of log sql size and returns TRPCLogger. +func (p *TRPCLogger) SetMaxSqlLength(length int) logger.Interface { + p.maxSqlSize = length + return p +} + // LogMode changes the log level and returns a new TRPCLogger. func (p *TRPCLogger) LogMode(level logger.LogLevel) logger.Interface { newLogger := *p @@ -84,14 +91,14 @@ func (p *TRPCLogger) Info(ctx context.Context, format string, args ...interface{ } } -// Warn prints logs at the Warn level. +// Warn logs warning level log. func (p *TRPCLogger) Warn(ctx context.Context, format string, args ...interface{}) { if p.config.LogLevel >= logger.Warn { log.WarnContextf(ctx, p.warnStr+format, append([]interface{}{utils.FileWithLineNum()}, args...)...) } } -// Error prints logs at the Error level. +// Error logs Error level log. func (p *TRPCLogger) Error(ctx context.Context, format string, args ...interface{}) { if p.config.LogLevel >= logger.Error { log.ErrorContextf(ctx, p.errStr+format, append([]interface{}{utils.FileWithLineNum()}, args...)...) @@ -109,7 +116,8 @@ func (p *TRPCLogger) Trace(ctx context.Context, begin time.Time, fc func() (stri switch { case err != nil && p.config.LogLevel >= logger.Error && (!errors.Is(err, gorm.ErrRecordNotFound) || !p.config.IgnoreRecordNotFoundError): - sql, rows := fc() + rawSql, rows := fc() + sql := p.truncateSQL(rawSql) if rows == -1 { log.ErrorContextf(ctx, p.traceErrStr, utils.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, "-", sql) @@ -118,7 +126,8 @@ func (p *TRPCLogger) Trace(ctx context.Context, begin time.Time, fc func() (stri utils.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, rows, sql) } case elapsed > p.config.SlowThreshold && p.config.SlowThreshold != 0 && p.config.LogLevel >= logger.Warn: - sql, rows := fc() + rawSql, rows := fc() + sql := p.truncateSQL(rawSql) slowLog := fmt.Sprintf("SLOW SQL >= %v", p.config.SlowThreshold) if rows == -1 { log.WarnContextf(ctx, p.traceWarnStr, @@ -128,7 +137,8 @@ func (p *TRPCLogger) Trace(ctx context.Context, begin time.Time, fc func() (stri utils.FileWithLineNum(), slowLog, float64(elapsed.Nanoseconds())/1e6, rows, sql) } case p.config.LogLevel == logger.Info: - sql, rows := fc() + rawSql, rows := fc() + sql := p.truncateSQL(rawSql) if rows == -1 { log.InfoContextf(ctx, p.traceStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, "-", sql) } else { @@ -136,3 +146,10 @@ func (p *TRPCLogger) Trace(ctx context.Context, begin time.Time, fc func() (stri } } } + +func (p *TRPCLogger) truncateSQL(sql string) string { + if p.maxSqlSize > 0 && len(sql) > p.maxSqlSize { + return sql[:p.maxSqlSize-1] + " ..." + } + return sql +} diff --git a/gorm/log_mock.go b/gorm/log_mock.go index a1765bd..0b6af75 100644 --- a/gorm/log_mock.go +++ b/gorm/log_mock.go @@ -7,8 +7,8 @@ package gorm import ( reflect "reflect" - gomock "github.com/golang/mock/gomock" log "trpc.group/trpc-go/trpc-go/log" + gomock "github.com/golang/mock/gomock" ) // MockLogger is a mock of Logger interface. diff --git a/gorm/log_test.go b/gorm/log_test.go index 4d42698..9bb62e0 100644 --- a/gorm/log_test.go +++ b/gorm/log_test.go @@ -116,6 +116,7 @@ func TestUnit_Log_Trace_P0(t *testing.T) { Convey("TestUnit_Log_Trace_P0", t, func() { silentLog := NewTRPCLogger(logger.Config{LogLevel: logger.Silent}) infoLog := NewTRPCLogger(logger.Config{LogLevel: logger.Info}) + infoLog.SetMaxSqlLength(2) calledTimes := 0 mockCtrl := gomock.NewController(t) @@ -131,7 +132,7 @@ func TestUnit_Log_Trace_P0(t *testing.T) { So(format, ShouldEqual, infoLog.traceStr) if calledTimes == 0 { So(args[2], ShouldEqual, 7) - So(args[3], ShouldEqual, "this is sql") + So(args[3], ShouldEqual, "t ...") } else { So(args[2], ShouldEqual, "-") So(args[3], ShouldEqual, "") diff --git a/gorm/mock/client_mock.go b/gorm/mock/client_mock.go new file mode 100644 index 0000000..1f60a83 --- /dev/null +++ b/gorm/mock/client_mock.go @@ -0,0 +1,229 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: client.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + sql "database/sql" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + gorm "gorm.io/gorm" +) + +// MockConnPool is a mock of ConnPool interface. +type MockConnPool struct { + ctrl *gomock.Controller + recorder *MockConnPoolMockRecorder +} + +// MockConnPoolMockRecorder is the mock recorder for MockConnPool. +type MockConnPoolMockRecorder struct { + mock *MockConnPool +} + +// NewMockConnPool creates a new mock instance. +func NewMockConnPool(ctrl *gomock.Controller) *MockConnPool { + mock := &MockConnPool{ctrl: ctrl} + mock.recorder = &MockConnPoolMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockConnPool) EXPECT() *MockConnPoolMockRecorder { + return m.recorder +} + +// BeginTx mocks base method. +func (m *MockConnPool) BeginTx(ctx context.Context, opts *sql.TxOptions) (gorm.ConnPool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BeginTx", ctx, opts) + ret0, _ := ret[0].(gorm.ConnPool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BeginTx indicates an expected call of BeginTx. +func (mr *MockConnPoolMockRecorder) BeginTx(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginTx", reflect.TypeOf((*MockConnPool)(nil).BeginTx), ctx, opts) +} + +// Exec mocks base method. +func (m *MockConnPool) Exec(query string, args ...interface{}) (sql.Result, error) { + m.ctrl.T.Helper() + varargs := []interface{}{query} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Exec", varargs...) + ret0, _ := ret[0].(sql.Result) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Exec indicates an expected call of Exec. +func (mr *MockConnPoolMockRecorder) Exec(query interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{query}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockConnPool)(nil).Exec), varargs...) +} + +// ExecContext mocks base method. +func (m *MockConnPool) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, query} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ExecContext", varargs...) + ret0, _ := ret[0].(sql.Result) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExecContext indicates an expected call of ExecContext. +func (mr *MockConnPoolMockRecorder) ExecContext(ctx, query interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, query}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecContext", reflect.TypeOf((*MockConnPool)(nil).ExecContext), varargs...) +} + +// GetDBConn mocks base method. +func (m *MockConnPool) GetDBConn() (*sql.DB, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDBConn") + ret0, _ := ret[0].(*sql.DB) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDBConn indicates an expected call of GetDBConn. +func (mr *MockConnPoolMockRecorder) GetDBConn() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDBConn", reflect.TypeOf((*MockConnPool)(nil).GetDBConn)) +} + +// Ping mocks base method. +func (m *MockConnPool) Ping() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Ping") + ret0, _ := ret[0].(error) + return ret0 +} + +// Ping indicates an expected call of Ping. +func (mr *MockConnPoolMockRecorder) Ping() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockConnPool)(nil).Ping)) +} + +// Prepare mocks base method. +func (m *MockConnPool) Prepare(query string) (*sql.Stmt, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Prepare", query) + ret0, _ := ret[0].(*sql.Stmt) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Prepare indicates an expected call of Prepare. +func (mr *MockConnPoolMockRecorder) Prepare(query interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Prepare", reflect.TypeOf((*MockConnPool)(nil).Prepare), query) +} + +// PrepareContext mocks base method. +func (m *MockConnPool) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PrepareContext", ctx, query) + ret0, _ := ret[0].(*sql.Stmt) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PrepareContext indicates an expected call of PrepareContext. +func (mr *MockConnPoolMockRecorder) PrepareContext(ctx, query interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrepareContext", reflect.TypeOf((*MockConnPool)(nil).PrepareContext), ctx, query) +} + +// Query mocks base method. +func (m *MockConnPool) Query(query string, args ...interface{}) (*sql.Rows, error) { + m.ctrl.T.Helper() + varargs := []interface{}{query} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Query", varargs...) + ret0, _ := ret[0].(*sql.Rows) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Query indicates an expected call of Query. +func (mr *MockConnPoolMockRecorder) Query(query interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{query}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockConnPool)(nil).Query), varargs...) +} + +// QueryContext mocks base method. +func (m *MockConnPool) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, query} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "QueryContext", varargs...) + ret0, _ := ret[0].(*sql.Rows) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryContext indicates an expected call of QueryContext. +func (mr *MockConnPoolMockRecorder) QueryContext(ctx, query interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, query}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryContext", reflect.TypeOf((*MockConnPool)(nil).QueryContext), varargs...) +} + +// QueryRow mocks base method. +func (m *MockConnPool) QueryRow(query string, args ...interface{}) *sql.Row { + m.ctrl.T.Helper() + varargs := []interface{}{query} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "QueryRow", varargs...) + ret0, _ := ret[0].(*sql.Row) + return ret0 +} + +// QueryRow indicates an expected call of QueryRow. +func (mr *MockConnPoolMockRecorder) QueryRow(query interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{query}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRow", reflect.TypeOf((*MockConnPool)(nil).QueryRow), varargs...) +} + +// QueryRowContext mocks base method. +func (m *MockConnPool) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, query} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "QueryRowContext", varargs...) + ret0, _ := ret[0].(*sql.Row) + return ret0 +} + +// QueryRowContext indicates an expected call of QueryRowContext. +func (mr *MockConnPoolMockRecorder) QueryRowContext(ctx, query interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, query}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRowContext", reflect.TypeOf((*MockConnPool)(nil).QueryRowContext), varargs...) +} diff --git a/gorm/plugin.go b/gorm/plugin.go index 625d975..c577f59 100644 --- a/gorm/plugin.go +++ b/gorm/plugin.go @@ -37,6 +37,7 @@ type loggerConfig struct { Colorful bool `yaml:"colorful"` IgnoreRecordNotFoundError bool `yaml:"ignore_record_not_found_error"` LogLevel logger.LogLevel `yaml:"log_level"` + MaxSqlSize int `yaml:"max_sql_size"` } // Config is the struct for the configuration of the SQL proxy. @@ -44,6 +45,7 @@ type Config struct { MaxIdle int `yaml:"max_idle"` // Maximum number of idle connections. MaxOpen int `yaml:"max_open"` // Maximum number of connections that can be open at same time. MaxLifetime int `yaml:"max_lifetime"` // The maximum lifetime of each connection, in milliseconds. + DriverName string `yaml:"driver_name"` // Driver name for customization. Logger *loggerConfig `yaml:"logger"` // Logger configuration. Service []struct { // In the case of having multiple database connections, @@ -76,8 +78,6 @@ func (m *Plugin) Setup(name string, configDesc plugin.Decoder) (err error) { if err = configDesc.Decode(&config); err != nil { return } - - poolConfigs := make(map[string]PoolConfig, len(config.Service)) if config.Logger != nil { loggers["*"] = NewTRPCLogger(logger.Config{ SlowThreshold: time.Duration(config.Logger.SlowThreshold) * time.Millisecond, @@ -85,14 +85,38 @@ func (m *Plugin) Setup(name string, configDesc plugin.Decoder) (err error) { IgnoreRecordNotFoundError: config.Logger.IgnoreRecordNotFoundError, LogLevel: config.Logger.LogLevel, }) + loggers["*"].maxSqlSize = config.Logger.MaxSqlSize + } + // Set pool configurations that effective for all GORM clients. + defaultClientTransport.DefaultPoolConfig = PoolConfig{ + MaxIdle: config.MaxIdle, + MaxOpen: config.MaxOpen, + MaxLifetime: time.Duration(config.MaxLifetime) * time.Millisecond, + DriverName: config.DriverName, } + setDefaultValueOfGlobalPoolConfig(&defaultClientTransport.DefaultPoolConfig) + // Set pool configurations that effective for each GORM client. + poolConfigs := make(map[string]PoolConfig, len(config.Service)) for _, s := range config.Service { - poolConfigs[s.Name] = PoolConfig{ - MaxIdle: s.MaxIdle, - MaxOpen: s.MaxOpen, - MaxLifetime: time.Duration(s.MaxLifetime) * time.Millisecond, - DriverName: s.DriverName, + servicePoolConfig := PoolConfig{ + MaxIdle: defaultClientTransport.DefaultPoolConfig.MaxIdle, + MaxOpen: defaultClientTransport.DefaultPoolConfig.MaxOpen, + MaxLifetime: defaultClientTransport.DefaultPoolConfig.MaxLifetime, + DriverName: defaultClientTransport.DefaultPoolConfig.DriverName, + } + if s.MaxIdle != 0 { + servicePoolConfig.MaxIdle = s.MaxIdle + } + if s.MaxOpen != 0 { + servicePoolConfig.MaxOpen = s.MaxOpen } + if s.MaxLifetime != 0 { + servicePoolConfig.MaxLifetime = time.Duration(s.MaxLifetime) * time.Millisecond + } + if s.DriverName != "" { + servicePoolConfig.DriverName = s.DriverName + } + poolConfigs[s.Name] = servicePoolConfig if s.Logger != nil { loggers[s.Name] = NewTRPCLogger(logger.Config{ SlowThreshold: time.Duration(s.Logger.SlowThreshold) * time.Millisecond, @@ -100,16 +124,29 @@ func (m *Plugin) Setup(name string, configDesc plugin.Decoder) (err error) { IgnoreRecordNotFoundError: s.Logger.IgnoreRecordNotFoundError, LogLevel: s.Logger.LogLevel, }) + loggers[s.Name].maxSqlSize = s.Logger.MaxSqlSize } } defaultClientTransport.PoolConfigs = poolConfigs - - defaultClientTransport.DefaultPoolConfig = PoolConfig{ - MaxIdle: config.MaxIdle, - MaxOpen: config.MaxOpen, - MaxLifetime: time.Duration(config.MaxLifetime) * time.Millisecond, - } // Need to call the register function explicitly, otherwise the configuration will not take effect. transport.RegisterClientTransport("gorm", defaultClientTransport) return nil } + +const ( + defaultMaxIdle = 10 + defaultMaxOpen = 10000 + defaultMaxLifetime = 3 * time.Minute +) + +func setDefaultValueOfGlobalPoolConfig(pc *PoolConfig) { + if pc.MaxIdle == 0 { + pc.MaxIdle = defaultMaxIdle + } + if pc.MaxOpen == 0 { + pc.MaxOpen = defaultMaxOpen + } + if pc.MaxLifetime == time.Duration(0) { + pc.MaxLifetime = defaultMaxLifetime + } +} diff --git a/gorm/plugin_test.go b/gorm/plugin_test.go index bc0d7c6..10f3cfb 100644 --- a/gorm/plugin_test.go +++ b/gorm/plugin_test.go @@ -5,37 +5,33 @@ import ( "testing" "time" - "github.com/google/go-cmp/cmp/cmpopts" - "gorm.io/gorm/logger" - "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + . "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" + "gorm.io/gorm/logger" "trpc.group/trpc-go/trpc-go" "trpc.group/trpc-go/trpc-go/plugin" "trpc.group/trpc-go/trpc-go/transport" - - . "github.com/smartystreets/goconvey/convey" ) -// TestUnit_GormPlugin_Type_P0 GormPlugin.Type test case. -func TestUnit_GormPlugin_Type_P0(t *testing.T) { - Convey("TestUnit_GormPlugin_Type_P0", t, func() { +func TestGetGormPluginType(t *testing.T) { + Convey("Get gorm plugin type", t, func() { sqlPlugin := new(Plugin) So(sqlPlugin.Type(), ShouldEqual, pluginType) }) } -// TestUnit_GormPlugin_Setup_P0 GormPlugin.Setup test case. -func TestUnit_GormPlugin_Setup_P0(t *testing.T) { - Convey("TestUnit_GormPlugin_Setup_P0", t, func() { +func TestGormPluginSetupCompleteConfigs(t *testing.T) { + Convey("Gorm plugin setup complete global config and complete service configs", t, func() { mp := &Plugin{} - Convey("Config Decode Fail", func() { + Convey("Config decode fail", func() { err := mp.Setup(pluginName, &plugin.YamlNodeDecoder{Node: nil}) So(err, ShouldNotBeNil) So(err.Error(), ShouldEqual, "yaml node empty") }) - Convey("Setup Success", func() { + Convey("Setup success", func() { var bts = ` plugins: database: @@ -58,6 +54,7 @@ plugins: colorful: true ignore_record_not_found_error: true log_level: 4 + max_sql_size: 1024 - name: trpc.mysql.test.db2 max_idle: 5 max_open: 10 @@ -68,7 +65,6 @@ plugins: ignore_record_not_found_error: false log_level: 3 ` - var cfg = trpc.Config{} err := yaml.Unmarshal([]byte(bts), &cfg) assert.Nil(t, err) @@ -116,6 +112,259 @@ plugins: }) } +func TestGormPluginSetupDefaultConfigs(t *testing.T) { + Convey("Gorm plugin setup default global config and default service configs", t, func() { + mp := &Plugin{} + Convey("Setup success", func() { + var bts = ` +plugins: + database: + gorm: + service: + - name: trpc.mysql.test.db1 + - name: trpc.mysql.test.db2 +` + var cfg = trpc.Config{} + err := yaml.Unmarshal([]byte(bts), &cfg) + assert.Nil(t, err) + var yamlNode *yaml.Node + if configP, ok := cfg.Plugins[pluginType]; ok { + if node, ok := configP[pluginName]; ok { + yamlNode = &node + } + } + So(yamlNode, ShouldNotBeNil) + + err = mp.Setup(pluginName, &plugin.YamlNodeDecoder{Node: yamlNode}) + So(err, ShouldBeNil) + + ct := transport.GetClientTransport(pluginName) + clientTransport, ok := ct.(*ClientTransport) + So(ok, ShouldBeTrue) + + expectedTransport := &ClientTransport{ + opts: &transport.ClientTransportOptions{}, + SQLDB: make(map[string]*sql.DB), + DefaultPoolConfig: PoolConfig{ + MaxIdle: defaultMaxIdle, + MaxOpen: defaultMaxOpen, + MaxLifetime: defaultMaxLifetime, + }, + PoolConfigs: map[string]PoolConfig{ + "trpc.mysql.test.db1": { + MaxIdle: defaultMaxIdle, + MaxOpen: defaultMaxOpen, + MaxLifetime: defaultMaxLifetime, + }, + "trpc.mysql.test.db2": { + MaxIdle: defaultMaxIdle, + MaxOpen: defaultMaxOpen, + MaxLifetime: defaultMaxLifetime, + }, + }, + } + // cmp.opener is of type func which is not comparable, so only compare the unexported fields if needed. + So(cmp.Equal(clientTransport.opts, expectedTransport.opts), ShouldBeTrue) + So(cmp.Equal(clientTransport, expectedTransport, cmpopts.IgnoreUnexported(ClientTransport{}), + cmpopts.IgnoreFields(ClientTransport{}, "SQLDBLock")), ShouldBeTrue) + }) + }) +} + +func TestGormPluginSetupEffectOrder_P0(t *testing.T) { + Convey("Service configs use the global config when there are no local configs", t, func() { + mp := &Plugin{} + Convey("Setup success", func() { + var bts = ` +plugins: + database: + gorm: + max_idle: 20 + max_open: 100 + max_lifetime: 180 + service: + - name: trpc.mysql.test.db1 + - name: trpc.mysql.test.db2 +` + var cfg = trpc.Config{} + err := yaml.Unmarshal([]byte(bts), &cfg) + assert.Nil(t, err) + var yamlNode *yaml.Node + if configP, ok := cfg.Plugins[pluginType]; ok { + if node, ok := configP[pluginName]; ok { + yamlNode = &node + } + } + So(yamlNode, ShouldNotBeNil) + + err = mp.Setup(pluginName, &plugin.YamlNodeDecoder{Node: yamlNode}) + So(err, ShouldBeNil) + + ct := transport.GetClientTransport(pluginName) + clientTransport, ok := ct.(*ClientTransport) + So(ok, ShouldBeTrue) + + expectedTransport := &ClientTransport{ + opts: &transport.ClientTransportOptions{}, + SQLDB: make(map[string]*sql.DB), + DefaultPoolConfig: PoolConfig{ + MaxIdle: 20, + MaxOpen: 100, + MaxLifetime: 180 * time.Millisecond, + }, + PoolConfigs: map[string]PoolConfig{ + "trpc.mysql.test.db1": { + MaxIdle: 20, + MaxOpen: 100, + MaxLifetime: 180 * time.Millisecond, + }, + "trpc.mysql.test.db2": { + MaxIdle: 20, + MaxOpen: 100, + MaxLifetime: 180 * time.Millisecond, + }, + }, + } + // cmp.opener is of type func which is not comparable, so only compare the unexported fields if needed. + So(cmp.Equal(clientTransport.opts, expectedTransport.opts), ShouldBeTrue) + So(cmp.Equal(clientTransport, expectedTransport, cmpopts.IgnoreUnexported(ClientTransport{}), + cmpopts.IgnoreFields(ClientTransport{}, "SQLDBLock")), ShouldBeTrue) + }) + }) +} + +func TestGormPluginSetupEffectOrder_P1(t *testing.T) { + Convey("Service local configs override default global config", t, func() { + mp := &Plugin{} + Convey("Setup success", func() { + var bts = ` +plugins: + database: + gorm: + service: + - name: trpc.mysql.test.db1 + max_idle: 20 + max_open: 300 + max_lifetime: 4000 + - name: trpc.mysql.test.db2 + max_idle: 50 + max_open: 600 + max_lifetime: 7000 +` + var cfg = trpc.Config{} + err := yaml.Unmarshal([]byte(bts), &cfg) + assert.Nil(t, err) + var yamlNode *yaml.Node + if configP, ok := cfg.Plugins[pluginType]; ok { + if node, ok := configP[pluginName]; ok { + yamlNode = &node + } + } + So(yamlNode, ShouldNotBeNil) + + err = mp.Setup(pluginName, &plugin.YamlNodeDecoder{Node: yamlNode}) + So(err, ShouldBeNil) + + ct := transport.GetClientTransport(pluginName) + clientTransport, ok := ct.(*ClientTransport) + So(ok, ShouldBeTrue) + + expectedTransport := &ClientTransport{ + opts: &transport.ClientTransportOptions{}, + SQLDB: make(map[string]*sql.DB), + DefaultPoolConfig: PoolConfig{ + MaxIdle: defaultMaxIdle, + MaxOpen: defaultMaxOpen, + MaxLifetime: defaultMaxLifetime, + }, + PoolConfigs: map[string]PoolConfig{ + "trpc.mysql.test.db1": { + MaxIdle: 20, + MaxOpen: 300, + MaxLifetime: 4000 * time.Millisecond, + }, + "trpc.mysql.test.db2": { + MaxIdle: 50, + MaxOpen: 600, + MaxLifetime: 7000 * time.Millisecond, + }, + }, + } + // cmp.opener is of type func which is not comparable, so only compare the unexported fields if needed. + So(cmp.Equal(clientTransport.opts, expectedTransport.opts), ShouldBeTrue) + So(cmp.Equal(clientTransport, expectedTransport, cmpopts.IgnoreUnexported(ClientTransport{}), + cmpopts.IgnoreFields(ClientTransport{}, "SQLDBLock")), ShouldBeTrue) + }) + }) +} + +func TestGormPluginSetupEffectOrder_P2(t *testing.T) { + Convey("Local config 1 use default global config and local config 2 override default global config", t, func() { + mp := &Plugin{} + Convey("Setup success", func() { + var bts = ` +plugins: + database: + gorm: + max_idle: 0 + max_open: 0 + max_lifetime: 0 + service: + - name: trpc.mysql.test.db1 + max_idle: 0 + max_open: 0 + - name: trpc.mysql.test.db2 + max_idle: 50 + max_open: 600 + max_lifetime: 7000 +` + var cfg = trpc.Config{} + err := yaml.Unmarshal([]byte(bts), &cfg) + assert.Nil(t, err) + var yamlNode *yaml.Node + if configP, ok := cfg.Plugins[pluginType]; ok { + if node, ok := configP[pluginName]; ok { + yamlNode = &node + } + } + So(yamlNode, ShouldNotBeNil) + + err = mp.Setup(pluginName, &plugin.YamlNodeDecoder{Node: yamlNode}) + So(err, ShouldBeNil) + + ct := transport.GetClientTransport(pluginName) + clientTransport, ok := ct.(*ClientTransport) + So(ok, ShouldBeTrue) + + expectedTransport := &ClientTransport{ + opts: &transport.ClientTransportOptions{}, + SQLDB: make(map[string]*sql.DB), + DefaultPoolConfig: PoolConfig{ + MaxIdle: defaultMaxIdle, + MaxOpen: defaultMaxOpen, + MaxLifetime: defaultMaxLifetime, + }, + PoolConfigs: map[string]PoolConfig{ + "trpc.mysql.test.db1": { + MaxIdle: defaultMaxIdle, + MaxOpen: defaultMaxOpen, + MaxLifetime: defaultMaxLifetime, + }, + "trpc.mysql.test.db2": { + MaxIdle: 50, + MaxOpen: 600, + MaxLifetime: 7000 * time.Millisecond, + }, + }, + } + // cmp.opener is of type func which is not comparable, so only compare the unexported fields if needed. + So(cmp.Equal(clientTransport.opts, expectedTransport.opts), ShouldBeTrue) + So(cmp.Equal(clientTransport, expectedTransport, cmpopts.IgnoreUnexported(ClientTransport{}), + cmpopts.IgnoreFields(ClientTransport{}, "SQLDBLock")), ShouldBeTrue) + }) + }) +} + func TestUnit_getLogger(t *testing.T) { Convey("TestUnit_getLogger", t, func() { loggers = map[string]*TRPCLogger{"*": DefaultTRPCLogger} diff --git a/gorm/transport.go b/gorm/transport.go index 534f002..e25efee 100644 --- a/gorm/transport.go +++ b/gorm/transport.go @@ -3,6 +3,7 @@ package gorm import ( "context" "database/sql" + "errors" "fmt" "strings" "sync" @@ -10,16 +11,19 @@ import ( "github.com/ClickHouse/clickhouse-go/v2" "github.com/go-sql-driver/mysql" + "golang.org/x/sync/singleflight" + dsn "trpc.group/trpc-go/trpc-selector-dsn" + "trpc.group/trpc-go/trpc-go/codec" "trpc.group/trpc-go/trpc-go/errs" "trpc.group/trpc-go/trpc-go/naming/selector" "trpc.group/trpc-go/trpc-go/transport" - trpcdsn "trpc.group/trpc-go/trpc-selector-dsn" ) func init() { transport.RegisterClientTransport("gorm", defaultClientTransport) - selector.Register("gorm+polaris", trpcdsn.NewResolvableSelector("polaris", &trpcdsn.URIHostExtractor{})) + selector.Register("gorm+polaris", dsn.NewResolvableSelectorWithOpts("polaris", + dsn.WithEnableParseAddr(true), dsn.WithExtractor(&dsn.URIHostExtractor{}))) } // PoolConfig is the configuration of the database connection pool. @@ -37,6 +41,7 @@ type ClientTransport struct { opts *transport.ClientTransportOptions SQLDB map[string]*sql.DB SQLDBLock sync.RWMutex + sfg singleflight.Group DefaultPoolConfig PoolConfig PoolConfigs map[string]PoolConfig } @@ -57,9 +62,9 @@ func NewClientTransport(opt ...transport.ClientTransportOption) *ClientTransport opts: opts, SQLDB: make(map[string]*sql.DB), DefaultPoolConfig: PoolConfig{ - MaxIdle: 10, - MaxOpen: 10000, - MaxLifetime: 3 * time.Minute, + MaxIdle: defaultMaxIdle, + MaxOpen: defaultMaxOpen, + MaxLifetime: defaultMaxLifetime, }, } } @@ -103,22 +108,49 @@ func (ct *ClientTransport) RoundTrip(ctx context.Context, reqBuf []byte, for _, o := range callOpts { o(sqlOpts) } + withCommonMetaCalleeAppServer(msg, ct.getDBType(msg.CalleeServiceName()), mask(sqlOpts.Address)) + + err = ct.getDBAndRunCommand(ctx, sqlOpts.Address, req, rsp) + if err == nil { + return + } + + // DB might be closed by gorm.io/gorm. If the failure is due to the closure of DB, recreate DB and retry. + // internal repository issues/525 + if isDBClosedErr(err) { + ct.deleteDB(sqlOpts.Address) + err = ct.getDBAndRunCommand(ctx, sqlOpts.Address, req, rsp) + } + return +} +func (ct *ClientTransport) getDBAndRunCommand(ctx context.Context, address string, req *Request, rsp *Response) error { // If a new type of database is added, the database type needs to be passed in here. // The CalleeServcieName can be read from sqlOpts.Msg, // which is the service name in the trpc framework configuration. // The database type can be obtained based on the second segment of the service name. - db, err := ct.GetDB(msg.CalleeServiceName(), sqlOpts.Address) + msg := codec.Message(ctx) + db, err := ct.GetDB(msg.CalleeServiceName(), address) if err != nil { - err = fmt.Errorf( - `err: %w, -current masked sqlOpts.Address: %s, -if it is not what you want, it is possible that your client config is not loaded correctly`, - err, mask(sqlOpts.Address)) // Mask out the credentials. - return + return wrapGetDbError(err, address) } - err = runCommand(ctx, db, req, rsp) - return + return runCommand(ctx, db, req, rsp) +} + +// withCommonMetaCalleeAppServer Populate the called app and the called server with real DB instances +// in CommonMeta for fault location. +func withCommonMetaCalleeAppServer(msg codec.Msg, calleeApp, calleeServer string) { + meta := msg.CommonMeta() + if meta == nil { + meta = codec.CommonMeta{} + } + const ( + appKey = "overrideCalleeApp" + serverKey = "overrideCalleeServer" + ) + meta[appKey] = calleeApp + meta[serverKey] = calleeServer + msg.WithCommonMeta(meta) } // mask masks the given string with '*' characters in the middle to prevent security vulnerabilities. @@ -199,6 +231,9 @@ func runCommand(ctx context.Context, db *sql.DB, req *Request, rsp *Response) er // The definition of sql.Row contains an error, so this operation does not handle err, // but passes it to the upstream in the result. row := db.QueryRowContext(ctx, req.Query, req.Args...) + if row != nil && isDBClosedErr(row.Err()) { + return row.Err() + } rsp.Row = row case OpBeginTx: // Default level is sql.LevelDefault. @@ -218,6 +253,8 @@ func runCommand(ctx context.Context, db *sql.DB, req *Request, rsp *Response) er return nil } +var errDBAreadyExist = errors.New("the db is already exist") + // GetDB retrieves the database connection, currently supports mysql/clickhouse, // can be extended for other types of databases. func (ct *ClientTransport) GetDB(serviceName, dsn string) (*sql.DB, error) { @@ -228,35 +265,52 @@ func (ct *ClientTransport) GetDB(serviceName, dsn string) (*sql.DB, error) { if ok { return db, nil } - ct.SQLDBLock.Lock() - defer ct.SQLDBLock.Unlock() - db, ok = ct.SQLDB[dsn] - if ok { - return db, nil - } - // Pass the database type as part of the serviceName, such as trpc.mysql.xxx.xxx/trpc.clickhouse.xxx.xxx, - // and use different drivers internally based on different types. - db, err := ct.initDB(serviceName, dsn) - if err != nil { - return nil, wrapperSQLOpenError(err) - } - poolConfig, ok := ct.PoolConfigs[serviceName] - if !ok { - poolConfig = ct.DefaultPoolConfig - } - if poolConfig.MaxIdle > 0 { + iDB, err, _ := ct.sfg.Do(dsn, func() (interface{}, error) { + ct.SQLDBLock.RLock() + db, ok = ct.SQLDB[dsn] + ct.SQLDBLock.RUnlock() + if ok { + return db, nil + } + // Pass the database type as part of the serviceName, such as trpc.mysql.xxx.xxx/trpc.clickhouse.xxx.xxx, + // and use different drivers internally based on different types. + db, err := ct.initDB(serviceName, dsn) + if err != nil { + return nil, wrapperSQLOpenError(err) + } + poolConfig, ok := ct.PoolConfigs[serviceName] + if !ok { + poolConfig = ct.DefaultPoolConfig + } db.SetMaxIdleConns(poolConfig.MaxIdle) - } - if poolConfig.MaxOpen > 0 { db.SetMaxOpenConns(poolConfig.MaxOpen) - } - if poolConfig.MaxLifetime > 0 { db.SetConnMaxLifetime(poolConfig.MaxLifetime) + ct.SQLDBLock.Lock() + ct.SQLDB[dsn] = db + ct.SQLDBLock.Unlock() + return db, nil + }) + if err == nil || errors.Is(err, errDBAreadyExist) { + return iDB.(*sql.DB), nil } + return nil, err +} + +func (ct *ClientTransport) deleteDB(dsn string) { + ct.SQLDBLock.Lock() + delete(ct.SQLDB, dsn) + ct.SQLDBLock.Unlock() +} - ct.SQLDB[dsn] = db - return db, nil +func (ct *ClientTransport) getDBType(s string) string { + splitServiceName := strings.Split(s, ".") + // Compatibility logic, keeps MySQL as the default. + // internal repository issues/235 + if len(splitServiceName) < 2 { + return "mysql" + } + return splitServiceName[1] } func (ct *ClientTransport) initDB(s, dsn string) (*sql.DB, error) { @@ -267,13 +321,10 @@ func (ct *ClientTransport) initDB(s, dsn string) (*sql.DB, error) { if conf, ok := ct.PoolConfigs[s]; ok && conf.DriverName != "" { return ct.opener(conf.DriverName, dsn) } - splitServiceName := strings.Split(s, ".") - if len(splitServiceName) < 2 { - return ct.opener("mysql", dsn) + if ct.DefaultPoolConfig.DriverName != "" { + return ct.opener(ct.DefaultPoolConfig.DriverName, dsn) } - // Compatibility logic, keeps MySQL as the default. - dbEngineType := splitServiceName[1] - switch dbEngineType { + switch ct.getDBType(s) { case "clickhouse": return ct.opener("clickhouse", dsn) case "postgres": @@ -292,3 +343,15 @@ func wrapperSQLOpenError(err error) error { } return err } + +func wrapGetDbError(err error, address string) error { + return fmt.Errorf( + `err: %w, +current masked sqlOpts.Address: %s, +if it is not what you want, it is possible that your client config is not loaded correctly`, + err, mask(address)) // Mask out the credentials. +} + +func isDBClosedErr(err error) bool { + return err != nil && err.Error() == "sql: database is closed" +} diff --git a/gorm/transport_test.go b/gorm/transport_test.go index d0d38d0..bcd5ebf 100644 --- a/gorm/transport_test.go +++ b/gorm/transport_test.go @@ -8,33 +8,21 @@ import ( "testing" "time" + "trpc.group/trpc-go/trpc-go/codec" + "trpc.group/trpc-go/trpc-go/errs" + "trpc.group/trpc-go/trpc-go/transport" + "github.com/ClickHouse/clickhouse-go/v2" "github.com/DATA-DOG/go-sqlmock" "github.com/go-sql-driver/mysql" . "github.com/smartystreets/goconvey/convey" - "trpc.group/trpc-go/trpc-go/codec" - "trpc.group/trpc-go/trpc-go/errs" - "trpc.group/trpc-go/trpc-go/transport" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -var dsn = "user:password@tcp(localhost:5555)/dbname" +var stdDsn = "user:password@tcp(localhost:5555)/dbname" var clickhouseDsn = "dsn://tcp://localhost:9000/dbname" -func TestUnit_NewClientTransport_P0(t *testing.T) { - Convey("TestUnit_NewClientTransport_P0", t, func() { - ctt := NewClientTransport() - - So(ctt.opener, ShouldEqual, sql.Open) - So(ctt.opts, ShouldResemble, &transport.ClientTransportOptions{}) - So(ctt.SQLDB, ShouldBeEmpty) - So(ctt.DefaultPoolConfig, ShouldResemble, PoolConfig{ - MaxIdle: 10, - MaxOpen: 10000, - MaxLifetime: 3 * time.Minute, - }) - }) -} - func TestUnit_ClientTransport_GetDB_P0(t *testing.T) { Convey("TestUnit_ClientTransport_GetDB_P0", t, func() { ct := new(ClientTransport) @@ -52,8 +40,8 @@ func TestUnit_ClientTransport_GetDB_P0(t *testing.T) { }, } Convey("DSN Already Exists", func() { - ct.SQLDB[dsn] = new(sql.DB) - db, err := ct.GetDB("", dsn) + ct.SQLDB[stdDsn] = new(sql.DB) + db, err := ct.GetDB("", stdDsn) So(db, ShouldNotBeNil) So(err, ShouldBeNil) }) @@ -61,7 +49,7 @@ func TestUnit_ClientTransport_GetDB_P0(t *testing.T) { ct.opener = func(driverName, dataSourceName string) (*sql.DB, error) { return nil, fmt.Errorf("fake error") } - db, err := ct.GetDB("", dsn) + db, err := ct.GetDB("", stdDsn) So(db, ShouldBeNil) So(err, ShouldNotBeNil) So(err.Error(), ShouldEqual, "fake error") @@ -70,7 +58,7 @@ func TestUnit_ClientTransport_GetDB_P0(t *testing.T) { ct.opener = func(driverName, dataSourceName string) (*sql.DB, error) { return new(sql.DB), nil } - db, err := ct.GetDB("", dsn) + db, err := ct.GetDB("", stdDsn) So(db, ShouldNotBeNil) So(err, ShouldBeNil) }) @@ -87,8 +75,46 @@ func TestUnit_ClientTransport_GetDB_P0(t *testing.T) { }) } -func TestUnit_CT_GormDbInit_P0(t *testing.T) { - Convey("TestUnit_CT_GormDbInit_P0", t, func() { +func TestConcurrentGetDB(t *testing.T) { + ct := NewClientTransport() + ct.opener = func(_, dataSourceName string) (*sql.DB, error) { + if dataSourceName == "db2" { + time.Sleep(10 * time.Second) + } + return new(sql.DB), nil + } + + // First, we can get db 1 successfully. + p1, err := ct.GetDB("service1", "db1") + require.Nil(t, err) + require.NotNil(t, p1) + + // But, getting db 2 takes a long time. + go func() { + p2, err := ct.GetDB("service2", "db2") + require.Nil(t, err) + require.NotNil(t, p2) + }() + time.Sleep(time.Millisecond * 200) + + // We should not block getting db 1 because of db 2 + finished := make(chan struct{}, 1) + go func() { + p1, err := ct.GetDB("service1", "db1") + require.Nil(t, err) + require.NotNil(t, p1) + finished <- struct{}{} + }() + select { + case <-finished: + case <-time.After(time.Second * 5): + require.FailNow(t, "get producer blocking") + } +} + +func TestUnit_ClientTransport_GormDbInit_P0(t *testing.T) { + sql.Register("MyDriver", mysql.MySQLDriver{}) + Convey("TestUnit_ClientTransport_GormDbInit_P0", t, func() { ct := new(ClientTransport) ct.SQLDB = make(map[string]*sql.DB) ct.DefaultPoolConfig = PoolConfig{ @@ -110,31 +136,38 @@ func TestUnit_CT_GormDbInit_P0(t *testing.T) { So(err, ShouldBeNil) }) Convey("Init mysql gorm DB", func() { - db, err := ct.GetDB("trpc.mysql.test.db", dsn) + db, err := ct.GetDB("trpc.mysql.test.db", stdDsn) So(db, ShouldNotBeNil) So(err, ShouldBeNil) }) Convey("Init postgresql gorm DB", func() { - db, err := ct.GetDB("trpc.postgres.test.db", dsn) + db, err := ct.GetDB("trpc.postgres.test.db", stdDsn) So(db, ShouldNotBeNil) So(err, ShouldBeNil) }) // Perform default initialization logic for MySQL. Convey("Init default gorm DB", func() { - db, err := ct.GetDB("db", dsn) + db, err := ct.GetDB("db", stdDsn) + So(db, ShouldNotBeNil) + So(err, ShouldBeNil) + }) + + ct.DefaultPoolConfig.DriverName = "MyDriver" + Convey("Init custom driver gorm DB", func() { + db, err := ct.GetDB("trpc.mysql.test.db", stdDsn) So(db, ShouldNotBeNil) So(err, ShouldBeNil) }) }) } -// TestUnit_CT_RoundTrip_P0 ClientTransport.RoundTrip test case. -func TestUnit_CT_RoundTrip_P0(t *testing.T) { - Convey("TestUnit_CT_RoundTrip_P0", t, func() { +// TestUnit_ClientTransport_RoundTrip_P0 ClientTransport.RoundTrip test case. +func TestUnit_ClientTransport_RoundTrip_P0(t *testing.T) { + Convey("TestUnit_ClientTransport_RoundTrip_P0", t, func() { ctx, msg := codec.WithNewMessage(context.Background()) opts := []transport.RoundTripOption{ - transport.WithDialAddress(dsn), + transport.WithDialAddress(stdDsn), } reqBuf := make([]byte, 0) ct := new(ClientTransport) @@ -175,125 +208,12 @@ func TestUnit_CT_RoundTrip_P0(t *testing.T) { }) } -func getFakeErr(mockErr error) error { - switch sqlErr := mockErr.(type) { - case *mysql.MySQLError: - return errs.Wrap(sqlErr, int(sqlErr.Number), sqlErr.Message) - case *clickhouse.Exception: - return errs.Wrap(sqlErr, int(sqlErr.Code), sqlErr.Message) - case *errs.Error, nil: - default: - return errs.Wrap(mockErr, errs.RetUnknown, mockErr.Error()) - } - return mockErr -} - -func prepareContextConvey(ctx context.Context, ct *ClientTransport, request *Request, reqBuf []byte, - fakeErr error, mock sqlmock.Sqlmock, opts []transport.RoundTripOption) { - Convey("Do PrepareContext", func() { - request.Op = OpPrepareContext - - Convey("PrepareContext Fail", func() { - mock.ExpectPrepare(".*").WillReturnError(fakeErr) - rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) - So(rspBuf, ShouldBeNil) - So(err, ShouldResemble, getFakeErr(fakeErr)) - }) - Convey("PrepareContext Success", func() { - mock.ExpectPrepare(".*").WillReturnError(nil) - rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) - So(rspBuf, ShouldBeNil) - So(err, ShouldBeNil) - }) - }) -} - -func execContextConvey(ctx context.Context, ct *ClientTransport, request *Request, response *Response, reqBuf []byte, - fakeErr error, mock sqlmock.Sqlmock, opts []transport.RoundTripOption) { - Convey("Do ExecContext", func() { - request.Op = OpExecContext - - Convey("ExecContext Fail", func() { - mock.ExpectExec(".*").WillReturnError(fakeErr) - rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) - So(rspBuf, ShouldBeNil) - So(err, ShouldResemble, getFakeErr(fakeErr)) - }) - Convey("ExecContext Success", func() { - mock.ExpectExec(".*").WillReturnResult(driver.RowsAffected(1)) - - rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) - So(rspBuf, ShouldBeNil) - So(err, ShouldBeNil) - - affected, err := response.Result.RowsAffected() - So(affected, ShouldEqual, 1) - So(err, ShouldBeNil) - }) - }) -} - -func queryContextConvey(ctx context.Context, ct *ClientTransport, request *Request, response *Response, reqBuf []byte, - fakeErr error, mock sqlmock.Sqlmock, opts []transport.RoundTripOption) { - Convey("Do QueryContext", func() { - request.Op = OpQueryContext - - Convey("QueryContext Fail", func() { - mock.ExpectQuery(".*").WillReturnError(fakeErr) - rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) - So(rspBuf, ShouldBeNil) - So(err, ShouldResemble, getFakeErr(fakeErr)) - }) - Convey("QueryContext Success", func() { - mock.ExpectQuery(".*").WillReturnRows(sqlmock.NewRows([]string{"column_1"}).AddRow("value_1")) - - rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) - So(rspBuf, ShouldBeNil) - So(err, ShouldBeNil) - - columns, err := response.Rows.Columns() - So(columns, ShouldResemble, []string{"column_1"}) - So(err, ShouldBeNil) - - var values []string - for response.Rows.Next() { - var value string - err = response.Rows.Scan(&value) - So(err, ShouldBeNil) - values = append(values, value) - } - So(values, ShouldResemble, []string{"value_1"}) - }) - }) -} - -func queryRowContextConvey(ctx context.Context, ct *ClientTransport, request *Request, response *Response, - reqBuf []byte, fakeErr error, mock sqlmock.Sqlmock, opts []transport.RoundTripOption) { - Convey("Do QueryRowContext", func() { - request.Op = OpQueryRowContext - - // Will not fail. - Convey("QueryRowContext Success", func() { - mock.ExpectQuery(".*").WillReturnRows(sqlmock.NewRows([]string{"column_1"}).AddRow("value_1")) - - rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) - So(rspBuf, ShouldBeNil) - So(err, ShouldBeNil) - - var value string - err = response.Row.Scan(&value) - So(err, ShouldBeNil) - So(value, ShouldEqual, "value_1") - }) - }) -} - -// TestUnit_CT_RoundTrip_P0_2 ClientTransport.RoundTrip test case the second part. -func TestUnit_CT_RoundTrip_P0_2(t *testing.T) { - Convey("TestUnit_CT_RoundTrip_P0_2", t, func() { +// TestUnit_ClientTransport_RoundTrip_P0_2 ClientTransport.RoundTrip test case the second part. +func TestUnit_ClientTransport_RoundTrip_P0_2(t *testing.T) { + Convey("TestUnit_ClientTransport_RoundTrip_P0_2", t, func() { ctx, msg := codec.WithNewMessage(context.Background()) opts := []transport.RoundTripOption{ - transport.WithDialAddress(dsn), + transport.WithDialAddress(stdDsn), } reqBuf := make([]byte, 0) ct := new(ClientTransport) @@ -303,7 +223,7 @@ func TestUnit_CT_RoundTrip_P0_2(t *testing.T) { // add SQLDB in ClientTransport ct.SQLDB = map[string]*sql.DB{ - dsn: db, + stdDsn: db, } // add Request in ReqHead @@ -319,18 +239,98 @@ func TestUnit_CT_RoundTrip_P0_2(t *testing.T) { Message: "fake error", } - prepareContextConvey(ctx, ct, request, reqBuf, fakeErr, mock, opts) + Convey("Do PrepareContext", func() { + request.Op = OpPrepareContext + + Convey("PrepareContext Fail", func() { + mock.ExpectPrepare(".*").WillReturnError(fakeErr) + rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) + So(rspBuf, ShouldBeNil) + So(err, ShouldResemble, errs.Wrap(fakeErr, int(fakeErr.Number), fakeErr.Message)) + }) + Convey("PrepareContext Success", func() { + mock.ExpectPrepare(".*").WillReturnError(nil) + rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) + So(rspBuf, ShouldBeNil) + So(err, ShouldBeNil) + }) + }) + + Convey("Do ExecContext", func() { + request.Op = OpExecContext - execContextConvey(ctx, ct, request, response, reqBuf, fakeErr, mock, opts) + Convey("ExecContext Fail", func() { + mock.ExpectExec(".*").WillReturnError(fakeErr) + rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) + So(rspBuf, ShouldBeNil) + So(err, ShouldResemble, errs.Wrap(fakeErr, int(fakeErr.Number), fakeErr.Message)) + }) + Convey("ExecContext Success", func() { + mock.ExpectExec(".*").WillReturnResult(driver.RowsAffected(1)) - queryContextConvey(ctx, ct, request, response, reqBuf, fakeErr, mock, opts) + rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) + So(rspBuf, ShouldBeNil) + So(err, ShouldBeNil) - queryRowContextConvey(ctx, ct, request, response, reqBuf, fakeErr, mock, opts) + affected, err := response.Result.RowsAffected() + So(affected, ShouldEqual, 1) + So(err, ShouldBeNil) + }) + }) + + Convey("Do QueryContext", func() { + request.Op = OpQueryContext + + Convey("QueryContext Fail", func() { + mock.ExpectQuery(".*").WillReturnError(fakeErr) + rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) + So(rspBuf, ShouldBeNil) + So(err, ShouldResemble, errs.Wrap(fakeErr, int(fakeErr.Number), fakeErr.Message)) + }) + Convey("QueryContext Success", func() { + mock.ExpectQuery(".*").WillReturnRows(sqlmock.NewRows([]string{"column_1"}).AddRow("value_1")) + + rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) + So(rspBuf, ShouldBeNil) + So(err, ShouldBeNil) + + columns, err := response.Rows.Columns() + So(columns, ShouldResemble, []string{"column_1"}) + So(err, ShouldBeNil) + + var values []string + for response.Rows.Next() { + var value string + err = response.Rows.Scan(&value) + So(err, ShouldBeNil) + values = append(values, value) + } + So(values, ShouldResemble, []string{"value_1"}) + }) + }) + + Convey("Do QueryRowContext", func() { + request.Op = OpQueryRowContext + + // Will not fail. + Convey("QueryRowContext Success", func() { + mock.ExpectQuery(".*").WillReturnRows(sqlmock.NewRows([]string{"column_1"}).AddRow("value_1")) + + rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) + So(rspBuf, ShouldBeNil) + So(err, ShouldBeNil) + + var value string + err = response.Row.Scan(&value) + So(err, ShouldBeNil) + So(value, ShouldEqual, "value_1") + }) + }) Convey("Do GetDB", func() { request.Op = OpGetDB mockDB := new(sql.DB) - ct.SQLDB[dsn] = mockDB + ct.SQLDB[stdDsn] = mockDB // Will not fail. Convey("GetDB Success", func() { rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) @@ -342,11 +342,11 @@ func TestUnit_CT_RoundTrip_P0_2(t *testing.T) { }) } -func TestUnit_CT_RoundTrip_TX_P0(t *testing.T) { - Convey("TestUnit_CT_RoundTrip_TX_P0", t, func() { +func TestUnit_ClientTransport_RoundTrip_TX_P0(t *testing.T) { + Convey("TestUnit_ClientTransport_RoundTrip_TX_P0", t, func() { ctx, msg := codec.WithNewMessage(context.Background()) opts := []transport.RoundTripOption{ - transport.WithDialAddress(dsn), + transport.WithDialAddress(stdDsn), } reqBuf := make([]byte, 0) ct := new(ClientTransport) @@ -373,22 +373,102 @@ func TestUnit_CT_RoundTrip_TX_P0(t *testing.T) { Message: "fake error", } - prepareContextConvey(ctx, ct, request, reqBuf, fakeErr, mock, opts) + Convey("Do PrepareContext", func() { + request.Op = OpPrepareContext - execContextConvey(ctx, ct, request, response, reqBuf, fakeErr, mock, opts) + Convey("PrepareContext Fail", func() { + mock.ExpectPrepare(".*").WillReturnError(fakeErr) + rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) + So(rspBuf, ShouldBeNil) + So(err, ShouldResemble, errs.Wrap(fakeErr, int(fakeErr.Code), fakeErr.Message)) + }) + Convey("PrepareContext Success", func() { + mock.ExpectPrepare(".*").WillReturnError(nil) + rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) + So(rspBuf, ShouldBeNil) + So(err, ShouldBeNil) + }) + }) + + Convey("Do ExecContext", func() { + request.Op = OpExecContext + + Convey("ExecContext Fail", func() { + mock.ExpectExec(".*").WillReturnError(fakeErr) + rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) + So(rspBuf, ShouldBeNil) + So(err, ShouldResemble, errs.Wrap(fakeErr, int(fakeErr.Code), fakeErr.Message)) + }) + Convey("ExecContext Success", func() { + mock.ExpectExec(".*").WillReturnResult(driver.RowsAffected(1)) + + rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) + So(rspBuf, ShouldBeNil) + So(err, ShouldBeNil) + + affected, err := response.Result.RowsAffected() + So(affected, ShouldEqual, 1) + So(err, ShouldBeNil) + }) + }) + + Convey("Do QueryContext", func() { + request.Op = OpQueryContext + + Convey("QueryContext Fail", func() { + mock.ExpectQuery(".*").WillReturnError(fakeErr) + rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) + So(rspBuf, ShouldBeNil) + So(err, ShouldResemble, errs.Wrap(fakeErr, int(fakeErr.Code), fakeErr.Message)) + }) + Convey("QueryContext Success", func() { + mock.ExpectQuery(".*").WillReturnRows(sqlmock.NewRows([]string{"column_1"}).AddRow("value_1")) + + rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) + So(rspBuf, ShouldBeNil) + So(err, ShouldBeNil) + + columns, err := response.Rows.Columns() + So(columns, ShouldResemble, []string{"column_1"}) + So(err, ShouldBeNil) + + var values []string + for response.Rows.Next() { + var value string + err = response.Rows.Scan(&value) + So(err, ShouldBeNil) + values = append(values, value) + } + So(values, ShouldResemble, []string{"value_1"}) + }) + }) + + Convey("Do QueryRowContext", func() { + request.Op = OpQueryRowContext + + // Will not fail. + Convey("QueryRowContext Success", func() { + mock.ExpectQuery(".*").WillReturnRows(sqlmock.NewRows([]string{"column_1"}).AddRow("value_1")) - queryContextConvey(ctx, ct, request, response, reqBuf, fakeErr, mock, opts) + rspBuf, err := ct.RoundTrip(ctx, reqBuf, opts...) + So(rspBuf, ShouldBeNil) + So(err, ShouldBeNil) - queryRowContextConvey(ctx, ct, request, response, reqBuf, fakeErr, mock, opts) + var value string + err = response.Row.Scan(&value) + So(err, ShouldBeNil) + So(value, ShouldEqual, "value_1") + }) + }) }) } -// TestUnit_CT_RoundTrip_P1 -func TestUnit_CT_RoundTrip_P1(t *testing.T) { - Convey("TestUnit_CT_RoundTrip_P1", t, func() { +// TestUnit_ClientTransport_RoundTrip_P1 +func TestUnit_ClientTransport_RoundTrip_P1(t *testing.T) { + Convey("TestUnit_ClientTransport_RoundTrip_P0", t, func() { ctx, msg := codec.WithNewMessage(context.Background()) opts := []transport.RoundTripOption{ - transport.WithDialAddress(dsn), + transport.WithDialAddress(stdDsn), } reqBuf := make([]byte, 0) ct := new(ClientTransport) @@ -406,7 +486,7 @@ func TestUnit_CT_RoundTrip_P1(t *testing.T) { // add SQLDB in ClientTransport ct.SQLDB = map[string]*sql.DB{ - dsn: db, + stdDsn: db, } fakeErr := &mysql.MySQLError{ @@ -459,12 +539,12 @@ func TestUnit_CT_RoundTrip_P1(t *testing.T) { }) } -// TestUnit_CT_RoundTrip_TX_P1 -func TestUnit_CT_RoundTrip_TX_P1(t *testing.T) { - Convey("TestUnit_CT_RoundTrip_TX_P1", t, func() { +// TestUnit_ClientTransport_RoundTrip_TX_P1 +func TestUnit_ClientTransport_RoundTrip_TX_P1(t *testing.T) { + Convey("TestUnit_ClientTransport_RoundTrip_TX_P1", t, func() { ctx, msg := codec.WithNewMessage(context.Background()) opts := []transport.RoundTripOption{ - transport.WithDialAddress(dsn), + transport.WithDialAddress(stdDsn), } reqBuf := make([]byte, 0) ct := new(ClientTransport) @@ -552,3 +632,26 @@ func TestMask(t *testing.T) { }) } } + +func TestDatabaseIsClosed(t *testing.T) { + ctx, msg := codec.WithNewMessage(context.Background()) + ct := NewClientTransport() + rsp := &Response{} + msg.WithClientReqHead(&Request{Op: OpPrepareContext}) + msg.WithClientRspHead(rsp) + var count int + ct.opener = func(driverName, dataSourceName string) (*sql.DB, error) { + db, sqlMock, _ := sqlmock.New() + sqlMock.ExpectPrepare("") + // First, we create a closed db. + if count == 0 { + count++ + db.Close() + return db, nil + } + // Roundtrip will retry to get another db. + return db, nil + } + _, err := ct.RoundTrip(ctx, make([]byte, 0), transport.WithDialAddress(stdDsn)) + assert.Nil(t, err) +}