github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/cmd/docgen/funcs.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 main
    12  
    13  import (
    14  	"bytes"
    15  	"fmt"
    16  	"io/ioutil"
    17  	"os"
    18  	"path/filepath"
    19  	"regexp"
    20  	"sort"
    21  	"strings"
    22  
    23  	"github.com/cockroachdb/cockroach/pkg/sql/sem/builtins"
    24  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    25  	"github.com/cockroachdb/errors"
    26  	"github.com/golang-commonmark/markdown"
    27  	"github.com/spf13/cobra"
    28  )
    29  
    30  func init() {
    31  	cmds = append(cmds, &cobra.Command{
    32  		Use:   "functions <output-dir>",
    33  		Short: "generate markdown documentation of functions and operators",
    34  		RunE: func(cmd *cobra.Command, args []string) error {
    35  			outDir := filepath.Join("docs", "generated", "sql")
    36  			if len(args) > 0 {
    37  				outDir = args[0]
    38  			}
    39  
    40  			if stat, err := os.Stat(outDir); err != nil {
    41  				return err
    42  			} else if !stat.IsDir() {
    43  				return errors.Errorf("%q is not a directory", outDir)
    44  			}
    45  
    46  			if err := ioutil.WriteFile(
    47  				filepath.Join(outDir, "functions.md"), generateFunctions(builtins.AllBuiltinNames, true), 0644,
    48  			); err != nil {
    49  				return err
    50  			}
    51  			if err := ioutil.WriteFile(
    52  				filepath.Join(outDir, "aggregates.md"), generateFunctions(builtins.AllAggregateBuiltinNames, false), 0644,
    53  			); err != nil {
    54  				return err
    55  			}
    56  			if err := ioutil.WriteFile(
    57  				filepath.Join(outDir, "window_functions.md"), generateFunctions(builtins.AllWindowBuiltinNames, false), 0644,
    58  			); err != nil {
    59  				return err
    60  			}
    61  			return ioutil.WriteFile(
    62  				filepath.Join(outDir, "operators.md"), generateOperators(), 0644,
    63  			)
    64  		},
    65  	})
    66  }
    67  
    68  type operation struct {
    69  	left  string
    70  	right string
    71  	ret   string
    72  	op    string
    73  }
    74  
    75  func (o operation) String() string {
    76  	if o.right == "" {
    77  		return fmt.Sprintf("<code>%s</code>%s", o.op, linkTypeName(o.left))
    78  	}
    79  	return fmt.Sprintf("%s <code>%s</code> %s", linkTypeName(o.left), o.op, linkTypeName(o.right))
    80  }
    81  
    82  type operations []operation
    83  
    84  func (p operations) Len() int      { return len(p) }
    85  func (p operations) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
    86  func (p operations) Less(i, j int) bool {
    87  	if p[i].right != "" && p[j].right == "" {
    88  		return false
    89  	}
    90  	if p[i].right == "" && p[j].right != "" {
    91  		return true
    92  	}
    93  	if p[i].left != p[j].left {
    94  		return p[i].left < p[j].left
    95  	}
    96  	if p[i].right != p[j].right {
    97  		return p[i].right < p[j].right
    98  	}
    99  	return p[i].ret < p[j].ret
   100  }
   101  
   102  func generateOperators() []byte {
   103  	ops := make(map[string]operations)
   104  	for optyp, overloads := range tree.UnaryOps {
   105  		op := optyp.String()
   106  		for _, untyped := range overloads {
   107  			v := untyped.(*tree.UnaryOp)
   108  			ops[op] = append(ops[op], operation{
   109  				left: v.Typ.String(),
   110  				ret:  v.ReturnType.String(),
   111  				op:   op,
   112  			})
   113  		}
   114  	}
   115  	for optyp, overloads := range tree.BinOps {
   116  		op := optyp.String()
   117  		for _, untyped := range overloads {
   118  			v := untyped.(*tree.BinOp)
   119  			left := v.LeftType.String()
   120  			right := v.RightType.String()
   121  			ops[op] = append(ops[op], operation{
   122  				left:  left,
   123  				right: right,
   124  				ret:   v.ReturnType.String(),
   125  				op:    op,
   126  			})
   127  		}
   128  	}
   129  	for optyp, overloads := range tree.CmpOps {
   130  		op := optyp.String()
   131  		for _, untyped := range overloads {
   132  			v := untyped.(*tree.CmpOp)
   133  			left := v.LeftType.String()
   134  			right := v.RightType.String()
   135  			ops[op] = append(ops[op], operation{
   136  				left:  left,
   137  				right: right,
   138  				ret:   "bool",
   139  				op:    op,
   140  			})
   141  		}
   142  	}
   143  	var opstrs []string
   144  	for k, v := range ops {
   145  		sort.Sort(v)
   146  		opstrs = append(opstrs, k)
   147  	}
   148  	sort.Strings(opstrs)
   149  	b := new(bytes.Buffer)
   150  	seen := map[string]bool{}
   151  	for _, op := range opstrs {
   152  		fmt.Fprintf(b, "<table><thead>\n")
   153  		fmt.Fprintf(b, "<tr><td><code>%s</code></td><td>Return</td></tr>\n", op)
   154  		fmt.Fprintf(b, "</thead><tbody>\n")
   155  		for _, v := range ops[op] {
   156  			s := fmt.Sprintf("<tr><td>%s</td><td>%s</td></tr>\n", v.String(), linkTypeName(v.ret))
   157  			if seen[s] {
   158  				continue
   159  			}
   160  			seen[s] = true
   161  			b.WriteString(s)
   162  		}
   163  		fmt.Fprintf(b, "</tbody></table>")
   164  		fmt.Fprintln(b)
   165  	}
   166  	return b.Bytes()
   167  }
   168  
   169  // TODO(mjibson): use the exported value from sql/parser/pg_builtins.go.
   170  const notUsableInfo = "Not usable; exposed only for compatibility with PostgreSQL."
   171  
   172  func generateFunctions(from []string, categorize bool) []byte {
   173  	functions := make(map[string][]string)
   174  	seen := make(map[string]struct{})
   175  	md := markdown.New(markdown.XHTMLOutput(true), markdown.Nofollow(true))
   176  	for _, name := range from {
   177  		// NB: funcs can appear more than once i.e. upper/lowercase variants for
   178  		// faster lookups, so normalize to lowercase and de-dupe using a set.
   179  		name = strings.ToLower(name)
   180  		if _, ok := seen[name]; ok {
   181  			continue
   182  		}
   183  		seen[name] = struct{}{}
   184  		props, fns := builtins.GetBuiltinProperties(name)
   185  		if !props.ShouldDocument() {
   186  			continue
   187  		}
   188  		for _, fn := range fns {
   189  			if fn.Info == notUsableInfo {
   190  				continue
   191  			}
   192  			// We generate docs for both aggregates and window functions in separate
   193  			// files, so we want to omit them when processing all builtins.
   194  			if categorize && (props.Class == tree.AggregateClass || props.Class == tree.WindowClass) {
   195  				continue
   196  			}
   197  			args := fn.Types.String()
   198  
   199  			retType := fn.FixedReturnType()
   200  			ret := retType.String()
   201  
   202  			cat := props.Category
   203  			if cat == "" {
   204  				cat = strings.ToUpper(ret)
   205  			}
   206  			if !categorize {
   207  				cat = ""
   208  			}
   209  			extra := ""
   210  			if fn.Info != "" {
   211  				// Render the info field to HTML upfront, because Markdown
   212  				// won't do it automatically in a table context.
   213  				// Boo Markdown, bad Markdown.
   214  				// TODO(knz): Do not use Markdown.
   215  				info := md.RenderToString([]byte(fn.Info))
   216  				extra = fmt.Sprintf("<span class=\"funcdesc\">%s</span>", info)
   217  			}
   218  			s := fmt.Sprintf("<tr><td><a name=\"%s\"></a><code>%s(%s) &rarr; %s</code></td><td>%s</td></tr>", name, name, linkArguments(args), linkArguments(ret), extra)
   219  			functions[cat] = append(functions[cat], s)
   220  		}
   221  	}
   222  	var cats []string
   223  	for k, v := range functions {
   224  		sort.Strings(v)
   225  		cats = append(cats, k)
   226  	}
   227  	sort.Strings(cats)
   228  	// HACK: swap "Compatibility" to be last.
   229  	// TODO(dt): Break up generated list be one _include per category, to allow
   230  	// manually written copy on some sections.
   231  	for i, cat := range cats {
   232  		if cat == "Compatibility" {
   233  			cats = append(append(cats[:i], cats[i+1:]...), "Compatibility")
   234  			break
   235  		}
   236  	}
   237  	b := new(bytes.Buffer)
   238  	for _, cat := range cats {
   239  		if categorize {
   240  			fmt.Fprintf(b, "### %s functions\n\n", cat)
   241  		}
   242  		b.WriteString("<table>\n<thead><tr><th>Function &rarr; Returns</th><th>Description</th></tr></thead>\n")
   243  		b.WriteString("<tbody>\n")
   244  		b.WriteString(strings.Join(functions[cat], "\n"))
   245  		b.WriteString("</tbody>\n</table>\n\n")
   246  	}
   247  	return b.Bytes()
   248  }
   249  
   250  var linkRE = regexp.MustCompile(`([a-z]+)([\.\[\]]*)$`)
   251  
   252  func linkArguments(t string) string {
   253  	sp := strings.Split(t, ", ")
   254  	for i, s := range sp {
   255  		sp[i] = linkRE.ReplaceAllStringFunc(s, func(s string) string {
   256  			match := linkRE.FindStringSubmatch(s)
   257  			s = linkTypeName(match[1])
   258  			return s + match[2]
   259  		})
   260  	}
   261  	return strings.Join(sp, ", ")
   262  }
   263  
   264  func linkTypeName(s string) string {
   265  	s = strings.TrimSuffix(s, "{}")
   266  	s = strings.TrimSuffix(s, "{*}")
   267  	name := s
   268  	switch s {
   269  	case "timestamptz":
   270  		s = "timestamp"
   271  	}
   272  	s = strings.TrimSuffix(s, "[]")
   273  	s = strings.TrimSuffix(s, "*")
   274  	switch s {
   275  	case "int", "decimal", "float", "bool", "date", "timestamp", "interval", "string", "bytes",
   276  		"inet", "uuid", "collatedstring", "time":
   277  		s = fmt.Sprintf("<a href=\"%s.html\">%s</a>", s, name)
   278  	}
   279  	return s
   280  }