SQL::Statementモジュール(日本語チョー訳)

by Hippo2000(2001/5/15)

SQL::Statementなのです。DBD::File、DBD::CSVのエンジンなのです。DBIと組み合わせて簡単になんちゃってRDBを作ることができます。(DBD::Excelもその例(^^))

原本の著作権はJochen Wiedmannさんがお持ちです。Jochen Wiedmann さんにはメールで了解をいただきました。

なお内容等が間違っていたら修正します。ご連絡ください。


目次


名前

SQL::Statement - SQL解析および処理エンジン


概要

    require SQL::Statement;
    # parserの作成
    my($parser) = SQL::Parser->new('Ansi');
    # SQLステートメントの解析
    $@ = '';
    my ($stmt) = eval {
        SQL::Statement->new("SELECT id, name FROM foo WHERE id > 1",
                            $parser);
    };
    if ($@) {
        die "Cannot parse statement: $@";
    }
    # 結果カラムのリストの問い合わせ;
    my $numColums = $stmt->columns();  # スカラ・コンテキスト
    my @columns = $stmt->columns();    # 配列コンテキスト
    # @columns には SQL::Statement::Column インスタンスが入っています
    # 同様に、ステートメントで使われたテーブルを問い合わせます:
    my $numTables = $stmt->tables();   # スカラ・コンテキスト
    my @tables = $stmt->tables();      # 配列コンテキスト
    # @tables にはSQL::Statement::Table インスタンスが入っています
    # WHERE節を問い合わせ;これはSQL::Statement::Opインスタンスを
    # 取り出します
    my $where = $stmt->where();
    # SQL::Evalオブジェクトにより与えられる具体的なデータで
    # WHERE節を評価します
    my $result = $stmt->eval_where($eval);
    # ステートメントの実行:
    $stmt->execute($data, $params);

説明

モジュールのインストール方法については、下記の"インストール"をご覧ください。

SQL::Statement モジュールは小さく、抽象のSQLエンジンを実装します。このモジュールはそれ自身では使うことができませんが具象SQLエンジンを派生させる基本クラスとして使うことができます。この実装はDBIドライバ DBD::CSVでうまく機能するように設計されています、そのためおそらくはより大きな環境にはあまり向いてはいないかもしれませんが、あまり問題なく拡張できるのではないかと期待しています。

SQL問い合わせを解析することによりSQL::Statementインスタンスを作成します。このインスタンスは取り出し文法、WHERE節、ステートメント評価のためのメソッドを提供します。

parserオブジェクトの作成

何が適切なSQLとして受け取られるかは、parserオブジェクトに依存します。parserが持ったり持たなかったりするいわゆる機能の組み合わせがあります。通常は組み込みparserではじめます:

    my $parser = SQL::Parser->new($name, [ \%attr ]);

現在2つのパーザーが組み込まれています。Ansi パーザーはANSI SQLの適切なサブセットを実装しています(少なくとも私はそう願っています:-))。SQL::Statement パーザーはDBD::CSVドライバで使われています。

個々の機能を問い合わせたり、設定することができます。現在利用できるのは以下のものがあります:

create.type_blob
create.type_real
create.type_text
これらはCREATE TABLE 節での対応するカラム型を可能にします。Ansi パーザーでは使用できないようになっていますが、SQL::Statement では使用可能になっています。例:
 
select.join
これはSELECTステートメントで複数のテーブルを使うことを可能にします、例えば
  SELECT a.id, b.name FROM a, b WHERE a.id = b.id AND a.id = 2

機能を使用可能にしたり不能にするために、例えばselect.join は以下のようにします

  # 機能を使用可能にする
  $parser->feature("select", "join", 1);
  # 機能を使用不能にする
  $parser->feature("select", "join", 0);

もちろん機能を問い合わせることもできます:

  # 機能を問い合わせる
  my $haveSelectJoin = $parser->feature("select", "join");

newメソッドは機能設定を省略することを可能にします。例えば以下のものはSQL::Statement と同等です。

  $parser = SQL::Statement->new('Ansi',
                                { 'create' => { 'type_text' => 1,
                                                'type_real' => 1,
                                                'type_blob' => 1 },
                                  'select' => { 'join' => 0 }});

