github.com/vescale/zgraph@v0.0.0-20230410094002-959c02d50f95/cmd/zgraph/main.go (about)

     1  // Copyright 2022 zGraph Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package main
    16  
    17  import (
    18  	"context"
    19  	"database/sql"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"strings"
    24  
    25  	tea "github.com/charmbracelet/bubbletea"
    26  	"github.com/jedib0t/go-pretty/v6/table"
    27  	"github.com/jedib0t/go-pretty/v6/text"
    28  	"github.com/knz/bubbline"
    29  	"github.com/spf13/cobra"
    30  	_ "github.com/vescale/zgraph"
    31  )
    32  
    33  type options struct {
    34  	global struct {
    35  		dataDir string
    36  	}
    37  	play struct {
    38  	}
    39  	serve struct {
    40  	}
    41  }
    42  
    43  func main() {
    44  	var opt options
    45  	cmd := cobra.Command{
    46  		Use: "zgraph [command] [flags]",
    47  		Long: `zgraph is a command line tool for wrapping the embeddable graph
    48  database 'github.com/vescale/zgraph'', and provide a convenient
    49  approach to experiencing the power to graph database`,
    50  		Example: strings.TrimLeft(`
    51    zgraph play                      # Launch a zGraph playground
    52    zgraph play --datadir ./test     # Specify the data directory of the playground
    53    zgraph service                   # Launch a zGraph as a data service`, "\n"),
    54  		RunE: func(cmd *cobra.Command, args []string) error {
    55  			return cmd.Help()
    56  		},
    57  		SilenceErrors:         true,
    58  		DisableFlagsInUseLine: true,
    59  	}
    60  
    61  	// Global options
    62  	cmd.Flags().StringVarP(&opt.global.dataDir, "datadir", "D", "./data", "Specify the data directory path")
    63  
    64  	// Subcommands
    65  	cmd.AddCommand(playCmd(&opt))
    66  	cmd.AddCommand(servCmd(&opt))
    67  
    68  	err := cmd.Execute()
    69  	cobra.CheckErr(err)
    70  }
    71  
    72  func playCmd(opt *options) *cobra.Command {
    73  	cmd := &cobra.Command{
    74  		Use:   "play -D <dirname>",
    75  		Short: "Run the zgraph as a playground",
    76  		RunE: func(cmd *cobra.Command, args []string) error {
    77  			db, err := sql.Open("zgraph", opt.global.dataDir)
    78  			if err != nil {
    79  				return err
    80  			}
    81  			defer db.Close()
    82  
    83  			conn, err := db.Conn(context.Background())
    84  			if err != nil {
    85  				return err
    86  			}
    87  			defer conn.Close()
    88  
    89  			interact(conn)
    90  
    91  			return nil
    92  		},
    93  		SilenceErrors: true,
    94  	}
    95  
    96  	return cmd
    97  }
    98  
    99  func servCmd(opt *options) *cobra.Command {
   100  	cmd := &cobra.Command{
   101  		Use:   "service -D <dirname> -L :8080 --apis api.yaml",
   102  		Short: "Run the zgraph as a data service instance",
   103  		RunE: func(cmd *cobra.Command, args []string) error {
   104  			// TODO: implement the data API.
   105  			return nil
   106  		},
   107  		SilenceErrors: true,
   108  	}
   109  
   110  	return cmd
   111  }
   112  
   113  func interact(conn *sql.Conn) {
   114  	fmt.Println("Welcome to zGraph interactive command line.")
   115  
   116  	m := bubbline.New()
   117  	m.Prompt = "zgraph> "
   118  	m.NextPrompt = "      > "
   119  	m.CheckInputComplete = func(input [][]rune, line, col int) bool {
   120  		inputLine := string(input[line])
   121  		if len(input) == 1 && strings.TrimSpace(inputLine) == "" {
   122  			return true
   123  		}
   124  		return strings.Contains(inputLine, ";")
   125  	}
   126  
   127  	var lastStmt string
   128  	for {
   129  		m.Reset()
   130  		if _, err := tea.NewProgram(m).Run(); err != nil {
   131  			outputError(err)
   132  			continue
   133  		}
   134  
   135  		if m.Err != nil {
   136  			if m.Err == io.EOF {
   137  				// No more input.
   138  				break
   139  			}
   140  			if errors.Is(m.Err, bubbline.ErrInterrupted) {
   141  				// Entered Ctrl+C to cancel input.
   142  				fmt.Println("^C")
   143  			} else {
   144  				outputError(m.Err)
   145  			}
   146  			continue
   147  		}
   148  
   149  		input := lastStmt + m.Value()
   150  		stmts := strings.Split(input, ";")
   151  		for i := 0; i < len(stmts)-1; i++ {
   152  			stmt := strings.TrimSpace(stmts[i])
   153  			if stmt == "" {
   154  				continue
   155  			}
   156  			runQuery(conn, stmt)
   157  		}
   158  		lastStmt = stmts[len(stmts)-1]
   159  	}
   160  }
   161  
   162  func runQuery(conn *sql.Conn, query string) {
   163  	rows, err := conn.QueryContext(context.Background(), query)
   164  	if err != nil {
   165  		outputError(err)
   166  		return
   167  	}
   168  	defer rows.Close()
   169  
   170  	output, err := render(rows)
   171  	if err != nil {
   172  		outputError(err)
   173  		return
   174  	}
   175  	if len(output) > 0 {
   176  		fmt.Println(output)
   177  	}
   178  }
   179  
   180  func render(rows *sql.Rows) (string, error) {
   181  	w := table.NewWriter()
   182  	w.Style().Format = table.FormatOptions{
   183  		Footer: text.FormatDefault,
   184  		Header: text.FormatDefault,
   185  		Row:    text.FormatDefault,
   186  	}
   187  
   188  	cols, err := rows.Columns()
   189  	if err != nil {
   190  		return "", err
   191  	}
   192  
   193  	if len(cols) > 0 {
   194  		var header []any
   195  		for _, col := range cols {
   196  			header = append(header, col)
   197  		}
   198  		w.AppendHeader(header)
   199  	}
   200  
   201  	dest := make([]any, len(cols))
   202  	for i := range cols {
   203  		var anyStr sql.NullString
   204  		dest[i] = &anyStr
   205  	}
   206  
   207  	for rows.Next() {
   208  		if err := rows.Scan(dest...); err != nil {
   209  			return "", err
   210  		}
   211  		var row []any
   212  		for _, d := range dest {
   213  			anyStr := *d.(*sql.NullString)
   214  			row = append(row, anyStr.String)
   215  		}
   216  		w.AppendRow(row)
   217  	}
   218  	if rows.Err() != nil {
   219  		return "", rows.Err()
   220  	}
   221  	return w.Render(), nil
   222  }
   223  
   224  func outputError(err error) {
   225  	fmt.Printf("Error: %v\n", err)
   226  }