Merge "Add JSON-encoded types for sqlalchemy"
This commit is contained in:
commit
7b671d3165
62
oslo_db/sqlalchemy/types.py
Normal file
62
oslo_db/sqlalchemy/types.py
Normal 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
|
86
oslo_db/tests/sqlalchemy/test_types.py
Normal file
86
oslo_db/tests/sqlalchemy/test_types.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user