問い合わせの解析

以下のようにしてステートメントを解析することができます:

    my $stmt = SQL::Statement->new($query, $parser);

文法エラーまたはその他の問題の場合、メソッドはPerl例外を投げます。このため例外を捕まえたければ、以下のようになります。

    $@ = '';
    my $stmt = eval { SQL::Statement->new($query, $parser) };
    if ($@) { print "An error occurred: $@"; }

簡単に拡張することができますが、受け取られるSQL文法は制限されています。下記のSQL文法をご覧ください。上記のparserオブジェクトの作成をご覧ください。

問い合わせ情報の取り出し

以下のメソッドは問い合わせについての情報を取得するために使うことができます:

command
SQLコマンドを返します。現在はSELECT, INSERT, UPDATE, DELETE, CREATE または DROP のどれか1つです。最後の2つはCREATE TABLEDROP TABLE を参照しています。下記のSQL文法をご覧ください。例:
    my $command = $stmt->command();
columns
    my $numColumns = $stmt->columns();  # Scalar context
    my @columnList = $stmt->columns();  # Array context
    my($col1, $col2) = ($stmt->columns(0), $stmt->columns(1));
このメソッドはカラムリストを取り出すために使われます。意味は問い合わせコマンドに依存します:
    SELECT $col1, $col2, ... $colN FROM $table WHERE ...
    UPDATE $table SET $col1 = $val1, $col2 = $val2, ...
        $colN = $valN WHERE ...
    INSERT INTO $table ($col1, $col2, ..., $colN) VALUES (...)

引数なしで使われると、カラムのリスト$col1, $col2, ..., $colNを返します。代わりにカラム番号を引数として使うこともできます。カラムリストは空であるかもしれないことに注意してください。例えば以下のような場合

    INSERT INTO $table VALUES (...)

そしてCREATE または DROP ステートメントでは。

しかし"カラムを返す"とはどういった意味でしょうか?それはSQL::Statement::Columnインスタンスを返します。このクラスはtableとnameというメソッド、両方とも対応するスカラを返すを実装しています。例えば以下のステートメントについて考えてみてください:

    INSERT INTO foo (bar) VALUES (1)
    SELECT bar FROM foo WHERE ...
    SELECT foo.bar FROM foo WHERE ...

このすべての場合、以下のような厳密の1つのcolumnインスタンスが返されます

    $col->name() eq 'bar'
    $col->table() eq 'foo'
tables
    my $tableNum = $stmt->tables();  # Scalar context
    my @tables = $stmt->tables();    # Array context
    my($table1, $table2) = ($stmt->tables(0), $stmt->tables(1));

columnsと同様に、このメソッドはSQL::Statement::Tableのインスタンスを返します。UPDATE, DELETE, INSERT, CREATE DROP では、常に1つのテーブルが返されます。SELECT ステートメントは、結合(join)の場合には1つ以上のテーブルを返すかもしれません。Tableオブジェクトは1つのメソッドnameを提供し、それはテーブル名を返します。
 
params
    my $paramNum = $stmt->params();  # Scalar context
    my @params = $stmt->params();    # Array context
    my($p1, $p2) = ($stmt->params(0), $stmt->params(1));
paramsメソッドはステートメントで使われる入力パラメータについての情報を返します。例えば以下について考えてみましょう
    INSERT INTO foo VALUES (?, ?)

これは2つのSQL::Statement::Paramのインスタンスを返します。Paramオブジェクトは1つのメソッド、$param->num()を実装し、それはパラメータ番号を取り出します(上の場合には0と1)。今のところ、とても便利とはいえませんが...:-)

row_values

    my $rowValueNum = $stmt->row_values(); # Scalar context
    my @rowValues = $stmt->row_values();   # Array context
    my($rval1, $rval2) = ($stmt->row_values(0),
                          $stmt->row_values(1));

このメソッドは以下のようなステートメントで$val1, $val2, ... $valNの値を読み込むために使われます
    UPDATE $table SET $col1 = $val1, $col2 = $val2, ...
        $colN = $valN WHERE ...
    INSERT INTO $table (...) VALUES ($val1, $val2, ..., $valN)

