Skip to content

Commit 6593767

Browse files
authored
Merge pull request #461 from fractaledmind/stmt-status
Implement sqlite3_stmt_status interface
2 parents 0ebe4ed + 3dc5fb8 commit 6593767

File tree

3 files changed

+320
-2
lines changed

3 files changed

+320
-2
lines changed

ext/sqlite3/statement.c

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,153 @@ bind_parameter_count(VALUE self)
417417
return INT2NUM(sqlite3_bind_parameter_count(ctx->st));
418418
}
419419

420+
enum stmt_stat_sym {
421+
stmt_stat_sym_fullscan_steps,
422+
stmt_stat_sym_sorts,
423+
stmt_stat_sym_autoindexes,
424+
stmt_stat_sym_vm_steps,
425+
#ifdef SQLITE_STMTSTATUS_REPREPARE
426+
stmt_stat_sym_reprepares,
427+
#endif
428+
#ifdef SQLITE_STMTSTATUS_RUN
429+
stmt_stat_sym_runs,
430+
#endif
431+
#ifdef SQLITE_STMTSTATUS_FILTER_MISS
432+
stmt_stat_sym_filter_misses,
433+
#endif
434+
#ifdef SQLITE_STMTSTATUS_FILTER_HIT
435+
stmt_stat_sym_filter_hits,
436+
#endif
437+
stmt_stat_sym_last
438+
};
439+
440+
static VALUE stmt_stat_symbols[stmt_stat_sym_last];
441+
442+
static void
443+
setup_stmt_stat_symbols(void)
444+
{
445+
if (stmt_stat_symbols[0] == 0) {
446+
#define S(s) stmt_stat_symbols[stmt_stat_sym_##s] = ID2SYM(rb_intern_const(#s))
447+
S(fullscan_steps);
448+
S(sorts);
449+
S(autoindexes);
450+
S(vm_steps);
451+
#ifdef SQLITE_STMTSTATUS_REPREPARE
452+
S(reprepares);
453+
#endif
454+
#ifdef SQLITE_STMTSTATUS_RUN
455+
S(runs);
456+
#endif
457+
#ifdef SQLITE_STMTSTATUS_FILTER_MISS
458+
S(filter_misses);
459+
#endif
460+
#ifdef SQLITE_STMTSTATUS_FILTER_HIT
461+
S(filter_hits);
462+
#endif
463+
#undef S
464+
}
465+
}
466+
467+
static size_t
468+
stmt_stat_internal(VALUE hash_or_sym, sqlite3_stmt *stmt)
469+
{
470+
VALUE hash = Qnil, key = Qnil;
471+
472+
setup_stmt_stat_symbols();
473+
474+
if (RB_TYPE_P(hash_or_sym, T_HASH)) {
475+
hash = hash_or_sym;
476+
}
477+
else if (SYMBOL_P(hash_or_sym)) {
478+
key = hash_or_sym;
479+
}
480+
else {
481+
rb_raise(rb_eTypeError, "non-hash or symbol argument");
482+
}
483+
484+
#define SET(name, stat_type) \
485+
if (key == stmt_stat_symbols[stmt_stat_sym_##name]) \
486+
return sqlite3_stmt_status(stmt, stat_type, 0); \
487+
else if (hash != Qnil) \
488+
rb_hash_aset(hash, stmt_stat_symbols[stmt_stat_sym_##name], SIZET2NUM(sqlite3_stmt_status(stmt, stat_type, 0)));
489+
490+
SET(fullscan_steps, SQLITE_STMTSTATUS_FULLSCAN_STEP);
491+
SET(sorts, SQLITE_STMTSTATUS_SORT);
492+
SET(autoindexes, SQLITE_STMTSTATUS_AUTOINDEX);
493+
SET(vm_steps, SQLITE_STMTSTATUS_VM_STEP);
494+
#ifdef SQLITE_STMTSTATUS_REPREPARE
495+
SET(reprepares, SQLITE_STMTSTATUS_REPREPARE);
496+
#endif
497+
#ifdef SQLITE_STMTSTATUS_RUN
498+
SET(runs, SQLITE_STMTSTATUS_RUN);
499+
#endif
500+
#ifdef SQLITE_STMTSTATUS_FILTER_MISS
501+
SET(filter_misses, SQLITE_STMTSTATUS_FILTER_MISS);
502+
#endif
503+
#ifdef SQLITE_STMTSTATUS_FILTER_HIT
504+
SET(filter_hits, SQLITE_STMTSTATUS_FILTER_HIT);
505+
#endif
506+
#undef SET
507+
508+
if (!NIL_P(key)) { /* matched key should return above */
509+
rb_raise(rb_eArgError, "unknown key: %"PRIsVALUE, rb_sym2str(key));
510+
}
511+
512+
return 0;
513+
}
514+
515+
/* call-seq: stmt.stats_as_hash(hash)
516+
*
517+
* Returns a Hash containing information about the statement.
518+
*/
519+
static VALUE
520+
stats_as_hash(VALUE self)
521+
{
522+
sqlite3StmtRubyPtr ctx;
523+
TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx);
524+
REQUIRE_OPEN_STMT(ctx);
525+
VALUE arg = rb_hash_new();
526+
527+
stmt_stat_internal(arg, ctx->st);
528+
return arg;
529+
}
530+
531+
/* call-seq: stmt.stmt_stat(hash_or_key)
532+
*
533+
* Returns a Hash containing information about the statement.
534+
*/
535+
static VALUE
536+
stat_for(VALUE self, VALUE key)
537+
{
538+
sqlite3StmtRubyPtr ctx;
539+
TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx);
540+
REQUIRE_OPEN_STMT(ctx);
541+
542+
if (SYMBOL_P(key)) {
543+
size_t value = stmt_stat_internal(key, ctx->st);
544+
return SIZET2NUM(value);
545+
}
546+
else {
547+
rb_raise(rb_eTypeError, "non-symbol given");
548+
}
549+
}
550+
551+
#ifdef SQLITE_STMTSTATUS_MEMUSED
552+
/* call-seq: stmt.memory_used
553+
*
554+
* Return the approximate number of bytes of heap memory used to store the prepared statement
555+
*/
556+
static VALUE
557+
memused(VALUE self)
558+
{
559+
sqlite3StmtRubyPtr ctx;
560+
TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx);
561+
REQUIRE_OPEN_STMT(ctx);
562+
563+
return INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_MEMUSED, 0));
564+
}
565+
#endif
566+
420567
#ifdef HAVE_SQLITE3_COLUMN_DATABASE_NAME
421568

