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

     1  package interactive
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"github.com/spf13/viper"
     7  	"log"
     8  	"strings"
     9  
    10  	"github.com/c-bata/go-prompt"
    11  	"github.com/turbot/go-kit/helpers"
    12  	"github.com/turbot/steampipe/pkg/constants"
    13  	"github.com/turbot/steampipe/pkg/db/db_common"
    14  	"github.com/turbot/steampipe/pkg/steampipeconfig"
    15  	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
    16  	"github.com/turbot/steampipe/pkg/utils"
    17  )
    18  
    19  func (c *InteractiveClient) initialiseSuggestions(ctx context.Context) error {
    20  	log.Printf("[TRACE] initialiseSuggestions")
    21  
    22  	conn, err := c.client().AcquireManagementConnection(ctx)
    23  	if err != nil {
    24  		return err
    25  	}
    26  	defer conn.Release()
    27  
    28  	connectionStateMap, err := steampipeconfig.LoadConnectionState(ctx, conn.Conn(), steampipeconfig.WithWaitUntilLoading())
    29  	if err != nil {
    30  		c.initialiseSuggestionsLegacy()
    31  		//nolint:golint,nilerr // valid condition - not an error
    32  		return nil
    33  	}
    34  
    35  	// reset suggestions
    36  	c.suggestions = newAutocompleteSuggestions()
    37  	c.initialiseSchemaAndTableSuggestions(connectionStateMap)
    38  	c.initialiseQuerySuggestions()
    39  	c.suggestions.sort()
    40  	return nil
    41  }
    42  
    43  // initialiseSchemaAndTableSuggestions build a list of schema and table querySuggestions
    44  func (c *InteractiveClient) initialiseSchemaAndTableSuggestions(connectionStateMap steampipeconfig.ConnectionStateMap) {
    45  	if c.schemaMetadata == nil {
    46  		return
    47  	}
    48  
    49  	// unqualified table names
    50  	// use lookup to avoid dupes from dynamic plugins
    51  	// (this is needed as GetFirstSearchPathConnectionForPlugins will return ALL dynamic connections)
    52  	var unqualifiedTablesToAdd = getIntrospectionTableSuggestions()
    53  
    54  	// add connection state and rate limit
    55  	unqualifiedTablesToAdd[constants.ConnectionTable] = struct{}{}
    56  	unqualifiedTablesToAdd[constants.PluginInstanceTable] = struct{}{}
    57  	unqualifiedTablesToAdd[constants.RateLimiterDefinitionTable] = struct{}{}
    58  	unqualifiedTablesToAdd[constants.PluginColumnTable] = struct{}{}
    59  	unqualifiedTablesToAdd[constants.ServerSettingsTable] = struct{}{}
    60  
    61  	// get the first search path connection for each plugin
    62  	firstConnectionPerPlugin := connectionStateMap.GetFirstSearchPathConnectionForPlugins(c.client().GetRequiredSessionSearchPath())
    63  	firstConnectionPerPluginLookup := utils.SliceToLookup(firstConnectionPerPlugin)
    64  	// NOTE: add temporary schema into firstConnectionPerPluginLookup
    65  	// as we want to add unqualified tables from there into autocomplete
    66  	firstConnectionPerPluginLookup[c.schemaMetadata.TemporarySchemaName] = struct{}{}
    67  
    68  	for schemaName, schemaDetails := range c.schemaMetadata.Schemas {
    69  		if connectionState, found := connectionStateMap[schemaName]; found && connectionState.State != constants.ConnectionStateReady {
    70  			log.Println("[TRACE] could not find schema in state map or connection is not Ready", schemaName)
    71  			continue
    72  		}
    73  
    74  		// fully qualified table names
    75  		var qualifiedTablesToAdd []prompt.Suggest
    76  
    77  		isTemporarySchema := schemaName == c.schemaMetadata.TemporarySchemaName
    78  		if !isTemporarySchema {
    79  			// add the schema into the list of schema
    80  			// we don't need to escape schema names, since schema names are derived from connection names
    81  			// which are validated so that we don't end up with names which need it
    82  			c.suggestions.schemas = append(c.suggestions.schemas, prompt.Suggest{Text: schemaName, Description: "Schema", Output: schemaName})
    83  		}
    84  
    85  		// add qualified names of all tables
    86  		for tableName := range schemaDetails {
    87  			// do not add temp tables to qualified tables
    88  			if !isTemporarySchema {
    89  				qualifiedTableName := fmt.Sprintf("%s.%s", schemaName, sanitiseTableName(tableName))
    90  				qualifiedTablesToAdd = append(qualifiedTablesToAdd, prompt.Suggest{Text: qualifiedTableName, Description: "Table", Output: qualifiedTableName})
    91  			}
    92  			if _, addToUnqualified := firstConnectionPerPluginLookup[schemaName]; addToUnqualified {
    93  				unqualifiedTablesToAdd[tableName] = struct{}{}
    94  			}
    95  		}
    96  
    97  		// add qualified table to tablesBySchema
    98  		if len(qualifiedTablesToAdd) > 0 {
    99  			c.suggestions.tablesBySchema[schemaName] = qualifiedTablesToAdd
   100  		}
   101  	}
   102  
   103  	// add unqualified table suggestions
   104  	for tableName := range unqualifiedTablesToAdd {
   105  		c.suggestions.unqualifiedTables = append(c.suggestions.unqualifiedTables, prompt.Suggest{Text: tableName, Description: "Table", Output: sanitiseTableName(tableName)})
   106  	}
   107  }
   108  
   109  func getIntrospectionTableSuggestions() map[string]struct{} {
   110  	res := make(map[string]struct{})
   111  	switch strings.ToLower(viper.GetString(constants.ArgIntrospection)) {
   112  	case constants.IntrospectionInfo:
   113  		res[constants.IntrospectionTableQuery] = struct{}{}
   114  		res[constants.IntrospectionTableControl] = struct{}{}
   115  		res[constants.IntrospectionTableBenchmark] = struct{}{}
   116  		res[constants.IntrospectionTableMod] = struct{}{}
   117  		res[constants.IntrospectionTableDashboard] = struct{}{}
   118  		res[constants.IntrospectionTableDashboardContainer] = struct{}{}
   119  		res[constants.IntrospectionTableDashboardCard] = struct{}{}
   120  		res[constants.IntrospectionTableDashboardChart] = struct{}{}
   121  		res[constants.IntrospectionTableDashboardFlow] = struct{}{}
   122  		res[constants.IntrospectionTableDashboardGraph] = struct{}{}
   123  		res[constants.IntrospectionTableDashboardHierarchy] = struct{}{}
   124  		res[constants.IntrospectionTableDashboardImage] = struct{}{}
   125  		res[constants.IntrospectionTableDashboardInput] = struct{}{}
   126  		res[constants.IntrospectionTableDashboardTable] = struct{}{}
   127  		res[constants.IntrospectionTableDashboardText] = struct{}{}
   128  		res[constants.IntrospectionTableVariable] = struct{}{}
   129  		res[constants.IntrospectionTableReference] = struct{}{}
   130  	case constants.IntrospectionControl:
   131  		res[constants.IntrospectionTableControl] = struct{}{}
   132  		res[constants.IntrospectionTableBenchmark] = struct{}{}
   133  	}
   134  	return res
   135  }
   136  
   137  func (c *InteractiveClient) initialiseQuerySuggestions() {
   138  	workspaceModName := c.initData.Workspace.Mod.Name()
   139  	resourceFunc := func(item modconfig.HclResource) (continueWalking bool, err error) {
   140  		continueWalking = true
   141  
   142  		// should we include this item
   143  		qp, ok := item.(modconfig.QueryProvider)
   144  		if !ok {
   145  			return
   146  		}
   147  		if qp.GetQuery() == nil && qp.GetSQL() == nil {
   148  			return
   149  		}
   150  		rm := item.(modconfig.ResourceWithMetadata)
   151  		if rm.IsAnonymous() {
   152  			return
   153  		}
   154  		mod := qp.GetMod()
   155  		isLocal := mod.Name() == workspaceModName
   156  		itemType := item.BlockType()
   157  
   158  		// only include global inputs
   159  		if itemType == modconfig.BlockTypeInput {
   160  			if _, ok := c.initData.Workspace.Mod.ResourceMaps.GlobalDashboardInputs[item.Name()]; !ok {
   161  				return
   162  			}
   163  		}
   164  		// special case for query
   165  		if itemType == modconfig.BlockTypeQuery {
   166  			itemType = "named query"
   167  		}
   168  		if isLocal {
   169  			suggestion := c.newSuggestion(itemType, qp.GetDescription(), qp.GetUnqualifiedName())
   170  			c.suggestions.unqualifiedQueries = append(c.suggestions.unqualifiedQueries, suggestion)
   171  		} else {
   172  			suggestion := c.newSuggestion(itemType, qp.GetDescription(), qp.Name())
   173  			c.suggestions.queriesByMod[mod.ShortName] = append(c.suggestions.queriesByMod[mod.ShortName], suggestion)
   174  		}
   175  
   176  		return
   177  	}
   178  
   179  	c.workspace().GetResourceMaps().WalkResources(resourceFunc)
   180  
   181  	// populate mod suggestions
   182  	for mod := range c.suggestions.queriesByMod {
   183  		suggestion := c.newSuggestion("mod", "", mod)
   184  		c.suggestions.mods = append(c.suggestions.mods, suggestion)
   185  	}
   186  }
   187  
   188  func sanitiseTableName(strToEscape string) string {
   189  	tokens := helpers.SplitByRune(strToEscape, '.')
   190  	var escaped []string
   191  	for _, token := range tokens {
   192  		// if string contains spaces or special characters(-) or upper case characters, escape it,
   193  		// as Postgres by default converts to lower case
   194  		if strings.ContainsAny(token, " -") || utils.ContainsUpper(token) {
   195  			token = db_common.PgEscapeName(token)
   196  		}
   197  		escaped = append(escaped, token)
   198  	}
   199  	return strings.Join(escaped, ".")
   200  }