--- /dev/null
+.. change::
+ :tags: usecase, sqlite
+ :tickets: 4766
+
+ Added support for composite (tuple) IN operators with SQLite, by rendering
+ the VALUES keyword for this backend. As other backends such as DB2 are
+ known to use the same syntax, the syntax is enabled in the base compiler
+ using a dialect-level flag ``tuple_in_values``. The change also includes
+ support for "empty IN tuple" expressions for SQLite when using "in_()"
+ between a tuple value and an empty set.
+
SQL Server.
* As "selectin" loading relies upon IN, for a mapping with composite primary
- keys, it must use the "tuple" form of IN, which looks like
- ``WHERE (table.column_a, table.column_b) IN ((?, ?), (?, ?), (?, ?))``.
- This syntax is not supported on every database; currently it is known
- to be only supported by modern PostgreSQL and MySQL versions. Therefore
- **selectin loading is not platform-agnostic for composite primary keys**.
- There is no special logic in SQLAlchemy to check ahead of time which platforms
- support this syntax or not; if run against a non-supporting platform (such
- as SQLite), the database will return an error immediately. An advantage to SQLAlchemy
- just running the SQL out for it to fail is that if a database like
- SQLite does start supporting this syntax, it will work without any changes
- to SQLAlchemy.
+ keys, it must use the "tuple" form of IN, which looks like ``WHERE
+ (table.column_a, table.column_b) IN ((?, ?), (?, ?), (?, ?))``. This syntax
+ is not supported on every database; within the dialects that are included
+ with SQLAlchemy, it is known to be supported by modern PostgreSQL, MySQL and
+ SQLite versions. Therefore **selectin loading is not platform-agnostic for
+ composite primary keys**. There is no special logic in SQLAlchemy to check
+ ahead of time which platforms support this syntax or not; if run against a
+ non-supporting platform, the database will return an error immediately. An
+ advantage to SQLAlchemy just running the SQL out for it to fail is that if a
+ particular database does start supporting this syntax, it will work without
+ any changes to SQLAlchemy.
In general, "selectin" loading is probably superior to "subquery" eager loading
in most ways, save for the syntax requirement with composite primary keys
self.process(binary.right, **kw),
)
- def visit_empty_set_expr(self, type_):
- return "SELECT 1 FROM (SELECT 1) WHERE 1!=1"
+ def visit_empty_set_expr(self, element_types):
+ return "SELECT %s FROM (SELECT %s) WHERE 1!=1" % (
+ ", ".join("1" for type_ in element_types or [INTEGER()]),
+ ", ".join("1" for type_ in element_types or [INTEGER()]),
+ )
class SQLiteDDLCompiler(compiler.DDLCompiler):
supports_empty_insert = False
supports_cast = True
supports_multivalues_insert = True
+ tuple_in_values = True
default_paramstyle = "qmark"
execution_ctx_cls = SQLiteExecutionContext
supports_simple_order_by_label = True
+ tuple_in_values = False
+
engine_config_types = util.immutabledict(
[
("convert_unicode", util.bool_or_str("force")),
for i, tuple_element in enumerate(values, 1)
for j, value in enumerate(tuple_element, 1)
]
- replacement_expressions[name] = ", ".join(
+ replacement_expressions[name] = (
+ "VALUES " if self.dialect.tuple_in_values else ""
+ ) + ", ".join(
"(%s)"
% ", ".join(
self.compiled.bindtemplate
sep = " "
else:
sep = OPERATORS[clauselist.operator]
- return sep.join(
+
+ text = sep.join(
s
for s in (
c._compiler_dispatch(self, **kw) for c in clauselist.clauses
)
if s
)
+ if clauselist._tuple_values and self.dialect.tuple_in_values:
+ text = "VALUES " + text
+ return text
def visit_case(self, clause, **kwargs):
x = "CASE "
)
return _boolean_compare(
- expr, op, ClauseList(*args).self_group(against=op), negate=negate_op
+ expr,
+ op,
+ ClauseList(_tuple_values=isinstance(expr, Tuple), *args).self_group(
+ against=op
+ ),
+ negate=negate_op,
)
self.operator = kwargs.pop("operator", operators.comma_op)
self.group = kwargs.pop("group", True)
self.group_contents = kwargs.pop("group_contents", True)
+ self._tuple_values = kwargs.pop("_tuple_values", False)
text_converter = kwargs.pop(
"_literal_as_text", _expression_literal_as_text
)
class BooleanClauseList(ClauseList, ColumnElement):
__visit_name__ = "clauselist"
+ _tuple_values = False
+
def __init__(self, *arg, **kw):
raise NotImplementedError(
"BooleanClauseList has a private constructor"
[(1, 2), (5, 12), (10, 19)]
)
+ .. versionchanged:: 1.3.6 Added support for SQLite IN tuples.
+
.. warning::
- The composite IN construct is not supported by all backends,
- and is currently known to work on PostgreSQL and MySQL,
- but not SQLite. Unsupported backends will raise
- a subclass of :class:`~sqlalchemy.exc.DBAPIError` when such
- an expression is invoked.
+ The composite IN construct is not supported by all backends, and is
+ currently known to work on PostgreSQL, MySQL, and SQLite.
+ Unsupported backends will raise a subclass of
+ :class:`~sqlalchemy.exc.DBAPIError` when such an expression is
+ invoked.
"""
from sqlalchemy import bindparam
from sqlalchemy import CheckConstraint
from sqlalchemy import Column
+from sqlalchemy import column
from sqlalchemy import create_engine
from sqlalchemy import DefaultClause
from sqlalchemy import event
from sqlalchemy import Table
from sqlalchemy import testing
from sqlalchemy import text
+from sqlalchemy import tuple_
from sqlalchemy import types as sqltypes
from sqlalchemy import UniqueConstraint
from sqlalchemy import util
dialect=sqlite.dialect(),
)
+ def test_in_tuple(self):
+ self.assert_compile(
+ tuple_(column("q"), column("p")).in_([(1, 2), (3, 4)]),
+ "(q, p) IN (VALUES (?, ?), (?, ?))",
+ )
+
class OnConflictDDLTest(fixtures.TestBase, AssertsCompiledSQL):
@property
def tuple_in(self):
- return only_on(["mysql", "postgresql"])
+ def _sqlite_tuple_in(config):
+ return against(
+ config, "sqlite"
+ ) and config.db.dialect.dbapi.sqlite_version_info >= (3, 15, 0)
+
+ return only_on(["mysql", "postgresql", _sqlite_tuple_in])
@property
def independent_cursors(self):
"((:param_1, :param_2), (:param_3, :param_4))",
)
+ dialect = default.DefaultDialect()
+ dialect.tuple_in_values = True
+ self.assert_compile(
+ tuple_(table1.c.myid, table1.c.name).in_([(1, "foo"), (5, "bar")]),
+ "(mytable.myid, mytable.name) IN "
+ "(VALUES (:param_1, :param_2), (:param_3, :param_4))",
+ dialect=dialect,
+ )
+
self.assert_compile(
tuple_(table1.c.myid, table1.c.name).in_(
[tuple_(table2.c.otherid, table2.c.othername)]
"(mytable.myid, mytable.name) IN ([EXPANDING_foo])",
)
+ dialect = default.DefaultDialect()
+ dialect.tuple_in_values = True
+ self.assert_compile(
+ tuple_(table1.c.myid, table1.c.name).in_(
+ bindparam("foo", expanding=True)
+ ),
+ "(mytable.myid, mytable.name) IN ([EXPANDING_foo])",
+ dialect=dialect,
+ )
+
self.assert_compile(
table1.c.myid.in_(bindparam("foo", expanding=True)),
"mytable.myid IN ([EXPANDING_foo])",