[ruby-dev:38464] Re: SQLiteライブラリ
From:
Hirotsugu Asari <asari.ruby@...>
Date:
2009-05-15 14:21:35 UTC
List:
ruby-dev #38464
浅里と申します。
JRubyを使う身分としてはFFI経由でやってくれると有り難いです。
「標準」という訳には行かなくなっちゃいますけど。
--
Hirotsugu Asari
On May 15, 2009, at 9:12 AM, NARUSE, Yui wrote:
> 成瀬です。
>
> Ruby 1.9でSQLiteライブラリを標準添付しませんか?
>
> 旧来からsqlite3-rubyという物は存在していましたが、
> http://rubyforge.org/projects/sqlite-ruby
> http://sqlite-ruby.rubyforge.org/sqlite3/
>
> * nativeでなくdl経由
> * 作者がメンテナンス終了を宣言
> http://weblog.jamisbuck.org/2009/2/25/net-ssh-capistrano-and-saying-goodbye
> という致命的な物を含む問題があります。
>
>
> これに対して今回新しく起こしたライブラリでは、
> * nativeかつコンパクトな実装
> * それなりにやる気のあるメンテナ
> という利点があります。
>
> 使う際には以下のような感じになります。
> 多段transactionやbackupが特徴でしょうか。
>
> SQLite.open(":memory:") do |db|
> db.transaction do
> db.exec("CREATE TABLE foo (int INTEGER, text TEXT)")
> 10.times do |i|
> db.exec("INSERT INTO foo VALUES (#{i}, 'foo')")
> end
>
> p db.exec("SELECT * FROM foo")[0][9999]
> p db.exec("SELECT * FROM foo")[0].colnames
>
> db.transaction do
> 10.times do |i|
> db.exec("INSERT INTO foo VALUES (#{i}, 'foo')")
> end
> db.rollback
> end
>
> SQLite.open(":memory:") do |db2|
> db.backup(db2)
> end
> end
> end
>
> ご意見等あればよろしくお願いします。
>
> --
> NARUSE, Yui <naruse@airemix.jp>
> diff --git a/ext/sqlite/extconf.rb b/ext/sqlite/extconf.rb
> new file mode 100644
> index 0000000..e042531
> --- /dev/null
> +++ b/ext/sqlite/extconf.rb
> @@ -0,0 +1,5 @@
> +require 'mkmf'
> +
> +dir_config('sqlite')
> +have_library('sqlite3')
> +create_makefile('sqlite')
> diff --git a/ext/sqlite/sqlite.c b/ext/sqlite/sqlite.c
> new file mode 100644
> index 0000000..d4d5644
> --- /dev/null
> +++ b/ext/sqlite/sqlite.c
> @@ -0,0 +1,421 @@
> +/************************************************
> + *
> + * sqlite.c -
> + *
> + ************************************************/
> +
> +#include "ruby/ruby.h"
> +#include "ruby/encoding.h"
> +#include "sqlite3.h"
> +
> +static VALUE rb_cSQLite;
> +
> +struct rb_sqlite {
> + sqlite3 *db;
> + VALUE transactions;
> + int success;
> +};
> +
> +static void
> +rb_sqlite_closed()
> +{
> + rb_raise(rb_eArgError, "closed SQLite Database");
> +}
> +
> +static struct rb_sqlite *
> +rb_sqlite_struct(VALUE obj)
> +{
> + struct rb_sqlite *sdb;
> + Data_Get_Struct(obj, struct rb_sqlite, sdb);
> + if (sdb == 0) rb_sqlite_closed();
> + if (sdb->db == 0) rb_sqlite_closed();
> + return sdb;
> +}
> +
> +static void
> +rb_sqlite_mark(void *p)
> +{
> + struct rb_sqlite *sdb = p;
> + if (sdb) {
> + rb_gc_mark(sdb->transactions);
> + }
> +}
> +
> +static int
> +rb_sqlite_p(VALUE obj)
> +{
> + struct rb_sqlite *ptr;
> + if (TYPE(time) == T_DATA && RDATA(obj)->dmark !=
> rb_sqlite_mark) {
> + return Qfalse;
> + }
> + return Qtrue;
> +}
> +
> +#define RSQLITE(obj) rb_sqlite_struct(obj)
> +#define RSQLITE_DB(obj) (RSQLITE(obj)->db)
> +#define RSQLITE_P(obj) rb_sqlite_p(obj)
> +#define rb_sqlite_transactions(obj) (RSQLITE(obj)->transactions)
> +
> +static void
> +rb_sqlite_free(struct rb_sqlite *sdb)
> +{
> + if (sdb->db) sqlite3_close(sdb->db);
> + xfree(sdb);
> +}
> +
> +static VALUE
> +rb_sqlite_alloc(VALUE klass)
> +{
> + struct rb_sqlite *sdb;
> + VALUE obj = Data_Make_Struct(klass, struct rb_sqlite,
> + rb_sqlite_mark, rb_sqlite_free, sdb);
> + sdb->db = NULL;
> + sdb->transactions = rb_ary_new();
> + sdb->success = Qfalse;
> + return obj;
> +}
> +
> +static int
> +rb_sqlite_sleep_callback(void *arg, int count)
> +{
> + if (count > 4) return 0;
> + rb_thread_sleep(250);
> + return 1;
> +}
> +
> +static VALUE
> +rb_sqlite_initialize(VALUE obj, VALUE path)
> +{
> + sqlite3 *db;
> + struct rb_sqlite *sdb;
> + int rc;
> +
> + FilePathValue(path);
> +
> + rc = sqlite3_open(RSTRING_PTR(path), &db);
> + if (rc != SQLITE_OK) {
> + VALUE msg = rb_str_new_cstr(sqlite3_errmsg(db));
> + sqlite3_close(db);
> + rb_raise(rb_eArgError, "Can't open database: %s (%s)",
> + RSTRING_PTR(path), RSTRING_PTR(msg));
> + }
> + rc = sqlite3_busy_handler(db, rb_sqlite_sleep_callback, NULL);
> + Data_Get_Struct(obj, struct rb_sqlite, sdb);
> + sdb->db = db;
> + return obj;
> +}
> +
> +/*
> + * call-seq: SQLite#close
> + *
> + * close database.
> + */
> +static VALUE
> +rb_sqlite_close(VALUE obj)
> +{
> + struct rb_sqlite *sdb = RSQLITE(obj);
> + sqlite3_close(sdb->db);
> + sdb->db = 0;
> +
> + return Qnil;
> +}
> +
> +/*
> + * call-seq: SQLite.open(filename)
> + *
> + * open SQLite database.
> + */
> +static VALUE
> +rb_sqlite_s_open(VALUE klass, VALUE path)
> +{
> + VALUE obj = rb_sqlite_alloc(klass);
> +
> + if (NIL_P(rb_sqlite_initialize(obj, path))) {
> + return Qnil;
> + }
> +
> + if (rb_block_given_p()) {
> + return rb_ensure(rb_yield, obj, rb_sqlite_close, obj);
> + }
> +
> + return obj;
> +}
> +
> +static VALUE
> +rb_sqlite_cols_colnames(VALUE self)
> +{
> + return rb_ivar_get(self, rb_intern("colnames"));
> +}
> +
> +static VALUE
> +rb_sqlite_cols_aref(int argc, VALUE *argv, VALUE self)
> +{
> + if (argc == 1 && TYPE(argv[0]) == T_STRING) {
> + VALUE colname = argv[0];
> + int i;
> + VALUE colnames = rb_sqlite_cols_colnames(self);
> + for (i = 0; i < RARRAY_LEN(colnames); i++) {
> + if (rb_str_equal(colname, rb_ary_entry(colnames, i))) {
> + return rb_ary_entry(self, i);
> + }
> + }
> + }
> + else {
> + return rb_ary_aref(argc, argv, self);
> + }
> +}
> +
> +static void
> +rb_sqlite_work_init(VALUE work, sqlite3_stmt *stmt)
> +{
> + VALUE colnames, proto_cols;
> + int i, count = sqlite3_column_count(stmt);
> +
> + colnames = rb_ary_new2(count);
> + for (i = 0; i < count; i++) {
> + rb_ary_push(colnames,
> rb_str_new_cstr(sqlite3_column_name(stmt, i)));
> + }
> +
> + proto_cols = rb_ary_new2(count);
> + rb_ivar_set(proto_cols, rb_intern("colnames"), colnames);
> + rb_define_singleton_method(proto_cols, "[]",
> rb_sqlite_cols_aref, -1);
> + rb_define_singleton_method(proto_cols, "colnames",
> rb_sqlite_cols_colnames, 0);
> + rb_ary_push(work, proto_cols);
> +}
> +
> +#define rb_sqlite_work_proto_cols(work) rb_ary_entry(work, 0)
> +
> +static VALUE
> +rb_sqlite_work_new(sqlite3_stmt *stmt)
> +{
> + VALUE work = rb_ary_new2(1); /* [proto_cols] */
> + rb_sqlite_work_init(work, stmt);
> + return work;
> +}
> +
> +static VALUE
> +rb_sqlite_work_cols_new(VALUE work, sqlite3_stmt *stmt)
> +{
> + VALUE proto_cols, cols;
> + int i, count = sqlite3_column_count(stmt);
> +
> + proto_cols = rb_sqlite_work_proto_cols(work);
> + cols = rb_obj_clone(proto_cols);
> + for (i = 0; i < count; i++) {
> + VALUE value;
> + int type = sqlite3_column_type(stmt, i);
> + switch (type) {
> + case SQLITE_INTEGER:
> +#ifdef HAVE_LONG_LONG
> + {
> + sqlite3_int64 intval = sqlite3_column_int64(stmt, i);
> + value = LL2NUM((LONG_LONG)intval);
> + }
> +#else
> + value = sqlite3_column_int(stmt, i);
> +#endif
> + break;
> + case SQLITE_FLOAT:
> + {
> + double dblval = sqlite3_column_double(stmt, i);
> + value = DBL2NUM(dblval);
> + }
> + break;
> + case SQLITE_TEXT:
> + case SQLITE_BLOB:
> + {
> + int len = sqlite3_column_bytes(stmt, i);
> + const unsigned char *text =
> sqlite3_column_text(stmt, i);
> + rb_encoding *enc =
> + type == SQLITE_TEXT ? rb_utf8_encoding() :
> rb_ascii8bit_encoding();
> + value = rb_str_new_cstr(text);
> + }
> + break;
> + case SQLITE_NULL:
> + value = Qnil;
> + break;
> + }
> + rb_ary_push(cols, value);
> + }
> +
> + return cols;
> +}
> +
> +static VALUE
> +rb_sqlite_exec(VALUE obj, VALUE sql)
> +{
> + VALUE work = Qnil, result = Qnil;
> + sqlite3_stmt *stmt;
> + int i, rc;
> +
> + rc = sqlite3_prepare_v2(RSQLITE_DB(obj),
> + RSTRING_PTR(sql), RSTRING_LEN(sql), &stmt, NULL);
> + if (rc != SQLITE_OK) {
> + rb_raise(rb_eArgError, "SQL error: %d %s", rc,
> sqlite3_errmsg(RSQLITE_DB(obj)));
> + }
> +
> + for (;;) {
> + rc = sqlite3_step(stmt);
> + switch (rc) {
> + case SQLITE_ROW:
> + {
> + VALUE cols;
> + if (NIL_P(work)) {
> + work = rb_sqlite_work_new(stmt);
> + if (!rb_block_given_p()) {
> + result =
> rb_ary_new2(sqlite3_data_count(stmt));
> + }
> + }
> + cols = rb_sqlite_work_cols_new(work, stmt);
> + if (rb_block_given_p()) {
> + rb_yield(cols);
> + }
> + else {
> + rb_ary_push(result, cols);
> + }
> + }
> + continue;
> + case SQLITE_DONE:
> + break;
> + case SQLITE_BUSY:
> + /* after process busy handler */
> + default:
> + sqlite3_finalize(stmt);
> + rb_raise(rb_eArgError, "SQL error: %d %s", rc,
> sqlite3_errmsg(RSQLITE_DB(obj)));
> + }
> + break;
> + }
> +
> + sqlite3_finalize(stmt);
> + return result;
> +}
> +
> +static VALUE
> +rb_sqlite_release(VALUE self)
> +{
> + int rc;
> + char *errmsg;
> + VALUE transactions, sql, savepoint;
> +
> + transactions = rb_sqlite_transactions(self);
> + savepoint = rb_ary_entry(transactions, -1);
> + if (NIL_P(savepoint)) return Qnil;
> +
> + sql = rb_str_append(rb_str_new_cstr("RELEASE
> RSQLITE_SAVEPOINT_"), savepoint);
> + rc = sqlite3_exec(RSQLITE_DB(self), RSTRING_PTR(sql), NULL,
> NULL, &errmsg);
> + if (rc != SQLITE_OK) {
> + VALUE msg = rb_str_new_cstr(errmsg);
> + sqlite3_free(errmsg);
> + rb_raise(rb_eRuntimeError, "Can't release (%s)",
> RSTRING_PTR(msg));
> + }
> + rb_ary_pop(transactions);
> + RSQLITE(self)->success = Qfalse;
> + return Qnil;
> +}
> +
> +static VALUE
> +rb_sqlite_rollback(VALUE self)
> +{
> + int rc;
> + char *errmsg;
> + VALUE transactions, sql, savepoint;
> +
> + transactions = rb_sqlite_transactions(self);
> + savepoint = rb_ary_entry(transactions, -1);
> + if (NIL_P(savepoint)) return Qnil;
> +
> + sql = rb_str_append(rb_str_new_cstr("ROLLBACK TO
> RSQLITE_SAVEPOINT_"), savepoint);
> + rc = sqlite3_exec(RSQLITE_DB(self), RSTRING_PTR(sql), NULL,
> NULL, &errmsg);
> + if (rc != SQLITE_OK) {
> + VALUE msg = rb_str_new_cstr(errmsg);
> + sqlite3_free(errmsg);
> + rb_raise(rb_eRuntimeError, "Can't rollback (%s)",
> RSTRING_PTR(msg));
> + }
> + return Qnil;
> +}
> +
> +static VALUE
> +rb_sqlite_transaction_end(VALUE self)
> +{
> + if (RSQLITE(self)->success == Qfalse) {
> + rb_sqlite_rollback(self);
> + }
> + return rb_sqlite_release(self);
> +}
> +
> +static VALUE
> +rb_sqlite_transaction_i(VALUE self)
> +{
> + VALUE res = rb_yield(self);
> + RSQLITE(self)->success = Qtrue;
> + return res;
> +}
> +
> +static VALUE
> +rb_sqlite_transaction(VALUE self)
> +{
> + VALUE result, transactions, sql, savepoint;
> + int rc;
> + char *errmsg;
> +
> + RETURN_ENUMERATOR(self, 0, 0);
> +
> + transactions = rb_sqlite_transactions(self);
> + savepoint = rb_sprintf("%d", RARRAY_LEN(transactions));
> +
> + sql = rb_str_append(rb_str_new_cstr("SAVEPOINT
> RSQLITE_SAVEPOINT_"), savepoint);
> + rc = sqlite3_exec(RSQLITE_DB(self), RSTRING_PTR(sql), NULL,
> NULL, &errmsg);
> +
> + if (rc != SQLITE_OK) {
> + VALUE msg = rb_str_new_cstr(errmsg);
> + sqlite3_free(errmsg);
> + rb_raise(rb_eRuntimeError, "Can't begin transaction (%s)",
> RSTRING_PTR(msg));
> + }
> +
> + rb_ary_push(transactions, savepoint);
> + RSQLITE(self)->success = Qfalse;
> +
> + return rb_ensure(rb_sqlite_transaction_i, self,
> rb_sqlite_transaction_end, self);
> +}
> +
> +/*
> + * call-seq: SQLite#backup(database)
> + *
> + */
> +static VALUE
> +rb_sqlite_backup(VALUE self, VALUE dst)
> +{
> + sqlite3 *db, *dstdb;
> + sqlite3_backup *backup;
> + int rc = 0;
> +
> + db = RSQLITE_DB(self);
> + dstdb = RSQLITE_DB(dst);
> +
> + backup = sqlite3_backup_init(dstdb, "main", db, "main");
> + if (backup) {
> + sqlite3_backup_step(backup, -1);
> + sqlite3_backup_finish(backup);
> + }
> + rc = sqlite3_errcode(dstdb);
> + if (rc != SQLITE_OK) {
> + rb_raise(rb_eArgError, "Can't backup: %s",
> sqlite3_errmsg(dstdb));
> + }
> +
> + return Qnil;
> +}
> +
> +void
> +Init_sqlite()
> +{
> + rb_cSQLite = rb_define_class("SQLite", rb_cObject);
> + rb_define_alloc_func(rb_cSQLite, rb_sqlite_alloc);
> + rb_define_singleton_method(rb_cSQLite, "open",
> rb_sqlite_s_open, 1);
> + rb_define_method(rb_cSQLite, "initialize",
> rb_sqlite_initialize, 1);
> + rb_define_method(rb_cSQLite, "close", rb_sqlite_close, 0);
> + rb_define_method(rb_cSQLite, "exec", rb_sqlite_exec, 1);
> + rb_define_method(rb_cSQLite, "rollback", rb_sqlite_rollback, 0);
> + rb_define_method(rb_cSQLite, "transaction",
> rb_sqlite_transaction, 0);
> + rb_define_method(rb_cSQLite, "backup", rb_sqlite_backup, 1);
> + rb_define_const(rb_cSQLite, "VERSION",
> rb_str_new_cstr(SQLITE_VERSION));
> +}
> diff --git a/test/sqlite/test_sqlite.rb b/test/sqlite/test_sqlite.rb
> new file mode 100644
> index 0000000..06a5973
> --- /dev/null
> +++ b/test/sqlite/test_sqlite.rb
> @@ -0,0 +1,86 @@
> +require 'test/unit'
> +
> +begin
> + require 'sqlite'
> +rescue LoadError
> +end
> +
> +class TestSQLite < Test::Unit::TestCase
> + def test_sqlite
> + assert_kind_of String, SQLite::VERSION
> + SQLite.open(":memory:") do |db|
> + assert_nil db.exec("CREATE TABLE foo (int INTEGER, text TEXT)")
> + 10.times do |i|
> + assert_nil db.exec("INSERT INTO foo VALUES (#{i}, 'foo')")
> + end
> +
> + assert_nil db.exec("SELECT * FROM foo")[0][9999]
> +
> + p db.exec("SELECT * FROM foo")[0].colnames
> + assert_equal %w/int text/, db.exec("SELECT * FROM foo")
> [0].colnames
> +
> + pp rows = db.exec("SELECT * FROM foo")
> + db.exec("SELECT * FROM foo") do |cols|
> + assert_kind_of Array, cols
> + end
> +
> + db.transaction do
> + 4294967295.upto(4294967297) do |i|
> + assert_nil db.exec("INSERT INTO foo VALUES (#{i}, 'foo')")
> + end
> +
> + i = (1 << 63) - 1
> + assert_nil db.exec("INSERT INTO foo VALUES (#{i},
> '63bit-1')")
> + value = db.exec("SELECT int from foo where text='63bit-1'")
> [0][0]
> + assert_kind_of Integer, value
> + assert_equal i, value
> +
> + i = (1 << 63)
> + assert_nil db.exec("INSERT INTO foo VALUES (#{i}, '63bit')")
> + value = db.exec("SELECT int from foo where text='63bit'")[0]
> [0]
> + assert_kind_of Float, value
> + assert_equal i, value
> + db.rollback
> + end
> + assert_equal rows, db.exec("SELECT * FROM foo")
> +
> + db.transaction do
> + assert_nil db.exec("INSERT INTO foo VALUES (11, '11')")
> + end
> + assert_not_equal rows, db.exec("SELECT * FROM foo")
> +
> + rows = db.exec("SELECT * FROM foo")
> + rows1 = nil
> + db.transaction do
> + assert_nil db.exec("INSERT INTO foo VALUES (12, '12')")
> + rows1 = db.exec("SELECT * FROM foo")
> + assert_not_equal rows, db.exec("SELECT * FROM foo")
> + db.transaction do
> + assert_nil db.exec("INSERT INTO foo VALUES (13, '13')")
> + assert_not_equal rows1, db.exec("SELECT * FROM foo")
> + db.rollback
> + db.rollback
> + end
> + assert_equal rows1, db.exec("SELECT * FROM foo")
> + end
> + assert_not_equal rows, db.exec("SELECT * FROM foo")
> + assert_equal rows1, db.exec("SELECT * FROM foo")
> + end
> + end
> +
> + def test_backup
> + SQLite.open(":memory:") do |db1|
> + SQLite.open(":memory:") do |db2|
> + db1.transaction do
> + assert_nil db1.exec("CREATE TABLE foo (int INTEGER, text
> TEXT)")
> + 10.times do |i|
> + assert_nil db1.exec("INSERT INTO foo VALUES (#{i},
> 'foo')")
> + end
> + end
> + assert_nil db1.backup(db2)
> + rows = db1.exec("SELECT * FROM foo")
> + assert_equal rows, db2.exec("SELECT * FROM foo")
> + end
> + end
> + end
> +end