Skip to content

Commit

Permalink
Describe/list db metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
nineinchnick authored and kenshaw committed Feb 2, 2021
1 parent 5a5a977 commit 9e0e91a
Show file tree
Hide file tree
Showing 13 changed files with 633 additions and 0 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,18 @@ Input/Output
\i FILE execute commands from file
\ir FILE as \i, but relative to location of current script
Informational
\d[S+] [NAME] list tables, views, and sequences or describe table, view, sequence, or index
\da[S+] [PATTERN] list aggregates
\df[S+] [PATTERN] list functions
\di[S+] [PATTERN] list indexes
\dm[S+] [PATTERN] list materialized views
\dn[S+] [PATTERN] list schemas
\ds[S+] [PATTERN] list sequences
\dt[S+] [PATTERN] list tables
\dv[S+] [PATTERN] list views
\l[+] list databases
Formatting
\pset [NAME [VALUE]] set table output option
\a toggle between unaligned and aligned output mode
Expand Down
2 changes: 2 additions & 0 deletions drivers/clickhouse/clickhouse.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

_ "github.com/ClickHouse/clickhouse-go" // DRIVER: clickhouse
"github.com/xo/usql/drivers"
"github.com/xo/usql/drivers/informationschema"
)

func init() {
Expand All @@ -16,5 +17,6 @@ func init() {
RowsAffected: func(sql.Result) (int64, error) {
return 0, nil
},
NewMetadataReader: informationschema.New,
})
}
31 changes: 31 additions & 0 deletions drivers/drivers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import (
"database/sql"
"encoding/json"
"fmt"
"io"
"strings"

"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/lexers"
"github.com/xo/dburl"
"github.com/xo/usql/drivers/metadata"
"github.com/xo/usql/stmt"
"github.com/xo/usql/text"
)
Expand Down Expand Up @@ -82,6 +84,10 @@ type Driver struct {
BatchAsTransaction bool
// BatchQueryPrefixes will be used by BatchQueryPrefixes if defined.
BatchQueryPrefixes map[string]string
// NewMetadataReader returns a db metadata introspector.
NewMetadataReader func(db DB) metadata.Reader
// NewMetadataWriter returns a db metadata printer.
NewMetadataWriter func(db DB, w io.Writer) metadata.Writer
}

