diff --git a/internal/constants/metaquery_commands.go b/internal/constants/metaquery_commands.go index 75a30f0b..f33f81ce 100644 --- a/internal/constants/metaquery_commands.go +++ b/internal/constants/metaquery_commands.go @@ -6,11 +6,11 @@ const ( //CmdTableList = ".tables" // List all tables CmdOutput = ".output" // Set output mode //CmdTiming = ".timing" // Toggle query timer - CmdHeaders = ".header" // Toggle headers output - CmdSeparator = ".separator" // Set the column separator - CmdExit = ".exit" // Exit the interactive prompt - CmdQuit = ".quit" // Alias for .exit - //CmdInspect = ".inspect" // inspect + CmdHeaders = ".header" // Toggle headers output + CmdSeparator = ".separator" // Set the column separator + CmdExit = ".exit" // Exit the interactive prompt + CmdQuit = ".quit" // Alias for .exit + CmdInspect = ".inspect" // inspect CmdMulti = ".multi" // toggle multi line query CmdClear = ".clear" // clear the console CmdHelp = ".help" // list all meta commands diff --git a/internal/database/tables.go b/internal/database/tables.go index f00d5680..179468c8 100644 --- a/internal/database/tables.go +++ b/internal/database/tables.go @@ -196,3 +196,42 @@ func GetTableViews(ctx context.Context) ([]string, error) { } return tableViews, nil } + +func GetTableViewSchema(ctx context.Context, viewName string) (map[string]string, error) { + // Open a DuckDB connection + db, err := sql.Open("duckdb", filepaths.TailpipeDbFilePath()) + if err != nil { + return nil, fmt.Errorf("failed to open DuckDB connection: %w", err) + } + defer db.Close() + + query := ` + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = ? ORDER BY columns.column_name; + ` + rows, err := db.QueryContext(ctx, query, viewName) + if err != nil { + return nil, fmt.Errorf("failed to get view schema for %s: %w", viewName, err) + } + defer rows.Close() + + schema := make(map[string]string) + for rows.Next() { + var columnName, columnType string + err = rows.Scan(&columnName, &columnType) + if err != nil { + return nil, fmt.Errorf("failed to scan column schema: %w", err) + } + if strings.HasPrefix(columnType, "STRUCT") { + columnType = "STRUCT" + } + schema[columnName] = columnType + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating over view schema rows: %w", err) + } + + return schema, nil +} diff --git a/internal/interactive/interactive_client.go b/internal/interactive/interactive_client.go index e9d3c0b4..b59ba0b5 100644 --- a/internal/interactive/interactive_client.go +++ b/internal/interactive/interactive_client.go @@ -492,8 +492,8 @@ func (c *InteractiveClient) queryCompleter(d prompt.Document) []prompt.Suggest { s = append(s, suggestions...) case metaquery.IsMetaQuery(text): suggestions := metaquery.Complete(&metaquery.CompleterInput{ - Query: text, - TableSuggestions: c.getTableSuggestions(lastWord(text)), + Query: text, + ViewSuggestions: c.getTableSuggestions(lastWord(text)), }) s = append(s, suggestions...) default: diff --git a/internal/metaquery/completers.go b/internal/metaquery/completers.go index 691607e9..036a23d6 100644 --- a/internal/metaquery/completers.go +++ b/internal/metaquery/completers.go @@ -8,8 +8,8 @@ import ( // CompleterInput is a struct defining input data for the metaquery completer type CompleterInput struct { - Query string - TableSuggestions []prompt.Suggest + Query string + ViewSuggestions []prompt.Suggest } type completer func(input *CompleterInput) []prompt.Suggest @@ -40,6 +40,6 @@ func completerFromArgsOf(cmd string) completer { } } -//func inspectCompleter(input *CompleterInput) []prompt.Suggest { -// return input.TableSuggestions -//} +func inspectCompleter(input *CompleterInput) []prompt.Suggest { + return input.ViewSuggestions +} diff --git a/internal/metaquery/definitions.go b/internal/metaquery/definitions.go index abb58717..95db8df3 100644 --- a/internal/metaquery/definitions.go +++ b/internal/metaquery/definitions.go @@ -100,17 +100,13 @@ func init() { }, completer: completerFromArgsOf(constants.CmdOutput), }, - // - //constants.CmdInspect: { - // title: constants.CmdInspect, - // handler: inspect, - // // .inspect only supports a single arg, however the arg validation code cannot understand escaped arguments - // // e.g. it will treat csv."my table" as 2 args - // // the logic to handle this escaping is lower down so we just validate to ensure at least one argument has been provided - // validator: atLeastNArgs(0), - // description: "View connections, tables & column information", - // completer: inspectCompleter, - //}, + constants.CmdInspect: { + title: constants.CmdInspect, + handler: inspect, + validator: atMostNArgs(1), + description: "View tables & column information", + completer: inspectCompleter, + }, constants.CmdClear: { title: constants.CmdClear, handler: clearScreen, diff --git a/internal/metaquery/handler_input.go b/internal/metaquery/handler_input.go index 8592d147..53fea612 100644 --- a/internal/metaquery/handler_input.go +++ b/internal/metaquery/handler_input.go @@ -1,7 +1,11 @@ package metaquery import ( + "context" + "github.com/c-bata/go-prompt" + + "github.com/turbot/tailpipe/internal/database" ) // HandlerInput defines input data for the metaquery handler @@ -9,8 +13,21 @@ type HandlerInput struct { Prompt *prompt.Prompt ClosePrompt func() Query string + + views *[]string } func (h *HandlerInput) args() []string { return getArguments(h.Query) } + +func (h *HandlerInput) GetViews() ([]string, error) { + if h.views == nil { + views, err := database.GetTableViews(context.Background()) + if err != nil { + return nil, err + } + h.views = &views + } + return *h.views, nil +} diff --git a/internal/metaquery/handler_inspect.go b/internal/metaquery/handler_inspect.go index 43b1dcbc..86eb7032 100644 --- a/internal/metaquery/handler_inspect.go +++ b/internal/metaquery/handler_inspect.go @@ -1,13 +1,87 @@ package metaquery -//// inspect -//func inspect(ctx context.Context, input *HandlerInput) error { -// // TODO #metaquery - implement inspect -// return nil -//} -// -//// list all the tables in the schema -//func listTables(ctx context.Context, input *HandlerInput) error { -// // TODO #metaquery - implement listTables -// return nil -//} +import ( + "context" + "fmt" + "slices" + "sort" + "strings" + + "github.com/turbot/tailpipe/internal/config" + "github.com/turbot/tailpipe/internal/database" + "github.com/turbot/tailpipe/internal/plugin" +) + +// inspect +func inspect(ctx context.Context, input *HandlerInput) error { + + views, err := input.GetViews() + if err != nil { + return fmt.Errorf("failed to get tables: %w", err) + } + + if len(input.args()) == 0 { + return listViews(ctx, input, views) + } + + viewName := input.args()[0] + if slices.Contains(views, viewName) { + return listViewSchema(ctx, input, viewName) + } + + return nil +} + +func listViews(ctx context.Context, input *HandlerInput, views []string) error { + var rows [][]string + rows = append(rows, []string{"Table", "Plugin"}) // Header + + for _, view := range views { + p, _ := getPluginForTable(ctx, view) + rows = append(rows, []string{view, p}) + } + + fmt.Println(buildTable(rows, true)) //nolint:forbidigo //UI output + return nil +} + +func listViewSchema(ctx context.Context, input *HandlerInput, viewName string) error { + schema, err := database.GetTableViewSchema(ctx, viewName) + if err != nil { + return fmt.Errorf("failed to get view schema: %w", err) + } + + var rows [][]string + rows = append(rows, []string{"Column", "Type"}) // Header + + var cols []string + for column := range schema { + cols = append(cols, column) + } + sort.Strings(cols) + + for _, col := range cols { + rows = append(rows, []string{col, schema[col]}) + } + + fmt.Println(buildTable(rows, true)) //nolint:forbidigo //UI output + return nil +} + +func getPluginForTable(ctx context.Context, tableName string) (string, error) { + prefix := strings.Split(tableName, "_")[0] + + ps, err := plugin.GetInstalledPlugins(ctx, config.GlobalConfig.PluginVersions) + if err != nil { + return "", fmt.Errorf("failed to get installed plugins: %w", err) + } + + for k, v := range ps { + pluginShortName := strings.Split(k, "/")[1] + if strings.HasPrefix(pluginShortName, prefix) { + return fmt.Sprintf("%s@%s", pluginShortName, v.String()), nil + } + } + + return "", nil +} diff --git a/internal/metaquery/validators.go b/internal/metaquery/validators.go index 0778fef8..b5e8a0db 100644 --- a/internal/metaquery/validators.go +++ b/internal/metaquery/validators.go @@ -115,17 +115,17 @@ func validatorFromArgsOf(cmd string) validator { // } //} -//var atMostNArgs = func(n int) validator { -// return func(args []string) ValidationResult { -// numArgs := len(args) -// if numArgs > n { -// return ValidationResult{ -// Err: fmt.Errorf("command needs at most %d %s - got %d", n, utils.Pluralize("argument", n), numArgs), -// } -// } -// return ValidationResult{ShouldRun: true} -// } -//} +var atMostNArgs = func(n int) validator { + return func(args []string) ValidationResult { + numArgs := len(args) + if numArgs > n { + return ValidationResult{ + Err: fmt.Errorf("command needs at most %d %s - got %d", n, utils.Pluralize("argument", n), numArgs), + } + } + return ValidationResult{ShouldRun: true} + } +} var exactlyNArgs = func(n int) validator { return func(args []string) ValidationResult {