From b772603d78cb10f0501f3d08b01553bb33914b6e Mon Sep 17 00:00:00 2001
From: Joe Chen <jc@unknwon.io>
Date: Sun, 12 Jun 2022 14:15:01 +0800
Subject: migrations: add tests and remove XORM (#7050)

---
 internal/dbutil/dsn.go      | 116 ++++++++++++++++++++++++++++++++++
 internal/dbutil/dsn_test.go | 149 ++++++++++++++++++++++++++++++++++++++++++++
 internal/dbutil/logger.go   |   4 ++
 3 files changed, 269 insertions(+)
 create mode 100644 internal/dbutil/dsn.go
 create mode 100644 internal/dbutil/dsn_test.go

(limited to 'internal/dbutil')

diff --git a/internal/dbutil/dsn.go b/internal/dbutil/dsn.go
new file mode 100644
index 00000000..30279988
--- /dev/null
+++ b/internal/dbutil/dsn.go
@@ -0,0 +1,116 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package dbutil
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/pkg/errors"
+	"gorm.io/driver/mysql"
+	"gorm.io/driver/postgres"
+	"gorm.io/driver/sqlite"
+	"gorm.io/driver/sqlserver"
+	"gorm.io/gorm"
+
+	"gogs.io/gogs/internal/conf"
+)
+
+// ParsePostgreSQLHostPort 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 ParsePostgreSQLHostPort(info string) (host, port string) {
+	host, port = "127.0.0.1", "5432"
+	if strings.Contains(info, ":") && !strings.HasSuffix(info, "]") {
+		idx := strings.LastIndex(info, ":")
+		host = info[:idx]
+		port = info[idx+1:]
+	} else if len(info) > 0 {
+		host = info
+	}
+	return host, port
+}
+
+// ParseMSSQLHostPort parses given input in various forms for MSSQL and returns
+// proper host and port number.
+func ParseMSSQLHostPort(info string) (host, port string) {
+	host, port = "127.0.0.1", "1433"
+	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
+	}
+	return host, port
+}
+
+// NewDSN takes given database options and returns parsed DSN.
+func NewDSN(opts conf.DatabaseOpts) (dsn string, err error) {
+	// In case the database name contains "?" with some parameters
+	concate := "?"
+	if strings.Contains(opts.Name, concate) {
+		concate = "&"
+	}
+
+	switch opts.Type {
+	case "mysql":
+		if opts.Host[0] == '/' { // Looks like a unix socket
+			dsn = fmt.Sprintf("%s:%s@unix(%s)/%s%scharset=utf8mb4&parseTime=true",
+				opts.User, opts.Password, opts.Host, opts.Name, concate)
+		} else {
+			dsn = fmt.Sprintf("%s:%s@tcp(%s)/%s%scharset=utf8mb4&parseTime=true",
+				opts.User, opts.Password, opts.Host, opts.Name, concate)
+		}
+
+	case "postgres":
+		host, port := ParsePostgreSQLHostPort(opts.Host)
+		dsn = fmt.Sprintf("user='%s' password='%s' host='%s' port='%s' dbname='%s' sslmode='%s' search_path='%s' application_name='gogs'",
+			opts.User, opts.Password, host, port, opts.Name, opts.SSLMode, opts.Schema)
+
+	case "mssql":
+		host, port := ParseMSSQLHostPort(opts.Host)
+		dsn = fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;",
+			host, port, opts.Name, opts.User, opts.Password)
+
+	case "sqlite3", "sqlite":
+		dsn = "file:" + opts.Path + "?cache=shared&mode=rwc"
+
+	default:
+		return "", errors.Errorf("unrecognized dialect: %s", opts.Type)
+	}
+
+	return dsn, nil
+}
+
+// OpenDB opens a new database connection encapsulated as gorm.DB using given
+// database options and GORM config.
+func OpenDB(opts conf.DatabaseOpts, cfg *gorm.Config) (*gorm.DB, error) {
+	dsn, err := NewDSN(opts)
+	if err != nil {
+		return nil, errors.Wrap(err, "parse DSN")
+	}
+
+	var dialector gorm.Dialector
+	switch opts.Type {
+	case "mysql":
+		dialector = mysql.Open(dsn)
+	case "postgres":
+		dialector = postgres.Open(dsn)
+	case "mssql":
+		dialector = sqlserver.Open(dsn)
+	case "sqlite3":
+		dialector = sqlite.Open(dsn)
+	case "sqlite":
+		dialector = sqlite.Open(dsn)
+		dialector.(*sqlite.Dialector).DriverName = "sqlite"
+	default:
+		panic("unreachable")
+	}
+
+	return gorm.Open(dialector, cfg)
+}
diff --git a/internal/dbutil/dsn_test.go b/internal/dbutil/dsn_test.go
new file mode 100644
index 00000000..0dd92901
--- /dev/null
+++ b/internal/dbutil/dsn_test.go
@@ -0,0 +1,149 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package dbutil
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"gogs.io/gogs/internal/conf"
+)
+
+func TestParsePostgreSQLHostPort(t *testing.T) {
+	tests := []struct {
+		info    string
+		expHost string
+		expPort string
+	}{
+		{info: "127.0.0.1:1234", expHost: "127.0.0.1", expPort: "1234"},
+		{info: "127.0.0.1", expHost: "127.0.0.1", expPort: "5432"},
+		{info: "[::1]:1234", expHost: "[::1]", expPort: "1234"},
+		{info: "[::1]", expHost: "[::1]", expPort: "5432"},
+		{info: "/tmp/pg.sock:1234", expHost: "/tmp/pg.sock", expPort: "1234"},
+		{info: "/tmp/pg.sock", expHost: "/tmp/pg.sock", expPort: "5432"},
+	}
+	for _, test := range tests {
+		t.Run("", func(t *testing.T) {
+			host, port := ParsePostgreSQLHostPort(test.info)
+			assert.Equal(t, test.expHost, host)
+			assert.Equal(t, test.expPort, port)
+		})
+	}
+}
+
+func TestParseMSSQLHostPort(t *testing.T) {
+	tests := []struct {
+		info    string
+		expHost string
+		expPort string
+	}{
+		{info: "127.0.0.1:1234", expHost: "127.0.0.1", expPort: "1234"},
+		{info: "127.0.0.1,1234", expHost: "127.0.0.1", expPort: "1234"},
+		{info: "127.0.0.1", expHost: "127.0.0.1", expPort: "1433"},
+	}
+	for _, test := range tests {
+		t.Run("", func(t *testing.T) {
+			host, port := ParseMSSQLHostPort(test.info)
+			assert.Equal(t, test.expHost, host)
+			assert.Equal(t, test.expPort, port)
+		})
+	}
+}
+
+func TestNewDSN(t *testing.T) {
+	t.Run("bad dialect", func(t *testing.T) {
+		_, err := NewDSN(conf.DatabaseOpts{
+			Type: "bad_dialect",
+		})
+		assert.Equal(t, "unrecognized dialect: bad_dialect", fmt.Sprintf("%v", err))
+	})
+
+	tests := []struct {
+		name    string
+		opts    conf.DatabaseOpts
+		wantDSN string
+	}{
+		{
+			name: "mysql: unix",
+			opts: conf.DatabaseOpts{
+				Type:     "mysql",
+				Host:     "/tmp/mysql.sock",
+				Name:     "gogs",
+				User:     "gogs",
+				Password: "pa$$word",
+			},
+			wantDSN: "gogs:pa$$word@unix(/tmp/mysql.sock)/gogs?charset=utf8mb4&parseTime=true",
+		},
+		{
+			name: "mysql: tcp",
+			opts: conf.DatabaseOpts{
+				Type:     "mysql",
+				Host:     "localhost:3306",
+				Name:     "gogs",
+				User:     "gogs",
+				Password: "pa$$word",
+			},
+			wantDSN: "gogs:pa$$word@tcp(localhost:3306)/gogs?charset=utf8mb4&parseTime=true",
+		},
+
+		{
+			name: "postgres: unix",
+			opts: conf.DatabaseOpts{
+				Type:     "postgres",
+				Host:     "/tmp/pg.sock",
+				Name:     "gogs",
+				Schema:   "test",
+				User:     "gogs@local",
+				Password: "pa$$word",
+				SSLMode:  "disable",
+			},
+			wantDSN: "user='gogs@local' password='pa$$word' host='/tmp/pg.sock' port='5432' dbname='gogs' sslmode='disable' search_path='test' application_name='gogs'",
+		},
+		{
+			name: "postgres: tcp",
+			opts: conf.DatabaseOpts{
+				Type:     "postgres",
+				Host:     "127.0.0.1",
+				Name:     "gogs",
+				Schema:   "test",
+				User:     "gogs@local",
+				Password: "pa$$word",
+				SSLMode:  "disable",
+			},
+			wantDSN: "user='gogs@local' password='pa$$word' host='127.0.0.1' port='5432' dbname='gogs' sslmode='disable' search_path='test' application_name='gogs'",
+		},
+
+		{
+			name: "mssql",
+			opts: conf.DatabaseOpts{
+				Type:     "mssql",
+				Host:     "127.0.0.1",
+				Name:     "gogs",
+				User:     "gogs@local",
+				Password: "pa$$word",
+			},
+			wantDSN: "server=127.0.0.1; port=1433; database=gogs; user id=gogs@local; password=pa$$word;",
+		},
+
+		{
+			name: "sqlite3",
+			opts: conf.DatabaseOpts{
+				Type: "sqlite3",
+				Path: "/tmp/gogs.db",
+			},
+			wantDSN: "file:/tmp/gogs.db?cache=shared&mode=rwc",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			dsn, err := NewDSN(test.opts)
+			require.NoError(t, err)
+			assert.Equal(t, test.wantDSN, dsn)
+		})
+	}
+}
diff --git a/internal/dbutil/logger.go b/internal/dbutil/logger.go
index 66426ae7..c189949e 100644
--- a/internal/dbutil/logger.go
+++ b/internal/dbutil/logger.go
@@ -1,3 +1,7 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
 package dbutil
 
 import (
-- 
cgit v1.2.3