Skip to content

Commit

Permalink
Merge pull request #17 from nao1215/feat-import-tsv
Browse files Browse the repository at this point in the history
Feat import tsv
  • Loading branch information
nao1215 authored Nov 12, 2022
2 parents b017a58 + b298074 commit b23c917
Show file tree
Hide file tree
Showing 15 changed files with 197 additions and 23 deletions.
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
[![reviewdog](https://github.com/nao1215/sqly/actions/workflows/reviewdog.yml/badge.svg)](https://github.com/nao1215/sqly/actions/workflows/reviewdog.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/nao1215/sqly)](https://goreportcard.com/report/github.com/nao1215/sqly)
![GitHub](https://img.shields.io/github/license/nao1215/sqly)
# sqly - execute SQL against CSV / JSON with shell.
# execute SQL against CSV / TSV / JSON with shell.
![demo](./doc/demo.gif)

**sqly** command imports CSV / JSON file(s) into an in-memory DB and executes SQL against the CSV / JSON. sqly uses [SQLite3](https://www.sqlite.org/index.html) as its DB. So, sql syntax is same as SQLite3.
**sqly** command imports CSV / TSV / JSON file(s) into an in-memory DB and executes SQL against the CSV / TSV / JSON. sqly uses [SQLite3](https://www.sqlite.org/index.html) as its DB. So, sql syntax is same as SQLite3.

The sqly command has sqly-shell. You can interactively execute SQL with sql completion and command history. Of course, you can also execute SQL without running the sqly-shell.

Expand All @@ -20,7 +20,7 @@ $ go install github.com/nao1215/sqly@latest


# How to use
sqly command automatically imports the CSV / JSON file into the DB when you pass a CSV / JSON file as an argument. DB table name is the same as the file name (e.g., if you import user.csv, sqly command create the user table)
sqly command automatically imports the CSV / TSV / JSON file into the DB when you pass a CSV / TSV / JSON file as an argument. DB table name is the same as the file name (e.g., if you import user.csv, sqly command create the user table)

## --sql option: execute sql in terminal
--sql option takes an SQL statement as an optional argument. You pass file path(s) as arguments to the sqly command. sqly command import them. sqly command automatically determines the file format from the file extension.
Expand All @@ -36,7 +36,7 @@ $ sqly --sql "SELECT user_name, position FROM user INNER JOIN identifier ON user
```

## Change output format
sqly command output sql results in ASCII table format (in faorto), CSV format (--csv option), and JSON format (--json option). This means that conversion between csv and json is supported.
sqly command output sql results in ASCII table format, CSV format (--csv option), TSV format (--tsv option) and JSON format (--json option). This means that conversion between csv and json is supported.
```
$ sqly --sql "SELECT * FROM user LIMIT 2" --csv testdata/user.csv
user_name,identifier,first_name,last_name
Expand Down Expand Up @@ -104,9 +104,14 @@ $ sqly --sql "SELECT * FROM user" testdata/user.csv --output=test.csv

# Features to be added
- [x] import json
- [x] output json file
- [ ] import tsv (output tsv)
- [ ] import ltsv (output ltsv)
- [x] print json format
- [ ] dump json file
- [x] import tsv
- [x] ptint tsv format
- [ ] dump tsv file
- [ ] import ltsv
- [ ] print ltsv format
- [ ] dump ltsv file
- [ ] import swagger
- [ ] print markdown format
- [ ] ignore csv header option
Expand Down
24 changes: 16 additions & 8 deletions config/argument.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,33 +45,41 @@ type Arg struct {
Version func()
}

type outputFlag struct {
csv bool
tsv bool
json bool
}

// NewArg return *Arg that is assigned the result of parsing os.Args.
func NewArg() (*Arg, error) {
csvFlag := false
jsonFlag := false
outputFlag := outputFlag{}

arg := &Arg{}
pflag.BoolVarP(&csvFlag, "csv", "c", false, "change output format to csv (default: table)")
pflag.BoolVarP(&jsonFlag, "json", "j", false, "change output format to json (default: table)")
pflag.BoolVarP(&outputFlag.csv, "csv", "c", false, "change output format to csv (default: table)")
pflag.BoolVarP(&outputFlag.tsv, "tsv", "t", false, "change output format to tsv (default: table)")
pflag.BoolVarP(&outputFlag.json, "json", "j", false, "change output format to json (default: table)")
pflag.BoolVarP(&arg.HelpFlag, "help", "h", false, "print help message")
pflag.BoolVarP(&arg.VersionFlag, "version", "v", false, "print help message")
pflag.Parse()

arg.Usage = usage
arg.Version = version
arg.Output = newOutput(*output, csvFlag, jsonFlag)
arg.Output = newOutput(*output, &outputFlag)
arg.FilePaths = pflag.Args()
arg.Query = *query

return arg, nil
}

// newOutput retur *Output
func newOutput(filePath string, csvFlag, jsonFlag bool) *Output {
func newOutput(filePath string, of *outputFlag) *Output {
mode := model.PrintModeTable
if csvFlag {
if of.csv {
mode = model.PrintModeCSV
} else if jsonFlag {
} else if of.tsv {
mode = model.PrintModeTSV
} else if of.json {
mode = model.PrintModeJSON
}
return &Output{
Expand Down
2 changes: 2 additions & 0 deletions di/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ func NewShell() (*shell.Shell, func(), error) {
shell.NewShell,
shell.NewCommands,
usecase.NewCSVInteractor,
usecase.NewTSVInteractor,
usecase.NewJSONInteractor,
usecase.NewHistoryInteractor,
usecase.NewSQLite3Interactor,
usecase.NewSQL,
persistence.NewCSVRepository,
persistence.NewTSVRepository,
persistence.NewJSONRepository,
persistence.NewHistoryRepository,
memory.NewSQLite3Repository,
Expand Down
4 changes: 3 additions & 1 deletion di/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions domain/model/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package model

// Header is CSV/TSV/Table header.
type Header []string

// Record is CSV/TSV/Table records.
type Record []string
6 changes: 0 additions & 6 deletions domain/model/csv.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ type CSV struct {
Records []Record
}

// Header is CSV header.
type Header []string

// Record is CSV records.
type Record []string

// IsHeaderEmpty return wherther header is empty or not
func (c *CSV) IsHeaderEmpty() bool {
return len(c.Header) == 0
Expand Down
14 changes: 14 additions & 0 deletions domain/model/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const (
PrintModeTable PrintMode = iota
// PrintModeCSV print data in csv format
PrintModeCSV
// PrintModeTSV print data in tsv format
PrintModeTSV
// PrintModeJSON print data in json format
PrintModeJSON
)
Expand All @@ -28,6 +30,8 @@ func (p PrintMode) String() string {
return "table"
case PrintModeCSV:
return "csv"
case PrintModeTSV:
return "tsv"
case PrintModeJSON:
return "json"
}
Expand Down Expand Up @@ -98,6 +102,8 @@ func (t *Table) Print(out *os.File, mode PrintMode) {
t.printTable(out)
case PrintModeCSV:
t.printCSV(out)
case PrintModeTSV:
t.printTSV(out)
case PrintModeJSON:
t.printJSON(out)
default:
Expand Down Expand Up @@ -126,6 +132,14 @@ func (t *Table) printCSV(out *os.File) {
}
}

// Print print all record with header; output format is tsv
func (t *Table) printTSV(out *os.File) {
fmt.Fprintln(out, strings.Join(t.Header, "\t"))
for _, v := range t.Records {
fmt.Fprintln(out, strings.Join(v, "\t"))
}
}

// Print print all record in json format
func (t *Table) printJSON(out *os.File) {
data := make([]map[string]interface{}, 0)
Expand Down
35 changes: 35 additions & 0 deletions domain/model/tsv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Package model defines Data Transfer Object (Entity, Value Object)
package model

import "strings"

// TSV is tsv data with header.
type TSV struct {
Name string
Header Header
Records []Record
}

// IsHeaderEmpty return wherther header is empty or not
func (t *TSV) IsHeaderEmpty() bool {
return len(t.Header) == 0
}

// SetHeader set header column.
func (t *TSV) SetHeader(header Header) {
t.Header = append(t.Header, header...)
}

// SetRecord set tsv record.
func (t *TSV) SetRecord(record Record) {
t.Records = append(t.Records, record)
}

// ToTable convert TSV to Table.
func (t *TSV) ToTable() *Table {
return &Table{
Name: strings.TrimSuffix(t.Name, ".tsv"),
Header: t.Header,
Records: t.Records,
}
}
13 changes: 13 additions & 0 deletions domain/repository/tsv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package repository

import (
"os"

"github.com/nao1215/sqly/domain/model"
)

// TSVRepository is a repository that handles TSV file.
type TSVRepository interface {
// List get tsv all data with header.
List(tsv *os.File) (*model.TSV, error)
}
43 changes: 43 additions & 0 deletions infrastructure/persistence/tsv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package persistence

import (
"encoding/csv"
"io"
"os"
"path/filepath"

"github.com/nao1215/sqly/domain/model"
"github.com/nao1215/sqly/domain/repository"
)

type tsvRepository struct{}

// NewTSVRepository return TSVRepository
func NewTSVRepository() repository.TSVRepository {
return &tsvRepository{}
}

// List return tsv all record.
func (tr *tsvRepository) List(f *os.File) (*model.TSV, error) {
r := csv.NewReader(f)
r.Comma = '\t'

t := model.TSV{
Name: filepath.Base(f.Name()),
}
for {
row, err := r.Read()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}

if t.IsHeaderEmpty() {
t.SetHeader(row)
continue
}
t.SetRecord(row)
}
return &t, nil
}
6 changes: 6 additions & 0 deletions shell/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ func (c CommandList) importCommand(s *Shell, argv []string) error {
return err
}
table = csv.ToTable()
} else if isTSV(v) {
tsv, err := s.tsvInteractor.List(v)
if err != nil {
return err
}
table = tsv.ToTable()
} else if isJSON(v) {
json, err := s.jsonInteractor.List(v)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions shell/mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func (c CommandList) modeCommand(s *Shell, argv []string) error {
fmt.Fprintln(Stdout, "[Output mode list]")
fmt.Fprintln(Stdout, " table")
fmt.Fprintln(Stdout, " csv")
fmt.Fprintln(Stdout, " tsv")
fmt.Fprintln(Stdout, " json")
return nil
}
Expand All @@ -30,6 +31,9 @@ func (c CommandList) modeCommand(s *Shell, argv []string) error {
case model.PrintModeCSV.String():
fmt.Printf("Change output mode from %s to csv\n", s.argument.Output.Mode.String())
s.argument.Output.Mode = model.PrintModeCSV
case model.PrintModeTSV.String():
fmt.Printf("Change output mode from %s to tsv\n", s.argument.Output.Mode.String())
s.argument.Output.Mode = model.PrintModeTSV
case model.PrintModeJSON.String():
fmt.Printf("Change output mode from %s to json\n", s.argument.Output.Mode.String())
s.argument.Output.Mode = model.PrintModeJSON
Expand Down
4 changes: 3 additions & 1 deletion shell/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@ type Shell struct {
config *config.Config
commands CommandList
csvInteractor *usecase.CSVInteractor
tsvInteractor *usecase.TSVInteractor
jsonInteractor *usecase.JSONInteractor
sqlite3Interactor *usecase.SQLite3Interactor
historyInteractor *usecase.HistoryInteractor
}

// NewShell return *Shell.
func NewShell(arg *config.Arg, cfg *config.Config, cmds CommandList,
csv *usecase.CSVInteractor, json *usecase.JSONInteractor,
csv *usecase.CSVInteractor, tsv *usecase.TSVInteractor, json *usecase.JSONInteractor,
sqlite3 *usecase.SQLite3Interactor, history *usecase.HistoryInteractor) *Shell {
return &Shell{
Ctx: context.Background(),
Expand All @@ -53,6 +54,7 @@ func NewShell(arg *config.Arg, cfg *config.Config, cmds CommandList,
config: cfg,
commands: cmds,
csvInteractor: csv,
tsvInteractor: tsv,
jsonInteractor: json,
sqlite3Interactor: sqlite3,
historyInteractor: history,
Expand Down
4 changes: 4 additions & 0 deletions shell/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ func isCSV(path string) bool {
return ext(path) == ".csv"
}

func isTSV(path string) bool {
return ext(path) == ".tsv"
}

func ext(path string) string {
base := filepath.Base(path)
pos := strings.LastIndex(base, ".")
Expand Down
35 changes: 35 additions & 0 deletions usecase/tsv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package usecase

import (
"os"

"github.com/nao1215/sqly/domain/model"
"github.com/nao1215/sqly/domain/repository"
)

// TSVInteractor implementation of use cases related to TSV handler.
type TSVInteractor struct {
Repository repository.TSVRepository
}

// NewTSVInteractor return TSVInteractor
func NewTSVInteractor(r repository.TSVRepository) *TSVInteractor {
return &TSVInteractor{Repository: r}
}

// List get TSV data.
// The sqly command does not open many TSV files. Therefore, the file is
// opened and closed in the usecase layer without worrying about processing speed.
func (ti *TSVInteractor) List(TSVFilePath string) (*model.TSV, error) {
f, err := os.Open(TSVFilePath)
if err != nil {
return nil, err
}
defer f.Close()

TSV, err := ti.Repository.List(f)
if err != nil {
return nil, err
}
return TSV, nil
}

0 comments on commit b23c917

Please sign in to comment.