Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLIツールの追加 #29

Merged
merged 4 commits into from
Aug 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ server/bin
**/bin/lambda
**/bin/lambda.zip
!*.go
cmd/twowaysql/twowaysql

# Goland
.idea

.env.local

.DS_Store
72 changes: 71 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
## Installation

```
go get github.com/future-architect/go-twowaysql
go get github.com/future-architect/go-twowaysql
```

## Usage
Expand Down Expand Up @@ -71,7 +71,77 @@ func main() {
//Person{EmpNo:2, DeptNo:11, FirstName:"Malvina", LastName:"FitzSimons", Email:"malvinafitzsimons@example.com"}

}
```

## CLI Tool

CLI tool `twowaysql` provides helper functions about two way sql

```
go install github.com/future-architect/go-twowaysql/...
```

### Database Connection

To connect database, *driver* and *source* strings are required. Driver is like `pgx` and source is `postgres://user:pass@host/dbname?sslmode=disable`.

You can pass them via options(`-d DRIVER`, `--driver=DRIVER`, `-c SOURCE`, `--source=SOURCE`) or by using `TWOWAYSQL_DRIVER`/`TWOWAYSQL_CONNECTION` environment variables.

This tool also read `.env` and `.env.local` files.

### Execute SQL

```
$ twowaysql run -p first_name=Malvina testdata/postgres/sql/select_person.sql
┌───────────────────────────────┬────────────┬────────────┐
│ email │ first_name │ last_name │
╞═══════════════════════════════╪════════════╪════════════╡
│ malvinafitzsimons@example.com │ Malvina │ FitzSimons │
└───────────────────────────────┴────────────┴────────────┘

Query takes 22.804166ms
```

* -p, --param=PARAM ... Parameter in single value or JSON (name=bob, or {"name": "bob"})
* -e, --explain Run with EXPLAIN to show execution plan
* -r, --rollback Run within transaction and then rollback
* -o, --output-format=default Result output format (default, md, json, yaml)

### Evaluate 2-Way-SQL

```
$ twowaysql eval -p first_name=Malvina testdata/postgres/sql/select_person.sql
# Converted Source

SELECT email, first_name, last_name FROM persons WHERE first_name=?/*first_name*/;

# Parameters

- Malvina
```

### Customize CLI tool

by default `twowaysql` integrated with the following drivers:

* ``github.com/jackc/pgx/v4``
* ``modernc.org/sqlite``
* ``github.com/go-sql-driver/mysql``

If you want to add/remove [drivers](https://github.com/golang/go/wiki/SQLDrivers), create simple main package and call `cli.Main()`.

```go
package main

import (
_ "github.com/sijms/go-ora/v2" // Oracle

"github.com/future-architect/go-twowaysql/cli"
)

func main() {
cli.Main()
}
```

## License
Expand Down
45 changes: 45 additions & 0 deletions cli/eval.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package cli

import (
"fmt"
"os"

"github.com/alecthomas/chroma/quick"
"github.com/fatih/color"
"github.com/goccy/go-yaml"

"github.com/future-architect/go-twowaysql"
)

func eval(srcPath string, params []string) error {
stat, _ := os.Stdin.Stat()
var finalParams map[string]any
var err error
if (stat.Mode() & os.ModeCharDevice) == 0 {
finalParams, err = parseParams(params, os.Stdout)
} else {
finalParams, err = parseParams(params, nil)
}
if err != nil {
return err
}

src, err := os.ReadFile(srcPath)
if err != nil {
return err
}

convertedSrc, sqlParams, err := twowaysql.Eval(string(src), finalParams)
if err != nil {
return err
}
title := color.New(color.FgHiRed, color.Bold)
title.Println("# Converted Source")
fmt.Printf("\n")
quick.Highlight(os.Stdout, convertedSrc, "sql", "terminal", "monokai")
title.Println("\n# Parameters")
fmt.Printf("\n")
sqlParamYaml, _ := yaml.Marshal(sqlParams)
quick.Highlight(os.Stdout, string(sqlParamYaml), "yaml", "terminal", "monokai")
return nil
}
14 changes: 14 additions & 0 deletions cli/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package cli

import (
"database/sql"
"os"

"github.com/alecthomas/chroma/quick"
"github.com/goccy/go-yaml"
)

func listDriver() {
drivers, _ := yaml.Marshal(sql.Drivers())
quick.Highlight(os.Stdout, string(drivers), "yaml", "terminal", "monokai")
}
51 changes: 51 additions & 0 deletions cli/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cli

import (
"database/sql"
"fmt"
"os"

"github.com/joho/godotenv"
"gopkg.in/alecthomas/kingpin.v2"
)

var (
app = kingpin.New("twowaysql", "2-Way-SQL helper tool")
driver = app.Flag("driver", `Database driver. TWOWAYSQL_DRIVER envvar is acceptable.`).Short('d').Envar("TWOWAYSQL_DRIVER").Enum(sql.Drivers()...)
source = app.Flag("source", `Database source (e.g. postgres://user:pass@host/dbname?sslmode=disable). TWOWAYSQL_CONNECTION envvar is acceptable.`).Short('c').Envar("TWOWAYSQL_CONNECTION").String()

runCommand = app.Command("run", "Execute SQL file")
runFile = runCommand.Arg("file", "SQL file").Required().NoEnvar().ExistingFile()
runParam = runCommand.Flag("param", "Parameter in single value or JSON (name=bob, or {\"name\": \"bob\"})").Short('p').NoEnvar().Strings()
runExplain = runCommand.Flag("explain", "Run with EXPLAIN to show execution plan").Short('e').NoEnvar().Bool()
runRollback = runCommand.Flag("rollback", "Run within transaction and then rollback").Short('r').NoEnvar().Bool()
runOutputFormat = runCommand.Flag("output-format", "Result output format (default, md, json, yaml)").Short('o').Default("default").Enum("default", "md", "json", "yaml")

evalCommand = app.Command("eval", "Parse and evaluate SQL")
evalFile = evalCommand.Arg("file", "SQL file").Required().NoEnvar().ExistingFile()
evalParam = evalCommand.Flag("param", "Parameter in single value or JSON (name=bob, or {\"name\": \"bob\"})").Short('p').NoEnvar().Strings()

listCommand = app.Command("list", "Inspection command")
listDriverCommand = listCommand.Command("driver", "Show supported drivers")
)

func Main() {
godotenv.Load(".env.local", ".env")

switch kingpin.MustParse(app.Parse(os.Args[1:])) {
case listDriverCommand.FullCommand():
listDriver()
case evalCommand.FullCommand():
err := eval(*evalFile, *evalParam)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
case runCommand.FullCommand():
err := run(*driver, *source, *runFile, *runParam, *runExplain, *runRollback, *runOutputFormat, nil)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
}
}
51 changes: 51 additions & 0 deletions cli/param.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cli

import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"

"github.com/hashicorp/go-multierror"
)

