Repository: danvergara/dblab (go)

Issue:
Support MySQL / MariaDB Sockets
Hello,

most databases I have to work with have no password for root user.
They use socket auth instead.

```
 mysql -S /var/run/mysqld/mysqld.sock
 ```
```
  -S, --socket=name   The socket file to use for connection.
 ```

Can that be supported?

Cheers!


Pull request description:
Add mysql socket conn
## Description

This PR adds a new way to connect to MySQL through `Unix Sockets`.  Therefore the `--socket` flag was added.

```sh
# URL option
$ dblab --url "mysql://user:pasword@unix(/path/to/socket/mysql.sock)/dbname?charset=utf8"

# Socket flag
$ dblab --socket /path/to/socket/mysql.sock --user user --db dbname --pass password --ssl disable --port 5432 --driver mysql --limit 50
```

Fixes #125

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [x] This change requires a documentation update

## How Has This Been Tested?

Followed this [guide](https://docs.fedoraproject.org/en-US/quick-docs/installing-mysql-mariadb/) to install MySQL/MariaDB on my local system.

Inspected the `/etc/my.cnf.d/community-mysql-server.cnf` config file to see where the socket file was. 

```
[mysqld]
socket=/var/lib/mysql/mysql.sock
```
Then, I managed to connect to MySQL through that socket file as expected.

![Screenshot from 2023-03-12 11-18-49](https://user-images.githubusercontent.com/12239167/224561174-cf758430-5acf-4d71-9343-cb6d1ffbcfa5.png)


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have checked my code and corrected any misspellings


Patch:
diff --git a/Makefile b/Makefile
index 7d5cea9..f92ec29 100644
--- a/Makefile
+++ b/Makefile
@@ -13,7 +13,7 @@ unit-test:
 .PHONY: int-test
 ## int-test: Runs the integration tests
 int-test:
-	docker compose run --entrypoint=make dblab test
+	docker-compose run --entrypoint=make dblab test
 
 .PHONY: linter
 ## linter: Runs the golangci-lint command
@@ -51,10 +51,20 @@ run: build
 run-mysql: build
 	./dblab --host localhost --user myuser --db mydb --pass 5@klkbN#ABC --ssl enable --port 3306 --driver mysql
 
+.PHONY: run-mysql-socket
+## run-mysql-socket: Runs the application with a connection to mysql through a socket file. In this example the socke file is located in /var/lib/mysql/mysql.sock.
+run-mysql-socket: build
+	./dblab --socket /var/lib/mysql/mysql.sock --user myuser --pass password --db mydb --ssl enable --port 3306 --driver mysql
+
+.PHONY: run-mysql-socket-url
+## run-mysql-socket-url: Runs the application with a connection to mysql through a socket file. In this example the socke file is located in /var/lib/mysql/mysql.sock.
+run-mysql-socket-url: build
+	./dblab --url "mysql://myuser:password@unix(/var/lib/mysql/mysql.sock)/mydb?charset=utf8"
+
 .PHONY: run-sqlite3
 ## run-sqlite3: Runs the application with a connection to sqlite3
 run-sqlite3: build-sqlite3
-	docker compose run --rm dblab-sqlite3
+	docker-compose run --rm dblab-sqlite3
 	./dblab --db db/dblab.db --driver sqlite3
 
 .PHONY: run-url
@@ -75,12 +85,12 @@ run-config: build
 .PHONY: up
 ## up: Runs all the containers listed in the docker-compose.yml file
 up:
-	docker compose up --build -d
+	docker-compose up --build -d
 
 .PHONY: down
 ## down: Shut down all the containers listed in the docker-compose.yml file
 down:
-	docker compose down
+	docker-compose down
 
 .PHONY: form
 ## form: Runs the application with no arguments
diff --git a/README.md b/README.md
index 18a87d9..e8f7163 100644
--- a/README.md
+++ b/README.md
@@ -102,6 +102,7 @@ Flags:
       --pass string     Password for user
       --port string     Server port
       --schema string   Database schema (postgres only)
+      --socket string     Path to a Unix socket file
       --ssl string      SSL mode
   -u, --url string      Database connection string
       --user string     Database user
@@ -135,6 +136,13 @@ $ dblab --host localhost --user myuser --db users --pass password --schema mysch
 $ dblab --url postgres://user:password@host:port/database?sslmode=[mode] --schema myschema
 ```
 
+As a request made in [#125](https://github.com/danvergara/dblab/issues/125), support for MySQL/MariaDB sockets was integrated.
+
+```sh
+$ dblab --url "mysql://user:pasword@unix(/path/to/socket/mysql.sock)/dbname?charset=utf8"
+$ dblab --socket /path/to/socket/mysql.sock --user user --db dbname --pass password --ssl disable --port 5432 --driver mysql --limit 50
+```
+
 ### Configuration
 
 Enter previous flags every time is tedious, so `dblab` provides a couple of flags to help with it: `--config` and `--cfg-name`.
diff --git a/cmd/root.go b/cmd/root.go
index 3ad8b81..ae31ab2 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -24,6 +24,7 @@ var (
 	db      string
 	ssl     string
 	limit   uint
+	socket  string
 )
 
 // NewRootCmd returns the root command.
@@ -53,6 +54,7 @@ func NewRootCmd() *cobra.Command {
 					Schema: schema,
 					SSL:    ssl,
 					Limit:  limit,
+					Socket: socket,
 				}
 
 				if form.IsEmpty(opts) {
@@ -118,4 +120,5 @@ func init() {
 	rootCmd.Flags().StringVarP(&schema, "schema", "", "", "Database schema (postgres only)")
 	rootCmd.Flags().StringVarP(&ssl, "ssl", "", "", "SSL mode")
 	rootCmd.Flags().UintVarP(&limit, "limit", "", 100, "Size of the result set from the table content query (should be greater than zero, otherwise the app will error out)")
+	rootCmd.Flags().StringVarP(&socket, "socket", "", "", "Path to a Unix socket file")
 }
diff --git a/pkg/command/command.go b/pkg/command/command.go
index b41506a..25af048 100644
--- a/pkg/command/command.go
+++ b/pkg/command/command.go
@@ -15,6 +15,7 @@ type Options struct {
 	// PostgreSQL only.
 	Schema string
 	Limit  uint
+	Socket string
 }
 
 // SetDefault returns a Options struct and fills the empty
diff --git a/pkg/connection/connection.go b/pkg/connection/connection.go
index 45e5c01..108c874 100644
--- a/pkg/connection/connection.go
+++ b/pkg/connection/connection.go
@@ -19,15 +19,19 @@ var (
 	// in the system to be used as database username.
 	ErrCantDetectUSer = errors.New("could not detect default username")
 	// ErrInvalidPostgresURLFormat is the error used to notify that the postgres given url is not valid.
-	ErrInvalidPostgresURLFormat = errors.New("invalid URL - Valid format: postgres://user:password@host:port/db?sslmode=mode")
+	ErrInvalidPostgresURLFormat = errors.New("invalid url - valid format: postgres://user:password@host:port/db?sslmode=mode")
 	// ErrInvalidMySQLURLFormat is the error used to notify that the given mysql url is not valid.
-	ErrInvalidMySQLURLFormat = errors.New("invalid URL - valid format: mysql://user:password@tcp(host:port)/db")
+	ErrInvalidMySQLURLFormat = errors.New("invalid url - valid format: mysql://user:password@tcp(host:port)/db")
 	// ErrInvalidURLFormat is used to notify the url is invalid.
 	ErrInvalidURLFormat = errors.New("invalid url")
 	// ErrInvalidDriver is used to notify that the provided driver is not supported.
 	ErrInvalidDriver = errors.New("invalid driver")
 	// ErrInvalidSqlite3Extension is used to notify that the selected file is not a sqlite3 file.
 	ErrInvalidSqlite3Extension = errors.New("invalid sqlite3 file extension")
+	// ErrSocketFileDoNotExist indicates that the given path to the socket files leads to no file.
+	ErrSocketFileDoNotExist = errors.New("socket file does not exist")
+	// ErrInvalidSocketFile indicates that the socket file must end with .sock as suffix.
+	ErrInvalidSocketFile = errors.New("invalid socket file - must end with .sock")
 )
 
 func init() {
@@ -92,6 +96,18 @@ func BuildConnectionFromOpts(opts command.Options) (string, command.Options, err
 
 		return connDB.String(), opts, nil
 	case "mysql":
+		if opts.Socket != "" {
+			if !validSocketFile(opts.Socket) {
+				return "", opts, ErrInvalidSocketFile
+			}
+
+			if !socketFileExists(opts.Socket) {
+				return "", opts, ErrSocketFileDoNotExist
+			}
+
+			return fmt.Sprintf("%s:%s@unix(%s)/%s?charset=utf8", opts.User, opts.Pass, opts.Socket, opts.DBName), opts, nil
+		}
+
 		return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", opts.User, opts.Pass, opts.Host, opts.Port, opts.DBName), opts, nil
 	case "sqlite3":
 		if hasValidSqlite3FileExtension(opts.DBName) {
@@ -224,3 +240,16 @@ func hasValidMySQLPrefix(rawurl string) bool {
 func hasValidSqlite3FileExtension(fileName string) bool {
 	return strings.HasSuffix(fileName, "sqlite") || strings.HasSuffix(fileName, "db") || strings.HasSuffix(fileName, "db3") || strings.HasSuffix(fileName, "sqlite3")
 }
+
+func socketFileExists(socketFile string) bool {
+	info, err := os.Stat(socketFile)
+	if os.IsNotExist(err) {
+		return false
+	}
+
+	return !info.IsDir()
+}
+
+func validSocketFile(socketFile string) bool {
+	return strings.HasSuffix(socketFile, ".sock")
+}


Test patch:
diff --git a/pkg/connection/connection_test.go b/pkg/connection/connection_test.go
index 396a58e..9321b1b 100644
--- a/pkg/connection/connection_test.go
+++ b/pkg/connection/connection_test.go
@@ -2,13 +2,26 @@ package connection
 
 import (
 	"errors"
+	"fmt"
+	"os"
 	"testing"
 
 	"github.com/danvergara/dblab/pkg/command"
 	"github.com/stretchr/testify/assert"
 )
 
+func createTempSocketFile(t *testing.T) *os.File {
+	t.Helper()
+
+	socketFile, err := os.CreateTemp("", "mysqld*.sock")
+	assert.NoError(t, err)
+
+	return socketFile
+}
+
 func TestBuildConnectionFromOptsFromURL(t *testing.T) {
+	socketFile := createTempSocketFile(t)
+
 	type given struct {
 		opts command.Options
 	}
@@ -102,6 +115,17 @@ func TestBuildConnectionFromOptsFromURL(t *testing.T) {
 				uri: "user:password@tcp(your-amazonaws-uri.com:3306)/dbname",
 			},
 		},
+		{
+			name: "valid socket connection",
+			given: given{
+				opts: command.Options{
+					URL: fmt.Sprintf("mysql://user:password@unix(%s)/dbname?charset=utf8", socketFile.Name()),
+				},
+			},
+			want: want{
+				uri: fmt.Sprintf("user:password@unix(%s)/dbname?charset=utf8", socketFile.Name()),
+			},
+		},
 		{
 			name: "error misspelled mysql",
 			given: given{
@@ -163,6 +187,8 @@ func TestBuildConnectionFromOptsFromURL(t *testing.T) {
 }
 
 func TestBuildConnectionFromOptsUserData(t *testing.T) {
+	socketFile := createTempSocketFile(t)
+
 	type given struct {
 		opts command.Options
 	}
@@ -274,6 +300,53 @@ func TestBuildConnectionFromOptsUserData(t *testing.T) {
 				uri: "user:password@tcp(your-amazonaws-uri.com:3306)/db",
 			},
 		},
+		{
+			name: "success - sockets connection",
+			given: given{
+				opts: command.Options{
+					Driver: "mysql",
+					User:   "user",
+					Pass:   "password",
+					DBName: "db",
+					Socket: socketFile.Name(),
+				},
+			},
+			want: want{
+				uri: fmt.Sprintf("user:password@unix(%s)/db?charset=utf8", socketFile.Name()),
+			},
+		},
+		{
+			name: "error - invalid socket file name",
+			given: given{
+				opts: command.Options{
+					Driver: "mysql",
+					User:   "user",
+					Pass:   "password",
+					DBName: "db",
+					Socket: "/path/to/not-wrong-file",
+				},
+			},
+			want: want{
+				hasError: true,
+				err:      ErrInvalidSocketFile,
+			},
+		},
+		{
+			name: "error - socket file do not exist",
+			given: given{
+				opts: command.Options{
+					Driver: "mysql",
+					User:   "user",
+					Pass:   "password",
+					DBName: "db",
+					Socket: "/path/to/not-existing-file.sock",
+				},
+			},
+			want: want{
+				hasError: true,
+				err:      ErrSocketFileDoNotExist,
+			},
+		},
 		// sqlite3.
 		{
 			name: "success - sqlite3 db file extension",
@@ -503,6 +576,17 @@ func TestFormatMySQLURL(t *testing.T) {
 				uri: "user:password@unix(/cloudsql/project-id:region-name:instance-name)/dbname",
 			},
 		},
+		{
+			name: "valid socket connection",
+			given: given{
+				opts: command.Options{
+					URL: "mysql://user:password@unix(/path/to/socket)/dbname?charset=utf8",
+				},
+			},
+			want: want{
+				uri: "user:password@unix(/path/to/socket)/dbname?charset=utf8",
+			},
+		},
 		{
 			name: "error misspelled mysql",
 			given: given{
@@ -594,6 +678,15 @@ func TestParseDSN(t *testing.T) {
 				uri: "mysql://user:password@unix(/cloudsql/project-id:region-name:instance-name)/dbname",
 			},
 		},
+		{
+			name: "valid socket connection",
+			given: given{
+				dsn: "mysql://user:password@unix(/path/to/socket)/dbname?charset=utf8",
+			},
+			want: want{
+				uri: "mysql://user:password@unix(/path/to/socket)/dbname?charset=utf8",
+			},
+		},
 		{
 			name: "error invalid url",
 			given: given{