wrds-download

TUI/CLI tool for browsing and downloading WRDS data
Log | Files | Refs | README

commit 6b65bccfd07d30b22a6c8ca105cfb5445021f4e5
parent 50acd5dd300ab27ade59be50b463ce63fe3c525c
Author: Erik Loualiche <[email protected]>
Date:   Fri, 20 Feb 2026 09:20:26 -0600

Add SELECT columns field to download form and --columns CLI flag

Allows users to specify which columns to download instead of always
using SELECT *. The TUI form shows column names from loaded metadata
as placeholder hints.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

Diffstat:
Mcmd/download.go | 23+++++++++++++++--------
Minternal/tui/app.go | 18+++++++++++++-----
Minternal/tui/dlform.go | 48++++++++++++++++++++++++++++++++++--------------
3 files changed, 62 insertions(+), 27 deletions(-)

diff --git a/cmd/download.go b/cmd/download.go @@ -11,13 +11,14 @@ import ( ) var ( - dlSchema string - dlTable string - dlWhere string - dlQuery string - dlOut string - dlFormat string - dlLimit int + dlSchema string + dlTable string + dlColumns string + dlWhere string + dlQuery string + dlOut string + dlFormat string + dlLimit int ) var downloadCmd = &cobra.Command{ @@ -27,6 +28,7 @@ var downloadCmd = &cobra.Command{ Examples: wrds download --schema crsp --table dsf --where "date='2020-01-02'" --out crsp_dsf.parquet + wrds download --schema comp --table funda --columns "gvkey,datadate,sale" --out funda.parquet wrds download --query "SELECT permno, date, prc FROM crsp.dsf LIMIT 1000" --out out.parquet wrds download --schema comp --table funda --out funda.csv --format csv`, RunE: runDownload, @@ -38,6 +40,7 @@ func init() { f := downloadCmd.Flags() f.StringVar(&dlSchema, "schema", "", "Schema name (e.g. crsp)") f.StringVar(&dlTable, "table", "", "Table name (e.g. dsf)") + f.StringVarP(&dlColumns, "columns", "c", "*", "Columns to select (comma-separated, default *)") f.StringVar(&dlWhere, "where", "", "SQL WHERE clause (without the WHERE keyword)") f.StringVar(&dlQuery, "query", "", "Full SQL query (overrides --schema/--table/--where)") f.StringVar(&dlOut, "out", "", "Output file path (required)") @@ -76,7 +79,11 @@ func buildQuery() (string, error) { return "", fmt.Errorf("either --query or both --schema and --table must be specified") } - q := fmt.Sprintf("SELECT * FROM wrds.%s.%s", dlSchema, dlTable) + sel := "*" + if dlColumns != "" && dlColumns != "*" { + sel = dlColumns + } + q := fmt.Sprintf("SELECT %s FROM wrds.%s.%s", sel, dlSchema, dlTable) if dlWhere != "" { q += " WHERE " + dlWhere diff --git a/internal/tui/app.go b/internal/tui/app.go @@ -229,11 +229,13 @@ func (a *App) loadMeta(schema, tbl string) tea.Cmd { func (a *App) startDownload(msg DlSubmitMsg) tea.Cmd { return func() tea.Msg { - var query string + sel := "*" + if msg.Columns != "" && msg.Columns != "*" { + sel = msg.Columns + } + query := fmt.Sprintf("SELECT %s FROM wrds.%s.%s", sel, msg.Schema, msg.Table) if msg.Where != "" { - query = fmt.Sprintf("SELECT * FROM wrds.%s.%s WHERE %s", msg.Schema, msg.Table, msg.Where) - } else { - query = fmt.Sprintf("SELECT * FROM wrds.%s.%s", msg.Schema, msg.Table) + query += " WHERE " + msg.Where } err := export.Export(query, msg.Out, export.Options{Format: msg.Format}) if err != nil { @@ -529,7 +531,13 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } if a.selectedSchema != "" && a.selectedTable != "" { - a.dlForm = newDlForm(a.selectedSchema, a.selectedTable) + var colNames []string + if a.previewMeta != nil { + for _, c := range a.previewMeta.Columns { + colNames = append(colNames, c.Name) + } + } + a.dlForm = newDlForm(a.selectedSchema, a.selectedTable, colNames) a.state = stateDownloadForm return a, nil } diff --git a/internal/tui/dlform.go b/internal/tui/dlform.go @@ -12,7 +12,8 @@ import ( type dlFormField int const ( - fieldWhere dlFormField = iota + fieldSelect dlFormField = iota + fieldWhere fieldOut fieldFormat fieldCount @@ -29,19 +30,33 @@ type DlForm struct { // DlSubmitMsg is sent when the user confirms the download form. type DlSubmitMsg struct { - Schema string - Table string - Where string - Out string - Format string + Schema string + Table string + Columns string + Where string + Out string + Format string } // DlCancelMsg is sent when the user cancels. type DlCancelMsg struct{} -func newDlForm(schema, table string) DlForm { +func newDlForm(schema, table string, colNames []string) DlForm { f := DlForm{schema: schema, table: table} + f.inputs[fieldSelect] = textinput.New() + placeholder := "e.g. gvkey, datadate, sale" + if len(colNames) > 0 { + hint := strings.Join(colNames, ", ") + if len(hint) > 60 { + hint = hint[:57] + "..." + } + placeholder = "e.g. " + hint + } + f.inputs[fieldSelect].Placeholder = placeholder + f.inputs[fieldSelect].CharLimit = 1024 + f.inputs[fieldSelect].SetValue("*") + f.inputs[fieldWhere] = textinput.New() f.inputs[fieldWhere].Placeholder = "e.g. date >= '2020-01-01'" f.inputs[fieldWhere].CharLimit = 512 @@ -56,7 +71,7 @@ func newDlForm(schema, table string) DlForm { f.inputs[fieldFormat].CharLimit = 10 f.inputs[fieldFormat].SetValue("parquet") - f.inputs[fieldWhere].Focus() + f.inputs[fieldSelect].Focus() return f } @@ -82,13 +97,18 @@ func (f DlForm) Update(msg tea.Msg) (DlForm, tea.Cmd) { if format == "" { format = "parquet" } + columns := strings.TrimSpace(f.inputs[fieldSelect].Value()) + if columns == "" { + columns = "*" + } return f, func() tea.Msg { return DlSubmitMsg{ - Schema: f.schema, - Table: f.table, - Where: f.inputs[fieldWhere].Value(), - Out: out, - Format: format, + Schema: f.schema, + Table: f.table, + Columns: columns, + Where: f.inputs[fieldWhere].Value(), + Out: out, + Format: format, } } case "tab", "down": @@ -115,7 +135,7 @@ func (f DlForm) View(width int) string { title := stylePanelHeader.Render(fmt.Sprintf("Download %s.%s", f.schema, f.table)) sb.WriteString(title + "\n\n") - labels := []string{"WHERE clause", "Output path", "Format (parquet/csv)"} + labels := []string{"SELECT columns", "WHERE clause", "Output path", "Format (parquet/csv)"} for i, label := range labels { style := lipgloss.NewStyle().Foreground(colorMuted) if dlFormField(i) == f.focused {