Skip to content

Commit

Permalink
sq inspect now shows catalog (when outputting in JSON and YAML format) (
Browse files Browse the repository at this point in the history
  • Loading branch information
neilotoole authored Nov 19, 2023
1 parent 30b7d64 commit d7fc315
Show file tree
Hide file tree
Showing 10 changed files with 106 additions and 58 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

Breaking changes are annotated with ☢️, and alpha/beta features with 🐥.

## [v0.43.1] - 2023-11-19

### Added

- Related to [#270], the output of `sq inspect` now includes the
source's catalog (in JSON and YAML output formats).

### Fixed

- MySQL driver didn't populate all expected values for `sq inspect --overview`.

### Changed

- ☢️ Removed unused `--exec` and `--query` flags from `sq sql` command.

## [v0.43.0] - 2023-11-18

### Added
Expand Down Expand Up @@ -871,3 +886,4 @@ make working with lots of sources much easier.
[v0.42.0]: https://github.com/neilotoole/sq/compare/v0.41.1...v0.42.0
[v0.42.1]: https://github.com/neilotoole/sq/compare/v0.42.0...v0.42.1
[v0.43.0]: https://github.com/neilotoole/sq/compare/v0.42.1...v0.43.0
[v0.43.1]: https://github.com/neilotoole/sq/compare/v0.43.0...v0.43.1
7 changes: 7 additions & 0 deletions cli/cmd_inspect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ func TestCmdInspect_json_yaml(t *testing.T) {
require.Nil(t, srcMeta.Tables)
require.Zero(t, srcMeta.TableCount)
require.Zero(t, srcMeta.ViewCount)
require.NotEmpty(t, srcMeta.Name)
require.NotEmpty(t, srcMeta.Schema)
require.NotEmpty(t, srcMeta.FQName)
require.NotEmpty(t, srcMeta.DBDriver)
require.NotEmpty(t, srcMeta.DBProduct)
require.NotEmpty(t, srcMeta.DBVersion)
require.NotZero(t, srcMeta.Size)
})

t.Run("inspect_dbprops", func(t *testing.T) {
Expand Down
35 changes: 11 additions & 24 deletions cli/cmd_sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,7 @@ func newSQLCmd() *cobra.Command {
Short: "Execute DB-native SQL query or statement",
Long: `Execute a SQL query or statement against the active source using the
source's SQL dialect. Use flag --src=@HANDLE to specify an alternative
source.
If flag --query is set, sq will run the input as a query
(SELECT) and return the query rows. If flag --exec is set,
sq will execute the input and return the result. If neither
flag is set, sq attempts to determine the appropriate mode.`,
source.`,
RunE: execSQL,
Example: ` # Select from active source
$ sq sql 'SELECT * FROM actor'
Expand All @@ -45,26 +40,19 @@ flag is set, sq attempts to determine the appropriate mode.`,
$ sq sql --src=@sakila_pg12 'SELECT * FROM actor'
# Drop table @sakila_pg12.actor
$ sq sql --exec --src=@sakila_pg12 'DROP TABLE actor'
$ sq sql --src=@sakila_pg12 'DROP TABLE actor'
# Select from active source and write results to @sakila_ms17.actor
$ sq sql 'SELECT * FROM actor' --insert=@sakila_ms17.actor`,
}

addQueryCmdFlags(cmd)

// TODO: These flags aren't actually implemented yet.
// This entire --exec mechanism needs to be revisited.
// User explicitly wants to execute the SQL using sql.DB.Query
// cmd.Flags().Bool(flag.SQLQuery, false, flag.SQLQueryUsage)
// User explicitly wants to execute the SQL using sql.DB.Exec
// cmd.Flags().Bool(flag.SQLExec, false, flag.SQLExecUsage)

return cmd
}

func execSQL(cmd *cobra.Command, args []string) error {
ru := run.FromContext(cmd.Context())
ctx := cmd.Context()
ru := run.FromContext(ctx)
switch len(args) {
default:
return errz.New("a single query string is required")
Expand All @@ -76,7 +64,7 @@ func execSQL(cmd *cobra.Command, args []string) error {
}
}

err := determineSources(cmd.Context(), ru, true)
err := determineSources(ctx, ru, true)
if err != nil {
return err
}
Expand All @@ -93,7 +81,7 @@ func execSQL(cmd *cobra.Command, args []string) error {
if !cmdFlagChanged(cmd, flag.Insert) {
// The user didn't specify the --insert=@src.tbl flag,
// so we just want to print the records.
return execSQLPrint(cmd.Context(), ru, activeSrc)
return execSQLPrint(ctx, ru, activeSrc)
}

// Instead of printing the records, they will be
Expand All @@ -113,7 +101,7 @@ func execSQL(cmd *cobra.Command, args []string) error {
return err
}

return execSQLInsert(cmd.Context(), ru, activeSrc, destSrc, destTbl)
return execSQLInsert(ctx, ru, activeSrc, destSrc, destTbl)
}

// execSQLPrint executes the SQL and prints resulting records
Expand Down Expand Up @@ -144,7 +132,7 @@ func execSQLInsert(ctx context.Context, ru *run.Run,
ctx, cancelFn := context.WithCancel(ctx)
defer cancelFn()

fromDB, err := pools.Open(ctx, fromSrc)
fromPool, err := pools.Open(ctx, fromSrc)
if err != nil {
return err
}
Expand All @@ -154,18 +142,17 @@ func execSQLInsert(ctx context.Context, ru *run.Run,
return err
}

// Note: We don't need to worry about closing fromDB and
// Note: We don't need to worry about closing fromPool and
// destPool because they are closed by pools.Close, which
// is invoked by ru.Close, and ru is closed further up the
// stack.

inserter := libsq.NewDBWriter(
destPool,
destTbl,
driver.OptTuningRecChanSize.Get(destSrc.Options),
libsq.DBWriterCreateTableIfNotExistsHook(destTbl),
)
err = libsq.QuerySQL(ctx, fromDB, nil, inserter, args[0])
err = libsq.QuerySQL(ctx, fromPool, nil, inserter, args[0])
if err != nil {
return errz.Wrapf(err, "insert to {%s} failed", source.Target(destSrc, destTbl))
}
Expand All @@ -178,7 +165,7 @@ func execSQLInsert(ctx context.Context, ru *run.Run,
lg.FromContext(ctx).Debug(lgm.RowsAffected, lga.Count, affected)

// TODO: Should really use a Printer here
fmt.Fprintf(ru.Out, stringz.Plu("Inserted %d row(s) into %s\n",
_, _ = fmt.Fprintf(ru.Out, stringz.Plu("Inserted %d row(s) into %s\n",
int(affected)), affected, source.Target(destSrc, destTbl))
return nil
}
36 changes: 21 additions & 15 deletions drivers/mysql/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,20 +291,17 @@ func getSourceMetadata(ctx context.Context, src *source.Source, db sqlz.DB, noSc
})
})

if noSchema {
return md, nil
}

g.Go(func() error {
return doRetry(gCtx, func() error {
var err error
md.Tables, err = getAllTblMetas(gCtx, db)
return err
if !noSchema {
g.Go(func() error {
return doRetry(gCtx, func() error {
var err error
md.Tables, err = getAllTblMetas(gCtx, db)
return err
})
})
})
}

err := g.Wait()
if err != nil {
if err := g.Wait(); err != nil {
return nil, err
}

Expand All @@ -322,20 +319,29 @@ func getSourceMetadata(ctx context.Context, src *source.Source, db sqlz.DB, noSc
func setSourceSummaryMeta(ctx context.Context, db sqlz.DB, md *source.Metadata) error {
const summaryQuery = `SELECT @@GLOBAL.version, @@GLOBAL.version_comment, @@GLOBAL.version_compile_os,
@@GLOBAL.version_compile_machine, DATABASE(), CURRENT_USER(),
(SELECT CATALOG_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = DATABASE() LIMIT 1),
(SELECT SUM( data_length + index_length )
FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()) AS size`

var version, versionComment, versionOS, versionArch, schema string
var size sql.NullInt64
err := db.QueryRowContext(ctx, summaryQuery).Scan(&version, &versionComment, &versionOS, &versionArch, &schema,
&md.User, &size)
err := db.QueryRowContext(ctx, summaryQuery).Scan(
&version,
&versionComment,
&versionOS,
&versionArch,
&schema,
&md.User,
&md.Catalog,
&size,
)
if err != nil {
return errw(err)
}

md.Name = schema
md.Schema = schema
md.FQName = schema
md.FQName = md.Catalog + "." + schema
if size.Valid {
md.Size = size.Int64
}
Expand Down
32 changes: 23 additions & 9 deletions drivers/mysql/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,16 +229,28 @@ func (d *driveri) ListSchemas(ctx context.Context, db sqlz.DB) ([]string, error)
return schemas, nil
}

// CurrentCatalog implements driver.SQLDriver. MySQL does not support catalogs,
// so this method returns an error.
func (d *driveri) CurrentCatalog(_ context.Context, _ sqlz.DB) (string, error) {
return "", errz.New("mysql: catalog mechanism not supported")
// CurrentCatalog implements driver.SQLDriver. Although MySQL doesn't really
// support catalogs, we return the value found in INFORMATION_SCHEMA.SCHEMATA,
// i.e. "def".
func (d *driveri) CurrentCatalog(ctx context.Context, db sqlz.DB) (string, error) {
var catalog string

if err := db.QueryRowContext(ctx, selectCatalog).Scan(&catalog); err != nil {
return "", errw(err)
}
return catalog, nil
}

// ListCatalogs implements driver.SQLDriver. MySQL does not support catalogs,
// so this method returns an error.
func (d *driveri) ListCatalogs(_ context.Context, _ sqlz.DB) ([]string, error) {
return nil, errz.New("mysql: catalog mechanism not supported")
// ListCatalogs implements driver.SQLDriver. MySQL does not really support catalogs,
// but this method simply delegates to CurrentCatalog, which returns the value
// found in INFORMATION_SCHEMA.SCHEMATA, i.e. "def".
func (d *driveri) ListCatalogs(ctx context.Context, db sqlz.DB) ([]string, error) {
catalog, err := d.CurrentCatalog(ctx, db)
if err != nil {
return nil, err
}

return []string{catalog}, nil
}

// AlterTableRename implements driver.SQLDriver.
Expand Down Expand Up @@ -600,12 +612,14 @@ func tblfmt[T string | tablefq.T](tbl T) string {
return tfq.Render(stringz.BacktickQuote)
}

const selectCatalog = `SELECT CATALOG_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = DATABASE() LIMIT 1`

func doRenderFuncCatalog(_ *render.Context, fn *ast.FuncNode) (string, error) {
if fn.FuncName() != ast.FuncNameCatalog {
// Shouldn't happen
return "", errz.Errorf("expected %s function, got %q", ast.FuncNameCatalog, fn.FuncName())
}

const frag = `(SELECT CATALOG_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = DATABASE() LIMIT 1)`
const frag = `(` + selectCatalog + `)`
return frag, nil
}
1 change: 1 addition & 0 deletions drivers/postgres/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ current_setting('server_version'), version(), "current_user"()`
return nil, errz.New("NULL value for current_schema(): check privileges and search_path")
}

md.Catalog = md.Name
md.Schema = schema.String
md.FQName = md.Name + "." + schema.String

Expand Down
4 changes: 3 additions & 1 deletion drivers/sqlite3/sqlite3.go
Original file line number Diff line number Diff line change
Expand Up @@ -960,7 +960,9 @@ func (p *pool) SourceMetadata(ctx context.Context, noSchema bool) (*source.Metad

md.Size = fi.Size()
md.Name = fi.Name()
md.FQName = fi.Name() + "/" + md.Schema
md.FQName = fi.Name() + "." + md.Schema
// SQLite doesn't support catalog, but we conventionally set it to "default"
md.Catalog = "default"
md.Location = p.src.Location

md.DBProperties, err = getDBProperties(ctx, p.db)
Expand Down
1 change: 1 addition & 0 deletions drivers/sqlserver/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ GROUP BY database_id) AS total_size_bytes`

md.Name = catalog
md.FQName = catalog + "." + schema
md.Catalog = catalog
md.Schema = schema

if md.DBProperties, err = getDBProperties(ctx, db); err != nil {
Expand Down
29 changes: 20 additions & 9 deletions libsq/driver/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -608,15 +608,16 @@ func TestSQLDriver_CurrentCatalog(t *testing.T) {
}
}

func TestSQLDriver_CurrentSchema(t *testing.T) {
func TestSQLDriver_CurrentSchemaCatalog(t *testing.T) {
testCases := []struct {
handle string
want string
handle string
wantSchema string
wantCatalog string
}{
{sakila.SL3, "main"},
{sakila.Pg, "public"},
{sakila.My, "sakila"},
{sakila.MS, "dbo"},
{sakila.SL3, "main", "default"},
{sakila.Pg, "public", "sakila"},
{sakila.My, "sakila", "def"},
{sakila.MS, "dbo", "sakila"},
}

for _, tc := range testCases {
Expand All @@ -627,16 +628,26 @@ func TestSQLDriver_CurrentSchema(t *testing.T) {

gotSchema, err := drvr.CurrentSchema(th.Context, db)
require.NoError(t, err)
require.Equal(t, tc.want, gotSchema)
require.Equal(t, tc.wantSchema, gotSchema)

md, err := pool.SourceMetadata(th.Context, false)
require.NoError(t, err)
require.NotNil(t, md)
require.Equal(t, md.Schema, gotSchema)
require.Equal(t, md.Schema, tc.wantSchema)
require.Equal(t, md.Catalog, tc.wantCatalog)

gotSchemas, err := drvr.ListSchemas(th.Context, db)
require.NoError(t, err)
require.Contains(t, gotSchemas, gotSchema)

if drvr.Dialect().Catalog {
gotCatalog, err := drvr.CurrentCatalog(th.Context, db)
require.NoError(t, err)
require.Equal(t, tc.wantCatalog, gotCatalog)
gotCatalogs, err := drvr.ListCatalogs(th.Context, db)
require.NoError(t, err)
require.Contains(t, gotCatalogs, gotCatalog)
}
})
}
}
Expand Down
3 changes: 3 additions & 0 deletions libsq/source/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type Metadata struct {
// This may be empty for some sources.
Schema string `json:"schema,omitempty" yaml:"schema,omitempty"`

// Catalog is the catalog name, for example "sakila".
Catalog string `json:"catalog,omitempty" yaml:"catalog,omitempty"`

// Driver is the source driver type.
Driver DriverType `json:"driver" yaml:"driver"`

Expand Down

0 comments on commit d7fc315

Please sign in to comment.