Nimで２Way-SQL的なPostgreSQLクライアントを作ってみる

# 概要

inpureライブラリのdb_postgresモジュールを利用して、PostgreSqlのクライアントっぽいものを作ってみました。

* なんちゃって２Way-SQL対応
* パラメータも渡せるよ

## ２Way-SQL

SQLのクエリパラメータを指定する方法として、？とか：１とか使いますが、対応するDBライブラリによってまちまちです。
私の想定する2Way-SQLは、以下のようなもの

* そのままでもSQLが発行できること
* パラメータ指定をすれば、パラメータ部を？に変換する
* 外部から渡されたパラメータをパラメータにバインドする

例えばこんな感じです。

```sql:test.sql
select 
  * 
from (
  select 'abc' as name,'hello' as name2
union all
  select 'def' as name,'hello' as name2
union all
  select 'ghi' as name,'hello' as name2
) dummy
where 1=1
and name = /*name*/'abc'
or  name in ( /*name2*/'def' , /*name3*/'ghi' ) 
```

上のSQLを実行時に、パラメータ部(```/*XXX*/YY```)を？に変換します。

```sql
select 
  * 
from (
  select 'abc' as name,'hello' as name2
union all
  select 'def' as name,'hello' as name2
union all
  select 'ghi' as name,'hello' as name2
) dummy
where 1=1
and name = ?
or  name in ( ? , ? ) 
```

nimで処理するなら、こんな感じですね。

```
for row in db.fastRows(SqlQuery(sql) , @["abc","def"]) :
  echo row
```

## 作ったもの

pというモジュール名で、上記のSQLを実行した結果です。
-Pname,-Pname2というパラメータの「値」が、上記SQLの```/*name*/'abc',/*name2*/'def'```にバインドされます。

```shell-session

# パラメータを２つ指定して実行
$ ./p -h=localhost -port=5432 -db=sample -u=postgres -p=postgres -i="test.sql" -Pname=abc -Pname2=ghi
abc     hello
ghi     hello

# パラメータを１つ指定して実行
$ ./p -h=localhost -port=5432 -db=sample -u=postgres -p=postgres -i="test.sql" -Pname=abc
abc     hello

# パラメータなしで実行すると、SQLファイルの内容が実行されます
$ ./p -h=localhost -port=5432 -db=sample -u=postgres -p=postgres -i="test.sql" 
abc     hello
def     hello
ghi     hello
```

パラメータ仕様はこんな感じにしています。

|パラメータ名|省略形|内容|デフォルト値|
|:--|:--|:--|:--|
|host   | h  | 接続先ホスト名  | localhost |
|port   |   | ポート番号  | 5432 |
|db   |   | データベース名  | |
|user   | u  | ユーザー名  | |
|password  | p  | パスワード  | | 
|file   | i  | SQLファイル名へのパス  | |
|sql   | c  | SQL文を直接指定  | |
|Pxxx | | 大文字Pで始まるパラメータは、クエリーのバインドパラメータ | |

fileおよびsqlオプションはあと勝ちとなります

## ソース

```nim:p.nim

# nimによる2Way-SQL対応 PostgreSQLクライアント
# ex)
# ./p -h=localhost -port=5432 -db=sample -u=postgres -p=postgres -c="select 1 " -Pname=abc -Pname2=ghi

import os
import parseopt2
import db_postgres
import tables
import nre
import sequtils
import strutils

type
  Sql2WayInfo = tuple[query: string, keys:seq[string]] 

# 2way SQLを渡して、パラメータを？に変換かつ、パラメータ名を順番に抽出し、
# Sql2WayInfoに格納して返却する
# パラメータ変換対象は、以下のパターン
#  /*PARAM_1*/''
#  /*PARAM_2*/'ABC'
#  /*PARAM_3*/0
#  /*PARAM_4*/123
proc initSql2WayInfo( str:string , params: Table[string,string] ) : Sql2WayInfo = 
  let reParam = re"\/\*([\w_]+)\*\/('[^']*'|\d+)"
  var q = str
  var p: seq[string] = @[]
  # パラメータが指定されていたら、変数名xxxを取り出し、/*XXX*/YYを？に変換する
  # パラメータが指定されていないなら、SQLはそのまま
  if params.len != 0 :
    for x in str.findIter(reParam) :
      # 変数名を取り出して格納する
      p.add(x.captures[0])
    # /*xxx*/yyを？に変換
    q = str.replace(reParam,"?")
  # 結果を設定
  result = (query:q, keys:p)

# Sql2WayInfoのkeysの順番で、パラメータを構築する
proc p(info:Sql2WayInfo, params:Table[string,string]) : seq[string] = 
  info.keys.mapIt( if it in params : params[it] else: "" ) 

# メイン処理
proc mainProc(args:Table[string,string],params:Table[string,string]) : int =
  result = 0
  # 2WaySqlをパース
  var qp = initSql2WayInfo(args["sql"],params)
  # 接続文字列の組み立て
  let conn = "host=" & args["host"] & " port=" & args["port"] & " dbname=" & args["db"]
  # Postgresqlに接続
  let db = open("", args["user"], args["password"], conn )
  defer:
    db.close
  # パラメータを指定して実行
  for row in db.fastRows( SqlQuery(qp.query) , qp.p(params)) :
    echo row.join("\t")

# メインモジュールとして起動しているかチェック
if isMainModule :
  var args = initTable[string,string]()
  var prms = initTable[string,string]()
  # デフォルト値
  args["host"] = "localhost"
  args["port"] = "5432"

  # イテレータで取得    
  for kind, key, val in getopt() :
    # (余談)kindは、enum型なので、case文ではすべてのenum値
    # を網羅していないとコンパイルエラーとなります
    case kind
    of cmdEnd:
      discard
    of cmdArgument:
      echo "無効な引数です"
      quit(1)
    of cmdLongOption, cmdShortOption:
      var val2 = val
      let key2 = case key
        of "h","host": "host"
        of "u","user": "user"
        of "p","password": "password"
        of "d","db" : "db"
        of "c","sql": "sql"
        of "port" : "port"
        of "i","file": 
          var buff = @[""]
          # ファイルを読み込む
          for x in val.lines() :
            buff.add(x)
          val2 = buff.join("\n")
          "sql"
        else:
          if key[0] == 'P' :
            prms[ key.substr(1)  ] = val
          ""
      if key2 != "" :
        args[key2] = val2 

  # メイン処理を呼び出し
  quit(mainProc(args,prms))

```

## 作ってみた感想

普段は、Groovy/Scala/Java系を利用しているのですが、nimはコンパイルするだけあって、起動やら処理自体も早いので良いですね。
今回は、db_postgresモジュールを使いましたが、db_odbcモジュールを使えば、いろいろなDBへの接続できるクライアントが作れそうです。