From e6412a317fda2104a802591b0fcb259f090170df Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Sun, 7 Jan 2024 20:46:40 +0100 Subject: [PATCH 01/12] Add various methods for the sqlite3_stmt_status interface --- ext/sqlite3/statement.c | 126 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/ext/sqlite3/statement.c b/ext/sqlite3/statement.c index cee106d1..f0c31690 100644 --- a/ext/sqlite3/statement.c +++ b/ext/sqlite3/statement.c @@ -409,6 +409,123 @@ static VALUE bind_parameter_count(VALUE self) return INT2NUM(sqlite3_bind_parameter_count(ctx->st)); } +/* call-seq: stmt.fullscan_steps + * + * Return the number of times that SQLite has stepped forward in a table as part of a full table scan + */ +static VALUE fullscan_steps(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_FULLSCAN_STEP, 0)); +} + +/* call-seq: stmt.sorts + * + * Return the number of sort operations that have occurred + */ +static VALUE sorts(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_SORT, 0)); +} + +/* call-seq: stmt.auto_indexes + * + * Return the number of rows inserted into transient indices that were created automatically in order to help joins run faster + */ +static VALUE auto_indexes(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_AUTOINDEX, 0)); +} + +/* call-seq: stmt.auto_indexes + * + * Return the number of virtual machine operations executed by the prepared statement + */ +static VALUE vm_steps(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_VM_STEP, 0)); +} + +/* call-seq: stmt.auto_indexes + * + * Return 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. + */ +static VALUE re_prepares(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_REPREPARE, 0)); +} + +/* call-seq: stmt.runs + * + * Return the number of times that the prepared statement has been run + */ +static VALUE runs(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_RUN, 0)); +} + +/* call-seq: stmt.filter_misses + * + * Return the number of times that the Bloom filter returned a find, and thus the join step had to be processed as normal. + */ +static VALUE filter_misses(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_FILTER_MISS, 0)); +} + +/* call-seq: stmt.filter_hits + * + * Return the number of times that a join step was bypassed because a Bloom filter returned not-found + */ +static VALUE filter_hits(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_FILTER_HIT, 0)); +} + +/* call-seq: stmt.memory_used + * + * Return the approximate number of bytes of heap memory used to store the prepared statement + */ +static VALUE memory_used(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)); +} + #ifdef HAVE_SQLITE3_COLUMN_DATABASE_NAME /* call-seq: stmt.database_name(column_index) @@ -444,6 +561,15 @@ void 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_method(cSqlite3Statement, "fullscan_steps", fullscan_steps, 0); + rb_define_method(cSqlite3Statement, "sorts", sorts, 0); + rb_define_method(cSqlite3Statement, "auto_indexes", auto_indexes, 0); + rb_define_method(cSqlite3Statement, "vm_steps", vm_steps, 0); + rb_define_method(cSqlite3Statement, "re_prepares", re_prepares, 0); + rb_define_method(cSqlite3Statement, "runs", runs, 0); + rb_define_method(cSqlite3Statement, "filter_misses", filter_misses, 0); + rb_define_method(cSqlite3Statement, "filter_hits", filter_hits, 0); + rb_define_method(cSqlite3Statement, "memory_used", memory_used, 0); #ifdef HAVE_SQLITE3_COLUMN_DATABASE_NAME rb_define_method(cSqlite3Statement, "database_name", database_name, 1); From 98c0ed1a1ee9ea7f1a855749643585b35187e23f Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Sun, 7 Jan 2024 20:46:52 +0100 Subject: [PATCH 02/12] WIP tests for sqlite3_stmt_status methods --- test/test_statement.rb | 107 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/test/test_statement.rb b/test/test_statement.rb index 5e317cb6..13384181 100644 --- a/test/test_statement.rb +++ b/test/test_statement.rb @@ -286,5 +286,112 @@ def test_clear_bindings! stmt.close end + + def test_fullscan_steps + @db.execute 'CREATE TABLE test1(a, b)' + @db.execute 'INSERT INTO test1 VALUES ("hello", "world")' + + stmt = @db.prepare('select * from test1 where b like "%orld"') + p stmt.execute.to_a + assert_equal 3, stmt.fullscan_steps + ensure + stmt.close + end + + def test_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.sorts + ensure + stmt.close + end + + def test_auto_indexes + @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.auto_indexes + ensure + stmt.close + end + + def test_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_equal 1, stmt.vm_steps + ensure + stmt.close + end + + def test_re_prepares + @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.re_prepares + ensure + stmt.close + end + + def test_runs + @db.execute 'CREATE TABLE test1(a)' + @db.execute 'INSERT INTO test1 VALUES (1)' + + stmt = @db.prepare('select * from test1') + stmt.execute.to_a + + assert_equal 1, stmt.runs + ensure + stmt.close + end + + def test_filter_misses + @db.execute 'CREATE TABLE test1(a)' + @db.execute 'INSERT INTO test1 VALUES (1)' + + stmt = @db.prepare('select * from test1') + stmt.execute.to_a + + assert_equal 1, stmt.filter_misses + ensure + stmt.close + end + + def test_filter_hits + @db.execute 'CREATE TABLE test1(a)' + @db.execute 'INSERT INTO test1 VALUES (1)' + + stmt = @db.prepare('select * from test1') + stmt.execute.to_a + + assert_equal 1, stmt.filter_hits + ensure + stmt.close + end + + def test_memory_used + @db.execute 'CREATE TABLE test1(a)' + @db.execute 'INSERT INTO test1 VALUES (1)' + + stmt = @db.prepare('select * from test1') + stmt.execute.to_a + + assert_equal 1, stmt.memory_used + ensure + stmt.close + end end end From b82388acce26ab04184827a0c66613ffe11ef9d8 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Mon, 8 Jan 2024 11:12:16 +0100 Subject: [PATCH 03/12] Get more tests passing --- test/test_statement.rb | 67 ++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/test/test_statement.rb b/test/test_statement.rb index 13384181..3ea2d30b 100644 --- a/test/test_statement.rb +++ b/test/test_statement.rb @@ -288,12 +288,15 @@ def test_clear_bindings! end def test_fullscan_steps - @db.execute 'CREATE TABLE test1(a, b)' - @db.execute 'INSERT INTO test1 VALUES ("hello", "world")' + @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 test1 where b like "%orld"') - p stmt.execute.to_a - assert_equal 3, stmt.fullscan_steps + stmt = @db.prepare("SELECT * FROM test_table WHERE name LIKE 'name%'") + stmt.execute.to_a + assert_equal 9, stmt.fullscan_steps ensure stmt.close end @@ -311,15 +314,18 @@ def test_sorts end def test_auto_indexes - @db.execute 'CREATE TABLE test1(a)' - @db.execute 'INSERT INTO test1 VALUES (1)' + @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 test1 order by a') + stmt = @db.prepare("SELECT * FROM t1, t2 WHERE a=c;") stmt.execute.to_a - - assert_equal 1, stmt.auto_indexes + assert_equal 9, stmt.auto_indexes ensure - stmt.close + stmt.close if stmt end def test_vm_steps @@ -329,21 +335,23 @@ def test_vm_steps stmt = @db.prepare('select * from test1 order by a') stmt.execute.to_a - assert_equal 1, stmt.vm_steps + assert_equal 17, stmt.vm_steps ensure stmt.close end def test_re_prepares - @db.execute 'CREATE TABLE test1(a)' - @db.execute 'INSERT INTO test1 VALUES (1)' + @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 test1 order by a') - stmt.execute.to_a + stmt = @db.prepare("SELECT * FROM test_table WHERE name LIKE ?") + stmt.execute('name%').to_a assert_equal 1, stmt.re_prepares ensure - stmt.close + stmt.close if stmt end def test_runs @@ -359,22 +367,29 @@ def test_runs end def test_filter_misses - @db.execute 'CREATE TABLE test1(a)' - @db.execute 'INSERT INTO test1 VALUES (1)' - - stmt = @db.prepare('select * from test1') + @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 1, stmt.filter_misses + assert_equal 10, stmt.filter_misses ensure stmt.close end def test_filter_hits - @db.execute 'CREATE TABLE test1(a)' - @db.execute 'INSERT INTO test1 VALUES (1)' + @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 test1') + stmt = @db.prepare("SELECT * FROM t1, t2 WHERE a=c AND b = '1' AND d = '1';") stmt.execute.to_a assert_equal 1, stmt.filter_hits @@ -389,7 +404,7 @@ def test_memory_used stmt = @db.prepare('select * from test1') stmt.execute.to_a - assert_equal 1, stmt.memory_used + assert_equal 2784, stmt.memory_used ensure stmt.close end From fea9321cdd48bfec89f17cfc721e45a8d5287dfa Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Mon, 8 Jan 2024 12:09:04 +0100 Subject: [PATCH 04/12] Rename methods to match SQLite constants and fix tests --- ext/sqlite3/statement.c | 18 +++++++++--------- test/test_statement.rb | 28 ++++++++++++++-------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/ext/sqlite3/statement.c b/ext/sqlite3/statement.c index f0c31690..ff37a680 100644 --- a/ext/sqlite3/statement.c +++ b/ext/sqlite3/statement.c @@ -435,11 +435,11 @@ static VALUE sorts(VALUE self) return INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_SORT, 0)); } -/* call-seq: stmt.auto_indexes +/* call-seq: stmt.autoindexes * * Return the number of rows inserted into transient indices that were created automatically in order to help joins run faster */ -static VALUE auto_indexes(VALUE self) +static VALUE autoindexes(VALUE self) { sqlite3StmtRubyPtr ctx; TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); @@ -448,7 +448,7 @@ static VALUE auto_indexes(VALUE self) return INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_AUTOINDEX, 0)); } -/* call-seq: stmt.auto_indexes +/* call-seq: stmt.vm_steps * * Return the number of virtual machine operations executed by the prepared statement */ @@ -461,11 +461,11 @@ static VALUE vm_steps(VALUE self) return INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_VM_STEP, 0)); } -/* call-seq: stmt.auto_indexes +/* call-seq: stmt.reprepares * * Return 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. */ -static VALUE re_prepares(VALUE self) +static VALUE reprepares(VALUE self) { sqlite3StmtRubyPtr ctx; TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); @@ -517,7 +517,7 @@ static VALUE filter_hits(VALUE self) * * Return the approximate number of bytes of heap memory used to store the prepared statement */ -static VALUE memory_used(VALUE self) +static VALUE memused(VALUE self) { sqlite3StmtRubyPtr ctx; TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); @@ -563,13 +563,13 @@ void init_sqlite3_statement(void) rb_define_method(cSqlite3Statement, "bind_parameter_count", bind_parameter_count, 0); rb_define_method(cSqlite3Statement, "fullscan_steps", fullscan_steps, 0); rb_define_method(cSqlite3Statement, "sorts", sorts, 0); - rb_define_method(cSqlite3Statement, "auto_indexes", auto_indexes, 0); + rb_define_method(cSqlite3Statement, "autoindexes", autoindexes, 0); rb_define_method(cSqlite3Statement, "vm_steps", vm_steps, 0); - rb_define_method(cSqlite3Statement, "re_prepares", re_prepares, 0); + rb_define_method(cSqlite3Statement, "reprepares", reprepares, 0); rb_define_method(cSqlite3Statement, "runs", runs, 0); rb_define_method(cSqlite3Statement, "filter_misses", filter_misses, 0); rb_define_method(cSqlite3Statement, "filter_hits", filter_hits, 0); - rb_define_method(cSqlite3Statement, "memory_used", memory_used, 0); + rb_define_method(cSqlite3Statement, "memused", memused, 0); #ifdef HAVE_SQLITE3_COLUMN_DATABASE_NAME rb_define_method(cSqlite3Statement, "database_name", database_name, 1); diff --git a/test/test_statement.rb b/test/test_statement.rb index 3ea2d30b..44053a18 100644 --- a/test/test_statement.rb +++ b/test/test_statement.rb @@ -298,7 +298,7 @@ def test_fullscan_steps stmt.execute.to_a assert_equal 9, stmt.fullscan_steps ensure - stmt.close + stmt.close if stmt end def test_sorts @@ -310,10 +310,10 @@ def test_sorts assert_equal 1, stmt.sorts ensure - stmt.close + stmt.close if stmt end - def test_auto_indexes + def test_autoindexes @db.execute "CREATE TABLE t1(a,b);" @db.execute "CREATE TABLE t2(c,d);" 10.times do |i| @@ -323,7 +323,7 @@ def test_auto_indexes stmt = @db.prepare("SELECT * FROM t1, t2 WHERE a=c;") stmt.execute.to_a - assert_equal 9, stmt.auto_indexes + assert_equal 9, stmt.autoindexes ensure stmt.close if stmt end @@ -337,10 +337,10 @@ def test_vm_steps assert_equal 17, stmt.vm_steps ensure - stmt.close + stmt.close if stmt end - def test_re_prepares + def test_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}" @@ -349,7 +349,7 @@ def test_re_prepares stmt = @db.prepare("SELECT * FROM test_table WHERE name LIKE ?") stmt.execute('name%').to_a - assert_equal 1, stmt.re_prepares + assert_equal 1, stmt.reprepares ensure stmt.close if stmt end @@ -363,7 +363,7 @@ def test_runs assert_equal 1, stmt.runs ensure - stmt.close + stmt.close if stmt end def test_filter_misses @@ -378,7 +378,7 @@ def test_filter_misses assert_equal 10, stmt.filter_misses ensure - stmt.close + stmt.close if stmt end def test_filter_hits @@ -386,7 +386,7 @@ def test_filter_hits @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] + @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';") @@ -394,19 +394,19 @@ def test_filter_hits assert_equal 1, stmt.filter_hits ensure - stmt.close + stmt.close if stmt end - def test_memory_used + def test_memused @db.execute 'CREATE TABLE test1(a)' @db.execute 'INSERT INTO test1 VALUES (1)' stmt = @db.prepare('select * from test1') stmt.execute.to_a - assert_equal 2784, stmt.memory_used + assert_equal 2784, stmt.memused ensure - stmt.close + stmt.close if stmt end end end From 5a9a812177e1bff796507da7503783fa385210ce Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Tue, 9 Jan 2024 23:52:55 +0100 Subject: [PATCH 05/12] Make the memused test more resilient, as different OSes use different amounts of memory --- test/test_statement.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_statement.rb b/test/test_statement.rb index 44053a18..8df53694 100644 --- a/test/test_statement.rb +++ b/test/test_statement.rb @@ -404,7 +404,7 @@ def test_memused stmt = @db.prepare('select * from test1') stmt.execute.to_a - assert_equal 2784, stmt.memused + assert_operator stmt.memused, :>, 0 ensure stmt.close if stmt end From 24b6b89e6df91febd9e09c55750c0638f0d42a53 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Wed, 10 Jan 2024 00:00:53 +0100 Subject: [PATCH 06/12] Make certain functions optionally defined based on whether or not the SQLite constant is defined --- ext/sqlite3/statement.c | 60 ++++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/ext/sqlite3/statement.c b/ext/sqlite3/statement.c index e462ca58..972bb02c 100644 --- a/ext/sqlite3/statement.c +++ b/ext/sqlite3/statement.c @@ -415,7 +415,8 @@ bind_parameter_count(VALUE self) * * Return the number of times that SQLite has stepped forward in a table as part of a full table scan */ -static VALUE fullscan_steps(VALUE self) +static VALUE +fullscan_steps(VALUE self) { sqlite3StmtRubyPtr ctx; TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); @@ -428,7 +429,8 @@ static VALUE fullscan_steps(VALUE self) * * Return the number of sort operations that have occurred */ -static VALUE sorts(VALUE self) +static VALUE +sorts(VALUE self) { sqlite3StmtRubyPtr ctx; TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); @@ -441,7 +443,8 @@ static VALUE sorts(VALUE self) * * Return the number of rows inserted into transient indices that were created automatically in order to help joins run faster */ -static VALUE autoindexes(VALUE self) +static VALUE +autoindexes(VALUE self) { sqlite3StmtRubyPtr ctx; TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); @@ -454,7 +457,8 @@ static VALUE autoindexes(VALUE self) * * Return the number of virtual machine operations executed by the prepared statement */ -static VALUE vm_steps(VALUE self) +static VALUE +vm_steps(VALUE self) { sqlite3StmtRubyPtr ctx; TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); @@ -463,11 +467,13 @@ static VALUE vm_steps(VALUE self) return INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_VM_STEP, 0)); } +#ifdef SQLITE_STMTSTATUS_REPREPARE /* call-seq: stmt.reprepares * * Return 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. */ -static VALUE reprepares(VALUE self) +static VALUE +reprepares(VALUE self) { sqlite3StmtRubyPtr ctx; TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); @@ -475,12 +481,15 @@ static VALUE reprepares(VALUE self) return INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_REPREPARE, 0)); } +#endif +#ifdef SQLITE_STMTSTATUS_RUN /* call-seq: stmt.runs * * Return the number of times that the prepared statement has been run */ -static VALUE runs(VALUE self) +static VALUE +runs(VALUE self) { sqlite3StmtRubyPtr ctx; TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); @@ -488,12 +497,15 @@ static VALUE runs(VALUE self) return INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_RUN, 0)); } +#endif +#ifdef SQLITE_STMTSTATUS_FILTER_MISS /* call-seq: stmt.filter_misses * * Return the number of times that the Bloom filter returned a find, and thus the join step had to be processed as normal. */ -static VALUE filter_misses(VALUE self) +static VALUE +filter_misses(VALUE self) { sqlite3StmtRubyPtr ctx; TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); @@ -501,12 +513,15 @@ static VALUE filter_misses(VALUE self) return INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_FILTER_MISS, 0)); } +#endif +#ifdef SQLITE_STMTSTATUS_FILTER_HIT /* call-seq: stmt.filter_hits * * Return the number of times that a join step was bypassed because a Bloom filter returned not-found */ -static VALUE filter_hits(VALUE self) +static VALUE +filter_hits(VALUE self) { sqlite3StmtRubyPtr ctx; TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); @@ -514,12 +529,15 @@ static VALUE filter_hits(VALUE self) return INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_FILTER_HIT, 0)); } +#endif +#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) +static VALUE +memused(VALUE self) { sqlite3StmtRubyPtr ctx; TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); @@ -527,6 +545,7 @@ static VALUE memused(VALUE self) return INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_MEMUSED, 0)); } +#endif #ifdef HAVE_SQLITE3_COLUMN_DATABASE_NAME @@ -568,14 +587,29 @@ init_sqlite3_statement(void) rb_define_method(cSqlite3Statement, "sorts", sorts, 0); rb_define_method(cSqlite3Statement, "autoindexes", autoindexes, 0); rb_define_method(cSqlite3Statement, "vm_steps", vm_steps, 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_REPREPARE rb_define_method(cSqlite3Statement, "reprepares", reprepares, 0); +#endif + +#ifdef SQLITE_STMTSTATUS_RUN rb_define_method(cSqlite3Statement, "runs", runs, 0); +#endif + +#ifdef SQLITE_STMTSTATUS_FILTER_MISS rb_define_method(cSqlite3Statement, "filter_misses", filter_misses, 0); +#endif + +#ifdef SQLITE_STMTSTATUS_FILTER_HIT rb_define_method(cSqlite3Statement, "filter_hits", filter_hits, 0); - rb_define_method(cSqlite3Statement, "memused", memused, 0); - rb_define_private_method(cSqlite3Statement, "prepare", prepare, 2); +#endif -#ifdef HAVE_SQLITE3_COLUMN_DATABASE_NAME - rb_define_method(cSqlite3Statement, "database_name", database_name, 1); +#ifdef SQLITE_STMTSTATUS_MEMUSED + rb_define_method(cSqlite3Statement, "memused", memused, 0); #endif } From 0ccc5e70e94da4a47c4d765a339d3975c437fb6c Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Wed, 10 Jan 2024 00:06:30 +0100 Subject: [PATCH 07/12] Skip tests for undefined methods --- test/test_statement.rb | 71 ++++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/test/test_statement.rb b/test/test_statement.rb index 8df53694..780e5a25 100644 --- a/test/test_statement.rb +++ b/test/test_statement.rb @@ -293,24 +293,23 @@ def test_fullscan_steps @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.fullscan_steps - ensure - stmt.close if stmt + + stmt.close end def test_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.sorts - ensure - stmt.close if stmt + + stmt.close end def test_autoindexes @@ -320,93 +319,103 @@ def test_autoindexes @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.autoindexes - ensure - stmt.close if stmt + + stmt.close end def test_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_equal 17, stmt.vm_steps - ensure - stmt.close if stmt + + stmt.close end def test_reprepares + stmt = @db.prepare("SELECT * FROM test_table WHERE name LIKE ?") + + skip("reprepares not defined") unless stmt.respond_to?(: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 assert_equal 1, stmt.reprepares - ensure - stmt.close if stmt + + stmt.close end def test_runs + stmt = @db.prepare('select * from test1') + + skip("runs not defined") unless stmt.respond_to?(:runs) + @db.execute 'CREATE TABLE test1(a)' @db.execute 'INSERT INTO test1 VALUES (1)' - - stmt = @db.prepare('select * from test1') stmt.execute.to_a assert_equal 1, stmt.runs - ensure - stmt.close if stmt + + stmt.close end def test_filter_misses + stmt = @db.prepare("SELECT * FROM t1, t2 WHERE a=c;") + + skip("filter_misses not defined") unless stmt.respond_to?(: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 assert_equal 10, stmt.filter_misses - ensure - stmt.close if stmt + + stmt.close end def test_filter_hits + stmt = @db.prepare("SELECT * FROM t1, t2 WHERE a=c AND b = '1' AND d = '1';") + + skip("filter_hits not defined") unless stmt.respond_to?(: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 assert_equal 1, stmt.filter_hits - ensure - stmt.close if stmt + + stmt.close end def test_memused + stmt = @db.prepare('select * from test1') + + skip("memused not defined") unless stmt.respond_to?(:memused) + @db.execute 'CREATE TABLE test1(a)' @db.execute 'INSERT INTO test1 VALUES (1)' - - stmt = @db.prepare('select * from test1') stmt.execute.to_a assert_operator stmt.memused, :>, 0 - ensure - stmt.close if stmt + + stmt.close end end end From 701b6b8de439cd6eddea6a094c5b61387f8bc5f2 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Wed, 10 Jan 2024 00:12:14 +0100 Subject: [PATCH 08/12] Fix test skipping --- test/test_statement.rb | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/test/test_statement.rb b/test/test_statement.rb index 780e5a25..7e1a61dd 100644 --- a/test/test_statement.rb +++ b/test/test_statement.rb @@ -339,14 +339,14 @@ def test_vm_steps end def test_reprepares - stmt = @db.prepare("SELECT * FROM test_table WHERE name LIKE ?") - - skip("reprepares not defined") unless stmt.respond_to?(: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 ?") + + skip("reprepares not defined") unless stmt.respond_to?(:reprepares) + stmt.execute('name%').to_a assert_equal 1, stmt.reprepares @@ -355,12 +355,12 @@ def test_reprepares end def test_runs + @db.execute 'CREATE TABLE test1(a)' + @db.execute 'INSERT INTO test1 VALUES (1)' stmt = @db.prepare('select * from test1') skip("runs not defined") unless stmt.respond_to?(:runs) - @db.execute 'CREATE TABLE test1(a)' - @db.execute 'INSERT INTO test1 VALUES (1)' stmt.execute.to_a assert_equal 1, stmt.runs @@ -369,16 +369,16 @@ def test_runs end def test_filter_misses - stmt = @db.prepare("SELECT * FROM t1, t2 WHERE a=c;") - - skip("filter_misses not defined") unless stmt.respond_to?(: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;") + + skip("filter_misses not defined") unless stmt.respond_to?(:filter_misses) + stmt.execute.to_a assert_equal 10, stmt.filter_misses @@ -387,16 +387,16 @@ def test_filter_misses end def test_filter_hits - stmt = @db.prepare("SELECT * FROM t1, t2 WHERE a=c AND b = '1' AND d = '1';") - - skip("filter_hits not defined") unless stmt.respond_to?(: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';") + + skip("filter_hits not defined") unless stmt.respond_to?(:filter_hits) + stmt.execute.to_a assert_equal 1, stmt.filter_hits @@ -405,12 +405,12 @@ def test_filter_hits 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) - @db.execute 'CREATE TABLE test1(a)' - @db.execute 'INSERT INTO test1 VALUES (1)' stmt.execute.to_a assert_operator stmt.memused, :>, 0 From 17e66195a4a085e7a294c84b36db60dec04114e8 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Wed, 10 Jan 2024 00:15:04 +0100 Subject: [PATCH 09/12] Make the vm_steps test more resilient to various OSes --- test/test_statement.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_statement.rb b/test/test_statement.rb index 7e1a61dd..646fee1f 100644 --- a/test/test_statement.rb +++ b/test/test_statement.rb @@ -333,7 +333,7 @@ def test_vm_steps stmt = @db.prepare('select * from test1 order by a') stmt.execute.to_a - assert_equal 17, stmt.vm_steps + assert_operator stmt.vm_steps, :>, 0 stmt.close end From b9bbeaa7f1e2df83bf0694d096e0ae41a7262761 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Thu, 11 Jan 2024 14:04:56 +0100 Subject: [PATCH 10/12] Replace separate methods with a single Statement#stat method that returns a hash Works like GC.stat, and can also take a symbol to fetch only one stat --- ext/sqlite3/statement.c | 217 ++++++++++++++++++--------------------- lib/sqlite3/statement.rb | 23 +++++ test/test_statement.rb | 64 +++++++----- 3 files changed, 158 insertions(+), 146 deletions(-) diff --git a/ext/sqlite3/statement.c b/ext/sqlite3/statement.c index 972bb02c..cf93f5a6 100644 --- a/ext/sqlite3/statement.c +++ b/ext/sqlite3/statement.c @@ -411,125 +411,129 @@ bind_parameter_count(VALUE self) return INT2NUM(sqlite3_bind_parameter_count(ctx->st)); } -/* call-seq: stmt.fullscan_steps - * - * Return the number of times that SQLite has stepped forward in a table as part of a full table scan - */ -static VALUE -fullscan_steps(VALUE self) -{ - sqlite3StmtRubyPtr ctx; - TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); - REQUIRE_OPEN_STMT(ctx); +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 +}; - return INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_FULLSCAN_STEP, 0)); -} +static VALUE stmt_stat_symbols[stmt_stat_sym_last]; -/* call-seq: stmt.sorts - * - * Return the number of sort operations that have occurred - */ -static VALUE -sorts(VALUE self) +static void +setup_stmt_stat_symbols(void) { - sqlite3StmtRubyPtr ctx; - TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); - REQUIRE_OPEN_STMT(ctx); - - return INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_SORT, 0)); + 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 + } } -/* call-seq: stmt.autoindexes - * - * Return the number of rows inserted into transient indices that were created automatically in order to help joins run faster - */ -static VALUE -autoindexes(VALUE self) +static size_t +stmt_stat_internal(VALUE hash_or_sym, sqlite3_stmt *stmt) { - sqlite3StmtRubyPtr ctx; - TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); - REQUIRE_OPEN_STMT(ctx); + VALUE hash = Qnil, key = Qnil; - return INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_AUTOINDEX, 0)); -} + setup_stmt_stat_symbols(); -/* call-seq: stmt.vm_steps - * - * Return the number of virtual machine operations executed by the prepared statement - */ -static VALUE -vm_steps(VALUE self) -{ - sqlite3StmtRubyPtr ctx; - TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); - REQUIRE_OPEN_STMT(ctx); + 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"); + } - return INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_VM_STEP, 0)); -} +#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 -/* call-seq: stmt.reprepares - * - * Return 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. - */ -static VALUE -reprepares(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_REPREPARE, 0)); -} + SET(reprepares, SQLITE_STMTSTATUS_REPREPARE); #endif - #ifdef SQLITE_STMTSTATUS_RUN -/* call-seq: stmt.runs - * - * Return the number of times that the prepared statement has been run - */ -static VALUE -runs(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_RUN, 0)); -} + SET(runs, SQLITE_STMTSTATUS_RUN); #endif - #ifdef SQLITE_STMTSTATUS_FILTER_MISS -/* call-seq: stmt.filter_misses - * - * Return the number of times that the Bloom filter returned a find, and thus the join step had to be processed as normal. - */ -static VALUE -filter_misses(VALUE self) -{ - sqlite3StmtRubyPtr ctx; - TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); - REQUIRE_OPEN_STMT(ctx); + 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 INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_FILTER_MISS, 0)); + return 0; } -#endif -#ifdef SQLITE_STMTSTATUS_FILTER_HIT -/* call-seq: stmt.filter_hits +/* call-seq: stmt.stmt_stat(hash_or_key) * - * Return the number of times that a join step was bypassed because a Bloom filter returned not-found + * Returns a Hash containing information about the statement. */ static VALUE -filter_hits(VALUE self) +stmt_stat(VALUE self, VALUE arg) // arg is (nil || hash || symbol) { sqlite3StmtRubyPtr ctx; TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); REQUIRE_OPEN_STMT(ctx); - return INT2NUM(sqlite3_stmt_status(ctx->st, SQLITE_STMTSTATUS_FILTER_HIT, 0)); + if (NIL_P(arg)) { + arg = rb_hash_new(); + } + else if (SYMBOL_P(arg)) { + size_t value = stmt_stat_internal(arg, ctx->st); + return SIZET2NUM(value); + } + else if (RB_TYPE_P(arg, T_HASH)) { + // ok + } + else { + rb_raise(rb_eTypeError, "non-hash or symbol given"); + } + + stmt_stat_internal(arg, ctx->st); + return arg; } -#endif #ifdef SQLITE_STMTSTATUS_MEMUSED /* call-seq: stmt.memory_used @@ -583,33 +587,10 @@ 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_method(cSqlite3Statement, "fullscan_steps", fullscan_steps, 0); - rb_define_method(cSqlite3Statement, "sorts", sorts, 0); - rb_define_method(cSqlite3Statement, "autoindexes", autoindexes, 0); - rb_define_method(cSqlite3Statement, "vm_steps", vm_steps, 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_REPREPARE - rb_define_method(cSqlite3Statement, "reprepares", reprepares, 0); -#endif - -#ifdef SQLITE_STMTSTATUS_RUN - rb_define_method(cSqlite3Statement, "runs", runs, 0); -#endif - -#ifdef SQLITE_STMTSTATUS_FILTER_MISS - rb_define_method(cSqlite3Statement, "filter_misses", filter_misses, 0); -#endif - -#ifdef SQLITE_STMTSTATUS_FILTER_HIT - rb_define_method(cSqlite3Statement, "filter_hits", filter_hits, 0); -#endif - + rb_define_method(cSqlite3Statement, "stmt_stat", stmt_stat, 1); #ifdef SQLITE_STMTSTATUS_MEMUSED rb_define_method(cSqlite3Statement, "memused", memused, 0); #endif + + rb_define_private_method(cSqlite3Statement, "prepare", prepare, 2); } diff --git a/lib/sqlite3/statement.rb b/lib/sqlite3/statement.rb index 0692b81f..6c99d1b1 100644 --- a/lib/sqlite3/statement.rb +++ b/lib/sqlite3/statement.rb @@ -145,6 +145,29 @@ 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 hash_or_key = nil + stmt_stat hash_or_key + 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 diff --git a/test/test_statement.rb b/test/test_statement.rb index 646fee1f..273ef1ab 100644 --- a/test/test_statement.rb +++ b/test/test_statement.rb @@ -287,7 +287,11 @@ def test_clear_bindings! stmt.close end - def test_fullscan_steps + 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}" @@ -296,23 +300,23 @@ def test_fullscan_steps stmt = @db.prepare("SELECT * FROM test_table WHERE name LIKE 'name%'") stmt.execute.to_a - assert_equal 9, stmt.fullscan_steps + assert_equal 9, stmt.stat(:fullscan_steps) stmt.close end - def test_sorts + 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.sorts + assert_equal 1, stmt.stat(:sorts) stmt.close end - def test_autoindexes + def test_stat_autoindexes @db.execute "CREATE TABLE t1(a,b);" @db.execute "CREATE TABLE t2(c,d);" 10.times do |i| @@ -322,53 +326,55 @@ def test_autoindexes stmt = @db.prepare("SELECT * FROM t1, t2 WHERE a=c;") stmt.execute.to_a - assert_equal 9, stmt.autoindexes + assert_equal 9, stmt.stat(:autoindexes) stmt.close end - def test_vm_steps + 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.vm_steps, :>, 0 + assert_operator stmt.stat(:vm_steps), :>, 0 stmt.close end - def test_reprepares + 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 ?") - - skip("reprepares not defined") unless stmt.respond_to?(:reprepares) - stmt.execute('name%').to_a - assert_equal 1, stmt.reprepares + 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_runs + def test_stat_runs @db.execute 'CREATE TABLE test1(a)' @db.execute 'INSERT INTO test1 VALUES (1)' stmt = @db.prepare('select * from test1') - - skip("runs not defined") unless stmt.respond_to?(:runs) - stmt.execute.to_a - assert_equal 1, stmt.runs + 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_filter_misses + def test_stat_filter_misses @db.execute "CREATE TABLE t1(a,b);" @db.execute "CREATE TABLE t2(c,d);" 10.times do |i| @@ -376,17 +382,18 @@ def test_filter_misses @db.execute 'INSERT INTO t2 (c, d) VALUES (?, ?)', [i, i.to_s] end stmt = @db.prepare("SELECT * FROM t1, t2 WHERE a=c;") - - skip("filter_misses not defined") unless stmt.respond_to?(:filter_misses) - stmt.execute.to_a - assert_equal 10, stmt.filter_misses + 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_filter_hits + def test_stat_filter_hits @db.execute "CREATE TABLE t1(a,b);" @db.execute "CREATE TABLE t2(c,d);" 10.times do |i| @@ -394,12 +401,13 @@ def test_filter_hits @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';") - - skip("filter_hits not defined") unless stmt.respond_to?(:filter_hits) - stmt.execute.to_a - assert_equal 1, stmt.filter_hits + 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 From 089d00eb10b8ddf52f3ae431b340083e94441abb Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Thu, 11 Jan 2024 21:56:59 +0100 Subject: [PATCH 11/12] Implement separate, private stats_as_hash and stat_for methods --- ext/sqlite3/statement.c | 36 ++++++++++++++++++++++-------------- lib/sqlite3/statement.rb | 8 ++++++-- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/ext/sqlite3/statement.c b/ext/sqlite3/statement.c index cf93f5a6..603e4e88 100644 --- a/ext/sqlite3/statement.c +++ b/ext/sqlite3/statement.c @@ -506,33 +506,40 @@ stmt_stat_internal(VALUE hash_or_sym, sqlite3_stmt *stmt) 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 -stmt_stat(VALUE self, VALUE arg) // arg is (nil || hash || symbol) +stat_for(VALUE self, VALUE key) { sqlite3StmtRubyPtr ctx; TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); REQUIRE_OPEN_STMT(ctx); - if (NIL_P(arg)) { - arg = rb_hash_new(); - } - else if (SYMBOL_P(arg)) { - size_t value = stmt_stat_internal(arg, ctx->st); + if (SYMBOL_P(key)) { + size_t value = stmt_stat_internal(key, ctx->st); return SIZET2NUM(value); } - else if (RB_TYPE_P(arg, T_HASH)) { - // ok - } else { - rb_raise(rb_eTypeError, "non-hash or symbol given"); + rb_raise(rb_eTypeError, "non-symbol given"); } - - stmt_stat_internal(arg, ctx->st); - return arg; } #ifdef SQLITE_STMTSTATUS_MEMUSED @@ -587,10 +594,11 @@ 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_method(cSqlite3Statement, "stmt_stat", stmt_stat, 1); #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); } diff --git a/lib/sqlite3/statement.rb b/lib/sqlite3/statement.rb index 6c99d1b1..0f471fc5 100644 --- a/lib/sqlite3/statement.rb +++ b/lib/sqlite3/statement.rb @@ -164,8 +164,12 @@ def must_be_open! # :nodoc: # 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 hash_or_key = nil - stmt_stat hash_or_key + def stat key = nil + if key + stat_for(key) + else + stats_as_hash + end end private From 3dc5fb81d8b504b46ce7f1c3145294401b39be2a Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Thu, 18 Jan 2024 09:31:59 +0100 Subject: [PATCH 12/12] Update statement.c --- ext/sqlite3/statement.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ext/sqlite3/statement.c b/ext/sqlite3/statement.c index 603e4e88..9411f546 100644 --- a/ext/sqlite3/statement.c +++ b/ext/sqlite3/statement.c @@ -594,6 +594,9 @@ 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); +#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