Repository: kayak/pypika
Pull request #371 (2020-01-27 10:45:30)

Added setting to disable immutability
Added a flag to querybuilder which enables the immutable behavior of chained methods to be disabled

coveralls: 
[![Coverage Status](https://coveralls.io/builds/28344159/badge)](https://coveralls.io/builds/28344159)

Coverage increased (+0.02%) to 97.67% when pulling **09d4c3144f4afb8a9bec33a2ae00e813b85fb359 on add_flag_to_disable_immutability** into **3b21a949f7ee8f8d2b5c97883f24be3d0fcfe508 on master**.


Patch:
diff --git a/pypika/dialects.py b/pypika/dialects.py
index fec7fee..2465c9d 100644
--- a/pypika/dialects.py
+++ b/pypika/dialects.py
@@ -2,9 +2,9 @@ from copy import copy
 
 from pypika.enums import Dialects
 from pypika.queries import (
+    CreateQueryBuilder,
     Query,
     QueryBuilder,
-    CreateQueryBuilder,
     Table,
 )
 from pypika.terms import (
@@ -24,8 +24,10 @@ class SnowFlakeQueryBuilder(QueryBuilder):
     QUOTE_CHAR = None
     ALIAS_QUOTE_CHAR = '"'
 
-    def __init__(self):
-        super(SnowFlakeQueryBuilder, self).__init__(dialect=Dialects.SNOWFLAKE)
+    def __init__(self, **kwargs):
+        super(SnowFlakeQueryBuilder, self).__init__(
+            dialect=Dialects.SNOWFLAKE, **kwargs
+        )
 
 
 class SnowflakeQuery(Query):
@@ -34,16 +36,16 @@ class SnowflakeQuery(Query):
     """
 
     @classmethod
-    def _builder(cls):
-        return SnowFlakeQueryBuilder()
+    def _builder(cls, **kwargs):
+        return SnowFlakeQueryBuilder(**kwargs)
 
 
 class MySQLQueryBuilder(QueryBuilder):
     QUOTE_CHAR = "`"
 
-    def __init__(self):
+    def __init__(self, **kwargs):
         super(MySQLQueryBuilder, self).__init__(
-            dialect=Dialects.MYSQL, wrap_union_queries=False
+            dialect=Dialects.MYSQL, wrap_union_queries=False, **kwargs
         )
         self._duplicate_updates = []
         self._modifiers = []
@@ -141,8 +143,8 @@ class MySQLQuery(Query):
     """
 
     @classmethod
-    def _builder(cls):
-        return MySQLQueryBuilder()
+    def _builder(cls, **kwargs):
+        return MySQLQueryBuilder(**kwargs)
 
     @classmethod
     def load(cls, fp):
@@ -150,8 +152,8 @@ class MySQLQuery(Query):
 
 
 class VerticaQueryBuilder(QueryBuilder):
-    def __init__(self):
-        super(VerticaQueryBuilder, self).__init__(dialect=Dialects.VERTICA)
+    def __init__(self, **kwargs):
+        super(VerticaQueryBuilder, self).__init__(dialect=Dialects.VERTICA, **kwargs)
         self._hint = None
 
     @builder
@@ -253,8 +255,8 @@ class VerticaQuery(Query):
     """
 
     @classmethod
-    def _builder(cls):
-        return VerticaQueryBuilder()
+    def _builder(cls, **kwargs):
+        return VerticaQueryBuilder(**kwargs)
 
     @classmethod
     def from_file(cls, fp):
@@ -266,8 +268,8 @@ class VerticaQuery(Query):
 
 
 class OracleQueryBuilder(QueryBuilder):
-    def __init__(self):
-        super(OracleQueryBuilder, self).__init__(dialect=Dialects.ORACLE)
+    def __init__(self, **kwargs):
+        super(OracleQueryBuilder, self).__init__(dialect=Dialects.ORACLE, **kwargs)
 
     def get_sql(self, *args, **kwargs):
         return super(OracleQueryBuilder, self).get_sql(
@@ -281,13 +283,13 @@ class OracleQuery(Query):
     """
 
     @classmethod
-    def _builder(cls):
-        return OracleQueryBuilder()
+    def _builder(cls, **kwargs):
+        return OracleQueryBuilder(**kwargs)
 
 
 class PostgreQueryBuilder(QueryBuilder):
-    def __init__(self):
-        super(PostgreQueryBuilder, self).__init__(dialect=Dialects.POSTGRESQL)
+    def __init__(self, **kwargs):
+        super(PostgreQueryBuilder, self).__init__(dialect=Dialects.POSTGRESQL, **kwargs)
         self._returns = []
         self._return_star = False
         self._on_conflict_field = None
@@ -446,8 +448,8 @@ class PostgreSQLQuery(Query):
     """
 
     @classmethod
-    def _builder(cls):
-        return PostgreQueryBuilder()
+    def _builder(cls, **kwargs):
+        return PostgreQueryBuilder(**kwargs)
 
 
 class RedshiftQuery(Query):
@@ -456,13 +458,13 @@ class RedshiftQuery(Query):
     """
 
     @classmethod
-    def _builder(cls):
-        return QueryBuilder(dialect=Dialects.REDSHIFT)
+    def _builder(cls, **kwargs):
+        return QueryBuilder(dialect=Dialects.REDSHIFT, **kwargs)
 
 
 class MSSQLQueryBuilder(QueryBuilder):
-    def __init__(self):
-        super(MSSQLQueryBuilder, self).__init__(dialect=Dialects.MSSQL)
+    def __init__(self, **kwargs):
+        super(MSSQLQueryBuilder, self).__init__(dialect=Dialects.MSSQL, **kwargs)
         self._top = None
 
     @builder
@@ -507,8 +509,8 @@ class MSSQLQuery(Query):
     """
 
     @classmethod
-    def _builder(cls):
-        return MSSQLQueryBuilder()
+    def _builder(cls, **kwargs):
+        return MSSQLQueryBuilder(**kwargs)
 
 
 class ClickHouseQuery(Query):
@@ -517,8 +519,10 @@ class ClickHouseQuery(Query):
     """
 
     @classmethod
-    def _builder(cls):
-        return QueryBuilder(dialect=Dialects.CLICKHOUSE, wrap_union_queries=False)
+    def _builder(cls, **kwargs):
+        return QueryBuilder(
+            dialect=Dialects.CLICKHOUSE, wrap_union_queries=False, **kwargs
+        )
 
 
 class SQLLiteValueWrapper(ValueWrapper):
@@ -534,5 +538,7 @@ class SQLLiteQuery(Query):
     """
 
     @classmethod
-    def _builder(cls):
-        return QueryBuilder(dialect=Dialects.SQLLITE, wrapper_cls=SQLLiteValueWrapper)
+    def _builder(cls, **kwargs):
+        return QueryBuilder(
+            dialect=Dialects.SQLLITE, wrapper_cls=SQLLiteValueWrapper, **kwargs
+        )
diff --git a/pypika/queries.py b/pypika/queries.py
index 51fa135..92afea8 100644
--- a/pypika/queries.py
+++ b/pypika/queries.py
@@ -277,11 +277,11 @@ class Query:
     """
 
     @classmethod
-    def _builder(cls):
-        return QueryBuilder()
+    def _builder(cls, **kwargs):
+        return QueryBuilder(**kwargs)
 
     @classmethod
-    def from_(cls, table):
+    def from_(cls, table, **kwargs):
         """
         Query builder entry point.  Initializes query building and sets the table to select from.  When using this
         function, the query becomes a SELECT query.
@@ -293,7 +293,7 @@ class Query:
 
         :returns QueryBuilder
         """
-        return cls._builder().from_(table)
+        return cls._builder(**kwargs).from_(table)
 
     @classmethod
     def create_table(cls, table):
@@ -308,7 +308,7 @@ class Query:
         return CreateQueryBuilder().create_table(table)
 
     @classmethod
-    def into(cls, table):
+    def into(cls, table, **kwargs):
         """
         Query builder entry point.  Initializes query building and sets the table to insert into.  When using this
         function, the query becomes an INSERT query.
@@ -320,14 +320,14 @@ class Query:
 
         :returns QueryBuilder
         """
-        return cls._builder().into(table)
+        return cls._builder(**kwargs).into(table)
 
     @classmethod
-    def with_(cls, table, name):
-        return cls._builder().with_(table, name)
+    def with_(cls, table, name, **kwargs):
+        return cls._builder(**kwargs).with_(table, name)
 
     @classmethod
-    def select(cls, *terms):
+    def select(cls, *terms, **kwargs):
         """
         Query builder entry point.  Initializes query building without a table and selects fields.  Useful when testing
         SQL functions.
@@ -340,10 +340,10 @@ class Query:
 
         :returns QueryBuilder
         """
-        return cls._builder().select(*terms)
+        return cls._builder(**kwargs).select(*terms)
 
     @classmethod
-    def update(cls, table):
+    def update(cls, table, **kwargs):
         """
         Query builder entry point.  Initializes query building and sets the table to update.  When using this
         function, the query becomes an UPDATE query.
@@ -355,7 +355,7 @@ class Query:
 
         :returns QueryBuilder
         """
-        return cls._builder().update(table)
+        return cls._builder(**kwargs).update(table)
 
 
 class _UnionQuery(Selectable, Term):
@@ -506,7 +506,13 @@ class QueryBuilder(Selectable, Term):
     SECONDARY_QUOTE_CHAR = "'"
     ALIAS_QUOTE_CHAR = None
 
-    def __init__(self, dialect=None, wrap_union_queries=True, wrapper_cls=ValueWrapper):
+    def __init__(
+        self,
+        dialect=None,
+        wrap_union_queries=True,
+        wrapper_cls=ValueWrapper,
+        immutable=True,
+    ):
         super(QueryBuilder, self).__init__(None)
 
         self._from = []
@@ -550,6 +556,8 @@ class QueryBuilder(Selectable, Term):
 
         self._wrapper_cls = wrapper_cls
 
+        self.immutable = immutable
+
     def __copy__(self):
         newone = type(self).__new__(type(self))
         newone.__dict__.update(self.__dict__)
diff --git a/pypika/utils.py b/pypika/utils.py
index b9d7daf..85aa5c5 100644
--- a/pypika/utils.py
+++ b/pypika/utils.py
@@ -44,7 +44,7 @@ def builder(func):
     import copy
 
     def _copy(self, *args, **kwargs):
-        self_copy = copy.copy(self)
+        self_copy = copy.copy(self) if getattr(self, "immutable", True) else self
         result = func(self_copy, *args, **kwargs)
 
         # Return self if the inner function returns None.  This way the inner function can return something


Test patch:
diff --git a/pypika/tests/test_immutability.py b/pypika/tests/test_immutability.py
index 9abad16..17b997f 100644
--- a/pypika/tests/test_immutability.py
+++ b/pypika/tests/test_immutability.py
@@ -13,6 +13,7 @@ class ImmutabilityTests(unittest.TestCase):
         query_a = Query.from_(self.table_a).select(self.table_a.foo)
         query_b = query_a.select(self.table_a.bar)
 
+        self.assertIsNot(query_a, query_b)
         self.assertNotEqual(str(query_a), str(query_b))
 
     def test_queries_after_join(self):
@@ -25,6 +26,11 @@ class ImmutabilityTests(unittest.TestCase):
 
         self.assertEqual('SELECT "foo" FROM "a"', str(query1))
         self.assertEqual(
-            'SELECT "a"."foo","b"."buz" FROM "a" ' 'JOIN "b" ON "a"."foo"="b"."bar"',
+            'SELECT "a"."foo","b"."buz" FROM "a" JOIN "b" ON "a"."foo"="b"."bar"',
             str(query2),
         )
+
+    def test_immutable_kwarg_on_query_builder_disables_immutability(self):
+        query0 = Query.from_(self.table_a, immutable=False)
+        query1 = query0.select(self.table_a.foo)
+        self.assertIs(query0, query1)