3

I have a stored procedure on MSSQL server, "prc_add_names", that takes a table-value parameter. The parameter itself is of a custom type "StringTable" defined like so:

CREATE TYPE [dbo].[StringTable] AS TABLE([strValue] [nvarchar](max) NULL)

I have no idea how to execute this procedure using SQLAlchemy. I am used to calling procedures with arguments using session.execute like this:

result = session.execute('prc_do_something :pArg', {pArg:'foo'})

However, this does not work if I simply pass a list of strings as the argument:

result = session.execute('prc_add_names :pArg', {pArg: ['Name One', 'Name Two']})

which leads to:

sqlalchemy.exc.ProgrammingError: (pymssql.ProgrammingError) (102, "Incorrect syntax near 'Name One'.DB-Lib error message 20018, severity 15:
General SQL Server error: Check messages from the SQL Server
") [SQL: 'prc_add_names %(pArg)s'] [parameters: {'pArg': ['Name One', 'Name Two']}] (Background on this error at: http://sqlalche.me/e/f405)

Obviously, SQLAlchemy does not understand my list of strings as an attempt to create my StringTable-type argument, but after a couple hours of googling and reading through the documentation, I haven't figured out how I should be handling this.

FYI, I am not in control of this database, so modifying the stored procedure or anything else there is not an option.

EDIT: I'm not married to SQLAlchemy. If there is another library that can handle this, I'd be happy to use it instead.

ForShame
  • 63
  • 1
  • 6

2 Answers2

4

There is a driver that really supports TVPs: Pytds. It's not officially supported, but there's a 3rd party dialect implementation for it: sqlalchemy-pytds. Using them you could call your stored procedure like so:

In [1]: engine.execute(DDL("CREATE TYPE [dbo].[StringTable] AS TABLE([strValue] [nvarchar](max) NULL)"))
Out[1]: <sqlalchemy.engine.result.ResultProxy at 0x7f235809ae48>

In [2]: engine.execute(DDL("CREATE PROC test_proc (@pArg [StringTable] READONLY) AS BEGIN SELECT * FROM @pArg END"))
Out[2]: <sqlalchemy.engine.result.ResultProxy at 0x7f2358027b70>

In [3]: arg = ['Name One', 'Name Two']

In [4]: import pytds

In [5]: tvp = pytds.TableValuedParam(type_name='StringTable',
   ...:                              rows=((x,) for x in arg))

In [6]: engine.execute('EXEC test_proc %s', (tvp,))
Out[6]: <sqlalchemy.engine.result.ResultProxy at 0x7f294e699e10>

In [7]: _.fetchall()
Out[7]: [('Name One',), ('Name Two',)]

This way you can pass potentially large amounts of data as params:

In [21]: tvp = pytds.TableValuedParam(type_name='StringTable',
    ...:                              rows=((str(x),) for x in range(100000)))

In [22]: engine.execute('EXEC test_proc %s', (tvp,))
Out[22]: <sqlalchemy.engine.result.ResultProxy at 0x7f294c6e9f98>

In [23]: _.fetchall()[-1]
Out[23]: ('99999',)

If on the other hand you're using a driver that does not support TVPs, you could declare a table variable, insert the values, and pass that as the argument to your procedure:

In [12]: engine.execute(
    ...:     """
    ...:     DECLARE @pArg AS [StringTable];
    ...:     INSERT INTO @pArg VALUES {placeholders};
    ...:     EXEC test_proc @pArg;
    ...:     """.format(placeholders=",".join(["(%s)"] * len(arg))),
    ...:     tuple(arg))
    ...:     
Out[12]: <sqlalchemy.engine.result.ResultProxy at 0x7f23580f2908>

In [15]: _.fetchall()
Out[15]: [('Name One',), ('Name Two',)]

Note that you cannot use any executemany methods, or you'll end up calling the procedure for each table value separately. That is why the placeholders are constructed manually and the table values passed as individual arguments. Care must be taken not to format any arguments directly in to the query, but the correct amount of placeholders for the DB-API instead. Row values are limited to a maximum of 1000.

It'd of course be nice, if the underlying DB-API driver provided proper support for table valued parameters, but at least I could not find a way for pymssql, which uses FreeTDS. A reference to TVPs on the mailing list makes it clear that they're not supported. The situation is not much better for PyODBC.

Disclaimer: I've not really used MS SQL Server before.

Ilja Everilä
  • 50,538
  • 7
  • 126
  • 127
  • I believe this would work, but creates a SQL injection vulnerability. – ForShame May 03 '18 at 16:14
  • 2
    It does not create an SQL injection vulnerability. Note that the *arg* values are not formatted to the query string, but a bunch of `%s` placeholders, that the **DB-API driver** fills in. No percent formatting takes place manually; *arg* is passed as the 2nd argument to `execute()`. It specifically avoids SQLi. – Ilja Everilä May 03 '18 at 17:46
  • The actual solution I'm working on involves huge numbers of these names. Do you know how efficiently this would scale to handle hundreds of thousands at a time? I know multi-row inserts suffer from some performance issues above a certain number of rows in a regular table, but I'm not sure if that extends to table variables as well. – ForShame May 03 '18 at 18:50
  • My guess is that it does not scale well, if at all, for such. There's that max limit of 1000 row values too. Been trying to read how drivers that *really* support table valued parameters work, and it's entirely different. Unfortunately FreeTDS doesn't seem to support them, unlike for example the driver used by C#. – Ilja Everilä May 03 '18 at 18:58
  • 1
    Thanks for doing so much work on this question. The Pytds suggestion is definitely the right answer. Unfortunately, I can't use it at my company, so we ended up asking the database manager to create a new procedure that takes XML as input, instead. – ForShame May 03 '18 at 20:09
  • 1
    **pyodbc** also supports TVPs as of version 4.0.25 (December 2018). See [this answer](https://stackoverflow.com/a/67198492/2144390) for details. – Gord Thompson Apr 21 '21 at 15:13
-1

I think you can use callproc() method and pass the input argument as a list.http://docs.sqlalchemy.org/en/latest/core/connections.html#calling-stored-procedures

mad_
  • 8,121
  • 2
  • 25
  • 40
  • This does not work. I get an error: `_mssql.MSSQLDriverException: Unable to convert value` and after exploring the code for the callproc method, it turns out there is no support for sequence-type arguments or table-valued parameters at all in the base pymssql DBAPI module. – ForShame May 02 '18 at 18:44