Search code examples
parameterspyodbc

python pyodbc: different behavior when passing multiple parameters to query than when passing only one


I have a pyodbc select where I wish to pass multiple parameterized variables. When one of the variables is passed in its native format with only a single parameter variable, the select statement completes successfully. However, when I pass two parameters, that same variable seemingly results in a not found situation. When I then hardcode a variable and pass THAT into the two-variable query, it works.

Here's the one where I use the variable natively. The variable comes from a prior select statement into a dictionary. I then assign the dictionary value to a variable. It maintains the type of "pyodbc.row". I did another run where I assigned a variable to that as "str(this_incid)" and the results were identical. And I should stress that in other parts of the code, where I pass the pyodbc.type to a single-parameter query, it works just fine. I could also post those results, but I think this is long enough already.

    auth_type = 'OC'
    print("This incid ", this_incid, "type", type(this_incid))
    print(str(this_incid))
    print("Auth type ", auth_type, "type", type(auth_type))
    bhfc_cursor.execute("""\
    select inc_auth.staff_id, inc_auth.rank, inc_auth.assignment, inc_auth.dttm from INC_AUTH
     INNER JOIN DBO.INC_MAIN ON inc_auth.inci_id = inc_main.inci_id
      where inc_auth.auth_type = (?) and inc_main.inci_id = (?) """, str(auth_type), str(this_incid))
    oic_details = bhfc_cursor.fetchone()
    print("oic details: " ,oic_details)
This incid  ('_53N0TYDNM', ) type <class 'pyodbc.Row'>
('_53N0TYDNM', )
temp incid  _53M07J2D6 type <class 'str'>
Auth type  OC type <class 'str'>
oic details:  None

Here's the one where I just do a hard assignment of the second variable:

    auth_type = 'OC'
    temp_incid = '_53M07J2D6'
    print("temp incid ", temp_incid, "type", type(temp_incid))
    print("Auth type ", auth_type, "type", type(auth_type))
    bhfc_cursor.execute("""\
    select inc_auth.staff_id, inc_auth.rank, inc_auth.assignment, inc_auth.dttm from INC_AUTH
     INNER JOIN DBO.INC_MAIN ON inc_auth.inci_id = inc_main.inci_id
      where inc_auth.auth_type = (?) and inc_main.inci_id = (?) """, str(auth_type), str(temp_incid))
    oic_details = bhfc_cursor.fetchone()
    print("oic details: " ,oic_details)

and that one works!

temp incid  _53M07J2D6 type <class 'str'>
Auth type  OC type <class 'str'>
oic details:  ('29-2190        ', 'FC        ', '          ', datetime.datetime(2018, 1, 6, 8, 48))

I do see the difference in my print statements where in the one that fails it shows surrounded by single ticks/apostrophes, whereas in the hard assignment scenario it does not. I'm not sure what to make of that.

Ah, what the heck- here's one of the single-parameter queries which succeeds with that variable in its native pyodbc.row type:

    print("Unit Record this_incid", this_incid, "type ", type(this_incid))
    bhfc_cursor.execute("""\
    select INC_UNIT.* from INC_UNIT 
    inner join INC_MAIN  on inc_unit.inci_no = inc_main.inci_no 
     where inc_main.INCI_ID = (?)""""", this_incid)
    unit_details = bhfc_cursor.fetchall()
    print(unit_details)
Unit Record this_incid ('_53N0TYDNM', ) type  <class 'pyodbc.Row'>
unit details:  [('_53N0TYEZE', '46132', datetime.datetime(2018, 1, 4, 0, 0), '1800511   ', 'CMD29 (snip)

You'll note that I didn't "typecast" the variables in this one (Not sure what that means, other than wrapping them in "str()". I did that because when I didn't do that with multiple variables I received a sql bind parameter error, and the alleged solution was to "type cast" the variables as posited in this answer: python: Invalid data type (0) (SQLBindParameter)

Anybody have any ideas/suggestions? Thanks.


Solution

  • The Python DBAPI Spec v2.0, a.k.a. "PEP 249", specifies that for the .execute() method:

    Parameters may be provided as sequence or mapping and will be bound to variables in the operation.

    That is, strict adherence to the spec requires that the parameter values be a single sequence (list, tuple, or range), or a mapping (dict), depending on the specific implementation of the software layer. With pyodbc that would be something like

    sql = "SELECT * FROM my_table WHERE city = ? AND state = ?"
    params = ("Nashville", "TN")  # tuple
    crsr.execute(sql, params)
    

    For convenience, pyodbc offers an extension to the DBAPI spec allowing us to pass multiple parameters as individual values, e.g.,

    sql = "SELECT * FROM my_table WHERE city = ? AND state = ?"
    crsr.execute(sql, "Nashville", "TN")
    

    In your case if you have a single value in a pyodbc.Row object (which acts like a sequence), both methods are equivalent

    my_state = crsr.execute("SELECT 'TN'").fetchone()
    print(type(my_state))  # <class 'pyodbc.Row'>
    print(my_state)  # ('TN', )
    sql = "SELECT * FROM my_table WHERE state = ?"
    
    crsr.execute(sql, my_state)
    # which is equivalent to
    # crsr.execute(sql, ('TN', ))
    
    crsr.execute(sql, my_state[0])
    # which is equivalent to
    # crsr.execute(sql, 'TN')
    

    However, it gets more complicated when we have more than one parameter. This query won't work correctly …

    sql = "SELECT * FROM my_table WHERE city = ? AND state = ?"
    
    crsr.execute(sql, "Nashville", my_state)
    # which is equivalent to
    # crsr.execute(sql, "Nashville", ('TN', ))
    

    … because the parameters are now an inconsistent mix of scalar ("Nashville") and sequence (('TN', )) values. You can fix that by either combining the values into a tuple

    params = ("Nashville", ) + tuple(my_state)
    print(type(params))  # <class 'tuple'>
    print(params)  # ('Nashville', 'TN')
    crsr.execute(sql, params)
    

    or by pulling the individual values out of the pyodbc.Row object using *

    crsr.execute(sql, "Nashville", *my_state)
    # which is equivalent to
    # crsr.execute(sql, "Nashville", "TN")