Skip to content

Commit

Permalink
Merge pull request #461 from fractaledmind/stmt-status
Browse files Browse the repository at this point in the history
Implement sqlite3_stmt_status interface
  • Loading branch information
tenderlove authored Jan 19, 2024
2 parents 0ebe4ed + 3dc5fb8 commit 6593767
Show file tree
Hide file tree
Showing 3 changed files with 320 additions and 2 deletions.
156 changes: 154 additions & 2 deletions ext/sqlite3/statement.c
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,153 @@ bind_parameter_count(VALUE self)
return INT2NUM(sqlite3_bind_parameter_count(ctx->st));
}

enum stmt_stat_sym {
stmt_stat_sym_fullscan_steps,
stmt_stat_sym_sorts,
stmt_stat_sym_autoindexes,
stmt_stat_sym_vm_steps,
#ifdef SQLITE_STMTSTATUS_REPREPARE
stmt_stat_sym_reprepares,
#endif
#ifdef SQLITE_STMTSTATUS_RUN
stmt_stat_sym_runs,
#endif
#ifdef SQLITE_STMTSTATUS_FILTER_MISS
stmt_stat_sym_filter_misses,
#endif
#ifdef SQLITE_STMTSTATUS_FILTER_HIT
stmt_stat_sym_filter_hits,
#endif
stmt_stat_sym_last
};

static VALUE stmt_stat_symbols[stmt_stat_sym_last];

static void
setup_stmt_stat_symbols(void)
{
if (stmt_stat_symbols[0] == 0) {
#define S(s) stmt_stat_symbols[stmt_stat_sym_##s] = ID2SYM(rb_intern_const(#s))
S(fullscan_steps);
S(sorts);
S(autoindexes);
S(vm_steps);
#ifdef SQLITE_STMTSTATUS_REPREPARE
S(reprepares);
#endif
#ifdef SQLITE_STMTSTATUS_RUN
S(runs);
#endif
#ifdef SQLITE_STMTSTATUS_FILTER_MISS
S(filter_misses);
#endif
#ifdef SQLITE_STMTSTATUS_FILTER_HIT
S(filter_hits);
#endif
#undef S
}
}

static size_t
stmt_stat_internal(VALUE hash_or_sym, sqlite3_stmt *stmt)
{
VALUE hash = Qnil, key = Qnil;

setup_stmt_stat_symbols();

if (RB_TYPE_P(hash_or_sym, T_HASH)) {
hash = hash_or_sym;
}
else if (SYMBOL_P(hash_or_sym)) {
key = hash_or_sym;
}
else {
rb_raise(rb_eTypeError, "non-hash or symbol argument");
}

#define SET(name, stat_type) \
if (key == stmt_stat_symbols[stmt_stat_sym_##name]) \
return sqlite3_stmt_status(stmt, stat_type, 0); \
else if (hash != Qnil) \
rb_hash_aset(hash, stmt_stat_symbols[stmt_stat_sym_##name], SIZET2NUM(sqlite3_stmt_status(stmt, stat_type, 0)));

SET(fullscan_steps, SQLITE_STMTSTATUS_FULLSCAN_STEP);
SET(sorts, SQLITE_STMTSTATUS_SORT);
SET(autoindexes, SQLITE_STMTSTATUS_AUTOINDEX);
SET(vm_steps, SQLITE_STMTSTATUS_VM_STEP);
#ifdef SQLITE_STMTSTATUS_REPREPARE
SET(reprepares, SQLITE_STMTSTATUS_REPREPARE);
#endif
#ifdef SQLITE_STMTSTATUS_RUN
SET(runs, SQLITE_STMTSTATUS_RUN);
#endif
#ifdef SQLITE_STMTSTATUS_FILTER_MISS
SET(filter_misses, SQLITE_STMTSTATUS_FILTER_MISS);
#endif
#ifdef SQLITE_STMTSTATUS_FILTER_HIT
SET(filter_hits, SQLITE_STMTSTATUS_FILTER_HIT);
#endif
#undef SET

if (!NIL_P(key)) { /* matched key should return above */
rb_raise(rb_eArgError, "unknown key: %"PRIsVALUE, rb_sym2str(key));
}

return 0;
}

/* call-seq: stmt.stats_as_hash(hash)
*
* Returns a Hash containing information about the statement.
*/
static VALUE
stats_as_hash(VALUE self)
{
sqlite3StmtRubyPtr ctx;
TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx);
REQUIRE_OPEN_STMT(ctx);
VALUE arg = rb_hash_new();

stmt_stat_internal(arg, ctx->st);
return arg;
}

/* call-seq: stmt.stmt_stat(hash_or_key)
*
* Returns a Hash containing information about the statement.
*/
static VALUE
stat_for(VALUE self, VALUE key)
{
sqlite3StmtRubyPtr ctx;
TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx);
REQUIRE_OPEN_STMT(ctx);

if (SYMBOL_P(key)) {
size_t value = stmt_stat_internal(key, ctx->st);
return SIZET2NUM(value);
}
else {
rb_raise(rb_eTypeError, "non-symbol given");
}
}

#ifdef SQLITE_STMTSTATUS_MEMUSED
/* call-seq: stmt.memory_used
*
* Return the approximate number of bytes of heap memory used to store the prepared statement
*/
static VALUE
memused(VALUE self)
{
sqlite3StmtRubyPtr ctx;
TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx);
REQUIRE_OPEN_STMT(ctx);

return INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_MEMUSED, 0));
}
#endif

