Skip to content

MDEV-39022 Add LOCAL spvar syntax for prepared statements and SYS_R…#5146

Closed
sanja-byelkin wants to merge 1 commit into
mainfrom
bb-13.0-bar-MDEV-39022
Closed

MDEV-39022 Add LOCAL spvar syntax for prepared statements and SYS_R…#5146
sanja-byelkin wants to merge 1 commit into
mainfrom
bb-13.0-bar-MDEV-39022

Conversation

@sanja-byelkin
Copy link
Copy Markdown
Member

…EFCURSORs

This patch adds the following syntax:

OPEN c0 FOR LOCAL spvar_with_ps_name;
PREPARE LOCAL spvar_with_ps_name FROM 'dynamic sql'; EXECUTE LOCAL spvar_with_ps_name;
DEALLOCATE PREPARE LOCAL spvar_with_ps_name;

OPEN c0 FOR PREPARE stmt;

@sanja-byelkin sanja-byelkin requested a review from Copilot May 29, 2026 12:05
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements support for opening cursors using prepared statements and local variables (e.g., 'OPEN c FOR PREPARE stmt' and 'OPEN c FOR LOCAL spvar'). The feedback identifies critical security and stability issues, including potential null pointer dereferences when executing prepared statements with local variables outside of stored procedures, and a potential out-of-bounds read due to non-null-terminated LEX_CSTRING variables. Additionally, it is recommended to introduce an 'is_ps' boolean flag to prevent incorrect fallbacks when statement names evaluate to empty, and to add clarifying comments for temporary string buffers.

