github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/interactive/interactive_client.go (about)

     1  package interactive
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"log"
     9  	"os"
    10  	"os/signal"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/alecthomas/chroma/formatters"
    16  	"github.com/alecthomas/chroma/lexers"
    17  	"github.com/alecthomas/chroma/styles"
    18  	"github.com/c-bata/go-prompt"
    19  	"github.com/jackc/pgx/v5/pgconn"
    20  	"github.com/spf13/viper"
    21  	"github.com/turbot/go-kit/helpers"
    22  	"github.com/turbot/steampipe/pkg/cmdconfig"
    23  	"github.com/turbot/steampipe/pkg/connection_sync"
    24  	"github.com/turbot/steampipe/pkg/constants"
    25  	"github.com/turbot/steampipe/pkg/db/db_common"
    26  	"github.com/turbot/steampipe/pkg/display"
    27  	"github.com/turbot/steampipe/pkg/error_helpers"
    28  	"github.com/turbot/steampipe/pkg/interactive/metaquery"
    29  	"github.com/turbot/steampipe/pkg/query"
    30  	"github.com/turbot/steampipe/pkg/query/queryhistory"
    31  	"github.com/turbot/steampipe/pkg/statushooks"
    32  	"github.com/turbot/steampipe/pkg/steampipeconfig"
    33  	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
    34  	"github.com/turbot/steampipe/pkg/utils"
    35  	"github.com/turbot/steampipe/pkg/version"
    36  )
    37  
    38  type AfterPromptCloseAction int
    39  
    40  const (
    41  	AfterPromptCloseExit AfterPromptCloseAction = iota
    42  	AfterPromptCloseRestart
    43  )
    44  
    45  // InteractiveClient is a wrapper over a LocalClient and a Prompt to facilitate interactive query prompt
    46  type InteractiveClient struct {
    47  	initData                *query.InitData
    48  	promptResult            *RunInteractivePromptResult
    49  	interactiveBuffer       []string
    50  	interactivePrompt       *prompt.Prompt
    51  	interactiveQueryHistory *queryhistory.QueryHistory
    52  	autocompleteOnEmpty     bool
    53  	// the cancellation function for the active query - may be nil
    54  	// NOTE: should ONLY be called by cancelActiveQueryIfAny
    55  	cancelActiveQuery context.CancelFunc
    56  	cancelPrompt      context.CancelFunc
    57  
    58  	// channel used internally to pass the initialisation result
    59  	initResultChan chan *db_common.InitResult
    60  	// flag set when initialisation is complete (with or without errors)
    61  	initialisationComplete bool
    62  	afterClose             AfterPromptCloseAction
    63  	// lock while execution is occurring to avoid errors/warnings being shown
    64  	executionLock sync.Mutex
    65  	// the schema metadata - this is loaded asynchronously during init
    66  	schemaMetadata *db_common.SchemaMetadata
    67  	highlighter    *Highlighter
    68  	// hidePrompt is used to render a blank as the prompt prefix
    69  	hidePrompt bool
    70  
    71  	suggestions *autoCompleteSuggestions
    72  }
    73  
    74  func getHighlighter(theme string) *Highlighter {
    75  	return newHighlighter(
    76  		lexers.Get("sql"),
    77  		formatters.Get("terminal256"),
    78  		styles.Native,
    79  	)
    80  }
    81  
    82  func newInteractiveClient(ctx context.Context, initData *query.InitData, result *RunInteractivePromptResult) (*InteractiveClient, error) {
    83  	interactiveQueryHistory, err := queryhistory.New()
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  	c := &InteractiveClient{
    88  		initData:                initData,
    89  		promptResult:            result,
    90  		interactiveQueryHistory: interactiveQueryHistory,
    91  		interactiveBuffer:       []string{},
    92  		autocompleteOnEmpty:     false,
    93  		initResultChan:          make(chan *db_common.InitResult, 1),
    94  		highlighter:             getHighlighter(viper.GetString(constants.ArgTheme)),
    95  		suggestions:             newAutocompleteSuggestions(),
    96  	}
    97  
    98  	// asynchronously wait for init to complete
    99  	// we start this immediately rather than lazy loading as we want to handle errors asap
   100  	go c.readInitDataStream(ctx)
   101  
   102  	return c, nil
   103  }
   104  
   105  // InteractivePrompt starts an interactive prompt and return
   106  func (c *InteractiveClient) InteractivePrompt(parentContext context.Context) {
   107  	// start a cancel handler for the interactive client - this will call activeQueryCancelFunc if it is set
   108  	// (registered when we call createQueryContext)
   109  	quitChannel := c.startCancelHandler()
   110  
   111  	// create a cancel context for the prompt - this will set c.cancelPrompt
   112  	ctx := c.createPromptContext(parentContext)
   113  
   114  	defer func() {
   115  		if r := recover(); r != nil {
   116  			error_helpers.ShowError(ctx, helpers.ToError(r))
   117  		}
   118  		// close up the SIGINT channel so that the receiver goroutine can quit
   119  		quitChannel <- true
   120  		close(quitChannel)
   121  
   122  		// cleanup the init data to ensure any services we started are stopped
   123  		c.initData.Cleanup(ctx)
   124  
   125  		// close the result stream
   126  		// this needs to be the last thing we do,
   127  		// as the query result display code will exit once the result stream is closed
   128  		c.promptResult.Streamer.Close()
   129  	}()
   130  
   131  	statushooks.Message(
   132  		ctx,
   133  		fmt.Sprintf("Welcome to Steampipe v%s", version.SteampipeVersion.String()),
   134  		fmt.Sprintf("For more information, type %s", constants.Bold(".help")),
   135  	)
   136  
   137  	// run the prompt in a goroutine, so we can also detect async initialisation errors
   138  	promptResultChan := make(chan struct{}, 1)
   139  	c.runInteractivePromptAsync(ctx, promptResultChan)
   140  
   141  	// select results
   142  	for {
   143  		select {
   144  		case initResult := <-c.initResultChan:
   145  			c.handleInitResult(ctx, initResult)
   146  			// if there was an error, handleInitResult will shut down the prompt
   147  			// - we must wait for it to shut down and not return immediately
   148  
   149  		case <-promptResultChan:
   150  			// persist saved history
   151  			//nolint:golint,errcheck // worst case is history is not persisted - not a failure
   152  			c.interactiveQueryHistory.Persist()
   153  			// check post-close action
   154  			if c.afterClose == AfterPromptCloseExit {
   155  				// clear prompt so any messages/warnings can be displayed without the prompt
   156  				c.hidePrompt = true
   157  				c.interactivePrompt.ClearLine()
   158  				return
   159  			}
   160  			// create new context with a cancellation func
   161  			ctx = c.createPromptContext(parentContext)
   162  			// now run it again
   163  			c.runInteractivePromptAsync(ctx, promptResultChan)
   164  		}
   165  	}
   166  }
   167  
   168  // ClosePrompt cancels the running prompt, setting the action to take after close
   169  func (c *InteractiveClient) ClosePrompt(afterClose AfterPromptCloseAction) {
   170  	c.afterClose = afterClose
   171  	c.cancelPrompt()
   172  }
   173  
   174  // retrieve both the raw query result and a sanitised version in list form
   175  func (c *InteractiveClient) loadSchema() error {
   176  	utils.LogTime("db_client.loadSchema start")
   177  	defer utils.LogTime("db_client.loadSchema end")
   178  
   179  	// load these schemas
   180  	// in a background context, since we are not running in a context - but GetSchemaFromDB needs one
   181  	metadata, err := c.client().GetSchemaFromDB(context.Background())
   182  	if err != nil {
   183  		return fmt.Errorf("failed to load schemas: %s", err.Error())
   184  	}
   185  
   186  	c.schemaMetadata = metadata
   187  	return nil
   188  }
   189  
   190  func (c *InteractiveClient) runInteractivePromptAsync(ctx context.Context, promptResultChan chan struct{}) {
   191  	go func() {
   192  		c.runInteractivePrompt(ctx)
   193  		promptResultChan <- struct{}{}
   194  	}()
   195  }
   196  
   197  func (c *InteractiveClient) runInteractivePrompt(ctx context.Context) {
   198  	defer func() {
   199  		// this is to catch the PANIC that gets raised by
   200  		// the executor of go-prompt
   201  		//
   202  		// We need to do it this way, since there is no
   203  		// clean way to reload go-prompt so that we can
   204  		// populate the history stack
   205  		//
   206  		if r := recover(); r != nil {
   207  			// show the panic and restart the prompt
   208  			error_helpers.ShowError(ctx, helpers.ToError(r))
   209  			c.afterClose = AfterPromptCloseRestart
   210  			c.hidePrompt = false
   211  			return
   212  		}
   213  	}()
   214  
   215  	callExecutor := func(line string) {
   216  		c.executor(ctx, line)
   217  	}
   218  	completer := func(d prompt.Document) []prompt.Suggest {
   219  		return c.queryCompleter(d)
   220  	}
   221  	c.interactivePrompt = prompt.New(
   222  		callExecutor,
   223  		completer,
   224  		prompt.OptionTitle("steampipe interactive client "),
   225  		prompt.OptionLivePrefix(func() (prefix string, useLive bool) {
   226  			prefix = "> "
   227  			useLive = true
   228  			if len(c.interactiveBuffer) > 0 {
   229  				prefix = ">>  "
   230  			}
   231  			if c.hidePrompt {
   232  				prefix = ""
   233  			}
   234  			return
   235  		}),
   236  		prompt.OptionFormatter(c.highlighter.Highlight),
   237  		prompt.OptionHistory(c.interactiveQueryHistory.Get()),
   238  		prompt.OptionInputTextColor(prompt.DefaultColor),
   239  		prompt.OptionPrefixTextColor(prompt.DefaultColor),
   240  		prompt.OptionMaxSuggestion(20),
   241  		// Known Key Bindings
   242  		prompt.OptionAddKeyBind(prompt.KeyBind{
   243  			Key: prompt.ControlC,
   244  			Fn:  func(b *prompt.Buffer) { c.breakMultilinePrompt(b) },
   245  		}),
   246  		prompt.OptionAddKeyBind(prompt.KeyBind{
   247  			Key: prompt.ControlD,
   248  			Fn: func(b *prompt.Buffer) {
   249  				if b.Text() == "" {
   250  					c.ClosePrompt(AfterPromptCloseExit)
   251  				}
   252  			},
   253  		}),
   254  		prompt.OptionAddKeyBind(prompt.KeyBind{
   255  			Key: prompt.Tab,
   256  			Fn: func(b *prompt.Buffer) {
   257  				if len(b.Text()) == 0 {
   258  					c.autocompleteOnEmpty = true
   259  				} else {
   260  					c.autocompleteOnEmpty = false
   261  				}
   262  			},
   263  		}),
   264  		prompt.OptionAddKeyBind(prompt.KeyBind{
   265  			Key: prompt.Escape,
   266  			Fn: func(b *prompt.Buffer) {
   267  				if len(b.Text()) == 0 {
   268  					c.autocompleteOnEmpty = false
   269  				}
   270  			},
   271  		}),
   272  		prompt.OptionAddKeyBind(prompt.KeyBind{
   273  			Key: prompt.ShiftLeft,
   274  			Fn:  prompt.GoLeftChar,
   275  		}),
   276  		prompt.OptionAddKeyBind(prompt.KeyBind{
   277  			Key: prompt.ShiftRight,
   278  			Fn:  prompt.GoRightChar,
   279  		}),
   280  		prompt.OptionAddKeyBind(prompt.KeyBind{
   281  			Key: prompt.ShiftUp,
   282  			Fn:  func(b *prompt.Buffer) { /*ignore*/ },
   283  		}),
   284  		prompt.OptionAddKeyBind(prompt.KeyBind{
   285  			Key: prompt.ShiftDown,
   286  			Fn:  func(b *prompt.Buffer) { /*ignore*/ },
   287  		}),
   288  		// Opt+LeftArrow
   289  		prompt.OptionAddASCIICodeBind(prompt.ASCIICodeBind{
   290  			ASCIICode: constants.OptLeftArrowASCIICode,
   291  			Fn:        prompt.GoLeftWord,
   292  		}),
   293  		// Opt+RightArrow
   294  		prompt.OptionAddASCIICodeBind(prompt.ASCIICodeBind{
   295  			ASCIICode: constants.OptRightArrowASCIICode,
   296  			Fn:        prompt.GoRightWord,
   297  		}),
   298  		// Alt+LeftArrow
   299  		prompt.OptionAddASCIICodeBind(prompt.ASCIICodeBind{
   300  			ASCIICode: constants.AltLeftArrowASCIICode,
   301  			Fn:        prompt.GoLeftWord,
   302  		}),
   303  		// Alt+RightArrow
   304  		prompt.OptionAddASCIICodeBind(prompt.ASCIICodeBind{
   305  			ASCIICode: constants.AltRightArrowASCIICode,
   306  			Fn:        prompt.GoRightWord,
   307  		}),
   308  		prompt.OptionBufferPreHook(func(input string) (modifiedInput string, ignore bool) {
   309  			// if this is not WSL, return as-is
   310  			if !utils.IsWSL() {
   311  				return input, false
   312  			}
   313  			return cleanBufferForWSL(input)
   314  		}),
   315  	)
   316  	// set this to a default
   317  	c.autocompleteOnEmpty = false
   318  	c.interactivePrompt.RunCtx(ctx)
   319  
   320  	return
   321  }
   322  
   323  func cleanBufferForWSL(s string) (string, bool) {
   324  	b := []byte(s)
   325  	// in WSL, 'Alt' combo-characters are denoted by [27, ASCII of character]
   326  	// if we get a combination which has 27 as prefix - we should ignore it
   327  	// this is inline with other interactive clients like pgcli
   328  	if len(b) > 1 && bytes.HasPrefix(b, []byte{byte(27)}) {
   329  		// ignore it
   330  		return "", true
   331  	}
   332  	return string(b), false
   333  }
   334  
   335  func (c *InteractiveClient) breakMultilinePrompt(buffer *prompt.Buffer) {
   336  	c.interactiveBuffer = []string{}
   337  }
   338  
   339  func (c *InteractiveClient) executor(ctx context.Context, line string) {
   340  	// take an execution lock, so that errors and warnings don't show up while
   341  	// we are underway
   342  	c.executionLock.Lock()
   343  	defer c.executionLock.Unlock()
   344  
   345  	// set afterClose to restart - is we are exiting the metaquery will set this to AfterPromptCloseExit
   346  	c.afterClose = AfterPromptCloseRestart
   347  
   348  	line = strings.TrimSpace(line)
   349  
   350  	resolvedQuery := c.getQuery(ctx, line)
   351  	if resolvedQuery == nil {
   352  		// we failed to resolve a query, or are in the middle of a multi-line entry
   353  		// restart the prompt, DO NOT clear the interactive buffer
   354  		c.restartInteractiveSession()
   355  		return
   356  	}
   357  
   358  	// we successfully retrieved a query
   359  
   360  	// create a  context for the execution of the query
   361  	queryCtx := c.createQueryContext(ctx)
   362  
   363  	if resolvedQuery.IsMetaQuery {
   364  		c.hidePrompt = true
   365  		c.interactivePrompt.Render()
   366  
   367  		if err := c.executeMetaquery(queryCtx, resolvedQuery.ExecuteSQL); err != nil {
   368  			error_helpers.ShowError(ctx, err)
   369  		}
   370  		c.hidePrompt = false
   371  
   372  		// cancel the context
   373  		c.cancelActiveQueryIfAny()
   374  	} else {
   375  		statushooks.Show(ctx)
   376  		defer statushooks.Done(ctx)
   377  		statushooks.SetStatus(ctx, "Executing query…")
   378  		// otherwise execute query
   379  		c.executeQuery(ctx, queryCtx, resolvedQuery)
   380  	}
   381  
   382  	// restart the prompt
   383  	c.restartInteractiveSession()
   384  }
   385  
   386  func (c *InteractiveClient) executeQuery(ctx context.Context, queryCtx context.Context, resolvedQuery *modconfig.ResolvedQuery) {
   387  	// if there is a custom search path, wait until the first connection of each plugin has loaded
   388  	if customSearchPath := c.client().GetCustomSearchPath(); customSearchPath != nil {
   389  		if err := connection_sync.WaitForSearchPathSchemas(ctx, c.client(), customSearchPath); err != nil {
   390  			error_helpers.ShowError(ctx, err)
   391  			return
   392  		}
   393  	}
   394  
   395  	t := time.Now()
   396  	result, err := c.client().Execute(queryCtx, resolvedQuery.ExecuteSQL, resolvedQuery.Args...)
   397  	if err != nil {
   398  		error_helpers.ShowError(ctx, error_helpers.HandleCancelError(err))
   399  		// if timing flag is enabled, show the time taken for the query to fail
   400  		if cmdconfig.Viper().GetString(constants.ArgTiming) != constants.ArgOff {
   401  			display.DisplayErrorTiming(t)
   402  		}
   403  	} else {
   404  		c.promptResult.Streamer.StreamResult(result)
   405  	}
   406  }
   407  
   408  func (c *InteractiveClient) getQuery(ctx context.Context, line string) *modconfig.ResolvedQuery {
   409  	// if it's an empty line, then we don't need to do anything
   410  	if line == "" {
   411  		return nil
   412  	}
   413  
   414  	// store the history (the raw line which was entered)
   415  	historyEntry := line
   416  	defer func() {
   417  		if len(historyEntry) > 0 {
   418  			// we want to store even if we fail to resolve a query
   419  			c.interactiveQueryHistory.Push(historyEntry)
   420  		}
   421  
   422  	}()
   423  
   424  	// wait for initialisation to complete so we can access the workspace
   425  	if !c.isInitialised() {
   426  		// create a context used purely to detect cancellation during initialisation
   427  		// this will also set c.cancelActiveQuery
   428  		queryCtx := c.createQueryContext(ctx)
   429  		defer func() {
   430  			// cancel this context
   431  			c.cancelActiveQueryIfAny()
   432  		}()
   433  
   434  		// show the spinner here while we wait for initialization to complete
   435  		statushooks.Show(ctx)
   436  		// wait for client initialisation to complete
   437  		err := c.waitForInitData(queryCtx)
   438  		statushooks.Done(ctx)
   439  		if err != nil {
   440  			// clear history entry
   441  			historyEntry = ""
   442  			// clear the interactive buffer
   443  			c.interactiveBuffer = nil
   444  			// error will have been handled elsewhere
   445  			return nil
   446  		}
   447  	}
   448  
   449  	// push the current line into the buffer
   450  	c.interactiveBuffer = append(c.interactiveBuffer, line)
   451  
   452  	// expand the buffer out into 'query'
   453  	queryString := strings.Join(c.interactiveBuffer, "\n")
   454  
   455  	// check if the contents in the buffer evaluates to a metaquery
   456  	if metaquery.IsMetaQuery(line) {
   457  		// this is a metaquery
   458  		// clear the interactive buffer
   459  		c.interactiveBuffer = nil
   460  		return &modconfig.ResolvedQuery{
   461  			ExecuteSQL:  line,
   462  			IsMetaQuery: true,
   463  		}
   464  	}
   465  
   466  	// in case of a named query call with params, parse the where clause
   467  	resolvedQuery, queryProvider, err := c.workspace().ResolveQueryAndArgsFromSQLString(queryString)
   468  	if err != nil {
   469  		// if we fail to resolve:
   470  		// - show error but do not return it so we  stay in the prompt
   471  		// - do not clear history item - we want to store bad entry in history
   472  		// - clear interactive buffer
   473  		c.interactiveBuffer = nil
   474  		error_helpers.ShowError(ctx, err)
   475  		return nil
   476  	}
   477  	isNamedQuery := queryProvider != nil
   478  
   479  	// should we execute?
   480  	// we will NOT execute if we are in multiline mode, there is no semi-colon
   481  	// and it is NOT a metaquery or a named query
   482  	if !c.shouldExecute(queryString, isNamedQuery) {
   483  		// is we are not executing, do not store history
   484  		historyEntry = ""
   485  		// do not clear interactive buffer
   486  		return nil
   487  	}
   488  
   489  	// so we need to execute
   490  	// clear the interactive buffer
   491  	c.interactiveBuffer = nil
   492  
   493  	// what are we executing?
   494  
   495  	// if the line is ONLY a semicolon, do nothing and restart interactive session
   496  	if strings.TrimSpace(resolvedQuery.ExecuteSQL) == ";" {
   497  		// do not store in history
   498  		historyEntry = ""
   499  		c.restartInteractiveSession()
   500  		return nil
   501  	}
   502  	// if this is a multiline query, update history entry
   503  	if !isNamedQuery && len(strings.Split(resolvedQuery.ExecuteSQL, "\n")) > 1 {
   504  		historyEntry = resolvedQuery.ExecuteSQL
   505  	}
   506  
   507  	return resolvedQuery
   508  }
   509  
   510  func (c *InteractiveClient) executeMetaquery(ctx context.Context, query string) error {
   511  	// the client must be initialised to get here
   512  	if !c.isInitialised() {
   513  		panic("client is not initalised")
   514  	}
   515  	// validate the metaquery arguments
   516  	validateResult := metaquery.Validate(query)
   517  	if validateResult.Message != "" {
   518  		fmt.Println(validateResult.Message)
   519  	}
   520  	if err := validateResult.Err; err != nil {
   521  		return err
   522  	}
   523  	if !validateResult.ShouldRun {
   524  		return nil
   525  	}
   526  	client := c.client()
   527  
   528  	// validation passed, now we will run
   529  	return metaquery.Handle(ctx, &metaquery.HandlerInput{
   530  		Query:                 query,
   531  		Client:                client,
   532  		Schema:                c.schemaMetadata,
   533  		SearchPath:            client.GetRequiredSessionSearchPath(),
   534  		Prompt:                c.interactivePrompt,
   535  		ClosePrompt:           func() { c.afterClose = AfterPromptCloseExit },
   536  		GetConnectionStateMap: c.getConnectionState,
   537  	})
   538  }
   539  
   540  // helper function to acquire db connection and retrieve connection state
   541  func (c *InteractiveClient) getConnectionState(ctx context.Context) (steampipeconfig.ConnectionStateMap, error) {
   542  	statushooks.Show(ctx)
   543  	defer statushooks.Done(ctx)
   544  
   545  	statushooks.SetStatus(ctx, "Loading connection state…")
   546  
   547  	conn, err := c.client().AcquireManagementConnection(ctx)
   548  	if err != nil {
   549  		return nil, err
   550  	}
   551  	defer conn.Release()
   552  	return steampipeconfig.LoadConnectionState(ctx, conn.Conn(), steampipeconfig.WithWaitUntilLoading())
   553  }
   554  
   555  func (c *InteractiveClient) restartInteractiveSession() {
   556  	// restart the prompt
   557  	c.ClosePrompt(c.afterClose)
   558  }
   559  
   560  func (c *InteractiveClient) shouldExecute(line string, namedQuery bool) bool {
   561  	if namedQuery {
   562  		// execute named queries with no ';' even in multiline mode
   563  		return true
   564  	}
   565  	if !cmdconfig.Viper().GetBool(constants.ArgMultiLine) {
   566  		// NOT multiline mode
   567  		return true
   568  	}
   569  	if metaquery.IsMetaQuery(line) {
   570  		// execute metaqueries with no ';' even in multiline mode
   571  		return true
   572  	}
   573  	if strings.HasSuffix(line, ";") {
   574  		// statement has terminating ';'
   575  		return true
   576  	}
   577  
   578  	return false
   579  }
   580  
   581  func (c *InteractiveClient) queryCompleter(d prompt.Document) []prompt.Suggest {
   582  	if !cmdconfig.Viper().GetBool(constants.ArgAutoComplete) {
   583  		return nil
   584  	}
   585  	if !c.isInitialised() {
   586  		return nil
   587  	}
   588  
   589  	text := strings.TrimLeft(strings.ToLower(d.CurrentLine()), " ")
   590  	if len(text) == 0 && !c.autocompleteOnEmpty {
   591  		// if nothing has been typed yet, no point
   592  		// giving suggestions
   593  		return nil
   594  	}
   595  
   596  	var s []prompt.Suggest
   597  
   598  	switch {
   599  	case isFirstWord(text):
   600  		suggestions := c.getFirstWordSuggestions(text)
   601  		s = append(s, suggestions...)
   602  	case metaquery.IsMetaQuery(text):
   603  		suggestions := metaquery.Complete(&metaquery.CompleterInput{
   604  			Query:            text,
   605  			TableSuggestions: c.getTableAndConnectionSuggestions(lastWord(text)),
   606  		})
   607  		s = append(s, suggestions...)
   608  	default:
   609  		if queryInfo := getQueryInfo(text); queryInfo.EditingTable {
   610  			tableSuggestions := c.getTableAndConnectionSuggestions(lastWord(text))
   611  			s = append(s, tableSuggestions...)
   612  		}
   613  	}
   614  
   615  	return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
   616  }
   617  
   618  func (c *InteractiveClient) getFirstWordSuggestions(word string) []prompt.Suggest {
   619  	var querySuggestions []prompt.Suggest
   620  	// if this a qualified query try to extract connection
   621  	parts := strings.Split(word, ".")
   622  	if len(parts) > 1 {
   623  		// if first word is a mod name we know about, return appropriate suggestions
   624  		modName := strings.TrimSpace(parts[0])
   625  		if modQueries, isMod := c.suggestions.queriesByMod[modName]; isMod {
   626  			querySuggestions = modQueries
   627  		} else {
   628  			//  otherwise return mods names and unqualified queries
   629  			//nolint:golint,gocritic // we want this to go into a different slice
   630  			querySuggestions = append(c.suggestions.mods, c.suggestions.unqualifiedQueries...)
   631  		}
   632  	}
   633  
   634  	var s []prompt.Suggest
   635  	// add all we know that can be the first words
   636  	// named queries
   637  	s = append(s, querySuggestions...)
   638  	// "select", "with"
   639  	s = append(s, prompt.Suggest{Text: "select", Output: "select"}, prompt.Suggest{Text: "with", Output: "with"})
   640  	// metaqueries
   641  	s = append(s, metaquery.PromptSuggestions()...)
   642  	return s
   643  }
   644  
   645  func (c *InteractiveClient) getTableAndConnectionSuggestions(word string) []prompt.Suggest {
   646  	// try to extract connection
   647  	parts := strings.SplitN(word, ".", 2)
   648  	if len(parts) == 1 {
   649  		// no connection, just return schemas and unqualified tables
   650  		return append(c.suggestions.schemas, c.suggestions.unqualifiedTables...)
   651  	}
   652  
   653  	connection := strings.TrimSpace(parts[0])
   654  	t := c.suggestions.tablesBySchema[connection]
   655  	return t
   656  }
   657  
   658  func (c *InteractiveClient) newSuggestion(itemType string, description string, name string) prompt.Suggest {
   659  	if description != "" {
   660  		itemType += fmt.Sprintf(": %s", description)
   661  	}
   662  	return prompt.Suggest{Text: name, Output: name, Description: itemType}
   663  }
   664  
   665  func (c *InteractiveClient) startCancelHandler() chan bool {
   666  	sigIntChannel := make(chan os.Signal, 1)
   667  	quitChannel := make(chan bool, 1)
   668  	signal.Notify(sigIntChannel, os.Interrupt)
   669  	go func() {
   670  		for {
   671  			select {
   672  			case <-sigIntChannel:
   673  				log.Println("[INFO] interactive client cancel handler got SIGINT")
   674  				// if initialisation is not complete, just close the prompt
   675  				// this will cancel the context used for initialisation so cancel any initialisation queries
   676  				if !c.isInitialised() {
   677  					c.ClosePrompt(AfterPromptCloseExit)
   678  					return
   679  				} else {
   680  					// otherwise call cancelActiveQueryIfAny which the for the active query, if there is one
   681  					c.cancelActiveQueryIfAny()
   682  					// keep waiting for further cancellations
   683  				}
   684  			case <-quitChannel:
   685  				log.Println("[INFO] cancel handler exiting")
   686  				c.cancelActiveQueryIfAny()
   687  				// we're done
   688  				return
   689  			}
   690  		}
   691  	}()
   692  	return quitChannel
   693  }
   694  
   695  func (c *InteractiveClient) listenToPgNotifications(ctx context.Context) {
   696  	c.initData.Client.RegisterNotificationListener(func(notification *pgconn.Notification) {
   697  		c.handlePostgresNotification(ctx, notification)
   698  	})
   699  }
   700  
   701  func (c *InteractiveClient) handlePostgresNotification(ctx context.Context, notification *pgconn.Notification) {
   702  	if notification == nil {
   703  		return
   704  	}
   705  	n := &steampipeconfig.PostgresNotification{}
   706  	err := json.Unmarshal([]byte(notification.Payload), n)
   707  	if err != nil {
   708  		log.Printf("[WARN] Error unmarshalling notification: %s", err)
   709  		return
   710  	}
   711  	switch n.Type {
   712  	case steampipeconfig.PgNotificationSchemaUpdate:
   713  		c.handleConnectionUpdateNotification(ctx)
   714  	case steampipeconfig.PgNotificationConnectionError:
   715  		// unmarshal the notification again, into the correct type
   716  		errorNotification := &steampipeconfig.ErrorsAndWarningsNotification{}
   717  		if err := json.Unmarshal([]byte(notification.Payload), errorNotification); err != nil {
   718  			log.Printf("[WARN] Error unmarshalling notification: %s", err)
   719  			return
   720  		}
   721  		c.handleErrorsAndWarningsNotification(ctx, errorNotification)
   722  	}
   723  }
   724  
   725  func (c *InteractiveClient) handleErrorsAndWarningsNotification(ctx context.Context, notification *steampipeconfig.ErrorsAndWarningsNotification) {
   726  	log.Printf("[TRACE] handleErrorsAndWarningsNotification")
   727  	output := viper.Get(constants.ArgOutput)
   728  	if output == constants.OutputFormatJSON || output == constants.OutputFormatCSV {
   729  		return
   730  	}
   731  
   732  	c.showMessages(ctx, func() {
   733  		for _, m := range append(notification.Errors, notification.Warnings...) {
   734  			error_helpers.ShowWarning(m)
   735  		}
   736  	})
   737  
   738  }
   739  func (c *InteractiveClient) handleConnectionUpdateNotification(ctx context.Context) {
   740  	// ignore schema update notifications until initialisation is complete
   741  	// (we may receive schema update messages from the initial refresh connections, but we do not need to reload
   742  	// the schema as we will have already loaded the correct schema)
   743  	if !c.initialisationComplete {
   744  		log.Printf("[INFO] received schema update notification but ignoring it as we are initializing")
   745  		return
   746  	}
   747  
   748  	// at present, we do not actually use the payload, we just do a brute force reload
   749  	// as an optimization we could look at the updates and only reload the required schemas
   750  
   751  	log.Printf("[INFO] handleConnectionUpdateNotification")
   752  
   753  	// first load user search path
   754  	if err := c.client().LoadUserSearchPath(ctx); err != nil {
   755  		log.Printf("[WARN] Error in handleConnectionUpdateNotification when loading foreign user search path: %s", err.Error())
   756  		return
   757  	}
   758  
   759  	//  reload schema
   760  	if err := c.loadSchema(); err != nil {
   761  		log.Printf("[WARN] Error unmarshalling notification: %s", err)
   762  		return
   763  	}
   764  
   765  	// reinitialise autocomplete suggestions
   766  
   767  	if err := c.initialiseSuggestions(ctx); err != nil {
   768  		log.Printf("[WARN] failed to initialise suggestions: %s", err)
   769  	}
   770  
   771  	// refresh the db session inside an execution lock
   772  	// we do this to avoid the postgres `cached plan must not change result type`` error
   773  	c.executionLock.Lock()
   774  	defer c.executionLock.Unlock()
   775  
   776  	// refresh all connections in the pool - since the search path may have changed
   777  	c.client().ResetPools(ctx)
   778  }