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  }