Skip to content

Commit 910e416

Browse files
committed
first naive implementation of virtual table
* see test/test_vtable.rb for example * only select is supported (update/insert is not yet implemented)
1 parent 2070182 commit 910e416

File tree

7 files changed

+343
-1
lines changed

7 files changed

+343
-1
lines changed

ext/sqlite3/database.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ static VALUE sqlite3val2rb(sqlite3_value * val)
299299
}
300300
}
301301

302-
static void set_sqlite3_func_result(sqlite3_context * ctx, VALUE result)
302+
void set_sqlite3_func_result(sqlite3_context * ctx, VALUE result)
303303
{
304304
switch(TYPE(result)) {
305305
case T_NIL:

ext/sqlite3/database.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
#include <sqlite3_ruby.h>
55

6+
// used by module.c too
7+
void set_sqlite3_func_result(sqlite3_context * ctx, VALUE result);
8+
69
struct _sqlite3Ruby {
710
sqlite3 *db;
811
};

ext/sqlite3/module.c

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
#include <stdio.h>
2+
#include <sqlite3_ruby.h>
3+
4+
#undef ENABLE_TRACE
5+
6+
#ifdef ENABLE_TRACE
7+
static FILE* pf;
8+
# define TRACE(str) \
9+
fprintf(pf, "%s:%d:%s\n", __FILE__, __LINE__, str); \
10+
fflush(pf);
11+
#else
12+
# define TRACE(str) ;
13+
#endif
14+
15+
VALUE cSqlite3Module;
16+
17+
/** structure for ruby virtual table: inherits from sqlite3_vtab */
18+
typedef struct {
19+
// mandatory sqlite3 fields
20+
const sqlite3_module* pModule;
21+
int nRef;
22+
char *zErrMsg;
23+
// Ruby fields
24+
VALUE vtable;
25+
} ruby_sqlite3_vtab;
26+
27+
/** structure for ruby cursors: inherits from sqlite3_vtab_cursor */
28+
typedef struct {
29+
ruby_sqlite3_vtab* pVTab;
30+
VALUE row;
31+
int rowid;
32+
} ruby_sqlite3_vtab_cursor;
33+
34+
35+
/**
36+
* lookup for a ruby class <ModuleName>::<TableName> and create an instance of this class
37+
* This instance is then used to bind sqlite vtab callbacks
38+
*/
39+
static int xCreate(sqlite3* db, VALUE *module_name,
40+
int argc, char **argv,
41+
ruby_sqlite3_vtab **ppVTab,
42+
char **pzErr)
43+
{
44+
VALUE sql_stmt, module, ruby_class;
45+
ID table_id, module_id;
46+
VALUE ruby_class_args[0];
47+
const char* module_name_cstr = (const char*)StringValuePtr(*module_name);
48+
const char* table_name_cstr = (const char*)argv[2];
49+
TRACE("xCreate");
50+
51+
52+
// lookup for ruby class named like <module_id>::<table_name>
53+
module_id = rb_intern( module_name_cstr );
54+
module = rb_const_get(rb_cObject, module_id);
55+
table_id = rb_intern( table_name_cstr );
56+
ruby_class = rb_const_get(module, table_id);
57+
58+
// alloc a new ruby_sqlite3_vtab object
59+
// and store related attributes
60+
(*ppVTab) = (ruby_sqlite3_vtab*)malloc(sizeof(ruby_sqlite3_vtab));
61+
62+
// create a new instance
63+
(*ppVTab)->vtable = rb_class_new_instance(0, ruby_class_args, ruby_class);
64+
65+
// call the create function
66+
sql_stmt = rb_funcall((*ppVTab)->vtable, rb_intern("create_statement"), 0);
67+
68+
#ifdef HAVE_RUBY_ENCODING_H
69+
if(!UTF8_P(sql_stmt)) {
70+
sql_stmt = rb_str_export_to_enc(sql_stmt, rb_utf8_encoding());
71+
}
72+
#endif
73+
if ( sqlite3_declare_vtab(db, StringValuePtr(sql_stmt)) )
74+
rb_raise(rb_eArgError, "fail to declare virtual table");
75+
76+
TRACE("xCreate done");
77+
return SQLITE_OK;
78+
}
79+
80+
static int xConnect(sqlite3* db, void *pAux,
81+
int argc, char **argv,
82+
ruby_sqlite3_vtab **ppVTab,
83+
char **pzErr)
84+
{
85+
TRACE("xConnect");
86+
return xCreate(db, pAux, argc, argv, ppVTab, pzErr);
87+
}
88+
89+
static int xBestIndex(ruby_sqlite3_vtab *pVTab, sqlite3_index_info* info)
90+
{
91+
TRACE("xBestIndex");
92+
return SQLITE_OK;
93+
}
94+
95+
static int xDestroy(ruby_sqlite3_vtab *pVTab)
96+
{
97+
TRACE("xDestroy");
98+
free(pVTab);
99+
return SQLITE_OK;
100+
}
101+
102+
static int xDisconnect(ruby_sqlite3_vtab *pVTab)
103+
{
104+
TRACE("xDisconnect");
105+
return xDestroy(pVTab);
106+
}
107+
108+
static int xOpen(ruby_sqlite3_vtab *pVTab, ruby_sqlite3_vtab_cursor **ppCursor)
109+
{
110+
TRACE("xOpen");
111+
rb_funcall( pVTab->vtable, rb_intern("open"), 0 );
112+
*ppCursor = (ruby_sqlite3_vtab_cursor*)malloc(sizeof(ruby_sqlite3_vtab_cursor));
113+
(*ppCursor)->pVTab = pVTab;
114+
(*ppCursor)->rowid = 0;
115+
return SQLITE_OK;
116+
}
117+
118+
static int xClose(ruby_sqlite3_vtab_cursor* cursor)
119+
{
120+
TRACE("xClose");
121+
rb_funcall( cursor->pVTab->vtable, rb_intern("close"), 0 );
122+
free(cursor);
123+
return SQLITE_OK;
124+
}
125+
126+
static int xNext(ruby_sqlite3_vtab_cursor* cursor)
127+
{
128+
TRACE("xNext");
129+
cursor->row = rb_funcall(cursor->pVTab->vtable, rb_intern("next"), 0);
130+
++(cursor->rowid);
131+
return SQLITE_OK;
132+
}
133+
134+
static int xFilter(ruby_sqlite3_vtab_cursor* cursor, int idxNum, const char *idxStr,
135+
int argc, sqlite3_value **argv)
136+
{
137+
TRACE("xFilter");
138+
cursor->rowid = 0;
139+
return xNext(cursor);
140+
}
141+
142+
static int xEof(ruby_sqlite3_vtab_cursor* cursor)
143+
{
144+
TRACE("xEof");
145+
return (cursor->row == Qnil);
146+
}
147+
148+
static int xColumn(ruby_sqlite3_vtab_cursor* cursor, sqlite3_context* context, int i)
149+
{
150+
VALUE val = rb_ary_entry(cursor->row, i);
151+
TRACE("xColumn(%d)");
152+
153+
set_sqlite3_func_result(context, val);
154+
return SQLITE_OK;
155+
}
156+
157+
static int xRowid(ruby_sqlite3_vtab_cursor* cursor, sqlite_int64 *pRowid)
158+
{
159+
TRACE("xRowid");
160+
*pRowid = cursor->rowid;
161+
return SQLITE_OK;
162+
}
163+
164+
static sqlite3_module ruby_proxy_module =
165+
{
166+
0, /* iVersion */
167+
xCreate, /* xCreate - create a vtable */
168+
xConnect, /* xConnect - associate a vtable with a connection */
169+
xBestIndex, /* xBestIndex - best index */
170+
xDisconnect, /* xDisconnect - disassociate a vtable with a connection */
171+
xDestroy, /* xDestroy - destroy a vtable */
172+
xOpen, /* xOpen - open a cursor */
173+
xClose, /* xClose - close a cursor */
174+
xFilter, /* xFilter - configure scan constraints */
175+
xNext, /* xNext - advance a cursor */
176+
xEof, /* xEof - indicate end of result set*/
177+
xColumn, /* xColumn - read data */
178+
xRowid, /* xRowid - read data */
179+
NULL, /* xUpdate - write data */
180+
NULL, /* xBegin - begin transaction */
181+
NULL, /* xSync - sync transaction */
182+
NULL, /* xCommit - commit transaction */
183+
NULL, /* xRollback - rollback transaction */
184+
NULL, /* xFindFunction - function overloading */
185+
};
186+
187+
static void deallocate(void * ctx)
188+
{
189+
sqlite3ModuleRubyPtr c = (sqlite3ModuleRubyPtr)ctx;
190+
xfree(c);
191+
}
192+
193+
static VALUE allocate(VALUE klass)
194+
{
195+
sqlite3ModuleRubyPtr ctx = xcalloc((size_t)1, sizeof(sqlite3ModuleRuby));
196+
ctx->module = NULL;
197+
198+
return Data_Wrap_Struct(klass, NULL, deallocate, ctx);
199+
}
200+
201+
static VALUE initialize(VALUE self, VALUE db, VALUE name)
202+
{
203+
sqlite3RubyPtr db_ctx;
204+
sqlite3ModuleRubyPtr ctx;
205+
206+
StringValue(name);
207+
208+
Data_Get_Struct(db, sqlite3Ruby, db_ctx);
209+
Data_Get_Struct(self, sqlite3ModuleRuby, ctx);
210+
211+
212+
if(!db_ctx->db)
213+
rb_raise(rb_eArgError, "initializing a module on a closed database");
214+
215+
#ifdef HAVE_RUBY_ENCODING_H
216+
if(!UTF8_P(name)) {
217+
name = rb_str_export_to_enc(name, rb_utf8_encoding());
218+
}
219+
#endif
220+
221+
// make possible to access to ruby object from c
222+
ctx->module_name = name;
223+
224+
sqlite3_create_module(
225+
db_ctx->db,
226+
(const char *)StringValuePtr(name),
227+
&ruby_proxy_module,
228+
&(ctx->module_name) //the vtable required the module name
229+
);
230+
231+
TRACE("module initialized");
232+
return self;
233+
}
234+
235+
void init_sqlite3_module()
236+
{
237+
#ifdef ENABLE_TRACE
238+
pf = fopen("trace.log", "w");
239+
#endif
240+
cSqlite3Module = rb_define_class_under(mSqlite3, "Module", rb_cObject);
241+
rb_define_alloc_func(cSqlite3Module, allocate);
242+
rb_define_method(cSqlite3Module, "initialize", initialize, 2);
243+
}
244+

ext/sqlite3/module.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#ifndef SQLITE3_MODULE_RUBY
2+
#define SQLITE3_MODULE_RUBY
3+
4+
#include <sqlite3_ruby.h>
5+
6+
struct _sqlite3ModuleRuby {
7+
sqlite3_module *module;
8+
VALUE module_name; // so that sqlite can bring the module_name up to the vtable
9+
};
10+
11+
typedef struct _sqlite3ModuleRuby sqlite3ModuleRuby;
12+
typedef sqlite3ModuleRuby * sqlite3ModuleRubyPtr;
13+
14+
void init_sqlite3_module();
15+
16+
#endif

ext/sqlite3/sqlite3.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ void Init_sqlite3_native()
3333
#ifdef HAVE_SQLITE3_BACKUP_INIT
3434
init_sqlite3_backup();
3535
#endif
36+
init_sqlite3_module();
3637

3738
rb_define_singleton_method(mSqlite3, "libversion", libversion, 0);
3839
rb_define_const(mSqlite3, "SQLITE_VERSION", rb_str_new2(SQLITE_VERSION));

ext/sqlite3/sqlite3_ruby.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,6 @@ extern VALUE cSqlite3Blob;
5050
#include <statement.h>
5151
#include <exception.h>
5252
#include <backup.h>
53+
#include <module.h>
5354

5455
#endif

test/test_vtable.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
require "helper"
2+
require "sqlite3"
3+
require "sqlite3_native"
4+
5+
#the ruby module name should be the one given to sqlite when creating the virtual table.
6+
module RubyModule
7+
class TestVTable
8+
def initialize
9+
@str = "A"*1500
10+
end
11+
12+
#required method for vtable
13+
#this method is needed to declare the type of each column to sqlite
14+
def create_statement
15+
"create table TestVTable(s text, x integer, y int)"
16+
end
17+
18+
#required method for vtable
19+
#called before each statement
20+
def open
21+
@count = 0
22+
end
23+
24+
#required method for vtable
25+
#called to retrieve a new row
26+
def next
27+
28+
#produce up to 100000 lines
29+
@count += 1
30+
if @count <= 100000
31+
[@str, rand(10), rand]
32+
else
33+
nil
34+
end
35+
36+
end
37+
38+
#required method for vtable
39+
#called after each statement
40+
def close
41+
end
42+
end
43+
end
44+
45+
module SQLite3
46+
class TestVTable < SQLite3::TestCase
47+
def setup
48+
@db = SQLite3::Database.new(":memory:")
49+
@m = SQLite3::Module.new(@db, "RubyModule")
50+
end
51+
52+
def test_exception_module
53+
#the following line throws an exception because NonExistingModule is not valid ruby module
54+
assert_raise SQLite3::SQLException do
55+
@db.execute("create virtual table TestVTable using NonExistingModule")
56+
end
57+
end
58+
59+
def test_exception_table
60+
#the following line throws an exception because no ruby class RubyModule::NonExistingVTable is found as vtable implementation
61+
assert_raise NameError do
62+
@db.execute("create virtual table NonExistingVTable using RubyModule")
63+
end
64+
end
65+
66+
def test_working
67+
#this will instantiate a new virtual table using implementation from RubyModule::TestVTable
68+
@db.execute("create virtual table if not exists TestVTable using RubyModule")
69+
70+
#execute an sql statement
71+
nb_row = @db.execute("select x, sum(y), avg(y), avg(y*y), min(y), max(y), count(y) from TestVTable group by x").each.count
72+
assert( nb_row > 0 )
73+
end
74+
75+
end if defined?(SQLite3::Module)
76+
end
77+

0 commit comments

Comments
 (0)