diff --git a/go.mod b/go.mod index 0f181866ce..420395003d 100644 --- a/go.mod +++ b/go.mod @@ -58,7 +58,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.4.3 gorm.io/driver/postgres v1.5.2 - gorm.io/gorm v1.25.2 + gorm.io/gorm v1.25.12 ) require ( diff --git a/go.sum b/go.sum index 8265831e0c..d476af2e51 100644 --- a/go.sum +++ b/go.sum @@ -3030,6 +3030,8 @@ gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho= gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= diff --git a/model/gorm_logger.go b/model/gorm_logger.go index 8143073023..e48f057b73 100644 --- a/model/gorm_logger.go +++ b/model/gorm_logger.go @@ -72,6 +72,11 @@ func sanitizeDBError(err error) error { } var pgErr *pgconn.PgError if errors.As(err, &pgErr) { + // 08P01 是 PgBouncer 对同连接重名 Parse 的 FATAL,42P05 是原生 PostgreSQL 的 + // duplicate_prepared_statement;都指向预处理语句与事务池代理不兼容。 + if pgErr.Code == "08P01" || pgErr.Code == "42P05" { + return fmt.Errorf("postgres error SQLSTATE %s: prepared statement conflict with a transaction-pooling proxy (PgBouncer/Neon/Supabase); other clients sharing this database must disable prepared statements, or upgrade PgBouncer to >=1.21 with max_prepared_statements enabled", pgErr.Code) + } return fmt.Errorf("postgres error SQLSTATE %s", pgErr.Code) } var chErr *proto.Exception diff --git a/model/gorm_logger_test.go b/model/gorm_logger_test.go index 6095e8beb3..7531b9ce31 100644 --- a/model/gorm_logger_test.go +++ b/model/gorm_logger_test.go @@ -35,6 +35,12 @@ func TestSanitizeDBErrorStripsDriverMessage(t *testing.T) { want: "postgres error SQLSTATE 23505", leaked: "secret-value", }, + { + name: "postgres pooler prepared statement conflict gets remediation hint", + err: &pgconn.PgError{Severity: "FATAL", Code: "08P01", Message: "prepared statement name is already in use: stmt_secret-value"}, + want: "postgres error SQLSTATE 08P01: prepared statement conflict with a transaction-pooling proxy (PgBouncer/Neon/Supabase); other clients sharing this database must disable prepared statements, or upgrade PgBouncer to >=1.21 with max_prepared_statements enabled", + leaked: "secret-value", + }, { name: "clickhouse exception", err: &proto.Exception{Code: 241, Message: "Memory limit exceeded while processing 'secret-value'"}, diff --git a/model/main.go b/model/main.go index 877b2019d9..3c10c3d36b 100644 --- a/model/main.go +++ b/model/main.go @@ -138,10 +138,12 @@ func chooseDB(envName string, isLog bool) (*gorm.DB, common.DatabaseType, error) if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") { // Use PostgreSQL common.SysLog("using PostgreSQL as database") + // 同时关闭 pgx 隐式与 GORM 显式预处理语句:命名 prepared statement 与 + // 事务池代理(PgBouncer/Neon/Supabase)不兼容,会触发 FATAL 08P01/42P05。 db, err := gorm.Open(postgres.New(postgres.Config{ DSN: dsn, - PreferSimpleProtocol: true, // disables implicit prepared statement usage - }), newGormConfig(true)) + PreferSimpleProtocol: true, + }), newGormConfig(false)) return db, common.DatabaseTypePostgreSQL, err } if strings.HasPrefix(dsn, "local") {