This commit is contained in:
@@ -8,11 +8,10 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
@@ -27,7 +26,7 @@ type CheckCollationsResult struct {
|
||||
InconsistentCollationColumns []string
|
||||
}
|
||||
|
||||
func findAvailableCollationsMySQL(x *xorm.Engine) (ret container.Set[string], err error) {
|
||||
func findAvailableCollationsMySQL(x EngineMigration) (ret container.Set[string], err error) {
|
||||
var res []struct {
|
||||
Collation string
|
||||
}
|
||||
@@ -41,7 +40,7 @@ func findAvailableCollationsMySQL(x *xorm.Engine) (ret container.Set[string], er
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func findAvailableCollationsMSSQL(x *xorm.Engine) (ret container.Set[string], err error) {
|
||||
func findAvailableCollationsMSSQL(x EngineMigration) (ret container.Set[string], err error) {
|
||||
var res []struct {
|
||||
Name string
|
||||
}
|
||||
@@ -55,7 +54,7 @@ func findAvailableCollationsMSSQL(x *xorm.Engine) (ret container.Set[string], er
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func CheckCollations(x *xorm.Engine) (*CheckCollationsResult, error) {
|
||||
func CheckCollations(x EngineMigration) (*CheckCollationsResult, error) {
|
||||
dbTables, err := x.DBMetas()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -143,7 +142,7 @@ func CheckCollationsDefaultEngine() (*CheckCollationsResult, error) {
|
||||
return CheckCollations(xormEngine)
|
||||
}
|
||||
|
||||
func alterDatabaseCollation(x *xorm.Engine, collation string) error {
|
||||
func alterDatabaseCollation(x EngineMigration, collation string) error {
|
||||
if x.Dialect().URI().DBType == schemas.MYSQL {
|
||||
_, err := x.Exec("ALTER DATABASE CHARACTER SET utf8mb4 COLLATE " + collation)
|
||||
return err
|
||||
@@ -156,7 +155,7 @@ func alterDatabaseCollation(x *xorm.Engine, collation string) error {
|
||||
}
|
||||
|
||||
// preprocessDatabaseCollation checks database & table column collation, and alter the database collation if needed
|
||||
func preprocessDatabaseCollation(x *xorm.Engine) {
|
||||
func preprocessDatabaseCollation(x EngineMigration) {
|
||||
r, err := CheckCollations(x)
|
||||
if err != nil {
|
||||
log.Error("Failed to check database collation: %v", err)
|
||||
|
||||
@@ -6,8 +6,8 @@ package db
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
type ConnOptions struct {
|
||||
Type setting.DatabaseType
|
||||
Host string
|
||||
Database string
|
||||
User string
|
||||
Passwd string
|
||||
Schema string
|
||||
SSLMode string
|
||||
|
||||
SQLitePath string
|
||||
SQLiteBusyTimeout int
|
||||
SQLiteJournalMode string
|
||||
}
|
||||
|
||||
type SQLiteConnStrOptions struct {
|
||||
FilePath string
|
||||
// how long a concurrent query can wait for others (milliseconds),
|
||||
// if timeout is reached, the error is something like "database is locked (SQLITE_BUSY)"
|
||||
BusyTimeout int
|
||||
JournalMode string
|
||||
}
|
||||
|
||||
func GlobalConnOptions() ConnOptions {
|
||||
return ConnOptions{
|
||||
Type: setting.Database.Type,
|
||||
Host: setting.Database.Host,
|
||||
Database: setting.Database.Name,
|
||||
User: setting.Database.User,
|
||||
Passwd: setting.Database.Passwd,
|
||||
Schema: setting.Database.Schema,
|
||||
SSLMode: setting.Database.SSLMode,
|
||||
|
||||
SQLitePath: setting.Database.Path,
|
||||
SQLiteBusyTimeout: setting.Database.SQLiteBusyTimeout,
|
||||
SQLiteJournalMode: setting.Database.SQLiteJournalMode,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
sqlDriverPostgresSchema = "postgresschema"
|
||||
sqlDriverSQLite3 = "sqlite3" // although database type also has "sqlite3", they are different, for different purposes
|
||||
)
|
||||
|
||||
var makeSQLiteConnStr = func(opts SQLiteConnStrOptions) (string, string, error) {
|
||||
return "", "", errors.New(`this Gitea binary was not built with SQLite3 support, get an official release or rebuild with correct "-tags"`)
|
||||
}
|
||||
|
||||
func registerSQLiteConnStrMaker(fn func(opts SQLiteConnStrOptions) (string, string, error)) {
|
||||
if slices.Contains(setting.SupportedDatabaseTypes, setting.DatabaseTypeSQLite3) {
|
||||
panic("another sqlite3 driver has been registered")
|
||||
}
|
||||
setting.SupportedDatabaseTypes = append(setting.SupportedDatabaseTypes, setting.DatabaseTypeSQLite3)
|
||||
makeSQLiteConnStr = fn
|
||||
}
|
||||
|
||||
func ConnStrDefaultDatabase(opts ConnOptions) (string, string, error) {
|
||||
opts.Database, opts.Schema = "", ""
|
||||
return ConnStr(opts)
|
||||
}
|
||||
|
||||
func ConnStr(opts ConnOptions) (string, string, error) {
|
||||
switch {
|
||||
case opts.Type.IsMySQL():
|
||||
// use unix socket or tcp socket
|
||||
connType := util.Iif(strings.HasPrefix(opts.Host, "/"), "unix", "tcp")
|
||||
// allow (Postgres-inspired) default value to work in MySQL
|
||||
tls := util.Iif(opts.SSLMode == "disable", "false", opts.SSLMode)
|
||||
// in case the database name is a partial connection string which contains "?" parameters
|
||||
paramSep := util.Iif(strings.Contains(opts.Database, "?"), "&", "?")
|
||||
connStr := fmt.Sprintf("%s:%s@%s(%s)/%s%sparseTime=true&tls=%s", opts.User, opts.Passwd, connType, opts.Host, opts.Database, paramSep, tls)
|
||||
return "mysql", connStr, nil
|
||||
|
||||
case opts.Type.IsPostgreSQL():
|
||||
connStr := makePgSQLConnStr(opts.Host, opts.User, opts.Passwd, opts.Database, opts.SSLMode)
|
||||
driver := util.Iif(opts.Schema == "", "postgres", sqlDriverPostgresSchema)
|
||||
registerPostgresSchemaDriver()
|
||||
return driver, connStr, nil
|
||||
|
||||
case opts.Type.IsMSSQL():
|
||||
host, port := parseMSSQLHostPort(opts.Host)
|
||||
connStr := fmt.Sprintf("server=%s; port=%s; user id=%s; password=%s;", host, port, opts.User, opts.Passwd)
|
||||
if opts.Database != "" {
|
||||
connStr += "; database=" + opts.Database
|
||||
}
|
||||
return "mssql", connStr, nil
|
||||
|
||||
case opts.Type.IsSQLite3():
|
||||
if opts.SQLitePath == "" {
|
||||
return "", "", errors.New("sqlite3 database path cannot be empty")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(opts.SQLitePath), os.ModePerm); err != nil {
|
||||
return "", "", fmt.Errorf("failed to create directories: %w", err)
|
||||
}
|
||||
return makeSQLiteConnStr(SQLiteConnStrOptions{
|
||||
FilePath: opts.SQLitePath,
|
||||
JournalMode: opts.SQLiteJournalMode,
|
||||
BusyTimeout: opts.SQLiteBusyTimeout,
|
||||
})
|
||||
}
|
||||
return "", "", fmt.Errorf("unknown database type: %s", opts.Type)
|
||||
}
|
||||
|
||||
// parsePgSQLHostPort parses given input in various forms defined in
|
||||
// https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING
|
||||
// and returns proper host and port number.
|
||||
func parsePgSQLHostPort(info string) (host, port string) {
|
||||
if h, p, err := net.SplitHostPort(info); err == nil {
|
||||
host, port = h, p
|
||||
} else {
|
||||
// treat the "info" as "host", if it's an IPv6 address, remove the wrapper
|
||||
host = info
|
||||
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
||||
host = host[1 : len(host)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// set fallback values
|
||||
if host == "" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
if port == "" {
|
||||
port = "5432"
|
||||
}
|
||||
return host, port
|
||||
}
|
||||
|
||||
func makePgSQLConnStr(dbHost, dbUser, dbPasswd, dbName, dbsslMode string) (connStr string) {
|
||||
dbName, dbParam, _ := strings.Cut(dbName, "?")
|
||||
host, port := parsePgSQLHostPort(dbHost)
|
||||
connURL := url.URL{
|
||||
Scheme: "postgres",
|
||||
User: url.UserPassword(dbUser, dbPasswd),
|
||||
Host: net.JoinHostPort(host, port),
|
||||
Path: dbName,
|
||||
OmitHost: false,
|
||||
RawQuery: dbParam,
|
||||
}
|
||||
query := connURL.Query()
|
||||
if strings.HasPrefix(host, "/") { // looks like a unix socket
|
||||
query.Add("host", host)
|
||||
connURL.Host = ":" + port
|
||||
}
|
||||
query.Set("sslmode", dbsslMode)
|
||||
connURL.RawQuery = query.Encode()
|
||||
return connURL.String()
|
||||
}
|
||||
|
||||
// parseMSSQLHostPort splits the host into host and port
|
||||
func parseMSSQLHostPort(info string) (string, string) {
|
||||
// the default port "0" might be related to MSSQL's dynamic port, maybe it should be double-confirmed in the future
|
||||
host, port := "127.0.0.1", "0"
|
||||
if strings.Contains(info, ":") {
|
||||
host = strings.Split(info, ":")[0]
|
||||
port = strings.Split(info, ":")[1]
|
||||
} else if strings.Contains(info, ",") {
|
||||
host = strings.Split(info, ",")[0]
|
||||
port = strings.TrimSpace(strings.Split(info, ",")[1])
|
||||
} else if len(info) > 0 {
|
||||
host = info
|
||||
}
|
||||
if host == "" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
if port == "" {
|
||||
port = "0"
|
||||
}
|
||||
return host, port
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParsePgSQLHostPort(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
HostPort string
|
||||
Host string
|
||||
Port string
|
||||
}{
|
||||
"host-port": {
|
||||
HostPort: "127.0.0.1:1234",
|
||||
Host: "127.0.0.1",
|
||||
Port: "1234",
|
||||
},
|
||||
"no-port": {
|
||||
HostPort: "127.0.0.1",
|
||||
Host: "127.0.0.1",
|
||||
Port: "5432",
|
||||
},
|
||||
"ipv6-port": {
|
||||
HostPort: "[::1]:1234",
|
||||
Host: "::1",
|
||||
Port: "1234",
|
||||
},
|
||||
"ipv6-no-port": {
|
||||
HostPort: "[::1]",
|
||||
Host: "::1",
|
||||
Port: "5432",
|
||||
},
|
||||
"unix-socket": {
|
||||
HostPort: "/tmp/pg.sock:1234",
|
||||
Host: "/tmp/pg.sock",
|
||||
Port: "1234",
|
||||
},
|
||||
"unix-socket-no-port": {
|
||||
HostPort: "/tmp/pg.sock",
|
||||
Host: "/tmp/pg.sock",
|
||||
Port: "5432",
|
||||
},
|
||||
}
|
||||
for k, test := range tests {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
t.Log(test.HostPort)
|
||||
host, port := parsePgSQLHostPort(test.HostPort)
|
||||
assert.Equal(t, test.Host, host)
|
||||
assert.Equal(t, test.Port, port)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakePgSQLConnStr(t *testing.T) {
|
||||
tests := []struct {
|
||||
Host string
|
||||
User string
|
||||
Passwd string
|
||||
Name string
|
||||
SSLMode string
|
||||
Output string
|
||||
}{
|
||||
{
|
||||
Host: "", // empty means default
|
||||
Output: "postgres://:@127.0.0.1:5432?sslmode=",
|
||||
},
|
||||
{
|
||||
Host: "/tmp/pg.sock",
|
||||
User: "testuser",
|
||||
Passwd: "space space !#$%^^%^```-=?=",
|
||||
Name: "gitea",
|
||||
SSLMode: "false",
|
||||
Output: "postgres://testuser:space%20space%20%21%23$%25%5E%5E%25%5E%60%60%60-=%3F=@:5432/gitea?host=%2Ftmp%2Fpg.sock&sslmode=false",
|
||||
},
|
||||
{
|
||||
Host: "/tmp/pg.sock:6432",
|
||||
User: "testuser",
|
||||
Passwd: "pass",
|
||||
Name: "gitea",
|
||||
SSLMode: "false",
|
||||
Output: "postgres://testuser:pass@:6432/gitea?host=%2Ftmp%2Fpg.sock&sslmode=false",
|
||||
},
|
||||
{
|
||||
Host: "localhost",
|
||||
User: "pgsqlusername",
|
||||
Passwd: "I love Gitea!",
|
||||
Name: "gitea",
|
||||
SSLMode: "true",
|
||||
Output: "postgres://pgsqlusername:I%20love%20Gitea%21@localhost:5432/gitea?sslmode=true",
|
||||
},
|
||||
{
|
||||
Host: "localhost:1234",
|
||||
User: "user",
|
||||
Passwd: "pass",
|
||||
Name: "gitea?param=1",
|
||||
Output: "postgres://user:pass@localhost:1234/gitea?param=1&sslmode=",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
connStr := makePgSQLConnStr(test.Host, test.User, test.Passwd, test.Name, test.SSLMode)
|
||||
assert.Equal(t, test.Output, connStr)
|
||||
}
|
||||
}
|
||||
@@ -11,18 +11,21 @@ import (
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type engineContextKeyType struct{}
|
||||
type contextKey struct{ key string }
|
||||
|
||||
var engineContextKey = engineContextKeyType{}
|
||||
var (
|
||||
contextKeyEngine = contextKey{"engine"}
|
||||
ContextKeyTestFixtures = contextKey{"test-fixtures"}
|
||||
)
|
||||
|
||||
func withContextEngine(ctx context.Context, e Engine) context.Context {
|
||||
return context.WithValue(ctx, engineContextKey, e)
|
||||
return context.WithValue(ctx, contextKeyEngine, e)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -68,7 +71,7 @@ func contextSafetyCheck(e Engine) {
|
||||
|
||||
// GetEngine gets an existing db Engine/Statement or creates a new Session
|
||||
func GetEngine(ctx context.Context) Engine {
|
||||
if engine, ok := ctx.Value(engineContextKey).(Engine); ok {
|
||||
if engine, ok := ctx.Value(contextKeyEngine).(Engine); ok {
|
||||
// if reusing the existing session, need to do "contextSafetyCheck" because the Iterate creates a "autoResetStatement=false" session
|
||||
contextSafetyCheck(engine)
|
||||
return engine
|
||||
@@ -309,7 +312,7 @@ func InTransaction(ctx context.Context) bool {
|
||||
}
|
||||
|
||||
func getTransactionSession(ctx context.Context) *xorm.Session {
|
||||
e, _ := ctx.Value(engineContextKey).(Engine)
|
||||
e, _ := ctx.Value(contextKeyEngine).(Engine)
|
||||
if sess, ok := e.(*xorm.Session); ok && sess.IsInTx() {
|
||||
return sess
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ package db
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/convert"
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"xorm.io/xorm/dialects"
|
||||
)
|
||||
|
||||
type postgresSchemaDriver struct{}
|
||||
|
||||
var registerPostgresSchemaDriver = sync.OnceFunc(func() {
|
||||
sql.Register(sqlDriverPostgresSchema, &postgresSchemaDriver{})
|
||||
dialects.RegisterDriver(sqlDriverPostgresSchema, dialects.QueryDriver("postgres"))
|
||||
})
|
||||
|
||||
// Open opens the postgres connection in the default manner with default schema support.
|
||||
// It immediately runs "set_config" to set the search_path appropriately.
|
||||
func (*postgresSchemaDriver) Open(connStr string) (driver.Conn, error) {
|
||||
conn, err := pq.Driver{}.Open(connStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
connExec, ok := conn.(driver.ExecerContext)
|
||||
if !ok {
|
||||
return nil, errors.New("postgres driver does not implement ExecerContext interface")
|
||||
}
|
||||
_, err = connExec.ExecContext(context.Background(), `SELECT set_config(
|
||||
'search_path',
|
||||
$1 || ',' || current_setting('search_path'),
|
||||
false)`,
|
||||
[]driver.NamedValue{{Ordinal: 1, Value: setting.Database.Schema}},
|
||||
)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build sqlite_mattn && sqlite_unlock_notify
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerSQLiteConnStrMaker(makeSQLiteConnStrMattnCGO)
|
||||
}
|
||||
|
||||
func makeSQLiteConnStrMattnCGO(opts SQLiteConnStrOptions) (string, string, error) {
|
||||
var params []string
|
||||
params = append(params, "cache=shared")
|
||||
params = append(params, "mode=rwc")
|
||||
params = append(params, "_busy_timeout="+strconv.Itoa(opts.BusyTimeout))
|
||||
params = append(params, "_txlock=immediate")
|
||||
if opts.JournalMode != "" {
|
||||
params = append(params, "_journal_mode="+opts.JournalMode)
|
||||
}
|
||||
connStr := fmt.Sprintf("file:%s?%s", opts.FilePath, strings.Join(params, "&"))
|
||||
return sqlDriverSQLite3, connStr, nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !sqlite_mattn
|
||||
|
||||
// modernc driver is chosen as the default one (compared to mattn, ncruces)
|
||||
// * mattn was used as default, but it requires CGO
|
||||
// * the CI times are almost the same for these three (race detector must be disabled)
|
||||
// * modernc increases the binary size about 2MB, ncruces increases about 7MB
|
||||
// * compiling time: modernc is slightly slower than mattn, ncruces is the slowest
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// this driver contains huge amount of Golang code, so it is much slower when "-race" check is enabled.
|
||||
registerSQLiteConnStrMaker(makeSQLiteConnStrModerncCCGO)
|
||||
sql.Register(sqlDriverSQLite3, &sqlite.Driver{})
|
||||
}
|
||||
|
||||
func makeSQLiteConnStrModerncCCGO(opts SQLiteConnStrOptions) (string, string, error) {
|
||||
var params []string
|
||||
// TODO: there is a changed behavior from mattn driver:
|
||||
// * mattn driver can wait for pretty long time for concurrent accesses (not limited by the busy timeout)
|
||||
// * but other drivers will report something like "database is locked (5) (SQLITE_BUSY)" if the timeout is reached
|
||||
// Maybe we need to relax the busy timeout to a reasonable long time in the future
|
||||
params = append(params, fmt.Sprintf("_pragma=busy_timeout(%d)", opts.BusyTimeout))
|
||||
params = append(params, "_txlock=immediate")
|
||||
if opts.JournalMode != "" {
|
||||
params = append(params, fmt.Sprintf("_pragma=journal_mode(%s)", opts.JournalMode))
|
||||
}
|
||||
connStr := fmt.Sprintf("file:%s?%s", opts.FilePath, strings.Join(params, "&"))
|
||||
return sqlDriverSQLite3, connStr, nil
|
||||
}
|
||||
@@ -11,11 +11,15 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"xorm.io/xorm"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql" // Needed for the MySQL driver
|
||||
_ "github.com/lib/pq" // Needed for the Postgresql driver
|
||||
_ "github.com/microsoft/go-mssqldb" // Needed for the MSSQL driver
|
||||
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/core"
|
||||
"xorm.io/xorm/dialects"
|
||||
"xorm.io/xorm/names"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -24,15 +28,15 @@ var (
|
||||
registeredInitFuncs []func() error
|
||||
)
|
||||
|
||||
// Engine represents a xorm engine or session.
|
||||
type Engine interface {
|
||||
Table(tableNameOrBean any) *xorm.Session
|
||||
// SQLSession represents a common interface for engine and session to execute SQLs
|
||||
type SQLSession interface {
|
||||
Count(...any) (int64, error)
|
||||
Decr(column string, arg ...any) *xorm.Session
|
||||
Delete(...any) (int64, error)
|
||||
Truncate(...any) (int64, error)
|
||||
Exec(...any) (sql.Result, error)
|
||||
Find(any, ...any) error
|
||||
FindAndCount(any, ...any) (int64, error)
|
||||
Get(beans ...any) (bool, error)
|
||||
ID(any) *xorm.Session
|
||||
In(string, ...any) *xorm.Session
|
||||
@@ -47,7 +51,6 @@ type Engine interface {
|
||||
Limit(limit int, start ...int) *xorm.Session
|
||||
NoAutoTime() *xorm.Session
|
||||
SumInt(bean any, columnName string) (res int64, err error)
|
||||
Sync(...any) error
|
||||
Select(string) *xorm.Session
|
||||
SetExpr(string, any) *xorm.Session
|
||||
NotIn(string, ...any) *xorm.Session
|
||||
@@ -56,14 +59,53 @@ type Engine interface {
|
||||
Distinct(...string) *xorm.Session
|
||||
Query(...any) ([]map[string][]byte, error)
|
||||
Cols(...string) *xorm.Session
|
||||
Table(tableNameOrBean any) *xorm.Session
|
||||
Context(ctx context.Context) *xorm.Session
|
||||
Ping() error
|
||||
QueryInterface(sqlOrArgs ...any) ([]map[string]any, error)
|
||||
IsTableExist(tableNameOrBean any) (bool, error)
|
||||
}
|
||||
|
||||
// Engine represents a xorm engine
|
||||
type Engine interface {
|
||||
SQLSession
|
||||
Sync(...any) error
|
||||
Ping() error
|
||||
}
|
||||
|
||||
// Session represents a xorm session interface
|
||||
type Session interface {
|
||||
Engine
|
||||
And(query any, args ...any) *xorm.Session
|
||||
Begin() error
|
||||
Close() error
|
||||
Commit() error
|
||||
IsInTx() bool
|
||||
Rollback() error
|
||||
Engine() *xorm.Engine
|
||||
}
|
||||
|
||||
// EngineMigration is a xorm engine interface used for migrations.
|
||||
// It extends Engine with additional methods that are only available on the engine (not on the session)
|
||||
// and are needed by the migration packages.
|
||||
type EngineMigration interface {
|
||||
Engine
|
||||
Close() error
|
||||
DB() *core.DB
|
||||
DBMetas() ([]*schemas.Table, error)
|
||||
Dialect() dialects.Dialect
|
||||
DropTables(beans ...any) error
|
||||
NewSession() *xorm.Session
|
||||
SetMapper(mapper names.Mapper)
|
||||
SyncWithOptions(opts xorm.SyncOptions, beans ...any) (*xorm.SyncResult, error)
|
||||
TableInfo(bean any) (*schemas.Table, error)
|
||||
TableName(bean any, includeSchema ...bool) string
|
||||
}
|
||||
|
||||
var (
|
||||
_ Engine = (*xorm.Engine)(nil)
|
||||
_ Engine = (*xorm.Session)(nil)
|
||||
_ Engine = (*xorm.Engine)(nil)
|
||||
_ Engine = (*xorm.Session)(nil)
|
||||
_ Session = (*xorm.Session)(nil)
|
||||
_ EngineMigration = (*xorm.Engine)(nil)
|
||||
)
|
||||
|
||||
// RegisterModel registers model, if initFuncs provided, it will be invoked after data model sync
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
|
||||
package db
|
||||
|
||||
import "xorm.io/xorm/schemas"
|
||||
import (
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
// DumpDatabase dumps all data from database according the special database SQL syntax to file system.
|
||||
func DumpDatabase(filePath, dbType string) error {
|
||||
func DumpDatabase(filePath string, dbType setting.DatabaseType) error {
|
||||
var tbs []*schemas.Table
|
||||
for _, t := range registeredModels {
|
||||
t, err := xormEngine.TableInfo(t)
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/gtprof"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/gtprof"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"xorm.io/xorm/contexts"
|
||||
)
|
||||
@@ -22,11 +22,17 @@ type EngineHook struct {
|
||||
var _ contexts.Hook = (*EngineHook)(nil)
|
||||
|
||||
func (*EngineHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
if c.Ctx.Value(ContextKeyTestFixtures) != nil {
|
||||
return c.Ctx, nil
|
||||
}
|
||||
ctx, _ := gtprof.GetTracer().Start(c.Ctx, gtprof.TraceSpanDatabase)
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (h *EngineHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
if c.Ctx.Value(ContextKeyTestFixtures) != nil {
|
||||
return nil
|
||||
}
|
||||
span := gtprof.GetContextSpan(c.Ctx)
|
||||
if span != nil {
|
||||
// Do not record SQL parameters here:
|
||||
|
||||
@@ -6,10 +6,9 @@ package db
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/names"
|
||||
@@ -24,31 +23,23 @@ func init() {
|
||||
|
||||
// newXORMEngine returns a new XORM engine from the configuration
|
||||
func newXORMEngine() (*xorm.Engine, error) {
|
||||
connStr, err := setting.DBConnStr()
|
||||
connOpts := GlobalConnOptions()
|
||||
driver, connStr, err := ConnStr(connOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var engine *xorm.Engine
|
||||
|
||||
if setting.Database.Type.IsPostgreSQL() && len(setting.Database.Schema) > 0 {
|
||||
// OK whilst we sort out our schema issues - create a schema aware postgres
|
||||
registerPostgresSchemaDriver()
|
||||
engine, err = xorm.NewEngine("postgresschema", connStr)
|
||||
} else {
|
||||
engine, err = xorm.NewEngine(setting.Database.Type.String(), connStr)
|
||||
}
|
||||
|
||||
engine, err := xorm.NewEngine(driver, connStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch setting.Database.Type {
|
||||
case "mysql":
|
||||
switch {
|
||||
case connOpts.Type.IsMySQL():
|
||||
engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"})
|
||||
case "mssql":
|
||||
case connOpts.Type.IsMSSQL():
|
||||
engine.Dialect().SetParams(map[string]string{"DEFAULT_VARCHAR": "nvarchar"})
|
||||
}
|
||||
engine.SetSchema(setting.Database.Schema)
|
||||
engine.SetSchema(connOpts.Schema)
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
@@ -56,10 +47,7 @@ func newXORMEngine() (*xorm.Engine, error) {
|
||||
func InitEngine(ctx context.Context) error {
|
||||
xe, err := newXORMEngine()
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "SQLite3 support") {
|
||||
return fmt.Errorf("sqlite3 requires: -tags sqlite,sqlite_unlock_notify\n%w", err)
|
||||
}
|
||||
return fmt.Errorf("failed to connect to database: %w", err)
|
||||
return fmt.Errorf("failed to init database engine: %w", err)
|
||||
}
|
||||
|
||||
xe.SetMapper(names.GonicMapper{})
|
||||
@@ -104,7 +92,7 @@ func UnsetDefaultEngine() {
|
||||
// When called from the "doctor" command, the migration function is a version check
|
||||
// that prevents the doctor from fixing anything in the database if the migration level
|
||||
// is different from the expected value.
|
||||
func InitEngineWithMigration(ctx context.Context, migrateFunc func(context.Context, *xorm.Engine) error) (err error) {
|
||||
func InitEngineWithMigration(ctx context.Context, migrateFunc func(context.Context, EngineMigration) error) (err error) {
|
||||
if err = InitEngine(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/models/db"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
_ "code.gitea.io/gitea/cmd" // for TestPrimaryKeys
|
||||
_ "gitea.dev/cmd" // for TestPrimaryKeys
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -30,7 +30,7 @@ func TestDumpDatabase(t *testing.T) {
|
||||
assert.NoError(t, db.GetEngine(t.Context()).Sync(new(Version)))
|
||||
|
||||
for _, dbType := range setting.SupportedDatabaseTypes {
|
||||
assert.NoError(t, db.DumpDatabase(filepath.Join(dir, dbType+".sql"), dbType))
|
||||
assert.NoError(t, db.DumpDatabase(filepath.Join(dir, dbType+".sql"), setting.DatabaseType(dbType)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func TestPrimaryKeys(t *testing.T) {
|
||||
// https://github.com/go-gitea/gitea/issues/21086
|
||||
// https://github.com/go-gitea/gitea/issues/16802
|
||||
// To avoid creating tables without primary key again, this test will check them.
|
||||
// Import "code.gitea.io/gitea/cmd" to make sure each db.RegisterModel in init functions has been called.
|
||||
// Import "gitea.dev/cmd" to make sure each db.RegisterModel in init functions has been called.
|
||||
|
||||
beans, err := db.NamesToBean()
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -6,7 +6,7 @@ package db
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
// ErrCancelled represents an error due to context cancellation
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
// ResourceIndex represents a resource index which could be used as issue/release and others
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -6,8 +6,8 @@ package install
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
// CheckDatabaseConnection checks the database connection
|
||||
|
||||
@@ -6,7 +6,7 @@ package db
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -6,10 +6,9 @@ package db
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -25,10 +24,9 @@ type Paginator interface {
|
||||
}
|
||||
|
||||
// SetSessionPagination sets pagination for a database session
|
||||
func SetSessionPagination(sess Engine, p Paginator) *xorm.Session {
|
||||
func SetSessionPagination(sess Engine, p Paginator) {
|
||||
skip, take := p.GetSkipTake()
|
||||
|
||||
return sess.Limit(take, skip)
|
||||
sess.Limit(take, skip)
|
||||
}
|
||||
|
||||
// ListOptions options to paginate results
|
||||
|
||||
@@ -6,9 +6,9 @@ package db_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/builder"
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"gitea.dev/modules/log"
|
||||
|
||||
xormlog "xorm.io/xorm/log"
|
||||
)
|
||||
|
||||
@@ -6,10 +6,10 @@ package db_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"gitea.dev/models/unittest"
|
||||
|
||||
_ "code.gitea.io/gitea/models"
|
||||
_ "code.gitea.io/gitea/models/repo"
|
||||
_ "gitea.dev/models"
|
||||
_ "gitea.dev/models/repo"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
// ErrNameReserved represents a "reserved name" error.
|
||||
|
||||
@@ -6,7 +6,7 @@ package paginator
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"gitea.dev/models/unittest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
@@ -6,8 +6,8 @@ package paginator
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
// CountBadSequences looks for broken sequences from recreate-table mistakes
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"xorm.io/xorm/dialects"
|
||||
)
|
||||
|
||||
var registerOnce sync.Once
|
||||
|
||||
func registerPostgresSchemaDriver() {
|
||||
registerOnce.Do(func() {
|
||||
sql.Register("postgresschema", &postgresSchemaDriver{})
|
||||
dialects.RegisterDriver("postgresschema", dialects.QueryDriver("postgres"))
|
||||
})
|
||||
}
|
||||
|
||||
type postgresSchemaDriver struct {
|
||||
pq.Driver
|
||||
}
|
||||
|
||||
// Open opens a new connection to the database. name is a connection string.
|
||||
// This function opens the postgres connection in the default manner but immediately
|
||||
// runs set_config to set the search_path appropriately
|
||||
func (d *postgresSchemaDriver) Open(name string) (driver.Conn, error) {
|
||||
conn, err := d.Driver.Open(name)
|
||||
if err != nil {
|
||||
return conn, err
|
||||
}
|
||||
schemaValue, _ := driver.String.ConvertValue(setting.Database.Schema)
|
||||
|
||||
// golangci lint is incorrect here - there is no benefit to using driver.ExecerContext here
|
||||
// and in any case pq does not implement it
|
||||
if execer, ok := conn.(driver.Execer); ok { //nolint:staticcheck // see above
|
||||
_, err := execer.Exec(`SELECT set_config(
|
||||
'search_path',
|
||||
$1 || ',' || current_setting('search_path'),
|
||||
false)`, []driver.Value{schemaValue})
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
stmt, err := conn.Prepare(`SELECT set_config(
|
||||
'search_path',
|
||||
$1 || ',' || current_setting('search_path'),
|
||||
false)`)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
// driver.String.ConvertValue will never return err for string
|
||||
|
||||
// golangci lint is incorrect here - there is no benefit to using stmt.ExecWithContext here
|
||||
_, err = stmt.Exec([]driver.Value{schemaValue}) //nolint:staticcheck // see above
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
Reference in New Issue
Block a user