summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/sql/elements.py
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2014-09-01 20:19:54 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2014-09-01 20:19:54 -0400
commit7c6a45c480a865ac9580eb33fcca2dae5b19dd11 (patch)
tree870c078707cde0af769a940b1fc1a15ce7966691 /lib/sqlalchemy/sql/elements.py
parent382f82538b5484b1c384c71fbf84438312cbe34f (diff)
downloadsqlalchemy-7c6a45c480a865ac9580eb33fcca2dae5b19dd11.tar.gz
- The :func:`~.expression.column` and :func:`~.expression.table`
constructs are now importable from the "from sqlalchemy" namespace, just like every other Core construct. - The implicit conversion of strings to :func:`.text` constructs when passed to most builder methods of :func:`.select` as well as :class:`.Query` now emits a warning with just the plain string sent. The textual conversion still proceeds normally, however. The only method that accepts a string without a warning are the "label reference" methods like order_by(), group_by(); these functions will now at compile time attempt to resolve a single string argument to a column or label expression present in the selectable; if none is located, the expression still renders, but you get the warning again. The rationale here is that the implicit conversion from string to text is more unexpected than not these days, and it is better that the user send more direction to the Core / ORM when passing a raw string as to what direction should be taken. Core/ORM tutorials have been updated to go more in depth as to how text is handled. fixes #2992
Diffstat (limited to 'lib/sqlalchemy/sql/elements.py')
-rw-r--r--lib/sqlalchemy/sql/elements.py110
1 files changed, 93 insertions, 17 deletions
diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py
index 8cae83169..0ea05fa0e 100644
--- a/lib/sqlalchemy/sql/elements.py
+++ b/lib/sqlalchemy/sql/elements.py
@@ -19,7 +19,8 @@ from .visitors import Visitable, cloned_traverse, traverse
from .annotation import Annotated
import itertools
from .base import Executable, PARSE_AUTOCOMMIT, Immutable, NO_ARG
-from .base import _generative, Generative
+from .base import _generative
+import numbers
import re
import operator
@@ -624,7 +625,7 @@ class ColumnElement(operators.ColumnOperators, ClauseElement):
__visit_name__ = 'column'
primary_key = False
foreign_keys = []
- _label = None
+ _label = _columns_clause_label = None
_key_label = key = None
_alt_names = ()
@@ -1180,6 +1181,10 @@ class TextClause(Executable, ClauseElement):
_hide_froms = []
+ # help in those cases where text() is
+ # interpreted in a column expression situation
+ key = _label = _columns_clause_label = None
+
def __init__(
self,
text,
@@ -1694,13 +1699,16 @@ class ClauseList(ClauseElement):
self.operator = kwargs.pop('operator', operators.comma_op)
self.group = kwargs.pop('group', True)
self.group_contents = kwargs.pop('group_contents', True)
+ text_converter = kwargs.pop(
+ '_literal_as_text',
+ _expression_literal_as_text)
if self.group_contents:
self.clauses = [
- _literal_as_text(clause).self_group(against=self.operator)
+ text_converter(clause).self_group(against=self.operator)
for clause in clauses]
else:
self.clauses = [
- _literal_as_text(clause)
+ text_converter(clause)
for clause in clauses]
def __iter__(self):
@@ -1767,7 +1775,7 @@ class BooleanClauseList(ClauseList, ColumnElement):
clauses = util.coerce_generator_arg(clauses)
for clause in clauses:
- clause = _literal_as_text(clause)
+ clause = _expression_literal_as_text(clause)
if isinstance(clause, continue_on):
continue
@@ -2280,6 +2288,13 @@ class Extract(ColumnElement):
return self.expr._from_objects
+class _label_reference(ColumnElement):
+ __visit_name__ = 'label_reference'
+
+ def __init__(self, text):
+ self.text = text
+
+
class UnaryExpression(ColumnElement):
"""Define a 'unary' expression.
@@ -2343,7 +2358,8 @@ class UnaryExpression(ColumnElement):
"""
return UnaryExpression(
- _literal_as_text(column), modifier=operators.nullsfirst_op)
+ _literal_as_label_reference(column),
+ modifier=operators.nullsfirst_op)
@classmethod
def _create_nullslast(cls, column):
@@ -2383,7 +2399,8 @@ class UnaryExpression(ColumnElement):
"""
return UnaryExpression(
- _literal_as_text(column), modifier=operators.nullslast_op)
+ _literal_as_label_reference(column),
+ modifier=operators.nullslast_op)
@classmethod
def _create_desc(cls, column):
@@ -2421,7 +2438,7 @@ class UnaryExpression(ColumnElement):
"""
return UnaryExpression(
- _literal_as_text(column), modifier=operators.desc_op)
+ _literal_as_label_reference(column), modifier=operators.desc_op)
@classmethod
def _create_asc(cls, column):
@@ -2458,7 +2475,7 @@ class UnaryExpression(ColumnElement):
"""
return UnaryExpression(
- _literal_as_text(column), modifier=operators.asc_op)
+ _literal_as_label_reference(column), modifier=operators.asc_op)
@classmethod
def _create_distinct(cls, expr):
@@ -2742,9 +2759,13 @@ class Over(ColumnElement):
"""
self.func = func
if order_by is not None:
- self.order_by = ClauseList(*util.to_list(order_by))
+ self.order_by = ClauseList(
+ *util.to_list(order_by),
+ _literal_as_text=_literal_as_label_reference)
if partition_by is not None:
- self.partition_by = ClauseList(*util.to_list(partition_by))
+ self.partition_by = ClauseList(
+ *util.to_list(partition_by),
+ _literal_as_text=_literal_as_label_reference)
@util.memoized_property
def type(self):
@@ -2804,7 +2825,8 @@ class Label(ColumnElement):
self.name = _anonymous_label(
'%%(%d %s)s' % (id(self), getattr(element, 'name', 'anon'))
)
- self.key = self._label = self._key_label = self.name
+ self.key = self._label = self._key_label = \
+ self._columns_clause_label = self.name
self._element = element
self._type = type_
self._proxies = [element]
@@ -2869,7 +2891,7 @@ class ColumnClause(Immutable, ColumnElement):
:class:`.Column` class, is typically invoked using the
:func:`.column` function, as in::
- from sqlalchemy.sql import column
+ from sqlalchemy import column
id, name = column("id"), column("name")
stmt = select([id, name]).select_from("user")
@@ -2909,7 +2931,7 @@ class ColumnClause(Immutable, ColumnElement):
:class:`.Column` class. The :func:`.column` function can
be invoked with just a name alone, as in::
- from sqlalchemy.sql import column
+ from sqlalchemy import column
id, name = column("id"), column("name")
stmt = select([id, name]).select_from("user")
@@ -2941,7 +2963,7 @@ class ColumnClause(Immutable, ColumnElement):
(which is the lightweight analogue to :class:`.Table`) to produce
a working table construct with minimal boilerplate::
- from sqlalchemy.sql import table, column
+ from sqlalchemy import table, column, select
user = table("user",
column("id"),
@@ -2957,6 +2979,10 @@ class ColumnClause(Immutable, ColumnElement):
:class:`.schema.MetaData`, DDL, or events, unlike its
:class:`.Table` counterpart.
+ .. versionchanged:: 1.0.0 :func:`.expression.column` can now
+ be imported from the plain ``sqlalchemy`` namespace like any
+ other SQL element.
+
:param text: the text of the element.
:param type: :class:`.types.TypeEngine` object which can associate
@@ -3035,6 +3061,13 @@ class ColumnClause(Immutable, ColumnElement):
def _label(self):
return self._gen_label(self.name)
+ @_memoized_property
+ def _columns_clause_label(self):
+ if self.table is None:
+ return None
+ else:
+ return self._label
+
def _gen_label(self, name):
t = self.table
@@ -3438,12 +3471,29 @@ def _clause_element_as_expr(element):
return element
-def _literal_as_text(element):
+def _literal_as_label_reference(element):
+ if isinstance(element, util.string_types):
+ return _label_reference(element)
+ else:
+ return _literal_as_text(element)
+
+
+def _expression_literal_as_text(element):
+ return _literal_as_text(element, warn=True)
+
+
+def _literal_as_text(element, warn=False):
if isinstance(element, Visitable):
return element
elif hasattr(element, '__clause_element__'):
return element.__clause_element__()
elif isinstance(element, util.string_types):
+ if warn:
+ util.warn_limited(
+ "Textual SQL expression %(expr)r should be "
+ "explicitly declared as text(%(expr)r)",
+ {"expr": util.ellipses_string(element)})
+
return TextClause(util.text_type(element))
elif isinstance(element, (util.NoneType, bool)):
return _const_expr(element)
@@ -3498,6 +3548,8 @@ def _literal_as_binds(element, name=None, type_=None):
else:
return element
+_guess_straight_column = re.compile(r'^\w\S*$', re.I)
+
def _interpret_as_column_or_from(element):
if isinstance(element, Visitable):
@@ -3512,7 +3564,31 @@ def _interpret_as_column_or_from(element):
elif hasattr(insp, "selectable"):
return insp.selectable
- return ColumnClause(str(element), is_literal=True)
+ # be forgiving as this is an extremely common
+ # and known expression
+ if element == "*":
+ guess_is_literal = True
+ elif isinstance(element, (numbers.Number)):
+ return ColumnClause(str(element), is_literal=True)
+ else:
+ element = str(element)
+ # give into temptation, as this fact we are guessing about
+ # is not one we've previously ever needed our users tell us;
+ # but let them know we are not happy about it
+ guess_is_literal = not _guess_straight_column.match(element)
+ util.warn_limited(
+ "Textual column expression %(column)r should be "
+ "explicitly declared with text(%(column)r), "
+ "or use %(literal_column)s(%(column)r) "
+ "for more specificity",
+ {
+ "column": util.ellipses_string(element),
+ "literal_column": "literal_column"
+ if guess_is_literal else "column"
+ })
+ return ColumnClause(
+ element,
+ is_literal=guess_is_literal)
def _const_expr(element):