2021-02-22 10:37:00 +03:00
|
|
|
package cli
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2024-01-15 04:45:34 +03:00
|
|
|
"log/slog"
|
2023-08-12 21:54:14 +03:00
|
|
|
"slices"
|
2021-02-22 10:37:00 +03:00
|
|
|
"strings"
|
2023-04-26 18:16:42 +03:00
|
|
|
"time"
|
|
|
|
|
2023-11-20 04:06:36 +03:00
|
|
|
"github.com/samber/lo"
|
|
|
|
"github.com/spf13/cobra"
|
2023-05-07 05:36:34 +03:00
|
|
|
|
2023-05-01 06:59:34 +03:00
|
|
|
"github.com/neilotoole/sq/cli/flag"
|
|
|
|
"github.com/neilotoole/sq/cli/output/format"
|
2023-11-20 04:06:36 +03:00
|
|
|
"github.com/neilotoole/sq/cli/run"
|
2023-04-02 22:49:45 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/core/lg"
|
2023-11-20 04:06:36 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/core/lg/lga"
|
|
|
|
"github.com/neilotoole/sq/libsq/core/lg/lgm"
|
|
|
|
"github.com/neilotoole/sq/libsq/core/options"
|
|
|
|
"github.com/neilotoole/sq/libsq/core/stringz"
|
|
|
|
"github.com/neilotoole/sq/libsq/core/timez"
|
2021-02-22 10:37:00 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/source"
|
|
|
|
)
|
|
|
|
|
2023-04-26 18:16:42 +03:00
|
|
|
var OptShellCompletionTimeout = options.NewDuration(
|
|
|
|
"shell-completion.timeout",
|
2023-05-22 18:08:14 +03:00
|
|
|
"",
|
2023-05-07 05:36:34 +03:00
|
|
|
0,
|
2023-04-26 18:16:42 +03:00
|
|
|
time.Millisecond*500,
|
2024-01-15 04:45:34 +03:00
|
|
|
"Shell completion timeout",
|
2024-01-29 01:16:41 +03:00
|
|
|
`How long shell completion should wait before giving up. This can become relevant
|
|
|
|
when shell completion inspects a source's metadata, e.g. to offer a list of
|
|
|
|
tables in a source.`,
|
2023-04-26 18:16:42 +03:00
|
|
|
)
|
|
|
|
|
2024-01-25 07:01:24 +03:00
|
|
|
var OptShellCompletionLog = options.NewBool(
|
|
|
|
"shell-completion.log",
|
|
|
|
"",
|
|
|
|
false,
|
|
|
|
0,
|
|
|
|
false,
|
|
|
|
"Enable logging of shell completion activity",
|
2024-01-29 00:55:51 +03:00
|
|
|
`Enable logging of shell completion activity. This is really only useful
|
|
|
|
for debugging shell completion functionality. It's disabled by default,
|
|
|
|
because it's frequently the case that shell completion handlers will trigger
|
|
|
|
work (such as inspecting the schema) that doesn't complete by the shell
|
|
|
|
completion timeout. This can result in the logs being filled with uninteresting
|
|
|
|
junk when the timeout triggers logging of errors.`,
|
2024-01-25 07:01:24 +03:00
|
|
|
)
|
|
|
|
|
2021-02-22 10:37:00 +03:00
|
|
|
// completionFunc is a shell completion function.
|
|
|
|
type completionFunc func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
|
|
|
|
|
|
|
|
var (
|
|
|
|
_ completionFunc = completeDriverType
|
|
|
|
_ completionFunc = completeSLQ
|
2023-04-01 12:48:24 +03:00
|
|
|
_ completionFunc = (*handleTableCompleter)(nil).complete
|
2021-02-22 10:37:00 +03:00
|
|
|
)
|
|
|
|
|
2023-04-26 18:16:42 +03:00
|
|
|
// completeStrings completes from a slice of string.
|
2024-01-15 04:45:34 +03:00
|
|
|
func completeStrings(max int, a ...string) completionFunc {
|
2023-04-26 18:16:42 +03:00
|
|
|
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
|
|
if max > 0 && len(args) >= max {
|
|
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
}
|
2023-05-07 05:36:34 +03:00
|
|
|
|
2023-08-04 08:41:33 +03:00
|
|
|
return a, cobra.ShellCompDirectiveNoFileComp & cobra.ShellCompDirectiveKeepOrder
|
2023-04-26 18:16:42 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-07 05:36:34 +03:00
|
|
|
// completeBool returns "true" and "false".
|
|
|
|
func completeBool(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
|
|
|
return []string{"true", "false"}, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
}
|
|
|
|
|
2021-02-22 10:37:00 +03:00
|
|
|
// completeHandle is a completionFunc that suggests handles.
|
2023-06-18 04:28:11 +03:00
|
|
|
// The max arg is the maximum number of completions. Set to 0
|
2021-02-22 10:37:00 +03:00
|
|
|
// for no limit.
|
|
|
|
func completeHandle(max int) completionFunc {
|
|
|
|
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
|
|
if max > 0 && len(args) >= max {
|
|
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
}
|
|
|
|
|
2023-05-19 17:24:18 +03:00
|
|
|
ru := getRun(cmd)
|
|
|
|
handles := ru.Config.Collection.Handles()
|
2023-04-16 01:28:51 +03:00
|
|
|
handles = lo.Reject(handles, func(item string, index int) bool {
|
|
|
|
return !strings.HasPrefix(item, toComplete)
|
|
|
|
})
|
2023-01-01 06:17:44 +03:00
|
|
|
|
2023-04-16 01:28:51 +03:00
|
|
|
slices.Sort(handles) // REVISIT: what's the logic for sorting or not?
|
|
|
|
handles, _ = lo.Difference(handles, args)
|
2023-05-05 17:32:50 +03:00
|
|
|
|
2023-05-19 17:24:18 +03:00
|
|
|
if ru.Config.Collection.Active() != nil {
|
2023-05-05 17:32:50 +03:00
|
|
|
handles = append([]string{source.ActiveHandle}, handles...)
|
|
|
|
}
|
|
|
|
|
2021-02-22 10:37:00 +03:00
|
|
|
return handles, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-16 01:28:51 +03:00
|
|
|
// completeGroup is a completionFunc that suggests groups.
|
2023-06-18 04:28:11 +03:00
|
|
|
// The max arg is the maximum number of completions. Set to 0
|
2023-04-16 01:28:51 +03:00
|
|
|
// for no limit.
|
|
|
|
func completeGroup(max int) completionFunc {
|
|
|
|
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
|
|
if max > 0 && len(args) >= max {
|
|
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
}
|
|
|
|
|
2023-05-19 17:24:18 +03:00
|
|
|
ru := getRun(cmd)
|
|
|
|
groups := ru.Config.Collection.Groups()
|
2023-04-16 01:28:51 +03:00
|
|
|
groups, _ = lo.Difference(groups, args)
|
|
|
|
groups = lo.Uniq(groups)
|
|
|
|
slices.Sort(groups)
|
|
|
|
return groups, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// completeHandleOrGroup returns the matching list of handles+groups.
|
|
|
|
func completeHandleOrGroup(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
|
|
switch {
|
|
|
|
case toComplete == "":
|
|
|
|
items, _ := completeHandle(0)(cmd, args, toComplete)
|
|
|
|
groups, _ := completeGroup(0)(cmd, args, toComplete)
|
|
|
|
items = append(items, groups...)
|
|
|
|
items = lo.Uniq(items)
|
|
|
|
return items, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
case toComplete == "/":
|
|
|
|
return []string{}, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
case toComplete[0] == '@':
|
|
|
|
return completeHandle(0)(cmd, args, toComplete)
|
|
|
|
case source.IsValidGroup(toComplete):
|
|
|
|
return completeGroup(0)(cmd, args, toComplete)
|
|
|
|
default:
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-22 10:37:00 +03:00
|
|
|
// completeSLQ is a completionFunc that completes SLQ queries.
|
|
|
|
// The completion functionality is rudimentary: it only
|
|
|
|
// completes the "table select" segment (that is, the @HANDLE.NAME)
|
|
|
|
// segment.
|
|
|
|
func completeSLQ(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
|
|
if len(args) != 0 {
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
c := &handleTableCompleter{}
|
|
|
|
return c.complete(cmd, args, toComplete)
|
|
|
|
}
|
|
|
|
|
|
|
|
// completeDriverType is a completionFunc that suggests drivers.
|
2023-04-01 11:38:32 +03:00
|
|
|
func completeDriverType(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
2023-05-19 17:24:18 +03:00
|
|
|
ru := getRun(cmd)
|
2024-01-15 04:45:34 +03:00
|
|
|
if ru.Grips == nil {
|
2023-05-19 17:24:18 +03:00
|
|
|
if err := preRun(cmd, ru); err != nil {
|
2023-04-30 17:18:56 +03:00
|
|
|
lg.Unexpected(logFrom(cmd), err)
|
2021-02-22 10:37:00 +03:00
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-19 17:24:18 +03:00
|
|
|
drivers := ru.DriverRegistry.Drivers()
|
2021-02-22 10:37:00 +03:00
|
|
|
types := make([]string, len(drivers))
|
2023-05-19 17:24:18 +03:00
|
|
|
for i, driver := range ru.DriverRegistry.Drivers() {
|
2021-02-22 10:37:00 +03:00
|
|
|
types[i] = string(driver.DriverMetadata().Type)
|
|
|
|
}
|
|
|
|
|
|
|
|
return types, cobra.ShellCompDirectiveNoFileComp
|
2023-05-01 06:59:34 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// completeOptKey is a completionFunc that completes keys for options.Opt.
|
|
|
|
// If flag.ConfigSrc is set on cmd, the returned completions are limited to
|
|
|
|
// Opt keys appropriate to that source. For example, if the source is Excel,
|
|
|
|
// then "driver.csv.delim" won't be offered.
|
|
|
|
func completeOptKey(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
2023-05-19 17:24:18 +03:00
|
|
|
ru := getRun(cmd)
|
|
|
|
keys := ru.OptionsRegistry.Keys()
|
2023-05-01 06:59:34 +03:00
|
|
|
|
|
|
|
if cmdFlagChanged(cmd, flag.ConfigSrc) {
|
|
|
|
// If using with --src, then we only want to show the opts
|
|
|
|
// that apply to that source.
|
|
|
|
handle, err := cmd.Flags().GetString(flag.ConfigSrc)
|
|
|
|
if err != nil {
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
2023-05-19 17:24:18 +03:00
|
|
|
src, err := ru.Config.Collection.Get(handle)
|
2023-05-01 06:59:34 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
2023-05-19 17:24:18 +03:00
|
|
|
opts := filterOptionsForSrc(src.Type, ru.OptionsRegistry.Opts()...)
|
2023-05-01 06:59:34 +03:00
|
|
|
keys = lo.Map(opts, func(item options.Opt, index int) string {
|
|
|
|
return item.Key()
|
|
|
|
})
|
2023-05-05 17:32:50 +03:00
|
|
|
|
|
|
|
if cmdFlagChanged(cmd, flag.ConfigDelete) {
|
|
|
|
if len(src.Options) == 0 {
|
|
|
|
// Nothing to delete
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
// There are options to delete
|
|
|
|
return src.Options.Keys(), cobra.ShellCompDirectiveDefault
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if cmdFlagChanged(cmd, flag.ConfigDelete) {
|
|
|
|
// At this stage, we have to offer all opts, because the user
|
|
|
|
// input could become: $ sq config set -D ingest.header --src @csv
|
2023-05-19 17:24:18 +03:00
|
|
|
return ru.OptionsRegistry.Keys(), cobra.ShellCompDirectiveDefault
|
2023-05-01 06:59:34 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
keys = lo.Filter(keys, func(item string, index int) bool {
|
|
|
|
return strings.HasPrefix(item, toComplete)
|
|
|
|
})
|
|
|
|
|
|
|
|
if len(keys) == 0 && len(toComplete) > 0 {
|
|
|
|
logFrom(cmd).Warn("Invalid option key", lga.Key, toComplete)
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
return keys, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
}
|
|
|
|
|
|
|
|
// completeOptValue is a completionFunc that completes values for options.Opt.
|
|
|
|
// It expects that args[0] is a valid Opt key.
|
|
|
|
func completeOptValue(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
|
|
if len(args) != 1 {
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
2023-05-19 17:24:18 +03:00
|
|
|
ru := getRun(cmd)
|
|
|
|
opt := ru.OptionsRegistry.Get(args[0])
|
2023-05-01 06:59:34 +03:00
|
|
|
if opt == nil {
|
|
|
|
logFrom(cmd).Warn("Invalid option key", lga.Key, args[0])
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
var a []string
|
|
|
|
switch opt.(type) {
|
2023-05-05 17:32:50 +03:00
|
|
|
case options.String:
|
2023-05-07 05:36:34 +03:00
|
|
|
switch opt.Key() {
|
|
|
|
case OptLogFile.Key():
|
2023-05-05 17:32:50 +03:00
|
|
|
// We return the default directive, so that the shell will offer
|
|
|
|
// regular ol' file completion.
|
|
|
|
return a, cobra.ShellCompDirectiveDefault
|
2023-05-07 05:36:34 +03:00
|
|
|
case OptDatetimeFormat.Key(), OptTimeFormat.Key(), OptDateFormat.Key():
|
|
|
|
return timez.NamedLayouts(), cobra.ShellCompDirectiveNoFileComp
|
2023-05-05 17:32:50 +03:00
|
|
|
}
|
2023-05-07 05:36:34 +03:00
|
|
|
|
2023-05-05 17:32:50 +03:00
|
|
|
case LogLevelOpt:
|
2024-01-15 04:45:34 +03:00
|
|
|
a = []string{slog.LevelDebug.String(), slog.LevelInfo.String(), slog.LevelWarn.String(), slog.LevelError.String()}
|
2023-05-22 18:08:14 +03:00
|
|
|
case format.Opt:
|
2024-01-15 04:45:34 +03:00
|
|
|
switch opt.Key() {
|
|
|
|
case OptErrorFormat.Key(), OptLogFormat.Key():
|
2023-07-27 07:19:11 +03:00
|
|
|
a = []string{string(format.Text), string(format.JSON)}
|
2024-01-15 04:45:34 +03:00
|
|
|
default:
|
2023-07-27 07:19:11 +03:00
|
|
|
a = stringz.Strings(format.All())
|
|
|
|
}
|
2023-05-01 06:59:34 +03:00
|
|
|
case options.Bool:
|
|
|
|
a = []string{"true", "false"}
|
|
|
|
default:
|
|
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
}
|
|
|
|
|
|
|
|
a = lo.Filter(a, func(item string, index int) bool {
|
|
|
|
return strings.HasPrefix(item, toComplete)
|
|
|
|
})
|
|
|
|
|
|
|
|
return a, cobra.ShellCompDirectiveNoFileComp
|
2021-02-22 10:37:00 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// completeTblCopy is a completionFunc for the "tbl copy" command.
|
|
|
|
func completeTblCopy(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
|
|
// Example invocation:
|
|
|
|
//
|
|
|
|
// sq tbl copy @sakila_sl3.actor .new_table
|
|
|
|
//
|
|
|
|
// Note that the second arg can only be a table name (and
|
|
|
|
// not a @HANDLE.TABLE), and it must also be a _new_ table
|
|
|
|
// (because we can't copy over an existing table), thus
|
|
|
|
// we only suggest "." for the second arg, forcing the user
|
|
|
|
// to supply the rest of that new table name.
|
|
|
|
switch len(args) {
|
|
|
|
case 0:
|
|
|
|
c := &handleTableCompleter{onlySQL: true}
|
|
|
|
return c.complete(cmd, args, toComplete)
|
|
|
|
case 1:
|
|
|
|
return []string{"."}, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
|
|
|
|
default:
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-19 03:05:48 +03:00
|
|
|
// activeSchemaCompleter encapsulates completion for flag.ActiveSchema.
|
|
|
|
// The completionFunc is activeSchemaCompleter.complete.
|
|
|
|
//
|
|
|
|
// Example usage:
|
|
|
|
//
|
|
|
|
// # Only schema
|
|
|
|
// $ sq --src.schema information_schema '.tables'
|
|
|
|
//
|
|
|
|
// # Using catalog.schema
|
|
|
|
// $ sq --src.schema postgres.information_schema '.tables'
|
|
|
|
//
|
|
|
|
// Note that some drivers don't support the catalog mechanism (e.g. SQLite).
|
|
|
|
//
|
|
|
|
// The returned slice contains the names of the schemas in the source, followed
|
|
|
|
// by the names of the catalogs (suffixed with a period, e.g. "sakila.", so
|
|
|
|
// that the user can complete the catalog.schema input, e.g. "sakila.public").
|
|
|
|
// For example:
|
|
|
|
//
|
|
|
|
// information_schema <-- this a schema in the active source
|
|
|
|
// pg_catalog
|
|
|
|
// public
|
|
|
|
// sakila. <-- note the trailing period, this is a catalog
|
|
|
|
// customers.
|
|
|
|
// postgres.
|
|
|
|
//
|
|
|
|
// If toComplete already contains a period (e.g. "sakila."), then the
|
|
|
|
// returned slice contains only the matching catalog-qualified schemas,
|
|
|
|
// e.g. "sakila.public", "sakila.information_schema", etc.
|
|
|
|
//
|
|
|
|
// Note the field activeSchemaCompleter.activeSourceFunc. This func is used to
|
|
|
|
// determine the source to act against. This is configurable because some commands
|
|
|
|
// may honor a flag (flag.ActiveSrc), but a different flag (or even cmd args)
|
|
|
|
// could also be used. Func getActiveSourceViaFlag is one such func impl. When
|
|
|
|
// that is used, if the command has flag.ActiveSrc set, it is honored. Otherwise,
|
|
|
|
// the config's active source is used. For example:
|
|
|
|
//
|
|
|
|
// $ sq --src @sakila/pg12 --src.schema postgres.information_schema '.tables'
|
|
|
|
//
|
|
|
|
// Note also: if the targeted source is not SQL (e.g. CSV), an error is returned.
|
|
|
|
type activeSchemaCompleter struct {
|
|
|
|
// activeSourceFunc is a function that returns the active source.
|
|
|
|
// Typically the active source comes from the config, but it can also
|
|
|
|
// be supplied via other means, e.g. flag.ActiveSrc or a command arg.
|
|
|
|
activeSourceFunc func(cmd *cobra.Command, args []string) (*source.Source, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c activeSchemaCompleter) complete(cmd *cobra.Command, args []string, toComplete string,
|
|
|
|
) ([]string, cobra.ShellCompDirective) {
|
|
|
|
const baseDirective = cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveKeepOrder
|
|
|
|
|
|
|
|
log, ru := logFrom(cmd), getRun(cmd)
|
|
|
|
if err := preRun(cmd, ru); err != nil {
|
|
|
|
lg.Unexpected(log, err)
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
src, err := c.activeSourceFunc(cmd, args)
|
|
|
|
if err != nil {
|
|
|
|
lg.Unexpected(log, err)
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
if src == nil {
|
|
|
|
log.Debug("No active source, thus no completion for flag", lga.Flag, flag.ActiveSrc)
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
// If toComplete contains a period, then we extract the part before
|
|
|
|
// the period into inputCatalog.
|
|
|
|
var inputCatalog string
|
|
|
|
if toComplete != "" {
|
|
|
|
if strings.ContainsRune(toComplete, '.') {
|
|
|
|
// User has supplied a catalog.schema (or at least a "catalog.")
|
|
|
|
parts := strings.Split(toComplete, ".")
|
|
|
|
if len(parts) > 2 {
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
inputCatalog = parts[0]
|
|
|
|
if inputCatalog == "" {
|
|
|
|
// User supplied input of the form ".schema" (leading period),
|
|
|
|
// which is invalid.
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
src.Catalog = inputCatalog
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ok, _ := isSQLDriver(ru, src.Handle); !ok {
|
|
|
|
// Not a SQL driver, completion is N/A.
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
drvr, err := ru.DriverRegistry.SQLDriverFor(src.Type)
|
|
|
|
if err != nil {
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
// We don't want the user to wait around forever for
|
|
|
|
// shell completion, so we set a timeout. Typically
|
|
|
|
// this is something like 500ms.
|
|
|
|
ctx, cancelFn := context.WithTimeout(cmd.Context(), OptShellCompletionTimeout.Get(ru.Config.Options))
|
|
|
|
defer cancelFn()
|
|
|
|
|
2024-01-15 04:45:34 +03:00
|
|
|
grip, err := ru.Grips.Open(ctx, src)
|
2023-11-19 03:05:48 +03:00
|
|
|
if err != nil {
|
|
|
|
lg.Unexpected(log, err)
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
2024-01-15 04:45:34 +03:00
|
|
|
db, err := grip.DB(ctx)
|
2023-11-19 03:05:48 +03:00
|
|
|
if err != nil {
|
|
|
|
lg.Unexpected(log, err)
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
defer lg.WarnIfCloseError(log, lgm.CloseDB, db)
|
|
|
|
|
|
|
|
a, err := drvr.ListSchemas(ctx, db)
|
|
|
|
if err != nil {
|
|
|
|
lg.Unexpected(log, err)
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(a) == 0 {
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
if inputCatalog != "" {
|
|
|
|
// We have a catalog, so we need to prepend it to each
|
|
|
|
// schema name.
|
|
|
|
for i := range a {
|
|
|
|
a[i] = inputCatalog + "." + a[i]
|
|
|
|
}
|
|
|
|
|
|
|
|
a = lo.Filter(a, func(item string, index int) bool {
|
|
|
|
return strings.HasPrefix(item, toComplete)
|
|
|
|
})
|
|
|
|
|
|
|
|
return a, baseDirective
|
|
|
|
}
|
|
|
|
|
|
|
|
if drvr.Dialect().Catalog {
|
|
|
|
var catalogs []string
|
|
|
|
if catalogs, err = drvr.ListCatalogs(ctx, db); err != nil {
|
|
|
|
// We continue even if an error occurs.
|
|
|
|
log.Warn("List catalogs", lga.Err, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := range catalogs {
|
|
|
|
a = append(a, catalogs[i]+".")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
a = lo.Filter(a, func(item string, index int) bool {
|
|
|
|
return strings.HasPrefix(item, toComplete)
|
|
|
|
})
|
|
|
|
|
|
|
|
for i := range a {
|
|
|
|
// If any of the completions has a trailing period (i.e. they've
|
|
|
|
// only typed the catalog name), then we need cobra.ShellCompDirectiveNoSpace,
|
|
|
|
// because the user has more typing to do to complete the catalog.schema.
|
|
|
|
if strings.HasSuffix(a[i], ".") {
|
|
|
|
return a, baseDirective | cobra.ShellCompDirectiveNoSpace
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return a, baseDirective
|
|
|
|
}
|
|
|
|
|
|
|
|
// getActiveSourceViaFlag returns the active source, either from the
|
|
|
|
// config or from flag.ActiveSrc. This function is intended for use
|
|
|
|
// with activeSchemaCompleter.activeSourceFunc.
|
|
|
|
func getActiveSourceViaFlag(cmd *cobra.Command, _ []string) (*source.Source, error) {
|
|
|
|
if !cmdFlagChanged(cmd, flag.ActiveSrc) {
|
|
|
|
// User has not supplied --src, so we'll use the
|
|
|
|
// config's active source.
|
|
|
|
return getRun(cmd).Config.Collection.Active(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
handle, err := cmd.Flags().GetString(flag.ActiveSrc)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var src *source.Source
|
|
|
|
if src, err = getRun(cmd).Config.Collection.Get(handle); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return src, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getActiveSourceViaArgs returns the active source, either from the
|
|
|
|
// config or from the handle in the first cmd arg. This function is intended
|
|
|
|
// for use with activeSchemaCompleter.activeSourceFunc.
|
|
|
|
func getActiveSourceViaArgs(cmd *cobra.Command, args []string) (*source.Source, error) {
|
|
|
|
if len(args) == 0 {
|
|
|
|
// No args supplied, so we'll use the config's active source.
|
|
|
|
return getRun(cmd).Config.Collection.Active(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
handle := args[0]
|
|
|
|
return getRun(cmd).Config.Collection.Get(handle)
|
|
|
|
}
|
|
|
|
|
2021-02-22 10:37:00 +03:00
|
|
|
// handleTableCompleter encapsulates completion of a handle
|
|
|
|
// ("@sakila_sl3"), table (".actor"), or @HANDLE.TABLE
|
|
|
|
// ("@sakila_sl3.actor"). Its complete method is a completionFunc.
|
|
|
|
type handleTableCompleter struct {
|
|
|
|
// onlySQL, when true, filters out non-SQL sources.
|
|
|
|
onlySQL bool
|
|
|
|
|
|
|
|
// handleRequired, when true, means that only @HANDLE.TABLE
|
|
|
|
// suggestions are offered. That is, naked .TABLE suggestions
|
|
|
|
// will not be offered.
|
|
|
|
handleRequired bool
|
|
|
|
|
|
|
|
// max indicates the maximum number of completions
|
|
|
|
// to offer. Use 0 to indicate no limit. Frequently this
|
|
|
|
// is set to 1 to if the command accepts only one argument.
|
|
|
|
max int
|
|
|
|
}
|
|
|
|
|
|
|
|
// complete is the completionFunc for handleTableCompleter.
|
2023-04-01 11:38:32 +03:00
|
|
|
func (c *handleTableCompleter) complete(cmd *cobra.Command, args []string,
|
|
|
|
toComplete string,
|
|
|
|
) ([]string, cobra.ShellCompDirective) {
|
2023-05-19 17:24:18 +03:00
|
|
|
ru := getRun(cmd)
|
|
|
|
if err := preRun(cmd, ru); err != nil {
|
2023-04-30 17:18:56 +03:00
|
|
|
lg.Unexpected(logFrom(cmd), err)
|
2021-02-22 10:37:00 +03:00
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
// We don't want the user to wait around forever for
|
|
|
|
// shell completion, so we set a timeout. Typically
|
|
|
|
// this is something like 500ms.
|
2023-05-19 17:24:18 +03:00
|
|
|
ctx, cancelFn := context.WithTimeout(cmd.Context(), OptShellCompletionTimeout.Get(ru.Config.Options))
|
2021-02-22 10:37:00 +03:00
|
|
|
defer cancelFn()
|
|
|
|
|
|
|
|
if c.max > 0 && len(args) >= c.max {
|
|
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
}
|
|
|
|
|
|
|
|
if toComplete == "" {
|
|
|
|
if c.handleRequired {
|
2023-05-19 17:24:18 +03:00
|
|
|
return c.completeHandle(ctx, ru, args, toComplete)
|
2021-02-22 10:37:00 +03:00
|
|
|
}
|
2023-05-19 17:24:18 +03:00
|
|
|
return c.completeEither(ctx, ru, args, toComplete)
|
2021-02-22 10:37:00 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// There's some input. We expect the input to be of the
|
2022-12-23 19:32:07 +03:00
|
|
|
// form "@handle" or ".table". That is, the input should
|
2021-02-22 10:37:00 +03:00
|
|
|
// start with either '@' or '.'.
|
|
|
|
switch toComplete[0] {
|
|
|
|
default:
|
|
|
|
// User input was something other than '@' or '.'
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
case '@':
|
2023-05-19 17:24:18 +03:00
|
|
|
return c.completeHandle(ctx, ru, args, toComplete)
|
2021-02-22 10:37:00 +03:00
|
|
|
case '.':
|
|
|
|
if c.handleRequired {
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
2023-05-19 17:24:18 +03:00
|
|
|
return c.completeTableOnly(ctx, ru, args, toComplete)
|
2021-02-22 10:37:00 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// completeTableOnly returns suggestions given input beginning with
|
|
|
|
// a period. Effectively this is completion for tables in the
|
|
|
|
// active src.
|
2023-05-19 17:24:18 +03:00
|
|
|
func (c *handleTableCompleter) completeTableOnly(ctx context.Context, ru *run.Run, _ []string,
|
2022-12-18 11:35:59 +03:00
|
|
|
toComplete string,
|
|
|
|
) ([]string, cobra.ShellCompDirective) {
|
2023-05-19 17:24:18 +03:00
|
|
|
activeSrc := ru.Config.Collection.Active()
|
2021-02-22 10:37:00 +03:00
|
|
|
if activeSrc == nil {
|
2023-05-03 15:36:10 +03:00
|
|
|
lg.FromContext(ctx).Error("Active source is nil")
|
2021-02-22 10:37:00 +03:00
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.onlySQL {
|
2023-11-19 03:05:48 +03:00
|
|
|
isSQL, err := isSQLDriver(ru, activeSrc.Handle)
|
2021-02-22 10:37:00 +03:00
|
|
|
if err != nil {
|
2023-05-03 15:36:10 +03:00
|
|
|
lg.Unexpected(lg.FromContext(ctx), err)
|
2021-02-22 10:37:00 +03:00
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
if !isSQL {
|
|
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-19 17:24:18 +03:00
|
|
|
tables, err := getTableNamesForHandle(ctx, ru, activeSrc.Handle)
|
2021-02-22 10:37:00 +03:00
|
|
|
if err != nil {
|
2023-05-03 15:36:10 +03:00
|
|
|
lg.Unexpected(lg.FromContext(ctx), err)
|
2021-02-22 10:37:00 +03:00
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
var suggestions []string
|
|
|
|
for _, table := range tables {
|
|
|
|
if strings.HasPrefix(table, toComplete[1:]) {
|
|
|
|
suggestions = append(suggestions, "."+table)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return suggestions, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
}
|
|
|
|
|
|
|
|
// completeHandle returns suggestions given input beginning with
|
|
|
|
// a '@'. The returned suggestions could be @HANDLE, or @HANDLE.TABLE.
|
2023-05-19 17:24:18 +03:00
|
|
|
func (c *handleTableCompleter) completeHandle(ctx context.Context, ru *run.Run, _ []string,
|
2022-12-18 11:35:59 +03:00
|
|
|
toComplete string,
|
|
|
|
) ([]string, cobra.ShellCompDirective) {
|
2021-02-22 10:37:00 +03:00
|
|
|
// We're dealing with a handle.
|
|
|
|
|
|
|
|
// But we could be dealing with just the handle ("@sakila_sl3")
|
|
|
|
// or a @HANDLE.TABLE ("@sakila_sl3.actor").
|
|
|
|
if strings.ContainsRune(toComplete, '.') {
|
|
|
|
if strings.Count(toComplete, ".") > 1 {
|
|
|
|
// Can only have one period
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
// It's a handle with a full handle and at least a
|
|
|
|
// partial table name, such as "@sakila_sl3.fil"
|
|
|
|
handle, partialTbl, err := source.ParseTableHandle(strings.TrimSuffix(toComplete, "."))
|
|
|
|
if err != nil {
|
2023-05-03 15:36:10 +03:00
|
|
|
lg.Unexpected(lg.FromContext(ctx), err)
|
2021-02-22 10:37:00 +03:00
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.onlySQL {
|
2022-12-18 09:07:38 +03:00
|
|
|
var isSQL bool
|
2023-11-19 03:05:48 +03:00
|
|
|
isSQL, err = isSQLDriver(ru, handle)
|
2021-02-22 10:37:00 +03:00
|
|
|
if err != nil {
|
2023-05-03 15:36:10 +03:00
|
|
|
lg.Unexpected(lg.FromContext(ctx), err)
|
2021-02-22 10:37:00 +03:00
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
if !isSQL {
|
|
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-19 17:24:18 +03:00
|
|
|
tables, err := getTableNamesForHandle(ctx, ru, handle)
|
2021-02-22 10:37:00 +03:00
|
|
|
if err != nil {
|
2023-05-03 15:36:10 +03:00
|
|
|
lg.Unexpected(lg.FromContext(ctx), err)
|
2021-02-22 10:37:00 +03:00
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
|
|
|
var suggestions []string
|
|
|
|
for _, table := range tables {
|
|
|
|
if strings.HasPrefix(table, partialTbl) {
|
|
|
|
suggestions = append(suggestions, handle+"."+table)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return suggestions, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
}
|
|
|
|
|
2023-05-19 17:24:18 +03:00
|
|
|
handles := ru.Config.Collection.Handles()
|
2023-05-05 17:32:50 +03:00
|
|
|
handles = append([]string{source.ActiveHandle}, handles...)
|
2021-02-22 10:37:00 +03:00
|
|
|
// Else, we're dealing with just a handle so far
|
|
|
|
var matchingHandles []string
|
|
|
|
for _, handle := range handles {
|
|
|
|
if strings.HasPrefix(handle, toComplete) {
|
|
|
|
if c.onlySQL {
|
2023-11-19 03:05:48 +03:00
|
|
|
isSQL, err := isSQLDriver(ru, handle)
|
2021-02-22 10:37:00 +03:00
|
|
|
if err != nil {
|
2023-05-03 15:36:10 +03:00
|
|
|
lg.Unexpected(lg.FromContext(ctx), err)
|
2021-02-22 10:37:00 +03:00
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
if !isSQL {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
matchingHandles = append(matchingHandles, handle)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
switch len(matchingHandles) {
|
|
|
|
default:
|
|
|
|
return matchingHandles, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
|
|
|
|
case 0:
|
|
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
case 1:
|
|
|
|
// Only one handle match, so we will present that complete
|
|
|
|
// handle, plus a suggestion (@HANDLE.TABLE) for each of the tables
|
|
|
|
// for that handle
|
|
|
|
}
|
|
|
|
|
2023-05-19 17:24:18 +03:00
|
|
|
tables, err := getTableNamesForHandle(ctx, ru, matchingHandles[0])
|
2021-02-22 10:37:00 +03:00
|
|
|
if err != nil {
|
2022-12-23 19:32:07 +03:00
|
|
|
// This means that we aren't able to get metadata for this source.
|
|
|
|
// This could be because the source is temporarily offline. The
|
|
|
|
// best we can do is just to return the handle, without the tables.
|
2024-01-15 04:45:34 +03:00
|
|
|
lg.WarnIfError(lg.FromContext(ctx), "Get metadata", err)
|
2022-12-23 19:32:07 +03:00
|
|
|
return matchingHandles, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
|
2021-02-22 10:37:00 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
suggestions := []string{matchingHandles[0]}
|
|
|
|
for _, table := range tables {
|
|
|
|
suggestions = append(suggestions, matchingHandles[0]+"."+table)
|
|
|
|
}
|
|
|
|
|
|
|
|
return suggestions, cobra.ShellCompDirectiveNoFileComp
|
|
|
|
}
|
|
|
|
|
2022-12-18 09:42:11 +03:00
|
|
|
// completeEither returns a union of all handles plus the tables from the active source.
|
2023-05-19 17:24:18 +03:00
|
|
|
func (c *handleTableCompleter) completeEither(ctx context.Context, ru *run.Run,
|
2023-04-01 11:38:32 +03:00
|
|
|
_ []string, _ string,
|
2022-12-18 11:35:59 +03:00
|
|
|
) ([]string, cobra.ShellCompDirective) {
|
2023-04-19 08:28:09 +03:00
|
|
|
var suggestions []string
|
|
|
|
|
2021-02-22 10:37:00 +03:00
|
|
|
// There's no input yet.
|
|
|
|
// Therefore we want to return a union of all handles
|
|
|
|
// plus the tables from the active source.
|
2023-05-19 17:24:18 +03:00
|
|
|
activeSrc := ru.Config.Collection.Active()
|
2023-04-19 08:28:09 +03:00
|
|
|
if activeSrc != nil {
|
|
|
|
var activeSrcTables []string
|
2023-11-19 03:05:48 +03:00
|
|
|
isSQL, err := isSQLDriver(ru, activeSrc.Handle)
|
2021-02-22 10:37:00 +03:00
|
|
|
if err != nil {
|
2023-05-03 15:36:10 +03:00
|
|
|
lg.Unexpected(lg.FromContext(ctx), err)
|
2021-02-22 10:37:00 +03:00
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
|
2023-04-19 08:28:09 +03:00
|
|
|
if !c.onlySQL || isSQL {
|
2023-05-19 17:24:18 +03:00
|
|
|
activeSrcTables, err = getTableNamesForHandle(ctx, ru, activeSrc.Handle)
|
2023-04-19 08:28:09 +03:00
|
|
|
if err != nil {
|
|
|
|
// This can happen if the active source is offline.
|
|
|
|
// Log the error, but continue below, because we still want to
|
|
|
|
// list the handles.
|
2023-05-03 15:36:10 +03:00
|
|
|
lg.FromContext(ctx).Warn("completion: failed to get table metadata from active source",
|
2023-04-19 08:28:09 +03:00
|
|
|
lga.Err, err, lga.Src, activeSrc)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, table := range activeSrcTables {
|
|
|
|
suggestions = append(suggestions, "."+table)
|
|
|
|
}
|
2021-02-22 10:37:00 +03:00
|
|
|
}
|
|
|
|
|
2023-05-19 17:24:18 +03:00
|
|
|
for _, src := range ru.Config.Collection.Sources() {
|
2021-02-22 10:37:00 +03:00
|
|
|
if c.onlySQL {
|
2023-11-19 03:05:48 +03:00
|
|
|
isSQL, err := isSQLDriver(ru, src.Handle)
|
2021-02-22 10:37:00 +03:00
|
|
|
if err != nil {
|
2023-05-03 15:36:10 +03:00
|
|
|
lg.Unexpected(lg.FromContext(ctx), err)
|
2021-02-22 10:37:00 +03:00
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
if !isSQL {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
suggestions = append(suggestions, src.Handle)
|
|
|
|
}
|
|
|
|
|
|
|
|
return suggestions, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
|
|
|
|
}
|
|
|
|
|
2023-11-19 03:05:48 +03:00
|
|
|
func isSQLDriver(ru *run.Run, handle string) (bool, error) {
|
2023-05-19 17:24:18 +03:00
|
|
|
src, err := ru.Config.Collection.Get(handle)
|
2021-02-22 10:37:00 +03:00
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
2023-05-19 17:24:18 +03:00
|
|
|
driver, err := ru.DriverRegistry.DriverFor(src.Type)
|
2021-02-22 10:37:00 +03:00
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return driver.DriverMetadata().IsSQL, nil
|
|
|
|
}
|
|
|
|
|
2023-05-19 17:24:18 +03:00
|
|
|
func getTableNamesForHandle(ctx context.Context, ru *run.Run, handle string) ([]string, error) {
|
|
|
|
src, err := ru.Config.Collection.Get(handle)
|
2021-02-22 10:37:00 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-01-15 04:45:34 +03:00
|
|
|
grip, err := ru.Grips.Open(ctx, src)
|
2021-02-22 10:37:00 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-11-19 03:05:48 +03:00
|
|
|
// TODO: We shouldn't have to load the full metadata just to get
|
|
|
|
// the table names. driver.SQLDriver should have a method ListTables.
|
2024-01-15 04:45:34 +03:00
|
|
|
md, err := grip.SourceMetadata(ctx, false)
|
2021-02-22 10:37:00 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return md.TableNames(), nil
|
|
|
|
}
|