github.com/cznic/ql@v1.2.1-0.20181122101857-b60735abf8a0/ql/main.go (about) 1 // Copyright 2014 The ql Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Command ql is a utility to explore a database, prototype a schema or test 6 // drive a query, etc. 7 // 8 // Installation: 9 // 10 // $ go get github.com/cznic/ql/ql 11 // 12 // Usage: 13 // 14 // ql [-db name] [-schema regexp] [-tables regexp] [-fld] statement_list 15 // 16 // Options: 17 // 18 // -db name Name of the database to use. Defaults to "ql.db". 19 // If the DB file does not exists it is created automatically. 20 // 21 // -schema re If re != "" show the CREATE statements of matching tables and exit. 22 // 23 // -tables re If re != "" show the matching table names and exit. 24 // 25 // -fld First row of a query result set will show field names. 26 // 27 // statement_list QL statements to execute. 28 // If no non flag arguments are present, ql reads from stdin. 29 // The list is wrapped into an automatic transaction. 30 // 31 // -t Report and measure time to execute, including creating/opening and closing the DB. 32 // 33 // Example: 34 // 35 // $ ql 'create table t (i int, s string)' 36 // $ ql << EOF 37 // > insert into t values 38 // > (1, "a"), 39 // > (2, "b"), 40 // > (3, "c"), 41 // > EOF 42 // $ ql 'select * from t' 43 // 3, "c" 44 // 2, "b" 45 // 1, "a" 46 // $ ql -fld 'select * from t where i != 2 order by s' 47 // "i", "s" 48 // 1, "a" 49 // 3, "c" 50 // $ 51 package main 52 53 import ( 54 "bufio" 55 "flag" 56 "fmt" 57 "io/ioutil" 58 "log" 59 "os" 60 "regexp" 61 "runtime" 62 "sort" 63 "strings" 64 "time" 65 66 "github.com/cznic/ql" 67 ) 68 69 func str(data []interface{}) string { 70 a := make([]string, len(data)) 71 for i, v := range data { 72 switch x := v.(type) { 73 case string: 74 a[i] = fmt.Sprintf("%q", x) 75 default: 76 a[i] = fmt.Sprint(x) 77 } 78 } 79 return strings.Join(a, ", ") 80 } 81 82 func main() { 83 if err := do(); err != nil { 84 log.Fatal(err) 85 } 86 } 87 88 type config struct { 89 db string 90 flds bool 91 schema string 92 tables string 93 time bool 94 help bool 95 interactive bool 96 } 97 98 func (c *config) parse() { 99 db := flag.String("db", "ql.db", "The DB file to open. It'll be created if missing.") 100 flds := flag.Bool("fld", false, "Show recordset's field names.") 101 schema := flag.String("schema", "", "If non empty, show the CREATE statements of matching tables and exit.") 102 tables := flag.String("tables", "", "If non empty, list matching table names and exit.") 103 time := flag.Bool("t", false, "Measure and report time to execute the statement(s) including DB create/open/close.") 104 help := flag.Bool("h", false, "Shows this help text.") 105 interactive := flag.Bool("i", false, "runs in interactive mode") 106 flag.Parse() 107 c.flds = *flds 108 c.db = *db 109 c.schema = *schema 110 c.tables = *tables 111 c.time = *time 112 c.help = *help 113 c.interactive = *interactive 114 } 115 116 func do() (err error) { 117 cfg := &config{} 118 cfg.parse() 119 if cfg.help { 120 flag.PrintDefaults() 121 return nil 122 } 123 if flag.NArg() == 0 && !cfg.interactive { 124 125 // Somehow we expect input to the ql tool. 126 // This will block trying to read input from stdin 127 b, err := ioutil.ReadAll(os.Stdin) 128 if err != nil || len(b) == 0 { 129 flag.PrintDefaults() 130 return nil 131 } 132 db, err := ql.OpenFile(cfg.db, &ql.Options{CanCreate: true}) 133 if err != nil { 134 return err 135 } 136 defer func() { 137 ec := db.Close() 138 switch { 139 case ec != nil && err != nil: 140 log.Println(ec) 141 case ec != nil: 142 err = ec 143 } 144 }() 145 return run(cfg, bufio.NewWriter(os.Stdout), string(b), db) 146 } 147 db, err := ql.OpenFile(cfg.db, &ql.Options{CanCreate: true}) 148 if err != nil { 149 return err 150 } 151 152 defer func() { 153 ec := db.Close() 154 switch { 155 case ec != nil && err != nil: 156 log.Println(ec) 157 case ec != nil: 158 err = ec 159 } 160 }() 161 r := bufio.NewReader(os.Stdin) 162 o := bufio.NewWriter(os.Stdout) 163 if cfg.interactive { 164 for { 165 o.WriteString("ql> ") 166 o.Flush() 167 src, err := readSrc(cfg.interactive, r) 168 if err != nil { 169 return err 170 } 171 err = run(cfg, o, src, db) 172 if err != nil { 173 fmt.Fprintln(o, err) 174 o.Flush() 175 } 176 } 177 return nil 178 } 179 src, err := readSrc(cfg.interactive, r) 180 if err != nil { 181 return err 182 } 183 return run(cfg, o, src, db) 184 } 185 186 func readSrc(i bool, in *bufio.Reader) (string, error) { 187 if i { 188 return in.ReadString('\n') 189 } 190 var src string 191 switch n := flag.NArg(); n { 192 case 0: 193 b, err := ioutil.ReadAll(in) 194 if err != nil { 195 return "", err 196 } 197 198 src = string(b) 199 default: 200 a := make([]string, n) 201 for i := range a { 202 a[i] = flag.Arg(i) 203 } 204 src = strings.Join(a, " ") 205 } 206 return src, nil 207 } 208 209 func run(cfg *config, o *bufio.Writer, src string, db *ql.DB) (err error) { 210 defer o.Flush() 211 if cfg.interactive { 212 src = strings.TrimSpace(src) 213 if strings.HasPrefix(src, "\\") || 214 strings.HasPrefix(src, ".") { 215 switch src { 216 case "\\clear", ".clear": 217 switch runtime.GOOS { 218 case "darwin", "linux": 219 fmt.Fprintln(o, "\033[H\033[2J") 220 default: 221 fmt.Fprintln(o, "clear not supported in this system") 222 } 223 return nil 224 case "\\q", "\\exit", ".q", ".exit": 225 // we make sure to close the database before exiting 226 db.Close() 227 os.Exit(1) 228 } 229 } 230 231 } 232 233 t0 := time.Now() 234 if cfg.time { 235 defer func() { 236 fmt.Fprintf(os.Stderr, "%s\n", time.Since(t0)) 237 }() 238 } 239 if pat := cfg.schema; pat != "" { 240 re, err := regexp.Compile(pat) 241 if err != nil { 242 return err 243 } 244 245 nfo, err := db.Info() 246 if err != nil { 247 return err 248 } 249 250 r := []string{} 251 for _, ti := range nfo.Tables { 252 if !re.MatchString(ti.Name) { 253 continue 254 } 255 256 a := []string{} 257 for _, ci := range ti.Columns { 258 a = append(a, fmt.Sprintf("%s %s", ci.Name, ci.Type)) 259 } 260 r = append(r, fmt.Sprintf("CREATE TABLE %s (%s);", ti.Name, strings.Join(a, ", "))) 261 } 262 sort.Strings(r) 263 if len(r) != 0 { 264 fmt.Fprintln(o, strings.Join(r, "\n")) 265 } 266 return nil 267 } 268 269 if pat := cfg.tables; pat != "" { 270 re, err := regexp.Compile(pat) 271 if err != nil { 272 return err 273 } 274 275 nfo, err := db.Info() 276 if err != nil { 277 return err 278 } 279 280 r := []string{} 281 for _, ti := range nfo.Tables { 282 if !re.MatchString(ti.Name) { 283 continue 284 } 285 286 r = append(r, ti.Name) 287 } 288 sort.Strings(r) 289 if len(r) != 0 { 290 fmt.Fprintln(o, strings.Join(r, "\n")) 291 } 292 return nil 293 } 294 295 src = strings.TrimSpace(src) 296 297 commit := "COMMIT;" 298 if !strings.HasSuffix(src, ";") { 299 commit = "; " + commit 300 } 301 src = "BEGIN TRANSACTION; " + src + commit 302 l, err := ql.Compile(src) 303 if err != nil { 304 log.Println(src) 305 return err 306 } 307 308 rs, i, err := db.Execute(ql.NewRWCtx(), l) 309 if err != nil { 310 a := strings.Split(strings.TrimSpace(fmt.Sprint(l)), "\n") 311 return fmt.Errorf("%v: %s", err, a[i]) 312 } 313 314 if len(rs) == 0 { 315 return 316 } 317 318 switch { 319 case l.IsExplainStmt(): 320 return rs[len(rs)-1].Do(cfg.flds, func(data []interface{}) (bool, error) { 321 fmt.Fprintln(o, data[0]) 322 return true, nil 323 }) 324 default: 325 for _, rst := range rs { 326 err = rst.Do(cfg.flds, func(data []interface{}) (bool, error) { 327 fmt.Fprintln(o, str(data)) 328 return true, nil 329 }) 330 o.Flush() 331 if err != nil { 332 return 333 } 334 } 335 return 336 } 337 }