mirror of
https://github.com/turbot/steampipe.git
synced 2026-05-13 06:04:27 -04:00
* Add test for #4812: Autocomplete suggestions should have size limits This test verifies that autocomplete suggestion maps enforce size limits to prevent unbounded memory growth. The test calls setTablesForSchema() and setQueriesForMod() methods that should enforce: - Maximum 100 schemas in tablesBySchema - Maximum 500 tables per schema - Maximum 100 mods in queriesByMod - Maximum 500 queries per mod This test will fail until the size limiting implementation is added. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix #4812: Add size limits to autocomplete suggestions maps Implements bounded size for autocomplete suggestion maps to prevent unbounded memory growth with large schemas: - Added constants for max schemas (100) and max tables per schema (500) - Created setTablesForSchema() and setQueriesForMod() methods that enforce limits using LRU-style eviction when limits are exceeded - Updated interactive_client_autocomplete.go to use the new bounded setter This prevents excessive memory consumption when dealing with databases that have hundreds of connections with many tables each. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
124 lines
4.7 KiB
Go
124 lines
4.7 KiB
Go
package interactive
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/c-bata/go-prompt"
|
|
"github.com/turbot/go-kit/helpers"
|
|
"github.com/turbot/pipe-fittings/v2/utils"
|
|
"github.com/turbot/steampipe/v2/pkg/constants"
|
|
"github.com/turbot/steampipe/v2/pkg/db/db_common"
|
|
"github.com/turbot/steampipe/v2/pkg/steampipeconfig"
|
|
)
|
|
|
|
func (c *InteractiveClient) initialiseSuggestions(ctx context.Context) error {
|
|
log.Printf("[TRACE] initialiseSuggestions")
|
|
|
|
conn, err := c.client().AcquireManagementConnection(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Release()
|
|
|
|
connectionStateMap, err := steampipeconfig.LoadConnectionState(ctx, conn.Conn(), steampipeconfig.WithWaitUntilLoading())
|
|
if err != nil {
|
|
log.Printf("[WARN] could not load connection state: %v", err)
|
|
//nolint:golint,nilerr // valid condition - not an error
|
|
return nil
|
|
}
|
|
|
|
// reset suggestions
|
|
c.suggestions = newAutocompleteSuggestions()
|
|
c.initialiseSchemaAndTableSuggestions(connectionStateMap)
|
|
c.initialiseQuerySuggestions()
|
|
c.suggestions.sort()
|
|
return nil
|
|
}
|
|
|
|
// initialiseSchemaAndTableSuggestions build a list of schema and table querySuggestions
|
|
func (c *InteractiveClient) initialiseSchemaAndTableSuggestions(connectionStateMap steampipeconfig.ConnectionStateMap) {
|
|
if c.schemaMetadata == nil {
|
|
return
|
|
}
|
|
|
|
// unqualified table names
|
|
// use lookup to avoid dupes from dynamic plugins
|
|
// (this is needed as GetFirstSearchPathConnectionForPlugins will return ALL dynamic connections)
|
|
var unqualifiedTablesToAdd = make(map[string]struct{})
|
|
|
|
// add connection state and rate limit
|
|
unqualifiedTablesToAdd[constants.ConnectionTable] = struct{}{}
|
|
unqualifiedTablesToAdd[constants.PluginInstanceTable] = struct{}{}
|
|
unqualifiedTablesToAdd[constants.RateLimiterDefinitionTable] = struct{}{}
|
|
unqualifiedTablesToAdd[constants.PluginColumnTable] = struct{}{}
|
|
unqualifiedTablesToAdd[constants.ServerSettingsTable] = struct{}{}
|
|
|
|
// get the first search path connection for each plugin
|
|
firstConnectionPerPlugin := connectionStateMap.GetFirstSearchPathConnectionForPlugins(c.client().GetRequiredSessionSearchPath())
|
|
firstConnectionPerPluginLookup := utils.SliceToLookup(firstConnectionPerPlugin)
|
|
// NOTE: add temporary schema into firstConnectionPerPluginLookup
|
|
// as we want to add unqualified tables from there into autocomplete
|
|
firstConnectionPerPluginLookup[c.schemaMetadata.TemporarySchemaName] = struct{}{}
|
|
|
|
for schemaName, schemaDetails := range c.schemaMetadata.Schemas {
|
|
if connectionState, found := connectionStateMap[schemaName]; found && connectionState.State != constants.ConnectionStateReady {
|
|
log.Println("[TRACE] could not find schema in state map or connection is not Ready", schemaName)
|
|
continue
|
|
}
|
|
|
|
// fully qualified table names
|
|
var qualifiedTablesToAdd []prompt.Suggest
|
|
|
|
isTemporarySchema := schemaName == c.schemaMetadata.TemporarySchemaName
|
|
if !isTemporarySchema {
|
|
// add the schema into the list of schema
|
|
// we don't need to escape schema names, since schema names are derived from connection names
|
|
// which are validated so that we don't end up with names which need it
|
|
c.suggestions.schemas = append(c.suggestions.schemas, prompt.Suggest{Text: schemaName, Description: "Schema", Output: schemaName})
|
|
}
|
|
|
|
// add qualified names of all tables
|
|
for tableName := range schemaDetails {
|
|
// do not add temp tables to qualified tables
|
|
if !isTemporarySchema {
|
|
qualifiedTableName := fmt.Sprintf("%s.%s", schemaName, sanitiseTableName(tableName))
|
|
qualifiedTablesToAdd = append(qualifiedTablesToAdd, prompt.Suggest{Text: qualifiedTableName, Description: "Table", Output: qualifiedTableName})
|
|
}
|
|
if _, addToUnqualified := firstConnectionPerPluginLookup[schemaName]; addToUnqualified {
|
|
unqualifiedTablesToAdd[tableName] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// add qualified table to tablesBySchema with size limits
|
|
if len(qualifiedTablesToAdd) > 0 {
|
|
c.suggestions.setTablesForSchema(schemaName, qualifiedTablesToAdd)
|
|
}
|
|
}
|
|
|
|
// add unqualified table suggestions
|
|
for tableName := range unqualifiedTablesToAdd {
|
|
c.suggestions.unqualifiedTables = append(c.suggestions.unqualifiedTables, prompt.Suggest{Text: tableName, Description: "Table", Output: sanitiseTableName(tableName)})
|
|
}
|
|
}
|
|
|
|
func (c *InteractiveClient) initialiseQuerySuggestions() {
|
|
// TODO add sql files???
|
|
}
|
|
|
|
func sanitiseTableName(strToEscape string) string {
|
|
tokens := helpers.SplitByRune(strToEscape, '.')
|
|
var escaped []string
|
|
for _, token := range tokens {
|
|
// if string contains spaces or special characters(-) or upper case characters, escape it,
|
|
// as Postgres by default converts to lower case
|
|
if strings.ContainsAny(token, " -") || utils.ContainsUpper(token) {
|
|
token = db_common.PgEscapeName(token)
|
|
}
|
|
escaped = append(escaped, token)
|
|
}
|
|
return strings.Join(escaped, ".")
|
|
}
|