2015年11月29日日曜日

[haskell][persistent][sqlite] Persistentパッケージ利用時にテーブルにインデックスを生成する方法

PersistentパッケージにはMigration機能が備わっており、自動的にテーブルを生成してくれます。スキーマ変更を行った際にも、変換が可能な限りテーブル内のレコードを保持したまま新しいスキーマに変換してくれます(Migration機能については過去のエントリでまとめています)。

自分が利用する上で、インデックスやトリガーを生成する手順が紹介されておらず困っていたのですが、rawExecuteという関数を用いることで自由にDDLを発行できることがわかりました。以下その手順とサンプルを紹介しておきます。

サンプルコード:

以下は、personテーブルのnameカラムにインデックスをs生成するサンプルです。runMigration実行直後に、runExecuteを実行することでインデックスを生成しています。このサンプルではインデックスを生成しているだけですが、同じ手順でトリガーの生成(CREATE TRIGGER)も可能です。
{-# LANGUAGE EmptyDataDecls             #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE GADTs                      #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE QuasiQuotes                #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeFamilies               #-}
import Control.Monad.IO.Class  (liftIO)
import Database.Persist         -- persistentパッケージ
import Database.Persist.Sqlite  -- persistent-sqliteパッケージ
import Database.Persist.TH      -- persistent-templateパッケージ

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Person
    name String
    age Int Maybe
    deriving Show
|]

main :: IO ()
main = runSqlite "test.db" $ do
    -- personテーブル生成
    runMigration migrateAll
    -- nameカラムにindexを生成する。placeholderがないため第2引数は[]でよい。
    -- 2回目以降、すでにインデックスが存在している場合のためにIF NOE EXISTSが必要。
    rawExecute "CREATE INDEX IF NOT EXISTS idx_name_on_person ON person(name);" [] -- here!

    michaelId <- insert $ Person "Michael" $ Just 26
    michael <- get michaelId
    liftIO $ print michael
runExecuteの第一引数には実行するSQLを記載します。既にインデックスが生成済みの場合には何も処理を行わないよう"IF NOT EXISTS"を指定しています。第二引数にはplaceholderの値を指定しますがこの例ではplaceholderは利用していないため"[]"を渡しています。
"IF NOT EXISTS"を指定しないと既に存在しているインデックスを再度生成しようとして、以下のようなエラーが報告されます。
*** Exception: SQLite3 returned ErrorError while attempting to perform prepare "CREATE INDEX idx_name_on_person ON person(name);": index idx_name_on_person already exists

補足:

Yesod BookのPersistentの解説には、rawQueryの利用手順が紹介されています。rawQueryとrawExecuteの型はそれぞれ以下のようになっています。
rawQueryが実行結果を配列で返すのに対し、rawExecuteの結果はunit(空)となっています。このためDDL (Data Definition Language)実行時にはrawExecuteを利用するのがよいと思われます。
さらにrawSqlという関数もあるようですが、こちらの使い分けはよく分からず…。ご存じの方是非ご教授ください。m(_ _)m
(2015/12/16:追記)rawQueryはData.Conduit.Source型で結果を返します。これは結果が巨大であってもストリーム処理できることを意味しています。これに対しrawSqlは単純にリストで結果を返すという違いがあります。

参考:

2015年11月8日日曜日

[haskell][stack] stack exec ghciで”Couldn't match expected type"エラーが発生する問題の対処

先日、haskellのパッケージ管理をcabalからstackに移行して「便利〜!」と感動していたところなのですが、stach exec ghciでソースをロードしようとすると"Couldn't match expected type: xxxxx"とエラーが発生する問題に遭遇しました。
ネットの情報を参考に解決することができたのでその手順をまとめておきます。

問題:

stack buildは成功するにもかかわらず、stack exec ghci xxx.hs(xxx.hsはbuild対象のファイル)がエラーになる。
stack exec ghci実行時のエラーログ:
% stack exec ghci FileToVec.hs
GHCi, version 7.10.2: http://www.haskell.org/ghc/  :? for help
[1 of 1] Compiling FileToVec        ( FileToVec.hs, interpreted )

FileToVec.hs:42:18:
    Couldn't match expected type ‘V.Vector a’
                with actual type ‘vector-0.10.12.3:Data.Vector.Vector a0’
    NB: ‘V.Vector’
          is defined in ‘Data.Vector’ in package ‘vector-0.11.0.0’
        ‘vector-0.10.12.3:Data.Vector.Vector’
          is defined in ‘Data.Vector’ in package ‘vector-0.10.12.3’
    Relevant bindings include
      v :: vector-0.10.12.3:Data.Vector.Vector a0
        (bound at FileToVec.hs:40:15)
      file_to_vec :: FilePath -> IO (V.Vector a)
        (bound at FileToVec.hs:33:1)
    In the first argument of ‘return’, namely ‘v’
    In a stmt of a 'do' block: return v
