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

22.4 SQL評価式

SQL評価式(value expression)は, SQLコマンドの中で値に評価される式である. SML#のSQL評価式は, サーバーで評価される式を構築する式である. SML#のSQL評価式として書いたほぼそのままの内容が, サーバーに送信されるSQL評価式となる. SQL評価式自体の評価はデータベースサーバーで行われる. 例えば,

_sql(1 + #employee.salary)

というSML#のSQL評価式は,サーバーに送信するSQLクエリの断片

1 + employee.salary

に評価される.

SQL評価式がSML#で評価できる部分式を含んでいる場合, その部分式はSQL評価式構築時に評価され,評価された値がサーバーに 送信するSQLクエリに埋め込まれる. 例えば,

_sql(1 + 2 + #employee.salary)

というSML#のSQL評価式は,サーバーに送信するSQLクエリの断片

3 + employee.salary

に評価される.

SML#では以下に階層的に定義される標準SQLのサブセット を含む式sqlexpをSQL評価式として 使用することができる.

  • SQL評価式(トップレベル)
    sqlexp ::= sqlinfexp  | not sqlexp SQLの論理否定  | sqlexp and sqlexp SQLの論理積  | sqlexp or sqlexp SQLの論理和

  • SQL演算子式
    sqlinfexp ::= sqlcastexp  | sqlinfexp vid sqlinfexp 二項演算

  • SQL型キャスト式
    sqlcastexp ::= sqlappexp  | (vid) sqlcastexp 型キャスト

  • SQL関数適用式
    sqlappexp ::= sqlatexp  | vid sqlatexp 関数適用  | sqlappexp sqlatexp SML#の関数適用  | sqlappexp is (not)? sqlis SQLのIS述語 sqlis ::= null | true | false | unknown

  • SQL原始式
    sqlatexp ::= scon SML#の定数  | true SML#のtrueリテラル  | false SML#のfalseリテラル  | null SQLのNULLリテラル  | #lab.lab 名前のある関係に属するカラムの参照  | #.lab 名前のない関係に属するカラムの参照  | vid SML#の変数参照  | op longvid SML#の変数参照  | (sqlexp, , sqlexp)  | ((_sql)? sqlselect) SQLのSELECTサブクエリ  | (_sql)? exists ((_sql)? sqlselect) SQLのEXISTSサブクエリ  | ((_sql)? sqlcommand)  | ((_sql)? sqlclause)  | (sqlexp)  | ( ... exp ) SQL評価式の埋め込み

他のSQL関連構文との見た目上の統一感を出すために, 一部のネストしたSQL評価式は予約語_sqlから始めても良い. これらの_sqlは単純に無視される.

SQL評価式sqlexpの型τは, 静的な文脈として与えられるテーブル集合の型τおよび データベース接続を識別するための型wの下で 決定される. ひとつのSQL評価式に含まれる全ての部分式の型は,同じ τおよびwの下で与えられる. 以下,式eτおよびwの下で型τを持つことを,eの型は (τ,w)τ型である,と言う. τおよびwに言及する必要がない場合は単に, eの型はτである,と言う.

22.4.1 SML#で評価される式

以下の再帰的な条件を満たすSQL評価式は SML#で評価され,その値がデータベースサーバーに送信される クエリに埋め込まれる.

  1. 1.

    定数式sconはSML#で評価される.

  2. 2.

    変数式vidおよびop longvidはSML#で 評価される.

  3. 3.

    (sqlexp1, , sqlexpn)は, 全てのsqlexpiがSML#で評価される式ならば, SML#で評価される.

  4. 4.

    関数適用式vid sqlatexpは, sqlatexpがSML#で評価される式ならば, SML#で評価される.

  5. 5.

    二項演算式 sqlinfexp1 vid sqlinfexp2は, 全てのsqlinfexpiがSML#で評価される式ならば, SML#で評価される.

  6. 6.

    SML#の関数適用式 sqlappexp sqlatexpは SML#で評価される. sqlappexpまたはsqlatexpが SML#で評価される式でない場合は構文エラーである.

例えば,SQLクエリ

val q = _sql db => select SOME 1 + sum(#a.b) from #db.a group by #a.c;

において,部分式SOME 1はSML#で評価され, その値がSQLクエリ断片に埋め込まれる (もしSOMEが未定義ならば未定義変数エラーが発生する). sum(#a.b)はSML#で評価されず,そのまま SQLクエリ断片に含まれる. qを実行したときにサーバーに送信されるSQLクエリは 以下の通りである.

SELECT 1 + SUM(a.b) AS "1" FROM a GROUP BY a.c

SML#で評価される式の型は, 22.1.1節で定義したSQLの基本型のうちの いずれかでなければならない.

SQL評価式に含まれるSML#で評価される式は, 通常のSML#の式と同じ順番で, SQL評価式が評価されるときにただちに評価される. SML#で評価される式に関数適用式を含むSQL評価式は expansiveである. SQL評価式やSQLクエリは多くの場合多相的であるため, SML#で評価される関数適用式を含むSQL評価式は トップレベルでvalue restrcition警告を引き起こす. 例えば,以下のようになる.

# _sql(1 + 2);
none:~1.~1-~1.~1 Warning:
(type inference 065) dummy type variable(s) are introduced due to value
restriction in: it
val it = _ : (?X1 -> int, ?X0) SQL.exp

SQL評価式をトップレベルに書いたり,多相的に使いたいときは, fn () => を付けるなどして, value restrictionを回避する必要がある. 例えば,上記の警告は以下のようにすれば回避できる.

# fn () => _sql(1 + 2);
val it = fn : [’a, ’b. unit -> (’a -> int, ’b) SQL.exp]

22.4.2 SQL定数式

定数式は全てSML#で評価される式である. 定数リテラルの書き方はSML#の文法に準じる. そのため,特に文字列リテラルの書き方については, 標準SQLとは大きく異なる.

trueおよびfalseSQL.bool3型を持たないことに注意しなければならない. これらは第一級の真偽値のリテラルであり, 論理演算式としては使えない. 論理演算式と真偽値の区別については 22.1.2節を参照せよ.

SQL99の機能T031に定義されているUNKNOWNリテラルは, SML#では提供されない. これは,主要なRDBMSの中では唯一T031を実装している PostgreSQLがUNKNOWNリテラルを持たないからであり,また将来 PostgreSQL以外にT031を実装するRDBMSが現れたとしても, UNKNOWNリテラルはNULLリテラルで代用可能だからである (なお,このUNKNOWNの仕様はSQLコアと一貫性が無いことが指摘されている).

22.4.3 SQL識別子式

SQL評価式に現れる全ての vidおよび op longvidは, SML#の変数参照である. SQL評価式の中では, ストラクチャ識別子が一つ以上前置された longvidには, 必ずopを前置しなければならない.

SQL評価式の中では いくつかの識別子が以下の通りinfix宣言される.

infix 7 %
infix 5 like ||
nonfix mod

識別子の型と値は, その識別子がSML#で評価される式に現れているかどうかによって 異なる. 識別子がSML#で評価される式に現れているならば, その型と値は 現在のSML#の環境の中でその識別子が束縛された型と値である. そうでなければ,識別子はSQL.Opストラクチャに定義された 型と値を持つ.

例えば,式

_sql(1 + 2 + #a.b)

に現れる2つの+は意味が異なる. 最初の+は現在のSML#の環境で束縛されている +であり,2つ目の+SQL.Op.+である.

SML#で評価される式でない箇所に現れる識別子は, SQLクエリに現れない関数適用式(vid) sqlappexpvidを除いて(22.4.5節参照), 識別子がそのまま大文字に変換されてからSQLクエリに埋め込まれる. SQL.Opストラクチャの値が実際に参照されるのは, トイプログラムを実行(22.8.3節参照) した場合に限られる. SQL.Opストラクチャに定義された識別子の型は SQL評価式の型付けに用いられる. 例えば_sql(1 + #a.b)がSML#で何型を持つかは, SQL.Op.+の型を調べることで分かる.

22.4.4 SQL関数適用式およびSQL演算子式

SQL関数適用式およびSQL演算子式は, 組み込み構文になっている論理演算子式を除いて, SML#の関数適用式および演算子式と同様に, SML#の式として構文解析される. 演算子の結合力はSML#の演算子宣言によって決定される. そのため,infix宣言の使い方によっては, 標準SQLとは異なる演算子の結合順位でSQLクエリが構築されることに 注意が必要である.

標準SQLとは異なり, SQL関数適用式の構文は, SML#の関数適用式と同様に関数と引数を空白で区切って書き, 引数を丸括弧で囲うことを必要としない. しかし, 標準SQLに似せるために, SML#の式構文が許す範囲で, 敢えて引数を丸括弧で囲い,関数と引数を詰めて書くことができる. SELECT句で集約関数を用いるときなどにこの記法が用いられる. 例えば以下のように書ける.

fn db => _sql select avg(#e.age)
              from #db.employee as e
              group by ()

このavg(#e.age)は, SQL.Op.avg関数に#e.ageを適用する関数適用式である.

論理演算子以外のSQLの演算子および関数は, SQL.Opストラクチャに定義された SML#のライブラリ関数として提供されている. SML#で評価されないSQL評価式で使用可能な関数および 演算子の一覧は第22.9.2節にある.

SQL関数適用式およびSQL演算子式は SML#のそれらと同様に型付けされる.

22.4.5 型キャスト式

SML#のSQL評価式では, 識別子だけを丸括弧で囲った式は特別な意味を持つ. 丸括弧で囲った識別子を式の前に前置すると, その式の評価結果に丸括弧で囲った識別子が指す関数を適用することを 表す. ただし,丸括弧で囲った識別子はサーバーに送信される SQLクエリには現れない. 丸括弧で囲われた識別子はSQL.Opストラクチャに定義された 識別子でなければならない. 以下,丸括弧で囲われた識別子を前置した式を, 型キャスト式という.

型キャスト式は, SML#がサポートしない 暗黙のキャストやオーバーロードへの対応として, SML#で型の辻褄を合わせるために用いられる. 主な使用用途のひとつはoption型や数値型の取り扱いである. 22.1節で述べたとおり, SML#はNULLになる可能性をoption型を用いて 型上で区別する. 一方,標準SQLでは,NULLはどの基本型にも含まれる. この違いのため, SQLでは型エラーでないクエリでも, SML#では型エラーとなることがある. また,数値型についても, 標準SQLでは暗黙に数値型のキャストが行われる一方, SML#は暗黙のキャストを行わないため, SML#では型エラーとなることがある. 例えば, 部署ごとの平均年齢より若い人の問い合わせる以下のクエリを見てみよう.

fn db => _sql select #e.department as department, #e.name as name
              from #db.empoloyee as e
              where #e.age < (select avg(#t.age)
                              from #db.employee as t
                              where #t.department = #e.department
                              group by ())

この式には型がつくものの, employeeテーブルのageカラムの型は SQL.numeric optionと推論される. 従って, ageint型のデータベースに対してこのクエリを実行できない. この原因は, 集約関数avgの結果とカラム#e.ageを比較していることにある. avgの結果の型は SQL.numeric option型である. SML#におけるSQL比較演算子の型付け規則により, 比較演算子の左右の式の型は一致していなければならない. 従って, #e.ageの型は SQL.numeric option型となる. 標準SQLであれば, 暗黙にNUMERIC型へのキャストが行われるため, ageカラムの型は数値型ならばどの型でも良いはずである.

このような, SML#がサポートしない 暗黙のキャストやオーバーロードに対応するため, 以下のように比較演算の箇所にNum関数への適用を加えると, 任意の数値型のageカラムに対して このクエリを実行できるようになる.

fn db => _sql select #e.department as department, #e.name as name
              from #db.empoloyee as e
              where (Num)#e.age < (select avg(#t.age)
                                   from #db.employee as t
                                   where #t.department = #e.department
                                   group by ())

この(Num)はSQLクエリの生成では無視される. 従って(Num)の有無にかかわらず サーバーに送信されるSQLクエリは変化しない. (Num)はSQL評価式の型付けのためだけに 静的に評価される.

型キャスト式のために用意されている 関数の一覧については 22.9.1節を 参照せよ.

22.4.6 SQL論理演算式

以下の論理演算式は組み込み構文である.

  • not sqlexp

  • sqlexp1 and sqlexp2

  • sqlexp1 or sqlexp2

  • sqlexp is (not)? sqlis

これらのうちandは, SML#の他の構文と曖昧にならないように, andを含む式全体が括弧で囲われていなければならない. 例えば以下の例は構文エラーとなる.

# fn () => _sql where #t.c >= 10 and #t.c <= 20;
(interactive):1.31-1.35 Error: Syntax error: deleting AND HASH

以下の例のようにandを含む式全体を括弧で囲むことで 構文エラーを回避できる.

# fn () => _sql where (#t.c >= 10 and #t.c <= 20);
val it = fn : [’a#t: ’b, ’b#c: int, ’c.
               unit -> (’a list -> ’a list, ’c) SQL.whr]

これらの論理演算式はそれぞれ,その全ての部分式が SQL.bool3型のとき, SQL.bool3型を持つ. また,ライブラリ関数として提供されるSQL比較演算子 (22.9.2節参照)もすべて, SQL.bool3型のクエリを返す.

22.1.2節で述べたように, 標準SQLでの真偽値の取り扱いに関する混乱を避けるため, SML#は論理演算式の型と真偽値の型を区別する. 従って,SQLのBOOLEAN型のカラムの参照や, trueなどの真偽値リテラルは, 論理演算式として書けないことに注意が必要である. 例えば,以下の例は型エラーになる.

# fn () => _sql(true is false);
(interactive):1.14-1.26 Error:
(type inference 016) operator and operand don’t agree
operator domain: SQL.bool3
operand: ’HBP::bool

一方,以下の例は型エラーにならない.

# fn () => _sql(#t.c = true is false);
val it = fn : [’a, ’b. unit -> (’a -> SQL.bool3, ’b) SQL.exp]

なぜなら,trueは真偽値リテラルであるが, #t.c = trueSQL.bool3型の比較演算式だからである.

22.4.7 SQLカラム参照式

あるテーブルのカラムを参照するには, #lab1.lab2と書く. ここで, lab1はテーブルの名前, lab2はカラムの名前である. SQLカラム参照式の型は,文脈で与えらえるテーブル集合の型のうち, テーブルlab1の カラムlab2の型である.

標準SQLでは,テーブル名を省略してカラムを参照できる ときがあるが,SML#のSQL評価式では,すべてのカラム名には テーブル名が前置されていなければならない. また,SML#の他の構文との衝突を避けるため, SQLカラム参照式には#を前置する.

テーブル名およびカラム名の大文字小文字は, SML#プログラムの中では区別される. 例えば,標準SQLでは

SELECT Employee.name FROM EMPLOYEE

のように,大文字と小文字を混ぜてEMPLOYEEテーブルを 参照できるが,SML#では

_sql db => select #Employee.name from #db.EMPLOYEE

と書くと型エラーとなる. SQLカラム参照式の評価結果には, 大文字小文字を変換することなく, SML#プログラムに書いた通りの テーブル名およびカラム名が埋め込まれる.

SQLでは名前が付けられていないテーブル(または関係)の カラムを参照することがある. その代表的な例は,SELECT句が計算したカラムをORDER BY句から 参照するときである. このようなカラムへの参照は,SML#では #.labと書く. その型はカラムlabの型である. 例えば,SQLクエリ

SELECT e.name AS name, e.age + 1 AS nextAge
FROM employee AS e
ORDER BY nextAge

を,SML#では

fn db => _sql select #e.name as name, #e.age + 1 as nextAge
              from employee as e
              order by #.nextAge

と書く.

22.4.8 SQLサブクエリ

SML#のSQL評価式では,以下の2種類のサブクエリを 書くことができる.

  • SELECTサブクエリ: ( sqlselect )

  • EXISTSサブクエリ: exists ( sqlselect )

SML#の他の構文との見た目上の統一感を出すために, SQL予約語の前に_sqlを書いてもよい. この_sqlは単純に無視される.

SELECTサブクエリ ( sqlselect )sqlselectは, 1行1列を返すクエリでなければならない. このうち,sqlselectが1列を返すクエリであることは, SML#の型システムによって静的に検査される. sqlselectは,SML#で

({1: τ} list, w) SQL.query

型を持たなければならない. このとき,サブクエリ(sqlselect)の型は, あるτについて(τ,w)τである. サブクエリが返す行数は,クエリを実行するサーバーが実行時に チェックする. クエリを実行したときにsqlselectが0行または2行以上の行を 返した場合は,例外SQL.Execが発生する.

EXISTSサブクエリ exists ( sqlselect )は, sqlselectの型が

(τ list, w) SQL.query

のとき,あるτについて型(τ,w)SQL.bool3を持つ.

22.4.9 SQL評価式の埋め込み

SQL評価式sqlexpを第一級のSML#オブジェクトとして 取り出すには_sql(sqlexp)と書く. sqlexpの型が (τ,w)τのとき, 式_sql(sqlexp)の型は (τ -> τ, w) SQL.expである. 例えば,

val q = _sql(1 + #employee.salary)

の型は

val q : [ ’a#{employee: ’b}, ’b#{salary: int}, ’w. (’a -> int, ’w) SQL.exp ]

である.

このようにして取り出したSQL評価式の断片を 別のSQL評価式に埋め込むには 埋め込み式(...exp)を用いる. 例えば,上述のSQL評価式断片qを用いて 以下のようにSQL評価式を構築できる.

_sql((...q) > 10)

このSQL評価式は以下のSQLクエリを表す.

1 + employee.salary > 10

(...exp)の型は, expの型が (τ -> τ, w) SQL.expのとき, (τ,w)τである. 埋め込み先の式が参照するテーブルの型とτに 齟齬がある場合,型エラーとなる. 例えば,以下の式は型エラーである.

_sql((...q) > 10 and #employee.salary = "abc")

(...exp記法に含まれる expは,SQL評価式ではなく, SML#の式である. このexpの中では, 通常のSML#の式と同様に, SQL予約語は予約語ではなく識別子と解釈される.

SQL評価式を埋め込んだ結果に定数式が現れたとしても, その定数式はSML#で評価されない. SQL評価式がSML#で評価される式かどうかは, 静的な構文構造によってのみ定まる. 例えば,以下の再帰関数nat nは, 1n個ならんだSQL評価式1 + 1 + + 1を構築する 関数であり,その評価結果であるnを計算する関数ではない.

# fun nat 1 = _sql(1)
    | nat n = _sql(1 + (...nat (n - 1)));
val nat = fn : [’a, ’b. int -> (’a -> int, ’d) SQL.exp]
# SQL.expToString (nat 5);
val it = "(1 + (1 + (1 + (1 + 1))))" : string