# Copyright DataStax, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from datetime import datetime, timedelta
import time
import six
from six.moves import filter

from cassandra.query import FETCH_SIZE_UNSET
from cassandra.cqlengine import columns
from cassandra.cqlengine import UnicodeMixin
from cassandra.cqlengine.functions import QueryValue
from cassandra.cqlengine.operators import BaseWhereOperator, InOperator, EqualsOperator, IsNotNullOperator


class StatementException(Exception):
    pass


class ValueQuoter(UnicodeMixin):

    def __init__(self, value):
        self.value = value

    def __unicode__(self):
        from cassandra.encoder import cql_quote
        if isinstance(self.value, (list, tuple)):
            return '[' + ', '.join([cql_quote(v) for v in self.value]) + ']'
        elif isinstance(self.value, dict):
            return '{' + ', '.join([cql_quote(k) + ':' + cql_quote(v) for k, v in self.value.items()]) + '}'
        elif isinstance(self.value, set):
            return '{' + ', '.join([cql_quote(v) for v in self.value]) + '}'
        return cql_quote(self.value)

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.value == other.value
        return False


class InQuoter(ValueQuoter):

    def __unicode__(self):
        from cassandra.encoder import cql_quote
        return '(' + ', '.join([cql_quote(v) for v in self.value]) + ')'


class BaseClause(UnicodeMixin):

    def __init__(self, field, value):
        self.field = field
        self.value = value
        self.context_id = None

    def __unicode__(self):
        raise NotImplementedError

    def __hash__(self):
        return hash(self.field) ^ hash(self.value)

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.field == other.field and self.value == other.value
        return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def get_context_size(self):
        """ returns the number of entries this clause will add to the query context """
        return 1

    def set_context_id(self, i):
        """ sets the value placeholder that will be used in the query """
        self.context_id = i

    def update_context(self, ctx):
        """ updates the query context with this clauses values """
        assert isinstance(ctx, dict)
        ctx[str(self.context_id)] = self.value


class WhereClause(BaseClause):
    """ a single where statement used in queries """

    def __init__(self, field, operator, value, quote_field=True):
        """

        :param field:
        :param operator:
        :param value:
        :param quote_field: hack to get the token function rendering properly
        :return:
        """
        if not isinstance(operator, BaseWhereOperator):
            raise StatementException(
                "operator must be of type {0}, got {1}".format(BaseWhereOperator, type(operator))
            )
        super(WhereClause, self).__init__(field, value)
        self.operator = operator
        self.query_value = self.value if isinstance(self.value, QueryValue) else QueryValue(self.value)
        self.quote_field = quote_field

    def __unicode__(self):
        field = ('"{0}"' if self.quote_field else '{0}').format(self.field)
        return u'{0} {1} {2}'.format(field, self.operator, six.text_type(self.query_value))

    def __hash__(self):
        return super(WhereClause, self).__hash__() ^ hash(self.operator)

    def __eq__(self, other):
        if super(WhereClause, self).__eq__(other):
            return self.operator.__class__ == other.operator.__class__
        return False

    def get_context_size(self):
        return self.query_value.get_context_size()

    def set_context_id(self, i):
        super(WhereClause, self).set_context_id(i)
        self.query_value.set_context_id(i)

    def update_context(self, ctx):
        if isinstance(self.operator, InOperator):
            ctx[str(self.context_id)] = InQuoter(self.value)
        else:
            self.query_value.update_context(ctx)


class IsNotNullClause(WhereClause):
    def __init__(self, field):
        super(IsNotNullClause, self).__init__(field, IsNotNullOperator(), '')

    def __unicode__(self):
        field = ('"{0}"' if self.quote_field else '{0}').format(self.field)
        return u'{0} {1}'.format(field, self.operator)

    def update_context(self, ctx):
        pass

    def get_context_size(self):
        return 0

# alias for convenience
IsNotNull = IsNotNullClause


class AssignmentClause(BaseClause):
    """ a single variable st statement """

    def __unicode__(self):
        return u'"{0}" = %({1})s'.format(self.field, self.context_id)

    def insert_tuple(self):
        return self.field, self.context_id


