diff --git a/Manifest.txt b/Manifest.txt index ca812a88..6385cfcd 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -13,6 +13,8 @@ ext/sqlite3/database.h ext/sqlite3/exception.c ext/sqlite3/exception.h ext/sqlite3/extconf.rb +ext/sqlite3/vtable.c +ext/sqlite3/vtable.h ext/sqlite3/sqlite3.c ext/sqlite3/sqlite3_ruby.h ext/sqlite3/statement.c @@ -29,6 +31,7 @@ lib/sqlite3/statement.rb lib/sqlite3/translator.rb lib/sqlite3/value.rb lib/sqlite3/version.rb +lib/sqlite3/vtable.rb setup.rb tasks/faq.rake tasks/gem.rake @@ -38,7 +41,9 @@ test/helper.rb test/test_backup.rb test/test_collation.rb test/test_database.rb +test/test_database_flags.rb test/test_database_readonly.rb +test/test_database_readwrite.rb test/test_deprecated.rb test/test_encoding.rb test/test_integration.rb @@ -50,3 +55,4 @@ test/test_result_set.rb test/test_sqlite3.rb test/test_statement.rb test/test_statement_execute.rb +test/test_vtable.rb diff --git a/ext/sqlite3/database.c b/ext/sqlite3/database.c index dea01dc0..6f7810de 100644 --- a/ext/sqlite3/database.c +++ b/ext/sqlite3/database.c @@ -297,7 +297,7 @@ static VALUE last_insert_row_id(VALUE self) return LL2NUM(sqlite3_last_insert_rowid(ctx->db)); } -static VALUE sqlite3val2rb(sqlite3_value * val) +VALUE sqlite3val2rb(sqlite3_value * val) { switch(sqlite3_value_type(val)) { case SQLITE_INTEGER: @@ -334,7 +334,7 @@ static VALUE sqlite3val2rb(sqlite3_value * val) } } -static void set_sqlite3_func_result(sqlite3_context * ctx, VALUE result) +void set_sqlite3_func_result(sqlite3_context * ctx, VALUE result) { switch(TYPE(result)) { case T_NIL: diff --git a/ext/sqlite3/database.h b/ext/sqlite3/database.h index 63e5e961..eb5f940d 100644 --- a/ext/sqlite3/database.h +++ b/ext/sqlite3/database.h @@ -3,6 +3,12 @@ #include +#define DEBUG_LOG(...) (void)(0) +//#define DEBUG_LOG(...) printf(__VA_ARGS__); fflush(stdout) +// used by vtable.c too +void set_sqlite3_func_result(sqlite3_context * ctx, VALUE result); +VALUE sqlite3val2rb(sqlite3_value * val); + struct _sqlite3Ruby { sqlite3 *db; }; diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c index 1c02011f..5888d426 100644 --- a/ext/sqlite3/sqlite3.c +++ b/ext/sqlite3/sqlite3.c @@ -135,6 +135,7 @@ void Init_sqlite3_native() #ifdef HAVE_SQLITE3_BACKUP_INIT init_sqlite3_backup(); #endif + init_sqlite3_vtable(); rb_define_singleton_method(mSqlite3, "libversion", libversion, 0); rb_define_singleton_method(mSqlite3, "threadsafe", threadsafe_p, 0); diff --git a/ext/sqlite3/sqlite3_ruby.h b/ext/sqlite3/sqlite3_ruby.h index 9e9e1751..35a00640 100644 --- a/ext/sqlite3/sqlite3_ruby.h +++ b/ext/sqlite3/sqlite3_ruby.h @@ -46,6 +46,7 @@ extern VALUE cSqlite3Blob; #include #include #include +#include int bignum_to_int64(VALUE big, sqlite3_int64 *result); diff --git a/ext/sqlite3/statement.c b/ext/sqlite3/statement.c index cf5956c2..a9281aed 100644 --- a/ext/sqlite3/statement.c +++ b/ext/sqlite3/statement.c @@ -118,7 +118,9 @@ static VALUE step(VALUE self) REQUIRE_OPEN_STMT(ctx); - if(ctx->done_p) return Qnil; + if(ctx->done_p) { + return Qnil; + } #ifdef HAVE_RUBY_ENCODING_H { @@ -128,8 +130,8 @@ static VALUE step(VALUE self) } #endif + stmt = ctx->st; - value = sqlite3_step(stmt); length = sqlite3_column_count(stmt); list = rb_ary_new2((long)length); diff --git a/ext/sqlite3/vtable.c b/ext/sqlite3/vtable.c new file mode 100644 index 00000000..d6198a2e --- /dev/null +++ b/ext/sqlite3/vtable.c @@ -0,0 +1,358 @@ +#include +#include + +VALUE cVTable; + +/** structure for ruby virtual table: inherits from sqlite3_vtab */ +typedef struct { + // mandatory sqlite3 fields + const sqlite3_module* pModule; + int nRef; + char *zErrMsg; + // Ruby fields + VALUE vtable; +} ruby_sqlite3_vtab; + +/** structure for ruby cursors: inherits from sqlite3_vtab_cursor */ +typedef struct { + ruby_sqlite3_vtab* pVTab; + VALUE row; + int rowid; +} ruby_sqlite3_vtab_cursor; + +/** + * lookup for a ruby class :: and create an instance of this class + * This instance is then used to bind sqlite vtab callbacks + */ +static int xCreate(sqlite3* db, VALUE *db_ruby, + int argc, char **argv, + ruby_sqlite3_vtab **ppVTab, + char **pzErr) +{ + VALUE sql_stmt, tables; + VALUE module_name, module; + VALUE table_name, table; + const char* module_name_cstr = (const char*)argv[0]; + const char* table_name_cstr = (const char*)argv[2]; + + // method will raise in case of error: no need to use pzErr + *pzErr = NULL; + + // lookup for hash db.vtables + tables = rb_funcall(*db_ruby, rb_intern("vtables"), 0); + if (!RB_TYPE_P(tables, T_HASH)) { + rb_raise(rb_eTypeError, "xCreate: expect db.vtables to be a Hash"); + } + module_name = rb_str_new2(module_name_cstr); + module = rb_hash_aref(tables, module_name); + if (module == Qnil ) { + rb_raise( + rb_eKeyError, + "xCreate: module %s is declared in sqlite3 but cant be found in db.vtables.", + module_name_cstr + ); + } + + table_name = rb_str_new2(table_name_cstr); + table = rb_hash_aref(module, table_name); + if (table == Qnil) { + rb_raise( + rb_eKeyError, + "no such table: %s in module %s", + table_name_cstr, + module_name_cstr + ); + } + if (rb_obj_is_kind_of(table, cVTable) != Qtrue) { + VALUE table_inspect = rb_funcall(table, rb_intern("inspect"), 0); + rb_raise( + rb_eTypeError, + "Object %s must inherit from VTable", + StringValuePtr(table_inspect) + ); + } + + // alloc a new ruby_sqlite3_vtab object + // and store related attributes + (*ppVTab) = (ruby_sqlite3_vtab*)malloc(sizeof(ruby_sqlite3_vtab)); + (*ppVTab)->vtable = table; + + // get the create statement + sql_stmt = rb_funcall((*ppVTab)->vtable, rb_intern("create_statement"), 0); + +#ifdef HAVE_RUBY_ENCODING_H + if(!UTF8_P(sql_stmt)) { + sql_stmt = rb_str_export_to_enc(sql_stmt, rb_utf8_encoding()); + } +#endif + if ( sqlite3_declare_vtab(db, StringValuePtr(sql_stmt)) ) { + rb_raise(rb_path2class("SQLite3::Exception"), "fail to declare virtual table with \"%s\": %s", StringValuePtr(sql_stmt), sqlite3_errmsg(db)); + } + + return SQLITE_OK; +} + +static int xConnect(sqlite3* db, void *pAux, + int argc, char **argv, + ruby_sqlite3_vtab **ppVTab, + char **pzErr) +{ + return xCreate(db, pAux, argc, argv, ppVTab, pzErr); +} + +static VALUE constraint_op_as_symbol(unsigned char op) +{ + ID op_id; + switch(op) { + case SQLITE_INDEX_CONSTRAINT_EQ: + op_id = rb_intern("=="); + break; + case SQLITE_INDEX_CONSTRAINT_GT: + op_id = rb_intern(">"); + break; + case SQLITE_INDEX_CONSTRAINT_LE: + op_id = rb_intern("<="); + break; + case SQLITE_INDEX_CONSTRAINT_LT: + op_id = rb_intern("<"); + break; + case SQLITE_INDEX_CONSTRAINT_GE: + op_id = rb_intern(">="); + break; + case SQLITE_INDEX_CONSTRAINT_MATCH: + op_id = rb_intern("match"); + break; +#if SQLITE_VERSION_NUMBER>=3010000 + case SQLITE_INDEX_CONSTRAINT_LIKE: + op_id = rb_intern("like"); + break; + case SQLITE_INDEX_CONSTRAINT_GLOB: + op_id = rb_intern("glob"); + break; + case SQLITE_INDEX_CONSTRAINT_REGEXP: + op_id = rb_intern("regexp"); + break; +#endif +#if SQLITE_VERSION_NUMBER>=3009000 + case SQLITE_INDEX_SCAN_UNIQUE: + op_id = rb_intern("unique"); + break; +#endif + default: + op_id = rb_intern("unsupported"); + } + return ID2SYM(op_id); +} + +static VALUE constraint_to_ruby(const struct sqlite3_index_constraint* c) +{ + VALUE cons = rb_ary_new2(2); + rb_ary_store(cons, 0, LONG2FIX(c->iColumn)); + rb_ary_store(cons, 1, constraint_op_as_symbol(c->op)); + return cons; +} + +static VALUE order_by_to_ruby(const struct sqlite3_index_orderby* c) +{ + VALUE order_by = rb_ary_new2(2); + rb_ary_store(order_by, 0, LONG2FIX(c->iColumn)); + rb_ary_store(order_by, 1, LONG2FIX(1-2*c->desc)); + return order_by; +} + +static int xBestIndex(ruby_sqlite3_vtab *pVTab, sqlite3_index_info* info) +{ + int i; + VALUE constraint = rb_ary_new(); + VALUE order_by = rb_ary_new2(info->nOrderBy); + VALUE ret, idx_num, estimated_cost, order_by_consumed, omit_all; +#if SQLITE_VERSION_NUMBER >= 3008002 + VALUE estimated_rows; +#endif +#if SQLITE_VERSION_NUMBER >= 3009000 + VALUE idx_flags; +#endif +#if SQLITE_VERSION_NUMBER >= 3010000 + VALUE col_used; +#endif + + // convert constraints to ruby + for (i = 0; i < info->nConstraint; ++i) { + if (info->aConstraint[i].usable) { + rb_ary_push(constraint, constraint_to_ruby(info->aConstraint + i)); + } + } + + // convert order_by to ruby + for (i = 0; i < info->nOrderBy; ++i) { + rb_ary_store(order_by, i, order_by_to_ruby(info->aOrderBy + i)); + } + + ret = rb_funcall( pVTab->vtable, rb_intern("best_index"), 2, constraint, order_by ); + if (ret != Qnil ) { + if (!RB_TYPE_P(ret, T_HASH)) { + rb_raise(rb_eTypeError, "best_index: expect returned value to be a Hash"); + } + idx_num = rb_hash_aref(ret, ID2SYM(rb_intern("idxNum"))); + if (idx_num == Qnil ) { + rb_raise(rb_eKeyError, "best_index: mandatory key 'idxNum' not found"); + } + info->idxNum = FIX2INT(idx_num); + estimated_cost = rb_hash_aref(ret, ID2SYM(rb_intern("estimatedCost"))); + if (estimated_cost != Qnil) { info->estimatedCost = NUM2DBL(estimated_cost); } + order_by_consumed = rb_hash_aref(ret, ID2SYM(rb_intern("orderByConsumed"))); + info->orderByConsumed = RTEST(order_by_consumed); +#if SQLITE_VERSION_NUMBER >= 3008002 + estimated_rows = rb_hash_aref(ret, ID2SYM(rb_intern("estimatedRows"))); + if (estimated_rows != Qnil) { bignum_to_int64(estimated_rows, &info->estimatedRows); } +#endif +#if SQLITE_VERSION_NUMBER >= 3009000 + idx_flags = rb_hash_aref(ret, ID2SYM(rb_intern("idxFlags"))); + if (idx_flags != Qnil) { info->idxFlags = FIX2INT(idx_flags); } +#endif +#if SQLITE_VERSION_NUMBER >= 3010000 + col_used = rb_hash_aref(ret, ID2SYM(rb_intern("colUsed"))); + if (col_used != Qnil) { bignum_to_int64(col_used, &info->colUsed); } +#endif + + // make sure that expression are given to filter + omit_all = rb_hash_aref(ret, ID2SYM(rb_intern("omitAllConstraint"))); + for (i = 0; i < info->nConstraint; ++i) { + if (RTEST(omit_all)) { + info->aConstraintUsage[i].omit = 1; + } + if (info->aConstraint[i].usable) { + info->aConstraintUsage[i].argvIndex = (i+1); + } + } + } + + return SQLITE_OK; +} + +static int xDestroy(ruby_sqlite3_vtab *pVTab) +{ + free(pVTab); + return SQLITE_OK; +} + +static int xDisconnect(ruby_sqlite3_vtab *pVTab) +{ + return xDestroy(pVTab); +} + +static int xOpen(ruby_sqlite3_vtab *pVTab, ruby_sqlite3_vtab_cursor **ppCursor) +{ + rb_funcall( pVTab->vtable, rb_intern("open"), 0 ); + *ppCursor = (ruby_sqlite3_vtab_cursor*)malloc(sizeof(ruby_sqlite3_vtab_cursor)); + (*ppCursor)->pVTab = pVTab; + (*ppCursor)->rowid = 0; + return SQLITE_OK; +} + +static int xClose(ruby_sqlite3_vtab_cursor* cursor) +{ + rb_funcall( cursor->pVTab->vtable, rb_intern("close"), 0 ); + free(cursor); + return SQLITE_OK; +} + +static int xNext(ruby_sqlite3_vtab_cursor* cursor) +{ + cursor->row = rb_funcall(cursor->pVTab->vtable, rb_intern("next"), 0); + ++(cursor->rowid); + return SQLITE_OK; +} + +static int xFilter(ruby_sqlite3_vtab_cursor* cursor, int idxNum, const char *idxStr, + int argc, sqlite3_value **argv) +{ + int i; + VALUE argv_ruby = rb_ary_new2(argc); + for (i = 0; i < argc; ++i) { + rb_ary_store(argv_ruby, i, sqlite3val2rb(argv[i])); + } + rb_funcall( cursor->pVTab->vtable, rb_intern("filter"), 2, LONG2FIX(idxNum), argv_ruby ); + cursor->rowid = 0; + return xNext(cursor); +} + +static int xEof(ruby_sqlite3_vtab_cursor* cursor) +{ + return (cursor->row == Qnil); +} + +static int xColumn(ruby_sqlite3_vtab_cursor* cursor, sqlite3_context* context, int i) +{ + VALUE val = rb_ary_entry(cursor->row, i); + set_sqlite3_func_result(context, val); + return SQLITE_OK; +} + +static int xRowid(ruby_sqlite3_vtab_cursor* cursor, sqlite_int64 *pRowid) +{ + *pRowid = cursor->rowid; + return SQLITE_OK; +} + +static sqlite3_module ruby_proxy_module = +{ + 0, /* iVersion */ + xCreate, /* xCreate - create a vtable */ + xConnect, /* xConnect - associate a vtable with a connection */ + xBestIndex, /* xBestIndex - best index */ + xDisconnect, /* xDisconnect - disassociate a vtable with a connection */ + xDestroy, /* xDestroy - destroy a vtable */ + xOpen, /* xOpen - open a cursor */ + xClose, /* xClose - close a cursor */ + xFilter, /* xFilter - configure scan constraints */ + xNext, /* xNext - advance a cursor */ + xEof, /* xEof - indicate end of result set*/ + xColumn, /* xColumn - read data */ + xRowid, /* xRowid - read data */ + NULL, /* xUpdate - write data */ + NULL, /* xBegin - begin transaction */ + NULL, /* xSync - sync transaction */ + NULL, /* xCommit - commit transaction */ + NULL, /* xRollback - rollback transaction */ + NULL, /* xFindFunction - function overloading */ +}; + +static VALUE create_module(VALUE self, VALUE db, VALUE name) +{ + VALUE *db_ruby; + sqlite3RubyPtr db_ctx; + StringValue(name); + Data_Get_Struct(db, sqlite3Ruby, db_ctx); + + if(!db_ctx->db) { + rb_raise(rb_eArgError, "create_module on a closed database"); + } + +#ifdef HAVE_RUBY_ENCODING_H + if(!UTF8_P(name)) { + name = rb_str_export_to_enc(name, rb_utf8_encoding()); + } +#endif + + db_ruby = xcalloc(1, sizeof(VALUE)); + *db_ruby = db; + if (sqlite3_create_module_v2( + db_ctx->db, + StringValuePtr(name), + &ruby_proxy_module, + db_ruby, + xfree + ) != SQLITE_OK) { + rb_raise(rb_path2class("SQLite3::Exception"), sqlite3_errmsg(db_ctx->db)); + } + + return Qnil; +} + +void init_sqlite3_vtable() +{ + cVTable = rb_define_class_under(mSqlite3, "VTable", rb_cObject); + rb_define_singleton_method(cVTable, "create_module", create_module, 2); +} + diff --git a/ext/sqlite3/vtable.h b/ext/sqlite3/vtable.h new file mode 100644 index 00000000..a6efbcec --- /dev/null +++ b/ext/sqlite3/vtable.h @@ -0,0 +1,8 @@ +#ifndef SQLITE3_MODULE_RUBY +#define SQLITE3_MODULE_RUBY + +#include + +void init_sqlite3_vtable(); + +#endif diff --git a/lib/sqlite3/database.rb b/lib/sqlite3/database.rb index 6c02bb15..dd33e945 100644 --- a/lib/sqlite3/database.rb +++ b/lib/sqlite3/database.rb @@ -34,6 +34,7 @@ module SQLite3 # hashes, then the results will all be indexible by field name. class Database attr_reader :collations + attr_accessor :vtables include Pragmas @@ -393,7 +394,7 @@ def self.finalize( &block ) end proxy = factory.new - proxy.extend(Module.new { + proxy.extend(::Module.new { attr_accessor :ctx def step( *args ) diff --git a/lib/sqlite3/version.rb b/lib/sqlite3/version.rb index 1c193d4b..da66d243 100644 --- a/lib/sqlite3/version.rb +++ b/lib/sqlite3/version.rb @@ -1,6 +1,6 @@ module SQLite3 - VERSION = '1.3.11' + VERSION = '1.3.11.vtable' module VersionProxy diff --git a/lib/sqlite3/vtable.rb b/lib/sqlite3/vtable.rb new file mode 100644 index 00000000..1a74d602 --- /dev/null +++ b/lib/sqlite3/vtable.rb @@ -0,0 +1,82 @@ +module SQLite3_VTables + # this module contains the vtable classes generated + # using SQLite3::vtable method +end + +module SQLite3 + class VTable + def register(db, module_name, table_name) + tables = (db.vtables ||= {}) + m = tables[module_name] + raise "VTable #{table_name} for module #{module_name} is already registered" if m && m[table_name] + unless m + self.class.create_module(db, module_name) + m = tables[module_name] = {} + end + m[table_name] = self + end + def initialize(db, module_name, table_name = nil) + register(db, module_name, table_name || self.class.name.split('::').last) + end + #this method is needed to declare the type of each column to sqlite + def create_statement + fail 'VTable#create_statement not implemented' + end + + #called before each statement + def open + # do nothing by default + end + + #called before each statement + def close + # do nothing by default + end + + #called to define the best suitable index + def best_index(constraint, order_by) + # one can return an evaluation of the index as shown below + # { idxNum: 1, estimatedCost: 10.0, orderByConsumed: true } + # see sqlite documentation for more details + end + + # may be called several times between open/close + # it initialize/reset cursor + def filter(idxNum, args) + fail 'VTable#filter not implemented' + end + + # called to retrieve a new row + def next + fail 'VTable#next not implemented' + end + end + + class VTableFromEnumerable < VTable + DEFAULT_MODULE = 'DEFAULT_MODULE' + def initialize(db, table_name, table_columns, enumerable) + super(db, DEFAULT_MODULE, table_name) + @table_name = table_name + @table_columns = table_columns + @enumerable = enumerable + db.execute("create virtual table #{table_name} using #{DEFAULT_MODULE}") + end + + def filter(idxNum, args) + @enum = @enumerable.to_enum + end + + def create_statement + "create table #{@table_name}(#{@table_columns})" + end + + def next + @enum.next + rescue StopIteration + nil + end + end + def self.vtable(db, table_name, table_columns, enumerable) + VTableFromEnumerable.new(db, table_name, table_columns, enumerable) + end +end diff --git a/test/helper.rb b/test/helper.rb index efa4a39d..b50a94f4 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -1,5 +1,6 @@ require 'sqlite3' require 'minitest/autorun' +require 'pathname' unless RUBY_VERSION >= "1.9" require 'iconv' @@ -11,6 +12,11 @@ class TestCase < Minitest::Test alias :assert_not_nil :refute_nil alias :assert_raise :assert_raises + + def assert_path_equal(p1, p2) + assert_equal( Pathname.new(p1).realpath, Pathname.new(p2).realpath ) + end + def assert_nothing_raised yield end diff --git a/test/test_database.rb b/test/test_database.rb index c50e05d3..539bad31 100644 --- a/test/test_database.rb +++ b/test/test_database.rb @@ -19,7 +19,7 @@ def test_db_filename assert_equal '', @db.filename('main') tf = Tempfile.new 'thing' @db = SQLite3::Database.new tf.path - assert_equal tf.path, @db.filename('main') + assert_path_equal tf.path, @db.filename('main') ensure tf.unlink if tf end @@ -29,7 +29,7 @@ def test_filename assert_equal '', @db.filename tf = Tempfile.new 'thing' @db = SQLite3::Database.new tf.path - assert_equal tf.path, @db.filename + assert_path_equal tf.path, @db.filename ensure tf.unlink if tf end @@ -39,7 +39,7 @@ def test_filename_with_attachment assert_equal '', @db.filename tf = Tempfile.new 'thing' @db.execute "ATTACH DATABASE '#{tf.path}' AS 'testing'" - assert_equal tf.path, @db.filename('testing') + assert_path_equal tf.path, @db.filename('testing') ensure tf.unlink if tf end diff --git a/test/test_vtable.rb b/test/test_vtable.rb new file mode 100644 index 00000000..76f51e9b --- /dev/null +++ b/test/test_vtable.rb @@ -0,0 +1,182 @@ +require "helper" +require 'sqlite3/vtable' + +class VTableTest < SQLite3::VTable + def initialize(db, module_name) + super(db, module_name) + @str = "A"*1500 + end + + #required method for vtable + #this method is needed to declare the type of each column to sqlite + def create_statement + "create table VTableTest(s text, x integer, y int)" + end + + #required method for vtable + #called before each statement + def open + end + + # this method initialize/reset cursor + def filter(id, args) + @count = 0 + end + + #required method for vtable + #called to retrieve a new row + def next + + #produce up to 100000 lines + @count += 1 + if @count <= 50 + [@str, rand(10), rand] + else + nil + end + + end +end + +module SQLite3 + class TestVTable < SQLite3::TestCase + def setup + @db = SQLite3::Database.new(":memory:") + GC.stress = true + end + + def teardown + GC.stress = false + end + + def test_exception_module + #the following line throws an exception because NonExistingModule has not been created in sqlite + err = assert_raise SQLite3::SQLException do + @db.execute("create virtual table VTableTest using NonExistingModule") + end + assert_includes(err.message, 'no such module: NonExistingModule') + end + + def test_exception_table + #the following line throws an exception because no ruby class NonExistingVTable has been registered + VTableTest.new(@db, 'TestModule') + err = assert_raise KeyError do + @db.execute("create virtual table NonExistingVTable using TestModule") + end + assert_includes(err.message, 'no such table: NonExistingVTable in module TestModule') + end + + def test_exception_bad_create_statement + t = VTableTest.new(@db, 'TestModule2').tap do |vtable| + vtable.define_singleton_method(:create_statement) { + 'create tab with a bad statement' + } + end + # this will fail because create_statement is not valid statement such as "create virtual table t(col1, col2)" + err = assert_raises SQLite3::Exception do + @db.execute('create virtual table VTableTest using TestModule2') + end + assert_includes(err.message, 'fail to declare virtual table') + assert_includes(err.message, t.create_statement) + end + + def test_working + # register vtable implementation under module RubyModule. RubyModule will be created in sqlite3 if not already existing + VTableTest.new(@db, 'RubyModule') + 2.times do |i| + #this will instantiate a new virtual table using implementation from VTableTest + @db.execute("create virtual table if not exists VTableTest using RubyModule") + + #execute an sql statement + nb_row = @db.execute("select x, sum(y), avg(y), avg(y*y), min(y), max(y), count(y) from VTableTest group by x").each.count + assert_operator nb_row, :>, 0 + end + end + + def test_vtable + # test compact declaration of virtual table. The last parameter should be an enumerable of Array. + SQLite3.vtable(@db, 'VTableTest2', 'a, b, c', [ + [1, 2, 3], + [2, 4, 6], + [3, 6, 9] + ]) + nb_row = @db.execute('select count(*) from VTableTest2').each.first[0] + assert_equal( 3, nb_row ) + sum_a, sum_b, sum_c = *@db.execute('select sum(a), sum(b), sum(c) from VTableTest2').each.first + assert_equal( 6, sum_a ) + assert_equal( 12, sum_b ) + assert_equal( 18, sum_c ) + end + + def test_multiple_vtable + # make sure it is possible to join virtual table using sqlite + SQLite3.vtable(@db, 'VTableTest3', 'col1', [['a'], ['b']]) + SQLite3.vtable(@db, 'VTableTest4', 'col2', [['c'], ['d']]) + rows = @db.execute('select col1, col2 from VTableTest3, VTableTest4').each.to_a + assert_includes rows, ['a', 'c'] + assert_includes rows, ['a', 'd'] + assert_includes rows, ['b', 'c'] + assert_includes rows, ['b', 'd'] + end + + def test_best_filter + # one can provide a best_filter implementation see SQLite3 documentation about best_filter + test = self + SQLite3.vtable(@db, 'VTableTest5', 'col1, col2', [['a', 1], ['b', 2]]).tap do |vtable| + vtable.define_singleton_method(:best_index) do |constraint, order_by| + # check constraint + test.assert_includes constraint, [0, :<=] # col1 <= 'c' + test.assert_includes constraint, [0, :>] # col1 > 'a' + test.assert_includes constraint, [1, :<] # col2 < 3 + @constraint = constraint + + # check order by + test.assert_equal( [ + [1, 1], # col2 + [0, -1], # col1 desc + ], order_by ) + + { idxNum: 45 } + end + vtable.singleton_class.send(:alias_method, :orig_filter, :filter) + vtable.define_singleton_method(:filter) do |idxNum, args| + # idxNum should be the one returned by best_index + test.assert_equal( 45, idxNum ) + + # args should be consistent with the constraint given to best_index + test.assert_equal( @constraint.size, args.size ) + filters = @constraint.zip(args) + test.assert_includes filters, [[0, :<=], 'c'] # col1 <= 'c' + test.assert_includes filters, [[0, :>], 'a'] # col1 > 'a' + test.assert_includes filters, [[1, :<], 3] # col2 < 3 + + orig_filter(idxNum, args) + end + end + rows = @db.execute('select col1 from VTableTest5 where col1 <= \'c\' and col1 > \'a\' and col2 < 3 order by col2, col1 desc').each.to_a + assert_equal( [['b']], rows ) + end + + def test_garbage_collection + # this test will check that everything is working even if rows are getting collected during the execution of the statement + started = false + n_deleted_during_request = 0 + finalizer = proc do |id| + n_deleted_during_request += 1 if started + end + SQLite3.vtable(@db, 'VTableTest6', 'col1 number, col2 number, col3 text', (1..Float::INFINITY).lazy.map do |i| + r = [i, i*5, "some text #{i}"] + ObjectSpace.define_finalizer(r, finalizer) + r + end) + started = true + @db.execute('select col1, col2 from VTableTest6 limit 10') do |row| + assert_equal(row[1], row[0]*5) + end + started = false + assert_operator(n_deleted_during_request, :>, 0) + end + + end if defined?(SQLite3::VTable) +end +