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