class ConditionalClause(BaseClause):
    """ A single variable iff statement """

    def __unicode__(self):
        return u'"{0}" = %({1})s'.format(self.field, self.context_id)

    def insert_tuple(self):
        return self.field, self.context_id


class ContainerUpdateTypeMapMeta(type):

    def __init__(cls, name, bases, dct):
        if not hasattr(cls, 'type_map'):
            cls.type_map = {}
        else:
            cls.type_map[cls.col_type] = cls
        super(ContainerUpdateTypeMapMeta, cls).__init__(name, bases, dct)


@six.add_metaclass(ContainerUpdateTypeMapMeta)
class ContainerUpdateClause(AssignmentClause):

    def __init__(self, field, value, operation=None, previous=None):
        super(ContainerUpdateClause, self).__init__(field, value)
        self.previous = previous
        self._assignments = None
        self._operation = operation
        self._analyzed = False

    def _analyze(self):
        raise NotImplementedError

    def get_context_size(self):
        raise NotImplementedError

    def update_context(self, ctx):
        raise NotImplementedError


class SetUpdateClause(ContainerUpdateClause):
    """ updates a set collection """

    col_type = columns.Set

    _additions = None
    _removals = None

    def __unicode__(self):
        qs = []
        ctx_id = self.context_id
        if (self.previous is None and
                self._assignments is None and
                self._additions is None and
                self._removals is None):
            qs += ['"{0}" = %({1})s'.format(self.field, ctx_id)]
        if self._assignments is not None:
            qs += ['"{0}" = %({1})s'.format(self.field, ctx_id)]
            ctx_id += 1
        if self._additions is not None:
            qs += ['"{0}" = "{0}" + %({1})s'.format(self.field, ctx_id)]
            ctx_id += 1
        if self._removals is not None:
            qs += ['"{0}" = "{0}" - %({1})s'.format(self.field, ctx_id)]

        return ', '.join(qs)

    def _analyze(self):
        """ works out the updates to be performed """
        if self.value is None or self.value == self.previous:
            pass
        elif self._operation == "add":
            self._additions = self.value
        elif self._operation == "remove":
            self._removals = self.value
        elif self.previous is None:
            self._assignments = self.value
        else:
            # partial update time
            self._additions = (self.value - self.previous) or None
            self._removals = (self.previous - self.value) or None
        self._analyzed = True

    def get_context_size(self):
        if not self._analyzed:
            self._analyze()
        if (self.previous is None and
                not self._assignments and
                self._additions is None and
                self._removals is None):
            return 1
        return int(bool(self._assignments)) + int(bool(self._additions)) + int(bool(self._removals))

    def update_context(self, ctx):
        if not self._analyzed:
            self._analyze()
        ctx_id = self.context_id
        if (self.previous is None and
                self._assignments is None and
                self._additions is None and
                self._removals is None):
            ctx[str(ctx_id)] = set()
        if self._assignments is not None:
            ctx[str(ctx_id)] = self._assignments
            ctx_id += 1
        if self._additions is not None:
            ctx[str(ctx_id)] = self._additions
            ctx_id += 1
        if self._removals is not None:
            ctx[str(ctx_id)] = self._removals


