Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Databases tree view #245

Merged
merged 6 commits into from
Nov 19, 2024
Merged
250 changes: 150 additions & 100 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package client

import (
"fmt"
"net/url"
"strings"

_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
Expand All @@ -26,6 +28,8 @@ type databaseQuerier interface {
TableStructure(tableName string) (string, []interface{}, error)
Constraints(tableName string) (string, []interface{}, error)
Indexes(tableName string) (string, []interface{}, error)
ShowDatabases() (string, []interface{}, error)
ShowTablesPerDB(database string) (string, []interface{}, error)
}

// Client is used to store the pool of db connection.
Expand All @@ -34,7 +38,10 @@ type Client struct {
databaseQuerier databaseQuerier
driver, schema string
paginationManager *pagination.Manager
activeDatabase string
limit uint
showDataCatalog bool
dbs map[string]*sqlx.DB
}

// New return an instance of the client.
Expand All @@ -49,10 +56,11 @@ func New(opts command.Options) (*Client, error) {
return nil, err
}

c := Client{
c := &Client{
db: db,
driver: opts.Driver,
limit: opts.Limit,
dbs: make(map[string]*sqlx.DB),
}

if opts.Schema == "" {
Expand All @@ -77,10 +85,28 @@ func New(opts command.Options) (*Client, error) {
return nil, fmt.Errorf("%s driver not supported", c.driver)
}

if opts.DBName == "" {
switch c.driver {
case drivers.PostgreSQL, drivers.Postgres, drivers.MySQL:
c.showDataCatalog = true
dbs, err := c.ShowDatabases()
if err != nil {
return nil, err
}

for _, d := range dbs {
db, err := getDB(c.driver, conn, d)
if err != nil {
continue
}

c.dbs[d] = db
}
}
}

switch c.driver {
case drivers.Postgres:
fallthrough
case drivers.PostgreSQL:
case drivers.PostgreSQL, drivers.Postgres:
if _, err = db.Exec(fmt.Sprintf("set search_path='%s'", c.schema)); err != nil {
return nil, err
}
Expand All @@ -93,15 +119,49 @@ func New(opts command.Options) (*Client, error) {

c.paginationManager = pm

return &c, nil
return c, nil
}

func (c *Client) SetActiveDatabase(database string) {
c.activeDatabase = database
}

func (c *Client) ActiveDatabase() string {
return c.activeDatabase
}

// DB Return the db attribute.
func (c *Client) DB() *sqlx.DB {
return c.db
}

// Driver returns the driver of the database.
func (c *Client) Driver() string {
return c.driver
}

func (c *Client) ShowDataCatalog() bool {
return c.showDataCatalog
}

// Query returns performs the query and returns the result set and the column names.
func (c *Client) Query(q string, args ...interface{}) ([][]string, []string, error) {
resultSet := [][]string{}
var (
resultSet = [][]string{}
db *sqlx.DB
)

if c.activeDatabase != "" {
switch c.driver {
case drivers.Postgres, drivers.PostgreSQL, drivers.MySQL:
db = c.dbs[c.activeDatabase]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this?

Suggested change
db = c.dbs[c.activeDatabase]
db, ok = c.dbs[c.activeDatabase]
if !ok {
return nil, nil, return nil, fmt.Errorf("connection with %s database not found", c.activeDatabase)
}

I'm afraid about possible panic, when calling db.Query

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good catch there!

I'll add this.

Thanks!

}
} else {
db = c.db
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this?

Suggested change
var (
resultSet = [][]string{}
db *sqlx.DB
)
if c.activeDatabase != "" {
switch c.driver {
case drivers.Postgres, drivers.PostgreSQL, drivers.MySQL:
db = c.dbs[c.activeDatabase]
}
} else {
db = c.db
}
var (
resultSet = [][]string{}
)
db = c.db
if c.activeDatabase != "" {
switch c.driver {
case drivers.Postgres, drivers.PostgreSQL, drivers.MySQL:
db = c.dbs[c.activeDatabase]
}
}

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so nice and elegant. Good catch!


// Runs the query extracting the content of the view calling the Buffer method.
rows, err := c.db.Queryx(q, args...)
rows, err := db.Queryx(q, args...)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -160,20 +220,6 @@ type Metadata struct {

// Metadata returns the most relevant data from a given table.
func (c *Client) Metadata(tableName string) (*Metadata, error) {
count, err := c.tableCount(tableName)
if err != nil {
return nil, err
}

pm, err := pagination.New(c.limit, count, tableName)
if err != nil {
return nil, err
}

c.paginationManager = pm

pages := c.paginationManager.TotalPages()

tcRows, tcColumns, err := c.tableContent(tableName)
if err != nil {
return nil, err
Expand Down Expand Up @@ -211,36 +257,38 @@ func (c *Client) Metadata(tableName string) (*Metadata, error) {
Rows: iRows,
Columns: iColumns,
},
TotalPages: pages,
}

return &m, nil
}

func (c *Client) TotalPages() int {
if c.paginationManager != nil {
return c.paginationManager.TotalPages()
}

return 0
}

// ShowTables list all the tables in the database on the tables panel.
func (c *Client) ShowTables() ([]string, error) {
func (c *Client) ShowTablesPerDB(database string) ([]string, error) {
var (
query string
err error
args []interface{}
db *sqlx.DB
ok bool
)

tables := make([]string, 0)

query, args, err = c.databaseQuerier.ShowTables()
query, args, err = c.databaseQuerier.ShowTablesPerDB(database)
if err != nil {
return nil, err
}

rows, err := c.db.Queryx(query, args...)
switch c.driver {
case drivers.PostgreSQL, drivers.Postgres, drivers.MySQL:
db, ok = c.dbs[database]
if !ok {
return nil, fmt.Errorf("connection with %s database not found", database)
}
default:
db = c.db
}

rows, err := db.Queryx(query, args...)
if err != nil {
return nil, err
}
Expand All @@ -257,69 +305,68 @@ func (c *Client) ShowTables() ([]string, error) {
return tables, nil
}

// NextPage returns the next page of the given table, based off the limit and the offsite.
func (c *Client) NextPage() (*Table, int, error) {
if err := c.paginationManager.NextPage(); err != nil {
return nil, 0, err
}
// ShowTables list all the tables in the database on the tables panel.
func (c *Client) ShowTables() ([]string, error) {
var (
query string
err error
args []interface{}
)

tables := make([]string, 0)

r, col, err := c.tableContent(c.paginationManager.CurrentTable())
query, args, err = c.databaseQuerier.ShowTables()
if err != nil {
return nil, 0, err
return nil, err
}

t := Table{
name: c.paginationManager.CurrentTable(),
Rows: r,
Columns: col,
rows, err := c.db.Queryx(query, args...)
if err != nil {
return nil, err
}

page := c.paginationManager.CurrentPage()

return &t, page, nil
}
for rows.Next() {
var table string
if err := rows.Scan(&table); err != nil {
return nil, err
}

// PreviousPage returns the next page of the given table, based off the limit and the offsite.
func (c *Client) PreviousPage() (*Table, int, error) {
if err := c.paginationManager.PreviousPage(); err != nil {
return nil, 0, err
tables = append(tables, table)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each row.Nect loop must be followed by checking rows.Err()

Suggested change
}
}
if err:= rows.Err(); err != nil {
return nil, err
}


r, col, err := c.tableContent(c.paginationManager.CurrentTable())
if err != nil {
return nil, 0, err
}
return tables, nil
}

t := Table{
name: c.paginationManager.CurrentTable(),
Rows: r,
Columns: col,
}
// ShowDatabases returns a list of the databases the user has access to.
func (c *Client) ShowDatabases() ([]string, error) {
var (
query string
err error
args []interface{}
)

page := c.paginationManager.CurrentPage()
databases := make([]string, 0)

return &t, page, nil
}
query, args, err = c.databaseQuerier.ShowDatabases()
if err != nil {
return nil, err
}

// ResetPagination resets the paginationManager field.
func (c *Client) ResetPagination() error {
pm, err := pagination.New(c.limit, 0, "")
rows, err := c.db.Queryx(query, args...)
if err != nil {
return err
return nil, err
}

c.paginationManager = pm
return nil
}
for rows.Next() {
var database string
if err := rows.Scan(&database); err != nil {
return nil, err
}

// DB Return the db attribute.
func (c *Client) DB() *sqlx.DB {
return c.db
}
databases = append(databases, database)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue about missing rows.Err check


// Driver returns the driver of the database.
func (c *Client) Driver() string {
return c.driver
return databases, nil
}

// TableContent returns all the rows of a table.
Expand Down Expand Up @@ -360,27 +407,6 @@ func (c *Client) tableContent(tableName string) ([][]string, []string, error) {
return c.Query(query)
}

// tableCount returns the count of a given table.
func (c *Client) tableCount(tableName string) (int, error) {
var (
query string
count int
)

switch c.driver {
case drivers.Postgres, drivers.PostgreSQL:
query = fmt.Sprintf("SELECT COUNT(*) FROM %q;", tableName)
default:
query = fmt.Sprintf("SELECT COUNT(*) FROM %s;", tableName)
}

if err := c.db.Get(&count, query); err != nil {
return 0, err
}

return count, nil
}

// tableStructure returns the structure of the table columns.
func (c *Client) tableStructure(tableName string) ([][]string, []string, error) {
var (
Expand Down Expand Up @@ -416,3 +442,27 @@ func (c *Client) indexes(tableName string) ([][]string, []string, error) {

return c.Query(query, args...)
}

func getDB(driver, connString, database string) (*sqlx.DB, error) {
var newConnString string

if driver == drivers.MySQL {
newConnString = strings.Replace(connString, "/", fmt.Sprintf("/%s", database), 1)
} else {
u, err := url.Parse(connString)
if err != nil {
return nil, err
}

u.Path = "/" + database
newConnString = u.String()
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use a switch and return an error when it's none of the supported database types

I'm a bit worried by the fact you will apply the postgres way to any future database type that might be added

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with using a switch here, but the driver validation is done somewhere. If the execution reaches this point, the driver is actually valid. Regarding applying the postgres way, yeah a switch statement might help supporting a new use case.


db, err := sqlx.Open(driver, newConnString)
if err != nil {

panic(err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a panic?

Suggested change
panic(err)
return nil, err

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left it there by accident. I'll remove it,
Thanks!

}

return db, nil
}
Loading