This patch removes unique index on the 'key' column of image_metadatum and replaces it with a compound UniqueConstraint on 'image_id' and 'key'. The 'key' column remains indexed.

Adds tests to ensure that two different images can use the same key, while preventing a single image from having two keys with the same name.
This commit is contained in:
Rick Harris 2010-10-15 17:16:50 +00:00 committed by Tarmac
commit b335ff2a87
3 changed files with 106 additions and 15 deletions

View File

@ -29,14 +29,20 @@ FLAGS = flags.FLAGS
_ENGINE = None _ENGINE = None
_MAKER = None _MAKER = None
def get_engine(echo=False):
global _ENGINE
if not _ENGINE:
_ENGINE = create_engine(FLAGS.sql_connection, echo=echo)
return _ENGINE
def get_session(autocommit=True, expire_on_commit=False): def get_session(autocommit=True, expire_on_commit=False):
"""Helper method to grab session""" """Helper method to grab session"""
global _ENGINE
global _MAKER global _MAKER
if not _MAKER: if not _MAKER:
if not _ENGINE: engine = get_engine()
_ENGINE = create_engine(FLAGS.sql_connection, echo=False) _MAKER = sessionmaker(bind=engine,
_MAKER = sessionmaker(bind=_ENGINE,
autocommit=autocommit, autocommit=autocommit,
expire_on_commit=expire_on_commit) expire_on_commit=expire_on_commit)
return _MAKER() return _MAKER()

View File

@ -26,9 +26,10 @@ import datetime
from sqlalchemy.orm import relationship, backref, exc, object_mapper, validates from sqlalchemy.orm import relationship, backref, exc, object_mapper, validates
from sqlalchemy import Column, Integer, String from sqlalchemy import Column, Integer, String
from sqlalchemy import ForeignKey, DateTime, Boolean, Text from sqlalchemy import ForeignKey, DateTime, Boolean, Text
from sqlalchemy import UniqueConstraint
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from glance.common.db.sqlalchemy.session import get_session from glance.common.db.sqlalchemy.session import get_session, get_engine
from glance.common import exception from glance.common import exception
from glance.common import flags from glance.common import flags
@ -133,14 +134,15 @@ class Image(BASE, ModelBase):
@validates('image_type') @validates('image_type')
def validate_image_type(self, key, image_type): def validate_image_type(self, key, image_type):
if not image_type in ('machine', 'kernel', 'ramdisk', 'raw'): if not image_type in ('machine', 'kernel', 'ramdisk', 'raw'):
raise exception.Invalid("Invalid image type '%s' for image." % image_type) raise exception.Invalid(
"Invalid image type '%s' for image." % image_type)
return image_type return image_type
@validates('status') @validates('status')
def validate_status(self, key, state): def validate_status(self, key, status):
if not state in ('available', 'pending', 'disabled'): if not status in ('available', 'pending', 'disabled'):
raise exception.Invalid("Invalid status '%s' for image." % status) raise exception.Invalid("Invalid status '%s' for image." % status)
return image_type return status
# TODO(sirp): should these be stored as metadata? # TODO(sirp): should these be stored as metadata?
#user_id = Column(String(255)) #user_id = Column(String(255))
@ -176,18 +178,24 @@ class ImageMetadatum(BASE, ModelBase):
"""Represents an image metadata in the datastore""" """Represents an image metadata in the datastore"""
__tablename__ = 'image_metadata' __tablename__ = 'image_metadata'
__prefix__ = 'img-meta' __prefix__ = 'img-meta'
__table_args__ = (UniqueConstraint('image_id', 'key'), {})
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
image_id = Column(Integer, ForeignKey('images.id'), nullable=False) image_id = Column(Integer, ForeignKey('images.id'), nullable=False)
image = relationship(Image, backref=backref('metadata')) image = relationship(Image, backref=backref('metadata'))
key = Column(String(255), index=True, unique=True) key = Column(String(255), index=True)
value = Column(Text) value = Column(Text)
def register_models(): def register_models():
"""Register Models and create metadata""" """Register Models and create metadata"""
from sqlalchemy import create_engine engine = get_engine()
models = (Image, ImageFile, ImageMetadatum) BASE.metadata.create_all(engine)
engine = create_engine(FLAGS.sql_connection, echo=False)
for model in models:
model.metadata.create_all(engine) def unregister_models():
"""Unregister Models, useful clearing out data before testing"""
engine = get_engine()
BASE.metadata.drop_all(engine)

View File

@ -0,0 +1,77 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 OpenStack, LLC
# All Rights Reserved.
#
# 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 unittest
import sqlalchemy.exceptions as sa_exc
from glance.common import exception
from glance.parallax import db
from glance.common import flags
from glance.parallax.db.sqlalchemy import models
FLAGS = flags.FLAGS
class TestModels(unittest.TestCase):
""" Test Parllax SQLAlchemy models using an in-memory sqlite DB"""
def setUp(self):
FLAGS.sql_connection = "sqlite://" # in-memory db
models.unregister_models()
models.register_models()
self.image = self._make_image(id=2, name='fake image #2')
def test_metadata_key_constraint_ok(self):
"""Two different images are permitted to have metadata that share the
same key
"""
self._make_metadatum(self.image, key="spam", value="eggs")
second_image = self._make_image(id=3, name='fake image #3')
self._make_metadatum(second_image, key="spam", value="eggs")
def test_metadata_key_constraint_bad(self):
"""The same image cannot have two distinct pieces of metadata with the
same key.
"""
self._make_metadatum(self.image, key="spam", value="eggs")
self.assertRaises(sa_exc.IntegrityError,
self._make_metadatum, self.image, key="spam", value="eggs")
def _make_image(self, id, name):
"""Convenience method to create an image with a given name and id"""
fixture = {'id': id,
'name': name,
'is_public': True,
'image_type': 'kernel',
'status': 'available'}
context = None
image = db.api.image_create(context, fixture)
return image
def _make_metadatum(self, image, key, value):
"""Convenience method to create metadata attached to an image"""
metadata = {'image_id': image['id'], 'key': key, 'value': value}
context = None
metadatum = db.api.image_metadatum_create(context, metadata)
return metadatum