From 0e3344880092944bb1171f2ba18fa3e619b2e09d Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Mon, 21 Sep 2026 09:49:43 -0700 Subject: [PATCH 1/2] Freeze @columns and @types on statement --- lib/sqlite3/statement.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/sqlite3/statement.rb b/lib/sqlite3/statement.rb index d322fa65..1af7e6c5 100644 --- a/lib/sqlite3/statement.rb +++ b/lib/sqlite3/statement.rb @@ -198,11 +198,11 @@ def stat key = nil def get_metadata @columns = Array.new(column_count) do |column| column_name column - end + end.freeze @types = Array.new(column_count) do |column| val = column_decltype(column) val&.downcase - end + end.freeze end end end From 72eb0ead14b05fb75024b2431b01048c81fdf1d4 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Mon, 21 Sep 2026 09:50:33 -0700 Subject: [PATCH 2/2] Move type downcase and intern into C We can avoid intermediate allocations by doing the downcasing and interning in C. This also changes the strings to be UTF-8. We only downcase ASCII here because that's what was previously done (as the strings used to be binary. --- ext/sqlite3/extconf.rb | 3 ++- ext/sqlite3/statement.c | 47 +++++++++++++++++++++++++++++++++++++++- lib/sqlite3/statement.rb | 3 +-- test/test_statement.rb | 17 +++++++++++++++ 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/ext/sqlite3/extconf.rb b/ext/sqlite3/extconf.rb index 0c3c40bc..3aea3923 100644 --- a/ext/sqlite3/extconf.rb +++ b/ext/sqlite3/extconf.rb @@ -115,9 +115,10 @@ def configure_extension abort_could_not_find(libname) unless find_library(libname, "sqlite3_libversion_number", "sqlite3.h") - # Truffle Ruby doesn't support this yet: + # Ruby 3.0+, but not on every implementation: # https://github.com/oracle/truffleruby/issues/3408 have_func("rb_enc_interned_str_cstr") + have_func("rb_enc_interned_str") # Functions defined in 1.9 but not 1.8 have_func("rb_proc_arity") diff --git a/ext/sqlite3/statement.c b/ext/sqlite3/statement.c index aac10f1b..318e75b8 100644 --- a/ext/sqlite3/statement.c +++ b/ext/sqlite3/statement.c @@ -413,6 +413,33 @@ interned_utf8_cstr(const char *str) } #endif +/* Like interned_utf8_cstr, but ASCII-downcases as it copies. */ +static VALUE +interned_downcased_utf8_cstr(const char *str) +{ +#if HAVE_RB_ENC_INTERNED_STR + VALUE tmp, ret; + char *buf; + size_t i, len; + + len = strlen(str); + buf = ALLOCV_N(char, tmp, len); + + for (i = 0; i < len; i++) { + unsigned char c = (unsigned char)str[i]; + buf[i] = (char)((c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c); + } + + ret = rb_enc_interned_str(buf, (long)len, rb_utf8_encoding()); + ALLOCV_END(tmp); + return ret; +#else + VALUE rb_str = rb_funcall(rb_utf8_str_new_cstr(str), rb_intern("downcase"), 1, + ID2SYM(rb_intern("ascii"))); + return rb_funcall(rb_str, rb_intern("-@"), 0); +#endif +} + /* call-seq: stmt.column_name(index) * * Get the column name at +index+. 0 based. @@ -455,7 +482,24 @@ column_decltype(VALUE self, VALUE index) name = sqlite3_column_decltype(ctx->st, (int)NUM2INT(index)); - if (name) { return rb_str_new2(name); } + if (name) { return rb_utf8_str_new_cstr(name); } + return Qnil; +} + +static VALUE +column_decltype_downcased(VALUE self, VALUE index) +{ + sqlite3StmtRubyPtr ctx; + const char *name; + + TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); + + REQUIRE_LIVE_DB(ctx); + REQUIRE_OPEN_STMT(ctx); + + name = sqlite3_column_decltype(ctx->st, (int)NUM2INT(index)); + + if (name) { return interned_downcased_utf8_cstr(name); } return Qnil; } @@ -748,6 +792,7 @@ init_sqlite3_statement(void) rb_define_method(cSqlite3Statement, "memused", memused, 0); #endif + rb_define_private_method(cSqlite3Statement, "column_decltype_downcased", column_decltype_downcased, 1); 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 1af7e6c5..f517a39a 100644 --- a/lib/sqlite3/statement.rb +++ b/lib/sqlite3/statement.rb @@ -200,8 +200,7 @@ def get_metadata column_name column end.freeze @types = Array.new(column_count) do |column| - val = column_decltype(column) - val&.downcase + column_decltype_downcased(column) end.freeze end end diff --git a/test/test_statement.rb b/test/test_statement.rb index b273bc4e..bf0a5b2e 100644 --- a/test/test_statement.rb +++ b/test/test_statement.rb @@ -203,6 +203,23 @@ def test_bind_blob end end + def test_decltype_is_utf8 + @db.execute %(create table foo(a integer, b "VARCHÄR(255)")) + @db.prepare("select a, b from foo") do |stmt| + assert_equal ["INTEGER", "VARCHÄR(255)"], [stmt.column_decltype(0), stmt.column_decltype(1)] + assert_equal [Encoding::UTF_8, Encoding::UTF_8], + [stmt.column_decltype(0).encoding, stmt.column_decltype(1).encoding] + end + end + + def test_types_are_utf8_and_downcased_as_ascii + @db.execute %(create table foo(a integer, b "VARCHÄR(255)")) + @db.prepare("select a, b from foo") do |stmt| + assert_equal ["integer", "varchÄr(255)"], stmt.types + assert_equal [Encoding::UTF_8, Encoding::UTF_8], stmt.types.map(&:encoding) + end + end + def test_bind_64 stmt = SQLite3::Statement.new(@db, "select ?") stmt.bind_param(1, 2**31)