LoginSignup
2
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-12-08

概要

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

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

2Way-SQL

SQLのクエリパラメータを指定する方法として、?とか:1とか使いますが、対応するDBライブラリによってまちまちです。
私の想定する2Way-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)を?に変換します。

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'にバインドされます。


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

# パラメータを1つ指定して実行
$ ./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オプションはあと勝ちとなります

ソース

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への接続できるクライアントが作れそうです。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1