summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/sql/base.py20
-rw-r--r--lib/sqlalchemy/sql/elements.py46
-rw-r--r--lib/sqlalchemy/sql/selectable.py30
-rw-r--r--lib/sqlalchemy/sql/util.py31
4 files changed, 98 insertions, 29 deletions
diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py
index a6870f8d4..b235f5132 100644
--- a/lib/sqlalchemy/sql/base.py
+++ b/lib/sqlalchemy/sql/base.py
@@ -39,6 +39,8 @@ NO_ARG = util.symbol("NO_ARG")
class Immutable(object):
"""mark a ClauseElement as 'immutable' when expressions are cloned."""
+ _is_immutable = True
+
def unique_params(self, *optionaldict, **kwargs):
raise NotImplementedError("Immutable objects do not support copying")
@@ -55,6 +57,8 @@ class Immutable(object):
class SingletonConstant(Immutable):
"""Represent SQL constants like NULL, TRUE, FALSE"""
+ _is_singleton_constant = True
+
def __new__(cls, *arg, **kw):
return cls._singleton
@@ -62,14 +66,16 @@ class SingletonConstant(Immutable):
def _create_singleton(cls):
obj = object.__new__(cls)
obj.__init__()
- cls._singleton = obj
- # don't proxy singletons. this means that a SingletonConstant
- # will never be a "corresponding column" in a statement; the constant
- # can be named directly and as it is often/usually compared against using
- # "IS", it can't be adapted to a subquery column in any case.
- # see :ticket:`6259`.
- proxy_set = frozenset()
+ # for a long time this was an empty frozenset, meaning
+ # a SingletonConstant would never be a "corresponding column" in
+ # a statement. This referred to #6259. However, in #7154 we see
+ # that we do in fact need "correspondence" to work when matching cols
+ # in result sets, so the non-correspondence was moved to a more
+ # specific level when we are actually adapting expressions for SQL
+ # render only.
+ obj.proxy_set = frozenset([obj])
+ cls._singleton = obj
def _from_objects(*elements):
diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py
index 8f02527b9..6f1756af3 100644
--- a/lib/sqlalchemy/sql/elements.py
+++ b/lib/sqlalchemy/sql/elements.py
@@ -214,6 +214,8 @@ class ClauseElement(
_is_bind_parameter = False
_is_clause_list = False
_is_lambda_element = False
+ _is_singleton_constant = False
+ _is_immutable = False
_order_by_label_element = None
@@ -1023,19 +1025,39 @@ class ColumnElement(
"""
return Label(name, self, self.type)
- def _anon_label(self, seed):
+ def _anon_label(self, seed, add_hash=None):
while self._is_clone_of is not None:
self = self._is_clone_of
# as of 1.4 anonymous label for ColumnElement uses hash(), not id(),
# as the identifier, because a column and its annotated version are
# the same thing in a SQL statement
+ hash_value = hash(self)
+
+ if add_hash:
+ # this path is used for disambiguating anon labels that would
+ # otherwise be the same name for the same element repeated.
+ # an additional numeric value is factored in for each label.
+
+ # shift hash(self) (which is id(self), typically 8 byte integer)
+ # 16 bits leftward. fill extra add_hash on right
+ assert add_hash < (2 << 15)
+ assert seed
+ hash_value = (hash_value << 16) | add_hash
+
+ # extra underscore is added for labels with extra hash
+ # values, to isolate the "deduped anon" namespace from the
+ # regular namespace. eliminates chance of these
+ # manufactured hash values overlapping with regular ones for some
+ # undefined python interpreter
+ seed = seed + "_"
+
if isinstance(seed, _anonymous_label):
return _anonymous_label.safe_construct(
- hash(self), "", enclosing_label=seed
+ hash_value, "", enclosing_label=seed
)
- return _anonymous_label.safe_construct(hash(self), seed or "anon")
+ return _anonymous_label.safe_construct(hash_value, seed or "anon")
@util.memoized_property
def _anon_name_label(self):
@@ -1093,8 +1115,7 @@ class ColumnElement(
def anon_key_label(self):
return self._anon_key_label
- @util.memoized_property
- def _dedupe_anon_label(self):
+ def _dedupe_anon_label_idx(self, idx):
"""label to apply to a column that is anon labeled, but repeated
in the SELECT, so that we have to make an "extra anon" label that
disambiguates it from the previous appearance.
@@ -1113,9 +1134,9 @@ class ColumnElement(
# "CAST(casttest.v1 AS DECIMAL) AS anon__1"
if label is None:
- return self._dedupe_anon_tq_label
+ return self._dedupe_anon_tq_label_idx(idx)
else:
- return self._anon_label(label + "_")
+ return self._anon_label(label, add_hash=idx)
@util.memoized_property
def _anon_tq_label(self):
@@ -1125,10 +1146,10 @@ class ColumnElement(
def _anon_tq_key_label(self):
return self._anon_label(getattr(self, "_tq_key_label", None))
- @util.memoized_property
- def _dedupe_anon_tq_label(self):
+ def _dedupe_anon_tq_label_idx(self, idx):
label = getattr(self, "_tq_label", None) or "anon"
- return self._anon_label(label + "_")
+
+ return self._anon_label(label, add_hash=idx)
class WrapsColumnExpression(object):
@@ -1178,14 +1199,13 @@ class WrapsColumnExpression(object):
return wce._anon_name_label
return super(WrapsColumnExpression, self)._anon_name_label
- @property
- def _dedupe_anon_label(self):
+ def _dedupe_anon_label_idx(self, idx):
wce = self.wrapped_column_expression
nal = wce._non_anon_label
if nal:
return self._anon_label(nal + "_")
else:
- return self._dedupe_anon_tq_label
+ return self._dedupe_anon_tq_label_idx(idx)
class BindParameter(roles.InElementRole, ColumnElement):
diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py
index 8f8e6b2a7..8e71dfb97 100644
--- a/lib/sqlalchemy/sql/selectable.py
+++ b/lib/sqlalchemy/sql/selectable.py
@@ -6066,6 +6066,14 @@ class Select(
table_qualified = self._label_style is LABEL_STYLE_TABLENAME_PLUS_COL
label_style_none = self._label_style is LABEL_STYLE_NONE
+ # a counter used for "dedupe" labels, which have double underscores
+ # in them and are never referred by name; they only act
+ # as positional placeholders. they need only be unique within
+ # the single columns clause they're rendered within (required by
+ # some dbs such as mysql). So their anon identity is tracked against
+ # a fixed counter rather than hash() identity.
+ dedupe_hash = 1
+
for c in cols:
repeated = False
@@ -6099,9 +6107,15 @@ class Select(
# here, "required_label_name" is sent as
# "None" and "fallback_label_name" is sent.
if table_qualified:
- fallback_label_name = c._dedupe_anon_tq_label
+ fallback_label_name = (
+ c._dedupe_anon_tq_label_idx(dedupe_hash)
+ )
+ dedupe_hash += 1
else:
- fallback_label_name = c._dedupe_anon_label
+ fallback_label_name = c._dedupe_anon_label_idx(
+ dedupe_hash
+ )
+ dedupe_hash += 1
else:
fallback_label_name = c._anon_name_label
else:
@@ -6142,11 +6156,13 @@ class Select(
if table_qualified:
required_label_name = (
fallback_label_name
- ) = c._dedupe_anon_tq_label
+ ) = c._dedupe_anon_tq_label_idx(dedupe_hash)
+ dedupe_hash += 1
else:
required_label_name = (
fallback_label_name
- ) = c._dedupe_anon_label
+ ) = c._dedupe_anon_label_idx(dedupe_hash)
+ dedupe_hash += 1
repeated = True
else:
names[required_label_name] = c
@@ -6156,11 +6172,13 @@ class Select(
if table_qualified:
required_label_name = (
fallback_label_name
- ) = c._dedupe_anon_tq_label
+ ) = c._dedupe_anon_tq_label_idx(dedupe_hash)
+ dedupe_hash += 1
else:
required_label_name = (
fallback_label_name
- ) = c._dedupe_anon_label
+ ) = c._dedupe_anon_label_idx(dedupe_hash)
+ dedupe_hash += 1
repeated = True
else:
names[effective_name] = c
diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py
index da5e31655..7fcb45709 100644
--- a/lib/sqlalchemy/sql/util.py
+++ b/lib/sqlalchemy/sql/util.py
@@ -847,7 +847,7 @@ class ClauseAdapter(visitors.ReplacingExternalTraversal):
return newcol
@util.preload_module("sqlalchemy.sql.functions")
- def replace(self, col):
+ def replace(self, col, _include_singleton_constants=False):
functions = util.preloaded.sql_functions
if isinstance(col, FromClause) and not isinstance(
@@ -881,6 +881,14 @@ class ClauseAdapter(visitors.ReplacingExternalTraversal):
elif not isinstance(col, ColumnElement):
return None
+ elif not _include_singleton_constants and col._is_singleton_constant:
+ # dont swap out NULL, TRUE, FALSE for a label name
+ # in a SQL statement that's being rewritten,
+ # leave them as the constant. This is first noted in #6259,
+ # however the logic to check this moved here as of #7154 so that
+ # it is made specific to SQL rewriting and not all column
+ # correspondence
+ return None
if "adapt_column" in col._annotations:
col = col._annotations["adapt_column"]
@@ -1001,8 +1009,25 @@ class ColumnAdapter(ClauseAdapter):
return newcol
def _locate_col(self, col):
-
- c = ClauseAdapter.traverse(self, col)
+ # both replace and traverse() are overly complicated for what
+ # we are doing here and we would do better to have an inlined
+ # version that doesn't build up as much overhead. the issue is that
+ # sometimes the lookup does in fact have to adapt the insides of
+ # say a labeled scalar subquery. However, if the object is an
+ # Immutable, i.e. Column objects, we can skip the "clone" /
+ # "copy internals" part since those will be no-ops in any case.
+ # additionally we want to catch singleton objects null/true/false
+ # and make sure they are adapted as well here.
+
+ if col._is_immutable:
+ for vis in self.visitor_iterator:
+ c = vis.replace(col, _include_singleton_constants=True)
+ if c is not None:
+ break
+ else:
+ c = col
+ else:
+ c = ClauseAdapter.traverse(self, col)
if self._wrap:
c2 = self._wrap._locate_col(c)