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

     1  package workspace
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/turbot/go-kit/helpers"
    11  	typehelpers "github.com/turbot/go-kit/types"
    12  	"github.com/turbot/steampipe/pkg/error_helpers"
    13  	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
    14  	"github.com/turbot/steampipe/pkg/steampipeconfig/parse"
    15  	"github.com/turbot/steampipe/pkg/utils"
    16  )
    17  
    18  // GetQueriesFromArgs retrieves queries from args
    19  //
    20  // For each arg check if it is a named query or a file, before falling back to treating it as sql
    21  func (w *Workspace) GetQueriesFromArgs(args []string) ([]*modconfig.ResolvedQuery, error) {
    22  	utils.LogTime("execute.GetQueriesFromArgs start")
    23  	defer utils.LogTime("execute.GetQueriesFromArgs end")
    24  
    25  	var queries = make([]*modconfig.ResolvedQuery, len(args))
    26  	for idx, arg := range args {
    27  		resolvedQuery, queryProvider, err := w.ResolveQueryAndArgsFromSQLString(arg)
    28  		if err != nil {
    29  			return nil, err
    30  		}
    31  		if len(resolvedQuery.ExecuteSQL) > 0 {
    32  			// default name to the query text
    33  			resolvedQuery.Name = resolvedQuery.ExecuteSQL
    34  			if queryProvider != nil {
    35  				resolvedQuery.Name = queryProvider.Name()
    36  			}
    37  			queries[idx] = resolvedQuery
    38  		}
    39  	}
    40  	return queries, nil
    41  }
    42  
    43  // ResolveQueryAndArgsFromSQLString attempts to resolve 'arg' to a query and query args
    44  func (w *Workspace) ResolveQueryAndArgsFromSQLString(sqlString string) (*modconfig.ResolvedQuery, modconfig.QueryProvider, error) {
    45  	var err error
    46  
    47  	// 1) check if this is a resource
    48  	// if this looks like a named query provider invocation, parse the sql string for arguments
    49  	resource, args, err := w.extractQueryProviderFromQueryString(sqlString)
    50  	if err != nil {
    51  		return nil, nil, err
    52  	}
    53  
    54  	if resource != nil {
    55  		log.Printf("[TRACE] query string is a query provider resource: %s", resource.Name())
    56  
    57  		// resolve the query for the query provider and return it
    58  		resolvedQuery, err := w.ResolveQueryFromQueryProvider(resource, args)
    59  		if err != nil {
    60  			return nil, nil, err
    61  		}
    62  		log.Printf("[TRACE] resolved query: %s", sqlString)
    63  		return resolvedQuery, resource, nil
    64  	}
    65  
    66  	// 2) is this a file
    67  	// get absolute filename
    68  	filePath, err := filepath.Abs(sqlString)
    69  	if err != nil {
    70  		return nil, nil, fmt.Errorf("%s", err.Error())
    71  	}
    72  	fileQuery, fileExists, err := w.getQueryFromFile(filePath)
    73  	if err != nil {
    74  		return nil, nil, fmt.Errorf("%s", err.Error())
    75  	}
    76  	if fileExists {
    77  		if fileQuery.ExecuteSQL == "" {
    78  			error_helpers.ShowWarning(fmt.Sprintf("file '%s' does not contain any data", filePath))
    79  			// (just return the empty query - it will be filtered above)
    80  		}
    81  		return fileQuery, nil, nil
    82  	}
    83  	// the argument cannot be resolved as an existing file
    84  	// if it has a sql suffix (i.e we believe the user meant to specify a file) return a file not found error
    85  	if strings.HasSuffix(strings.ToLower(sqlString), ".sql") {
    86  		return nil, nil, fmt.Errorf("file '%s' does not exist", filePath)
    87  	}
    88  
    89  	// so we have not managed to resolve this - if it looks like a named query or control
    90  	// (i.e we believe the user meant to specify a query) return a not found error
    91  	// NOTE: this needs to come after the file suffix check because this handles the resource name query.sql edge case
    92  	if name, isResource := queryLooksLikeExecutableResource(sqlString); isResource {
    93  		return nil, nil, fmt.Errorf("'%s' not found in %s (%s)", name, w.Mod.Name(), w.Path)
    94  	}
    95  
    96  	// 3) just use the query string as is and assume it is valid SQL
    97  	return &modconfig.ResolvedQuery{RawSQL: sqlString, ExecuteSQL: sqlString}, nil, nil
    98  }
    99  
   100  // ResolveQueryFromQueryProvider resolves the query for the given QueryProvider
   101  func (w *Workspace) ResolveQueryFromQueryProvider(queryProvider modconfig.QueryProvider, runtimeArgs *modconfig.QueryArgs) (*modconfig.ResolvedQuery, error) {
   102  	log.Printf("[TRACE] ResolveQueryFromQueryProvider for %s", queryProvider.Name())
   103  
   104  	query := queryProvider.GetQuery()
   105  	sql := queryProvider.GetSQL()
   106  
   107  	params := queryProvider.GetParams()
   108  
   109  	// merge the base args with the runtime args
   110  	var err error
   111  	runtimeArgs, err = modconfig.MergeArgs(queryProvider, runtimeArgs)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	// determine the source for the query
   117  	// - this will either be the control itself or any named query the control refers to
   118  	// either via its SQL proper ty (passing a query name) or Query property (using a reference to a query object)
   119  
   120  	// if a query is provided, use that to resolve the sql
   121  	if query != nil {
   122  		return w.ResolveQueryFromQueryProvider(query, runtimeArgs)
   123  	}
   124  
   125  	// must have sql is there is no query
   126  	if sql == nil {
   127  		return nil, fmt.Errorf("%s does not define  either a 'sql' property or a 'query' property\n", queryProvider.Name())
   128  	}
   129  
   130  	queryProviderSQL := typehelpers.SafeString(sql)
   131  	log.Printf("[TRACE] control defines inline SQL")
   132  
   133  	// if the SQL refers to a named query, this is the same as if the 'Query' property is set
   134  	if namedQueryProvider, ok := w.GetQueryProvider(queryProviderSQL); ok {
   135  		// in this case, it is NOT valid for the query provider to define its own Param definitions
   136  		if params != nil {
   137  			return nil, fmt.Errorf("%s has an 'SQL' property which refers to %s, so it cannot define 'param' blocks", queryProvider.Name(), namedQueryProvider.Name())
   138  		}
   139  		return w.ResolveQueryFromQueryProvider(namedQueryProvider, runtimeArgs)
   140  	}
   141  
   142  	// so the  sql is NOT a named query
   143  	return queryProvider.GetResolvedQuery(runtimeArgs)
   144  
   145  }
   146  
   147  // try to treat the input string as a file name and if it exists, return its contents
   148  func (w *Workspace) getQueryFromFile(input string) (*modconfig.ResolvedQuery, bool, error) {
   149  	// get absolute filename
   150  	path, err := filepath.Abs(input)
   151  	if err != nil {
   152  		//nolint:golint,nilerr // if this gives any error, return not exist
   153  		return nil, false, nil
   154  	}
   155  
   156  	// does it exist?
   157  	if _, err := os.Stat(path); err != nil {
   158  		//nolint:golint,nilerr // if this gives any error, return not exist (we may get a not found or a path too long for example)
   159  		return nil, false, nil
   160  	}
   161  
   162  	// read file
   163  	fileBytes, err := os.ReadFile(path)
   164  	if err != nil {
   165  		return nil, true, err
   166  	}
   167  
   168  	res := &modconfig.ResolvedQuery{
   169  		RawSQL:     string(fileBytes),
   170  		ExecuteSQL: string(fileBytes),
   171  	}
   172  	return res, true, nil
   173  }
   174  
   175  // does the input look like a resource which can be executed as a query
   176  // Note: if anything fails just return nil values
   177  func (w *Workspace) extractQueryProviderFromQueryString(input string) (modconfig.QueryProvider, *modconfig.QueryArgs, error) {
   178  	// can we extract a resource name from the string
   179  	parsedResourceName := extractResourceNameFromQuery(input)
   180  	if parsedResourceName == nil {
   181  		return nil, nil, nil
   182  	}
   183  	// ok we managed to extract a resource name - does this resource exist?
   184  	resource, ok := w.GetResource(parsedResourceName)
   185  	if !ok {
   186  		return nil, nil, nil
   187  	}
   188  
   189  	//- is the resource a query provider, and if so does it have a query?
   190  	queryProvider, ok := resource.(modconfig.QueryProvider)
   191  	if !ok {
   192  		return nil, nil, fmt.Errorf("%s cannot be executed as a query", input)
   193  	}
   194  
   195  	_, args, err := parse.ParseQueryInvocation(input)
   196  	if err != nil {
   197  		return nil, nil, err
   198  	}
   199  	// success
   200  	return queryProvider, args, nil
   201  }
   202  
   203  func extractResourceNameFromQuery(input string) *modconfig.ParsedResourceName {
   204  	// remove parameters from the input string before calling ParseResourceName
   205  	// as parameters may break parsing
   206  	openBracketIdx := strings.Index(input, "(")
   207  	if openBracketIdx != -1 {
   208  		input = input[:openBracketIdx]
   209  	}
   210  	parsedName, err := modconfig.ParseResourceName(input)
   211  	// do not bubble error up, just return nil parsed name
   212  	// it is expected that this function may fail if a raw query is passed to it
   213  	if err != nil {
   214  		return nil
   215  	}
   216  	return parsedName
   217  }
   218  
   219  func queryLooksLikeExecutableResource(input string) (string, bool) {
   220  	// remove parameters from the input string before calling ParseResourceName
   221  	// as parameters may break parsing
   222  	openBracketIdx := strings.Index(input, "(")
   223  	if openBracketIdx != -1 {
   224  		input = input[:openBracketIdx]
   225  	}
   226  	parsedName, err := modconfig.ParseResourceName(input)
   227  	if err == nil && helpers.StringSliceContains(modconfig.QueryProviderBlocks, parsedName.ItemType) {
   228  		return parsedName.ToResourceName(), true
   229  	}
   230  	// do not bubble error up, just return false
   231  	return "", false
   232  
   233  }