Comment thread sql/sql_prepare.cc Outdated
Comment on lines +2819 to +2822
if (name.local_var_name().length)
{
sp_variable *spvar= lex->spcont->find_variable_or_error(
name.local_var_name(), false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

If PREPARE LOCAL spvar FROM ... is executed as a top-level statement outside of a stored procedure, lex->spcont will be nullptr. Accessing lex->spcont->find_variable_or_error without a null check will cause a server crash. We must add a check to ensure lex->spcont is not null before attempting to look up the local variable.

  if (name.local_var_name().length)
  {
    if (!lex->spcont)
    {
      my_error(ER_SP_UNDECLARED_VAR, MYF(0), name.local_var_name().str);
      return true;
    }
    sp_variable *spvar= lex->spcont->find_variable_or_error(
                                                name.local_var_name(), false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added this piece of the code:

    if (!lex->spcont)
    {
      /*
         A statement was called from the top level, outside of a routine, e.g.:
         - PREPARE LOCAL spvar FROM 'SELECT 1';
         - EXECUTE LOCAL spvar;
         - DEALLOCATE PREPARE LOCAL spvar;
      */
      my_error(ER_SP_UNDECLARED_VAR, MYF(0), name.local_var_name().str);
      return true;
    }

Comment thread sql/sp_instr.cc
Comment on lines +2647 to +2661
StringBuffer<64> value_buffer, converter_buffer;
Lex_ident_sys ps_name= lps.evaluate_name(thd, &value_buffer,
&converter_buffer, "OPEN");
if (ps_name.str == nullptr && lps.var().rcontext_handler()/*FOR LOCAL*/)
{
/*
We're here if this is an "OPEN c0 FOR LOCAL spvar" statement
and the SP variable evaluates to SQL NULL:
SET spvar= NULL;
OPEN c0 FOR LOCAL spvar;
evaluate_name() should have raised the ER_UNKNOWN_STMT_HANDLER error.
*/
DBUG_ASSERT(thd->is_error());
DBUG_RETURN(-1);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

We should evaluate whether the cursor is a prepared-statement-based cursor (either a simple PS or a LOCAL variable) and store this in a boolean variable is_ps. This prevents us from incorrectly falling back to a regular cursor open or dynamic string open when the evaluated statement name is empty (e.g., when spvar is ''). Also, since value_buffer and converter_buffer are String objects initialized with temporary buffers whose charsets are expected to be immediately overwritten, we should add a comment indicating this.

  StringBuffer<64> value_buffer, converter_buffer; // charset will be overwritten
  Lex_ident_sys ps_name= lps.evaluate_name(thd, &value_buffer,
                                           &converter_buffer, "OPEN");
  bool is_ps= lps.var().rcontext_handler() != nullptr || lps.name().str != nullptr;
  if (ps_name.str == nullptr && lps.var().rcontext_handler()/*FOR LOCAL*/)
  {
    /*
      We're here if this is an "OPEN c0 FOR LOCAL spvar" statement
      and the SP variable evaluates to SQL NULL:
        SET spvar= NULL;
        OPEN c0 FOR LOCAL spvar;
      evaluate_name() should have raised the ER_UNKNOWN_STMT_HANDLER error.
    */
    DBUG_ASSERT(thd->is_error());
    DBUG_RETURN(-1);
  }
References
  1. When a String object is initialized with a temporary buffer and its charset is expected to be immediately overwritten (e.g., by a subsequent call to field->val_str()), add a comment such as '// charset will be overwritten' to prevent confusion for future developers.

Copy link
Copy Markdown
Contributor

@abarkov abarkov Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a bug indeed in here, however I have fixed it in a different way:

  • Added a new method Lex_prepared_stmt::is_for_local() for shorter notation.
  • Changed the code in two places below to test ps_name.str rather than ps_name.length.

Also I have added new tests covering

  • Empty variable name: DECLARE `` TEXT DEFAULT 'stmt'
  • Variable with an empty string value: DECLARE spvar TEXT DEFAULT '';

Comment thread sql/sp_instr.cc Outdated
Comment on lines +2692 to +2696
int rc= ps_name.length ? cursor->open_from_ps(thd, ps_name) :
!lps.code() ? cursor->open(thd, m_cursor_name, return_type) :
cursor->open_from_dynamic_string(thd,
set_placeholder_first_ip,
m_set_ps_placeholder_count);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Use the is_ps boolean to determine whether to open the cursor from a prepared statement, rather than checking ps_name.length. This ensures that if the evaluated statement name is empty, we still attempt to open it as a prepared statement (which will correctly fail with an unknown statement handler error) instead of falling back to opening it as a regular cursor or dynamic string.

    int rc= is_ps ? cursor->open_from_ps(thd, ps_name) :
            !lps.code() ? cursor->open(thd, m_cursor_name, return_type) :
                          cursor->open_from_dynamic_string(thd,
                                                set_placeholder_first_ip,
                                                m_set_ps_placeholder_count);

Copy link
Copy Markdown
Contributor

@abarkov abarkov Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I use Lex_prepared_stmt::is_for_local() and test ps_name.str instead of ps_name.length. Also see the previous comment.

Comment thread sql/sp_instr.cc Outdated
Comment on lines 2720 to 2724
DBUG_RETURN(ps_name.length ? cursor->open_from_ps(thd, ps_name) :
!lps.code() ? cursor->open(thd, m_cursor_name, return_type,
false/*don't check max_open_cursors*/) :
cursor->open_from_dynamic_string(thd, set_placeholder_first_ip,
m_set_ps_placeholder_count));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Use the is_ps boolean here as well to ensure consistent behavior when reopening the cursor.

  DBUG_RETURN(is_ps ? cursor->open_from_ps(thd, ps_name) :
              !lps.code() ? cursor->open(thd, m_cursor_name, return_type,
                                       false/*don't check max_open_cursors*/) :
              cursor->open_from_dynamic_string(thd, set_placeholder_first_ip,
                                               m_set_ps_placeholder_count));

Copy link
Copy Markdown
Contributor

@abarkov abarkov Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I use Lex_prepared_stmt::is_for_local() and test ps_name.str instead of ps_name.length. Also see the comment before the previous.

Comment thread sql/sql_prepare.cc Outdated
Comment on lines +2842 to +2845
if (open_for.local_var_name().length)
{
sp_variable *spvar= lex->spcont->find_variable_or_error(
open_for.local_var_name(), false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

For defensive programming and to prevent potential crashes in release builds (where DBUG_ASSERT(lex->spcont) is compiled out), we should also check if lex->spcont is null before calling find_variable_or_error in this overload.

  if (open_for.local_var_name().length)
  {
    if (!lex->spcont)
    {
      my_error(ER_SP_UNDECLARED_VAR, MYF(0), open_for.local_var_name().str);
      return true;
    }
    sp_variable *spvar= lex->spcont->find_variable_or_error(
                                             open_for.local_var_name(), false);

Copy link
Copy Markdown
Contributor

@abarkov abarkov Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OPEN statement is allowed only inside a stored procedure. We cannot get to here from the top-level statement.

Comment thread sql/sp_pcontext.h
Comment on lines +543 to +550
sp_variable *find_variable_or_error(const LEX_CSTRING &name,
bool current_scope_only) const
{
sp_variable *res= find_variable(&name, current_scope_only);
if (!res)
my_error(ER_SP_UNDECLARED_VAR, MYF(0), name.str);
return res;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

LEX_CSTRING is not guaranteed to be null-terminated (it is often a slice of the query buffer). Passing name.str directly to my_error with the %s format specifier can result in printing garbage or reading out of bounds. We should ensure the name is safely null-terminated before passing it to my_error.

  sp_variable *find_variable_or_error(const LEX_CSTRING &name,
                                      bool current_scope_only) const
  {
    sp_variable *res= find_variable(&name, current_scope_only);
    if (!res)
    {
      char buf[128];
      size_t len= name.length < sizeof(buf) ? name.length : sizeof(buf) - 1;
      memcpy(buf, name.str, len);
      buf[len]= '\0';
      my_error(ER_SP_UNDECLARED_VAR, MYF(0), buf);
    }
    return res;
  }

Copy link
Copy Markdown
Contributor

@abarkov abarkov Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable name should come from the parser only and should already be converted to utf8 (on mem_root) and should be null-terminated.
Added this line:
DBUG_ASSERT(name.str[name.length] == '\0');

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds support for using LOCAL SP variables as prepared statement names, and for opening cursor variables from prepared statements (including via LOCAL variables), aligning behavior with ORACLE-mode SYS_REFCURSOR usage.

Changes:

  • Extend the SQL grammar to accept PREPARE/EXECUTE/DEALLOCATE PREPARE LOCAL spvar and OPEN c FOR PREPARE stmt / OPEN c FOR LOCAL spvar.
  • Add runtime support to resolve prepared-statement names from SP local variables (including erroring when the variable evaluates to SQL NULL).
  • Add mysql-test coverage for the new syntax and cursor open behaviors (including disallowed cases for strict ref cursors and stored functions).

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
sql/sql_yacc.yy Extends parser rules/types to accept LOCAL statement names and enhanced OPEN ... FOR ... forms.
sql/sql_prepare.cc Implements variable-based statement-name resolution and wires it into PREPARE/EXECUTE/DEALLOCATE execution paths.
sql/sql_lex.h Introduces Lex_sql_statement_name_st and Lex_open_for_st, and extends Lex_prepared_stmt to store SP-variable address for LOCAL.
sql/sql_lex.cc Updates statement builders and OPEN cursor handling to support PREPARE and LOCAL sources, plus validation/error paths.
sql/sp_pcontext.h Adds find_variable_or_error() helper to consolidate SP-variable lookup + error reporting.
sql/sp_instr.cc Updates cursor-open instruction execution to resolve prepared statement name at runtime (including LOCAL variable evaluation).
mysql-test/main/sp-cursor-for-ps.test New tests for OPEN ... FOR PREPARE and OPEN ... FOR LOCAL, including error cases.
mysql-test/main/sp-cursor-for-ps.result Expected output for the new sp-cursor-for-ps test.
mysql-test/main/ps-local.test New tests for PREPARE/EXECUTE/DEALLOCATE ... LOCAL and mixed simple/extended usages.
mysql-test/main/ps-local.result Expected output for the new ps-local test.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread sql/sql_prepare.cc
Comment on lines +2809 to +2851
bool Lex_prepared_stmt::set(LEX *lex, const Lex_sql_statement_name_st &name,
Item *code, List<Item> *params)
{
DBUG_ASSERT(m_params.elements == 0);
// Simple and extended prepared statement name cannot co-exist
DBUG_ASSERT(!name.ps_name().length || !name.local_var_name().length);
if (params)
m_params= *params;
m_code= code;
m_name= name.ps_name();
if (name.local_var_name().length)
{
sp_variable *spvar= lex->spcont->find_variable_or_error(
name.local_var_name(), false);
if (!spvar)
return true;
m_var= sp_rcontext_addr(&sp_rcontext_handler_local, spvar->offset);
}
return false;
}


bool Lex_prepared_stmt::set(LEX *lex, const Lex_open_for_st &open_for,
List<Item> *params)
{
DBUG_ASSERT(m_params.elements == 0);
DBUG_ASSERT(lex->spcont);
// Simple and extended prepared statement name cannot co-exist
DBUG_ASSERT(!open_for.ps_name().length || !open_for.local_var_name().length);
if (params)
m_params= *params;
m_name= open_for.ps_name();
m_code= open_for.dynamic_string();
if (open_for.local_var_name().length)
{
sp_variable *spvar= lex->spcont->find_variable_or_error(
open_for.local_var_name(), false);
if (!spvar)
return true;
m_var= sp_rcontext_addr(&sp_rcontext_handler_local, spvar->offset);
}
return false;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added m_var= sp_rcontext_addr(nullptr, 0); in both overloads.

Comment thread sql/sql_yacc.yy
Comment on lines +3800 to +3808
open_for:
select_for_open_cursor // OPEN c FOR SELECT 1;
{
$$= Lex_open_for_st::make_for_query();
}
| bit_expr // OPEN c FOR 'SELECT 1';
{
$$= Lex_open_for_st::make_for_dynamic_string($1);
}
Copy link
Copy Markdown
Contributor

@abarkov abarkov Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, there were new bison conflicts with expr.

OPEN FOR is meaningful only for string result expressions. They are all inside bit_expr.

expr adds only boolean expressions on top of bit_expr. So the change from expr to bit_expr is safe.

Comment thread sql/sql_lex.cc Outdated
if (stmt->prepared_stmt.code())

/*
It can be either of this:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment thread sql/sql_lex.cc Outdated
Comment on lines +7893 to +7894
It can be rewritten to use a weak cursor variable:
OPEN weak_cursor_variable FORM PREPARE stmt;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment thread sql/sql_lex.cc Outdated
Comment on lines +7905 to +7906
It can be rewritten to use a weak cursor variable:
OPEN weak_cursor_variable FORM LOCAL ps_name_variable;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Copy Markdown
Member Author

@sanja-byelkin sanja-byelkin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally all is good, but fix small things.

Missing coverage:

  • EXECUTE LOCAL spvar USING — the USING parameter path is never exercised with the LOCAL form. The test only covers the "not all parameters bound" error case.
  • PREPARE LOCAL spvar where spvar holds NULL (error path) — covered for OPEN but not for standalone PREPARE/EXECUTE/DEALLOCATE.
  • OPEN c FOR LOCAL spvar inside a trigger — only stored functions are tested for the "not allowed" path;

Comment thread sql/sql_lex.cc Outdated
This statement is not supported:
OPEN strict_cursor_variable FOR PREPARE stmt;
It can be rewritten to use a weak cursor variable:
OPEN weak_cursor_variable FORM PREPARE stmt;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FORM -> FOR

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment thread sql/sql_lex.cc Outdated
This statement is not supported:
OPEN strict_cursor_variable FOR LOCAL ps_name_variable;
It can be rewritten to use a weak cursor variable:
OPEN weak_cursor_variable FORM LOCAL ps_name_variable;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FORM -> FOR

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment thread sql/sql_lex.h
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may be reset m_var here?

or reset it in set where it set conditionally.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added m_var= sp_rcontext_addr(nullptr, 0); here and into two overloads of Lex_prepared_stmt::set() as AI suggested.

Comment thread sql/sql_prepare.cc
Item *item= thd->get_variable(m_var);
if (!(item= thd->sp_fix_func_item(&item)))
return Lex_ident_sys();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here if item is NULL so there was an error. Why it after it return return Lex_ident_sys(tmp.str, tmp.length); as if nothing happened. If it is error handling please put comment here.

Copy link
Copy Markdown
Contributor

@abarkov abarkov Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Item is NULL it means that some error happened and we return Lex_ident_sys() in such case, so the caller can test the res.str == nullptr to catch the error.

I have added DBUG_ASSERT(thd->is_error()); and added the comment // Some error happened.

Comment thread sql/sql_lex.h Outdated
Lex_open_for_st res;
res.m_type= QUERY;
res.m_name= Lex_ident_sys();
res.m_dynamic_string= nullptr;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  union
  {
    /*
      m_name contains either PS name or an SP variable name:
        OPEN c FOR PREPARE stmt;
        OPEN c FOR LOCAL spvar;
    */
    Lex_ident_sys_st m_name;
    Item *m_dynamic_string;      // OPEN c FOR expr;
  };

so both part of union is assigned

Copy link
Copy Markdown
Contributor

@abarkov abarkov Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: removed initialization of non-relevant members. This is safe because these members cannot be accessed directly from outside:

  • the class encapsulates them
  • the getter methods do not read them in case m_type is not suitable

…EFCURSORs

This patch adds the following syntax:

OPEN c0 FOR LOCAL spvar_with_ps_name;
PREPARE LOCAL spvar_with_ps_name FROM 'dynamic sql';
EXECUTE LOCAL spvar_with_ps_name;
DEALLOCATE PREPARE LOCAL spvar_with_ps_name;

OPEN c0 FOR PREPARE stmt;
@abarkov abarkov force-pushed the bb-13.0-bar-MDEV-39022 branch from 4ac0833 to 7420e45 Compare June 3, 2026 09:25
@abarkov abarkov closed this Jun 3, 2026
@abarkov
Copy link
Copy Markdown
Contributor

abarkov commented Jun 3, 2026

Generally all is good, but fix small things.

Missing coverage:

* EXECUTE LOCAL spvar USING  — the USING parameter path is never exercised with the LOCAL form. The test only covers the "not all parameters bound" error case.

Added.

* PREPARE LOCAL spvar where spvar holds NULL (error path) — covered for OPEN but not for standalone PREPARE/EXECUTE/DEALLOCATE.

LOCAL requires an SP variable. It's not possible to test this in a standalone statement.

* OPEN c FOR LOCAL spvar inside a trigger — only stored functions are tested for the "not allowed" path;

Added.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

5 participants