--- /dev/null
+.. change::
+ :tags: bug, sql
+ :tickets: 4730
+
+ Fixed a series of quoting issues which all stemmed from the concept of the
+ :func:`.literal_column` construct, which when being "proxied" through a
+ subquery to be referred towards by a label that matches its text, the label
+ would not have quoting rules applied to it, even if the string in the
+ :class:`.Label` were set up as a :class:`.quoted_name` construct. Not
+ applying quoting to the text of the :class:`.Label` is a bug because this
+ text is strictly a SQL identifier name and not a SQL expression, and the
+ string should not have quotes embedded into it already unlike the
+ :func:`.literal_column` which it may be applied towards. The existing
+ behavior of a non-labeled :func:`.literal_column` being propagated as is on
+ the outside of a subquery is maintained in order to help with manual
+ quoting schemes, although it's not clear if valid SQL can be generated for
+ such a construct in any case.
\ No newline at end of file
)
if is_literal:
+ # note we are not currently accommodating for
+ # literal_column(quoted_name('ident', True)) here
name = self.escape_literal_column(name)
else:
name = self.preparer.quote(name)
def _make_proxy(self, selectable, name=None, **kw):
e = self.element._make_proxy(
- selectable, name=name if name else self.name
+ selectable,
+ name=name if name else self.name,
+ disallow_is_literal=True,
)
e._proxies.append(self)
if self._type is not None:
:ref:`sqlexpression_literal_column`
"""
-
self.key = self.name = text
self.table = _selectable
self.type = type_api.to_instance(type_)
name=None,
attach=True,
name_is_truncatable=False,
+ disallow_is_literal=False,
**kw
):
- # propagate the "is_literal" flag only if we are keeping our name,
- # otherwise its considered to be a label
- is_literal = self.is_literal and (name is None or name == self.name)
+ # the "is_literal" flag normally should never be propagated; a proxied
+ # column is always a SQL identifier and never the actual expression
+ # being evaluated. however, there is a case where the "is_literal" flag
+ # might be used to allow the given identifier to have a fixed quoting
+ # pattern already, so maintain the flag for the proxy unless a
+ # :class:`.Label` object is creating the proxy. See [ticket:4730].
+ is_literal = (
+ not disallow_is_literal
+ and self.is_literal
+ and (
+ # note this does not accommodate for quoted_name differences
+ # right now
+ name is None
+ or name == self.name
+ )
+ )
c = self._constructor(
coercions.expect(roles.TruncatedLabelRole, name or self.name)
if name_is_truncatable
key = c.anon_label
else:
key = None
-
c._make_proxy(self, key=key, name=name, name_is_truncatable=True)
def _refresh_for_new_column(self, column):
from sqlalchemy import func
from sqlalchemy import Index
from sqlalchemy import Integer
+from sqlalchemy import literal_column
from sqlalchemy import MetaData
from sqlalchemy import or_
from sqlalchemy import outerjoin
from sqlalchemy.dialects.oracle import cx_oracle
from sqlalchemy.engine import default
from sqlalchemy.sql import column
+from sqlalchemy.sql import quoted_name
from sqlalchemy.sql import table
from sqlalchemy.testing import assert_raises_message
from sqlalchemy.testing import AssertsCompiledSQL
"UPDATE",
)
+ def test_limit_special_quoting(self):
+ """Oracle-specific test for #4730.
+
+ Even though this issue is generic, test the originally reported Oracle
+ use case.
+
+ """
+
+ col = literal_column("SUM(ABC)").label("SUM(ABC)")
+ tbl = table("my_table")
+ query = select([col]).select_from(tbl).order_by(col).limit(100)
+
+ self.assert_compile(
+ query,
+ 'SELECT anon_1."SUM(ABC)" FROM '
+ '(SELECT SUM(ABC) AS "SUM(ABC)" '
+ "FROM my_table ORDER BY SUM(ABC)) anon_1 "
+ "WHERE ROWNUM <= :param_1",
+ )
+
+ col = literal_column("SUM(ABC)").label(quoted_name("SUM(ABC)", True))
+ tbl = table("my_table")
+ query = select([col]).select_from(tbl).order_by(col).limit(100)
+
+ self.assert_compile(
+ query,
+ 'SELECT anon_1."SUM(ABC)" FROM '
+ '(SELECT SUM(ABC) AS "SUM(ABC)" '
+ "FROM my_table ORDER BY SUM(ABC)) anon_1 "
+ "WHERE ROWNUM <= :param_1",
+ )
+
+ col = literal_column("SUM(ABC)").label("SUM(ABC)_")
+ tbl = table("my_table")
+ query = select([col]).select_from(tbl).order_by(col).limit(100)
+
+ self.assert_compile(
+ query,
+ 'SELECT anon_1."SUM(ABC)_" FROM '
+ '(SELECT SUM(ABC) AS "SUM(ABC)_" '
+ "FROM my_table ORDER BY SUM(ABC)) anon_1 "
+ "WHERE ROWNUM <= :param_1",
+ )
+
+ col = literal_column("SUM(ABC)").label(quoted_name("SUM(ABC)_", True))
+ tbl = table("my_table")
+ query = select([col]).select_from(tbl).order_by(col).limit(100)
+
+ self.assert_compile(
+ query,
+ 'SELECT anon_1."SUM(ABC)_" FROM '
+ '(SELECT SUM(ABC) AS "SUM(ABC)_" '
+ "FROM my_table ORDER BY SUM(ABC)) anon_1 "
+ "WHERE ROWNUM <= :param_1",
+ )
+
def test_for_update(self):
table1 = table(
"mytable", column("myid"), column("name"), column("description")
') AS "Alias1"',
)
+ def test_literal_column_label_embedded_select_samename(self):
+ col = sql.literal_column("NEEDS QUOTES").label("NEEDS QUOTES")
+
+ # embedded SELECT use case, going away in 1.4 however use a
+ # SelectStatementGrouping here when that merges
+ self.assert_compile(
+ select([col]).select(),
+ 'SELECT "NEEDS QUOTES" FROM (SELECT NEEDS QUOTES AS '
+ '"NEEDS QUOTES")',
+ )
+
+ def test_literal_column_label_alias_samename(self):
+ col = sql.literal_column("NEEDS QUOTES").label("NEEDS QUOTES")
+
+ self.assert_compile(
+ select([col]).alias().select(),
+ 'SELECT anon_1."NEEDS QUOTES" FROM (SELECT NEEDS QUOTES AS '
+ '"NEEDS QUOTES") AS anon_1',
+ )
+
+ def test_literal_column_label_embedded_select_diffname(self):
+ col = sql.literal_column("NEEDS QUOTES").label("NEEDS QUOTES_")
+
+ # embedded SELECT use case, going away in 1.4 however use a
+ # SelectStatementGrouping here when that merges
+ self.assert_compile(
+ select([col]).select(),
+ 'SELECT "NEEDS QUOTES_" FROM (SELECT NEEDS QUOTES AS '
+ '"NEEDS QUOTES_")',
+ )
+
+ def test_literal_column_label_alias_diffname(self):
+ col = sql.literal_column("NEEDS QUOTES").label("NEEDS QUOTES_")
+
+ self.assert_compile(
+ select([col]).alias().select(),
+ 'SELECT anon_1."NEEDS QUOTES_" FROM (SELECT NEEDS QUOTES AS '
+ '"NEEDS QUOTES_") AS anon_1',
+ )
+
+ def test_literal_column_label_embedded_select_samename_explcit_quote(self):
+ col = sql.literal_column("NEEDS QUOTES").label(
+ quoted_name("NEEDS QUOTES", True)
+ )
+
+ # embedded SELECT use case, going away in 1.4 however use a
+ # SelectStatementGrouping here when that merges
+ self.assert_compile(
+ select([col]).select(),
+ 'SELECT "NEEDS QUOTES" FROM '
+ '(SELECT NEEDS QUOTES AS "NEEDS QUOTES")',
+ )
+
+ def test_literal_column_label_alias_samename_explcit_quote(self):
+ col = sql.literal_column("NEEDS QUOTES").label(
+ quoted_name("NEEDS QUOTES", True)
+ )
+
+ self.assert_compile(
+ select([col]).alias().select(),
+ 'SELECT anon_1."NEEDS QUOTES" FROM '
+ '(SELECT NEEDS QUOTES AS "NEEDS QUOTES") AS anon_1',
+ )
+
+ def test_literal_column_label_embedded_select_diffname_explcit_quote(self):
+ col = sql.literal_column("NEEDS QUOTES").label(
+ quoted_name("NEEDS QUOTES_", True)
+ )
+
+ # embedded SELECT use case, going away in 1.4 however use a
+ # SelectStatementGrouping here when that merges
+ self.assert_compile(
+ select([col]).select(),
+ 'SELECT "NEEDS QUOTES_" FROM '
+ '(SELECT NEEDS QUOTES AS "NEEDS QUOTES_")',
+ )
+
+ def test_literal_column_label_alias_diffname_explcit_quote(self):
+ col = sql.literal_column("NEEDS QUOTES").label(
+ quoted_name("NEEDS QUOTES_", True)
+ )
+
+ self.assert_compile(
+ select([col]).alias().select(),
+ 'SELECT anon_1."NEEDS QUOTES_" FROM '
+ '(SELECT NEEDS QUOTES AS "NEEDS QUOTES_") AS anon_1',
+ )
+
+ def test_literal_column_wo_label_currently_maintained(self):
+ # test related to [ticket:4730] where we are maintaining that
+ # literal_column() proxied outwards *without* a label is maintained
+ # as is; in most cases literal_column would need proxying however
+ # at least if the column is being used to generate quoting in some
+ # way, it's maintined as given
+ col = sql.literal_column('"NEEDS QUOTES"')
+
+ self.assert_compile(
+ select([col]).alias().select(),
+ 'SELECT anon_1."NEEDS QUOTES" FROM '
+ '(SELECT "NEEDS QUOTES") AS anon_1',
+ )
+
def test_apply_labels_should_quote(self):
# Not lower case names, should quote
metadata = MetaData()
from sqlalchemy import union
from sqlalchemy import util
from sqlalchemy.sql import column
+from sqlalchemy.sql import quoted_name
from sqlalchemy.sql import table
from sqlalchemy.sql import util as sql_util
from sqlalchemy.testing import assert_raises_message
from sqlalchemy.testing import fixtures
from sqlalchemy.types import NullType
-
table1 = table(
"mytable",
column("myid", Integer),
def test_select_composition_six(self):
# test that "auto-labeling of subquery columns"
# doesn't interfere with literal columns,
- # exported columns don't get quoted
+ # exported columns don't get quoted.
+ # [ticket:4730] refines this but for the moment the behavior with
+ # no columns is being maintained.
self.assert_compile(
select(
[
"GROUP BY anon_1.myid",
)
+ def test_order_by_literal_col_quoting_one(self):
+ col = literal_column("SUM(ABC)").label("SUM(ABC)")
+ tbl = table("my_table")
+ query = select([col]).select_from(tbl).order_by(col)
+ self.assert_compile(
+ query,
+ 'SELECT SUM(ABC) AS "SUM(ABC)" FROM my_table ORDER BY "SUM(ABC)"',
+ )
+
+ def test_order_by_literal_col_quoting_two(self):
+ col = literal_column("SUM(ABC)").label("SUM(ABC)_")
+ tbl = table("my_table")
+ query = select([col]).select_from(tbl).order_by(col)
+ self.assert_compile(
+ query,
+ 'SELECT SUM(ABC) AS "SUM(ABC)_" FROM my_table ORDER BY '
+ '"SUM(ABC)_"',
+ )
+
+ def test_order_by_literal_col_quoting_one_explict_quote(self):
+ col = literal_column("SUM(ABC)").label(quoted_name("SUM(ABC)", True))
+ tbl = table("my_table")
+ query = select([col]).select_from(tbl).order_by(col)
+ self.assert_compile(
+ query,
+ 'SELECT SUM(ABC) AS "SUM(ABC)" FROM my_table ORDER BY "SUM(ABC)"',
+ )
+
+ def test_order_by_literal_col_quoting_two_explicit_quote(self):
+ col = literal_column("SUM(ABC)").label(quoted_name("SUM(ABC)_", True))
+ tbl = table("my_table")
+ query = select([col]).select_from(tbl).order_by(col)
+ self.assert_compile(
+ query,
+ 'SELECT SUM(ABC) AS "SUM(ABC)_" FROM my_table ORDER BY '
+ '"SUM(ABC)_"',
+ )
+
def test_order_by_func_label_desc(self):
stmt = select([func.foo("bar").label("fb"), table1]).order_by(
desc("fb")