summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/sql
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/sql')
-rw-r--r--lib/sqlalchemy/sql/base.py10
-rw-r--r--lib/sqlalchemy/sql/coercions.py21
-rw-r--r--lib/sqlalchemy/sql/compiler.py64
-rw-r--r--lib/sqlalchemy/sql/elements.py175
-rw-r--r--lib/sqlalchemy/sql/expression.py2
-rw-r--r--lib/sqlalchemy/sql/functions.py382
-rw-r--r--lib/sqlalchemy/sql/roles.py4
-rw-r--r--lib/sqlalchemy/sql/selectable.py178
-rw-r--r--lib/sqlalchemy/sql/sqltypes.py64
-rw-r--r--lib/sqlalchemy/sql/traversals.py5
-rw-r--r--lib/sqlalchemy/sql/type_api.py2
-rw-r--r--lib/sqlalchemy/sql/visitors.py6
12 files changed, 785 insertions, 128 deletions
diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py
index fe9455fb9..c76c9ae14 100644
--- a/lib/sqlalchemy/sql/base.py
+++ b/lib/sqlalchemy/sql/base.py
@@ -935,13 +935,11 @@ class Executable(roles.CoerceTextStatementRole, Generative):
@util.deprecated_20(
":meth:`.Executable.scalar`",
- alternative="All statement execution in SQLAlchemy 2.0 is performed "
- "by the :meth:`_engine.Connection.execute` method of "
+ alternative="Scalar execution in SQLAlchemy 2.0 is performed "
+ "by the :meth:`_engine.Connection.scalar` method of "
":class:`_engine.Connection`, "
- "or in the ORM by the :meth:`.Session.execute` method of "
- ":class:`.Session`; the :meth:`_future.Result.scalar` "
- "method can then be "
- "used to return a scalar result.",
+ "or in the ORM by the :meth:`.Session.scalar` method of "
+ ":class:`.Session`.",
)
def scalar(self, *multiparams, **params):
"""Compile and execute this :class:`.Executable`, returning the
diff --git a/lib/sqlalchemy/sql/coercions.py b/lib/sqlalchemy/sql/coercions.py
index 3b972be41..d4b208c56 100644
--- a/lib/sqlalchemy/sql/coercions.py
+++ b/lib/sqlalchemy/sql/coercions.py
@@ -437,6 +437,20 @@ class ExpressionElementImpl(_ColumnCoercions, RoleImpl):
except exc.ArgumentError as err:
self._raise_for_expected(element, err=err)
+ def _raise_for_expected(self, element, argname=None, resolved=None, **kw):
+ if isinstance(element, roles.AnonymizedFromClauseRole):
+ advice = (
+ "To create a "
+ "column expression from a FROM clause row "
+ "as a whole, use the .table_valued() method."
+ )
+ else:
+ advice = None
+
+ return super(ExpressionElementImpl, self)._raise_for_expected(
+ element, argname=argname, resolved=resolved, advice=advice, **kw
+ )
+
class BinaryElementImpl(ExpressionElementImpl, RoleImpl):
@@ -603,6 +617,13 @@ class ColumnArgumentOrKeyImpl(_ReturnsStringKey, RoleImpl):
__slots__ = ()
+class StrAsPlainColumnImpl(_CoerceLiterals, RoleImpl):
+ __slots__ = ()
+
+ def _text_coercion(self, element, argname=None):
+ return elements.ColumnClause(element)
+
+
class ByOfImpl(_CoerceLiterals, _ColumnCoercions, RoleImpl, roles.ByOfRole):
__slots__ = ()
diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py
index 3925b251d..696b38e64 100644
--- a/lib/sqlalchemy/sql/compiler.py
+++ b/lib/sqlalchemy/sql/compiler.py
@@ -494,6 +494,12 @@ class Compiled(object):
else:
raise exc.ObjectNotExecutableError(self.statement)
+ def visit_unsupported_compilation(self, element, err):
+ util.raise_(
+ exc.UnsupportedCompilationError(self, type(element)),
+ replace_context=err,
+ )
+
@property
def sql_compiler(self):
"""Return a Compiled that is capable of processing SQL expressions.
@@ -1718,13 +1724,18 @@ class SQLCompiler(Compiled):
extract.expr._compiler_dispatch(self, **kwargs),
)
+ def visit_scalar_function_column(self, element, **kw):
+ compiled_fn = self.visit_function(element.fn, **kw)
+ compiled_col = self.visit_column(element, **kw)
+ return "(%s).%s" % (compiled_fn, compiled_col)
+
def visit_function(self, func, add_to_result_map=None, **kwargs):
if add_to_result_map is not None:
add_to_result_map(func.name, func.name, (), func.type)
disp = getattr(self, "visit_%s_func" % func.name.lower(), None)
if disp:
- return disp(func, **kwargs)
+ text = disp(func, **kwargs)
else:
name = FUNCTIONS.get(func.__class__, None)
if name:
@@ -1739,7 +1750,7 @@ class SQLCompiler(Compiled):
else name
)
name = name + "%(expr)s"
- return ".".join(
+ text = ".".join(
[
(
self.preparer.quote(tok)
@@ -1752,6 +1763,10 @@ class SQLCompiler(Compiled):
+ [name]
) % {"expr": self.function_argspec(func, **kwargs)}
+ if func._with_ordinality:
+ text += " WITH ORDINALITY"
+ return text
+
def visit_next_value_func(self, next_value, **kw):
return self.visit_sequence(next_value.sequence)
@@ -2533,6 +2548,15 @@ class SQLCompiler(Compiled):
else:
return self.preparer.format_alias(cte, cte_name)
+ def visit_table_valued_alias(self, element, **kw):
+ if element._is_lateral:
+ return self.visit_lateral(element, **kw)
+ else:
+ return self.visit_alias(element, **kw)
+
+ def visit_table_valued_column(self, element, **kw):
+ return self.visit_column(element, **kw)
+
def visit_alias(
self,
alias,
@@ -2584,6 +2608,24 @@ class SQLCompiler(Compiled):
ret = inner + self.get_render_as_alias_suffix(
self.preparer.format_alias(alias, alias_name)
)
+
+ if alias._supports_derived_columns and alias._render_derived:
+ ret += "(%s)" % (
+ ", ".join(
+ "%s%s"
+ % (
+ col.name,
+ " %s"
+ % self.dialect.type_compiler.process(
+ col.type, **kwargs
+ )
+ if alias._render_derived_w_types
+ else "",
+ )
+ for col in alias.c
+ )
+ )
+
if fromhints and alias in fromhints:
ret = self.format_from_hint_text(
ret, alias, fromhints[alias], iscrud
@@ -2600,9 +2642,9 @@ class SQLCompiler(Compiled):
kw["subquery"] = True
return self.visit_alias(subquery, **kw)
- def visit_lateral(self, lateral, **kw):
+ def visit_lateral(self, lateral_, **kw):
kw["lateral"] = True
- return "LATERAL %s" % self.visit_alias(lateral, **kw)
+ return "LATERAL %s" % self.visit_alias(lateral_, **kw)
def visit_tablesample(self, tablesample, asfrom=False, **kw):
text = "%s TABLESAMPLE %s" % (
@@ -3737,6 +3779,20 @@ class StrSQLCompiler(SQLCompiler):
def _fallback_column_name(self, column):
return "<name unknown>"
+ @util.preload_module("sqlalchemy.engine.url")
+ def visit_unsupported_compilation(self, element, err, **kw):
+ if element.stringify_dialect != "default":
+ url = util.preloaded.engine_url
+ dialect = url.URL.create(element.stringify_dialect).get_dialect()()
+
+ compiler = dialect.statement_compiler(dialect, None)
+ if not isinstance(compiler, StrSQLCompiler):
+ return compiler.process(element)
+
+ return super(StrSQLCompiler, self).visit_unsupported_compilation(
+ element, err
+ )
+
def visit_getitem_binary(self, binary, operator, **kw):
return "%s[%s]" % (
self.process(binary.left, **kw),
diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py
index a9f21cd5f..1bdef1932 100644
--- a/lib/sqlalchemy/sql/elements.py
+++ b/lib/sqlalchemy/sql/elements.py
@@ -3856,6 +3856,8 @@ class Over(ColumnElement):
.. seealso::
+ :ref:`tutorial_window_functions` - in the :ref:`unified_tutorial`
+
:data:`.expression.func`
:func:`_expression.within_group`
@@ -4007,6 +4009,9 @@ class WithinGroup(ColumnElement):
.. seealso::
+ :ref:`tutorial_functions_within_group` - in the
+ :ref:`unified_tutorial`
+
:data:`.expression.func`
:func:`_expression.over`
@@ -4104,8 +4109,10 @@ class FunctionFilter(ColumnElement):
.. seealso::
- :meth:`.FunctionElement.filter`
+ :ref:`tutorial_functions_within_group` - in the
+ :ref:`unified_tutorial`
+ :meth:`.FunctionElement.filter`
"""
self.func = func
@@ -4317,11 +4324,82 @@ class Label(roles.LabeledColumnExprRole, ColumnElement):
return self.key, e
+class NamedColumn(ColumnElement):
+ is_literal = False
+ table = None
+
+ def _compare_name_for_result(self, other):
+ return (hasattr(other, "name") and self.name == other.name) or (
+ hasattr(other, "_label") and self._label == other._label
+ )
+
+ @util.memoized_property
+ def description(self):
+ if util.py3k:
+ return self.name
+ else:
+ return self.name.encode("ascii", "backslashreplace")
+
+ @HasMemoized.memoized_attribute
+ def _key_label(self):
+ if self.key != self.name:
+ return self._gen_label(self.key)
+ else:
+ return self._label
+
+ @HasMemoized.memoized_attribute
+ def _label(self):
+ return self._gen_label(self.name)
+
+ @HasMemoized.memoized_attribute
+ def _render_label_in_columns_clause(self):
+ return True
+
+ def _gen_label(self, name, dedupe_on_key=True):
+ return name
+
+ def _bind_param(self, operator, obj, type_=None, expanding=False):
+ return BindParameter(
+ self.key,
+ obj,
+ _compared_to_operator=operator,
+ _compared_to_type=self.type,
+ type_=type_,
+ unique=True,
+ expanding=expanding,
+ )
+
+ def _make_proxy(
+ self,
+ selectable,
+ name=None,
+ name_is_truncatable=False,
+ disallow_is_literal=False,
+ **kw
+ ):
+ c = ColumnClause(
+ coercions.expect(roles.TruncatedLabelRole, name or self.name)
+ if name_is_truncatable
+ else (name or self.name),
+ type_=self.type,
+ _selectable=selectable,
+ is_literal=False,
+ )
+ c._propagate_attrs = selectable._propagate_attrs
+ if name is None:
+ c.key = self.key
+ c._proxies = [self]
+ if selectable._is_clone_of is not None:
+ c._is_clone_of = selectable._is_clone_of.columns.get(c.key)
+ return c.key, c
+
+
class ColumnClause(
roles.DDLReferredColumnRole,
roles.LabeledColumnExprRole,
+ roles.StrAsPlainColumnRole,
Immutable,
- ColumnElement,
+ NamedColumn,
):
"""Represents a column expression from any textual string.
@@ -4360,6 +4438,9 @@ class ColumnClause(
"""
+ table = None
+ is_literal = False
+
__visit_name__ = "column"
_traverse_internals = [
@@ -4470,27 +4551,6 @@ class ColumnClause(
self.type = type_api.to_instance(type_)
self.is_literal = is_literal
- def _compare_name_for_result(self, other):
- if (
- self.is_literal
- or self.table is None
- or self.table._is_textual
- or not hasattr(other, "proxy_set")
- or (
- isinstance(other, ColumnClause)
- and (
- other.is_literal
- or other.table is None
- or other.table._is_textual
- )
- )
- ):
- return (hasattr(other, "name") and self.name == other.name) or (
- hasattr(other, "_label") and self._label == other._label
- )
- else:
- return other.proxy_set.intersection(self.proxy_set)
-
def get_children(self, column_tables=False, **kw):
# override base get_children() to not return the Table
# or selectable that is parent to this column. Traversals
@@ -4505,24 +4565,6 @@ class ColumnClause(
else:
return []
- @util.memoized_property
- def description(self):
- if util.py3k:
- return self.name
- else:
- return self.name.encode("ascii", "backslashreplace")
-
- @HasMemoized.memoized_attribute
- def _key_label(self):
- if self.key != self.name:
- return self._gen_label(self.key)
- else:
- return self._label
-
- @HasMemoized.memoized_attribute
- def _label(self):
- return self._gen_label(self.name)
-
@HasMemoized.memoized_attribute
def _render_label_in_columns_clause(self):
return self.table is not None
@@ -4531,6 +4573,27 @@ class ColumnClause(
def _ddl_label(self):
return self._gen_label(self.name, dedupe_on_key=False)
+ def _compare_name_for_result(self, other):
+ if (
+ self.is_literal
+ or self.table is None
+ or self.table._is_textual
+ or not hasattr(other, "proxy_set")
+ or (
+ isinstance(other, ColumnClause)
+ and (
+ other.is_literal
+ or other.table is None
+ or other.table._is_textual
+ )
+ )
+ ):
+ return (hasattr(other, "name") and self.name == other.name) or (
+ hasattr(other, "_label") and self._label == other._label
+ )
+ else:
+ return other.proxy_set.intersection(self.proxy_set)
+
def _gen_label(self, name, dedupe_on_key=True):
t = self.table
if self.is_literal:
@@ -4575,17 +4638,6 @@ class ColumnClause(
else:
return name
- def _bind_param(self, operator, obj, type_=None, expanding=False):
- return BindParameter(
- self.key,
- obj,
- _compared_to_operator=operator,
- _compared_to_type=self.type,
- type_=type_,
- unique=True,
- expanding=expanding,
- )
-
def _make_proxy(
self,
selectable,
@@ -4627,6 +4679,25 @@ class ColumnClause(
return c.key, c
+class TableValuedColumn(NamedColumn):
+ __visit_name__ = "table_valued_column"
+
+ _traverse_internals = [
+ ("name", InternalTraversal.dp_anon_name),
+ ("type", InternalTraversal.dp_type),
+ ("scalar_alias", InternalTraversal.dp_clauseelement),
+ ]
+
+ def __init__(self, scalar_alias, type_):
+ self.scalar_alias = scalar_alias
+ self.key = self.name = scalar_alias.name
+ self.type = type_
+
+ @property
+ def _from_objects(self):
+ return [self.scalar_alias]
+
+
class CollationClause(ColumnElement):
__visit_name__ = "collation"
diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py
index 3eb1443a3..46f13fedd 100644
--- a/lib/sqlalchemy/sql/expression.py
+++ b/lib/sqlalchemy/sql/expression.py
@@ -34,6 +34,7 @@ __all__ = [
"Select",
"Selectable",
"TableClause",
+ "TableValuedAlias",
"Update",
"Values",
"alias",
@@ -172,6 +173,7 @@ from .selectable import Subquery # noqa
from .selectable import subquery # noqa
from .selectable import TableClause # noqa
from .selectable import TableSample # noqa
+from .selectable import TableValuedAlias # noqa
from .selectable import TextAsFrom # noqa
from .selectable import TextualSelect # noqa
from .selectable import Values # noqa
diff --git a/lib/sqlalchemy/sql/functions.py b/lib/sqlalchemy/sql/functions.py
index a9ea98d04..78f7ead2e 100644
--- a/lib/sqlalchemy/sql/functions.py
+++ b/lib/sqlalchemy/sql/functions.py
@@ -17,6 +17,7 @@ from . import sqltypes
from . import util as sqlutil
from .base import ColumnCollection
from .base import Executable
+from .base import Generative
from .base import HasMemoized
from .elements import _type_from_args
from .elements import BinaryExpression
@@ -28,11 +29,12 @@ from .elements import Extract
from .elements import FunctionFilter
from .elements import Grouping
from .elements import literal_column
+from .elements import NamedColumn
from .elements import Over
from .elements import WithinGroup
-from .selectable import Alias
from .selectable import FromClause
from .selectable import Select
+from .selectable import TableValuedAlias
from .visitors import InternalTraversal
from .visitors import TraversibleType
from .. import util
@@ -63,7 +65,7 @@ def register_function(identifier, fn, package="_default"):
reg[identifier] = fn
-class FunctionElement(Executable, ColumnElement, FromClause):
+class FunctionElement(Executable, ColumnElement, FromClause, Generative):
"""Base for SQL function-oriented constructs.
.. seealso::
@@ -80,11 +82,17 @@ class FunctionElement(Executable, ColumnElement, FromClause):
"""
- _traverse_internals = [("clause_expr", InternalTraversal.dp_clauseelement)]
+ _traverse_internals = [
+ ("clause_expr", InternalTraversal.dp_clauseelement),
+ ("_with_ordinality", InternalTraversal.dp_boolean),
+ ("_table_value_type", InternalTraversal.dp_has_cache_key),
+ ]
packagenames = ()
_has_args = False
+ _with_ordinality = False
+ _table_value_type = None
def __init__(self, *clauses, **kwargs):
r"""Construct a :class:`.FunctionElement`.
@@ -123,27 +131,169 @@ class FunctionElement(Executable, ColumnElement, FromClause):
self, multiparams, params, execution_options
)
+ def scalar_table_valued(self, name, type_=None):
+ """Return a column expression that's against this
+ :class:`_functions.FunctionElement` as a scalar
+ table-valued expression.
+
+ The returned expression is similar to that returned by a single column
+ accessed off of a :meth:`_functions.FunctionElement.table_valued`
+ construct, except no FROM clause is generated; the function is rendered
+ in the similar way as a scalar subquery.
+
+ E.g.::
+
+ >>> from sqlalchemy import func, select
+ >>> fn = func.jsonb_each("{'k', 'v'}").scalar_table_valued("key")
+ >>> print(select(fn))
+ SELECT (jsonb_each(:jsonb_each_1)).key
+
+ .. versionadded:: 1.4.0b2
+
+ .. seealso::
+
+ :meth:`_functions.FunctionElement.table_valued`
+
+ :meth:`_functions.FunctionElement.alias`
+
+ :meth:`_functions.FunctionElement.column_valued`
+
+ """ # noqa E501
+
+ return ScalarFunctionColumn(self, name, type_)
+
+ def table_valued(self, *expr, **kw):
+ """Return a :class:`_sql.TableValuedAlias` representation of this
+ :class:`_functions.FunctionElement` with table-valued expressions added.
+
+ e.g.::
+
+ >>> fn = (
+ ... func.generate_series(1, 5).
+ ... table_valued("value", "start", "stop", "step")
+ ... )
+
+ >>> print(select(fn))
+ SELECT anon_1.value, anon_1.start, anon_1.stop, anon_1.step
+ FROM generate_series(:generate_series_1, :generate_series_2) AS anon_1
+
+ >>> print(select(fn.c.value, fn.c.stop).where(fn.c.value > 2))
+ SELECT anon_1.value, anon_1.stop
+ FROM generate_series(:generate_series_1, :generate_series_2) AS anon_1
+ WHERE anon_1.value > :value_1
+
+ A WITH ORDINALITY expression may be generated by passing the keyword
+ argument "with_ordinality"::
+
+ >>> fn = func.generate_series(4, 1, -1).table_valued("gen", with_ordinality="ordinality")
+ >>> print(select(fn))
+ SELECT anon_1.gen, anon_1.ordinality
+ FROM generate_series(:generate_series_1, :generate_series_2, :generate_series_3) WITH ORDINALITY AS anon_1
+
+ :param \*expr: A series of string column names that will be added to the
+ ``.c`` collection of the resulting :class:`_sql.TableValuedAlias`
+ construct as columns. :func:`_sql.column` objects with or without
+ datatypes may also be used.
+
+ :param name: optional name to assign to the alias name that's generated.
+ If omitted, a unique anonymizing name is used.
+
+ :param with_ordinality: string name that when present results in the
+ ``WITH ORDINALITY`` clause being added to the alias, and the given
+ string name will be added as a column to the .c collection
+ of the resulting :class:`_sql.TableValuedAlias`.
+
+ .. versionadded:: 1.4.0b2
+
+ .. seealso::
+
+ :ref:`tutorial_functions_table_valued` - in the :ref:`unified_tutorial`
+
+ :ref:`postgresql_table_valued` - in the :ref:`postgresql_toplevel` documentation
+
+ :meth:`_functions.FunctionElement.scalar_table_valued` - variant of
+ :meth:`_functions.FunctionElement.table_valued` which delivers the
+ complete table valued expression as a scalar column expression
+
+ :meth:`_functions.FunctionElement.column_valued`
+
+ :meth:`_sql.TableValuedAlias.render_derived` - renders the alias
+ using a derived column clause, e.g. ``AS name(col1, col2, ...)``
+
+ """ # noqa 501
+
+ new_func = self._generate()
+
+ with_ordinality = kw.pop("with_ordinality", None)
+ name = kw.pop("name", None)
+
+ if with_ordinality:
+ expr += (with_ordinality,)
+ new_func._with_ordinality = True
+
+ new_func.type = new_func._table_value_type = sqltypes.TableValueType(
+ *expr
+ )
+
+ return new_func.alias(name=name)
+
+ def column_valued(self, name=None):
+ """Return this :class:`_functions.FunctionElement` as a column expression that
+ selects from itself as a FROM clause.
+
+ E.g.::
+
+ >>> from sqlalchemy import select, func
+ >>> gs = func.generate_series(1, 5, -1).column_valued()
+ >>> print(select(gs))
+ SELECT anon_1
+ FROM generate_series(:generate_series_1, :generate_series_2, :generate_series_3) AS anon_1
+
+ This is shorthand for::
+
+ gs = func.generate_series(1, 5, -1).alias().column
+
+
+ .. seealso::
+
+ :ref:`tutorial_functions_column_valued` - in the :ref:`unified_tutorial`
+
+ :ref:`postgresql_column_valued` - in the :ref:`postgresql_toplevel` documentation
+
+ :meth:`_functions.FunctionElement.table_valued`
+
+ """ # noqa 501
+
+ return self.alias(name=name).column
+
@property
def columns(self):
r"""The set of columns exported by this :class:`.FunctionElement`.
- Function objects currently have no result column names built in;
- this method returns a single-element column collection with
- an anonymously named column.
+ This is a placeholder collection that allows the function to be
+ placed in the FROM clause of a statement::
- An interim approach to providing named columns for a function
- as a FROM clause is to build a :func:`_expression.select` with the
- desired columns::
+ >>> from sqlalchemy import column, select, func
+ >>> stmt = select(column('x'), column('y')).select_from(func.myfunction())
+ >>> print(stmt)
+ SELECT x, y FROM myfunction()
- from sqlalchemy.sql import column
+ The above form is a legacy feature that is now superseded by the
+ fully capable :meth:`_functions.FunctionElement.table_valued`
+ method; see that method for details.
- stmt = select(column('x'), column('y')).\
- select_from(func.myfunction())
+ .. seealso::
+ :meth:`_functions.FunctionElement.table_valued` - generates table-valued
+ SQL function expressions.
- """
- col = self.label(None)
- return ColumnCollection(columns=[(col.key, col)])
+ """ # noqa E501
+ if self.type._is_table_value:
+ cols = self.type._elements
+ else:
+ cols = [self.label(None)]
+
+ return ColumnCollection(columns=[(col.key, col) for col in cols])
@HasMemoized.memoized_attribute
def clauses(self):
@@ -170,6 +320,12 @@ class FunctionElement(Executable, ColumnElement, FromClause):
See :func:`_expression.over` for a full description.
+ .. seealso::
+
+ :func:`_expression.over`
+
+ :ref:`tutorial_window_functions` - in the :ref:`unified_tutorial`
+
"""
return Over(
self,
@@ -191,6 +347,12 @@ class FunctionElement(Executable, ColumnElement, FromClause):
.. versionadded:: 1.1
+ .. seealso::
+
+ :ref:`tutorial_functions_within_group` -
+ in the :ref:`unified_tutorial`
+
+
"""
return WithinGroup(self, *order_by)
@@ -213,6 +375,9 @@ class FunctionElement(Executable, ColumnElement, FromClause):
.. seealso::
+ :ref:`tutorial_functions_within_group` -
+ in the :ref:`unified_tutorial`
+
:class:`.FunctionFilter`
:func:`.funcfilter`
@@ -226,6 +391,9 @@ class FunctionElement(Executable, ColumnElement, FromClause):
def as_comparison(self, left_index, right_index):
"""Interpret this expression as a boolean comparison between two values.
+ This method is used for an ORM use case described at
+ :ref:`relationship_custom_operator_sql_function`.
+
A hypothetical SQL function "is_equal()" which compares to values
for equality would be written in the Core expression language as::
@@ -250,9 +418,8 @@ class FunctionElement(Executable, ColumnElement, FromClause):
to manipulate the "left" and "right" sides of the ON clause of a JOIN
expression. The purpose of this method is to provide a SQL function
construct that can also supply this information to the ORM, when used
- with the :paramref:`_orm.relationship.primaryjoin` parameter.
- The return
- value is a containment object called :class:`.FunctionAsBinary`.
+ with the :paramref:`_orm.relationship.primaryjoin` parameter. The
+ return value is a containment object called :class:`.FunctionAsBinary`.
An ORM example is as follows::
@@ -286,6 +453,11 @@ class FunctionElement(Executable, ColumnElement, FromClause):
.. versionadded:: 1.3
+ .. seealso::
+
+ :ref:`relationship_custom_operator_sql_function` -
+ example use within the ORM
+
"""
return FunctionAsBinary(self, left_index, right_index)
@@ -305,37 +477,65 @@ class FunctionElement(Executable, ColumnElement, FromClause):
return None
- def alias(self, name=None, flat=False):
+ def alias(self, name=None):
r"""Produce a :class:`_expression.Alias` construct against this
:class:`.FunctionElement`.
+ .. tip::
+
+ The :meth:`_functions.FunctionElement.alias` method is part of the
+ mechanism by which "table valued" SQL functions are created.
+ However, most use cases are covered by higher level methods on
+ :class:`_functions.FunctionElement` including
+ :meth:`_functions.FunctionElement.table_valued`, and
+ :meth:`_functions.FunctionElement.column_valued`.
+
This construct wraps the function in a named alias which
is suitable for the FROM clause, in the style accepted for example
- by PostgreSQL.
+ by PostgreSQL. A column expression is also provided using the
+ special ``.column`` attribute, which may
+ be used to refer to the output of the function as a scalar value
+ in the columns or where clause, for a backend such as PostgreSQL.
+
+ For a full table-valued expression, use the
+ :meth:`_function.FunctionElement.table_valued` method first to
+ establish named columns.
e.g.::
- from sqlalchemy.sql import column
+ >>> from sqlalchemy import func, select, column
+ >>> data_view = func.unnest([1, 2, 3]).alias("data_view")
+ >>> print(select(data_view.column))
+ SELECT data_view
+ FROM unnest(:unnest_1) AS data_view
- stmt = select(column('data_view')).\
- select_from(SomeTable).\
- select_from(func.unnest(SomeTable.data).alias('data_view')
- )
+ The :meth:`_functions.FunctionElement.column_valued` method provides
+ a shortcut for the above pattern::
- Would produce:
+ >>> data_view = func.unnest([1, 2, 3]).column_valued("data_view")
+ >>> print(select(data_view))
+ SELECT data_view
+ FROM unnest(:unnest_1) AS data_view
- .. sourcecode:: sql
+ .. versionadded:: 1.4.0b2 Added the ``.column`` accessor
- SELECT data_view
- FROM sometable, unnest(sometable.data) AS data_view
+ .. seealso::
+
+ :ref:`tutorial_functions_table_valued` -
+ in the :ref:`unified_tutorial`
+
+ :meth:`_functions.FunctionElement.table_valued`
+
+ :meth:`_functions.FunctionElement.scalar_table_valued`
+
+ :meth:`_functions.FunctionElement.column_valued`
- .. versionadded:: 0.9.8 The :meth:`.FunctionElement.alias` method
- is now supported. Previously, this method's behavior was
- undefined and did not behave consistently across versions.
"""
- return Alias._construct(self, name)
+ return TableValuedAlias._construct(
+ self, name, table_value_type=self.type
+ )
def select(self):
"""Produce a :func:`_expression.select` construct
@@ -351,6 +551,14 @@ class FunctionElement(Executable, ColumnElement, FromClause):
s = s.execution_options(**self._execution_options)
return s
+ @util.deprecated_20(
+ ":meth:`.FunctionElement.scalar`",
+ alternative="Scalar execution in SQLAlchemy 2.0 is performed "
+ "by the :meth:`_engine.Connection.scalar` method of "
+ ":class:`_engine.Connection`, "
+ "or in the ORM by the :meth:`.Session.scalar` method of "
+ ":class:`.Session`.",
+ )
def scalar(self):
"""Execute this :class:`.FunctionElement` against an embedded
'bind' and return a scalar value.
@@ -365,6 +573,14 @@ class FunctionElement(Executable, ColumnElement, FromClause):
"""
return self.select().execute().scalar()
+ @util.deprecated_20(
+ ":meth:`.FunctionElement.execute`",
+ alternative="All statement execution in SQLAlchemy 2.0 is performed "
+ "by the :meth:`_engine.Connection.execute` method of "
+ ":class:`_engine.Connection`, "
+ "or in the ORM by the :meth:`.Session.execute` method of "
+ ":class:`.Session`.",
+ )
def execute(self):
"""Execute this :class:`.FunctionElement` against an embedded
'bind'.
@@ -441,6 +657,24 @@ class FunctionAsBinary(BinaryExpression):
self.sql_function.clauses.clauses[self.right_index - 1] = value
+class ScalarFunctionColumn(NamedColumn):
+ __visit_name__ = "scalar_function_column"
+
+ _traverse_internals = [
+ ("name", InternalTraversal.dp_anon_name),
+ ("type", InternalTraversal.dp_type),
+ ("fn", InternalTraversal.dp_clauseelement),
+ ]
+
+ is_literal = False
+ table = None
+
+ def __init__(self, fn, name, type_=None):
+ self.fn = fn
+ self.name = name
+ self.type = sqltypes.to_instance(type_)
+
+
class _FunctionGenerator(object):
"""Generate SQL function expressions.
@@ -586,10 +820,9 @@ class Function(FunctionElement):
func.mypackage.some_function(col1, col2)
-
.. seealso::
- :ref:`coretutorial_functions`
+ :ref:`tutorial_functions` - in the :ref:`unified_tutorial`
:data:`.func` - namespace which produces registered or ad-hoc
:class:`.Function` instances.
@@ -607,6 +840,23 @@ class Function(FunctionElement):
("type", InternalTraversal.dp_type),
]
+ type = sqltypes.NULLTYPE
+ """A :class:`_types.TypeEngine` object which refers to the SQL return
+ type represented by this SQL function.
+
+ This datatype may be configured when generating a
+ :class:`_functions.Function` object by passing the
+ :paramref:`_functions.Function.type_` parameter, e.g.::
+
+ >>> select(func.lower("some VALUE", type_=String))
+
+ The small number of built-in classes of :class:`_functions.Function` come
+ with a built-in datatype that's appropriate to the class of function and
+ its arguments. For functions that aren't known, the type defaults to the
+ "null type".
+
+ """
+
@util.deprecated_params(
bind=(
"2.0",
@@ -825,6 +1075,8 @@ class next_value(GenericFunction):
class AnsiFunction(GenericFunction):
+ """Define a function in "ansi" format, which doesn't render parenthesis."""
+
inherit_cache = True
def __init__(self, *args, **kwargs):
@@ -856,29 +1108,61 @@ class coalesce(ReturnTypeFromArgs):
inherit_cache = True
-class max(ReturnTypeFromArgs): # noqa
+class max(ReturnTypeFromArgs): # noqa A001
+ """The SQL MAX() aggregate function."""
+
inherit_cache = True
-class min(ReturnTypeFromArgs): # noqa
+class min(ReturnTypeFromArgs): # noqa A001
+ """The SQL MIN() aggregate function."""
+
inherit_cache = True
-class sum(ReturnTypeFromArgs): # noqa
+class sum(ReturnTypeFromArgs): # noqa A001
+ """The SQL SUM() aggregate function."""
+
inherit_cache = True
-class now(GenericFunction): # noqa
+class now(GenericFunction):
+ """The SQL now() datetime function.
+
+ SQLAlchemy dialects will usually render this particular function
+ in a backend-specific way, such as rendering it as ``CURRENT_TIMESTAMP``.
+
+ """
+
type = sqltypes.DateTime
inherit_cache = True
class concat(GenericFunction):
+ """The SQL CONCAT() function, which concatenates strings.
+
+ E.g.::
+
+ >>> print(select(func.concat('a', 'b')))
+ SELECT concat(:concat_2, :concat_3) AS concat_1
+
+ String concatenation in SQLAlchemy is more commonly available using the
+ Python ``+`` operator with string datatypes, which will render a
+ backend-specific concatenation operator, such as ::
+
+ >>> print(select(literal("a") + "b"))
+ SELECT :param_1 || :param_2 AS anon_1
+
+
+ """
+
type = sqltypes.String
inherit_cache = True
class char_length(GenericFunction):
+ """The CHAR_LENGTH() SQL function."""
+
type = sqltypes.Integer
inherit_cache = True
@@ -887,6 +1171,8 @@ class char_length(GenericFunction):
class random(GenericFunction):
+ """The RANDOM() SQL function."""
+
_has_args = True
inherit_cache = True
@@ -922,46 +1208,64 @@ class count(GenericFunction):
class current_date(AnsiFunction):
+ """The CURRENT_DATE() SQL function."""
+
type = sqltypes.Date
inherit_cache = True
class current_time(AnsiFunction):
+ """The CURRENT_TIME() SQL function."""
+
type = sqltypes.Time
inherit_cache = True
class current_timestamp(AnsiFunction):
+ """The CURRENT_TIMESTAMP() SQL function."""
+
type = sqltypes.DateTime
inherit_cache = True
class current_user(AnsiFunction):
+ """The CURRENT_USER() SQL function."""
+
type = sqltypes.String
inherit_cache = True
class localtime(AnsiFunction):
+ """The localtime() SQL function."""
+
type = sqltypes.DateTime
inherit_cache = True
class localtimestamp(AnsiFunction):
+ """The localtimestamp() SQL function."""
+
type = sqltypes.DateTime
inherit_cache = True
class session_user(AnsiFunction):
+ """The SESSION_USER() SQL function."""
+
type = sqltypes.String
inherit_cache = True
class sysdate(AnsiFunction):
+ """The SYSDATE() SQL function."""
+
type = sqltypes.DateTime
inherit_cache = True
class user(AnsiFunction):
+ """The USER() SQL function."""
+
type = sqltypes.String
inherit_cache = True
diff --git a/lib/sqlalchemy/sql/roles.py b/lib/sqlalchemy/sql/roles.py
index 2c4ff75c4..52743bd50 100644
--- a/lib/sqlalchemy/sql/roles.py
+++ b/lib/sqlalchemy/sql/roles.py
@@ -44,6 +44,10 @@ class ColumnArgumentOrKeyRole(ColumnArgumentRole):
_role_name = "Column expression or string key"
+class StrAsPlainColumnRole(ColumnArgumentRole):
+ _role_name = "Column expression or string key"
+
+
class ColumnListRole(SQLRole):
"""Elements suitable for forming comma separated lists of expressions."""
diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py
index 1bd4f5d81..a273e0c90 100644
--- a/lib/sqlalchemy/sql/selectable.py
+++ b/lib/sqlalchemy/sql/selectable.py
@@ -51,6 +51,7 @@ from .elements import ColumnClause
from .elements import GroupedElement
from .elements import Grouping
from .elements import literal_column
+from .elements import TableValuedColumn
from .elements import UnaryExpression
from .visitors import InternalTraversal
from .. import exc
@@ -623,6 +624,34 @@ class FromClause(roles.AnonymizedFromClauseRole, Selectable):
return Alias._construct(self, name)
+ @util.preload_module("sqlalchemy.sql.sqltypes")
+ def table_valued(self):
+ """Return a :class:`_sql.TableValuedColumn` object for this
+ :class:`_expression.FromClause`.
+
+ A :class:`_sql.TableValuedColumn` is a :class:`_sql.ColumnElement` that
+ represents a complete row in a table. Support for this construct is
+ backend dependent, and is supported in various forms by backends
+ such as PostgreSQL, Oracle and SQL Server.
+
+ E.g.::
+
+ >>> from sqlalchemy import select, column, func, table
+ >>> a = table("a", column("id"), column("x"), column("y"))
+ >>> stmt = select(func.row_to_json(a.table_valued()))
+ >>> print(stmt)
+ SELECT row_to_json(a) AS row_to_json_1
+ FROM a
+
+ .. versionadded:: 1.4.0b2
+
+ .. seealso::
+
+ :ref:`tutorial_functions` - in the :ref:`unified_tutorial`
+
+ """
+ return TableValuedColumn(self, type_api.TABLEVALUE)
+
def tablesample(self, sampling, name=None, seed=None):
"""Return a TABLESAMPLE alias of this :class:`_expression.FromClause`.
@@ -1519,6 +1548,8 @@ class AliasedReturnsRows(NoInit, FromClause):
_is_from_container = True
named_with_column = True
+ _supports_derived_columns = False
+
_traverse_internals = [
("element", InternalTraversal.dp_clauseelement),
("name", InternalTraversal.dp_anon_name),
@@ -1678,6 +1709,151 @@ class Alias(roles.DMLTableRole, AliasedReturnsRows):
).alias(name=name, flat=flat)
+class TableValuedAlias(Alias):
+ """An alias against a "table valued" SQL function.
+
+ This construct provides for a SQL function that returns columns
+ to be used in the FROM clause of a SELECT statement. The
+ object is generated using the :meth:`_functions.FunctionElement.table_valued`
+ method, e.g.::
+
+ >>> from sqlalchemy import select, func
+ >>> fn = func.json_array_elements_text('["one", "two", "three"]').table_valued("value")
+ >>> print(select(fn.c.value))
+ SELECT anon_1.value
+ FROM json_array_elements_text(:json_array_elements_text_1) AS anon_1
+
+ .. versionadded:: 1.4.0b2
+
+ .. seealso::
+
+ :ref:`tutorial_functions_table_valued` - in the :ref:`unified_tutorial`
+
+ """ # noqa E501
+
+ __visit_name__ = "table_valued_alias"
+
+ _supports_derived_columns = True
+ _render_derived = False
+ _render_derived_w_types = False
+
+ _traverse_internals = [
+ ("element", InternalTraversal.dp_clauseelement),
+ ("name", InternalTraversal.dp_anon_name),
+ ("_tableval_type", InternalTraversal.dp_type),
+ ("_render_derived", InternalTraversal.dp_boolean),
+ ("_render_derived_w_types", InternalTraversal.dp_boolean),
+ ]
+
+ def _init(self, selectable, name=None, table_value_type=None):
+ super(TableValuedAlias, self)._init(selectable, name=name)
+
+ self._tableval_type = (
+ type_api.TABLEVALUE
+ if table_value_type is None
+ else table_value_type
+ )
+
+ @HasMemoized.memoized_attribute
+ def column(self):
+ """Return a column expression representing this
+ :class:`_sql.TableValuedAlias`.
+
+ This accessor is used to implement the
+ :meth:`_functions.FunctionElement.column_valued` method. See that
+ method for further details.
+
+ E.g.::
+
+ >>> print(select(func.some_func().table_valued("value").column))
+ SELECT anon_1 FROM some_func() AS anon_1
+
+ .. seealso::
+
+ :meth:`_functions.FunctionElement.column_valued`
+
+ """
+
+ return TableValuedColumn(self, self._tableval_type)
+
+ def alias(self, name=None):
+ """Return a new alias of this :class:`_sql.TableValuedAlias`.
+
+ This creates a distinct FROM object that will be distinguished
+ from the original one when used in a SQL statement.
+
+ """
+
+ tva = TableValuedAlias._construct(self, name=name)
+ if self._render_derived:
+ tva._render_derived = True
+ tva._render_derived_w_types = self._render_derived_w_types
+ return tva
+
+ def lateral(self, name=None):
+ """Return a new :class:`_sql.TableValuedAlias` with the lateral flag set,
+ so that it renders as LATERAL.
+
+ .. seealso::
+
+ :func:`_expression.lateral`
+
+ """
+ tva = self.alias(name=name)
+ tva._is_lateral = True
+ return tva
+
+ def render_derived(self, name=None, with_types=False):
+ """Apply "render derived" to this :class:`_sql.TableValuedAlias`.
+
+ This has the effect of the individual column names listed out
+ after the alias name in the "AS" sequence, e.g.::
+
+ >>> print(
+ ... select(
+ ... func.unnest(array(["one", "two", "three"])).
+ table_valued("x", with_ordinality="o").render_derived()
+ ... )
+ ... )
+ SELECT anon_1.x, anon_1.o
+ FROM unnest(ARRAY[%(param_1)s, %(param_2)s, %(param_3)s]) WITH ORDINALITY AS anon_1(x, o)
+
+ The ``with_types`` keyword will render column types inline within
+ the alias expression (this syntax currently applies to the
+ PostgreSQL database)::
+
+ >>> print(
+ ... select(
+ ... func.json_to_recordset(
+ ... '[{"a":1,"b":"foo"},{"a":"2","c":"bar"}]'
+ ... )
+ ... .table_valued(column("a", Integer), column("b", String))
+ ... .render_derived(with_types=True)
+ ... )
+ ... )
+ SELECT anon_1.a, anon_1.b FROM json_to_recordset(:json_to_recordset_1)
+ AS anon_1(a INTEGER, b VARCHAR)
+
+ :param name: optional string name that will be applied to the alias
+ generated. If left as None, a unique anonymizing name will be used.
+
+ :param with_types: if True, the derived columns will include the
+ datatype specification with each column. This is a special syntax
+ currently known to be required by PostgreSQL for some SQL functions.
+
+ """ # noqa E501
+
+ # note: don't use the @_generative system here, keep a reference
+ # to the original object. otherwise you can have re-use of the
+ # python id() of the original which can cause name conflicts if
+ # a new anon-name grabs the same identifier as the local anon-name
+ # (just saw it happen on CI)
+ new_alias = TableValuedAlias._construct(self, name=name)
+ new_alias._render_derived = True
+ new_alias._render_derived_w_types = with_types
+ return new_alias
+
+
class Lateral(AliasedReturnsRows):
"""Represent a LATERAL subquery.
@@ -2473,7 +2649,7 @@ class Values(Generative, FromClause):
value_expr = values(
column('id', Integer),
- column('name', Integer),
+ column('name', String),
name="my_values"
).data(
[(1, 'name1'), (2, 'name2'), (3, 'name3')]
diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py
index d20c8168d..3484b3ac8 100644
--- a/lib/sqlalchemy/sql/sqltypes.py
+++ b/lib/sqlalchemy/sql/sqltypes.py
@@ -26,6 +26,8 @@ from .elements import _defer_name
from .elements import quoted_name
from .elements import Slice
from .elements import TypeCoerce as type_coerce # noqa
+from .traversals import HasCacheKey
+from .traversals import InternalTraversal
from .type_api import Emulated
from .type_api import NativeForEmulated # noqa
from .type_api import to_instance
@@ -2049,17 +2051,18 @@ class JSON(Indexable, TypeEngine):
JSON types. Since it supports JSON SQL operations, it only
works on backends that have an actual JSON type, currently:
- * PostgreSQL - see :class:`_postgresql.JSON` and
- :class:`_postgresql.JSONB` for backend-specific notes
+ * PostgreSQL - see :class:`sqlalchemy.dialects.postgresql.JSON` and
+ :class:`sqlalchemy.dialects.postgresql.JSONB` for backend-specific
+ notes
- * MySQL as of version 5.7 (MariaDB as of the 10.2 series does not) - see
- :class:`_mysql.JSON` for backend-specific notes
+ * MySQL - see
+ :class:`sqlalchemy.dialects.mysql.JSON` for backend-specific notes
- * SQLite as of version 3.9 - see :class:`_sqlite.JSON` for
- backend-specific notes
+ * SQLite as of version 3.9 - see
+ :class:`sqlalchemy.dialects.sqlite.JSON` for backend-specific notes
- * Microsoft SQL Server 2016 and later - see :class:`_mssql.JSON` for
- backend-specific notes
+ * Microsoft SQL Server 2016 and later - see
+ :class:`sqlalchemy.dialects.mssql.JSON` for backend-specific notes
:class:`_types.JSON` is part of the Core in support of the growing
popularity of native JSON datatypes.
@@ -2102,9 +2105,10 @@ class JSON(Indexable, TypeEngine):
.. versionadded:: 1.3.11
Additional operations may be available from the dialect-specific versions
- of :class:`_types.JSON`, such as :class:`_postgresql.JSON` and
- :class:`_postgresql.JSONB` which both offer additional PostgreSQL-specific
- operations.
+ of :class:`_types.JSON`, such as
+ :class:`sqlalchemy.dialects.postgresql.JSON` and
+ :class:`sqlalchemy.dialects.postgresql.JSONB` which both offer additional
+ PostgreSQL-specific operations.
**Casting JSON Elements to Other Types**
@@ -2219,13 +2223,13 @@ class JSON(Indexable, TypeEngine):
.. seealso::
- :class:`_postgresql.JSON`
+ :class:`sqlalchemy.dialects.postgresql.JSON`
- :class:`_postgresql.JSONB`
+ :class:`sqlalchemy.dialects.postgresql.JSONB`
- :class:`.mysql.JSON`
+ :class:`sqlalchemy.dialects.mysql.JSON`
- :class:`_sqlite.JSON`
+ :class:`sqlalchemy.dialects.sqlite.JSON`
.. versionadded:: 1.1
@@ -2579,11 +2583,11 @@ class ARRAY(SchemaEventTarget, Indexable, Concatenable, TypeEngine):
"""Represent a SQL Array type.
.. note:: This type serves as the basis for all ARRAY operations.
- However, currently **only the PostgreSQL backend has support
- for SQL arrays in SQLAlchemy**. It is recommended to use the
- :class:`_postgresql.ARRAY` type directly when using ARRAY types
- with PostgreSQL, as it provides additional operators specific
- to that backend.
+ However, currently **only the PostgreSQL backend has support for SQL
+ arrays in SQLAlchemy**. It is recommended to use the PostgreSQL-specific
+ :class:`sqlalchemy.dialects.postgresql.ARRAY` type directly when using
+ ARRAY types with PostgreSQL, as it provides additional operators
+ specific to that backend.
:class:`_types.ARRAY` is part of the Core in support of various SQL
standard functions such as :class:`_functions.array_agg`
@@ -2665,7 +2669,7 @@ class ARRAY(SchemaEventTarget, Indexable, Concatenable, TypeEngine):
.. seealso::
- :class:`_postgresql.ARRAY`
+ :class:`sqlalchemy.dialects.postgresql.ARRAY`
"""
@@ -3110,6 +3114,22 @@ class NullType(TypeEngine):
comparator_factory = Comparator
+class TableValueType(HasCacheKey, TypeEngine):
+ """Refers to a table value type."""
+
+ _is_table_value = True
+
+ _traverse_internals = [
+ ("_elements", InternalTraversal.dp_clauseelement_list),
+ ]
+
+ def __init__(self, *elements):
+ self._elements = [
+ coercions.expect(roles.StrAsPlainColumnRole, elem)
+ for elem in elements
+ ]
+
+
class MatchType(Boolean):
"""Refers to the return type of the MATCH operator.
@@ -3131,6 +3151,7 @@ BOOLEANTYPE = Boolean()
STRINGTYPE = String()
INTEGERTYPE = Integer()
MATCHTYPE = MatchType()
+TABLEVALUE = TableValueType()
_type_map = {
int: Integer(),
@@ -3183,5 +3204,6 @@ type_api.INTEGERTYPE = INTEGERTYPE
type_api.NULLTYPE = NULLTYPE
type_api.MATCHTYPE = MATCHTYPE
type_api.INDEXABLE = Indexable
+type_api.TABLEVALUE = TABLEVALUE
type_api._resolve_value_to_type = _resolve_value_to_type
TypeEngine.Comparator.BOOLEANTYPE = BOOLEANTYPE
diff --git a/lib/sqlalchemy/sql/traversals.py b/lib/sqlalchemy/sql/traversals.py
index 2887813ad..51a531000 100644
--- a/lib/sqlalchemy/sql/traversals.py
+++ b/lib/sqlalchemy/sql/traversals.py
@@ -857,7 +857,10 @@ class _GetChildren(InternalTraversal):
with a _traverse_internals collection."""
def visit_has_cache_key(self, element, **kw):
- return (element,)
+ # the GetChildren traversal refers explicitly to ClauseElement
+ # structures. Within these, a plain HasCacheKey is not a
+ # ClauseElement, so don't include these.
+ return ()
def visit_clauseelement(self, element, **kw):
return (element,)
diff --git a/lib/sqlalchemy/sql/type_api.py b/lib/sqlalchemy/sql/type_api.py
index 462a8763b..46751cb22 100644
--- a/lib/sqlalchemy/sql/type_api.py
+++ b/lib/sqlalchemy/sql/type_api.py
@@ -24,6 +24,7 @@ NULLTYPE = None
STRINGTYPE = None
MATCHTYPE = None
INDEXABLE = None
+TABLEVALUE = None
_resolve_value_to_type = None
@@ -45,6 +46,7 @@ class TypeEngine(Traversible):
_sqla_type = True
_isnull = False
_is_tuple_type = False
+ _is_table_value = False
class Comparator(operators.ColumnOperators):
"""Base class for custom comparison operations defined at the
diff --git a/lib/sqlalchemy/sql/visitors.py b/lib/sqlalchemy/sql/visitors.py
index fe0fbf669..8e113849e 100644
--- a/lib/sqlalchemy/sql/visitors.py
+++ b/lib/sqlalchemy/sql/visitors.py
@@ -75,10 +75,8 @@ def _generate_compiler_dispatch(cls):
try:
meth = getter(visitor)
except AttributeError as err:
- util.raise_(
- exc.UnsupportedCompilationError(visitor, cls),
- replace_context=err,
- )
+ return visitor.visit_unsupported_compilation(self, err, **kw)
+
else:
return meth(self, **kw)