これはスカラの値またはSQL::Statement::Paramインスタンスを返します。

order
    my $orderNum = $stmt->order();   # Scalar context
    my @order = $stmt->order();      # Array context
    my($o1, $o2) = ($stmt->order(0), $stmt->order(1));

SELECT ステートメントでは、これを使ってORDER節を探すために使うことができます。例:
    SELECT * FROM FOO ORDER BY id DESC, name

この場合、orderは2つのSQL::Statement::Orderインスタンスを返します。Orderオブジェクトを試すために$o->table()、$o->column()そして$o->desc()を使うことができます。

limit
    my $l = $stmt->limit();
    if ($l) {
      my $offset = $l->offset();
      my $limit = $l->limit();
    }

SELECT ステートメントでは、カーソル化を実装するためにLIMIT節を使うことができます:
    SELECT * FROM FOO LIMIT 5
    SELECT * FROM FOO LIMIT 5, 5
    SELECT * FROM FOO LIMIT 10, 5

これら3つのステートメントはそれぞれテーブルFOOの行0..4, 5..9, 10..14を取り出します。LIMIT節が使われていなければ、$stmt->limitはundefを返します。そうでなければSQL::Statement::Limit.のインスタンスを返します。このオブジェクトはそれぞれ最初の行のインデックスと行の最大数を取り出すためのoffsetとlimitメソッドを持ちます。

where
    my $where = $stmt->where();

このメソッドはWHERE節のシンタックス・ツリーを検査するために使われます。(もしWHERE節が使われていなければ)undefを返すか、SQL::Statement::Opのインスタンスを返します。Opインスタンスは4つのメソッドを提供します:
op
演算子を返します。以下のもののいずれか1つです AND, OR, =, <>, >=, >, <=, <, LIKE, CLIKE ,IS.
arg1
arg2
演算子の左辺、右辺を返します。これはスカラ値、SQL::Statement::Param オブジェクトあるいはまだ別のSQL::Statement::Opインスタンスであるかもしれません。
neg
評価の後、結果を演算結果を否定しなければならなければ、TRUE値を返します。
 

WHERE 節を評価するには、whereメソッドで一番上のOpインスタンスを取り出します。そして演算子の左辺と右辺をおそらく再起的に評価します。それが済んだら演算子を適用し、もし必要であれば最終的に結果を否定します。

上記のことを図にするため、以下のWHERE節を考えてみてください:

    WHERE NOT (id > 2 AND name = 'joe') OR name IS NULL

これを以下のツリーで表すことができます。

              (id > 2)   (name = 'joe')
                     \   /
          NOT         AND
                         \      (name IS NULL)
                          \    /
                            OR

こうしてWHERE節はop()フィールドが'OR'であるSQL::Statement::Op インスタンスを返します。arg2()フィールドは別のSQL::Statement::Opインスタンスを返します。それはarg1()がid(訳者注:nameじゃないの?)を表すSQL::Statement::Columnインスタンスで、arg2()フィールドには値undef(NULL)が入り、op()フィールドが'IS'です。

一番上のOpインスタンスのarg1()フィールドはop()が'AND'でneg()がTRUEを返すOpインスタンスを返すでしょう。arg1()とarg2()フィールドはOpの"id>2"と"name='joe'"を表すOpになるでしょう。

もちろんWHERE節評価のためのあらかじめ用意されたメソッドがあります。

 

WHERE節の評価

WHERE 節の評価はパラメータとカラムの値を取り出すために使われるオブジェクトに依存します。通常、これはSQL::Evalオブジェクトになりますが、実際には以下のメソッドが適用できるすべてのオブジェクトであればなんでもすることができます:

    $val = $eval->param($paramNum);
    $val = $eval->column($table, $column);

これらのメソッドについての詳しい説明は SQL::Eval をご覧ください。そのようなオブジェクトを持っていれば、以下のように呼び出してください

    $match = $stmt->eval_where($eval);

 

問い合わせの評価

