プログラミング言語SML#解説 4.1.0版
22 SQL式とコマンド

22.7 SQL実行関数式

SQL実行関数式_sql pat => sqlfnは, SQLコマンドsqlfnをデータベースサーバーで実行する 関数を生成する. データベースサーバーへの接続ハンドルを引数として この関数を呼び出すと,式sqlfnが評価され,その結果得られた SQLコマンドがサーバーに送信され実行される. 実行に成功したならば,この関数はその実行結果を返し, 実行に失敗したときはSQL.Exec例外を発生させる. 具体的には,この関数は以下のことを行う.

  1. 1.

    τ SQL.conn型のデータベースサーバー接続ハンドルを 引数として受け取る.

  2. 2.

    データベースサーバー接続ハンドルから (τ, w) SQL.db型のデータベースの実体を取り出し, パターンpatの識別子をそれに束縛する.

  3. 3.

    sqlfnを評価し, (τ, w) SQL.command型のコマンドを得る. ただし,sqlfnsqlselectの場合は, (τ, w) SQL.query型のSELECTクエリを (τ SQL.cursor, w) SQL.command型のSQLコマンドと解釈する.

  4. 4.

    コマンドをサーバーに送信し実行する.

  5. 5.

    実行に成功したならば,τ型のコマンド実行結果を返す.

この振る舞いから分かるように, コマンドの型が (τ, w) SQL.db -> (τ, w) SQL.commandの とき,この関数の型は τ SQL.conn -> τである. 一般にτは, sqlfnsqlselectのとき SQL.cursor型,そうでないときunit型になる.

SELECTクエリやSQLコマンドをSQL実行関数式に直接書くのが, もっとも簡便なSQLコマンドの実行方法である. 例えば,

val q = _sql db => select #t.name as name, #t.age as age
                   from #db.employee as t
                   where #t.salary >= 300
                   order by #.age

と書くと,このクエリをデータベースサーバーで実行する関数qが 直ちに得られる. 段階的に構築したSELECTクエリを実行可能にするには, 以下のようにSQL実行関数内で select...(exp)記法を用いる.

val s = _sql select #t.name as name, #t.age as age
val f = fn db => _sql from #db.employee as t
val g = fn db => select...(s) from...(f db)
val q = _sql db => select...(q db)

SQLコマンドの場合は, SQL実行関数内にSQL.command型の式を書く.

val w = fn () => _sql where #employee.name = "Taro"
val c = fn db => _sql update #db.employee
                      set āge = #employee.age + 1,
                      salary = #employee.salary + 100
                      where...(w ())
val q = _sql db => ...(c db)

SQL実行関数式 _sql pat => sqlfnには, 以下の型に関する制約がある.

  • sqlfnの型 (τ,w) SQL.commandまたは (τ,w) SQL.querywは,sqlfnの型以外のどこにも使われていない型変数でなければならない.

この型制約は, 複数のデータベースにまたがるクエリの実行を禁止するために 導入されている. 例えば,以下の関数は型エラーになる.

# fun f exp = _sql db => select #e.name, (...exp) from #db.employee as e;
(interactive):1.12-1.65 Error:
  (type inference 067) User type variable cannot be generalized: ’$h

この直接の原因は,selectの中に関数fの引数expが 入っているために,select の型 (τ,w) SQL.querywが 引数expの型(τ,w) SQL.expwとしても用いられており, 上述の制約に違反することである. より実践的には,引数expは, _sql db => dbとは異なる データベースを参照するSQL評価式かもしれないからである. このことを理解するために, 例えば,この関数fを呼び出す別の関数を考えてみよう.

fun badExample conn1 conn2 =
    (_sql db2 =>
          select...((f _sql((select #t1.c1 from #db2.t1)) conn1;
                     _sql(select #t2.c2 from #db2.t2))))
      conn2

このbadExample関数自体は,fが多相関数ならば, 上述の制約にかかわらず型が付く. badExample関数は, 異なる2つのデータベース接続ハンドルconn1conn2を受け取り, 関数fのクエリをconn1で,それとは別のクエリをconn2で 実行する. この関数の奇妙なところは, SQL実行関数の本体から関数fのクエリを実行しようとしていることである. しかも,関数fの引数に渡されるのは, conn2のデータベースdb2を用いて書かれたサブクエリである. 結果として,fが実行しようとするクエリは, fが束縛するdbbadExampleが束縛するdb2の 2つのデータベースを参照するクエリになる. このようなクエリを実行することはできない.

この制約は,実質的に, プログラム中でSQL実行関数が書ける場所を制限する. 特に注意が必要なのは, 上述の関数fのように, 引数として受け取ったクエリを実行する関数は書けないことである. この制限を回避するひとつの方法は, クエリを作る関数と実行する関数を分けることである. 例えば,上述のfを SQL実行関数ではなくクエリを返す以下のような関数に書き直せば, この制限を回避できる.

fun f exp = fn db => _sql select #e.name, (...exp) from #db.employee as e
val q = _sql db => select...(f _sql(#e.saraly) db)

SQLコマンドの評価でエラーが発生しSQL.Exec例外が 発生する場合には,以下の場合が含まれる.

  1. 1.

    NOT NULL制約以外の制約違反.

  2. 2.

    整数や文字列のオーバーフロー.

  3. 3.

    0除算.

  4. 4.

    SML#のSQL構文では許されているが, クエリを実行するデータベースサーバーが解釈できない 構文が使われている.

このうち4.に関して補足する. SML#がサポートするSQL構文は, 標準規格SQL99を基礎とし,主要なベンダーによる共通の拡張と, 言語拡張を単純にするための(関数型言語の観点から見て)自然な拡張からなる. 標準SQLに対する準拠度や,クエリの書き方に関する 細かな規則は,接続先のデータベースエンジンによって異なる. そのため,SML#のSQL機能を使用する場合は, SML#が提供する全てのSQL機能を無条件に使うのではなく, 使用するデータベースエンジンに合わせて機能を選択するべきである. 例えば,以下は本マニュアル執筆時点で判明している SML#とデータベースの差異である.

  • PostgreSQLでは,FROM句において,テーブルを結合した結果に ASで名前が付けられているとき,結合対象のテーブルにASで 付けた名前をSELECT句から参照することができない. 例えば,以下のクエリはテーブルxが参照できないとして エラーとなる.

    SELECT x.col FROM (a AS x NATURAL JOIN b AS y) AS z

  • SQLite3はgroup by ()記法をサポートしない. 代わりにgroup by ""などを用いることができる.