422569
/* call-seq: stmt.database_name(column_index)
@@ -453,9 +600,14 @@ init_sqlite3_statement(void)
453600
rb_define_method(cSqlite3Statement, "column_name", column_name, 1);
454601
rb_define_method(cSqlite3Statement, "column_decltype", column_decltype, 1);
455602
rb_define_method(cSqlite3Statement, "bind_parameter_count", bind_parameter_count, 0);
456-
rb_define_private_method(cSqlite3Statement, "prepare", prepare, 2);
457-
458603
#ifdef HAVE_SQLITE3_COLUMN_DATABASE_NAME
459604
rb_define_method(cSqlite3Statement, "database_name", database_name, 1);
460605
#endif
606+
#ifdef SQLITE_STMTSTATUS_MEMUSED
607+
rb_define_method(cSqlite3Statement, "memused", memused, 0);
608+
#endif
609+
610+
rb_define_private_method(cSqlite3Statement, "prepare", prepare, 2);
611+
rb_define_private_method(cSqlite3Statement, "stats_as_hash", stats_as_hash, 0);
612+
rb_define_private_method(cSqlite3Statement, "stat_for", stat_for, 1);
461613
}

lib/sqlite3/statement.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,33 @@ def must_be_open! # :nodoc:
145145
end
146146
end
147147

148+
# Returns a Hash containing information about the statement.
149+
# The contents of the hash are implementation specific and may change in
150+
# the future without notice. The hash includes information about internal
151+
# statistics about the statement such as:
152+
# - +fullscan_steps+: the number of times that SQLite has stepped forward
153+
# in a table as part of a full table scan
154+
# - +sorts+: the number of sort operations that have occurred
155+
# - +autoindexes+: the number of rows inserted into transient indices
156+
# that were created automatically in order to help joins run faster
157+
# - +vm_steps+: the number of virtual machine operations executed by the
158+
# prepared statement
159+
# - +reprepares+: the number of times that the prepare statement has been
160+
# automatically regenerated due to schema changes or changes to bound
161+
# parameters that might affect the query plan
162+
# - +runs+: the number of times that the prepared statement has been run
163+
# - +filter_misses+: the number of times that the Bloom filter returned
164+
# a find, and thus the join step had to be processed as normal
165+
# - +filter_hits+: the number of times that a join step was bypassed
166+
# because a Bloom filter returned not-found
167+
def stat key = nil
168+
if key
169+
stat_for(key)
170+
else
171+
stats_as_hash
172+
end
173+
end
174+
148175
private
149176

150177
# A convenience method for obtaining the metadata about the query. Note

test/test_statement.rb

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,5 +288,144 @@ def test_clear_bindings!
288288

289289
stmt.close
290290
end
291+
292+
def test_stat
293+
assert @stmt.stat.is_a?(Hash)
294+
end
295+
296+
def test_stat_fullscan_steps
297+
@db.execute 'CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT);'
298+
10.times do |i|
299+
@db.execute 'INSERT INTO test_table (name) VALUES (?)', "name_#{i}"
300+
end
301+
@db.execute 'DROP INDEX IF EXISTS idx_test_table_id;'
302+
stmt = @db.prepare("SELECT * FROM test_table WHERE name LIKE 'name%'")
303+
stmt.execute.to_a
304+
305+
assert_equal 9, stmt.stat(:fullscan_steps)
306+
307+
stmt.close
308+
end
309+
310+
def test_stat_sorts
311+
@db.execute 'CREATE TABLE test1(a)'
312+
@db.execute 'INSERT INTO test1 VALUES (1)'
313+
stmt = @db.prepare('select * from test1 order by a')
314+
stmt.execute.to_a
315+
316+
assert_equal 1, stmt.stat(:sorts)
317+
318+
stmt.close
319+
end
320+
321+
def test_stat_autoindexes
322+
@db.execute "CREATE TABLE t1(a,b);"
323+
@db.execute "CREATE TABLE t2(c,d);"
324+
10.times do |i|
325+
@db.execute 'INSERT INTO t1 (a, b) VALUES (?, ?)', [i, i.to_s]
326+
@db.execute 'INSERT INTO t2 (c, d) VALUES (?, ?)', [i, i.to_s]
327+
end
328+
stmt = @db.prepare("SELECT * FROM t1, t2 WHERE a=c;")
329+
stmt.execute.to_a
330+
331+
assert_equal 9, stmt.stat(:autoindexes)
332+
333+
stmt.close
334+
end
335+
336+
def test_stat_vm_steps
337+
@db.execute 'CREATE TABLE test1(a)'
338+
@db.execute 'INSERT INTO test1 VALUES (1)'
339+
stmt = @db.prepare('select * from test1 order by a')
340+
stmt.execute.to_a
341+
342+
assert_operator stmt.stat(:vm_steps), :>, 0
343+
344+
stmt.close
345+
end
346+
347+
def test_stat_reprepares
348+
@db.execute 'CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT);'
349+
10.times do |i|
350+
@db.execute 'INSERT INTO test_table (name) VALUES (?)', "name_#{i}"
351+
end
352+
stmt = @db.prepare("SELECT * FROM test_table WHERE name LIKE ?")
353+
stmt.execute('name%').to_a
354+
355+
if stmt.stat.key?(:reprepares)
356+
assert_equal 1, stmt.stat(:reprepares)
357+
else
358+
assert_raises(ArgumentError, "unknown key: reprepares") { stmt.stat(:reprepares) }
359+
end
360+
361+
stmt.close
362+
end
363+
364+
def test_stat_runs
365+
@db.execute 'CREATE TABLE test1(a)'
366+
@db.execute 'INSERT INTO test1 VALUES (1)'
367+
stmt = @db.prepare('select * from test1')
368+
stmt.execute.to_a
369+
370+
if stmt.stat.key?(:runs)
371+
assert_equal 1, stmt.stat(:runs)
372+
else
373+
assert_raises(ArgumentError, "unknown key: runs") { stmt.stat(:runs) }
374+
end
375+
376+
stmt.close
377+
end
378+
379+
def test_stat_filter_misses
380+
@db.execute "CREATE TABLE t1(a,b);"
381+
@db.execute "CREATE TABLE t2(c,d);"
382+
10.times do |i|
383+
@db.execute 'INSERT INTO t1 (a, b) VALUES (?, ?)', [i, i.to_s]
384+
@db.execute 'INSERT INTO t2 (c, d) VALUES (?, ?)', [i, i.to_s]
385+
end
386+
stmt = @db.prepare("SELECT * FROM t1, t2 WHERE a=c;")
387+
stmt.execute.to_a
388+
389+
if stmt.stat.key?(:filter_misses)
390+
assert_equal 10, stmt.stat(:filter_misses)
391+
else
392+
assert_raises(ArgumentError, "unknown key: filter_misses") { stmt.stat(:filter_misses) }
393+
end
394+
395+
stmt.close
396+
end
397+
398+
def test_stat_filter_hits
399+
@db.execute "CREATE TABLE t1(a,b);"
400+
@db.execute "CREATE TABLE t2(c,d);"
401+
10.times do |i|
402+
@db.execute 'INSERT INTO t1 (a, b) VALUES (?, ?)', [i, i.to_s]
403+
@db.execute 'INSERT INTO t2 (c, d) VALUES (?, ?)', [i+1, i.to_s]
404+
end
405+
stmt = @db.prepare("SELECT * FROM t1, t2 WHERE a=c AND b = '1' AND d = '1';")
406+
stmt.execute.to_a
407+
408+
if stmt.stat.key?(:filter_hits)
409+
assert_equal 1, stmt.stat(:filter_hits)
410+
else
411+
assert_raises(ArgumentError, "unknown key: filter_hits") { stmt.stat(:filter_hits) }
412+
end
413+
414+
stmt.close
415+
end
416+
417+
def test_memused
418+
@db.execute 'CREATE TABLE test1(a)'
419+
@db.execute 'INSERT INTO test1 VALUES (1)'
420+
stmt = @db.prepare('select * from test1')
421+
422+
skip("memused not defined") unless stmt.respond_to?(:memused)
423+
424+
stmt.execute.to_a
425+
426+
assert_operator stmt.memused, :>, 0
427+
428+
stmt.close
429+
end
291430
end
292431
end

0 commit comments

Comments
 (0)