できるかぎりすべてのメソッドが具象になっています。しかし実行と問い合わせのためのインターフェースは抽象です。つまりそれらを使うためにはすくなくともないメソッドを実装し、さらに/または他のものを上書きするSQL::Statementのサブクラスを派生させる必要があります。例のサブクラスについてはtest.plスクリプトをご覧ください。

すべてのメソッドが共通で持っていることは、エラーのさいにPerl例外を単純に投げるということです。

execute
ステートメントを作成した後、executeメソッドを呼び出すことにより実行しなければなりません。通常、この呼び出しをevalステートメントで囲みます:
    $@ = '';
    my $rows = eval { $self->execute($data); };
    if ($@) { die "An error occurred!"; }

成功した場合、メソッドは影響を受けた行の数を、わからなければ-1を返します。さらに以下の属性を設定します。

    $self->{'NUM_OF_FIELDS'}
    $self->{'NUM_OF_ROWS'}
    $self->{'data'}

最後のものは結果行の配列リファレンスになります。引数$dataは具象サブクラスによりプライベートに使われ、すべてのメソッドにメソッドに渡されます。(内部的には属性として実装されていません。そうでなければガベージ・コレクションを妨げる自己参照データ構造になってしまうでしょう)

CREATE
DROP
INSERT
UPDATE
DELETE
SELECT
executeにより実際に動くために呼び出されます。通常、これらは$self->open_tables()を呼び出し、$self->verify_columns()そして刈られ自身の仕事をすることにより、SQL::Evalオブジェクトを作成します。最終的には以下の3つのものを返します。
    ($self->{'NUM_OF_ROWS'}, $self->{'NUM_OF_FIELDS'},
     $self->{'data'})

そこでexecuteはこれらの属性を設定することができます。例:

    ($self->{'NUM_OF_ROWS'}, $self->{'NUM_OF_FIELDS'},
     $self->{'data'}) = $self->SELECT($data);
verify_columns
ステートメントで利用される行の名前を確認するために呼び出されます。例:
    $self->verify_columns($eval, $data);
open_tables
SQL::Evalオブジェクトを作成するために呼び出されます。実際にはこれが返すものがSQL::Evalから派生されたものである必要はありません。それはメソッドの同じインターフェースを実装することで完全に十分です。詳しくはSQL::Eval をご覧ください。$data,$createModeそして$lockModeは、それぞれSQL::Eval::Table::open_tableのものに対応し、通常はそのまま渡されます。例:
    my $eval = $self->open_tables($data, $createMode, $lockMode);

evalオブジェクトは$self->verify_columnsまたは$self->eval_whereを呼び出すために使われます。

open_table
このメソッドは完全に抽象でサブクラスにより実装されなければなりません。$self->open_tablesのデフォルトの実装は、ステートメントによって使われるすべてのテーブルについてこのメソッドを呼び出します。サブクラスの実装例についてはtest.plスクリプトをご覧ください。
 

SQL文法

SQL::StatementモジュールはANSI SQLやそれに類似するものとはかけ離れており、DBD::CSVを実装するために設計されています。DBD::CSV(3)をご覧ください。

ここではフォーマルな文法をしめすのではなく、インフォーマルな説明をしたいと思います。厳密なものが必要であれば、sql_yacc.yにあるステートメント定義をお読みください。

文法での主な語彙要素は以下のものです:

Integers (整数)
Reals (実数)
文法上、明白
 