class ListUpdateClause(ContainerUpdateClause):
    """ updates a list collection """

    col_type = columns.List

    _append = None
    _prepend = None

    def __unicode__(self):
        if not self._analyzed:
            self._analyze()
        qs = []
        ctx_id = self.context_id
        if self._assignments is not None:
            qs += ['"{0}" = %({1})s'.format(self.field, ctx_id)]
            ctx_id += 1

        if self._prepend is not None:
            qs += ['"{0}" = %({1})s + "{0}"'.format(self.field, ctx_id)]
            ctx_id += 1

        if self._append is not None:
            qs += ['"{0}" = "{0}" + %({1})s'.format(self.field, ctx_id)]

        return ', '.join(qs)

    def get_context_size(self):
        if not self._analyzed:
            self._analyze()
        return int(self._assignments is not None) + int(bool(self._append)) + int(bool(self._prepend))

    def update_context(self, ctx):
        if not self._analyzed:
            self._analyze()
        ctx_id = self.context_id
        if self._assignments is not None:
            ctx[str(ctx_id)] = self._assignments
            ctx_id += 1
        if self._prepend is not None:
            ctx[str(ctx_id)] = self._prepend
            ctx_id += 1
        if self._append is not None:
            ctx[str(ctx_id)] = self._append

    def _analyze(self):
        """ works out the updates to be performed """
        if self.value is None or self.value == self.previous:
            pass

        elif self._operation == "append":
            self._append = self.value

        elif self._operation == "prepend":
            self._prepend = self.value

        elif self.previous is None:
            self._assignments = self.value

        elif len(self.value) < len(self.previous):
            # if elements have been removed,
            # rewrite the whole list
            self._assignments = self.value

        elif len(self.previous) == 0:
            # if we're updating from an empty
            # list, do a complete insert
            self._assignments = self.value
        else:

            # the max start idx we want to compare
            search_space = len(self.value) - max(0, len(self.previous) - 1)

            # the size of the sub lists we want to look at
            search_size = len(self.previous)

            for i in range(search_space):
                # slice boundary
                j = i + search_size
                sub = self.value[i:j]
                idx_cmp = lambda idx: self.previous[idx] == sub[idx]
                if idx_cmp(0) and idx_cmp(-1) and self.previous == sub:
                    self._prepend = self.value[:i] or None
                    self._append = self.value[j:] or None
                    break

            # if both append and prepend are still None after looking
            # at both lists, an insert statement will be created
            if self._prepend is self._append is None:
                self._assignments = self.value

        self._analyzed = True


class MapUpdateClause(ContainerUpdateClause):
    """ updates a map collection """

    col_type = columns.Map

    _updates = None
    _removals = None

    def _analyze(self):
        if self._operation == "update":
            self._updates = self.value.keys()
        elif self._operation == "remove":
            self._removals = {v for v in self.value.keys()}
        else:
            if self.previous is None:
                self._updates = sorted([k for k, v in self.value.items()])
            else:
                self._updates = sorted([k for k, v in self.value.items() if v != self.previous.get(k)]) or None
        self._analyzed = True

    def get_context_size(self):
        if self.is_assignment:
            return 1
        return int((len(self._updates or []) * 2) + int(bool(self._removals)))

    def update_context(self, ctx):
        ctx_id = self.context_id
        if self.is_assignment:
            ctx[str(ctx_id)] = {}
        elif self._removals is not None:
            ctx[str(ctx_id)] = self._removals
        else:
            for key in self._updates or []:
                val = self.value.get(key)
                ctx[str(ctx_id)] = key
                ctx[str(ctx_id + 1)] = val
                ctx_id += 2

    @property
    def is_assignment(self):
        if not self._analyzed:
            self._analyze()
        return self.previous is None and not self._updates and not self._removals

    def __unicode__(self):
        qs = []

        ctx_id = self.context_id
        if self.is_assignment:
            qs += ['"{0}" = %({1})s'.format(self.field, ctx_id)]
        elif self._removals is not None:
            qs += ['"{0}" = "{0}" - %({1})s'.format(self.field, ctx_id)]
            ctx_id += 1
        else:
            for _ in self._updates or []:
                qs += ['"{0}"[%({1})s] = %({2})s'.format(self.field, ctx_id, ctx_id + 1)]
                ctx_id += 2

        return ', '.join(qs)


class CounterUpdateClause(AssignmentClause):

    col_type = columns.Counter

    def __init__(self, field, value, previous=None):
        super(CounterUpdateClause, self).__init__(field, value)
        self.previous = previous or 0

    def get_context_size(self):
        return 1

    def update_context(self, ctx):
        ctx[str(self.context_id)] = abs(self.value - self.previous)

    def __unicode__(self):
        delta = self.value - self.previous
        sign = '-' if delta < 0 else '+'
        return '"{0}" = "{0}" {1} %({2})s'.format(self.field, sign, self.context_id)


class BaseDeleteClause(BaseClause):
    pass


class FieldDeleteClause(BaseDeleteClause):
    """ deletes a field from a row """

    def __init__(self, field):
        super(FieldDeleteClause, self).__init__(field, None)

    def __unicode__(self):
        return '"{0}"'.format(self.field)

    def update_context(self, ctx):
        pass

    def get_context_size(self):
        return 0


