sq inspect now shows catalog (when outputting in JSON and YAML format) (#329)

This commit is contained in:
Neil O'Toole 2023-11-19 07:21:38 -07:00 committed by GitHub
parent 30b7d6421e
commit d7fc315028
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 106 additions and 58 deletions

View File

@ -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
@ -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

View File

@ -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) {

View File

@ -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'
@ -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")
@ -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
}
@ -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
@ -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
@ -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
}
@ -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))
}
@ -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
}

View File

@ -291,20 +291,17 @@ func getSourceMetadata(ctx context.Context, src *source.Source, db sqlz.DB, noSc
})
})
if noSchema {
return md, nil
if !noSchema {
g.Go(func() error {
return doRetry(gCtx, func() error {
var err error
md.Tables, err = getAllTblMetas(gCtx, db)
return err
})
})
}
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
}
@ -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
}

View File

@ -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.
@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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 {
@ -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)
}
})
}
}

View File

@ -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"`