Skip to content

Commit

Permalink
support for best_index/filter
Browse files Browse the repository at this point in the history
  • Loading branch information
lionelperrin committed Apr 1, 2016
1 parent 2054e2e commit 4367569
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 15 deletions.
1 change: 1 addition & 0 deletions Manifest.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,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
Expand Down
2 changes: 1 addition & 1 deletion ext/sqlite3/database.c
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions ext/sqlite3/database.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

// used by module.c too
void set_sqlite3_func_result(sqlite3_context * ctx, VALUE result);
VALUE sqlite3val2rb(sqlite3_value * val);

struct _sqlite3Ruby {
sqlite3 *db;
Expand Down
134 changes: 134 additions & 0 deletions ext/sqlite3/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,136 @@ static int xConnect(sqlite3* db, void *pAux,
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));
} else {
printf("ignoring %d %d\n", info->aConstraint[i].iColumn, info->aConstraint[i].op);
}
}

// 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;
}

Expand Down Expand Up @@ -117,6 +245,12 @@ static int xNext(ruby_sqlite3_vtab_cursor* cursor)
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);
}
Expand Down
44 changes: 31 additions & 13 deletions lib/sqlite3/vtable.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module SQLite3Vtable
module SQLite3_VTables
# this module contains the vtable classes generated
# using SQLite3::vtable method
end
Expand All @@ -20,34 +20,52 @@ 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 'VTableInterface#filter not implemented'
end

# called to retrieve a new row
def next
fail 'VTableInterface#next not implemented'
end
end

def self.vtable(db, table_name, table_columns)
if SQLite3Vtable.const_defined?(table_name, inherit = false)
def self.vtable(db, table_name, table_columns, enumerable)
Module.new(db, 'SQLite3_VTables')
if SQLite3_VTables.const_defined?(table_name, inherit = false)
raise "'#{table_name}' already declared"
end

klass = Class.new(VTableInterface) do
def initialize(enumerable)
@enumerable = enumerable
end
def create_statement
"create table #{table_name}(#{table_columns})"
end
def next
klass = Class.new(VTableInterface)
klass.send(:define_method, :filter) do |idxNum, args|
@enumerable = enumerable.to_enum
end
klass.send(:define_method, :create_statement) do
"create table #{table_name}(#{table_columns})"
end
klass.send(:define_method, :next) do
begin
@enumerable.next
rescue StopIteration
nil
end
end

begin
SQLite3Vtable.const_set(table_name, klass)
SQLite3_VTables.const_set(table_name, klass)
rescue NameError
raise "'#{table_name}' must be a valid ruby constant name"
end
db.execute("create virtual table #{table_name} using SQLite3Vtable")
db.execute("create virtual table #{table_name} using SQLite3_VTables")
klass
end
end
67 changes: 66 additions & 1 deletion test/test_vtable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

#the ruby module name should be the one given to sqlite when creating the virtual table.
module RubyModule
class TestVTable < VTableInterface
class TestVTable < SQLite3::VTableInterface
def initialize
@str = "A"*1500
end
Expand All @@ -17,6 +17,10 @@ def create_statement
#required method for vtable
#called before each statement
def open
end

# this method initialize/reset cursor
def filter(id, args)
@count = 0
end

Expand Down Expand Up @@ -66,6 +70,67 @@ def test_working
assert( nb_row > 0 )
end

def test_vtable
SQLite3.vtable(@db, 'TestVTable2', 'a, b, c', [
[1, 2, 3],
[2, 4, 6],
[3, 6, 9]
])
nb_row = @db.execute('select count(*) from TestVTable2').each.first[0]
assert( nb_row == 3 )
sum_a, sum_b, sum_c = *@db.execute('select sum(a), sum(b), sum(c) from TestVTable2').each.first
assert( sum_a = 6 )
assert( sum_b == 12 )
assert( sum_c == 18 )
end

def test_multiple_vtable
SQLite3.vtable(@db, 'TestVTable3', 'col1', [['a'], ['b']])
SQLite3.vtable(@db, 'TestVTable4', 'col2', [['c'], ['d']])
rows = @db.execute('select col1, col2 from TestVTable3, TestVTable4').each.to_a
assert( rows.include?(['a', 'c']) )
assert( rows.include?(['a', 'd']) )
assert( rows.include?(['b', 'c']) )
assert( rows.include?(['b', 'd']) )
end

def test_best_filter
test = self
SQLite3.vtable(@db, 'TestVTable5', 'col1, col2', [['a', 1], ['b', 2]]).tap do |vtable|
vtable.send(:define_method, :best_index) do |constraint, order_by|
# check constraint
test.assert( constraint.include?([0, :<=]) ) # col1 <= 'c'
test.assert( constraint.include?([0, :>]) ) # col1 > 'a'
test.assert( constraint.include?([1, :<]) ) # col2 < 3
@constraint = constraint

# check order by
test.assert( order_by == [
[1, 1], # col2
[0, -1], # col1 desc
] )

{ idxNum: 45 }
end
vtable.send(:alias_method, :orig_filter, :filter)
vtable.send(:define_method, :filter) do |idxNum, args|
# idxNum should be the one returned by best_index
test.assert( idxNum == 45 )

# args should be consistent with the constraint given to best_index
test.assert( args.size == @constraint.size )
filters = @constraint.zip(args)
test.assert( filters.include?([[0, :<=], 'c']) ) # col1 <= 'c'
test.assert( filters.include?([[0, :>], 'a']) ) # col1 > 'a'
test.assert( filters.include?([[1, :<], 3]) ) # col2 < 3

orig_filter(idxNum, args)
end
end
rows = @db.execute('select col1 from TestVTable5 where col1 <= \'c\' and col1 > \'a\' and col2 < 3 order by col2, col1 desc').each.to_a
assert( rows == [['b']] )
end

end if defined?(SQLite3::Module)
end

0 comments on commit 4367569

Please sign in to comment.