class MapDeleteClause(BaseDeleteClause):
    """ removes keys from a map """

    def __init__(self, field, value, previous=None):
        super(MapDeleteClause, self).__init__(field, value)
        self.value = self.value or {}
        self.previous = previous or {}
        self._analyzed = False
        self._removals = None

    def _analyze(self):
        self._removals = sorted([k for k in self.previous if k not in self.value])
        self._analyzed = True

    def update_context(self, ctx):
        if not self._analyzed:
            self._analyze()
        for idx, key in enumerate(self._removals):
            ctx[str(self.context_id + idx)] = key

    def get_context_size(self):
        if not self._analyzed:
            self._analyze()
        return len(self._removals)

    def __unicode__(self):
        if not self._analyzed:
            self._analyze()
        return ', '.join(['"{0}"[%({1})s]'.format(self.field, self.context_id + i) for i in range(len(self._removals))])


class BaseCQLStatement(UnicodeMixin):
    """ The base cql statement class """

    def __init__(self, table, timestamp=None, where=None, fetch_size=None, conditionals=None):
        super(BaseCQLStatement, self).__init__()
        self.table = table
        self.context_id = 0
        self.context_counter = self.context_id
        self.timestamp = timestamp
        self.fetch_size = fetch_size if fetch_size else FETCH_SIZE_UNSET

        self.where_clauses = []
        for clause in where or []:
            self._add_where_clause(clause)

        self.conditionals = []
        for conditional in conditionals or []:
            self.add_conditional_clause(conditional)

    def _update_part_key_values(self, field_index_map, clauses, parts):
        for clause in filter(lambda c: c.field in field_index_map, clauses):
            parts[field_index_map[clause.field]] = clause.value

    def partition_key_values(self, field_index_map):
        parts = [None] * len(field_index_map)
        self._update_part_key_values(field_index_map, (w for w in self.where_clauses if w.operator.__class__ == EqualsOperator), parts)
        return parts

    def add_where(self, column, operator, value, quote_field=True):
        value = column.to_database(value)
        clause = WhereClause(column.db_field_name, operator, value, quote_field)
        self._add_where_clause(clause)

    def _add_where_clause(self, clause):
        clause.set_context_id(self.context_counter)
        self.context_counter += clause.get_context_size()
        self.where_clauses.append(clause)

    def get_context(self):
        """
        returns the context dict for this statement
        :rtype: dict
        """
        ctx = {}
        for clause in self.where_clauses or []:
            clause.update_context(ctx)
        return ctx

    def add_conditional_clause(self, clause):
        """
        Adds a iff clause to this statement

        :param clause: The clause that will be added to the iff statement
        :type clause: ConditionalClause
        """
        clause.set_context_id(self.context_counter)
        self.context_counter += clause.get_context_size()
        self.conditionals.append(clause)

    def _get_conditionals(self):
        return 'IF {0}'.format(' AND '.join([six.text_type(c) for c in self.conditionals]))

    def get_context_size(self):
        return len(self.get_context())

    def update_context_id(self, i):
        self.context_id = i
        self.context_counter = self.context_id
        for clause in self.where_clauses:
            clause.set_context_id(self.context_counter)
            self.context_counter += clause.get_context_size()

    @property
    def timestamp_normalized(self):
        """
        we're expecting self.timestamp to be either a long, int, a datetime, or a timedelta
        :return:
        """
        if not self.timestamp:
            return None

        if isinstance(self.timestamp, six.integer_types):
            return self.timestamp

        if isinstance(self.timestamp, timedelta):
            tmp = datetime.now() + self.timestamp
        else:
            tmp = self.timestamp

        return int(time.mktime(tmp.timetuple()) * 1e+6 + tmp.microsecond)

    def __unicode__(self):
        raise NotImplementedError

    def __repr__(self):
        return self.__unicode__()

    @property
    def _where(self):
        return 'WHERE {0}'.format(' AND '.join([six.text_type(c) for c in self.where_clauses]))