// drivers is the map of drivers funcs.
Expand Down Expand Up @@ -419,3 +425,28 @@ func ForceQueryParameters(params []string) func(*dburl.URL) {
func NextResultSet(q *sql.Rows) bool {
return q.NextResultSet()
}

// NewMetadataReader wraps creating a new database introspector for the specified driver.
func NewMetadataReader(u *dburl.URL, db DB, w io.Writer) (metadata.Reader, error) {
d, ok := drivers[u.Driver]
if !ok || d.NewMetadataReader == nil {
return nil, fmt.Errorf(text.NotSupportedByDriver, `describe commands`)
}
return d.NewMetadataReader(db), nil
}

// NewMetadataWriter wraps creating a new database metadata printer for the specified driver.
func NewMetadataWriter(u *dburl.URL, db DB, w io.Writer) (metadata.Writer, error) {
d, ok := drivers[u.Driver]
if !ok {
return nil, fmt.Errorf(text.NotSupportedByDriver, `describe commands`)
}
if d.NewMetadataWriter != nil {
return d.NewMetadataWriter(db, w), nil
}
if d.NewMetadataReader == nil {
return nil, fmt.Errorf(text.NotSupportedByDriver, `describe commands`)
}
newMetadataWriter := metadata.NewDefaultWriter(d.NewMetadataReader(db))
return newMetadataWriter(db, w), nil
}
167 changes: 167 additions & 0 deletions drivers/informationschema/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package informationschema

import (
"fmt"
"strings"

"github.com/xo/usql/drivers"
"github.com/xo/usql/drivers/metadata"
)

type InformationSchema struct {
db drivers.DB
}

var _ metadata.Reader = &InformationSchema{}

func New(db drivers.DB) metadata.Reader {
return &InformationSchema{
db: db,
}
}

func (s InformationSchema) Columns(catalog, schema, table string) (*metadata.ColumnSet, error) {
qstr := `SELECT
table_catalog,
table_schema,
table_name,
column_name,
ordinal_position,
data_type,
COALESCE(column_default, ''),
COALESCE(character_maximum_length, numeric_precision, datetime_precision, interval_precision, 0) AS column_size,
COALESCE(numeric_scale, 0),
COALESCE(numeric_precision_radix, 0),
COALESCE(character_octet_length, 0),
COALESCE(is_nullable, '') AS is_nullable,
COALESCE(is_generated, '') AS is_generated,
COALESCE(is_identity, '') AS is_identity
FROM information_schema.columns`
conds := []string{}
vals := []interface{}{}
if catalog != "" {
vals = append(vals, catalog)
conds = append(conds, fmt.Sprintf("table_catalog = $%d", len(vals)))
}
if schema != "" {
vals = append(vals, schema)
conds = append(conds, fmt.Sprintf("table_schema LIKE $%d", len(vals)))
}
if table != "" {
vals = append(vals, table)
conds = append(conds, fmt.Sprintf("table_name LIKE $%d", len(vals)))
}
if len(conds) != 0 {
qstr += " WHERE " + strings.Join(conds, " AND ")
}
rows, err := s.db.Query(qstr, vals...)
if err != nil {
return nil, err
}
defer rows.Close()

results := []metadata.Column{}
for rows.Next() {
rec := metadata.Column{}
err = rows.Scan(
&rec.Catalog,
&rec.Schema,
&rec.Table,
&rec.Name,
&rec.OrdinalPosition,
&rec.DataType,
&rec.ColumnDefault,
&rec.ColumnSize,
&rec.DecimalDigits,
&rec.NumPrecRadix,
&rec.CharOctetLength,
&rec.IsNullable,
&rec.IsGenerated,
&rec.IsIdentity,
)
if err != nil {
return nil, err
}
results = append(results, rec)
}
if rows.Err() != nil {
return nil, rows.Err()
}
return metadata.NewColumnSet(results), nil
}

func (s InformationSchema) Tables(catalog, schemaPattern, tableNamePattern string, types []string) (*metadata.TableSet, error) {
qstr := `SELECT
table_catalog,
table_schema,
table_name,
table_type
FROM information_schema.tables`
conds := []string{}
vals := []interface{}{}
if catalog != "" {
vals = append(vals, catalog)
conds = append(conds, fmt.Sprintf("table_catalog = $%d", len(vals)))
}
if schemaPattern != "" {
vals = append(vals, schemaPattern)
conds = append(conds, fmt.Sprintf("table_schema LIKE $%d", len(vals)))
}
if tableNamePattern != "" {
vals = append(vals, tableNamePattern)
conds = append(conds, fmt.Sprintf("table_name LIKE $%d", len(vals)))
}
if len(types) != 0 {
pholders := []string{}
for _, t := range types {
vals = append(vals, t)
pholders = append(pholders, fmt.Sprintf("$%d", len(vals)))
}
conds = append(conds, "table_type IN ("+strings.Join(pholders, ", ")+")")
}
if len(conds) != 0 {
qstr += " WHERE " + strings.Join(conds, " AND ")
}
rows, err := s.db.Query(qstr, vals...)
if err != nil {
return nil, err
}
defer rows.Close()

results := []metadata.Table{}
for rows.Next() {
rec := metadata.Table{}
err = rows.Scan(&rec.Catalog, &rec.Schema, &rec.Name, &rec.Type)
if err != nil {
return nil, err
}
results = append(results, rec)
}
if rows.Err() != nil {
return nil, rows.Err()
}
return metadata.NewTableSet(results), nil
}

func (s InformationSchema) Schemas() (*metadata.SchemaSet, error) {
qstr := "SELECT catalog_name, schema_name FROM information_schema.schemata"
rows, err := s.db.Query(qstr)
if err != nil {
return nil, err
}
defer rows.Close()

results := []metadata.Schema{}
for rows.Next() {
rec := metadata.Schema{}
err = rows.Scan(&rec.Schema, &rec.Catalog)
if err != nil {
return nil, err
}
results = append(results, rec)
}
if rows.Err() != nil {
return nil, rows.Err()
}
return metadata.NewSchemaSet(results), nil
}
Loading

0 comments on commit 9e0e91a

Please sign in to comment.