Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement sqlite3_stmt_status interface #461

Merged
merged 13 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
156 changes: 154 additions & 2 deletions ext/sqlite3/statement.c
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,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 @@ -447,9 +594,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
# that this will actually execute the SQL, which means it can be a
Expand Down
139 changes: 139 additions & 0 deletions test/test_statement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -286,5 +286,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