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) → %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 → 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 }