diff --git a/designate/sqlalchemy/models.py b/designate/sqlalchemy/models.py index 0bd9a2d0c..007395194 100644 --- a/designate/sqlalchemy/models.py +++ b/designate/sqlalchemy/models.py @@ -13,8 +13,11 @@ # 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 sqlalchemy import Column, DateTime from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import object_mapper +from sqlalchemy.types import CHAR +from designate.openstack.common import timeutils from designate import exceptions @@ -82,3 +85,15 @@ class Base(object): if not k[0] == '_']) local.update(joined) return local.iteritems() + + +class SoftDeleteMixin(object): + deleted = Column(CHAR(32), nullable=False, default="0") + deleted_at = Column(DateTime, nullable=True, default=None) + + def soft_delete(self, session=None): + """ Mark this object as deleted. """ + self.deleted = self.id + self.deleted_at = timeutils.utcnow() + + self.save(session=session) diff --git a/designate/storage/impl_sqlalchemy/__init__.py b/designate/storage/impl_sqlalchemy/__init__.py index 1b180355c..88a9c02a8 100644 --- a/designate/storage/impl_sqlalchemy/__init__.py +++ b/designate/storage/impl_sqlalchemy/__init__.py @@ -21,6 +21,7 @@ from designate.openstack.common import log as logging from designate import exceptions from designate.storage import base from designate.storage.impl_sqlalchemy import models +from designate.sqlalchemy.models import SoftDeleteMixin from designate.sqlalchemy.session import get_session from designate.sqlalchemy.session import get_engine from designate.sqlalchemy.session import SQLOPTS @@ -64,6 +65,16 @@ class SQLAlchemyStorage(base.Storage): return query + def _apply_deleted_criteria(self, context, model, query): + if issubclass(model, SoftDeleteMixin): + if context.show_deleted: + LOG.debug('Including deleted items in query results') + else: + LOG.debug('Filtering deleted items from query results') + query = query.filter(model.deleted == "0") + + return query + # Quota Methods def create_quota(self, context, values): quota = models.Quota() @@ -281,6 +292,19 @@ class SQLAlchemyStorage(base.Storage): return self.session.query(distinct(models.Domain.tenant_id)).count() # Domain Methods + def _find_domains(self, context, criterion, one=False): + query = self.session.query(models.Domain) + query = self._apply_criterion(models.Domain, query, criterion) + query = self._apply_deleted_criteria(context, models.Domain, query) + + if one: + try: + return query.one() + except (exc.NoResultFound, exc.MultipleResultsFound): + raise exceptions.DomainNotFound() + else: + return query.all() + def create_domain(self, context, values): domain = models.Domain() @@ -294,54 +318,26 @@ class SQLAlchemyStorage(base.Storage): return dict(domain) def get_domains(self, context, criterion=None): - query = self.session.query(models.Domain) - query = self._apply_criterion(models.Domain, query, criterion) + domains = self._find_domains(context, criterion) - try: - result = query.all() - except exc.NoResultFound: - LOG.debug('No results found') - return [] - else: - return [dict(o) for o in result] - - def _get_domain(self, context, domain_id): - query = self.session.query(models.Domain) - - domain = query.get(domain_id) - - if not domain: - raise exceptions.DomainNotFound(domain_id) - else: - return domain + return [dict(d) for d in domains] def get_domain(self, context, domain_id): - domain = self._get_domain(context, domain_id) + domain = self._find_domains(context, {'id': domain_id}, True) return dict(domain) - def _find_domains(self, context, criterion, one=False): - query = self.session.query(models.Domain) - query = self._apply_criterion(models.Domain, query, criterion) - - if one: - try: - domain = query.one() - return dict(domain) - except (exc.NoResultFound, exc.MultipleResultsFound): - raise exceptions.DomainNotFound() - else: - domains = query.all() - return [dict(d) for d in domains] - def find_domains(self, context, criterion): - return self._find_domains(context, criterion) + domains = self._find_domains(context, criterion) + + return [dict(d) for d in domains] def find_domain(self, context, criterion): - return self._find_domains(context, criterion, one=True) + domain = self._find_domains(context, criterion, one=True) + return dict(domain) def update_domain(self, context, domain_id, values): - domain = self._get_domain(context, domain_id) + domain = self._find_domains(context, {'id': domain_id}, True) domain.update(values) @@ -353,13 +349,15 @@ class SQLAlchemyStorage(base.Storage): return dict(domain) def delete_domain(self, context, domain_id): - domain = self._get_domain(context, domain_id) + domain = self._find_domains(context, {'id': domain_id}, True) - domain.delete(self.session) + domain.soft_delete(self.session) def count_domains(self, context, criterion=None): query = self.session.query(models.Domain) query = self._apply_criterion(models.Domain, query, criterion) + query = self._apply_deleted_criteria(context, models.Domain, query) + return query.count() # Record Methods diff --git a/designate/storage/impl_sqlalchemy/migrate_repo/versions/016_add_deleted_columns.py b/designate/storage/impl_sqlalchemy/migrate_repo/versions/016_add_deleted_columns.py new file mode 100644 index 000000000..145315729 --- /dev/null +++ b/designate/storage/impl_sqlalchemy/migrate_repo/versions/016_add_deleted_columns.py @@ -0,0 +1,64 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# Author: Kiall Mac Innes +# +# 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 sqlalchemy import MetaData, Table, Column, DateTime +from sqlalchemy.types import CHAR +from migrate.changeset.constraint import UniqueConstraint + +meta = MetaData() + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + + domains_table = Table('domains', meta, autoload=True) + + # Create the new columns + deleted_column = Column('deleted', CHAR(32), nullable=False, default="0") + deleted_column.create(domains_table, populate_default=True) + + deleted_at_column = Column('deleted_at', DateTime, nullable=True, + default=None) + deleted_at_column.create(domains_table, populate_default=True) + + # Drop the old single column unique + domains_table.c.name.alter(unique=False) + + # Add a new multi-column unique index + constraint = UniqueConstraint('name', 'deleted', name='unique_domain_name', + table=domains_table) + constraint.create() + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + + domains_table = Table('domains', meta, autoload=True) + + # Drop the multi-column unique index + constraint = UniqueConstraint('name', 'deleted', name='unique_domain_name', + table=domains_table) + constraint.drop() + + # Revert to single column unique + domains_table.c.name.alter(unique=True) + + # Drop the deleted columns + deleted_column = Column('deleted', CHAR(32), nullable=True, default=None) + deleted_column.drop(domains_table) + + deleted_at_column = Column('deleted_at', DateTime, nullable=True, + default=None) + deleted_at_column.drop(domains_table) diff --git a/designate/storage/impl_sqlalchemy/models.py b/designate/storage/impl_sqlalchemy/models.py index d2c1f10da..92c988aaf 100644 --- a/designate/storage/impl_sqlalchemy/models.py +++ b/designate/storage/impl_sqlalchemy/models.py @@ -25,6 +25,7 @@ from designate.openstack.common import timeutils from designate.openstack.common.uuidutils import generate_uuid from designate.sqlalchemy.types import UUID from designate.sqlalchemy.models import Base as CommonBase +from designate.sqlalchemy.models import SoftDeleteMixin from sqlalchemy.ext.declarative import declarative_base LOG = logging.getLogger(__name__) @@ -66,12 +67,15 @@ class Server(Base): name = Column(String(255), nullable=False, unique=True) -class Domain(Base): +class Domain(SoftDeleteMixin, Base): __tablename__ = 'domains' + __table_args__ = ( + UniqueConstraint('name', 'deleted', name='unique_domain_name'), + ) tenant_id = Column(String(36), default=None, nullable=True) - name = Column(String(255), nullable=False, unique=True) + name = Column(String(255), nullable=False) email = Column(String(255), nullable=False) ttl = Column(Integer, default=3600, nullable=False) diff --git a/designate/tests/test_storage/__init__.py b/designate/tests/test_storage/__init__.py index 53d6ec41d..1afcd7273 100644 --- a/designate/tests/test_storage/__init__.py +++ b/designate/tests/test_storage/__init__.py @@ -584,6 +584,15 @@ class StorageTestCase(TestCase): uuid = 'caf771fc-6b05-4891-bee1-c2a48621f57b' self.storage.get_domain(self.admin_context, uuid) + def test_get_deleted_domain(self): + context = self.get_admin_context() + context.show_deleted = True + + _, domain = self.create_domain() + + self.storage.delete_domain(self.admin_context, domain['id']) + self.storage.get_domain(context, domain['id']) + def test_find_domain_criterion(self): _, domain_one = self.create_domain(0) _, domain_two = self.create_domain(1)