Add JSON-encoded types for sqlalchemy

This code is imported from Ironic with new tests, so that it can
be reused in other projects (I need it for ironic-inspector).

Some small enhancements were made to the imported code:
* Non-capitalized JSON word in names
* Base type can be used on its own

Change-Id: Ic991f34c5b5f091d8627643367cdaa73ad2b1236
This commit is contained in:
Dmitry Tantsur 2015-07-30 15:42:28 +02:00
parent 7733f9604d
commit 3a17d826a5
2 changed files with 148 additions and 0 deletions

View File

@ -0,0 +1,62 @@
# 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 json
from sqlalchemy.types import TypeDecorator, Text
class JsonEncodedType(TypeDecorator):
"""Base column type for data serialized as JSON-encoded string in db."""
type = None
impl = Text
def process_bind_param(self, value, dialect):
if value is None:
if self.type is not None:
# Save default value according to current type to keep the
# interface consistent.
value = self.type()
elif self.type is not None and not isinstance(value, self.type):
raise TypeError("%s supposes to store %s objects, but %s given"
% (self.__class__.__name__,
self.type.__name__,
type(value).__name__))
serialized_value = json.dumps(value)
return serialized_value
def process_result_value(self, value, dialect):
if value is not None:
value = json.loads(value)
return value
class JsonEncodedDict(JsonEncodedType):
"""Represents dict serialized as json-encoded string in db.
Note that this type does NOT track mutations. If you want to update it, you
have to assign existing value to a temporary variable, update, then assign
back. See this page for more robust work around:
http://docs.sqlalchemy.org/en/rel_1_0/orm/extensions/mutable.html
"""
type = dict
class JsonEncodedList(JsonEncodedType):
"""Represents list serialized as json-encoded string in db.
Note that this type does NOT track mutations. If you want to update it, you
have to assign existing value to a temporary variable, update, then assign
back. See this page for more robust work around:
http://docs.sqlalchemy.org/en/rel_1_0/orm/extensions/mutable.html
"""
type = list

View File

@ -0,0 +1,86 @@
# 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.
"""Tests for JSON SQLAlchemy types."""
from sqlalchemy import Column, Integer
from sqlalchemy.ext.declarative import declarative_base
from oslo_db import exception as db_exc
from oslo_db.sqlalchemy import models
from oslo_db.sqlalchemy import test_base
from oslo_db.sqlalchemy import types
BASE = declarative_base()
class JsonTable(BASE, models.ModelBase):
__tablename__ = 'test_json_types'
id = Column(Integer, primary_key=True)
jdict = Column(types.JsonEncodedDict)
jlist = Column(types.JsonEncodedList)
json = Column(types.JsonEncodedType)
class JsonTypesTestCase(test_base.DbTestCase):
def setUp(self):
super(JsonTypesTestCase, self).setUp()
JsonTable.__table__.create(self.engine)
self.addCleanup(JsonTable.__table__.drop, self.engine)
self.session = self.sessionmaker()
self.addCleanup(self.session.close)
def test_default_value(self):
with self.session.begin():
JsonTable(id=1).save(self.session)
obj = self.session.query(JsonTable).filter_by(id=1).one()
self.assertEqual([], obj.jlist)
self.assertEqual({}, obj.jdict)
self.assertIsNone(obj.json)
def test_dict(self):
test = {'a': 42, 'b': [1, 2, 3]}
with self.session.begin():
JsonTable(id=1, jdict=test).save(self.session)
obj = self.session.query(JsonTable).filter_by(id=1).one()
self.assertEqual(test, obj.jdict)
def test_list(self):
test = [1, True, "hello", {}]
with self.session.begin():
JsonTable(id=1, jlist=test).save(self.session)
obj = self.session.query(JsonTable).filter_by(id=1).one()
self.assertEqual(test, obj.jlist)
def test_dict_type_check(self):
self.assertRaises(db_exc.DBError,
JsonTable(id=1, jdict=[]).save, self.session)
def test_list_type_check(self):
self.assertRaises(db_exc.DBError,
JsonTable(id=1, jlist={}).save, self.session)
def test_generic(self):
tested = [
"string",
42,
True,
None,
[1, 2, 3],
{'a': 'b'}
]
for i, test in enumerate(tested):
with self.session.begin():
JsonTable(id=i, json=test).save(self.session)
obj = self.session.query(JsonTable).filter_by(id=i).one()
self.assertEqual(test, obj.json)