Repository: kayak/pypika
Pull request #324 (2019-09-24 13:47:55)

Fixed quote chars for snowflake
Cleaned up usage of quote characters. Added support for "alias_quote_char" for use with Snowflake.


Patch:
diff --git a/pypika/dialects.py b/pypika/dialects.py
index eb8a345..9cf44b3 100644
--- a/pypika/dialects.py
+++ b/pypika/dialects.py
@@ -20,10 +20,10 @@ from pypika.utils import (
 
 class SnowFlakeQueryBuilder(QueryBuilder):
     QUOTE_CHAR = None
+    ALIAS_QUOTE_CHAR = '"'
 
     def __init__(self):
-        super(SnowFlakeQueryBuilder, self).__init__(quote_char=self.QUOTE_CHAR,
-                                                    dialect=Dialects.SNOWFLAKE)
+        super(SnowFlakeQueryBuilder, self).__init__(dialect=Dialects.SNOWFLAKE)
 
 
 class SnowflakeQuery(Query):
@@ -40,8 +40,7 @@ class MySQLQueryBuilder(QueryBuilder):
     QUOTE_CHAR = '`'
 
     def __init__(self):
-        super(MySQLQueryBuilder, self).__init__(quote_char=self.QUOTE_CHAR,
-                                                dialect=Dialects.MYSQL,
+        super(MySQLQueryBuilder, self).__init__(dialect=Dialects.MYSQL,
                                                 wrap_union_queries=False)
         self._duplicate_updates = []
         self._modifiers = []
@@ -56,20 +55,19 @@ class MySQLQueryBuilder(QueryBuilder):
         field = Field(field) if not isinstance(field, Field) else field
         self._duplicate_updates.append((field, ValueWrapper(value)))
 
-    def get_sql(self, with_alias=False, subquery=False, **kwargs):
-        querystring = super(MySQLQueryBuilder, self).get_sql(with_alias, subquery, **kwargs)
+    def get_sql(self, **kwargs):
+        self._set_kwargs_defaults(kwargs)
+        querystring = super(MySQLQueryBuilder, self).get_sql(**kwargs)
         if querystring and self._duplicate_updates:
             querystring += self._on_duplicate_key_update_sql(**kwargs)
         return querystring
 
     def _on_duplicate_key_update_sql(self, **kwargs):
-        return ' ON DUPLICATE KEY UPDATE {updates}'.format(
-            updates=','.join(
-                '{field}={value}'.format(
-                    field=field.get_sql(**kwargs),
-                    value=value.get_sql(**kwargs)) for field, value in self._duplicate_updates
-            )
-        )
+        return ' ON DUPLICATE KEY UPDATE {updates}' \
+            .format(updates=','.join('{field}={value}'
+                                     .format(field=field.get_sql(**kwargs),
+                                             value=value.get_sql(**kwargs))
+                                     for field, value in self._duplicate_updates))
 
     @builder
     def modifier(self, value):
@@ -212,11 +210,11 @@ class PostgreQueryBuilder(QueryBuilder):
             elif len(self._on_conflict_updates) > 0:
                 if self._on_conflict_field:
                     conflict_query += ' DO UPDATE SET {updates}'.format(
-                        updates=','.join(
-                            '{field}={value}'.format(
-                                field=field.get_sql(**kwargs),
-                                value=value.get_sql(**kwargs)) for field, value in self._on_conflict_updates
-                        )
+                          updates=','.join(
+                                '{field}={value}'.format(
+                                      field=field.get_sql(**kwargs),
+                                      value=value.get_sql(**kwargs)) for field, value in self._on_conflict_updates
+                          )
                     )
                 else:
                     raise QueryException('Can not have fieldless on conflict do update')
@@ -242,8 +240,8 @@ class PostgreQueryBuilder(QueryBuilder):
             if not any([self._insert_table, self._update_table, self._delete_from]):
                 raise QueryException('Returning can\'t be used in this query')
             if (
-                    field.table not in {self._insert_table, self._update_table}
-                    and term not in self._from
+                  field.table not in {self._insert_table, self._update_table}
+                  and term not in self._from
             ):
                 raise QueryException('You can\'t return from other tables')
 
@@ -286,8 +284,8 @@ class PostgreQueryBuilder(QueryBuilder):
 
     def _returning_sql(self, **kwargs):
         return ' RETURNING {returning}'.format(
-            returning=','.join(term.get_sql(with_alias=True, **kwargs)
-                               for term in self._returns),
+              returning=','.join(term.get_sql(with_alias=True, **kwargs)
+                                 for term in self._returns),
         )
 
     def get_sql(self, with_alias=False, subquery=False, **kwargs):
@@ -348,10 +346,10 @@ class MSSQLQueryBuilder(QueryBuilder):
 
     def _select_sql(self, **kwargs):
         return 'SELECT {distinct}{top}{select}'.format(
-            top=self._top_sql(),
-            distinct='DISTINCT ' if self._distinct else '',
-            select=','.join(term.get_sql(with_alias=True, subquery=True, **kwargs)
-                            for term in self._selects),
+              top=self._top_sql(),
+              distinct='DISTINCT ' if self._distinct else '',
+              select=','.join(term.get_sql(with_alias=True, subquery=True, **kwargs)
+                              for term in self._selects),
         )
 
 
@@ -386,6 +384,7 @@ class SQLLiteQuery(Query):
     """
     Defines a query class for use with Microsoft SQL Server.
     """
+
     @classmethod
     def _builder(cls):
         return QueryBuilder(dialect=Dialects.SQLLITE, wrapper_cls=SQLLiteValueWrapper)
diff --git a/pypika/queries.py b/pypika/queries.py
index f15f5f3..3b2eb06 100644
--- a/pypika/queries.py
+++ b/pypika/queries.py
@@ -21,8 +21,8 @@ from pypika.utils import (
     QueryException,
     RollupException,
     UnionException,
-    alias_sql,
     builder,
+    format_alias_sql,
     format_quotes,
     ignore_copy,
 )
@@ -127,15 +127,17 @@ class Table(Selectable):
         self._table_name = name
         self._schema = self._init_schema(schema)
 
-    def get_sql(self, quote_char=None, **kwargs):
+    def get_sql(self, **kwargs):
+        quote_char = kwargs.get('quote_char')
         # FIXME escape
         table_sql = format_quotes(self._table_name, quote_char)
 
         if self._schema is not None:
             table_sql = '{schema}.{table}' \
-                .format(schema=self._schema.get_sql(quote_char=quote_char, **kwargs),
+                .format(schema=self._schema.get_sql(**kwargs),
                         table=table_sql)
-        return alias_sql(table_sql, self.alias, quote_char)
+
+        return format_alias_sql(table_sql, self.alias, **kwargs)
 
     def __str__(self):
         return self.get_sql(quote_char='"')
@@ -354,7 +356,7 @@ class _UnionQuery(Selectable, Term):
         kwargs.setdefault('dialect', self.base_query.dialect)
         # This initializes the quote char based on the base query, which could be a dialect specific query class
         # This might be overridden if quote_char is set explicitly in kwargs
-        kwargs.setdefault('quote_char', self.base_query.quote_char)
+        kwargs.setdefault('quote_char', self.base_query.QUOTE_CHAR)
 
         base_querystring = self.base_query.get_sql(subquery=self.base_query.wrap_union_queries, **kwargs)
 
@@ -383,7 +385,7 @@ class _UnionQuery(Selectable, Term):
             querystring = '({query})'.format(query=querystring, **kwargs)
 
         if with_alias:
-            return alias_sql(querystring, self.alias or self._table_name, kwargs.get('quote_char'))
+            return format_alias_sql(querystring, self.alias or self._table_name, **kwargs)
 
         return querystring
 
@@ -420,10 +422,11 @@ class QueryBuilder(Selectable, Term):
     Query Builder is the main class in pypika which stores the state of a query and offers functions which allow the
     state to be branched immutably.
     """
+    QUOTE_CHAR = '"'
+    SECONDARY_QUOTE_CHAR = "'"
+    ALIAS_QUOTE_CHAR = None
 
     def __init__(self,
-                 quote_char='"',
-                 secondary_quote_char="'",
                  dialect=None,
                  wrap_union_queries=True,
                  wrapper_cls=ValueWrapper):
@@ -464,8 +467,6 @@ class QueryBuilder(Selectable, Term):
         self._subquery_count = 0
         self._foreign_table = False
 
-        self.quote_char = quote_char
-        self.secondary_quote_char = secondary_quote_char
         self.dialect = dialect
         self.wrap_union_queries = wrap_union_queries
 
@@ -856,7 +857,7 @@ class QueryBuilder(Selectable, Term):
                                  for value in values])
 
     def __str__(self):
-        return self.get_sql(quote_char=self.quote_char, dialect=self.dialect)
+        return self.get_sql(dialect=self.dialect)
 
     def __repr__(self):
         return self.__str__()
@@ -876,11 +877,15 @@ class QueryBuilder(Selectable, Term):
     def __hash__(self):
         return hash(self.alias) + sum(hash(clause) for clause in self._from)
 
-    def get_sql(self, with_alias=False, subquery=False, **kwargs):
-        kwargs.setdefault('quote_char', self.quote_char)
-        kwargs.setdefault('secondary_quote_char', self.secondary_quote_char)
+    def _set_kwargs_defaults(self, kwargs):
+        kwargs.setdefault('quote_char', self.QUOTE_CHAR)
+        kwargs.setdefault('secondary_quote_char', self.SECONDARY_QUOTE_CHAR)
+        kwargs.setdefault('alias_quote_char', self.ALIAS_QUOTE_CHAR)
         kwargs.setdefault('dialect', self.dialect)
 
+    def get_sql(self, with_alias=False, subquery=False, **kwargs):
+        self._set_kwargs_defaults(kwargs)
+
         if not (self._selects or self._insert_table or self._delete_from or self._update_table):
             return ''
         if self._insert_table and not (self._selects or self._values):
@@ -893,12 +898,12 @@ class QueryBuilder(Selectable, Term):
         has_subquery_from_clause = 0 < len(self._from) and isinstance(self._from[0], QueryBuilder)
         has_reference_to_foreign_table = self._foreign_table
 
-        kwargs['with_namespace'] = any((
+        kwargs['with_namespace'] = any([
             has_joins,
             has_multiple_from_clauses,
             has_subquery_from_clause,
             has_reference_to_foreign_table
-        ))
+        ])
 
         if self._update_table:
             querystring = self._update_sql(**kwargs)
@@ -980,7 +985,7 @@ class QueryBuilder(Selectable, Term):
             querystring = '({query})'.format(query=querystring)
 
         if with_alias:
-            return alias_sql(querystring, self.alias, kwargs.get('quote_char'))
+            return format_alias_sql(querystring, self.alias, **kwargs)
 
         return querystring
 
@@ -1057,7 +1062,7 @@ class QueryBuilder(Selectable, Term):
     def _where_sql(self, quote_char=None, **kwargs):
         return ' WHERE {where}'.format(where=self._wheres.get_sql(quote_char=quote_char, subquery=True, **kwargs))
 
-    def _group_sql(self, quote_char=None, groupby_alias=True, **kwargs):
+    def _group_sql(self, quote_char=None, alias_quote_char=None, groupby_alias=True, **kwargs):
         """
         Produces the GROUP BY part of the query.  This is a list of fields. The clauses are stored in the query under
         self._groupbys as a list fields.
@@ -1071,16 +1076,18 @@ class QueryBuilder(Selectable, Term):
         selected_aliases = {s.alias for s in self._selects}
         for field in self._groupbys:
             if groupby_alias and field.alias and field.alias in selected_aliases:
-                clauses.append(format_quotes(field.alias, quote_char))
+                clauses.append(format_quotes(field.alias, alias_quote_char or quote_char))
             else:
-                clauses.append(field.get_sql(quote_char=quote_char, **kwargs))
+                clauses.append(field.get_sql(quote_char=quote_char, alias_quote_char=alias_quote_char, **kwargs))
 
         sql = ' GROUP BY {groupby}'.format(groupby=','.join(clauses))
+
         if self._with_totals:
             return sql + ' WITH TOTALS'
+
         return sql
 
-    def _orderby_sql(self, quote_char=None, orderby_alias=True, **kwargs):
+    def _orderby_sql(self, quote_char=None, alias_quote_char=None, orderby_alias=True, **kwargs):
         """
         Produces the ORDER BY part of the query.  This is a list of fields and possibly their directionality, ASC or
         DESC. The clauses are stored in the query under self._orderbys as a list of tuples containing the field and
@@ -1094,9 +1101,9 @@ class QueryBuilder(Selectable, Term):
         clauses = []
         selected_aliases = {s.alias for s in self._selects}
         for field, directionality in self._orderbys:
-            term = format_quotes(field.alias, quote_char) \
+            term = format_quotes(field.alias, alias_quote_char or quote_char) \
                 if orderby_alias and field.alias and field.alias in selected_aliases \
-                else field.get_sql(quote_char=quote_char, **kwargs)
+                else field.get_sql(quote_char=quote_char, alias_quote_char=alias_quote_char, **kwargs)
 
             clauses.append('{term} {orient}'.format(term=term, orient=directionality.value)
                            if directionality is not None else term)
diff --git a/pypika/terms.py b/pypika/terms.py
index 61ba3b1..e06b158 100644
--- a/pypika/terms.py
+++ b/pypika/terms.py
@@ -1,6 +1,7 @@
 import inspect
 import itertools
 import re
+from copy import copy
 from datetime import date
 from enum import Enum
 
@@ -19,7 +20,7 @@ from pypika.enums import (
 )
 from pypika.utils import (
     CaseException,
-    alias_sql,
+    format_alias_sql,
     builder,
     format_quotes,
     ignore_copy,
@@ -280,7 +281,7 @@ class ValueWrapper(Term):
         return []
 
     def get_value_sql(self, **kwargs):
-        quote_char = kwargs.get('secondary_quote_char')
+        quote_char = kwargs.get('secondary_quote_char') or ''
 
         # FIXME escape values
         if isinstance(self.value, Term):
@@ -301,7 +302,7 @@ class ValueWrapper(Term):
 
     def get_sql(self, quote_char=None, secondary_quote_char="'", **kwargs):
         sql = self.get_value_sql(quote_char=quote_char, secondary_quote_char=secondary_quote_char, **kwargs)
-        return alias_sql(sql, self.alias, quote_char)
+        return format_alias_sql(sql, self.alias, quote_char=quote_char, **kwargs)
 
 
 class JSON(Term):
@@ -381,7 +382,7 @@ class NullValue(Term):
 
     def get_sql(self, quote_char=None, **kwargs):
         sql = 'NULL'
-        return alias_sql(sql, self.alias, quote_char)
+        return format_alias_sql(sql, self.alias, **kwargs)
 
 
 class Criterion(Term):
@@ -472,12 +473,10 @@ class Field(Criterion, JSON):
             )
 
         field_alias = getattr(self, 'alias', None)
-        if not with_alias or field_alias is None:
-            return field_sql
+        if with_alias:
+            return format_alias_sql(field_sql, field_alias, quote_char=quote_char, **kwargs)
 
-        return alias_sql(field_sql, field_alias, quote_char)
-
-    # JSON functions (for PostgreSQL)
+        return field_sql
 
 
 class Star(Field):
@@ -555,11 +554,8 @@ class Bracket(Tuple):
         super(Bracket, self).__init__(term)
 
     def get_sql(self, **kwargs):
-        return alias_sql(
-              sql=super(Bracket, self).get_sql(**kwargs),
-              alias=self.alias,
-              quote_char=kwargs.get('quote_char', None),
-        )
+        sql = super(Bracket, self).get_sql(**kwargs)
+        return format_alias_sql(sql=sql, alias=self.alias, **kwargs)
 
 
 class NestedCriterion(Criterion):
@@ -606,8 +602,9 @@ class NestedCriterion(Criterion):
               nested_comparator=self.nested_comparator.value,
               nested=self.nested.get_sql(**kwargs)
         )
-        if with_alias and self.alias:
-            return '{sql} "{alias}"'.format(sql=sql, alias=self.alias)
+
+        if with_alias:
+            return format_alias_sql(sql=sql, alias=self.alias, **kwargs)
 
         return sql
 
@@ -923,10 +920,10 @@ class ArithmeticExpression(Term):
               right=("({})" if is_mul and is_right_add else "{}").format(self.right.get_sql(**kwargs)),
         )
 
-        if not with_alias or self.alias is None:
-            return arithmatic_sql
+        if with_alias:
+            return format_alias_sql(arithmatic_sql, self.alias, **kwargs)
 
-        return alias_sql(arithmatic_sql, self.alias, quote_char)
+        return arithmatic_sql
 
 
 class Case(Term):
@@ -981,10 +978,10 @@ class Case(Term):
 
         case_sql = 'CASE {cases}{else_} END'.format(cases=cases, else_=else_)
 
-        if not with_alias or self.alias is None:
-            return case_sql
+        if with_alias:
+            return format_alias_sql(case_sql, self.alias, **kwargs)
 
-        return alias_sql(case_sql, self.alias, kwargs.get('quote_char'))
+        return case_sql
 
     def fields(self):
         fields = []
@@ -1022,11 +1019,10 @@ class Not(Criterion):
     def fields(self):
         return self.term.fields() if self.term.fields else []
 
-    def get_sql(self, quote_char=None, **kwargs):
+    def get_sql(self, **kwargs):
         kwargs['subcriterion'] = True
-        sql = "NOT {term}".format(term=self.term.get_sql(quote_char=quote_char,
-                                                         **kwargs))
-        return alias_sql(sql, self.alias, quote_char=quote_char)
+        sql = "NOT {term}".format(term=self.term.get_sql(**kwargs))
+        return format_alias_sql(sql, self.alias, **kwargs)
 
     @ignore_copy
     def __getattr__(self, name):
@@ -1134,10 +1130,10 @@ class Function(Criterion):
                 .format(schema=self.schema.get_sql(quote_char=quote_char, dialect=dialect, **kwargs),
                         function=function_sql)
 
-        if not with_alias or self.alias is None:
-            return function_sql
+        if with_alias:
+            return format_alias_sql(function_sql, self.alias, quote_char=quote_char, **kwargs)
 
-        return alias_sql(function_sql, self.alias, quote_char)
+        return function_sql
 
 
 class AggregateFunction(Function):
diff --git a/pypika/utils.py b/pypika/utils.py
index 2d43733..9865303 100644
--- a/pypika/utils.py
+++ b/pypika/utils.py
@@ -96,10 +96,10 @@ def format_quotes(value, quote_char):
         .format(value=value, quote=quote_char or '')
 
 
-def alias_sql(sql, alias, quote_char=None):
+def format_alias_sql(sql, alias, quote_char=None, alias_quote_char=None, **kwargs):
     if alias is None:
         return sql
-    return '{sql} {alias}'.format(sql=sql, alias=format_quotes(alias, quote_char))
+    return '{sql} {alias}'.format(sql=sql, alias=format_quotes(alias, alias_quote_char or quote_char))
 
 
 def validate(*args, exc=None, type=None):


Test patch:
diff --git a/pypika/tests/dialects/test_snowflake.py b/pypika/tests/dialects/test_snowflake.py
index 2b3bcb7..9d967cb 100644
--- a/pypika/tests/dialects/test_snowflake.py
+++ b/pypika/tests/dialects/test_snowflake.py
@@ -1,13 +1,46 @@
 import unittest
 
-from pypika import Table
+from pypika import (
+    Tables,
+    functions as fn,
+)
 from pypika.dialects import SnowflakeQuery
 
 
 class QuoteTests(unittest.TestCase):
-    table_abc = Table('abc')
+    table_abc, table_efg = Tables('abc', 'efg')
 
-    def test_no_double_quotes(self):
-        q = SnowflakeQuery.from_('abc').select('a')
+    def test_use_double_quotes_on_alias_but_not_on_terms(self):
+        q = SnowflakeQuery.from_(self.table_abc).select(self.table_abc.a.as_('bar'))
 
-        self.assertEqual('SELECT a FROM abc', str(q))
+        self.assertEqual('SELECT a "bar" '
+                         'FROM abc', q.get_sql(with_namespace=True))
+
+    def test_use_double_quotes_on_alias_but_not_on_terms_with_joins(self):
+        foo = self.table_abc.as_('foo')
+        bar = self.table_efg.as_('bar')
+
+        q = SnowflakeQuery \
+            .from_(foo) \
+            .join(bar) \
+            .on(foo.fk == bar.id) \
+            .select(foo.a, bar.b)
+
+        self.assertEqual('SELECT foo.a,bar.b '
+                         'FROM abc "foo" '
+                         'JOIN efg "bar" '
+                         'ON foo.fk=bar.id', q.get_sql(with_namespace=True))
+
+    def test_use_double_quotes_on_alias_but_not_on_terms(self):
+        idx = self.table_abc.index.as_('idx')
+        val = fn.Sum(self.table_abc.value).as_('val')
+        q = SnowflakeQuery \
+            .from_(self.table_abc) \
+            .select(idx, val) \
+            .groupby(idx) \
+            .orderby(idx)
+
+        self.assertEqual('SELECT index "idx",SUM(value) "val" '
+                         'FROM abc '
+                         'GROUP BY "idx" '
+                         'ORDER BY "idx"', q.get_sql(with_namespace=True))
diff --git a/pypika/tests/test_formats.py b/pypika/tests/test_formats.py
index f89477a..9197597 100644
--- a/pypika/tests/test_formats.py
+++ b/pypika/tests/test_formats.py
@@ -43,7 +43,7 @@ class QuoteTests(unittest.TestCase):
                          'FROM `efg`'
                          ') `sq1` ON `sq0`.`foo`=`sq1`.`foo_two`', self.query.get_sql(quote_char='`'))
 
-    def test_remove_quote_char_in_complex_query(self):
+    def test_no_quote_char_in_complex_query(self):
         self.assertEqual('SELECT '
                          'sq0.foo,sq0.fizzbuzz,'
                          'sq1.foo_two,sq1.bar '
diff --git a/pypika/tests/test_joins.py b/pypika/tests/test_joins.py
index c4b1f57..499808f 100644
--- a/pypika/tests/test_joins.py
+++ b/pypika/tests/test_joins.py
@@ -255,15 +255,15 @@ class JoinBehaviorTests(unittest.TestCase):
                          'JOIN "abc" "abc2" ON "abc"."foo"="abc2"."bar"', str(q))
 
     def test_join_same_table_with_prefixes(self):
-        table1 = Table('abc', alias='x')
-        table2 = Table('abc', alias='y')
+        table1 = Table('abc').as_('x')
+        table2 = Table('abc').as_('y')
         q = Query.from_(table1).join(table2).on(table1.foo == table2.bar).select(table1.foo, table2.buz)
 
         self.assertEqual('SELECT "x"."foo","y"."buz" FROM "abc" "x" '
                          'JOIN "abc" "y" ON "x"."foo"="y"."bar"', str(q))
 
     def test_join_table_twice(self):
-        table1, table2 = Table('efg', alias='efg1'), Table('efg', alias='efg2')
+        table1, table2 = Table('efg').as_('efg1'), Table('efg').as_('efg2')
         q = Query.from_(self.table_abc) \
             .join(table1).on(self.table_abc.foo == table1.bar) \
             .join(table2).on(self.table_abc.foo == table2.bam) \
@@ -386,7 +386,7 @@ class JoinBehaviorTests(unittest.TestCase):
                          ') "sq0" '
                          'ON "sq0"."x"="abc"."id"', str(test_query))
 
-    def test_join_query_with_alias(self):
+    def test_join_query_with_column_alias(self):
         subquery = Query.from_(self.table_efg) \
             .select(self.table_efg.base_id.as_('x'), self.table_efg.fizz, self.table_efg.buzz) \
             .as_('subq')
@@ -402,6 +402,23 @@ class JoinBehaviorTests(unittest.TestCase):
                          ') "subq" '
                          'ON "subq"."x"="abc"."id"', str(test_query))
 
+    def test_join_query_with_table_alias(self):
+        xxx = self.table_efg.as_('xxx')
+        subquery = Query.from_(xxx) \
+            .select(xxx.base_id, xxx.fizz, xxx.buzz) \
+            .as_('subq')
+
+        test_query = Query.from_(self.table_abc) \
+            .join(subquery) \
+            .on(subquery.x == self.table_abc.id) \
+            .select(self.table_abc.foo, subquery.fizz, subquery.buzz)
+
+        self.assertEqual('SELECT "abc"."foo","subq"."fizz","subq"."buzz" '
+                         'FROM "abc" JOIN ('
+                         'SELECT "xxx"."base_id","xxx"."fizz","xxx"."buzz" FROM "efg" "xxx"'
+                         ') "subq" '
+                         'ON "subq"."x"="abc"."id"', str(test_query))
+
 
 class UnionTests(unittest.TestCase):
     table1, table2 = Tables('abc', 'efg')
diff --git a/pypika/tests/test_selects.py b/pypika/tests/test_selects.py
index 9c153a8..4c10648 100644
--- a/pypika/tests/test_selects.py
+++ b/pypika/tests/test_selects.py
@@ -94,6 +94,11 @@ class SelectTests(unittest.TestCase):
 
         self.assertEqual('SELECT "foo" "bar" FROM "abc"', str(q))
 
+    def test_select__column__single__table_alias__str(self):
+        q = Query.from_(self.table_abc.as_('fizzbuzz')).select(self.table_abc.foo.as_('bar'))
+
+        self.assertEqual('SELECT "foo" "bar" FROM "abc" "fizzbuzz"', str(q))
+
     def test_select__column__single__field(self):
         t = Table('abc')
         q = Query.from_(t).select(t.foo)