Skip to content

Commit 4367569

Browse files
committed
support for best_index/filter
1 parent 2054e2e commit 4367569

File tree

6 files changed

+234
-15
lines changed

6 files changed

+234
-15
lines changed

Manifest.txt

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ lib/sqlite3/statement.rb
3131
lib/sqlite3/translator.rb
3232
lib/sqlite3/value.rb
3333
lib/sqlite3/version.rb
34+
lib/sqlite3/vtable.rb
3435
setup.rb
3536
tasks/faq.rake
3637
tasks/gem.rake

ext/sqlite3/database.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ static VALUE last_insert_row_id(VALUE self)
297297
return LL2NUM(sqlite3_last_insert_rowid(ctx->db));
298298
}
299299

300-
static VALUE sqlite3val2rb(sqlite3_value * val)
300+
VALUE sqlite3val2rb(sqlite3_value * val)
301301
{
302302
switch(sqlite3_value_type(val)) {
303303
case SQLITE_INTEGER:

ext/sqlite3/database.h

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

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

910
struct _sqlite3Ruby {
1011
sqlite3 *db;

ext/sqlite3/module.c

+134
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,136 @@ static int xConnect(sqlite3* db, void *pAux,
7575
return xCreate(db, pAux, argc, argv, ppVTab, pzErr);
7676
}
7777

78+
static VALUE constraint_op_as_symbol(unsigned char op)
79+
{
80+
ID op_id;
81+
switch(op) {
82+
case SQLITE_INDEX_CONSTRAINT_EQ:
83+
op_id = rb_intern("==");
84+
break;
85+
case SQLITE_INDEX_CONSTRAINT_GT:
86+
op_id = rb_intern(">");
87+
break;
88+
case SQLITE_INDEX_CONSTRAINT_LE:
89+
op_id = rb_intern("<=");
90+
break;
91+
case SQLITE_INDEX_CONSTRAINT_LT:
92+
op_id = rb_intern("<");
93+
break;
94+
case SQLITE_INDEX_CONSTRAINT_GE:
95+
op_id = rb_intern(">=");
96+
break;
97+
case SQLITE_INDEX_CONSTRAINT_MATCH:
98+
op_id = rb_intern("match");
99+
break;
100+
#if SQLITE_VERSION_NUMBER>=3010000
101+
case SQLITE_INDEX_CONSTRAINT_LIKE:
102+
op_id = rb_intern("like");
103+
break;
104+
case SQLITE_INDEX_CONSTRAINT_GLOB:
105+
op_id = rb_intern("glob");
106+
break;
107+
case SQLITE_INDEX_CONSTRAINT_REGEXP:
108+
op_id = rb_intern("regexp");
109+
break;
110+
#endif
111+
#if SQLITE_VERSION_NUMBER>=3009000
112+
case SQLITE_INDEX_SCAN_UNIQUE:
113+
op_id = rb_intern("unique");
114+
break;
115+
#endif
116+
default:
117+
op_id = rb_intern("unsupported");
118+
}
119+
return ID2SYM(op_id);
120+
}
121+
122+
static VALUE constraint_to_ruby(const struct sqlite3_index_constraint* c)
123+
{
124+
VALUE cons = rb_ary_new2(2);
125+
rb_ary_store(cons, 0, LONG2FIX(c->iColumn));
126+
rb_ary_store(cons, 1, constraint_op_as_symbol(c->op));
127+
return cons;
128+
}
129+
130+
static VALUE order_by_to_ruby(const struct sqlite3_index_orderby* c)
131+
{
132+
VALUE order_by = rb_ary_new2(2);
133+
rb_ary_store(order_by, 0, LONG2FIX(c->iColumn));
134+
rb_ary_store(order_by, 1, LONG2FIX(1-2*c->desc));
135+
return order_by;
136+
}
137+
78138
static int xBestIndex(ruby_sqlite3_vtab *pVTab, sqlite3_index_info* info)
79139
{
140+
int i;
141+
VALUE constraint = rb_ary_new();
142+
VALUE order_by = rb_ary_new2(info->nOrderBy);
143+
VALUE ret, idx_num, estimated_cost, order_by_consumed, omit_all;
144+
#if SQLITE_VERSION_NUMBER >= 3008002
145+
VALUE estimated_rows;
146+
#endif
147+
#if SQLITE_VERSION_NUMBER >= 3009000
148+
VALUE idx_flags;
149+
#endif
150+
#if SQLITE_VERSION_NUMBER >= 3010000
151+
VALUE col_used;
152+
#endif
153+
154+
// convert constraints to ruby
155+
for (i = 0; i < info->nConstraint; ++i) {
156+
if (info->aConstraint[i].usable) {
157+
rb_ary_push(constraint, constraint_to_ruby(info->aConstraint + i));
158+
} else {
159+
printf("ignoring %d %d\n", info->aConstraint[i].iColumn, info->aConstraint[i].op);
160+
}
161+
}
162+
163+
// convert order_by to ruby
164+
for (i = 0; i < info->nOrderBy; ++i) {
165+
rb_ary_store(order_by, i, order_by_to_ruby(info->aOrderBy + i));
166+
}
167+
168+
169+
ret = rb_funcall( pVTab->vtable, rb_intern("best_index"), 2, constraint, order_by );
170+
if (ret != Qnil ) {
171+
if (!RB_TYPE_P(ret, T_HASH)) {
172+
rb_raise(rb_eTypeError, "best_index: expect returned value to be a Hash");
173+
}
174+
idx_num = rb_hash_aref(ret, ID2SYM(rb_intern("idxNum")));
175+
if (idx_num == Qnil ) {
176+
rb_raise(rb_eKeyError, "best_index: mandatory key 'idxNum' not found");
177+
}
178+
info->idxNum = FIX2INT(idx_num);
179+
estimated_cost = rb_hash_aref(ret, ID2SYM(rb_intern("estimatedCost")));
180+
if (estimated_cost != Qnil) { info->estimatedCost = NUM2DBL(estimated_cost); }
181+
order_by_consumed = rb_hash_aref(ret, ID2SYM(rb_intern("orderByConsumed")));
182+
info->orderByConsumed = RTEST(order_by_consumed);
183+
#if SQLITE_VERSION_NUMBER >= 3008002
184+
estimated_rows = rb_hash_aref(ret, ID2SYM(rb_intern("estimatedRows")));
185+
if (estimated_rows != Qnil) { bignum_to_int64(estimated_rows, &info->estimatedRows); }
186+
#endif
187+
#if SQLITE_VERSION_NUMBER >= 3009000
188+
idx_flags = rb_hash_aref(ret, ID2SYM(rb_intern("idxFlags")));
189+
if (idx_flags != Qnil) { info->idxFlags = FIX2INT(idx_flags); }
190+
#endif
191+
#if SQLITE_VERSION_NUMBER >= 3010000
192+
col_used = rb_hash_aref(ret, ID2SYM(rb_intern("colUsed")));
193+
if (col_used != Qnil) { bignum_to_int64(col_used, &info->colUsed); }
194+
#endif
195+
196+
// make sure that expression are given to filter
197+
omit_all = rb_hash_aref(ret, ID2SYM(rb_intern("omitAllConstraint")));
198+
for (i = 0; i < info->nConstraint; ++i) {
199+
if (RTEST(omit_all)) {
200+
info->aConstraintUsage[i].omit = 1;
201+
}
202+
if (info->aConstraint[i].usable) {
203+
info->aConstraintUsage[i].argvIndex = (i+1);
204+
}
205+
}
206+
}
207+
80208
return SQLITE_OK;
81209
}
82210

@@ -117,6 +245,12 @@ static int xNext(ruby_sqlite3_vtab_cursor* cursor)
117245
static int xFilter(ruby_sqlite3_vtab_cursor* cursor, int idxNum, const char *idxStr,
118246
int argc, sqlite3_value **argv)
119247
{
248+
int i;
249+
VALUE argv_ruby = rb_ary_new2(argc);
250+
for (i = 0; i < argc; ++i) {
251+
rb_ary_store(argv_ruby, i, sqlite3val2rb(argv[i]));
252+
}
253+
rb_funcall( cursor->pVTab->vtable, rb_intern("filter"), 2, LONG2FIX(idxNum), argv_ruby );
120254
cursor->rowid = 0;
121255
return xNext(cursor);
122256
}

lib/sqlite3/vtable.rb

+31-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module SQLite3Vtable
1+
module SQLite3_VTables
22
# this module contains the vtable classes generated
33
# using SQLite3::vtable method
44
end
@@ -20,34 +20,52 @@ def close
2020
# do nothing by default
2121
end
2222

23+
#called to define the best suitable index
24+
def best_index(constraint, order_by)
25+
# one can return an evaluation of the index as shown below
26+
# { idxNum: 1, estimatedCost: 10.0, orderByConsumed: true }
27+
# see sqlite documentation for more details
28+
end
29+
30+
# may be called several times between open/close
31+
# it initialize/reset cursor
32+
def filter(idxNum, args)
33+
fail 'VTableInterface#filter not implemented'
34+
end
35+
2336
# called to retrieve a new row
2437
def next
2538
fail 'VTableInterface#next not implemented'
2639
end
2740
end
2841

29-
def self.vtable(db, table_name, table_columns)
30-
if SQLite3Vtable.const_defined?(table_name, inherit = false)
42+
def self.vtable(db, table_name, table_columns, enumerable)
43+
Module.new(db, 'SQLite3_VTables')
44+
if SQLite3_VTables.const_defined?(table_name, inherit = false)
3145
raise "'#{table_name}' already declared"
3246
end
3347

34-
klass = Class.new(VTableInterface) do
35-
def initialize(enumerable)
36-
@enumerable = enumerable
37-
end
38-
def create_statement
39-
"create table #{table_name}(#{table_columns})"
40-
end
41-
def next
48+
klass = Class.new(VTableInterface)
49+
klass.send(:define_method, :filter) do |idxNum, args|
50+
@enumerable = enumerable.to_enum
51+
end
52+
klass.send(:define_method, :create_statement) do
53+
"create table #{table_name}(#{table_columns})"
54+
end
55+
klass.send(:define_method, :next) do
56+
begin
4257
@enumerable.next
58+
rescue StopIteration
59+
nil
4360
end
4461
end
4562

4663
begin
47-
SQLite3Vtable.const_set(table_name, klass)
64+
SQLite3_VTables.const_set(table_name, klass)
4865
rescue NameError
4966
raise "'#{table_name}' must be a valid ruby constant name"
5067
end
51-
db.execute("create virtual table #{table_name} using SQLite3Vtable")
68+
db.execute("create virtual table #{table_name} using SQLite3_VTables")
69+
klass
5270
end
5371
end

test/test_vtable.rb

+66-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
#the ruby module name should be the one given to sqlite when creating the virtual table.
55
module RubyModule
6-
class TestVTable < VTableInterface
6+
class TestVTable < SQLite3::VTableInterface
77
def initialize
88
@str = "A"*1500
99
end
@@ -17,6 +17,10 @@ def create_statement
1717
#required method for vtable
1818
#called before each statement
1919
def open
20+
end
21+
22+
# this method initialize/reset cursor
23+
def filter(id, args)
2024
@count = 0
2125
end
2226

@@ -66,6 +70,67 @@ def test_working
6670
assert( nb_row > 0 )
6771
end
6872

73+
def test_vtable
74+
SQLite3.vtable(@db, 'TestVTable2', 'a, b, c', [
75+
[1, 2, 3],
76+
[2, 4, 6],
77+
[3, 6, 9]
78+
])
79+
nb_row = @db.execute('select count(*) from TestVTable2').each.first[0]
80+
assert( nb_row == 3 )
81+
sum_a, sum_b, sum_c = *@db.execute('select sum(a), sum(b), sum(c) from TestVTable2').each.first
82+
assert( sum_a = 6 )
83+
assert( sum_b == 12 )
84+
assert( sum_c == 18 )
85+
end
86+
87+
def test_multiple_vtable
88+
SQLite3.vtable(@db, 'TestVTable3', 'col1', [['a'], ['b']])
89+
SQLite3.vtable(@db, 'TestVTable4', 'col2', [['c'], ['d']])
90+
rows = @db.execute('select col1, col2 from TestVTable3, TestVTable4').each.to_a
91+
assert( rows.include?(['a', 'c']) )
92+
assert( rows.include?(['a', 'd']) )
93+
assert( rows.include?(['b', 'c']) )
94+
assert( rows.include?(['b', 'd']) )
95+
end
96+
97+
def test_best_filter
98+
test = self
99+
SQLite3.vtable(@db, 'TestVTable5', 'col1, col2', [['a', 1], ['b', 2]]).tap do |vtable|
100+
vtable.send(:define_method, :best_index) do |constraint, order_by|
101+
# check constraint
102+
test.assert( constraint.include?([0, :<=]) ) # col1 <= 'c'
103+
test.assert( constraint.include?([0, :>]) ) # col1 > 'a'
104+
test.assert( constraint.include?([1, :<]) ) # col2 < 3
105+
@constraint = constraint
106+
107+
# check order by
108+
test.assert( order_by == [
109+
[1, 1], # col2
110+
[0, -1], # col1 desc
111+
] )
112+
113+
{ idxNum: 45 }
114+
end
115+
vtable.send(:alias_method, :orig_filter, :filter)
116+
vtable.send(:define_method, :filter) do |idxNum, args|
117+
# idxNum should be the one returned by best_index
118+
test.assert( idxNum == 45 )
119+
120+
# args should be consistent with the constraint given to best_index
121+
test.assert( args.size == @constraint.size )
122+
filters = @constraint.zip(args)
123+
test.assert( filters.include?([[0, :<=], 'c']) ) # col1 <= 'c'
124+
test.assert( filters.include?([[0, :>], 'a']) ) # col1 > 'a'
125+
test.assert( filters.include?([[1, :<], 3]) ) # col2 < 3
126+
127+
orig_filter(idxNum, args)
128+
end
129+
end
130+
rows = @db.execute('select col1 from TestVTable5 where col1 <= \'c\' and col1 > \'a\' and col2 < 3 order by col2, col1 desc').each.to_a
131+
assert( rows == [['b']] )
132+
end
133+
69134
end if defined?(SQLite3::Module)
70135
end
71136

0 commit comments

Comments
 (0)