From b6d0089ff1d470fc258a8460460379e404c162a9 Mon Sep 17 00:00:00 2001 From: Nemo Date: Fri, 22 Aug 2025 11:30:53 +0530 Subject: [PATCH 1/5] 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/5] 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/5] 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 From 219d7979ec99f05383222a910d4b86719ddaedb1 Mon Sep 17 00:00:00 2001 From: Nemo Date: Mon, 10 Nov 2025 14:59:33 +0100 Subject: [PATCH 4/5] Always :VVV, @VVV, $VVV as named params Even in cases where VVV is a numeric value, these are considered named parameters by sqlite. --- ext/sqlite3/statement.c | 13 +++++-------- test/test_statement.rb | 4 ++-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/ext/sqlite3/statement.c b/ext/sqlite3/statement.c index 81c5f8c6..fae06ed1 100644 --- a/ext/sqlite3/statement.c +++ b/ext/sqlite3/statement.c @@ -484,15 +484,12 @@ 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/$NNN/@NNN/:NNN form are used - // there may be gaps in the list. - if (name) { + // We ignore numbered parameters (starting with ?) + // And null values, since there can be gaps in the list + if (name && *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); - } + 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 e3c27d72..b273bc4e 100644 --- a/test/test_statement.rb +++ b/test/test_statement.rb @@ -257,8 +257,8 @@ def test_named_bind_not_found end def test_params - stmt = SQLite3::Statement.new(@db, "select ?1, :foo, ?, $bar, @zed, ?250, @999") - assert_equal ["foo", "bar", "zed"], stmt.named_params + stmt = SQLite3::Statement.new(@db, "select ?1, :foo, ?, $bar, @zed, ?250, @999, :123, $777") + assert_equal ["foo", "bar", "zed", "999", "123", "777"], stmt.named_params stmt.close end From 779d16c840d898ea35072dde637ccaef360fac5e Mon Sep 17 00:00:00 2001 From: Nemo Date: Mon, 10 Nov 2025 15:07:46 +0100 Subject: [PATCH 5/5] Update FAQ No point in listing @NNN and @VVV separately, since the documentation talks about alpha-numeric identifiers including numbers already --- FAQ.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/FAQ.md b/FAQ.md index b55ff35c..a3886605 100644 --- a/FAQ.md +++ b/FAQ.md @@ -122,20 +122,17 @@ 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 -number). When the placeholder is associated with a number, that number -identifies the index of the bind variable to replace it with. When it -is an identifier, it identifies the name of the corresponding bind -variable. (In the instance of the first format--a single question -mark--the placeholder is assigned a number one greater than the last -index used, or 1 if it is the first.) +Where _n_ is an integer, and _word_ is an alpha-numeric identifier(or number). +When the placeholder is associated with a number (only in case of `?_nnn_`), +that number identifies the index of the bind variable to replace it with. +When it is an identifier, it identifies the name of the corresponding bind +variable. (In the instance of the first format--a single question mark--the +placeholder is assigned a number one greater than the last index used, or 1 +if it is the first.) For example, here is a query using these placeholder formats: