github.com/cockroachdb/cockroachdb-parser@v0.23.3-0.20240213214944-911057d40c9a/pkg/sql/parser/help.go (about)

     1  // Copyright 2017 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  // Package parser contains exposes a SQL parser for cockroach.
    12  package parser
    13  
    14  import (
    15  	"bytes"
    16  	"context"
    17  	"fmt"
    18  	"io"
    19  	"sort"
    20  	"strings"
    21  	"text/tabwriter"
    22  
    23  	"github.com/cockroachdb/cockroachdb-parser/pkg/docs"
    24  	"github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode"
    25  	"github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror"
    26  	"github.com/cockroachdb/cockroachdb-parser/pkg/sql/sem/builtins/builtinsregistry"
    27  	"github.com/cockroachdb/cockroachdb-parser/pkg/sql/sem/tree"
    28  	"github.com/cockroachdb/errors"
    29  	"golang.org/x/text/cases"
    30  	"golang.org/x/text/language"
    31  )
    32  
    33  // HelpMessage describes a contextual help message.
    34  type HelpMessage struct {
    35  	// Command is set if the message is about a statement.
    36  	Command string
    37  	// Function is set if the message is about a built-in function.
    38  	Function string
    39  
    40  	// HelpMessageBody contains the details of the message.
    41  	HelpMessageBody
    42  }
    43  
    44  // helpHintPrefix is the special prefix on a hint payload that is
    45  // recognized by the CLI code.
    46  const helpHintPrefix = "help:"
    47  
    48  // String implements the fmt.String interface.
    49  func (h *HelpMessage) String() string {
    50  	var buf bytes.Buffer
    51  	buf.WriteString(helpHintPrefix + "\n")
    52  	h.Format(&buf)
    53  	return buf.String()
    54  }
    55  
    56  // Format prints out details about the message onto the specified output stream.
    57  func (h *HelpMessage) Format(w io.Writer) {
    58  	if h.Command != "" {
    59  		fmt.Fprintf(w, "Command:     %s\n", h.Command)
    60  	}
    61  	if h.Function != "" {
    62  		fmt.Fprintf(w, "Function:    %s\n", h.Function)
    63  	}
    64  	if h.ShortDescription != "" {
    65  		fmt.Fprintf(w, "Description: %s\n", h.ShortDescription)
    66  	}
    67  	if h.Category != "" {
    68  		fmt.Fprintf(w, "Category:    %s\n", h.Category)
    69  	}
    70  	if h.Command != "" {
    71  		fmt.Fprintln(w, "Syntax:")
    72  	}
    73  	fmt.Fprintln(w, strings.TrimSpace(h.Text))
    74  	if h.SeeAlso != "" {
    75  		fmt.Fprintf(w, "\nSee also:\n  %s\n", h.SeeAlso)
    76  	}
    77  }
    78  
    79  // helpWith is to be used in parser actions to mark the parser "in
    80  // error", with the error set to a contextual help message about the
    81  // current statement.
    82  func helpWith(sqllex sqlLexer, helpText string) int {
    83  	scan := sqllex.(*lexer)
    84  	if helpText == "" {
    85  		scan.lastError = pgerror.WithCandidateCode(errors.New("help upon syntax error"), pgcode.Syntax)
    86  		scan.populateHelpMsg("help:\n" + AllHelp)
    87  		return 1
    88  	}
    89  	msg := HelpMessage{Command: helpText, HelpMessageBody: HelpMessages[helpText]}
    90  	scan.SetHelp(msg)
    91  	// We return non-zero to indicate to the caller of Parse() that the
    92  	// parse was unsuccessful.
    93  	return 1
    94  }
    95  
    96  // helpWithFunction is to be used in parser actions to mark the parser
    97  // "in error", with the error set to a contextual help message about
    98  // the current built-in function.
    99  func helpWithFunction(sqllex sqlLexer, f tree.ResolvableFunctionReference) int {
   100  	// A resolver is not needed because we do not provide contextual help
   101  	// messages for user-defined functions.
   102  	d, err := f.Resolve(context.Background(), tree.EmptySearchPath, nil /* resolver */)
   103  	if err != nil {
   104  		return 1
   105  	}
   106  
   107  	props, _ := builtinsregistry.GetBuiltinProperties(d.Name)
   108  	msg := HelpMessage{
   109  		Function: f.String(),
   110  		HelpMessageBody: HelpMessageBody{
   111  			Category: props.Category,
   112  			SeeAlso:  docs.URL("functions-and-operators.html"),
   113  		},
   114  	}
   115  
   116  	var buf bytes.Buffer
   117  	w := tabwriter.NewWriter(&buf, 0, 0, 1, ' ', 0)
   118  
   119  	// Each function definition contains one or more overloads. We need
   120  	// to extract them all; moreover each overload may have a different
   121  	// documentation, so we need to also combine the descriptions
   122  	// together.
   123  	lastInfo := ""
   124  	for i, b := range d.Overloads {
   125  		if b.Info != "" && b.Info != lastInfo {
   126  			if i > 0 {
   127  				fmt.Fprintln(w, "---")
   128  			}
   129  			fmt.Fprintf(w, "\n%s\n\n", b.Info)
   130  			fmt.Fprintln(w, "Signature")
   131  		}
   132  		lastInfo = b.Info
   133  
   134  		simplifyRet := b.Class == tree.GeneratorClass
   135  		fmt.Fprintf(w, "%s%s\n", d.Name, b.Signature(simplifyRet))
   136  	}
   137  	_ = w.Flush()
   138  	msg.Text = buf.String()
   139  
   140  	sqllex.(*lexer).SetHelp(msg)
   141  	return 1
   142  }
   143  
   144  func helpWithFunctionByName(sqllex sqlLexer, s string) int {
   145  	un := &tree.UnresolvedName{NumParts: 1, Parts: tree.NameParts{s}}
   146  	return helpWithFunction(sqllex, tree.ResolvableFunctionReference{FunctionReference: un})
   147  }
   148  
   149  const (
   150  	hGroup        = ""
   151  	hDDL          = "schema manipulation"
   152  	hDML          = "data manipulation"
   153  	hTxn          = "transaction control"
   154  	hPriv         = "privileges and security"
   155  	hMisc         = "miscellaneous"
   156  	hCfg          = "configuration"
   157  	hExperimental = "experimental"
   158  	hCCL          = "enterprise features"
   159  )
   160  
   161  // HelpMessageBody defines the body of a help text. The messages are
   162  // structured to facilitate future help navigation functionality.
   163  type HelpMessageBody struct {
   164  	Category         string
   165  	ShortDescription string
   166  	Text             string
   167  	SeeAlso          string
   168  }
   169  
   170  // HelpMessages is the registry of all help messages, keyed by the
   171  // top-level statement that they document. The key is intended for use
   172  // via the \h client-side command.
   173  var HelpMessages = func(h map[string]HelpMessageBody) map[string]HelpMessageBody {
   174  	appendSeeAlso := func(newItem, prevItems string) string {
   175  		// "See also" items start with no indentation, and then use two
   176  		// space indentation from the 2nd item onward.
   177  		if prevItems != "" {
   178  			return newItem + "\n  " + prevItems
   179  		}
   180  		return newItem
   181  	}
   182  	reformatSeeAlso := func(seeAlso string) string {
   183  		return strings.Replace(
   184  			strings.Replace(seeAlso, ", ", "\n  ", -1),
   185  			"WEBDOCS", docs.URLBase, -1)
   186  	}
   187  	srcMsg := h["<SOURCE>"]
   188  	srcMsg.SeeAlso = reformatSeeAlso(strings.TrimSpace(srcMsg.SeeAlso))
   189  	selectMsg := h["<SELECTCLAUSE>"]
   190  	selectMsg.SeeAlso = reformatSeeAlso(strings.TrimSpace(selectMsg.SeeAlso))
   191  	for k, m := range h {
   192  		m = h[k]
   193  		m.ShortDescription = strings.TrimSpace(m.ShortDescription)
   194  		m.Text = strings.TrimSpace(m.Text)
   195  		m.SeeAlso = strings.TrimSpace(m.SeeAlso)
   196  
   197  		// If the description contains <source>, append the <source> help.
   198  		if strings.Contains(m.Text, "<source>") && k != "<SOURCE>" {
   199  			m.Text = strings.TrimSpace(m.Text) + "\n\n" + strings.TrimSpace(srcMsg.Text)
   200  			m.SeeAlso = appendSeeAlso(srcMsg.SeeAlso, m.SeeAlso)
   201  		}
   202  		// Ditto for <selectclause>.
   203  		if strings.Contains(m.Text, "<selectclause>") && k != "<SELECTCLAUSE>" {
   204  			m.Text = strings.TrimSpace(m.Text) + "\n\n" + strings.TrimSpace(selectMsg.Text)
   205  			m.SeeAlso = appendSeeAlso(selectMsg.SeeAlso, m.SeeAlso)
   206  		}
   207  		// If the description contains <tablename>, mention SHOW TABLES in "See Also".
   208  		if strings.Contains(m.Text, "<tablename>") {
   209  			m.SeeAlso = appendSeeAlso("SHOW TABLES", m.SeeAlso)
   210  		}
   211  		m.SeeAlso = reformatSeeAlso(m.SeeAlso)
   212  		h[k] = m
   213  	}
   214  	return h
   215  }(helpMessages)
   216  
   217  // AllHelp contains an overview of all statements with help messages.
   218  // For example, displayed in the CLI shell with \h without additional parameters.
   219  var AllHelp = func(h map[string]HelpMessageBody) string {
   220  	// Aggregate the help items.
   221  	cmds := make(map[string][]string)
   222  	for c, details := range h {
   223  		if details.Category == "" {
   224  			continue
   225  		}
   226  		cmds[details.Category] = append(cmds[details.Category], c)
   227  	}
   228  
   229  	// Ensure the result is deterministic.
   230  	var categories []string
   231  	for c, l := range cmds {
   232  		categories = append(categories, c)
   233  		sort.Strings(l)
   234  	}
   235  	sort.Strings(categories)
   236  
   237  	// Compile the final help index.
   238  	var buf bytes.Buffer
   239  	w := tabwriter.NewWriter(&buf, 0, 0, 1, ' ', 0)
   240  	for _, cat := range categories {
   241  		fmt.Fprintf(w, "%s:\n", cases.Title(language.English, cases.NoLower).String(cat))
   242  		for _, item := range cmds[cat] {
   243  			fmt.Fprintf(w, "\t\t%s\t%s\n", item, h[item].ShortDescription)
   244  		}
   245  		fmt.Fprintln(w)
   246  	}
   247  	_ = w.Flush()
   248  	return buf.String()
   249  }(helpMessages)