Strings (文字列)
シングルまたはダブル・クォートで囲まれている;いくつかの文字はバックシュラッシュでエスケープされる必要があります、特にバックシュラッシュそのもの(\\)、NULバイト(\0)、改行(\n)、キャリッジリターン(\r)、そしてクォート(\'または\")。
 
Parameters (パラメータ)
パラメータは整数(Integer)、実数(Real)そして文字列(String)のようにスカラ値を表します。しかしそれらはPrepare()の中ではなく、Execute()の中で読み込まれます。パラメータ(Parameter)はクエスチョン・マークにより表されます。
 
Identifiers (識別子)
識別子はテーブル名やカラム名です。構文上、これらは英字で始まり英数字が続くもので構成されます。SELECT, INSERT, INTO, ORDER, BY, WHERE, ...のような識別子は許されず他のトークンのために予約されています。

提供するものを以下に示します:

CREATE

これはCREATE TABLE command:

    CREATE TABLE $table ( $col1 $type1, ..., $colN $typeN,
                          [ PRIMARY KEY ($col1, ... $colM) ] )

カラム名は$col1, ... $colNです。カラム型はNTEGER, CHAR(n), VARCHAR(n), REAL または BLOBにすることができます。これらの型は現在のところ完全に無視されます。それは(オプションの)PRIMARY KEY節についても同じです。

DROP

とっても簡単:

    DROP TABLE $table

INSERT

以下のようにできます

    INSERT INTO $table [ ( $col1, ..., $colN ) ]
        VALUES ( $val1, ... $valN )

DELETE

    DELETE FROM $table [ WHERE $where_clause ]

$where_clauseの説明については下記のSELECTをご覧ください。

UPDATE

    UPDATE $table SET $col1 = $val1, ... $colN = $valN
        [ WHERE $where_clause ]

$where_clauseの説明については下記のSELECTをご覧ください。

SELECT

    SELECT [DISTINCT] $col1, ... $colN FROM $table
        [ WHERE $where_clause ] [ ORDER BY $ocol1, ... $ocolM ]

$where_clause は$val1 $op $val2という形式のブール式を基本としています。$opには以下のものがいずれかになります。'=', '<>', '>', '<', '>=', '<=', 'LIKE', 'CLIKE' ,'IS'。そうしたブール式を組み合わせるためにOR,ANDそして括弧やそれらを否定するためのNOTを使うこともできます。


インストール

ほかのほとんどのPerlモジュールと同様に、以下のようにするだけです

    perl Makefile.PL
    make                (Win32を使っているのであれば、nmake または dmake)
    make test           (テストが失敗したら教えてください)
    make install

わかっている問題には以下のものがあります:

 


内部実装

内部的に、このモジュールは3つの部分に分けられます:

 

Perlとは独立したC部分

この部分はファイルsql_yacc.y、sql_data.h、sql_data.cそしてsql_op.cに入っているおり、完全にPerlからは独立しています。他のスクリプト言語、例えばTclや本当のCのアプリケーションからも使うことができるでしょう。

なぜPerlから独立しているのか疑問に思うかもしれません。まず最初に、私はこれ自体が価値のあるターゲットであると思いました。しかし主な理由はbisonが生成するコードの中にPerlヘッダを使えなかったことです。なんらかの理由で、PerlヘッダはXSへのYaccインターフェースのほとんどすべてをエクスポートします。このため私自身のbisonコードより作成された定数や構造体が再定義されてしまうのです :-(。

 

Perlに依存した C部分

これはStatement.xsに入っています。C部分とはCの構造体sql_stmnt_tを介して通信します。実際には、SQL::Statementオブジェクトはそうした構造体へのポインタにすぎません。XSはcolumns()、Table()、where()、...を呼び出し、この構造体からデータを取り出し、Perlオブジェクトに変換するだけです。この構造体についての詳細はsql_stmt_t構造体をご覧ください。

 

Perl部分

ステートメント・データを取り出すためのスタブ関数とともに、これは主にWHERE節の評価を除く問い合わせ処理です。

sql_stmt_t 構造体

この構造体は最適なパフォーマンスのために設計されています。典型的な問い合わせは4つまたは5つのmalloc()呼び出しとともに解析されます;特に文字列を格納するためにはメモリは取得されません;問い合わせ文字列へのポインタだけが使われます。

ステーメントはそのトークンをvalues配列に格納します。その配列要素はsql_val_t型で、それはほとんどの興味あるトークンを表すことができるunionです;例えば整数と実数はunionのdata.iとdata.d部分に格納され、文字列はdata.str部分に、カラムはdata.col部分にといった具合です。配列は64個の要素の塊で占有されます。このため1つのmalloc()で完全な配列を占有するのには通常十分です。同じ型はvalues配列へのポインタを使います:例えば演算子がsql_op_tに格納され、それにvalueテーブルへのポインタ、他の演算子あるいはスカラを指しているポインタである要素arg1とarg2が入っているといった場合です。これらのポインタはインデックスで格納され、そのため配列をrealloc()を使って拡張することができます。

sql_stmt_t構造体には他の配列も入っています:columns、tables、rowvals、order、... はcolumns(),tables(),row_values()そしてorder()メソッドで返されるデータを表します。これらすべては再び整数で格納されるvalues配列へのポインタを持っています。

配列はSQL_Statement_Prepareのなかで_InitArray呼び出しで初期化され、SQL_Statement_Destroyでの_DestroyArrayで廃棄されます。配列要素は_AllocDataを呼び出すことで取得され、それはインデックスを返します。数値-1はエラーまたはNULL値のために使われます。

WHERE節の評価

WHERE節はSQL_Statement_EvalWhere()を呼び出すことにより評価されます。この関数はPerl独立部分にありますが、例えばカラムやパラメータの値など、Perl部分からデータを取り出すことができる必要があります。これらの値はsql_eval_t構造体に格納されたコールバックを介して取り出されます。フィールドstmt->evalData はそのような構造体を示します。もちろんメソッドの呼び出しは(Statement.xsでeval_whereがやっているように)SQL_Statement_EvalWhereにより使われないプライベートなデータを入れるためにsql_eval_t構造体を拡張することができます。

 

機能(FEATURE)

sql_parser_tを介して異なるパーザーが実装されます。これは主にyse/noフラグの組み合わせです。もし機能を追加したければ以下のようにしてください:

まず最初にsql_parser_t構造体を拡張します。例えば"select_join"のように、あなたの機能があるステートメントの一部であれば、statementsセクションにそれを置いてください。そうでなければ"misc"または"general"のようなセクションを選んでください。(セクション設計には特別なことはありませんが、構造体がいたむことはないでしょう)

その次に、sql_yacc.yにあなたの機能を追加してください。もしあなたの機能がlexerも拡張する必要があれば、以下のようにしてください:

    if (FEATURE(misc, myfeature) {
        /*  Scan your new symbols  */
        ...
    }

例として BOOL シンボルをご覧ください。

もしパーザーを拡張する必要があれば、以下のようにしてください:

    my_new_rule:
        /*  NULL, old behaviour, doesn't use my feature  */
        | my_feature
            { YFEATURE(misc, myfeature); }
    ;

こうするとFEATURE(misc, myfeature)のセットを持たないすべてのパーザーが解析エラーをここで起こします。再び例としてBOOLシンボルをご覧ください。

3番目にすることは組み込みパーザーを拡張することです。もしあなたの機能をサポートするのであれば1を、そうでなければ0を追加してください。現在は2つの組み込みパーザーがあります: ansiParser は sql_yacc.y に sqlEvalParser は Statement.xsにあります。

最後にStatement.xsのfeatureメソッドにあなたの機能のサポートを追加します。これだけです!

 


マルチスレッド化

モジュールコードはすべてリエントラントです。特に、パーザーは%pure_parserとともに作成されいます。リエントラントなパーザーについての詳細はbison(1)をご覧ください。つまりこれはスレッド間でハンドルを共有しない限り、このモジュールはマルチスレッド化の準備ができているということです。例のパーザーのための読み込みのみのハンドラーであれば共有することができます。

ステートメント・ハンドルはスレッドで共有することはできません、少なくともシリアライズされたアクセスを許すのでなければ。スレッドごとのハンドルはいつでも安全です。


作者と著作権(AUTHOR AND COPYRIGHT)

(原文のまま)

This module is Copyright (C) 1998 by

    Jochen Wiedmann
    Am Eisteich 9
    72555 Metzingen
    Germany

    Email: joe@ispsoft.de
    Phone: +49 7123 14887

All rights reserved.

You may distribute this module under the terms of either the GNU General Public License or the Artistic License, as specified in the Perl README file.




参考資料

DBI(3), DBD::CSV(3)


ホーム Perlの小技

ご意見、ご質問はこちらの掲示板で受け付けています。
またメールは河馬屋(Nifty)にお願いします。