🦎

csvデータをdbにInsertするCLIツールcsqlの紹介

2023/12/20に公開

はじめに

もうすぐクリスマスですね。今年のクリスマスは家でkubernetesと戦っていると思います。ところで昨今はクリーンアーキテクチャの流行りなども相まって、UnitTestやE2Eテストなど多くのテストがクリスマスもgithub actionsを走っていると思います。特にDBに関連したテストは重要度が高いですよね。というのも相まって、csvにあるデータをDBに移行できたらテスト用の大量データとかも楽そうなだあと思って作りました。

csql

https://github.com/seipan/csql
リポジトリはこれです。starください。

Install

go install github.com/seipan/csql

Usage

現在はmysql,postgres,sqlite3,mariadb(mysqlやん)に対応しています。cliのoptionとしては

Usage:
  csql [flags]

Flags:
  -c, --check         check csv format
  -d, --dsn string    DSN for Connecting Database
  -h, --help          help for csql
  -p, --path string   FilePath for Parsing CSVFile
  -q, --query         output query
  -t, --type string   Database Type

以上のようなものがあります。
csqlにはいろいろなオプションがあります。まずは、checkオプションです。

--check option

if success patern

csql --check --path=./testdata/csv/test01.csv --type=mysql --dsn=hogehoge

             ___________ ____    __ 
            / ____/ ___// __ \  / / 
           / /    \__ \/ / / / / /  
          / /___ ___/ / /_/ / / /___
          \____//____/\___\_\/_____/
                                                                          
                                                                   

csv format is correct

failed pattern

csql --check --path=./testdata/csv/test02.csv --type=mysql --dsn=hogehoge

             ___________ ____    __ 
            / ____/ ___// __ \  / / 
           / /    \__ \/ / / / / /  
          / /___ ___/ / /_/ / / /___
          \____//____/\___\_\/_____/
                                                                          
                                                                   

csv format is incorrect : table name is empty
exit status 1

check optionではcsvがcsqlのフォーマットに沿っているかどうかチェックします。
例えば今回failed patternのtest02.csvは以下のようなフォーマットです。

,name,id,email
,tarou,12,hoge@example.com
,hanako,13,huga@example.com

このフォーマットだとテーブル名が足りないってエラーが帰ってきます。

--query option

 csql --query --path=./testdata/csv/test01.csv --type=mysql --dsn="hoge:hoge@tcp(hoge:3306)/hoge?charset=utf8&parseTime=true"


             ___________ ____    __ 
            / ____/ ___// __ \  / / 
           / /    \__ \/ / / / / /  
          / /___ ___/ / /_/ / / /___
          \____//____/\___\_\/_____/
                                                                          
                                                                   

INSERT INTO user (name, id, email) VALUES (?, ?, ?)

queryオプションは、csvをinsertする際にどのようなqueryが走るかを返してくれます。

Insert

最後に実際にInsertします。

csql --path=./testdata/csv/test01.csv --type=mysql --dsn="hoge:hoge@tcp(localhost:3308)/hoge?parseTime=true&collation=utf8mb4_bin"


             ___________ ____    __ 
            / ____/ ___// __ \  / / 
           / /    \__ \/ / / / / /  
          / /___ ___/ / /_/ / / /___
          \____//____/\___\_\/_____/
                                                                          
                                                                   

insert 2 rows
Inserting: | 100%% 

こんな感じでinsertします。

実装

ちょこっとだけ内部実装についてもお話します。今回mysql,sqlite3,postgresでパターン分けするために、Interfaceを切って場合分けてます。

type Inserter interface {
	Query() string
	Insert() error
}

これをmysqlの実装だと


package mysql

import (
	"database/sql"
	"fmt"
	"strings"

	"github.com/seipan/csql/query"
)

type MySQLInserter struct {
	keys      query.KeyValues
	tableName string
	db        *sql.DB
}

func (i *MySQLInserter) Query() string {
	placeholders := make([]string, 0, len(i.keys))
	keys := make([]string, 0, len(i.keys))

	for _, kv := range i.keys {
		keys = append(keys, kv.Key)
		placeholders = append(placeholders, "?")
	}

	query := fmt.Sprintf(
		"INSERT INTO %s(%s) VALUES (%s);",
		i.tableName,
		strings.Join(keys, ", "),
		strings.Join(placeholders, ", "),
	)
	return query
}

func (i *MySQLInserter) Insert() error {
	if i.db.Ping() != nil {
		return fmt.Errorf("failed to connect to database: %w", i.db.Ping())
	}
	stmt, err := i.db.Prepare(i.Query())
	if err != nil {
		return fmt.Errorf("failed to prepare statement: %w", err)
	}
	defer stmt.Close()

	values := make([]interface{}, 0, len(i.keys))
	for _, kv := range i.keys {
		values = append(values, kv.Value)
	}

	_, err = stmt.Exec(values...)
	if err != nil {
		return fmt.Errorf("failed to execute statement: %w", err)
	}
	return nil
}

func NewMySQLInserter(kv query.KeyValues, tableName string, db *sql.DB) query.Inserter {
	return &MySQLInserter{
		keys:      kv,
		tableName: tableName,
		db:        db,
	}
}

こんな感じですね。
https://github.com/seipan/csql/blob/main/mysql/query.go

終わりに

今回は作ったCLIにツールについて書きました。読んでくれてありがとうございます。あとはパフォーマンス改善など頑張りたいです。

https://github.com/seipan/csql

Discussion