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 }