# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# 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.
import time
import hashlib

from oslo_log import log as logging
from sqlalchemy import select, distinct, func
from sqlalchemy.sql.expression import or_

from designate import exceptions
from designate import objects
from designate.sqlalchemy import base as sqlalchemy_base
from designate.storage import base as storage_base
from designate.storage.impl_sqlalchemy import tables


LOG = logging.getLogger(__name__)

MAXIMUM_SUBZONE_DEPTH = 128


class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
    """SQLAlchemy connection"""
    __plugin_name__ = 'sqlalchemy'

    def __init__(self):
        super(SQLAlchemyStorage, self).__init__()

    def get_name(self):
        return self.name

    # CRUD for our resources (quota, server, tsigkey, tenant, zone & record)
    # R - get_*, find_*s
    #
    # Standard Arguments
    # self      - python object for the class
    # context   - a dictionary of details about the request (http etc),
    #             provided by flask.
    # criterion - dictionary of filters to be applied
    #

    # Quota Methods
    def _find_quotas(self, context, criterion, one=False, marker=None,
                     limit=None, sort_key=None, sort_dir=None):
        return self._find(
            context, tables.quotas, objects.Quota, objects.QuotaList,
            exceptions.QuotaNotFound, criterion, one, marker, limit,
            sort_key, sort_dir)

    def create_quota(self, context, quota):
        if not isinstance(quota, objects.Quota):
            # TODO(kiall): Quotas should always use Objects
            quota = objects.Quota(**quota)

        return self._create(
            tables.quotas, quota, exceptions.DuplicateQuota)

    def get_quota(self, context, quota_id):
        return self._find_quotas(context, {'id': quota_id}, one=True)

    def find_quotas(self, context, criterion=None, marker=None, limit=None,
                    sort_key=None, sort_dir=None):
        return self._find_quotas(context, criterion, marker=marker,
                                 limit=limit, sort_key=sort_key,
                                 sort_dir=sort_dir)

    def find_quota(self, context, criterion):
        return self._find_quotas(context, criterion, one=True)

    def update_quota(self, context, quota):
        return self._update(
            context, tables.quotas, quota, exceptions.DuplicateQuota,
            exceptions.QuotaNotFound)

    def delete_quota(self, context, quota_id):
        # Fetch the existing quota, we'll need to return it.
        quota = self._find_quotas(context, {'id': quota_id}, one=True)
        return self._delete(context, tables.quotas, quota,
                            exceptions.QuotaNotFound)

    # TLD Methods
    def _find_tlds(self, context, criterion, one=False, marker=None,
                   limit=None, sort_key=None, sort_dir=None):
        return self._find(
            context, tables.tlds, objects.Tld, objects.TldList,
            exceptions.TldNotFound, criterion, one, marker, limit,
            sort_key, sort_dir)

    def create_tld(self, context, tld):
        return self._create(
            tables.tlds, tld, exceptions.DuplicateTld)

    def get_tld(self, context, tld_id):
        return self._find_tlds(context, {'id': tld_id}, one=True)

    def find_tlds(self, context, criterion=None, marker=None, limit=None,
                  sort_key=None, sort_dir=None):
        return self._find_tlds(context, criterion, marker=marker, limit=limit,
                               sort_key=sort_key, sort_dir=sort_dir)

    def find_tld(self, context, criterion):
        return self._find_tlds(context, criterion, one=True)

    def update_tld(self, context, tld):
        return self._update(
            context, tables.tlds, tld, exceptions.DuplicateTld,
            exceptions.TldNotFound)

    def delete_tld(self, context, tld_id):
        # Fetch the existing tld, we'll need to return it.
        tld = self._find_tlds(context, {'id': tld_id}, one=True)
        return self._delete(context, tables.tlds, tld, exceptions.TldNotFound)

    # TSIG Key Methods
    def _find_tsigkeys(self, context, criterion, one=False, marker=None,
                       limit=None, sort_key=None, sort_dir=None):
        return self._find(
            context, tables.tsigkeys, objects.TsigKey, objects.TsigKeyList,
            exceptions.TsigKeyNotFound, criterion, one, marker, limit,
            sort_key, sort_dir)

    def create_tsigkey(self, context, tsigkey):
        return self._create(
            tables.tsigkeys, tsigkey, exceptions.DuplicateTsigKey)

    def get_tsigkey(self, context, tsigkey_id):
        return self._find_tsigkeys(context, {'id': tsigkey_id}, one=True)

    def find_tsigkeys(self, context, criterion=None, marker=None, limit=None,
                      sort_key=None, sort_dir=None):
        return self._find_tsigkeys(context, criterion, marker=marker,
                                   limit=limit, sort_key=sort_key,
                                   sort_dir=sort_dir)

    def find_tsigkey(self, context, criterion):
        return self._find_tsigkeys(context, criterion, one=True)

    def update_tsigkey(self, context, tsigkey):
        return self._update(
            context, tables.tsigkeys, tsigkey, exceptions.DuplicateTsigKey,
            exceptions.TsigKeyNotFound)

    def delete_tsigkey(self, context, tsigkey_id):
        # Fetch the existing tsigkey, we'll need to return it.
        tsigkey = self._find_tsigkeys(context, {'id': tsigkey_id}, one=True)
        return self._delete(context, tables.tsigkeys, tsigkey,
                            exceptions.TsigKeyNotFound)

    ##
    # Tenant Methods
    ##
    def find_tenants(self, context):
        # returns an array of tenant_id & count of their zones
        query = select([tables.zones.c.tenant_id,
                        func.count(tables.zones.c.id)])
        query = self._apply_tenant_criteria(context, tables.zones, query)
        query = self._apply_deleted_criteria(context, tables.zones, query)
        query = query.group_by(tables.zones.c.tenant_id)

        resultproxy = self.session.execute(query)
        results = resultproxy.fetchall()

        tenant_list = objects.TenantList(
            objects=[objects.Tenant(id=t[0], zone_count=t[1]) for t in
                     results])

        tenant_list.obj_reset_changes()

        return tenant_list

    def get_tenant(self, context, tenant_id):
        # get list list & count of all zones owned by given tenant_id
        query = select([tables.zones.c.name])
        query = self._apply_tenant_criteria(context, tables.zones, query)
        query = self._apply_deleted_criteria(context, tables.zones, query)
        query = query.where(tables.zones.c.tenant_id == tenant_id)

        resultproxy = self.session.execute(query)
        results = resultproxy.fetchall()

        return objects.Tenant(
            id=tenant_id,
            zone_count=len(results),
            zones=[r[0] for r in results])

    def count_tenants(self, context):
        # tenants are the owner of zones, count the number of unique tenants
        # select count(distinct tenant_id) from zones
        query = select([func.count(distinct(tables.zones.c.tenant_id))])
        query = self._apply_tenant_criteria(context, tables.zones, query)
        query = self._apply_deleted_criteria(context, tables.zones, query)

        resultproxy = self.session.execute(query)
        result = resultproxy.fetchone()

        if result is None:
            return 0

        return result[0]

    ##
    # Zone Methods
    ##
    def _find_zones(self, context, criterion, one=False, marker=None,
                    limit=None, sort_key=None, sort_dir=None):
        # Check to see if the criterion can use the reverse_name column
        criterion = self._rname_check(criterion)

        zones = self._find(
            context, tables.zones, objects.Zone, objects.ZoneList,
            exceptions.ZoneNotFound, criterion, one, marker, limit,
            sort_key, sort_dir)

        def _load_relations(zone):
            if zone.type == 'SECONDARY':
                zone.masters = self._find_zone_masters(
                    context, {'zone_id': zone.id})
            else:
                # This avoids an extra DB call per primary zone. This will
                # always have 0 results for a PRIMARY zone.
                zone.masters = objects.ZoneMasterList()

            zone.attributes = self._find_zone_attributes(
                context, {'zone_id': zone.id, "key": "!master"})

            zone.obj_reset_changes(['masters', 'attributes'])

        # TODO(Federico) refactor part of _find_zones into _find_zone, move
        # _load_relations out

        if one:
            _load_relations(zones)
        else:
            zones.total_count = self.count_zones(context, criterion)
            for d in zones:
                _load_relations(d)

        if one:
            LOG.debug("Fetched zone %s", zones)
        return zones

    def create_zone(self, context, zone):
        # Patch in the reverse_name column
        extra_values = {"reverse_name": zone.name[::-1]}

        # Don't handle recordsets for now
        zone = self._create(
            tables.zones, zone, exceptions.DuplicateZone,
            ['attributes', 'recordsets', 'masters'],
            extra_values=extra_values)

        if zone.obj_attr_is_set('attributes'):
            for attrib in zone.attributes:
                self.create_zone_attribute(context, zone.id, attrib)
        else:
            zone.attributes = objects.ZoneAttributeList()
        if zone.obj_attr_is_set('masters'):
            for master in zone.masters:
                self.create_zone_master(context, zone.id, master)
        else:
            zone.masters = objects.ZoneMasterList()
        zone.obj_reset_changes(['masters', 'attributes'])

        return zone

    def get_zone(self, context, zone_id):
        zone = self._find_zones(context, {'id': zone_id}, one=True)
        return zone

    def find_zones(self, context, criterion=None, marker=None, limit=None,
                   sort_key=None, sort_dir=None):
        zones = self._find_zones(context, criterion, marker=marker,
                                 limit=limit, sort_key=sort_key,
                                 sort_dir=sort_dir)
        return zones

    def find_zone(self, context, criterion):
        zone = self._find_zones(context, criterion, one=True)
        return zone

    def update_zone(self, context, zone):
        tenant_id_changed = False
        if 'tenant_id' in zone.obj_what_changed():
            tenant_id_changed = True

        # Don't handle recordsets for now
        LOG.debug("Updating zone %s", zone)
        updated_zone = self._update(
            context, tables.zones, zone, exceptions.DuplicateZone,
            exceptions.ZoneNotFound,
            ['attributes', 'recordsets', 'masters'])

        if zone.obj_attr_is_set('attributes'):
            # Gather the Attribute ID's we have
            have = set([r.id for r in self._find_zone_attributes(
                context, {'zone_id': zone.id})])

            # Prep some lists of changes
            keep = set([])
            create = []
            update = []

            # Determine what to change
            for i in zone.attributes:
                keep.add(i.id)
                try:
                    i.obj_get_original_value('id')
                except KeyError:
                    create.append(i)
                else:
                    update.append(i)

            # NOTE: Since we're dealing with mutable objects, the return value
            #       of create/update/delete attribute is not needed.
            #       The original item will be mutated in place on the input
            #       "zone.attributes" list.

            # Delete Attributes
            for i_id in have - keep:
                attr = self._find_zone_attributes(
                    context, {'id': i_id}, one=True)
                self.delete_zone_attribute(context, attr.id)

            # Update Attributes
            for i in update:
                self.update_zone_attribute(context, i)

            # Create Attributes
            for attr in create:
                attr.zone_id = zone.id
                self.create_zone_attribute(context, zone.id, attr)

        if zone.obj_attr_is_set('masters'):
            # Gather the Attribute ID's we have
            have = set([r.id for r in self._find_zone_masters(
                context, {'zone_id': zone.id})])

            # Prep some lists of changes
            keep = set([])
            create = []
            update = []

            # Determine what to change
            for i in zone.masters:
                keep.add(i.id)
                try:
                    i.obj_get_original_value('id')
                except KeyError:
                    create.append(i)
                else:
                    update.append(i)

            # NOTE: Since we're dealing with mutable objects, the return value
            #       of create/update/delete attribute is not needed.
            #       The original item will be mutated in place on the input
            #       "zone.attributes" list.

            # Delete Attributes
            for i_id in have - keep:
                attr = self._find_zone_masters(
                    context, {'id': i_id}, one=True)
                self.delete_zone_master(context, attr.id)

            # Update Attributes
            for i in update:
                self.update_zone_master(context, i)

            # Create Attributes
            for attr in create:
                attr.zone_id = zone.id
                self.create_zone_master(context, zone.id, attr)

        if zone.obj_attr_is_set('recordsets'):
            existing = self.find_recordsets(context, {'zone_id': zone.id})

            data = {}
            for rrset in existing:
                data[rrset.name, rrset.type] = rrset

            keep = set()
            for rrset in zone.recordsets:
                current = data.get((rrset.name, rrset.type))

                if current:
                    current.update(rrset)
                    current.records = rrset.records
                    self.update_recordset(context, current)
                    keep.add(current.id)
                else:
                    self.create_recordset(context, zone.id, rrset)
                    keep.add(rrset.id)

            if zone.type == 'SECONDARY':
                # Purge anything that shouldn't be there :P
                for i in set([i.id for i in data.values()]) - keep:
                    self.delete_recordset(context, i)

        if tenant_id_changed:
            recordsets_query = tables.recordsets.update().\
                where(tables.recordsets.c.zone_id == zone.id)\
                .values({'tenant_id': zone.tenant_id})

            records_query = tables.records.update().\
                where(tables.records.c.zone_id == zone.id).\
                values({'tenant_id': zone.tenant_id})

            self.session.execute(records_query)
            self.session.execute(recordsets_query)

        return updated_zone

    def delete_zone(self, context, zone_id):
        """
        """
        # Fetch the existing zone, we'll need to return it.
        zone = self._find_zones(context, {'id': zone_id}, one=True)
        return self._delete(context, tables.zones, zone,
                            exceptions.ZoneNotFound)

    def purge_zone(self, context, zone):
        """Effectively remove a zone database record.
        """
        return self._delete(context, tables.zones, zone,
                            exceptions.ZoneNotFound, hard_delete=True)

    def _walk_up_zones(self, current, zones_by_id):
        """Walk upwards in a zone hierarchy until we find a parent zone
        that does not belong to "zones_by_id"
        :returns: parent zone ID or None
        """
        max_steps = MAXIMUM_SUBZONE_DEPTH
        while current.parent_zone_id in zones_by_id:
            current = zones_by_id[current.parent_zone_id]
            max_steps -= 1
            if max_steps == 0:
                raise exceptions.IllegalParentZone("Loop detected in the"
                                                   " zone hierarchy")

        return current.parent_zone_id

    def purge_zones(self, context, criterion, limit):
        """Purge deleted zones.
        Reparent orphan childrens, if any.
        Transactions/locks are not needed.
        :returns: number of purged zones
        """
        if 'deleted' in criterion:
            context.show_deleted = True

        zones = self.find_zones(
            context=context,
            criterion=criterion,
            limit=limit,
        )
        if not zones:
            LOG.info("No zones to be purged")
            return

        LOG.debug("Purging %d zones", len(zones))

        zones_by_id = {z.id: z for z in zones}

        for zone in zones:

            # Reparent child zones, if any.
            surviving_parent_id = self._walk_up_zones(zone, zones_by_id)
            query = tables.zones.update().\
                where(tables.zones.c.parent_zone_id == zone.id).\
                values(parent_zone_id=surviving_parent_id)

            resultproxy = self.session.execute(query)
            LOG.debug("%d child zones updated", resultproxy.rowcount)

            self.purge_zone(context, zone)

        LOG.info("Purged %d zones", len(zones))
        return len(zones)

    def count_zones(self, context, criterion=None):
        query = select([func.count(tables.zones.c.id)])
        query = self._apply_criterion(tables.zones, query, criterion)
        query = self._apply_tenant_criteria(context, tables.zones, query)
        query = self._apply_deleted_criteria(context, tables.zones, query)

        resultproxy = self.session.execute(query)
        result = resultproxy.fetchone()

        if result is None:
            return 0

        return result[0]

    # Zone attribute methods
    def _find_zone_attributes(self, context, criterion, one=False,
                              marker=None, limit=None, sort_key=None,
                              sort_dir=None):
        return self._find(context, tables.zone_attributes,
                          objects.ZoneAttribute, objects.ZoneAttributeList,
                          exceptions.ZoneAttributeNotFound, criterion, one,
                          marker, limit, sort_key, sort_dir)

    def create_zone_attribute(self, context, zone_id, zone_attribute):
        zone_attribute.zone_id = zone_id
        return self._create(tables.zone_attributes, zone_attribute,
                            exceptions.DuplicateZoneAttribute)

    def get_zone_attributes(self, context, zone_attribute_id):
        return self._find_zone_attributes(
            context, {'id': zone_attribute_id}, one=True)

    def find_zone_attributes(self, context, criterion=None, marker=None,
                             limit=None, sort_key=None, sort_dir=None):
        return self._find_zone_attributes(context, criterion, marker=marker,
                                          limit=limit, sort_key=sort_key,
                                          sort_dir=sort_dir)

    def find_zone_attribute(self, context, criterion):
        return self._find_zone_attributes(context, criterion, one=True)

    def update_zone_attribute(self, context, zone_attribute):
        return self._update(context, tables.zone_attributes,
                            zone_attribute,
                            exceptions.DuplicateZoneAttribute,
                            exceptions.ZoneAttributeNotFound)

    def delete_zone_attribute(self, context, zone_attribute_id):
        zone_attribute = self._find_zone_attributes(
            context, {'id': zone_attribute_id}, one=True)
        deleted_zone_attribute = self._delete(
            context, tables.zone_attributes, zone_attribute,
            exceptions.ZoneAttributeNotFound)

        return deleted_zone_attribute

    # Zone master methods
    def _find_zone_masters(self, context, criterion, one=False,
                           marker=None, limit=None, sort_key=None,
                           sort_dir=None):

        return self._find(context, tables.zone_masters,
                          objects.ZoneMaster, objects.ZoneMasterList,
                          exceptions.ZoneMasterNotFound, criterion, one,
                          marker, limit, sort_key, sort_dir)

    def create_zone_master(self, context, zone_id, zone_master):

        zone_master.zone_id = zone_id
        return self._create(tables.zone_masters, zone_master,
                            exceptions.DuplicateZoneMaster)

    def get_zone_masters(self, context, zone_attribute_id):
        return self._find_zone_masters(
            context, {'id': zone_attribute_id}, one=True)

    def find_zone_masters(self, context, criterion=None, marker=None,
                          limit=None, sort_key=None, sort_dir=None):
        return self._find_zone_masters(context, criterion, marker=marker,
                                       limit=limit, sort_key=sort_key,
                                       sort_dir=sort_dir)

    def find_zone_master(self, context, criterion):
        return self._find_zone_master(context, criterion, one=True)

    def update_zone_master(self, context, zone_master):

        return self._update(context, tables.zone_masters,
                            zone_master,
                            exceptions.DuplicateZoneMaster,
                            exceptions.ZoneMasterNotFound)

    def delete_zone_master(self, context, zone_master_id):
        zone_master = self._find_zone_masters(
            context, {'id': zone_master_id}, one=True)
        deleted_zone_master = self._delete(
            context, tables.zone_masters, zone_master,
            exceptions.ZoneMasterNotFound)

        return deleted_zone_master

    # RecordSet Methods
    def _find_recordsets(self, context, criterion, one=False, marker=None,
                         limit=None, sort_key=None, sort_dir=None,
                         force_index=False):

        # Check to see if the criterion can use the reverse_name column
        criterion = self._rname_check(criterion)

        if criterion is not None \
                and not criterion.get('zones_deleted', True):
            # remove 'zones_deleted' from the criterion, as _apply_criterion
            # assumes each key in criterion to be a column name.
            del criterion['zones_deleted']

        if one:
            rjoin = tables.recordsets.join(
                tables.zones,
                tables.recordsets.c.zone_id == tables.zones.c.id)
            query = select([tables.recordsets]).select_from(rjoin).\
                where(tables.zones.c.deleted == '0')

            recordsets = self._find(
                context, tables.recordsets, objects.RecordSet,
                objects.RecordSetList, exceptions.RecordSetNotFound, criterion,
                one, marker, limit, sort_key, sort_dir, query)

            recordsets.records = self._find_records(
                context, {'recordset_id': recordsets.id})

            recordsets.obj_reset_changes(['records'])

        else:
            tc, recordsets = self._find_recordsets_with_records(
                    context, criterion, tables.zones, tables.recordsets,
                    tables.records, limit=limit, marker=marker,
                    sort_key=sort_key, sort_dir=sort_dir,
                    force_index=force_index)

            recordsets.total_count = tc

        return recordsets

    def find_recordsets_axfr(self, context, criterion=None):
        query = None

        # Check to see if the criterion can use the reverse_name column
        criterion = self._rname_check(criterion)

        rjoin = tables.records.join(
            tables.recordsets,
            tables.records.c.recordset_id == tables.recordsets.c.id)

        query = select([tables.recordsets.c.id, tables.recordsets.c.type,
                        tables.recordsets.c.ttl, tables.recordsets.c.name,
                        tables.records.c.data, tables.records.c.action]).\
            select_from(rjoin).where(tables.records.c.action != 'DELETE')

        query = query.order_by(tables.recordsets.c.id)

        raw_rows = self._select_raw(
            context, tables.recordsets, criterion, query)

        return raw_rows

    def create_recordset(self, context, zone_id, recordset):
        # Fetch the zone as we need the tenant_id
        zone = self._find_zones(context, {'id': zone_id}, one=True)

        recordset.tenant_id = zone.tenant_id
        recordset.zone_id = zone_id

        # Patch in the reverse_name column
        extra_values = {"reverse_name": recordset.name[::-1]}

        recordset = self._create(
            tables.recordsets, recordset, exceptions.DuplicateRecordSet,
            ['records'], extra_values=extra_values)

        if recordset.obj_attr_is_set('records'):
            for record in recordset.records:
                # NOTE: Since we're dealing with a mutable object, the return
                #       value is not needed. The original item will be mutated
                #       in place on the input "recordset.records" list.
                self.create_record(context, zone_id, recordset.id, record)
        else:
            recordset.records = objects.RecordList()

        recordset.obj_reset_changes(['records'])

        return recordset

    def find_recordsets_export(self, context, criterion=None):
        query = None

        rjoin = tables.records.join(
            tables.recordsets,
            tables.records.c.recordset_id == tables.recordsets.c.id)

        query = select([tables.recordsets.c.name, tables.recordsets.c.ttl,
                        tables.recordsets.c.type, tables.records.c.data]).\
            select_from(rjoin)

        query = query.order_by(tables.recordsets.c.created_at)

        raw_rows = self._select_raw(
            context, tables.recordsets, criterion, query)

        return raw_rows

    def get_recordset(self, context, recordset_id):
        return self._find_recordsets(context, {'id': recordset_id}, one=True)

    def find_recordsets(self, context, criterion=None, marker=None, limit=None,
                        sort_key=None, sort_dir=None, force_index=False):
        return self._find_recordsets(context, criterion, marker=marker,
                                     sort_dir=sort_dir, sort_key=sort_key,
                                     limit=limit, force_index=force_index)

    def find_recordset(self, context, criterion):
        return self._find_recordsets(context, criterion, one=True)

    def update_recordset(self, context, recordset):
        recordset = self._update(
            context, tables.recordsets, recordset,
            exceptions.DuplicateRecordSet, exceptions.RecordSetNotFound,
            ['records'])

        if recordset.obj_attr_is_set('records'):
            # Gather the Record ID's we have
            have_records = set([r.id for r in self._find_records(
                context, {'recordset_id': recordset.id})])

            # Prep some lists of changes
            keep_records = set([])
            create_records = []
            update_records = []

            # Determine what to change
            for record in recordset.records:
                keep_records.add(record.id)
                try:
                    record.obj_get_original_value('id')
                except KeyError:
                    create_records.append(record)
                else:
                    update_records.append(record)

            # NOTE: Since we're dealing with mutable objects, the return value
            #       of create/update/delete record is not needed. The original
            #       item will be mutated in place on the input
            #       "recordset.records" list.

            # Delete Records
            for record_id in have_records - keep_records:
                self.delete_record(context, record_id)

            # Update Records
            for record in update_records:
                self.update_record(context, record)

            # Create Records
            for record in create_records:
                self.create_record(
                    context, recordset.zone_id, recordset.id, record)

        return recordset

    def delete_recordset(self, context, recordset_id):
        # Fetch the existing recordset, we'll need to return it.
        recordset = self._find_recordsets(
            context, {'id': recordset_id}, one=True)

        return self._delete(context, tables.recordsets, recordset,
                            exceptions.RecordSetNotFound)

    def count_recordsets(self, context, criterion=None):
        # Ensure that we return only active recordsets
        rjoin = tables.recordsets.join(
            tables.zones,
            tables.recordsets.c.zone_id == tables.zones.c.id)

        query = select([func.count(tables.recordsets.c.id)]).\
            select_from(rjoin).\
            where(tables.zones.c.deleted == '0')

        query = self._apply_criterion(tables.recordsets, query, criterion)
        query = self._apply_tenant_criteria(context, tables.recordsets, query)
        query = self._apply_deleted_criteria(context, tables.recordsets, query)

        resultproxy = self.session.execute(query)
        result = resultproxy.fetchone()

        if result is None:
            return 0

        return result[0]

    # Record Methods
    def _find_records(self, context, criterion, one=False, marker=None,
                      limit=None, sort_key=None, sort_dir=None):
        return self._find(
            context, tables.records, objects.Record, objects.RecordList,
            exceptions.RecordNotFound, criterion, one, marker, limit,
            sort_key, sort_dir)

    def _recalculate_record_hash(self, record):
        """
        Calculates the hash of the record, used to ensure record uniqueness.
        """
        md5 = hashlib.md5()
        md5.update(("%s:%s" % (record.recordset_id,
                               record.data)).encode('utf-8'))

        return md5.hexdigest()

    def create_record(self, context, zone_id, recordset_id, record):
        # Fetch the zone as we need the tenant_id
        zone = self._find_zones(context, {'id': zone_id}, one=True)

        record.tenant_id = zone.tenant_id
        record.zone_id = zone_id
        record.recordset_id = recordset_id
        record.hash = self._recalculate_record_hash(record)

        return self._create(
            tables.records, record, exceptions.DuplicateRecord)

    def get_record(self, context, record_id):
        return self._find_records(context, {'id': record_id}, one=True)

    def find_records(self, context, criterion=None, marker=None, limit=None,
                     sort_key=None, sort_dir=None):
        return self._find_records(context, criterion, marker=marker,
                                  limit=limit, sort_key=sort_key,
                                  sort_dir=sort_dir)

    def find_record(self, context, criterion):
        return self._find_records(context, criterion, one=True)

    def update_record(self, context, record):
        if record.obj_what_changed():
            record.hash = self._recalculate_record_hash(record)

        return self._update(
            context, tables.records, record, exceptions.DuplicateRecord,
            exceptions.RecordNotFound)

    def delete_record(self, context, record_id):
        # Fetch the existing record, we'll need to return it.
        record = self._find_records(context, {'id': record_id}, one=True)
        return self._delete(context, tables.records, record,
                            exceptions.RecordNotFound)

    def count_records(self, context, criterion=None):
        # Ensure that we return only active records
        rjoin = tables.records.join(
            tables.zones,
            tables.records.c.zone_id == tables.zones.c.id)

        query = select([func.count(tables.records.c.id)]).\
            select_from(rjoin).\
            where(tables.zones.c.deleted == '0')

        query = self._apply_criterion(tables.records, query, criterion)
        query = self._apply_tenant_criteria(context, tables.records, query)
        query = self._apply_deleted_criteria(context, tables.records, query)

        resultproxy = self.session.execute(query)
        result = resultproxy.fetchone()

        if result is None:
            return 0

        return result[0]

    # Blacklist Methods
    def _find_blacklists(self, context, criterion, one=False, marker=None,
                         limit=None, sort_key=None, sort_dir=None):
        return self._find(
            context, tables.blacklists, objects.Blacklist,
            objects.BlacklistList, exceptions.BlacklistNotFound, criterion,
            one, marker, limit, sort_key, sort_dir)

    def create_blacklist(self, context, blacklist):
        return self._create(
            tables.blacklists, blacklist, exceptions.DuplicateBlacklist)

    def get_blacklist(self, context, blacklist_id):
        return self._find_blacklists(context, {'id': blacklist_id}, one=True)

    def find_blacklists(self, context, criterion=None, marker=None, limit=None,
                        sort_key=None, sort_dir=None):
        return self._find_blacklists(context, criterion, marker=marker,
                                     limit=limit, sort_key=sort_key,
                                     sort_dir=sort_dir)

    def find_blacklist(self, context, criterion):
        return self._find_blacklists(context, criterion, one=True)

    def update_blacklist(self, context, blacklist):
        return self._update(
            context, tables.blacklists, blacklist,
            exceptions.DuplicateBlacklist, exceptions.BlacklistNotFound)

    def delete_blacklist(self, context, blacklist_id):
        # Fetch the existing blacklist, we'll need to return it.
        blacklist = self._find_blacklists(
            context, {'id': blacklist_id}, one=True)

        return self._delete(context, tables.blacklists, blacklist,
                            exceptions.BlacklistNotFound)

    # Pool methods
    def _find_pools(self, context, criterion, one=False, marker=None,
                    limit=None, sort_key=None, sort_dir=None):
        pools = self._find(context, tables.pools, objects.Pool,
                           objects.PoolList, exceptions.PoolNotFound,
                           criterion, one, marker, limit, sort_key,
                           sort_dir)

        # Load Relations
        def _load_relations(pool):
            pool.attributes = self._find_pool_attributes(
                context, {'pool_id': pool.id})

            pool.ns_records = self._find_pool_ns_records(
                context, {'pool_id': pool.id})

            pool.nameservers = self._find_pool_nameservers(
                context, {'pool_id': pool.id})

            pool.targets = self._find_pool_targets(
                context, {'pool_id': pool.id})

            pool.also_notifies = self._find_pool_also_notifies(
                context, {'pool_id': pool.id})

            pool.obj_reset_changes(['attributes', 'ns_records', 'nameservers',
                                    'targets', 'also_notifies'])

        if one:
            _load_relations(pools)
        else:
            for pool in pools:
                _load_relations(pool)

        return pools

    def create_pool(self, context, pool):
        pool = self._create(
            tables.pools, pool, exceptions.DuplicatePool,
            ['attributes', 'ns_records', 'nameservers', 'targets',
             'also_notifies'])

        if pool.obj_attr_is_set('attributes'):
            for pool_attribute in pool.attributes:
                self.create_pool_attribute(context, pool.id, pool_attribute)
        else:
            pool.attributes = objects.PoolAttributeList()

        if pool.obj_attr_is_set('ns_records'):
            for ns_record in pool.ns_records:
                self.create_pool_ns_record(context, pool.id, ns_record)
        else:
            pool.ns_records = objects.PoolNsRecordList()

        if pool.obj_attr_is_set('nameservers'):
            for nameserver in pool.nameservers:
                self.create_pool_nameserver(context, pool.id, nameserver)
        else:
            pool.nameservers = objects.PoolNameserverList()

        if pool.obj_attr_is_set('targets'):
            for target in pool.targets:
                self.create_pool_target(context, pool.id, target)
        else:
            pool.targets = objects.PoolTargetList()

        if pool.obj_attr_is_set('also_notifies'):
            for also_notify in pool.also_notifies:
                self.create_pool_also_notify(context, pool.id, also_notify)
        else:
            pool.also_notifies = objects.PoolAlsoNotifyList()

        pool.obj_reset_changes(['attributes', 'ns_records', 'nameservers',
                                'targets', 'also_notifies'])

        return pool

    def get_pool(self, context, pool_id):
        return self._find_pools(context, {'id': pool_id}, one=True)

    def find_pools(self, context, criterion=None, marker=None,
                   limit=None, sort_key=None, sort_dir=None):
        return self._find_pools(context, criterion, marker=marker,
                                limit=limit, sort_key=sort_key,
                                sort_dir=sort_dir)

    def find_pool(self, context, criterion):
        return self._find_pools(context, criterion, one=True)

    def update_pool(self, context, pool):
        pool = self._update(context, tables.pools, pool,
                            exceptions.DuplicatePool, exceptions.PoolNotFound,
                            ['attributes', 'ns_records', 'nameservers',
                             'targets', 'also_notifies'])

        for attribute_name in ('attributes', 'ns_records', 'nameservers',
                               'targets', 'also_notifies'):
            if pool.obj_attr_is_set(attribute_name):
                self._update_pool_items(context, pool, attribute_name)

        # Call get_pool to get the ids of all the attributes/ns_records
        # refreshed in the pool object
        updated_pool = self.get_pool(context, pool.id)

        return updated_pool

    def delete_pool(self, context, pool_id):
        pool = self._find_pools(context, {'id': pool_id}, one=True)

        return self._delete(context, tables.pools, pool,
                            exceptions.PoolNotFound)

    # Pool attribute methods
    def _find_pool_attributes(self, context, criterion, one=False, marker=None,
                              limit=None, sort_key=None, sort_dir=None):
        return self._find(context, tables.pool_attributes,
                          objects.PoolAttribute, objects.PoolAttributeList,
                          exceptions.PoolAttributeNotFound, criterion, one,
                          marker, limit, sort_key, sort_dir)

    def create_pool_attribute(self, context, pool_id, pool_attribute):
        pool_attribute.pool_id = pool_id

        return self._create(tables.pool_attributes, pool_attribute,
                            exceptions.DuplicatePoolAttribute)

    def get_pool_attribute(self, context, pool_attribute_id):
        return self._find_pool_attributes(
            context, {'id': pool_attribute_id}, one=True)

    def find_pool_attributes(self, context, criterion=None, marker=None,
                             limit=None, sort_key=None, sort_dir=None):
        return self._find_pool_attributes(context, criterion, marker=marker,
                                          limit=limit, sort_key=sort_key,
                                          sort_dir=sort_dir)

    def find_pool_attribute(self, context, criterion):
        return self._find_pool_attributes(context, criterion, one=True)

    def update_pool_attribute(self, context, pool_attribute):
        return self._update(context, tables.pool_attributes, pool_attribute,
                            exceptions.DuplicatePoolAttribute,
                            exceptions.PoolAttributeNotFound)

    def delete_pool_attribute(self, context, pool_attribute_id):
        pool_attribute = self._find_pool_attributes(
            context, {'id': pool_attribute_id}, one=True)
        deleted_pool_attribute = self._delete(
            context, tables.pool_attributes, pool_attribute,
            exceptions.PoolAttributeNotFound)

        return deleted_pool_attribute

    # Pool ns_record methods
    def _find_pool_ns_records(self, context, criterion, one=False, marker=None,
                              limit=None, sort_key=None, sort_dir=None):
        return self._find(context, tables.pool_ns_records,
                          objects.PoolNsRecord, objects.PoolNsRecordList,
                          exceptions.PoolNsRecordNotFound, criterion, one,
                          marker, limit, sort_key, sort_dir)

    def create_pool_ns_record(self, context, pool_id, pool_ns_record):
        pool_ns_record.pool_id = pool_id

        return self._create(tables.pool_ns_records, pool_ns_record,
                            exceptions.DuplicatePoolNsRecord)

    def get_pool_ns_record(self, context, pool_ns_record_id):
        return self._find_pool_ns_records(
            context, {'id': pool_ns_record_id}, one=True)

    def find_pool_ns_records(self, context, criterion=None, marker=None,
                             limit=None, sort_key=None, sort_dir=None):
        return self._find_pool_ns_records(context, criterion, marker=marker,
                                          limit=limit, sort_key=sort_key,
                                          sort_dir=sort_dir)

    def find_pool_ns_record(self, context, criterion):
        return self._find_pool_ns_records(context, criterion, one=True)

    def update_pool_ns_record(self, context, pool_ns_record):
        return self._update(context, tables.pool_ns_records, pool_ns_record,
                            exceptions.DuplicatePoolNsRecord,
                            exceptions.PoolNsRecordNotFound)

    def delete_pool_ns_record(self, context, pool_ns_record_id):
        pool_ns_record = self._find_pool_ns_records(
            context, {'id': pool_ns_record_id}, one=True)
        deleted_pool_ns_record = self._delete(
            context, tables.pool_ns_records, pool_ns_record,
            exceptions.PoolNsRecordNotFound)

        return deleted_pool_ns_record

    # PoolNameserver methods
    def _find_pool_nameservers(self, context, criterion, one=False,
                               marker=None, limit=None, sort_key=None,
                               sort_dir=None):
        return self._find(context, tables.pool_nameservers,
                          objects.PoolNameserver, objects.PoolNameserverList,
                          exceptions.PoolNameserverNotFound, criterion, one,
                          marker, limit, sort_key, sort_dir)

    def create_pool_nameserver(self, context, pool_id, pool_nameserver):
        pool_nameserver.pool_id = pool_id

        return self._create(tables.pool_nameservers, pool_nameserver,
                            exceptions.DuplicatePoolNameserver)

    def get_pool_nameserver(self, context, pool_nameserver_id):
        return self._find_pool_nameservers(
            context, {'id': pool_nameserver_id}, one=True)

    def find_pool_nameservers(self, context, criterion=None, marker=None,
                   limit=None, sort_key=None, sort_dir=None):
        return self._find_pool_nameservers(context, criterion, marker=marker,
                                          limit=limit, sort_key=sort_key,
                                          sort_dir=sort_dir)

    def find_pool_nameserver(self, context, criterion):
        return self._find_pool_nameservers(context, criterion, one=True)

    def update_pool_nameserver(self, context, pool_nameserver):
        return self._update(context, tables.pool_nameservers, pool_nameserver,
                            exceptions.DuplicatePoolNameserver,
                            exceptions.PoolNameserverNotFound)

    def delete_pool_nameserver(self, context, pool_nameserver_id):
        pool_nameserver = self._find_pool_nameservers(
            context, {'id': pool_nameserver_id}, one=True)
        deleted_pool_nameserver = self._delete(
            context, tables.pool_nameservers, pool_nameserver,
            exceptions.PoolNameserverNotFound)

        return deleted_pool_nameserver

    # PoolTarget methods
    def _find_pool_targets(self, context, criterion, one=False, marker=None,
                           limit=None, sort_key=None, sort_dir=None):
        pool_targets = self._find(
            context, tables.pool_targets, objects.PoolTarget,
            objects.PoolTargetList, exceptions.PoolTargetNotFound,
            criterion, one, marker, limit, sort_key,
            sort_dir)

        # Load Relations
        def _load_relations(pool_target):
            pool_target.options = self._find_pool_target_options(
                context, {'pool_target_id': pool_target.id})

            pool_target.masters = self._find_pool_target_masters(
                context, {'pool_target_id': pool_target.id})

            pool_target.obj_reset_changes(['options', 'masters'])

        if one:
            _load_relations(pool_targets)
        else:
            for pool_target in pool_targets:
                _load_relations(pool_target)

        return pool_targets

    def create_pool_target(self, context, pool_id, pool_target):
        pool_target.pool_id = pool_id

        pool_target = self._create(
            tables.pool_targets, pool_target, exceptions.DuplicatePoolTarget,
            ['options', 'masters'])

        if pool_target.obj_attr_is_set('options'):
            for pool_target_option in pool_target.options:
                self.create_pool_target_option(
                    context, pool_target.id, pool_target_option)
        else:
            pool_target.options = objects.PoolTargetOptionList()

        if pool_target.obj_attr_is_set('masters'):
            for pool_target_master in pool_target.masters:
                self.create_pool_target_master(
                    context, pool_target.id, pool_target_master)
        else:
            pool_target.masters = objects.PoolTargetMasterList()

        pool_target.obj_reset_changes(['options', 'masters'])

        return pool_target

    def get_pool_target(self, context, pool_target_id):
        return self._find_pool_targets(
            context, {'id': pool_target_id}, one=True)

    def find_pool_targets(self, context, criterion=None, marker=None,
                          limit=None, sort_key=None, sort_dir=None):
        return self._find_pool_targets(context, criterion, marker=marker,
                                limit=limit, sort_key=sort_key,
                                sort_dir=sort_dir)

    def find_pool_target(self, context, criterion):
        return self._find_pool_targets(context, criterion, one=True)

    def _update_pool_items(self, context, pool, attribute_name):
        """Update attributes beloging to a pool
        """
        assert attribute_name in ('attributes', 'ns_records', 'nameservers',
                                  'targets', 'also_notifies')

        # Gather the pool ID's we have
        finder = getattr(self, "_find_pool_%s" % attribute_name)
        have_items = set()
        for r in finder(context, {'pool_id': pool.id}):
            have_items.add(r.id)

        # Prep some lists of changes
        keep_items = set([])
        create_items = []
        update_items = []

        items = []
        if pool.obj_attr_is_set(attribute_name):
            for r in getattr(pool, attribute_name).objects:
                items.append(r)

        # Determine what to change
        for item in items:
            keep_items.add(item.id)
            try:
                item.obj_get_original_value('id')
            except KeyError:
                create_items.append(item)
            else:
                update_items.append(item)

        # NOTE: Since we're dealing with mutable objects, the return value
        #       of create/update/delete option is not needed. The
        #       original item will be mutated in place on the input
        #       "pool.options" list.

        # singular: attributes -> attribute, 'notify' is as a corner case
        if attribute_name == 'also_notifies':
            singular = 'also_notify'
        else:
            singular = attribute_name[:-1]

        # Delete items
        fn = getattr(self, "delete_pool_%s" % singular)
        for item_id in have_items - keep_items:
            fn(context, item_id)

        # Update items
        fn = getattr(self, "update_pool_%s" % singular)
        for item in update_items:
            fn(context, item)

        # Create items
        fn = getattr(self, "create_pool_%s" % singular)
        for item in create_items:
            fn(context, pool.id, item)

    def _update_pool_target_items(self, context, pool_target, attribute_name):
        """Update attributes beloging to a pool target
        """
        assert attribute_name in ('options', 'masters')

        # Gather the pool ID's we have
        finder = getattr(self, "_find_pool_target_%s" % attribute_name)
        have_items = set()
        for r in finder(context, {'pool_target_id': pool_target.id}):
            have_items.add(r.id)

        # Prep some lists of changes
        keep_items = set([])
        create_items = []
        update_items = []

        items = []
        if pool_target.obj_attr_is_set(attribute_name):
            for r in getattr(pool_target, attribute_name).objects:
                items.append(r)

        # Determine what to change
        for item in items:
            keep_items.add(item.id)
            try:
                item.obj_get_original_value('id')
            except KeyError:
                create_items.append(item)
            else:
                update_items.append(item)

        # NOTE: Since we're dealing with mutable objects, the return value
        #       of create/update/delete option is not needed. The
        #       original item will be mutated in place on the input
        #       "pool.options" list.

        # singular: options -> option
        singular = attribute_name[:-1]

        # Delete items
        fn = getattr(self, "delete_pool_target_%s" % singular)
        for item_id in have_items - keep_items:
            fn(context, item_id)

        # Update items
        fn = getattr(self, "update_pool_target_%s" % singular)
        for item in update_items:
            fn(context, item)

        # Create items
        fn = getattr(self, "create_pool_target_%s" % singular)
        for item in create_items:
            fn(context, pool_target.id, item)

    def update_pool_target(self, context, pool_target):
        pool_target = self._update(
            context, tables.pool_targets, pool_target,
            exceptions.DuplicatePoolTarget, exceptions.PoolTargetNotFound,
            ['options', 'masters'])

        for attribute_name in ('options', 'masters'):
            if pool_target.obj_attr_is_set(attribute_name):
                self._update_pool_target_items(context, pool_target,
                                               attribute_name)

        # Call get_pool to get the ids of all the attributes/ns_records
        # refreshed in the pool object
        updated_pool_target = self.get_pool_target(context, pool_target.id)

        return updated_pool_target

    def delete_pool_target(self, context, pool_target_id):
        pool_target = self._find_pool_targets(
            context, {'id': pool_target_id}, one=True)

        return self._delete(context, tables.pool_targets, pool_target,
                            exceptions.PoolTargetNotFound)

    # PoolTargetOption methods
    def _find_pool_target_options(self, context, criterion, one=False,
                                  marker=None, limit=None, sort_key=None,
                                  sort_dir=None):
        return self._find(
            context, tables.pool_target_options,
            objects.PoolTargetOption, objects.PoolTargetOptionList,
            exceptions.PoolTargetOptionNotFound, criterion, one,
            marker, limit, sort_key, sort_dir)

    def create_pool_target_option(self, context, pool_target_id,
                                  pool_target_option):
        pool_target_option.pool_target_id = pool_target_id

        return self._create(tables.pool_target_options, pool_target_option,
                            exceptions.DuplicatePoolTargetOption)

    def get_pool_target_option(self, context, pool_target_option_id):
        return self._find_pool_target_options(
            context, {'id': pool_target_option_id}, one=True)

    def find_pool_target_options(self, context, criterion=None, marker=None,
                                 limit=None, sort_key=None, sort_dir=None):
        return self._find_pool_target_options(
            context, criterion, marker=marker, limit=limit, sort_key=sort_key,
            sort_dir=sort_dir)

    def find_pool_target_option(self, context, criterion):
        return self._find_pool_target_options(context, criterion, one=True)

    def update_pool_target_option(self, context, pool_target_option):
        return self._update(
            context, tables.pool_target_options, pool_target_option,
            exceptions.DuplicatePoolTargetOption,
            exceptions.PoolTargetOptionNotFound)

    def delete_pool_target_option(self, context, pool_target_option_id):
        pool_target_option = self._find_pool_target_options(
            context, {'id': pool_target_option_id}, one=True)
        deleted_pool_target_option = self._delete(
            context, tables.pool_target_options, pool_target_option,
            exceptions.PoolTargetOptionNotFound)

        return deleted_pool_target_option

    # PoolTargetMaster methods
    def _find_pool_target_masters(self, context, criterion, one=False,
                                  marker=None, limit=None, sort_key=None,
                                  sort_dir=None):
        return self._find(
            context, tables.pool_target_masters,
            objects.PoolTargetMaster, objects.PoolTargetMasterList,
            exceptions.PoolTargetMasterNotFound, criterion, one,
            marker, limit, sort_key, sort_dir)

    def create_pool_target_master(self, context, pool_target_id,
                                  pool_target_master):
        pool_target_master.pool_target_id = pool_target_id

        return self._create(tables.pool_target_masters, pool_target_master,
                            exceptions.DuplicatePoolTargetMaster)

    def get_pool_target_master(self, context, pool_target_master_id):
        return self._find_pool_target_masters(
            context, {'id': pool_target_master_id}, one=True)

    def find_pool_target_masters(self, context, criterion=None, marker=None,
                                 limit=None, sort_key=None, sort_dir=None):
        return self._find_pool_target_masters(
            context, criterion, marker=marker, limit=limit, sort_key=sort_key,
            sort_dir=sort_dir)

    def find_pool_target_master(self, context, criterion):
        return self._find_pool_target_masters(context, criterion, one=True)

    def update_pool_target_master(self, context, pool_target_master):
        return self._update(
            context, tables.pool_target_masters, pool_target_master,
            exceptions.DuplicatePoolTargetMaster,
            exceptions.PoolTargetMasterNotFound)

    def delete_pool_target_master(self, context, pool_target_master_id):
        pool_target_master = self._find_pool_target_masters(
            context, {'id': pool_target_master_id}, one=True)
        deleted_pool_target_master = self._delete(
            context, tables.pool_target_masters, pool_target_master,
            exceptions.PoolTargetMasterNotFound)

        return deleted_pool_target_master

    # PoolAlsoNotify methods
    def _find_pool_also_notifies(self, context, criterion, one=False,
                                 marker=None, limit=None, sort_key=None,
                                 sort_dir=None):
        return self._find(context, tables.pool_also_notifies,
                          objects.PoolAlsoNotify, objects.PoolAlsoNotifyList,
                          exceptions.PoolAlsoNotifyNotFound, criterion, one,
                          marker, limit, sort_key, sort_dir)

    def create_pool_also_notify(self, context, pool_id, pool_also_notify):
        pool_also_notify.pool_id = pool_id

        return self._create(tables.pool_also_notifies, pool_also_notify,
                            exceptions.DuplicatePoolAlsoNotify)

    def get_pool_also_notify(self, context, pool_also_notify_id):
        return self._find_pool_also_notifies(
            context, {'id': pool_also_notify_id}, one=True)

    def find_pool_also_notifies(self, context, criterion=None, marker=None,
                                limit=None, sort_key=None, sort_dir=None):
        return self._find_pool_also_notifies(context, criterion, marker=marker,
                                          limit=limit, sort_key=sort_key,
                                          sort_dir=sort_dir)

    def find_pool_also_notify(self, context, criterion):
        return self._find_pool_also_notifies(context, criterion, one=True)

    def update_pool_also_notify(self, context, pool_also_notify):
        return self._update(
            context, tables.pool_also_notifies, pool_also_notify,
            exceptions.DuplicatePoolAlsoNotify,
            exceptions.PoolAlsoNotifyNotFound)

    def delete_pool_also_notify(self, context, pool_also_notify_id):
        pool_also_notify = self._find_pool_also_notifies(
            context, {'id': pool_also_notify_id}, one=True)
        deleted_pool_also_notify = self._delete(
            context, tables.pool_also_notifies, pool_also_notify,
            exceptions.PoolAlsoNotifyNotFound)

        return deleted_pool_also_notify

    # Zone Transfer Methods
    def _find_zone_transfer_requests(self, context, criterion, one=False,
                                     marker=None, limit=None, sort_key=None,
                                     sort_dir=None):

        table = tables.zone_transfer_requests

        ljoin = tables.zone_transfer_requests.join(
            tables.zones,
            tables.zone_transfer_requests.c.zone_id == tables.zones.c.id)

        query = select(
            [table, tables.zones.c.name.label("zone_name")]
        ).select_from(ljoin)

        if not context.all_tenants:
            query = query.where(or_(
                table.c.tenant_id == context.project_id,
                table.c.target_tenant_id == context.project_id))

        return self._find(
            context, table, objects.ZoneTransferRequest,
            objects.ZoneTransferRequestList,
            exceptions.ZoneTransferRequestNotFound,
            criterion,
            one=one, marker=marker, limit=limit, sort_dir=sort_dir,
            sort_key=sort_key, query=query, apply_tenant_criteria=False
        )

    def create_zone_transfer_request(self, context, zone_transfer_request):

        try:
            criterion = {"zone_id": zone_transfer_request.zone_id,
                         "status": "ACTIVE"}
            self.find_zone_transfer_request(
                context, criterion)
        except exceptions.ZoneTransferRequestNotFound:
            return self._create(
                tables.zone_transfer_requests,
                zone_transfer_request,
                exceptions.DuplicateZoneTransferRequest)
        else:
            raise exceptions.DuplicateZoneTransferRequest()

    def find_zone_transfer_requests(self, context, criterion=None,
                                    marker=None, limit=None, sort_key=None,
                                    sort_dir=None):

        return self._find_zone_transfer_requests(
            context, criterion, marker=marker,
            limit=limit, sort_key=sort_key,
            sort_dir=sort_dir)

    def get_zone_transfer_request(self, context, zone_transfer_request_id):
        request = self._find_zone_transfer_requests(
            context,
            {'id': zone_transfer_request_id},
            one=True)

        return request

    def find_zone_transfer_request(self, context, criterion):

        return self._find_zone_transfer_requests(context, criterion, one=True)

    def update_zone_transfer_request(self, context, zone_transfer_request):

        zone_transfer_request.obj_reset_changes(('zone_name'))

        updated_zt_request = self._update(
            context,
            tables.zone_transfer_requests,
            zone_transfer_request,
            exceptions.DuplicateZoneTransferRequest,
            exceptions.ZoneTransferRequestNotFound,
            skip_values=['zone_name'])

        return updated_zt_request

    def delete_zone_transfer_request(self, context, zone_transfer_request_id):

        zone_transfer_request = self._find_zone_transfer_requests(
            context,
            {'id': zone_transfer_request_id},
            one=True)

        return self._delete(
            context,
            tables.zone_transfer_requests,
            zone_transfer_request,
            exceptions.ZoneTransferRequestNotFound)

    def count_zone_transfer_accept(self, context, criterion=None):
        query = select([func.count(tables.zone_transfer_accepts.c.id)])
        query = self._apply_criterion(tables.zone_transfer_accepts,
                    query, criterion)
        query = self._apply_deleted_criteria(context,
                    tables.zone_transfer_accepts, query)

        resultproxy = self.session.execute(query)
        result = resultproxy.fetchone()

        if result is None:
            return 0

        return result[0]

    def _find_zone_transfer_accept(self, context, criterion, one=False,
                                   marker=None, limit=None, sort_key=None,
                                   sort_dir=None):

        zone_transfer_accept = self._find(
            context, tables.zone_transfer_accepts,
            objects.ZoneTransferAccept,
            objects.ZoneTransferAcceptList,
            exceptions.ZoneTransferAcceptNotFound, criterion,
            one, marker, limit, sort_key, sort_dir)

        if not one:
            zone_transfer_accept.total_count = self.count_zone_transfer_accept(
                context, criterion)

        return zone_transfer_accept

    def create_zone_transfer_accept(self, context, zone_transfer_accept):

        return self._create(
            tables.zone_transfer_accepts,
            zone_transfer_accept,
            exceptions.DuplicateZoneTransferAccept)

    def find_zone_transfer_accepts(self, context, criterion=None,
                                   marker=None, limit=None, sort_key=None,
                                   sort_dir=None):
        return self._find_zone_transfer_accept(
            context, criterion, marker=marker, limit=limit, sort_key=sort_key,
            sort_dir=sort_dir)

    def get_zone_transfer_accept(self, context, zone_transfer_accept_id):
        return self._find_zone_transfer_accept(
            context,
            {'id': zone_transfer_accept_id},
            one=True)

    def find_zone_transfer_accept(self, context, criterion):
        return self._find_zone_transfer_accept(
            context,
            criterion,
            one=True)

    def update_zone_transfer_accept(self, context, zone_transfer_accept):

        return self._update(
            context,
            tables.zone_transfer_accepts,
            zone_transfer_accept,
            exceptions.DuplicateZoneTransferAccept,
            exceptions.ZoneTransferAcceptNotFound)

    def delete_zone_transfer_accept(self, context, zone_transfer_accept_id):

        zone_transfer_accept = self._find_zone_transfer_accept(
            context,
            {'id': zone_transfer_accept_id},
            one=True)

        return self._delete(
            context,
            tables.zone_transfer_accepts,
            zone_transfer_accept,
            exceptions.ZoneTransferAcceptNotFound)

    # Zone Import Methods
    def _find_zone_imports(self, context, criterion, one=False, marker=None,
                   limit=None, sort_key=None, sort_dir=None):
        if not criterion:
            criterion = {}
        criterion['task_type'] = 'IMPORT'
        zone_imports = self._find(
            context, tables.zone_tasks, objects.ZoneImport,
            objects.ZoneImportList, exceptions.ZoneImportNotFound, criterion,
            one, marker, limit, sort_key, sort_dir)

        if not one:
            zone_imports.total_count = self.count_zone_tasks(
                context, criterion)

        return zone_imports

    def create_zone_import(self, context, zone_import):
        return self._create(
            tables.zone_tasks, zone_import, exceptions.DuplicateZoneImport)

    def get_zone_import(self, context, zone_import_id):
        return self._find_zone_imports(context, {'id': zone_import_id},
                                     one=True)

    def find_zone_imports(self, context, criterion=None, marker=None,
                  limit=None, sort_key=None, sort_dir=None):
        return self._find_zone_imports(context, criterion, marker=marker,
                               limit=limit, sort_key=sort_key,
                               sort_dir=sort_dir)

    def find_zone_import(self, context, criterion):
        return self._find_zone_imports(context, criterion, one=True)

    def update_zone_import(self, context, zone_import):
        return self._update(
            context, tables.zone_tasks, zone_import,
            exceptions.DuplicateZoneImport, exceptions.ZoneImportNotFound)

    def delete_zone_import(self, context, zone_import_id):
        # Fetch the existing zone_import, we'll need to return it.
        zone_import = self._find_zone_imports(context, {'id': zone_import_id},
                                one=True)
        return self._delete(context, tables.zone_tasks, zone_import,
                    exceptions.ZoneImportNotFound)

    # Zone Export Methods
    def _find_zone_exports(self, context, criterion, one=False, marker=None,
                   limit=None, sort_key=None, sort_dir=None):
        if not criterion:
            criterion = {}
        criterion['task_type'] = 'EXPORT'
        zone_exports = self._find(
            context, tables.zone_tasks, objects.ZoneExport,
            objects.ZoneExportList, exceptions.ZoneExportNotFound, criterion,
            one, marker, limit, sort_key, sort_dir)
        if not one:
            zone_exports.total_count = self.count_zone_tasks(
                context, criterion)

        return zone_exports

    def create_zone_export(self, context, zone_export):
        return self._create(
            tables.zone_tasks, zone_export, exceptions.DuplicateZoneExport)

    def get_zone_export(self, context, zone_export_id):
        return self._find_zone_exports(context, {'id': zone_export_id},
                                     one=True)

    def find_zone_exports(self, context, criterion=None, marker=None,
                  limit=None, sort_key=None, sort_dir=None):
        return self._find_zone_exports(context, criterion, marker=marker,
                               limit=limit, sort_key=sort_key,
                               sort_dir=sort_dir)

    def find_zone_export(self, context, criterion):
        return self._find_zone_exports(context, criterion, one=True)

    def update_zone_export(self, context, zone_export):
        return self._update(
            context, tables.zone_tasks, zone_export,
            exceptions.DuplicateZoneExport, exceptions.ZoneExportNotFound)

    def delete_zone_export(self, context, zone_export_id):
        # Fetch the existing zone_export, we'll need to return it.
        zone_export = self._find_zone_exports(context, {'id': zone_export_id},
                                one=True)
        return self._delete(context, tables.zone_tasks, zone_export,
                    exceptions.ZoneExportNotFound)

    def count_zone_tasks(self, context, criterion=None):
        query = select([func.count(tables.zone_tasks.c.id)])
        query = self._apply_criterion(tables.zone_tasks, query, criterion)
        query = self._apply_tenant_criteria(context, tables.zone_tasks, query)
        query = self._apply_deleted_criteria(context, tables.zone_tasks, query)

        resultproxy = self.session.execute(query)
        result = resultproxy.fetchone()

        if result is None:
            return 0

        return result[0]

    # Service Status Methods
    def _find_service_statuses(self, context, criterion, one=False,
                               marker=None, limit=None, sort_key=None,
                               sort_dir=None):
        return self._find(
            context, tables.service_status, objects.ServiceStatus,
            objects.ServiceStatusList, exceptions.ServiceStatusNotFound,
            criterion, one, marker, limit, sort_key, sort_dir)

    def find_service_status(self, context, criterion):
        return self._find_service_statuses(context, criterion, one=True)

    def find_service_statuses(self, context, criterion=None, marker=None,
                              limit=None, sort_key=None, sort_dir=None):
        return self._find_service_statuses(context, criterion, marker=marker,
                                           limit=limit, sort_key=sort_key,
                                           sort_dir=sort_dir)

    def create_service_status(self, context, service_status):
        return self._create(
            tables.service_status, service_status,
            exceptions.DuplicateServiceStatus)

    def update_service_status(self, context, service_status):
        return self._update(
            context, tables.service_status, service_status,
            exceptions.DuplicateServiceStatus,
            exceptions.ServiceStatusNotFound)

    # diagnostics
    def ping(self, context):
        start_time = time.time()

        try:
            result = self.engine.execute('SELECT 1').first()
        except Exception:
            status = False
        else:
            status = True if result[0] == 1 else False

        return {
            'status': status,
            'rtt': "%f" % (time.time() - start_time)
        }

    # Reverse Name utils
    def _rname_check(self, criterion):
        # If the criterion has 'name' in it, switch it out for reverse_name
        if criterion is not None and criterion.get('name', "").startswith('*'):
            criterion['reverse_name'] = criterion.pop('name')[::-1]
        return criterion