class SelectStatement(BaseCQLStatement):
    """ a cql select statement """

    def __init__(self,
                 table,
                 fields=None,
                 count=False,
                 where=None,
                 order_by=None,
                 limit=None,
                 allow_filtering=False,
                 distinct_fields=None,
                 fetch_size=None):

        """
        :param where
        :type where list of cqlengine.statements.WhereClause
        """
        super(SelectStatement, self).__init__(
            table,
            where=where,
            fetch_size=fetch_size
        )

        self.fields = [fields] if isinstance(fields, six.string_types) else (fields or [])
        self.distinct_fields = distinct_fields
        self.count = count
        self.order_by = [order_by] if isinstance(order_by, six.string_types) else order_by
        self.limit = limit
        self.allow_filtering = allow_filtering

    def __unicode__(self):
        qs = ['SELECT']
        if self.distinct_fields:
            if self.count:
                qs += ['DISTINCT COUNT({0})'.format(', '.join(['"{0}"'.format(f) for f in self.distinct_fields]))]
            else:
                qs += ['DISTINCT {0}'.format(', '.join(['"{0}"'.format(f) for f in self.distinct_fields]))]
        elif self.count:
            qs += ['COUNT(*)']
        else:
            qs += [', '.join(['"{0}"'.format(f) for f in self.fields]) if self.fields else '*']
        qs += ['FROM', self.table]

        if self.where_clauses:
            qs += [self._where]

        if self.order_by and not self.count:
            qs += ['ORDER BY {0}'.format(', '.join(six.text_type(o) for o in self.order_by))]

        if self.limit:
            qs += ['LIMIT {0}'.format(self.limit)]

        if self.allow_filtering:
            qs += ['ALLOW FILTERING']

        return ' '.join(qs)


class AssignmentStatement(BaseCQLStatement):
    """ value assignment statements """

    def __init__(self,
                 table,
                 assignments=None,
                 where=None,
                 ttl=None,
                 timestamp=None,
                 conditionals=None):
        super(AssignmentStatement, self).__init__(
            table,
            where=where,
            conditionals=conditionals
        )
        self.ttl = ttl
        self.timestamp = timestamp

        # add assignments
        self.assignments = []
        for assignment in assignments or []:
            self._add_assignment_clause(assignment)

    def update_context_id(self, i):
        super(AssignmentStatement, self).update_context_id(i)
        for assignment in self.assignments:
            assignment.set_context_id(self.context_counter)
            self.context_counter += assignment.get_context_size()

    def partition_key_values(self, field_index_map):
        parts = super(AssignmentStatement, self).partition_key_values(field_index_map)
        self._update_part_key_values(field_index_map, self.assignments, parts)
        return parts

    def add_assignment(self, column, value):
        value = column.to_database(value)
        clause = AssignmentClause(column.db_field_name, value)
        self._add_assignment_clause(clause)

    def _add_assignment_clause(self, clause):
        clause.set_context_id(self.context_counter)
        self.context_counter += clause.get_context_size()
        self.assignments.append(clause)

    @property
    def is_empty(self):
        return len(self.assignments) == 0

    def get_context(self):
        ctx = super(AssignmentStatement, self).get_context()
        for clause in self.assignments:
            clause.update_context(ctx)
        return ctx


class InsertStatement(AssignmentStatement):
    """ an cql insert statement """

    def __init__(self,
                 table,
                 assignments=None,
                 where=None,
                 ttl=None,
                 timestamp=None,
                 if_not_exists=False):
        super(InsertStatement, self).__init__(table,
                                              assignments=assignments,
                                              where=where,
                                              ttl=ttl,
                                              timestamp=timestamp)

        self.if_not_exists = if_not_exists

    def __unicode__(self):
        qs = ['INSERT INTO {0}'.format(self.table)]

        # get column names and context placeholders
        fields = [a.insert_tuple() for a in self.assignments]
        columns, values = zip(*fields)

        qs += ["({0})".format(', '.join(['"{0}"'.format(c) for c in columns]))]
        qs += ['VALUES']
        qs += ["({0})".format(', '.join(['%({0})s'.format(v) for v in values]))]

        if self.if_not_exists:
            qs += ["IF NOT EXISTS"]

        using_options = []
        if self.ttl:
            using_options += ["TTL {}".format(self.ttl)]

        if self.timestamp:
            using_options += ["TIMESTAMP {}".format(self.timestamp_normalized)]

        if using_options:
            qs += ["USING {}".format(" AND ".join(using_options))]
        return ' '.join(qs)


class UpdateStatement(AssignmentStatement):
    """ an cql update select statement """

    def __init__(self,
                 table,
                 assignments=None,
                 where=None,
                 ttl=None,
                 timestamp=None,
                 conditionals=None,
                 if_exists=False):
        super(UpdateStatement, self). __init__(table,
                                               assignments=assignments,
                                               where=where,
                                               ttl=ttl,
                                               timestamp=timestamp,
                                               conditionals=conditionals)

        self.if_exists = if_exists

    def __unicode__(self):
        qs = ['UPDATE', self.table]

        using_options = []

        if self.ttl:
            using_options += ["TTL {0}".format(self.ttl)]

        if self.timestamp:
            using_options += ["TIMESTAMP {0}".format(self.timestamp_normalized)]

        if using_options:
            qs += ["USING {0}".format(" AND ".join(using_options))]

        qs += ['SET']
        qs += [', '.join([six.text_type(c) for c in self.assignments])]

        if self.where_clauses:
            qs += [self._where]

        if len(self.conditionals) > 0:
            qs += [self._get_conditionals()]

        if self.if_exists:
            qs += ["IF EXISTS"]

        return ' '.join(qs)

    def get_context(self):
        ctx = super(UpdateStatement, self).get_context()
        for clause in self.conditionals:
            clause.update_context(ctx)
        return ctx

    def update_context_id(self, i):
        super(UpdateStatement, self).update_context_id(i)
        for conditional in self.conditionals:
            conditional.set_context_id(self.context_counter)
            self.context_counter += conditional.get_context_size()

    def add_update(self, column, value, operation=None, previous=None):
        value = column.to_database(value)
        col_type = type(column)
        container_update_type = ContainerUpdateClause.type_map.get(col_type)
        if container_update_type:
            previous = column.to_database(previous)
            clause = container_update_type(column.db_field_name, value, operation, previous)
        elif col_type == columns.Counter:
            clause = CounterUpdateClause(column.db_field_name, value, previous)
        else:
            clause = AssignmentClause(column.db_field_name, value)
        if clause.get_context_size():  # this is to exclude map removals from updates. Can go away if we drop support for C* < 1.2.4 and remove two-phase updates
            self._add_assignment_clause(clause)


class DeleteStatement(BaseCQLStatement):
    """ a cql delete statement """

    def __init__(self, table, fields=None, where=None, timestamp=None, conditionals=None, if_exists=False):
        super(DeleteStatement, self).__init__(
            table,
            where=where,
            timestamp=timestamp,
            conditionals=conditionals
        )
        self.fields = []
        if isinstance(fields, six.string_types):
            fields = [fields]
        for field in fields or []:
            self.add_field(field)

        self.if_exists = if_exists

    def update_context_id(self, i):
        super(DeleteStatement, self).update_context_id(i)
        for field in self.fields:
            field.set_context_id(self.context_counter)
            self.context_counter += field.get_context_size()
        for t in self.conditionals:
            t.set_context_id(self.context_counter)
            self.context_counter += t.get_context_size()

    def get_context(self):
        ctx = super(DeleteStatement, self).get_context()
        for field in self.fields:
            field.update_context(ctx)
        for clause in self.conditionals:
            clause.update_context(ctx)
        return ctx

    def add_field(self, field):
        if isinstance(field, six.string_types):
            field = FieldDeleteClause(field)
        if not isinstance(field, BaseClause):
            raise StatementException("only instances of AssignmentClause can be added to statements")
        field.set_context_id(self.context_counter)
        self.context_counter += field.get_context_size()
        self.fields.append(field)

    def __unicode__(self):
        qs = ['DELETE']
        if self.fields:
            qs += [', '.join(['{0}'.format(f) for f in self.fields])]
        qs += ['FROM', self.table]

        delete_option = []

        if self.timestamp:
            delete_option += ["TIMESTAMP {0}".format(self.timestamp_normalized)]

        if delete_option:
            qs += [" USING {0} ".format(" AND ".join(delete_option))]

        if self.where_clauses:
            qs += [self._where]

        if self.conditionals:
            qs += [self._get_conditionals()]

        if self.if_exists:
            qs += ["IF EXISTS"]

        return ' '.join(qs)
