mirror of
https://github.com/sqlalchemy/sqlalchemy.git
synced 2026-05-09 18:30:39 -04:00
Implement SQL VALUES in core.
Added a core :class:`Values` object that enables a VALUES construct
to be used in the FROM clause of an SQL statement for databases that
support it (mainly PostgreSQL and SQL Server).
Fixes: #4868
Closes: #5030
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/5030
Pull-request-sha: 84684038a8
Change-Id: Ib8109b63bc1a9dc04ab987c5322ca3375f7e824d
This commit is contained in:
committed by
Mike Bayer
parent
e6b6ec78e6
commit
8e3a05ab98
+7
@@ -0,0 +1,7 @@
|
||||
.. change::
|
||||
:tags: change, sql
|
||||
:tickets: 4868
|
||||
|
||||
Added a core :class:`Values` object that enables a VALUES construct
|
||||
to be used in the FROM clause of an SQL statement for databases that
|
||||
support it (mainly PostgreSQL and SQL Server).
|
||||
@@ -76,6 +76,7 @@ from .sql import type_coerce # noqa
|
||||
from .sql import union # noqa
|
||||
from .sql import union_all # noqa
|
||||
from .sql import update # noqa
|
||||
from .sql import values # noqa
|
||||
from .sql import within_group # noqa
|
||||
from .types import ARRAY # noqa
|
||||
from .types import BIGINT # noqa
|
||||
|
||||
@@ -77,6 +77,8 @@ from .expression import union # noqa
|
||||
from .expression import union_all # noqa
|
||||
from .expression import Update # noqa
|
||||
from .expression import update # noqa
|
||||
from .expression import Values # noqa
|
||||
from .expression import values # noqa
|
||||
from .expression import within_group # noqa
|
||||
from .visitors import ClauseVisitor # noqa
|
||||
|
||||
|
||||
@@ -2281,6 +2281,46 @@ class SQLCompiler(Compiled):
|
||||
|
||||
return text
|
||||
|
||||
def visit_values(self, element, asfrom=False, from_linter=None, **kw):
|
||||
v = "VALUES %s" % ", ".join(
|
||||
self.process(elem, literal_binds=element.literal_binds)
|
||||
for elem in element._data
|
||||
)
|
||||
|
||||
if isinstance(element.name, elements._truncated_label):
|
||||
name = self._truncated_identifier("values", element.name)
|
||||
else:
|
||||
name = element.name
|
||||
|
||||
if element._is_lateral:
|
||||
lateral = "LATERAL "
|
||||
else:
|
||||
lateral = ""
|
||||
|
||||
if asfrom:
|
||||
if from_linter:
|
||||
from_linter.froms[element] = (
|
||||
name if name is not None else "(unnamed VALUES element)"
|
||||
)
|
||||
|
||||
if name:
|
||||
v = "%s(%s)%s (%s)" % (
|
||||
lateral,
|
||||
v,
|
||||
self.get_render_as_alias_suffix(self.preparer.quote(name)),
|
||||
(
|
||||
", ".join(
|
||||
c._compiler_dispatch(
|
||||
self, include_table=False, **kw
|
||||
)
|
||||
for c in element.columns
|
||||
)
|
||||
),
|
||||
)
|
||||
else:
|
||||
v = "%s(%s)" % (lateral, v)
|
||||
return v
|
||||
|
||||
def get_render_as_alias_suffix(self, alias_name_text):
|
||||
return " AS " + alias_name_text
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ __all__ = [
|
||||
"Selectable",
|
||||
"TableClause",
|
||||
"Update",
|
||||
"Values",
|
||||
"alias",
|
||||
"and_",
|
||||
"asc",
|
||||
@@ -80,6 +81,7 @@ __all__ = [
|
||||
"Subquery",
|
||||
"TableSample",
|
||||
"tablesample",
|
||||
"values",
|
||||
]
|
||||
|
||||
|
||||
@@ -156,6 +158,7 @@ from .selectable import TableClause # noqa
|
||||
from .selectable import TableSample # noqa
|
||||
from .selectable import TextAsFrom # noqa
|
||||
from .selectable import TextualSelect # noqa
|
||||
from .selectable import Values # noqa
|
||||
from .visitors import Visitable # noqa
|
||||
from ..util.langhelpers import public_factory # noqa
|
||||
|
||||
@@ -184,6 +187,7 @@ label = public_factory(Label, ".sql.expression.label")
|
||||
case = public_factory(Case, ".sql.expression.case")
|
||||
cast = public_factory(Cast, ".sql.expression.cast")
|
||||
cte = public_factory(CTE._factory, ".sql.expression.cte")
|
||||
values = public_factory(Values, ".sql.expression.values")
|
||||
extract = public_factory(Extract, ".sql.expression.extract")
|
||||
tuple_ = public_factory(Tuple, ".sql.expression.tuple_")
|
||||
except_ = public_factory(
|
||||
|
||||
@@ -46,6 +46,7 @@ from .elements import ColumnClause
|
||||
from .elements import GroupedElement
|
||||
from .elements import Grouping
|
||||
from .elements import literal_column
|
||||
from .elements import Tuple
|
||||
from .elements import UnaryExpression
|
||||
from .visitors import InternalTraversal
|
||||
from .. import exc
|
||||
@@ -2011,6 +2012,127 @@ class ForUpdateArg(ClauseElement):
|
||||
self.of = None
|
||||
|
||||
|
||||
class Values(Generative, FromClause):
|
||||
"""represent a ``VALUES`` construct that can be used as a FROM element
|
||||
in a statement.
|
||||
|
||||
The :class:`.Values` object is created from the
|
||||
:func:`~.sql.expression.values` function.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
|
||||
"""
|
||||
|
||||
named_with_column = True
|
||||
__visit_name__ = "values"
|
||||
|
||||
_data = ()
|
||||
|
||||
_traverse_internals = [
|
||||
("_column_args", InternalTraversal.dp_clauseelement_list,),
|
||||
("_data", InternalTraversal.dp_clauseelement_list),
|
||||
("name", InternalTraversal.dp_string),
|
||||
("literal_binds", InternalTraversal.dp_boolean),
|
||||
]
|
||||
|
||||
def __init__(self, *columns, **kw):
|
||||
r"""Construct a :class:`.Values` construct.
|
||||
|
||||
The column expressions and the actual data for
|
||||
:class:`.Values` are given in two separate steps. The
|
||||
constructor receives the column expressions typically as
|
||||
:func:`.column` constructs, and the data is then passed via the
|
||||
:meth:`.Values.data` method as a list, which can be called multiple
|
||||
times to add more data, e.g.::
|
||||
|
||||
from sqlalchemy import column
|
||||
from sqlalchemy import values
|
||||
|
||||
value_expr = values(
|
||||
column('id', Integer),
|
||||
column('name', Integer),
|
||||
name="my_values"
|
||||
).data(
|
||||
[(1, 'name1'), (2, 'name2'), (3, 'name3')]
|
||||
)
|
||||
|
||||
:param \*columns: column expressions, typically composed using
|
||||
:func:`.column` objects.
|
||||
|
||||
:param name: the name for this VALUES construct. If omitted, the
|
||||
VALUES construct will be unnamed in a SQL expression. Different
|
||||
backends may have different requirements here.
|
||||
|
||||
:param literal_binds: Defaults to False. Whether or not to render
|
||||
the data values inline in the SQL output, rather than using bound
|
||||
parameters.
|
||||
|
||||
"""
|
||||
|
||||
super(Values, self).__init__()
|
||||
self._column_args = columns
|
||||
self.name = kw.pop("name", None)
|
||||
self.literal_binds = kw.pop("literal_binds", False)
|
||||
self.named_with_column = self.name is not None
|
||||
|
||||
@_generative
|
||||
def alias(self, name, **kw):
|
||||
"""Return a new :class:`.Values` construct that is a copy of this
|
||||
one with the given name.
|
||||
|
||||
This method is a VALUES-specific specialization of the
|
||||
:class:`.FromClause.alias` method.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`core_tutorial_aliases`
|
||||
|
||||
:func:`~.expression.alias`
|
||||
|
||||
"""
|
||||
self.name = name
|
||||
self.named_with_column = self.name is not None
|
||||
|
||||
@_generative
|
||||
def lateral(self, name=None):
|
||||
"""Return a new :class:`.Values` with the lateral flag set, so that
|
||||
it renders as LATERAL.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:func:`~.expression.lateral`
|
||||
|
||||
"""
|
||||
self._is_lateral = True
|
||||
if name is not None:
|
||||
self.name = name
|
||||
|
||||
@_generative
|
||||
def data(self, values):
|
||||
"""Return a new :class:`.Values` construct, adding the given data
|
||||
to the data list.
|
||||
|
||||
E.g.::
|
||||
|
||||
my_values = my_values.data([(1, 'value 1'), (2, 'value2')])
|
||||
|
||||
:param values: a sequence (i.e. list) of tuples that map to the
|
||||
column expressions given in the :class:`.Values` constructor.
|
||||
|
||||
"""
|
||||
|
||||
self._data += tuple(Tuple(*row).self_group() for row in values)
|
||||
|
||||
def _populate_column_collection(self):
|
||||
for c in self._column_args:
|
||||
self._columns.add(c)
|
||||
c.table = self
|
||||
|
||||
@property
|
||||
def _from_objects(self):
|
||||
return [self]
|
||||
|
||||
|
||||
class SelectBase(
|
||||
roles.SelectStatementRole,
|
||||
roles.DMLSelectRole,
|
||||
|
||||
@@ -25,6 +25,7 @@ from sqlalchemy import tuple_
|
||||
from sqlalchemy import union
|
||||
from sqlalchemy import union_all
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy import values
|
||||
from sqlalchemy.dialects import mysql
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.schema import Sequence
|
||||
@@ -471,6 +472,43 @@ class CoreFixtures(object):
|
||||
table("a", column("q"), column("y", Integer)),
|
||||
),
|
||||
lambda: (table_a, table_b),
|
||||
lambda: (
|
||||
values(
|
||||
column("mykey", Integer),
|
||||
column("mytext", String),
|
||||
column("myint", Integer),
|
||||
name="myvalues",
|
||||
).data([(1, "textA", 99), (2, "textB", 88)]),
|
||||
values(
|
||||
column("mykey", Integer),
|
||||
column("mytext", String),
|
||||
column("myint", Integer),
|
||||
name="myothervalues",
|
||||
).data([(1, "textA", 99), (2, "textB", 88)]),
|
||||
values(
|
||||
column("mykey", Integer),
|
||||
column("mytext", String),
|
||||
column("myint", Integer),
|
||||
name="myvalues",
|
||||
).data([(1, "textA", 89), (2, "textG", 88)]),
|
||||
values(
|
||||
column("mykey", Integer),
|
||||
column("mynottext", String),
|
||||
column("myint", Integer),
|
||||
name="myvalues",
|
||||
).data([(1, "textA", 99), (2, "textB", 88)]),
|
||||
# TODO: difference in type
|
||||
# values(
|
||||
# [
|
||||
# column("mykey", Integer),
|
||||
# column("mytext", Text),
|
||||
# column("myint", Integer),
|
||||
# ],
|
||||
# (1, "textA", 99),
|
||||
# (2, "textB", 88),
|
||||
# alias_name="myvalues",
|
||||
# ),
|
||||
),
|
||||
]
|
||||
|
||||
dont_compare_values_fixtures = [
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
from sqlalchemy import alias
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import column
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy import Integer
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy import testing
|
||||
from sqlalchemy import true
|
||||
from sqlalchemy.engine import default
|
||||
from sqlalchemy.sql import select
|
||||
from sqlalchemy.sql import Values
|
||||
from sqlalchemy.sql.compiler import FROM_LINTING
|
||||
from sqlalchemy.testing import AssertsCompiledSQL
|
||||
from sqlalchemy.testing import fixtures
|
||||
|
||||
|
||||
class ValuesTest(fixtures.TablesTest, AssertsCompiledSQL):
|
||||
__dialect__ = default.DefaultDialect(supports_native_boolean=True)
|
||||
|
||||
run_setup_bind = None
|
||||
|
||||
run_create_tables = None
|
||||
|
||||
@classmethod
|
||||
def define_tables(cls, metadata):
|
||||
Table(
|
||||
"people",
|
||||
metadata,
|
||||
Column("people_id", Integer, primary_key=True),
|
||||
Column("age", Integer),
|
||||
Column("name", String(30)),
|
||||
)
|
||||
Table(
|
||||
"bookcases",
|
||||
metadata,
|
||||
Column("bookcase_id", Integer, primary_key=True),
|
||||
Column(
|
||||
"bookcase_owner_id", Integer, ForeignKey("people.people_id")
|
||||
),
|
||||
Column("bookcase_shelves", Integer),
|
||||
Column("bookcase_width", Integer),
|
||||
)
|
||||
Table(
|
||||
"books",
|
||||
metadata,
|
||||
Column("book_id", Integer, primary_key=True),
|
||||
Column(
|
||||
"bookcase_id", Integer, ForeignKey("bookcases.bookcase_id")
|
||||
),
|
||||
Column("book_owner_id", Integer, ForeignKey("people.people_id")),
|
||||
Column("book_weight", Integer),
|
||||
)
|
||||
|
||||
def test_column_quoting(self):
|
||||
v1 = Values(
|
||||
column("CaseSensitive", Integer),
|
||||
column("has spaces", String),
|
||||
name="Spaces and Cases",
|
||||
).data([(1, "textA", 99), (2, "textB", 88)])
|
||||
self.assert_compile(
|
||||
select([v1]),
|
||||
'SELECT "Spaces and Cases"."CaseSensitive", '
|
||||
'"Spaces and Cases"."has spaces" FROM '
|
||||
"(VALUES (:param_1, :param_2, :param_3), "
|
||||
"(:param_4, :param_5, :param_6)) "
|
||||
'AS "Spaces and Cases" ("CaseSensitive", "has spaces")',
|
||||
)
|
||||
|
||||
@testing.fixture
|
||||
def literal_parameter_fixture(self):
|
||||
def go(literal_binds):
|
||||
return Values(
|
||||
column("mykey", Integer),
|
||||
column("mytext", String),
|
||||
column("myint", Integer),
|
||||
name="myvalues",
|
||||
literal_binds=literal_binds,
|
||||
).data([(1, "textA", 99), (2, "textB", 88)])
|
||||
|
||||
return go
|
||||
|
||||
def test_bound_parameters(self, literal_parameter_fixture):
|
||||
literal_parameter_fixture = literal_parameter_fixture(False)
|
||||
|
||||
stmt = select([literal_parameter_fixture])
|
||||
|
||||
self.assert_compile(
|
||||
stmt,
|
||||
"SELECT myvalues.mykey, myvalues.mytext, myvalues.myint FROM "
|
||||
"(VALUES (:param_1, :param_2, :param_3), "
|
||||
"(:param_4, :param_5, :param_6)"
|
||||
") AS myvalues (mykey, mytext, myint)",
|
||||
checkparams={
|
||||
"param_1": 1,
|
||||
"param_2": "textA",
|
||||
"param_3": 99,
|
||||
"param_4": 2,
|
||||
"param_5": "textB",
|
||||
"param_6": 88,
|
||||
},
|
||||
)
|
||||
|
||||
def test_literal_parameters(self, literal_parameter_fixture):
|
||||
literal_parameter_fixture = literal_parameter_fixture(True)
|
||||
|
||||
stmt = select([literal_parameter_fixture])
|
||||
|
||||
self.assert_compile(
|
||||
stmt,
|
||||
"SELECT myvalues.mykey, myvalues.mytext, myvalues.myint FROM "
|
||||
"(VALUES (1, 'textA', 99), (2, 'textB', 88)"
|
||||
") AS myvalues (mykey, mytext, myint)",
|
||||
checkparams={},
|
||||
)
|
||||
|
||||
def test_with_join_unnamed(self):
|
||||
people = self.tables.people
|
||||
values = Values(
|
||||
column("column1", Integer), column("column2", Integer),
|
||||
).data([(1, 1), (2, 1), (3, 2), (3, 3)])
|
||||
stmt = select([people, values]).select_from(
|
||||
people.join(values, values.c.column2 == people.c.people_id)
|
||||
)
|
||||
self.assert_compile(
|
||||
stmt,
|
||||
"SELECT people.people_id, people.age, people.name, column1, "
|
||||
"column2 FROM people JOIN (VALUES (:param_1, :param_2), "
|
||||
"(:param_3, :param_4), (:param_5, :param_6), "
|
||||
"(:param_7, :param_8)) "
|
||||
"ON people.people_id = column2",
|
||||
checkparams={
|
||||
"param_1": 1,
|
||||
"param_2": 1,
|
||||
"param_3": 2,
|
||||
"param_4": 1,
|
||||
"param_5": 3,
|
||||
"param_6": 2,
|
||||
"param_7": 3,
|
||||
"param_8": 3,
|
||||
},
|
||||
)
|
||||
|
||||
def test_with_join_named(self):
|
||||
people = self.tables.people
|
||||
values = Values(
|
||||
column("bookcase_id", Integer),
|
||||
column("bookcase_owner_id", Integer),
|
||||
name="bookcases",
|
||||
).data([(1, 1), (2, 1), (3, 2), (3, 3)])
|
||||
stmt = select([people, values]).select_from(
|
||||
people.join(
|
||||
values, values.c.bookcase_owner_id == people.c.people_id
|
||||
)
|
||||
)
|
||||
self.assert_compile(
|
||||
stmt,
|
||||
"SELECT people.people_id, people.age, people.name, "
|
||||
"bookcases.bookcase_id, bookcases.bookcase_owner_id FROM people "
|
||||
"JOIN (VALUES (:param_1, :param_2), (:param_3, :param_4), "
|
||||
"(:param_5, :param_6), (:param_7, :param_8)) AS bookcases "
|
||||
"(bookcase_id, bookcase_owner_id) "
|
||||
"ON people.people_id = bookcases.bookcase_owner_id",
|
||||
checkparams={
|
||||
"param_1": 1,
|
||||
"param_2": 1,
|
||||
"param_3": 2,
|
||||
"param_4": 1,
|
||||
"param_5": 3,
|
||||
"param_6": 2,
|
||||
"param_7": 3,
|
||||
"param_8": 3,
|
||||
},
|
||||
)
|
||||
|
||||
def test_with_aliased_join(self):
|
||||
people = self.tables.people
|
||||
values = (
|
||||
Values(
|
||||
column("bookcase_id", Integer),
|
||||
column("bookcase_owner_id", Integer),
|
||||
)
|
||||
.data([(1, 1), (2, 1), (3, 2), (3, 3)])
|
||||
.alias("bookcases")
|
||||
)
|
||||
stmt = select([people, values]).select_from(
|
||||
people.join(
|
||||
values, values.c.bookcase_owner_id == people.c.people_id
|
||||
)
|
||||
)
|
||||
self.assert_compile(
|
||||
stmt,
|
||||
"SELECT people.people_id, people.age, people.name, "
|
||||
"bookcases.bookcase_id, bookcases.bookcase_owner_id FROM people "
|
||||
"JOIN (VALUES (:param_1, :param_2), (:param_3, :param_4), "
|
||||
"(:param_5, :param_6), (:param_7, :param_8)) AS bookcases "
|
||||
"(bookcase_id, bookcase_owner_id) "
|
||||
"ON people.people_id = bookcases.bookcase_owner_id",
|
||||
checkparams={
|
||||
"param_1": 1,
|
||||
"param_2": 1,
|
||||
"param_3": 2,
|
||||
"param_4": 1,
|
||||
"param_5": 3,
|
||||
"param_6": 2,
|
||||
"param_7": 3,
|
||||
"param_8": 3,
|
||||
},
|
||||
)
|
||||
|
||||
def test_with_standalone_aliased_join(self):
|
||||
people = self.tables.people
|
||||
values = Values(
|
||||
column("bookcase_id", Integer),
|
||||
column("bookcase_owner_id", Integer),
|
||||
).data([(1, 1), (2, 1), (3, 2), (3, 3)])
|
||||
values = alias(values, "bookcases")
|
||||
|
||||
stmt = select([people, values]).select_from(
|
||||
people.join(
|
||||
values, values.c.bookcase_owner_id == people.c.people_id
|
||||
)
|
||||
)
|
||||
self.assert_compile(
|
||||
stmt,
|
||||
"SELECT people.people_id, people.age, people.name, "
|
||||
"bookcases.bookcase_id, bookcases.bookcase_owner_id FROM people "
|
||||
"JOIN (VALUES (:param_1, :param_2), (:param_3, :param_4), "
|
||||
"(:param_5, :param_6), (:param_7, :param_8)) AS bookcases "
|
||||
"(bookcase_id, bookcase_owner_id) "
|
||||
"ON people.people_id = bookcases.bookcase_owner_id",
|
||||
checkparams={
|
||||
"param_1": 1,
|
||||
"param_2": 1,
|
||||
"param_3": 2,
|
||||
"param_4": 1,
|
||||
"param_5": 3,
|
||||
"param_6": 2,
|
||||
"param_7": 3,
|
||||
"param_8": 3,
|
||||
},
|
||||
)
|
||||
|
||||
def test_lateral(self):
|
||||
people = self.tables.people
|
||||
values = (
|
||||
Values(
|
||||
column("bookcase_id", Integer),
|
||||
column("bookcase_owner_id", Integer),
|
||||
name="bookcases",
|
||||
)
|
||||
.data([(1, 1), (2, 1), (3, 2), (3, 3)])
|
||||
.lateral()
|
||||
)
|
||||
stmt = select([people, values]).select_from(
|
||||
people.join(values, true())
|
||||
)
|
||||
self.assert_compile(
|
||||
stmt,
|
||||
"SELECT people.people_id, people.age, people.name, "
|
||||
"bookcases.bookcase_id, bookcases.bookcase_owner_id FROM people "
|
||||
"JOIN LATERAL (VALUES (:param_1, :param_2), (:param_3, :param_4), "
|
||||
"(:param_5, :param_6), (:param_7, :param_8)) AS bookcases "
|
||||
"(bookcase_id, bookcase_owner_id) "
|
||||
"ON true",
|
||||
checkparams={
|
||||
"param_1": 1,
|
||||
"param_2": 1,
|
||||
"param_3": 2,
|
||||
"param_4": 1,
|
||||
"param_5": 3,
|
||||
"param_6": 2,
|
||||
"param_7": 3,
|
||||
"param_8": 3,
|
||||
},
|
||||
)
|
||||
|
||||
def test_from_linting_named(self):
|
||||
people = self.tables.people
|
||||
values = Values(
|
||||
column("bookcase_id", Integer),
|
||||
column("bookcase_owner_id", Integer),
|
||||
name="bookcases",
|
||||
).data([(1, 1), (2, 1), (3, 2), (3, 3)])
|
||||
stmt = select([people, values])
|
||||
|
||||
with testing.expect_warnings(
|
||||
r"SELECT statement has a cartesian product between FROM "
|
||||
r'element\(s\) "(?:bookcases|people)" and '
|
||||
r'FROM element "(?:people|bookcases)"'
|
||||
):
|
||||
stmt.compile(linting=FROM_LINTING)
|
||||
|
||||
def test_from_linting_unnamed(self):
|
||||
people = self.tables.people
|
||||
values = Values(
|
||||
column("bookcase_id", Integer),
|
||||
column("bookcase_owner_id", Integer),
|
||||
).data([(1, 1), (2, 1), (3, 2), (3, 3)])
|
||||
stmt = select([people, values])
|
||||
|
||||
with testing.expect_warnings(
|
||||
r"SELECT statement has a cartesian product between FROM "
|
||||
r'element\(s\) "(?:\(unnamed VALUES element\)|people)" and '
|
||||
r'FROM element "(?:people|\(unnamed VALUES element\))"'
|
||||
):
|
||||
stmt.compile(linting=FROM_LINTING)
|
||||
Reference in New Issue
Block a user