add support for sqlalchemy composite columns

This commit is contained in:
Simon Hewitt 2016-08-19 12:13:13 -07:00
parent 17ba01570a
commit 9e185b01c2
3 changed files with 88 additions and 7 deletions

View File

@ -31,6 +31,30 @@ def convert_sqlalchemy_column(column):
return convert_sqlalchemy_type(getattr(column, 'type', None), column)
def convert_sqlalchemy_composite(composite):
try:
return convert_sqlalchemy_composite.registry[composite.composite_class](composite)
except KeyError:
try:
raise Exception(
"Don't know how to convert the composite field %s (%s)" %
(composite, composite.composite_class))
except AttributeError:
# handle fields that are not attached to a class yet (don't have a parent)
raise Exception(
"Don't know how to convert the composite field %r (%s)" %
(composite, composite.composite_class))
def _register_composite_class(cls):
def inner(fn):
convert_sqlalchemy_composite.registry[cls] = fn
return inner
convert_sqlalchemy_composite.registry = {}
convert_sqlalchemy_composite.register = _register_composite_class
@singledispatch
def convert_sqlalchemy_type(type, column):
raise Exception(

View File

@ -1,5 +1,6 @@
from py.test import raises
from sqlalchemy import Column, Table, types
from sqlalchemy.orm import composite
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy_utils.types.choice import ChoiceType
from sqlalchemy.dialects import postgresql
@ -7,6 +8,7 @@ from sqlalchemy.dialects import postgresql
import graphene
from graphene.core.types.custom_scalars import JSONString
from graphene.contrib.sqlalchemy.converter import (convert_sqlalchemy_column,
convert_sqlalchemy_composite,
convert_sqlalchemy_relationship)
from graphene.contrib.sqlalchemy.fields import (ConnectionOrListField,
SQLAlchemyModelField)
@ -23,6 +25,19 @@ def assert_column_conversion(sqlalchemy_type, graphene_field, **kwargs):
return field
def assert_composite_conversion(composite_class, composite_columns, graphene_field,
**kwargs):
composite_column = composite(composite_class, *composite_columns,
doc='Custom Help Text', **kwargs)
graphene_type = convert_sqlalchemy_composite(composite_column)
assert isinstance(graphene_type, graphene_field)
field = graphene_type.as_field()
# SQLAlchemy currently does not persist the doc onto the column, even though
# the documentation says it does....
# assert field.description == 'Custom Help Text'
return field
def test_should_unknown_sqlalchemy_field_raise_exception():
with raises(Exception) as excinfo:
convert_sqlalchemy_column(None)
@ -148,3 +163,36 @@ def test_should_postgresql_jsonb_convert():
def test_should_postgresql_hstore_convert():
assert_column_conversion(postgresql.HSTORE(), JSONString)
def test_should_composite_convert():
class CompositeClass(object):
def __init__(self, col1, col2):
self.col1 = col1
self.col2 = col2
@convert_sqlalchemy_composite.register(CompositeClass)
def convert_composite_class(composite):
return graphene.String(description=composite.doc)
assert_composite_conversion(CompositeClass,
(Column(types.Unicode(50)),
Column(types.Unicode(50))),
graphene.String)
def test_should_unknown_sqlalchemy_composite_raise_exception():
with raises(Exception) as excinfo:
class CompositeClass(object):
def __init__(self, col1, col2):
self.col1 = col1
self.col2 = col2
assert_composite_conversion(CompositeClass,
(Column(types.Unicode(50)),
Column(types.Unicode(50))),
graphene.String)
assert 'Don\'t know how to convert the composite field' in str(excinfo.value)

View File

@ -5,9 +5,10 @@ from sqlalchemy.inspection import inspect as sqlalchemyinspect
from sqlalchemy.orm.exc import NoResultFound
from ...core.classtypes.objecttype import ObjectType, ObjectTypeMeta
from ...relay.types import Node, NodeMeta
from ...relay.connection import Connection
from ...relay.types import Node, NodeMeta
from .converter import (convert_sqlalchemy_column,
convert_sqlalchemy_composite,
convert_sqlalchemy_relationship)
from .options import SQLAlchemyOptions
from .utils import get_query, is_mapped
@ -34,17 +35,25 @@ class SQLAlchemyObjectTypeMeta(ObjectTypeMeta):
converted_relationship = convert_sqlalchemy_relationship(relationship)
cls.add_to_class(relationship.key, converted_relationship)
for name, column in inspected_model.columns.items():
def filter_included(l):
for name, value in l.items():
is_not_in_only = only_fields and name not in only_fields
is_already_created = name in already_created_fields
is_excluded = name in exclude_fields or is_already_created
if is_not_in_only or is_excluded:
# We skip this field if we specify only_fields and is not
# in there. Or when we excldue this field in exclude_fields
if is_not_in_only or is_excluded:
continue
yield name, value
for name, column in filter_included(inspected_model.columns):
converted_column = convert_sqlalchemy_column(column)
cls.add_to_class(name, converted_column)
for name, composite in filter_included(inspected_model.composites):
converted_composite = convert_sqlalchemy_composite(composite)
cls.add_to_class(name, converted_composite)
def construct(cls, *args, **kwargs):
cls = super(SQLAlchemyObjectTypeMeta, cls).construct(*args, **kwargs)
if not cls._meta.abstract: