Skip to content

Commit 31e44c1

Browse files
committed
Merge branch 'main' into statement-timeout
2 parents 13d4067 + 6593767 commit 31e44c1

File tree

4 files changed

+321
-3
lines changed

4 files changed

+321
-3
lines changed

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ source "https://rubygems.org"
33
gemspec
44

55
group :development do
6-
gem "minitest", "5.20.0"
6+
gem "minitest", "5.21.1"
77

88
gem "rake-compiler", "1.2.5"
99
gem "rake-compiler-dock", "1.4.0"

ext/sqlite3/statement.c

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

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

423570
/* call-seq: stmt.database_name(column_index)
@@ -454,9 +601,14 @@ init_sqlite3_statement(void)
454601
rb_define_method(cSqlite3Statement, "column_name", column_name, 1);
455602
rb_define_method(cSqlite3Statement, "column_decltype", column_decltype, 1);
456603
rb_define_method(cSqlite3Statement, "bind_parameter_count", bind_parameter_count, 0);
457-
rb_define_private_method(cSqlite3Statement, "prepare", prepare, 2);
458-
459604
#ifdef HAVE_SQLITE3_COLUMN_DATABASE_NAME
460605
rb_define_method(cSqlite3Statement, "database_name", database_name, 1);
461606
#endif
607+
#ifdef SQLITE_STMTSTATUS_MEMUSED
608+
rb_define_method(cSqlite3Statement, "memused", memused, 0);
609+
#endif
610+
611+
rb_define_private_method(cSqlite3Statement, "prepare", prepare, 2);
612+
rb_define_private_method(cSqlite3Statement, "stats_as_hash", stats_as_hash, 0);
613+
rb_define_private_method(cSqlite3Statement, "stat_for", stat_for, 1);
462614
}

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)