Skip to content

Commit

Permalink
cmd: very basic REPL implementation (#97)
Browse files Browse the repository at this point in the history
* cmd: very basic REPL implementation

Basic REPL that only supports query executions.
  • Loading branch information
ajnavarro authored and smola committed Jan 13, 2017
1 parent 2bbc3bd commit 761ba5e
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 131 deletions.
1 change: 1 addition & 0 deletions cmd/gitql/main.go
Expand Up @@ -10,6 +10,7 @@ import (
func main() {
parser := flags.NewNamedParser("gitql", flags.Default)
parser.AddCommand("query", "Execute a SQL query a repository.", "", &CmdQuery{})
parser.AddCommand("shell", "Start an interactive session.", "", &CmdShell{})
parser.AddCommand("version", "Show the version information.", "", &CmdVersion{})

_, err := parser.Parse()
Expand Down
134 changes: 3 additions & 131 deletions cmd/gitql/query.go
@@ -1,31 +1,12 @@
package main

import (
"errors"
"fmt"
"io"
"os"
"path/filepath"

"github.com/gitql/gitql"
gitqlgit "github.com/gitql/gitql/git"
"github.com/gitql/gitql/internal/format"
"github.com/gitql/gitql/sql"

"gopkg.in/src-d/go-git.v4"
)

type CmdQuery struct {
cmd
cmdQueryBase

Path string `short:"p" long:"path" description:"Path where the git repository is located"`
Format string `short:"f" long:"format" default:"pretty" description:"Ouptut format. Formats supported: pretty, csv, json."`
Args struct {
SQL string `positional-arg-name:"sql" required:"true" description:"SQL query to execute"`
} `positional-args:"yes"`

r *git.Repository
db sql.Database
}

func (c *CmdQuery) Execute(args []string) error {
Expand All @@ -37,119 +18,10 @@ func (c *CmdQuery) Execute(args []string) error {
return err
}

return c.executeQuery()
}

func (c *CmdQuery) validate() error {
var err error
c.Path, err = findDotGitFolder(c.Path)
return err
}

func (c *CmdQuery) buildDatabase() error {
c.print("opening %q repository...\n", c.Path)

var err error
c.r, err = git.NewFilesystemRepository(c.Path)
if err != nil {
return err
}

empty, err := c.r.IsEmpty()
if err != nil {
return err
}

if empty {
return errors.New("error: the repository is empty")
}

head, err := c.r.Head()
if err != nil {
return err
}

c.print("current HEAD %q\n", head.Hash())

name := filepath.Base(filepath.Join(c.Path, ".."))
c.db = gitqlgit.NewDatabase(name, c.r)
return nil
}

func (c *CmdQuery) executeQuery() error {
c.print("executing %q at %q\n", c.Args.SQL, c.db.Name())

fmt.Println(c.Args.SQL)
e := gitql.New()
e.AddDatabase(c.db)
schema, iter, err := e.Query(c.Args.SQL)
if err != nil {
return err
}

return c.printQuery(schema, iter)
}

func (c *CmdQuery) printQuery(schema sql.Schema, iter sql.RowIter) error {
f, err := format.NewFormat(c.Format, os.Stdout)
schema, rowIter, err := c.executeQuery(c.Args.SQL)
if err != nil {
return err
}

headers := []string{}
for _, f := range schema {
headers = append(headers, f.Name)
}

if err := f.WriteHeader(headers); err != nil {
return err
}

for {
row, err := iter.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}

record := make([]interface{}, len(row))
for i := 0; i < len(row); i++ {
record[i] = row[i]
}

if err := f.Write(record); err != nil {
return err
}
}

return f.Close()
}

func findDotGitFolder(path string) (string, error) {
if path == "" {
var err error
path, err = os.Getwd()
if err != nil {
return "", err
}
}

git := filepath.Join(path, ".git")
_, err := os.Stat(git)
if err == nil {
return git, nil
}

if !os.IsNotExist(err) {
return "", err
}

next := filepath.Join(path, "..")
if next == path {
return "", errors.New("unable to find a git repository")
}

return findDotGitFolder(next)
return c.printQuery(schema, rowIter, c.Format)
}
134 changes: 134 additions & 0 deletions cmd/gitql/query_base.go
@@ -0,0 +1,134 @@
package main

import (
"errors"
"io"
"os"
"path/filepath"

"github.com/gitql/gitql"
gitqlgit "github.com/gitql/gitql/git"
"github.com/gitql/gitql/internal/format"
"github.com/gitql/gitql/sql"

"gopkg.in/src-d/go-git.v4"
)

type cmdQueryBase struct {
cmd

Path string `short:"p" long:"path" description:"Path where the git repository is located"`

db sql.Database
e *gitql.Engine
}

func (c *cmdQueryBase) validate() error {
var err error
c.Path, err = findDotGitFolder(c.Path)
return err
}

func (c *cmdQueryBase) buildDatabase() error {
c.print("opening %q repository...\n", c.Path)

var err error
r, err := git.NewFilesystemRepository(c.Path)
if err != nil {
return err
}

empty, err := r.IsEmpty()
if err != nil {
return err
}

if empty {
return errors.New("error: the repository is empty")
}

head, err := r.Head()
if err != nil {
return err
}

c.print("current HEAD %q\n", head.Hash())

name := filepath.Base(filepath.Join(c.Path, ".."))

c.db = gitqlgit.NewDatabase(name, r)
c.e = gitql.New()
c.e.AddDatabase(c.db)

return nil
}

func (c *cmdQueryBase) executeQuery(sql string) (sql.Schema, sql.RowIter, error) {
c.print("executing %q at %q\n", sql, c.db.Name())

return c.e.Query(sql)
}

func (c *cmdQueryBase) printQuery(schema sql.Schema, iter sql.RowIter, formatId string) error {
f, err := format.NewFormat(formatId, os.Stdout)
if err != nil {
return err
}

headers := []string{}
for _, f := range schema {
headers = append(headers, f.Name)
}

if err := f.WriteHeader(headers); err != nil {
return err
}

for {
row, err := iter.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}

dataRow := make([]interface{}, len(row))
for i := range row {
dataRow[i] = interface{}(row[i])
}

if err := f.Write(dataRow); err != nil {
return err
}
}

return f.Close()
}

func findDotGitFolder(path string) (string, error) {
if path == "" {
var err error
path, err = os.Getwd()
if err != nil {
return "", err
}
}

git := filepath.Join(path, ".git")
_, err := os.Stat(git)
if err == nil {
return git, nil
}

if !os.IsNotExist(err) {
return "", err
}

next := filepath.Join(path, "..")
if next == path {
return "", errors.New("unable to find a git repository")
}

return findDotGitFolder(next)
}
89 changes: 89 additions & 0 deletions cmd/gitql/shell.go
@@ -0,0 +1,89 @@
package main

import (
"bufio"
"bytes"
"fmt"
"os"
"strings"
)

type CmdShell struct {
cmdQueryBase
}

func (c *CmdShell) Execute(args []string) error {
if err := c.validate(); err != nil {
return err
}

if err := c.buildDatabase(); err != nil {
return err
}

fmt.Print(`
gitQL SHELL
-----------
You must end your queries with ';'
`)

s := bufio.NewScanner(os.Stdin)

s.Split(scanQueries)

for {
fmt.Print("!> ")

if !s.Scan() {
break
}

query := s.Text()

query = strings.Replace(query, "\n", " ", -1)
query = strings.TrimSpace(query)

fmt.Printf("\n--> Executing query: %s\n\n", query)

schema, rowIter, err := c.executeQuery(query)
if err != nil {
c.printError(err)
continue
}

if err := c.printQuery(schema, rowIter, "pretty"); err != nil {
c.printError(err)
}
}

return s.Err()
}

func (c *CmdShell) printError(err error) {
fmt.Printf("ERROR: %v\n\n", err)
}

func scanQueries(data []byte, atEOF bool) (int, []byte, error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, ';'); i >= 0 {
// We have a full newline-terminated line.
return i + 1, dropCR(data[0:i]), nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), dropCR(data), nil
}
// Request more data.
return 0, nil, nil
}

// dropCR drops a terminal \r from the data.
func dropCR(data []byte) []byte {
if len(data) > 0 && data[len(data)-1] == '\r' {
return data[0 : len(data)-1]
}
return data
}

0 comments on commit 761ba5e

Please sign in to comment.