sqlalchemy: Incorrect code for sa.values on SQLite and MariaDB

Describe the bug

SQLAlchemy generates incorrect SQL for Values queries on SQLite and MariaDB. The code example is for SQLite.

To Reproduce

import sqlalchemy as sa

myvalues = sa.values(sa.column('x'), name='myvalues').data([(7,)])
stmt = sa.select(myvalues)

engine = sa.create_engine("sqlite+pysqlite:///:memory:")
with engine.connect() as conn:
    print(conn.execute(stmt).all())

Error

Traceback (most recent call last):
  File "/home/asbuch/lib/virtualenvs/alchemy/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 1799, in _execute_context
    self.dialect.do_execute(
  File "/home/asbuch/lib/virtualenvs/alchemy/lib/python3.8/site-packages/sqlalchemy/engine/default.py", line 717, in do_execute
    cursor.execute(statement, parameters)
sqlite3.OperationalError: near "(": syntax error

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "bugreport1.py", line 9, in <module>
    print(conn.execute(stmt).all())
  File "/home/asbuch/lib/virtualenvs/alchemy/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 1286, in execute
    return meth(self, multiparams, params, _EMPTY_EXECUTION_OPTS)
  File "/home/asbuch/lib/virtualenvs/alchemy/lib/python3.8/site-packages/sqlalchemy/sql/elements.py", line 323, in _execute_on_connection
    return connection._execute_clauseelement(
  File "/home/asbuch/lib/virtualenvs/alchemy/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 1478, in _execute_clauseelement
    ret = self._execute_context(
  File "/home/asbuch/lib/virtualenvs/alchemy/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 1842, in _execute_context
    self._handle_dbapi_exception(
  File "/home/asbuch/lib/virtualenvs/alchemy/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 2023, in _handle_dbapi_exception
    util.raise_(
  File "/home/asbuch/lib/virtualenvs/alchemy/lib/python3.8/site-packages/sqlalchemy/util/compat.py", line 207, in raise_
    raise exception
  File "/home/asbuch/lib/virtualenvs/alchemy/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 1799, in _execute_context
    self.dialect.do_execute(
  File "/home/asbuch/lib/virtualenvs/alchemy/lib/python3.8/site-packages/sqlalchemy/engine/default.py", line 717, in do_execute
    cursor.execute(statement, parameters)
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "(": syntax error
[SQL: SELECT myvalues.x 
FROM (VALUES (?)) AS myvalues (x)]
[parameters: (7,)]
(Background on this error at: https://sqlalche.me/e/14/e3q8)

Versions

  • OS: Ubuntu 20.04.3
  • Python: 3.8.10
  • SQLAlchemy: 1.4.25
  • Database: SQLite 2.6.0 and MariaDB 10.5.12
  • DBAPI: sqlite3 3.31.1 and mysql-connector-python 8.0.22

Additional context

This SQL works on both SQLite and MariaDB:

select anon.x from (select 3 as x, 1 as y union all values (1,2), (3,4)) anon;

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 1
  • Comments: 15 (11 by maintainers)

Most upvoted comments

but I dont see a way to select named columns from it

Apart from a UNION statement, you can also use a CTE, and include the column names in the CTE name.

The example from the report:

myvalues = sa.values(sa.column('x'), name='myvalues').data([(7,)])
stmt = sa.select(myvalues)

can be rendered as

with myvalues(x) as (values (7)) select * from myvalues;

Demo in the sqlite3 cli:

sqlite> .headers on
sqlite> .mode columns
sqlite> with myvalues(x) as (values (7)) select * from myvalues;
x
----------
7

You’d need Values to inherit from HasCTE to support .cte(), and an update to the SQLite compiler to use the Values object as a CTE with the right name and column names, of course.

oh sorry! I was working on a recipe now. We probably should include this in the docs, or in the examples, or something, and maybe even have it in the test suite for now seeing that two dfiferent datbases use the same workaround. The script below runs for all three targeted DBs:

import sqlalchemy as sa
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.sql import literal
from sqlalchemy.sql import roles
from sqlalchemy.sql import select
from sqlalchemy.sql import union_all
from sqlalchemy.sql import Values


class SubValues(Values, roles.CompoundElementRole):
    """a Values element that can be added to a union_all()."""


@compiles(Values, "sqlite")
@compiles(Values, "mariadb")
@compiles(Values, "mysql")  # for mariadb connections w/ "mysql://"
def _render_values_w_union(element, compiler, **kw):
    all_data = [elem for chunk in element._data for elem in chunk]

    remaining_data = all_data[1:]
    stmt = select(
        *[
            literal(val).label(col.name)
            for col, val in zip(element.columns, all_data[0])
        ]
    )
    if remaining_data:
        stmt = union_all(
            stmt, SubValues(*element.columns).data(remaining_data)
        )

    stmt = stmt.subquery(element.name)

    return compiler.process(stmt, **kw)


@compiles(SubValues, "sqlite")
@compiles(SubValues, "mariadb")
@compiles(SubValues, "mysql")
def _render_subvalues_w_union(element, compiler, **kw):
    # omit rendering parenthesis, columns, "AS name", etc.
    kw.pop("asfrom", None)
    return compiler.visit_values(element, **kw)


for engine in [
    sa.create_engine("postgresql://scott:tiger@localhost/test", echo=True),
    sa.create_engine("sqlite+pysqlite:///:memory:", echo=True),
    sa.create_engine("mariadb://scott:tiger@localhost/test", echo=True),
]:

    with engine.connect() as conn:

        for values in [
            sa.values(sa.column("x"), name="myvalues").data([(7,)]),
            sa.values(sa.column("x"), name="myvalues").data([(7,), (8,)]),
            sa.values(sa.column("x"), sa.column("y"), name="myvalues").data(
                [(7, 10), (8, 12), (14, 15)]
            ),
        ]:
            print(conn.execute(select(values)).all())

Thanks for the comments! Values can be used without typing select on both sqlite and mariadb:

sqlite> values (2,4), (5,1);
2|4
5|1

I don’t know if any column names are assigned on sqlite. On MariaDB the column names appear to be the same as the values in the first row, but I don’t know if that is documented anywhere:

MariaDB [(none)]> select x, y from (values ('x','y'), (1,2), (3,8)) as anon limit 3 offset 1;
+---+---+
| x | y |
+---+---+
| 1 | 2 |
| 3 | 8 |
+---+---+

That is what I call a hack! 😃