#ifdef HAVE_SQLITE3_COLUMN_DATABASE_NAME

/* call-seq: stmt.database_name(column_index)
Expand Down Expand Up @@ -453,9 +600,14 @@ init_sqlite3_statement(void)
rb_define_method(cSqlite3Statement, "column_name", column_name, 1);
rb_define_method(cSqlite3Statement, "column_decltype", column_decltype, 1);
rb_define_method(cSqlite3Statement, "bind_parameter_count", bind_parameter_count, 0);
rb_define_private_method(cSqlite3Statement, "prepare", prepare, 2);

#ifdef HAVE_SQLITE3_COLUMN_DATABASE_NAME
rb_define_method(cSqlite3Statement, "database_name", database_name, 1);
#endif
#ifdef SQLITE_STMTSTATUS_MEMUSED
rb_define_method(cSqlite3Statement, "memused", memused, 0);
#endif

rb_define_private_method(cSqlite3Statement, "prepare", prepare, 2);
rb_define_private_method(cSqlite3Statement, "stats_as_hash", stats_as_hash, 0);
rb_define_private_method(cSqlite3Statement, "stat_for", stat_for, 1);
}
27 changes: 27 additions & 0 deletions lib/sqlite3/statement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,33 @@ def must_be_open! # :nodoc:
end
end

# Returns a Hash containing information about the statement.
# The contents of the hash are implementation specific and may change in
# the future without notice. The hash includes information about internal
# statistics about the statement such as:
# - +fullscan_steps+: the number of times that SQLite has stepped forward
# in a table as part of a full table scan
# - +sorts+: the number of sort operations that have occurred
# - +autoindexes+: the number of rows inserted into transient indices
# that were created automatically in order to help joins run faster
# - +vm_steps+: the number of virtual machine operations executed by the
# prepared statement
# - +reprepares+: the number of times that the prepare statement has been
# automatically regenerated due to schema changes or changes to bound
# parameters that might affect the query plan
# - +runs+: the number of times that the prepared statement has been run
# - +filter_misses+: the number of times that the Bloom filter returned
# a find, and thus the join step had to be processed as normal
# - +filter_hits+: the number of times that a join step was bypassed
# because a Bloom filter returned not-found
def stat key = nil
if key
stat_for(key)
else
stats_as_hash
end
end

private

# A convenience method for obtaining the metadata about the query. Note
Expand Down
139 changes: 139 additions & 0 deletions test/test_statement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -288,5 +288,144 @@ def test_clear_bindings!

stmt.close
end

def test_stat
assert @stmt.stat.is_a?(Hash)
end

def test_stat_fullscan_steps
@db.execute 'CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT);'
10.times do |i|
@db.execute 'INSERT INTO test_table (name) VALUES (?)', "name_#{i}"
end
@db.execute 'DROP INDEX IF EXISTS idx_test_table_id;'
stmt = @db.prepare("SELECT * FROM test_table WHERE name LIKE 'name%'")
stmt.execute.to_a

assert_equal 9, stmt.stat(:fullscan_steps)

stmt.close
end

def test_stat_sorts
@db.execute 'CREATE TABLE test1(a)'
@db.execute 'INSERT INTO test1 VALUES (1)'
stmt = @db.prepare('select * from test1 order by a')
stmt.execute.to_a

assert_equal 1, stmt.stat(:sorts)

stmt.close
end

def test_stat_autoindexes
@db.execute "CREATE TABLE t1(a,b);"
@db.execute "CREATE TABLE t2(c,d);"
10.times do |i|
@db.execute 'INSERT INTO t1 (a, b) VALUES (?, ?)', [i, i.to_s]
@db.execute 'INSERT INTO t2 (c, d) VALUES (?, ?)', [i, i.to_s]
end
stmt = @db.prepare("SELECT * FROM t1, t2 WHERE a=c;")
stmt.execute.to_a

assert_equal 9, stmt.stat(:autoindexes)

stmt.close
end

def test_stat_vm_steps
@db.execute 'CREATE TABLE test1(a)'
@db.execute 'INSERT INTO test1 VALUES (1)'
stmt = @db.prepare('select * from test1 order by a')
stmt.execute.to_a

assert_operator stmt.stat(:vm_steps), :>, 0

stmt.close
end

def test_stat_reprepares
@db.execute 'CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT);'
10.times do |i|
@db.execute 'INSERT INTO test_table (name) VALUES (?)', "name_#{i}"
end
stmt = @db.prepare("SELECT * FROM test_table WHERE name LIKE ?")
stmt.execute('name%').to_a

if stmt.stat.key?(:reprepares)
assert_equal 1, stmt.stat(:reprepares)
else
assert_raises(ArgumentError, "unknown key: reprepares") { stmt.stat(:reprepares) }
end

stmt.close
end

def test_stat_runs
@db.execute 'CREATE TABLE test1(a)'
@db.execute 'INSERT INTO test1 VALUES (1)'
stmt = @db.prepare('select * from test1')
stmt.execute.to_a

if stmt.stat.key?(:runs)
assert_equal 1, stmt.stat(:runs)
else
assert_raises(ArgumentError, "unknown key: runs") { stmt.stat(:runs) }
end

stmt.close
end

def test_stat_filter_misses
@db.execute "CREATE TABLE t1(a,b);"
@db.execute "CREATE TABLE t2(c,d);"
10.times do |i|
@db.execute 'INSERT INTO t1 (a, b) VALUES (?, ?)', [i, i.to_s]
@db.execute 'INSERT INTO t2 (c, d) VALUES (?, ?)', [i, i.to_s]
end
stmt = @db.prepare("SELECT * FROM t1, t2 WHERE a=c;")
stmt.execute.to_a

if stmt.stat.key?(:filter_misses)
assert_equal 10, stmt.stat(:filter_misses)
else
assert_raises(ArgumentError, "unknown key: filter_misses") { stmt.stat(:filter_misses) }
end

stmt.close
end

def test_stat_filter_hits
@db.execute "CREATE TABLE t1(a,b);"
@db.execute "CREATE TABLE t2(c,d);"
10.times do |i|
@db.execute 'INSERT INTO t1 (a, b) VALUES (?, ?)', [i, i.to_s]
@db.execute 'INSERT INTO t2 (c, d) VALUES (?, ?)', [i+1, i.to_s]
end
stmt = @db.prepare("SELECT * FROM t1, t2 WHERE a=c AND b = '1' AND d = '1';")
stmt.execute.to_a

if stmt.stat.key?(:filter_hits)
assert_equal 1, stmt.stat(:filter_hits)
else
assert_raises(ArgumentError, "unknown key: filter_hits") { stmt.stat(:filter_hits) }
end

stmt.close
end

def test_memused
@db.execute 'CREATE TABLE test1(a)'
@db.execute 'INSERT INTO test1 VALUES (1)'
stmt = @db.prepare('select * from test1')

skip("memused not defined") unless stmt.respond_to?(:memused)

stmt.execute.to_a

assert_operator stmt.memused, :>, 0

stmt.close
end
end
end

0 comments on commit 6593767

Please sign in to comment.