gadfly はリレーショナルデータベースの機能を提供する Python モジュールの集合であり,全てが Python で記述されています.gadfly は 銀河系標準の RDBMS SQL (Standard Query Language) のサブセットを サポートしています.
gadfly の最大の魅力は,Python が稼動するところならどこでも動かせる という点と,標準の Python socket インタフェースをサポートする プラットフォーム上であれば,どこでもクライアント/サーバ動作をサポート しているという点です. gadfly がストレージに用いるファイルフォーマット ですら,クロスプラットフォームです --- つまり, gadfly データベースの ディレクトリは,バイナリをコピーする機構によって Win95 システムから Linux システムへと移動させることができ, gadfly はこのデータベースを 読み込んで動作することができます.
gadfly は,インデクスで構造化されたテーブルの集合からなり, パージステントなデータベースをサポートします.そしてこれらのテーブルに アクセスし,内容を更新するための SQL の大きなサブセットをサポートします.
gadfly はログに基づいたリカバリプロトコルをサポートします.これにより, データベースが正しい方法でシャットダウンされなかった場合 (すなわち, ディスククラッシュ以外の,CPU かソフトウェアのクラッシュが起きた場合) でさえ,データベースに対するコミット済みの操作を復元することができます.
gadfly は TCP/IP に基づいたクライアント/サーバモードをサポートします. これにより,遠隔のクライアントが (インターネットのような) TCP/IP ネットワークを介して,設定・変更の可能なセキュリティメカニズムの下で gadfly データベースにアクセスすることができます.
gadfly は (現時点では) 真の同期制御機構をもたず,ファイルシステム ベースのインデクス生成を行うため,複数プロセスによる非常に巨大な トランザクションを前提とするシステムでの利用には向きません.
gadfly は kwParsing パッケージに非常に強く依存しているため, kwParsing パッケージの一部分として, kwParsing と同じ寛容なコピーライトの 下で配布されています.
gadfly を使うことで,テーブル形式のデータのストア,取得,クエリ 検索を,外部のデータベースエンジンやパッケージに依存することなく, Python プログラムで行うことができます.つまり, gadfly は,「コミット されたトランザクションの復元可能性」および,「トランザクションの 棄却機能」という点で,完全,単純かつ簡便で,比較的効率がよく,メモリ上 に配置されるリレーショナルデータベーススタイルのエンジンを Python プログラムで利用できるようにします.
unix システムの /etc ディレクトリまたは win32 システムのレジストリ, またはファイルシステムのそこかしこに横たわる無数 (buzzillions) の設定 ファイル群をひもとけば,最近のプログラムは非常に強くテーブル様のデータに 依存していることは明白です.さらに,メモリの価格は下がりつづけていて, 低価格のマシンの性能が向上して,より多くのメモリ容量を持つようになって きています.これにともなって,データベーススタイルの作業は,もっと メモリ内に置いた大きなデータセット上で行われるべきだということが明らかに なってきています.このため, gadfly のようなメモリ上に載る SQL 実装は 本格的な業務で有用となってゆくでしょう.
gadfly はリレーショナルスタイルの表現と SQL クエリ言語を用いています. これらは広く理解されており,多くのプログラマの間で親しみのある手法だから です. SQL は万能ではありませんが,多くの重要な問題を簡単かつ上手に解決 できるという点でよくできた部分をもっています (そして Python がその他の 部分を解決してくれます) .
主モジュール "gadfly" は Greg Stein の Python Database API に厳密に 準拠していて, Python DB-SIG で議論され,認証されています.とはいう ものの,私がまだ理解していないいくつかの API は実装されていません. どのインタフェースが実装されているのか,あるいはスタブしか存在しない のかを正確に知るには, gadfly.py を読んでください.
データベースの同期更新はまだサポートされていません.現在, "個々の データベース" は,独立に一つのプロセスによって書き込み/更新が行われる ように設計されています.アクセスを TCP/IP 越しのサーバプロセスによって アービトレーションすれば,複数のプロセスから一つの gadfly データベースに アクセスすることができます.
多くの Python - データベースエンジン間インタフェースと違って,gadfly データベースは Python を使って生成しなくてはなりません (例えば Oracle なら,他のツールを使うでしょう) .データベースを生成するには :
import gadfly
connection = gadfly.gadfly()
と,引数なしで呼び出し,以下のスタートアップメソッドでデータベースを 動作開始します:
connection.startup("mydatabase", "mydirectory")
ここで "mydirectory" は実際に存在するディレクトリで,データベース ファイルをストアするために書き込み可能でなければなりません. スタートアップメソッドは "mydirectory" にいくつかのファイルを生成します. この操作は, "mydirectory" 下にある "mydatabase" と呼ばれる gadfly データベースを上書き (clobber) する効果を持つことになります.とはいえ, gadfly は同じデータベースへの二重接続を禁止するでしょう.
最初の import gadfly は SQL 文の解釈に必要なやや大きなデータ構造を 読み込んで初期化するため,他のモジュールの import より長い時間がかかる かもしれないので注意してください.
さて,この新しいデータベースで,テーブルを生成し,テーブルの内容を 増やし,ハッピーになったところでその結果を commit してみましょう :
cursor = connection.cursor()
cursor.execute("create table ph (nm varchar, ph varchar)")
cursor.execute("insert into ph(nm, ph) values ('arw', '3367')")
cursor.execute("select * from ph")
for x in cursor.fetchall():
print x
# prints ('arw', '3367')
connection.commit()
データベースがすでに存在するなら,以下のようにして再接続することが できます :
import gadfly
connection = gadfly.gadfly("mydatabase", "mydirectory")
この操作はデータベーステーブルを読み込み,もっとも最近コミットされた 値に設定します.初期化されたデータベースにはクエリを発行したり更新したり できます :
cursor = connection.cursor()
cursor.execute("update ph set nm='aaron' where nm='arw'")
cursor.execute("select * from ph")
for x in cursor.fetchall():
print x
# prints ('aaron', '3367')
もし更新内容をコミットしたくないのなら,単に接続オブジェクトに対して commit の実行 (テーブルをファイルに書き込む) をしないようにできます. もし現在のテーブルをもとの値に復元したいのなら以下を使います :
connection.abort()
更新は connection.commit() を発行したときにのみ保存されます[実際には, autocheckpoint が無効化されていれば,更新内容は checkpoint でのみ テーブルファイルに保存されます --- 詳しくはリカバリ機構についての ドキュメントを読んでください] .
結果を美しくフォーマットした出力 (pretty print) を得たいなら:
print cursor.pp()
とします. (何も SELECT していないカーソルはNoneを返します) .
現行のバージョンでは,データベースへの「接続」時にすべてのテーブルが メモリ上に読み込まれ,「手を触れた」テーブルは checkpoint 時に書き込まれ ます.各テーブルはデータベース格納先のディレクトリ下で,別個のファイル として表され,「データ定義ファイル」も同様にして表されます.また, テーブルの使用状態がアクティブである間は,同様のログファイルがアクティブな ディレクトリ下に現れます.プロセスがクラッシュした場合,このログファイルは コミットされた操作を復元するために用いられます.
現時点では,gadfly は ODBC 2.0 仕様で要求されている SQL 文法のかなり 大部分をサポートします.より詳細な SQL 表現については, SQL 生成のページ を参照してください. SELECT を含む SQL 文でサポートされているのは以下の とおりです :
SELECT [DISTINCT|ALL] expressions or *
FROM tables
[WHERE condition]
[GROUP BY group-expressions]
[HAVING aggregate-condition]
[union-clause]
[ORDER BY columns]
この構文はかなり強力です.この構文は以下のように直感的に理解できます:
1) 複数のテーブルに含まれる行の組み合わせを生成する (FROM の行)
2) 条件を満たさない組み合わせを除外する (WHERE の行)
3) group-expressions を満たす集合に集約する (GROUP 行が存在する場合)
4) aggregate-condition を満たさない集合を除外する (HAVING が存在する場合)
5) 結果に保持しておくカラムデータを計算する (SELECT の行)
6) 他の select 構文の結果との combine (union, difference, intersect) を 行う (union-clause がある場合)
7) DISTINCT があれば,重複するエントリを捨て去る
8) 結果をカラムのデータに従って,(指定した順位に対し,昇順または降順に) 並べる (ORDER がある場合)
gadfly における実際の実装,特にステップ 1 および 2 は, (変換の最適化や ハッシュ結合アルゴリズムを組み合わせることによって) 直感的な構文解析よりも より最適化されています.
条件式は評価式間の等号,不等号を扱うことができます.条件式はまた, AND, OR, NOT で組み合わせることができます.条件式にはカラム名,定数, そしてそれらに対する標準的な算術演算を含みます.
埋め込みクエリはサポートされています.これにはサブクエリ式, expr IN (サブセレクション),量での比較 (quantified comparisons),EXISTS (サブセレクション) 述語を含みます.
集約テストと計算は GROUP 後で,かつカラムを選択する前に行われます (3, 4, 5 ステップ).集約操作には COUNT(*), COUNT(式), SUM(式), MAX(式), MIN(式), および非標準 SQL の MEDIAN (式) が含まれます. これらには重複を除外するために, COUNT (DISTINCT drinker) のように DISTINCT を適用することができます. GROUP が存在しない場合,集約演算は ステップ 2 後の結果全体に対して適用されます.
SELECT 構文についてはさらに多くの知っておくべきことがあります. ``test/test_gadfly.py`` テストスイートには SELECT 構文の例がたくさん あります.
サポートされている構文の詳細な定義については,gadfly/grammar.py を 実行してください.SQLに関する本は500ばかりありますが,そこから各々の 構文の意味の解説を探してみてください.そして,もしそれらの構文の いずれかが gadfly で実行された際に間違った結果を出力するようなら, ぜひ私に知らせてください!
CREATE TABLE 構文を使ったテーブルの生成は以下のようにします :
CREATE TABLE name (colname datatype [, colname datatype...])
今のところ「サポートされている」データ型は,整数 (integer), 浮動小数点数 (float), そして文字 (varchar) です.これらの型の区別は 実装上は無視されていて,現在のところハッシュ可能でマーシャライズ (整列化)できるどんな型も任意のカラムに入れることができます (しかし これは変更されるかも知れません). 例えば以下のようにしてテーブルを 生成します :
create table frequents
(drinker varchar,
bar varchar,
perweek integer)
現状では "varchar" と指定したカラムには,タプル,複素数,その他の データを入れることができます.しかし今後常にそうであるとは当てに しないでください.
gadfly は検索つき DELETE および UPDATE, INSERT VALUES および INSERT サブセレクト, CREATE/DROP INDEX, そして DROP TABLE をサポートします. これらには略式の構文があります :
DELETE FROM table WHERE condition
UPDATE table SET col=expr [, col=expr...] WHERE condition
INSERT INTO table [(column [, column...])] values (value [, value...])
INSERT INTO table [(column [, column...])] subselect
CREATE [UNIQUE] INDEX name ON table (column [, column...])
DROP TABLE table
DROP INDEX name
以下の例は test/test_gadfly.py で見てください :
delete from templikes where be='rollingrock'
update templikes set dr='norman' where dr='norm'
insert into ph(nm,ph) values ('nan', '0356')
insert into templikes(dr, be)
select drinker, beer from likes
create index sbb on serves (beer, bar)
drop table templikes
drop index tdindex
複数の SQL 構文は,S に構文をセミコロンで区切って記述することにより, 一つの cursor.execute(S) で実行することができます.例えば,S には以下の ような文字列を指定することができます :
drop index tdindex;
drop table templikes
(末尾にはセミコロンを入れないように!)
これら構文の大半のサンプルは gftest.py を見てください.SQL はケース インセンシティブ (キーワード文字列の大文字,小文字は区別しない) ことに 注意してください.サポートされているすべての構文要素の詳細な定義は sqlgram.py を見てください.
式には,以下のように特殊な式 ? (ODBC スタイルの動的表現) を含める
ことができます :
insertstat = "insert into ph(nm,ph) values (?, ?)"
cursor.execute(insertstat, ('nan', "0356"))
cursor.execute(insertstat, ('bill', "2356"))
cursor.execute(insertstat, ('tom', "4356"))
動的な値は似た操作を行うために,カーソルがパース済みの同じ式を何度も 使えるようにします.上の insertstat はたった一度だけパースされ, データベースにバインドされます.動的な属性を用いると,アクセスを高速化 することができます.従って,上の例は以下の等価な操作よりもより高速に 動作します :
cursor.execute("insert into ph(nm,ph) values ('nan', '0356')");
cursor.execute("insert into ph(nm,ph) values ('bill', '2356')");
cursor.execute("insert into ph(nm,ph) values ('tom', '4356')");
動的な属性は他の式 (SELECT, UPDATE, そしてDELETE も)を含む構文の中 でも用いることができます.
SELECT, UPDATE, および DELETE に関しては,動的なSQL式への代入は以下の ように一つのタプルから構成されなくてはなりません :
stat = "select * from ph where nm=?"
cursor.execute(stat, ("nan",))
...
cursor.execute(stat, ("bob",))
...
動的な値の代入によってパースとバインド (コストの高い操作です!) の 必要性をなくすことができるので,上の操作は以下の等価な式よりも高速に 動作します:
cursor.execute("select * from ph where nm='nan'")
...
cursor.execute("select * from ph where nm='bob'")
...
もしいくつかの似たようなクエリを複数回繰り返すのなら,個々のクエリの 「テンプレート文字列」に対し,一意にカーソルオブジェクトを関連付けて ください.それによりそれぞれのテンプレート文字列は一度だけパースおよび バインドされます.テストスイートにある比較的複雑なクエリは,たとえ kjbuckets 拡張ライブラリがなくても,パースおよびバインドされた後では 2 倍から 3 倍高速に動作することを知っておくとよいでしょう.kjbuckets 拡張ライブラリのもとでは 5 倍から 10 倍高速に動作します.
入力したいタプルのリストを INSERT VALUES するような特殊なケースに 対しては,クエリエンジンに最適化されたバッチモードで INSERT を行わせる ようにできます.従って,上で与えられた 3 つの INSERT を行うためのより 高速な方法は以下のようになります :
data = [('nan', "0356")), ('bill', "2356"), ('tom', "4356")]
stat = "insert into ph(nm,ph) values (?, ?)"
cursor.execute(stat, data)
そしてこの方法はカーソルが以前他のデータに対して SQL 構文を実行して いる場合でさえより高速になります (なぜなら parsing や binding をしない からです).
標準では, gadfly データベースは「データベースの構造をクエリする」 ためのクエリができる内観的 ("introspective") なデータベースを含んで います --- 例えば,テーブル名とテーブルに含まれる行の名前を見るには以 下のようにします :
>>> g = gadfly()
>>> g.startup("dbtest", "dbtest")
>>> c = g.cursor()
>>> c.execute("select * from __table_names__")
>>> print c.pp()
IS_VIEW | TABLE_NAME
=========================
1 | __TABLE_NAMES__
1 | DUAL
1 | __DATADEFS__
1 | __COLUMNS__
1 | __INDICES__
1 | __INDEXCOLS__
ここで DUAL (Oracle が伝統的に使っている名前です) は,標準的な一行 一列のテストテーブルです.他のテーブルはデータベースのスキーマ構造に ついての情報を提供しています :
>>> c.execute("create table t1 (a varchar, b varchar)")
>>> c.execute("create table t2 (b varchar, c varchar)")
>>> c.execute("create unique index t1a on t1(a)")
>>> c.execute("create index t1b on t1(b)")
>>> c.execute("select * from __table_names__")
>>> print c.pp()
IS_VIEW | TABLE_NAME
=========================
0 | T1
1 | __DATADEFS__
1 | __INDICES__
0 | T2
1 | __TABLE_NAMES__
1 | __COLUMNS__
1 | DUAL
1 | __INDEXCOLS__
>>> c.execute("select * from __columns__")
>>> print c.pp()
COLUMN_NAME | TABLE_NAME
=============================
A | T1
B | T1
NAME | __DATADEFS__
DEFN | __DATADEFS__
INDEX_NAME | __INDICES__
TABLE_NAME | __INDICES__
IS_UNIQUE | __INDICES__
TABLE_NAME | __TABLE_NAMES__
IS_VIEW | __TABLE_NAMES__
B | T2
C | T2
COLUMN1 | DUAL
TABLE_NAME | __COLUMNS__
COLUMN_NAME | __COLUMNS__
INDEX_NAME | __INDEXCOLS__
COLUMN_NAME | __INDEXCOLS__
>>> c.execute("select * from __indices__")
>>> print c.pp()
IS_UNIQUE | TABLE_NAME | INDEX_NAME
===================================
0 | T1 | T1B
1 | T1 | T1A
>>> c.execute("select * from __indexcols__")
>>> print c.pp()
COLUMN_NAME | INDEX_NAME
========================
B | T1B
A | T1A
>>> c.execute("select * from dual")
>>> print c.pp()
COLUMN1
=======
0
インストールが終わったら,同じディレクトリから対話的インタプリタを 使って,作成されたデータベースを対話的にテストできます.例えば以下の ように :
Python 2.1.3 (#1, Apr 30 2002, 19:37:40)
[GCC 2.96 20000731 (Red Hat Linux 7.1 2.96-96)] on linux2
Type "copyright", "credits" or "license" for more information.
>>>
>>> from gadfly import gadfly
>>> connection = gadfly("test", "dbtest")
>>> cursor = connection.cursor()
>>> cursor.execute("select * from frequents")
>>> cursor.description
(('DRINKER', None, None, None, None, None, None), ('PERWEEK', None, None,
None, None, None, None), ('BAR', None, None, None, None, None, None))
>>> print cursor.pp()
DRINKER | PERWEEK | BAR
============================
adam | 1 | lolas
woody | 5 | cheers
sam | 5 | cheers
norm | 3 | cheers
wilt | 2 | joes
norm | 1 | joes
lola | 6 | lolas
norm | 2 | lolas
woody | 1 | lolas
pierre | 0 | frankies
>>>
SQL 文法は grammer.py で記述されています.構文から構文オブジェクト へのバインディングは bundings.py に, 構文オブジェクトとその実行の ためのストラテジは semantics.py に記述されています.構文解釈では多くの 古典的な,あるいはそうではないロジック (正確には cylindric logic) や, 最適化のためのヒューリスティックを用いており,比較的効率が高く,おそらく 正確な SQL の実装を行っています.勇敢な人は semantics.py を読んで, 12 年にわたるデータベース,論理学,そしてプログラム言語研究がなした 貢献を垣間見ることをおすすめします.根底にある (別のフレームワークでの) ロジックは,
Watters, "Interpreting a Reconstructed Relational Calculus", ACM SIGMOD Proceedings, 1993, Washington DC, pp. 367-376. で与えられて います.
実装に用いられた基本的なデータ構造の大部分はは kjbuckets0.py , またはより高速な kjbucketsmodule.c で与えられています.これらは同じデ ータ型シグネチャをそれぞれ Python と Python の C 拡張で実装しています.
database.py モジュールは gadfly システムに標準 DBAPI インタフェースを 提供するためのシンプルなラッパ (wrapper) です.
test/test_gadfly.py テストスイートは回帰テストと gadfly システムの デモンストレーションを提供するための試みです.
また,SQL パーザはさらに多くの Python モジュールからなる kwParsing パーザジェネレータパッケージを必要とします.
バグを発見したら,http://gadfly.sf.net/ で 我々にレポートしてください.
クエリエンジンは kjbuckets ビルトインモジュール (C モジュール) をイ ンストールすればより高速に動作します. kjbuckets をインストールして いない場合,「 python イミテーション」の kjbuckets0.py を使います. あるテストでは, kjbucets でテストスイートは 2 倍高速に動作しました. おそらくより多くのデータセットを扱う場合にはより多くの利益を得られると 予想されます.