pypika icon indicating copy to clipboard operation
pypika copied to clipboard

Subquery failing when used in outer where clause

Open ajustintrue opened this issue 3 years ago • 2 comments

I wrote the 2 unit tests below to illustrate an issue I am facing with subqueries used within the where clause of an outer query.

My expectation is that I should be able to use a subquery anywhere I can use any other field or function. However, use within the where clause is failing to produce a viable query.

class SubQueryTests(unittest.TestCase):
    table_a, table_b, table_c, table_d = Tables("a", "b", "c", "d")
    subquery_select_column = Field("selected_from_subquery", table=table_a)
    subquery_join_column = Field("id", table=table_a)
    outer_query_join_column = Field("a_id", table=table_b)
    subquery = (
        Query.from_(table_a).where(subquery_join_column == outer_query_join_column).select(subquery_select_column).as_("a")
    )

    # #1 - results in AttributeError: 'bool' object has no attribute 'fields_'
    def test_subquery_in_outer_where_clause(self):
        outer_query = Query.from_(self.table_b).where(self.subquery == 1).select("id")
        self.assertEqual(
            'SELECT "id" FROM "b" WHERE "(SELECT "a"."selected_from_subquery" FROM "a" WHERE "a"."id"="b"."a_id")"=1', str(outer_query)
        )

    # #2 - Field class wrapper helps but results in a malformed query because the subquery is missing parens
    def test_subquery_in_outer_where_clause_field_wrapped(self):
        outer_query = Query.from_(self.table_b).where(Field(self.subquery) == 1).select("id")
        self.assertEqual(
            'SELECT "id" FROM "b" WHERE "(SELECT "a"."selected_from_subquery" FROM "a" WHERE "a"."id"="b"."a_id")"=1', str(outer_query)
        )

    # #3 - results in AttributeError: 'bool' object has no attribute 'fields_'
    def test_subquery_in_outer_where_clause_get_sql_method(self):
        outer_query = Query.from_(self.table_b).where(self.subquery.get_sql(subquery=True) == 1).select("id")
        self.assertEqual(
            'SELECT "id" FROM "b" WHERE "(SELECT "a"."selected_from_subquery" FROM "a" WHERE "a"."id"="b"."a_id")"=1', str(outer_query)

        )

    # #4 successful when subquery=True passed and the subquery is wrapped in Field class
    def test_subquery_in_outer_where_clause_get_sql_method_field_wrapped(self):
        outer_query = Query.from_(self.table_b).where(Field(self.subquery.get_sql(subquery=True)) == 1).select("id")
        self.assertEqual(
            'SELECT "id" FROM "b" WHERE "(SELECT "a"."selected_from_subquery" FROM "a" WHERE "a"."id"="b"."a_id")"=1', str(outer_query)

        )
    
    # #5 an example where subquery works fine as a selected field
    def test_subquery_in_outer_select(self):
        outer_query = Query.from_(self.table_b).select(Field(self.subquery))
        self.assertEqual(
            'SELECT (SELECT "a"."selected_from_subquery" FROM "a" WHERE "a"."id"="b"."a_id") FROM "b"', str(outer_query)
        )

    # #6 example where subquery works fine within a function
    def test_subquery_in_function(self):
        outer_query = Query.from_(self.table_b).select(functions.Upper(self.subquery))
        self.assertEqual(
            'SELECT UPPER((SELECT "a"."selected_from_subquery" FROM "a" WHERE "a"."id"="b"."a_id")) FROM "b"',
            str(outer_query),
        )

My current workaround is to override the get_sql method inside my project and useQuery and QueryBuilder as superclasses like the below examples. I would prefer to not do this and can try to help with a solution that works better than this.

class SubQuery(Query):
    @classmethod
    def _builder(cls):
        return SubQueryBuilder()


class SubQueryBuilder(QueryBuilder):
    def get_sql(self, with_alias=False, subquery=False, **kwargs):
        _subquery = True
        return super().get_sql(with_alias=with_alias, subquery=_subquery, **kwargs)

ajustintrue avatar Sep 09 '21 15:09 ajustintrue