func parseParams(params []string, stdin io.Reader) (map[string]any, error) {
err := &multierror.Error{}

result := make(map[string]any)

if stdin != nil {
d := json.NewDecoder(stdin)
e := d.Decode(&result)
if e != nil {
err = multierror.Append(err, fmt.Errorf("JSON parse error: %w", e))
}
}

for _, s := range params {
if strings.HasPrefix(s, "{") {
d := json.NewDecoder(strings.NewReader(s))
e := d.Decode(&result)
if e != nil {
err = multierror.Append(err, fmt.Errorf("JSON parse error: %w", e))
}
} else {
key, raw, found := strings.Cut(s, "=")
if !found {
err = multierror.Append(err, fmt.Errorf("invalid format: '%s' key=value or JSON is supported", s))
}
if value, err := strconv.ParseFloat(raw, 64); err == nil {
result[key] = value
} else {
result[key] = raw
}
}
}

if err.Len() > 0 {
return nil, err
}

return result, nil
}
97 changes: 97 additions & 0 deletions cli/param_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package cli

import (
"io"
"math"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
"gotest.tools/v3/assert"
)

func Test_parseParams(t *testing.T) {
type args struct {
params []string
stdin io.Reader
}
tests := []struct {
name string
args args
want map[string]any
wantErr string
}{
{
name: "empty values",
args: args{
params: []string{},
},
want: map[string]any{},
},
{
name: "single raw values",
args: args{
params: []string{"name=tokyo", "utcOffset=9", "lat=35.6", "lon=139.6"},
},
want: map[string]any{
"name": "tokyo",
"utcOffset": float64(9),
"lat": 35.6,
"lon": 139.6,
},
},
{
name: "JSON values",
args: args{
params: []string{`{"name": "tokyo", "utcOffset": 9, "lat": 35.6, "lon": 139.6}`},
},
want: map[string]any{
"name": "tokyo",
"utcOffset": float64(9),
"lat": 35.6,
"lon": 139.6,
},
},
{
name: "JSON from stdin",
args: args{
stdin: strings.NewReader(`{"name": "tokyo", "utcOffset": 9, "lat": 35.6, "lon": 139.6}`),
},
want: map[string]any{
"name": "tokyo",
"utcOffset": float64(9),
"lat": 35.6,
"lon": 139.6,
},
},
{
name: "invalid error (1): key only",
args: args{
params: []string{"name"},
},
want: map[string]any{},
wantErr: "1 error occurred:\n\t* invalid format: 'name' key=value or JSON is supported\n\n",
},
{
name: "invalid error (2): JSON parse error",
args: args{
params: []string{`{"name": "tokyo",}`},
},
want: map[string]any{},
wantErr: "1 error occurred:\n\t* JSON parse error: invalid character '}' looking for beginning of object key string\n\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseParams(tt.args.params, tt.args.stdin)
if tt.wantErr != "" {
assert.Error(t, err, tt.wantErr)
} else {
assert.NilError(t, err)
assert.DeepEqual(t, got, tt.want, cmp.Comparer(func(x, y float64) bool {
return math.Abs(x-y) < 0.01
}))
}
})
}
}
Loading