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節、ステートメント評価のためのメソッドを提供します。
何が適切なSQLとして受け取られるかは、parserオブジェクトに依存します。parserが持ったり持たなかったりするいわゆる機能の組み合わせがあります。通常は組み込みparserではじめます:
my $parser = SQL::Parser->new($name, [ \%attr ]);
現在2つのパーザーが組み込まれています。Ansi パーザーはANSI SQLの適切なサブセットを実装しています(少なくとも私はそう願っています:-))。SQL::Statement パーザーはDBD::CSVドライバで使われています。
個々の機能を問い合わせたり、設定することができます。現在利用できるのは以下のものがあります:
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オブジェクトの作成をご覧ください。
以下のメソッドは問い合わせについての情報を取得するために使うことができます:
my $command = $stmt->command();
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'
my $tableNum = $stmt->tables(); # Scalar context
my @tables = $stmt->tables(); # Array context
my($table1, $table2) = ($stmt->tables(0), $stmt->tables(1));
my $paramNum = $stmt->params(); # Scalar context
my @params = $stmt->params(); # Array context
my($p1, $p2) = ($stmt->params(0), $stmt->params(1));
INSERT INTO foo VALUES (?, ?)
これは2つのSQL::Statement::Paramのインスタンスを返します。Paramオブジェクトは1つのメソッド、$param->num()を実装し、それはパラメータ番号を取り出します(上の場合には0と1)。今のところ、とても便利とはいえませんが...:-)
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));
UPDATE $table SET $col1 = $val1, $col2 = $val2, ...
$colN = $valN WHERE ...
INSERT INTO $table (...) VALUES ($val1, $val2, ..., $valN)
これはスカラの値またはSQL::Statement::Paramインスタンスを返します。
my $orderNum = $stmt->order(); # Scalar context
my @order = $stmt->order(); # Array context
my($o1, $o2) = ($stmt->order(0), $stmt->order(1));
SELECT * FROM FOO ORDER BY id DESC, name
この場合、orderは2つのSQL::Statement::Orderインスタンスを返します。Orderオブジェクトを試すために$o->table()、$o->column()そして$o->desc()を使うことができます。
my $l = $stmt->limit();
if ($l) {
my $offset = $l->offset();
my $limit = $l->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メソッドを持ちます。
my $where = $stmt->where();
AND, OR, =,
<>, >=, >,
<=, <, LIKE,
CLIKE ,IS. 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 節の評価はパラメータとカラムの値を取り出すために使われるオブジェクトに依存します。通常、これはSQL::Evalオブジェクトになりますが、実際には以下のメソッドが適用できるすべてのオブジェクトであればなんでもすることができます:
$val = $eval->param($paramNum);
$val = $eval->column($table, $column);
これらのメソッドについての詳しい説明は SQL::Eval をご覧ください。そのようなオブジェクトを持っていれば、以下のように呼び出してください
$match = $stmt->eval_where($eval);
できるかぎりすべてのメソッドが具象になっています。しかし実行と問い合わせのためのインターフェースは抽象です。つまりそれらを使うためにはすくなくともないメソッドを実装し、さらに/または他のものを上書きするSQL::Statementのサブクラスを派生させる必要があります。例のサブクラスについてはtest.plスクリプトをご覧ください。
すべてのメソッドが共通で持っていることは、エラーのさいにPerl例外を単純に投げるということです。
$@ = '';
my $rows = eval { $self->execute($data); };
if ($@) { die "An error occurred!"; }
成功した場合、メソッドは影響を受けた行の数を、わからなければ-1を返します。さらに以下の属性を設定します。
$self->{'NUM_OF_FIELDS'}
$self->{'NUM_OF_ROWS'}
$self->{'data'}
最後のものは結果行の配列リファレンスになります。引数$dataは具象サブクラスによりプライベートに使われ、すべてのメソッドにメソッドに渡されます。(内部的には属性として実装されていません。そうでなければガベージ・コレクションを妨げる自己参照データ構造になってしまうでしょう)
($self->{'NUM_OF_ROWS'}, $self->{'NUM_OF_FIELDS'},
$self->{'data'})
そこでexecuteはこれらの属性を設定することができます。例:
($self->{'NUM_OF_ROWS'}, $self->{'NUM_OF_FIELDS'},
$self->{'data'}) = $self->SELECT($data);
$self->verify_columns($eval, $data);
my $eval = $self->open_tables($data, $createMode, $lockMode);
evalオブジェクトは$self->verify_columnsまたは$self->eval_whereを呼び出すために使われます。
SQL::StatementモジュールはANSI SQLやそれに類似するものとはかけ離れており、DBD::CSVを実装するために設計されています。DBD::CSV(3)をご覧ください。
ここではフォーマルな文法をしめすのではなく、インフォーマルな説明をしたいと思います。厳密なものが必要であれば、sql_yacc.yにあるステートメント定義をお読みください。
文法での主な語彙要素は以下のものです:
提供するものを以下に示します:
これは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 TABLE $table
以下のようにできます
INSERT INTO $table [ ( $col1, ..., $colN ) ]
VALUES ( $val1, ... $valN )
DELETE FROM $table [ WHERE $where_clause ]
$where_clauseの説明については下記のSELECTをご覧ください。
UPDATE $table SET $col1 = $val1, ... $colN = $valN
[ WHERE $where_clause ]
$where_clauseの説明については下記の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
わかっている問題には以下のものがあります:
もう1つの選択肢は外部のalloca.cを使うことです、例えば
http://www.pu.informatik.th-darmstadt.de/FTP/pub/pu/alloca.c http://www.cs.purdue.edu/homes/young/src2www-example/alloca.c.html
私はこれらについてテストしていませんし、SQL::Statementモジュールにそれらを入れるための詳しい方法を示すこともできません。しかし例えばalloca.oをsql_yacc.oの後ろに入れることでsql_yacc.c同じ命令でalloca.cをコンパイルし、最後にリンカー・コマンドを繰り返させるだけで十分でしょう。
私がalloca()なしに動くようにソースを修正できないことに注意してください。というのもそれはbisonパーザーがalloca()を使っているからで、私はbisonにより生成されるコードが手元にないからです。
この問題と可能な解決策を示してくれたTheo Perterson<theo@acsp.com>に感謝します。
内部的に、このモジュールは3つの部分に分けられます:
この部分はファイルsql_yacc.y、sql_data.h、sql_data.cそしてsql_op.cに入っているおり、完全にPerlからは独立しています。他のスクリプト言語、例えばTclや本当のCのアプリケーションからも使うことができるでしょう。
なぜPerlから独立しているのか疑問に思うかもしれません。まず最初に、私はこれ自体が価値のあるターゲットであると思いました。しかし主な理由はbisonが生成するコードの中にPerlヘッダを使えなかったことです。なんらかの理由で、PerlヘッダはXSへのYaccインターフェースのほとんどすべてをエクスポートします。このため私自身のbisonコードより作成された定数や構造体が再定義されてしまうのです :-(。
これはStatement.xsに入っています。C部分とはCの構造体sql_stmnt_tを介して通信します。実際には、SQL::Statementオブジェクトはそうした構造体へのポインタにすぎません。XSはcolumns()、Table()、where()、...を呼び出し、この構造体からデータを取り出し、Perlオブジェクトに変換するだけです。この構造体についての詳細はsql_stmt_t構造体をご覧ください。
ステートメント・データを取り出すためのスタブ関数とともに、これは主にWHERE節の評価を除く問い合わせ処理です。
この構造体は最適なパフォーマンスのために設計されています。典型的な問い合わせは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節はSQL_Statement_EvalWhere()を呼び出すことにより評価されます。この関数はPerl独立部分にありますが、例えばカラムやパラメータの値など、Perl部分からデータを取り出すことができる必要があります。これらの値はsql_eval_t構造体に格納されたコールバックを介して取り出されます。フィールドstmt->evalData はそのような構造体を示します。もちろんメソッドの呼び出しは(Statement.xsでeval_whereがやっているように)SQL_Statement_EvalWhereにより使われないプライベートなデータを入れるためにsql_eval_t構造体を拡張することができます。
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)をご覧ください。つまりこれはスレッド間でハンドルを共有しない限り、このモジュールはマルチスレッド化の準備ができているということです。例のパーザーのための読み込みのみのハンドラーであれば共有することができます。
ステートメント・ハンドルはスレッドで共有することはできません、少なくともシリアライズされたアクセスを許すのでなければ。スレッドごとのハンドルはいつでも安全です。
(原文のまま)
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.
ご意見、ご質問はこちらの掲示板で受け付けています。
またメールは河馬屋(Nifty)にお願いします。