From b6d0089ff1d470fc258a8460460379e404c162a9 Mon Sep 17 00:00:00 2001 From: Nemo Date: Fri, 22 Aug 2025 11:30:53 +0530 Subject: [PATCH 1/3] Implements stmt.named_params - Closes #627 --- ext/sqlite3/statement.c | 31 +++++++++++++++++++++++++++++++ test/test_statement.rb | 8 ++++++++ 2 files changed, 39 insertions(+) diff --git a/ext/sqlite3/statement.c b/ext/sqlite3/statement.c index 9dedcd2d..57e6f111 100644 --- a/ext/sqlite3/statement.c +++ b/ext/sqlite3/statement.c @@ -460,6 +460,36 @@ bind_parameter_count(VALUE self) return INT2NUM(sqlite3_bind_parameter_count(ctx->st)); } +/** call-seq: stmt.named_params + * + * Return the list of named parameters in the statement. + */ +static VALUE +named_params(VALUE self) +{ + sqlite3StmtRubyPtr ctx; + TypedData_Get_Struct(self, sqlite3StmtRuby, &statement_type, ctx); + + REQUIRE_LIVE_DB(ctx); + REQUIRE_OPEN_STMT(ctx); + + int param_count = sqlite3_bind_parameter_count(ctx->st); + VALUE params = rb_ary_new2(param_count); + + // The first host parameter has an index of 1, not 0. + for (int i = 1; i <= param_count; i++) { + const char *name = sqlite3_bind_parameter_name(ctx->st, i); + // If parameters of the ?NNN form are used, there may be gaps in the list. + if (name) { + VALUE rb_name = interned_utf8_cstr(name); + // The initial ":" or "$" or "@" or "?" is included as part of the name. + rb_name = rb_str_substr(rb_name, 1, RSTRING_LEN(rb_name) - 1); + rb_ary_push(params, rb_name); + } + } + return rb_obj_freeze(params); +} + enum stmt_stat_sym { stmt_stat_sym_fullscan_steps, stmt_stat_sym_sorts, @@ -689,6 +719,7 @@ 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, "named_params", named_params, 0); rb_define_method(cSqlite3Statement, "sql", get_sql, 0); rb_define_method(cSqlite3Statement, "expanded_sql", get_expanded_sql, 0); #ifdef HAVE_SQLITE3_COLUMN_DATABASE_NAME diff --git a/test/test_statement.rb b/test/test_statement.rb index b6a55001..c02a4fcc 100644 --- a/test/test_statement.rb +++ b/test/test_statement.rb @@ -256,6 +256,14 @@ def test_named_bind_not_found stmt.close end + def test_named_params + assert_equal [], @stmt.named_params + + stmt = SQLite3::Statement.new(@db, "select :foo, $bar, @zed") + assert_equal ["foo", "bar", "zed"], stmt.named_params + stmt.close + end + def test_each r = nil @stmt.each do |row| From c73faa9b2d0608a6579de5eb9cbb0b0f8197dc95 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sun, 14 Sep 2025 13:42:21 -0400 Subject: [PATCH 2/3] style(rubocop): fix assertion --- 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 c02a4fcc..1fb9ac70 100644 --- a/test/test_statement.rb +++ b/test/test_statement.rb @@ -257,7 +257,7 @@ def test_named_bind_not_found end def test_named_params - assert_equal [], @stmt.named_params + assert_empty @stmt.named_params stmt = SQLite3::Statement.new(@db, "select :foo, $bar, @zed") assert_equal ["foo", "bar", "zed"], stmt.named_params From 969b6f197715a4b1ea98a44cd02427d69dc12d68 Mon Sep 17 00:00:00 2001 From: Nemo Date: Wed, 17 Sep 2025 19:29:40 +0530 Subject: [PATCH 3/3] named_params: Ignore numeric params numeric unused params are undistinguishable from numeric bindable parameters. So a simple loop from 1-bind_parameter_count is the best you can do. This means .named_params can be focused on the truly named parameters only, which is what This commit does by dropping numeric parameters --- FAQ.md | 5 +++++ ext/sqlite3/statement.c | 22 +++++++++++++++------- test/test_statement.rb | 6 ++---- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/FAQ.md b/FAQ.md index eb43875b..b55ff35c 100644 --- a/FAQ.md +++ b/FAQ.md @@ -122,6 +122,11 @@ Placeholders in an SQL statement take any of the following formats: * `?` * `?_nnn_` * `:_word_` +* `:_nnn_` +* `$_word_` +* `$_nnn_` +* `@_word_` +* `@_nnn_` Where _n_ is an integer, and _word_ is an alpha-numeric identifier (or diff --git a/ext/sqlite3/statement.c b/ext/sqlite3/statement.c index 57e6f111..81c5f8c6 100644 --- a/ext/sqlite3/statement.c +++ b/ext/sqlite3/statement.c @@ -460,9 +460,14 @@ bind_parameter_count(VALUE self) return INT2NUM(sqlite3_bind_parameter_count(ctx->st)); } -/** call-seq: stmt.named_params +/** call-seq: stmt.params + * + * Return the list of named alphanumeric parameters in the statement. + * This returns a list of strings. + * The values of this list can be used to bind parameters + * to the statement using bind_param. Numeric and anonymous parameters + * are ignored. * - * Return the list of named parameters in the statement. */ static VALUE named_params(VALUE self) @@ -479,12 +484,15 @@ named_params(VALUE self) // The first host parameter has an index of 1, not 0. for (int i = 1; i <= param_count; i++) { const char *name = sqlite3_bind_parameter_name(ctx->st, i); - // If parameters of the ?NNN form are used, there may be gaps in the list. + // If parameters of the ?NNN/$NNN/@NNN/:NNN form are used + // there may be gaps in the list. if (name) { - VALUE rb_name = interned_utf8_cstr(name); - // The initial ":" or "$" or "@" or "?" is included as part of the name. - rb_name = rb_str_substr(rb_name, 1, RSTRING_LEN(rb_name) - 1); - rb_ary_push(params, rb_name); + // We ignore numeric parameters + int n = atoi(name + 1); + if (n == 0) { + VALUE param = interned_utf8_cstr(name + 1); + rb_ary_push(params, param); + } } } return rb_obj_freeze(params); diff --git a/test/test_statement.rb b/test/test_statement.rb index 1fb9ac70..e3c27d72 100644 --- a/test/test_statement.rb +++ b/test/test_statement.rb @@ -256,10 +256,8 @@ def test_named_bind_not_found stmt.close end - def test_named_params - assert_empty @stmt.named_params - - stmt = SQLite3::Statement.new(@db, "select :foo, $bar, @zed") + def test_params + stmt = SQLite3::Statement.new(@db, "select ?1, :foo, ?, $bar, @zed, ?250, @999") assert_equal ["foo", "bar", "zed"], stmt.named_params stmt.close end