Failed, modules loaded: none.
Leaving GHCi.
この環境でのstack buildは以下のようなログで成功しています。
% stack build
csv2db-0.1.0.0: build
Preprocessing executable 'csv2db' for csv2db-0.1.0.0...
[2 of 4] Compiling FileToVec        ( FileToVec.hs, .stack-work/dist/x86_64-osx/Cabal-1.22.4.0/build/csv2db/csv2db-tmp/FileToVec.o )
[3 of 4] Compiling DbRecord         ( DbRecord.hs, .stack-work/dist/x86_64-osx/Cabal-1.22.4.0/build/csv2db/csv2db-tmp/DbRecord.o ) [TH]
Linking .stack-work/dist/x86_64-osx/Cabal-1.22.4.0/build/csv2db/csv2db ...
    DbRecord
    FileToVec
    MyArgs
csv2db-0.1.0.0: install
Installing executable(s) in XXXX

解決方法:

以下の2つの方法があります。
  1. execコマンドを用いず、stack ghciで起動する
  2. execコマンドを利用しなければロードすることができます。普通はこちらの手順を実行するのが正しいはずです。が、ファイル指定で起動できないのでプロンプトから:loadコマンドを実行する必要があります。
    % stack ghci
    Using main module: Package `csv2db' component exe:csv2db with main-is file: /Users/kurokawa/git/work_haskell/csv2db/Main.hs
    Configuring GHCi with the following packages: csv2db
    GHCi, version 7.10.2: http://www.haskell.org/ghc/  :? for help
    [1 of 4] Compiling FileToVec        ( FileToVec.hs, interpreted )
    [2 of 4] Compiling MyArgs           ( MyArgs.hs, interpreted )
    [3 of 4] Compiling DbRecord         ( DbRecord.hs, interpreted )
    [4 of 4] Compiling Main             ( XXXX/Main.hs, interpreted )
    Ok, modules loaded: MyArgs, DbRecord, Main, FileToVec.
    *Main> :load FileToVec
    [1 of 1] Compiling FileToVec        ( FileToVec.hs, interpreted )
    Ok, modules loaded: FileToVec.
    *FileToVec>
    
    stack ghci実行時にソースファイルを引数で指定した場合、以下のようなエラーになるのでご注意を。
    % stack ghci FileToVec.hs
    Error parsing targets: Directory not found: FileToVec.hs
    
  3. cabalを直実行して必要パッケージをインストールする
  4. stackを利用しないでcabal installで依存パッケージをインストールし、ローカルのxxx.hsがコンパイルできる状態にし、stackを利用しないでcabal exec ghci xxx.hsを実行する。成功時のログ:
    % cabal install vector
     ... snip...
    % cabal exec ghci FileToVec.hs
    GHCi, version 7.10.2: http://www.haskell.org/ghc/  :? for help
    [1 of 1] Compiling FileToVec        ( FileToVec.hs, interpreted )
    Ok, modules loaded: FileToVec.
    *FileToVec>
    

エラー原因(の推測):

stack build実行時に起動されるcabalが参照するパッケージリストと、stack exec ghciで起動されるghciが参照するパッケージリストが、異なっているのが原因だと思われます。
stack build実行時には、stackが内部で管理している(?)パッケージリストを参照しているようですが、stack exec ghciで起動されたghciはcabalを直接起動したときに参照されるパッケージリストを参照し、バージョンのズレが生じてエラーになります。

参考:

2015年11月3日火曜日

[cygwin] cygwinのシェル起動時にPATHの先頭に/usr/binと/usr/local/binが勝手に追加されないようにする

cygwinのデフォルトの設定では、シェル起動時に以下の2つのディレクトリが自動的にPATHの先頭に追加されます。
  • /usr/local/bin
  • /usr/bin

cygwinでインストールされているコマンドと同名の別コマンドを優先して起動したい場合には、この設定が邪魔になります。
これを無効にするには以下の方法があります。お好みでどちらかを選択してください。
  1. /etc/profileもしくは/etc/csh.loginの該当処理をコメントアウトする(bash / tcsh)
    • cygwinがPATHを上書きしているのは、/etc/profile(bashの場合)と/etc/csh.login(tcshの場合)です。これらのスクリプトを編集することで、/usr/binと/usr/local/binが勝手に追加されないようにできます。
  2. ORIGINAL_PATHでPATHを上書きする(bashのみ)
    • シェルにbashを利用している場合は、cygwinがPATHを上書き設定する際、環境変数ORIGINAL_PATHにオリジナルの変数を保存してくれています。これを利用してPATHに上書きすれば、/usr/binと/usr/local/binを含まない設定にすることができます。

具体的な修正内容:

  1. /etc/profileもしくは/etc/csh.loginの該当処理をコメントアウトする
    • tcshの場合は/etc/csh.loginの7行目をコメントアウトする
    • #set path=( /usr/local/bin /usr/bin /bin $path:q )    # <= ここ
      
      
    • bashの場合は/etc/profileの37行目をコメントアウトする
    •   : ${ORIGINAL_PATH=${PATH}}
        if [ ${CYGWIN_NOWINPATH-addwinpath} = "addwinpath" ] ; then
      #    PATH="/usr/local/bin:/usr/bin${PATH:+:${PATH}}"  # <= ここ
        else
          PATH="/usr/local/bin:/usr/bin"
        fi
      
      
  2. ORIGINAL_PATHでPATHを上書きする
    • tcshではこの方法では対応できません
    • bashの場合は~/.bashrcに以下の記述を追加する
    • export PATH=$ORIGINAL_PATH
      

参考: