diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-09-01 20:19:54 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-09-01 20:19:54 -0400 |
commit | 7c6a45c480a865ac9580eb33fcca2dae5b19dd11 (patch) | |
tree | 870c078707cde0af769a940b1fc1a15ce7966691 /lib/sqlalchemy/sql/elements.py | |
parent | 382f82538b5484b1c384c71fbf84438312cbe34f (diff) | |
download | sqlalchemy-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.py | 110 |
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): |