Repository: marshmallow-code/marshmallow-sqlalchemy

Issue:
Ordering output doesn't work for auto_field
In marshmallow, to order serialization output, set ordered = True in the Meta class as follows.

```python
class MyModelSchema(SQLAlchemyAutoSchema):
    class Meta:
        model = MyModel
        ordered = True

    some_field = auto_field(validate=validate.Range(min=0, max=10))
```

It works well without ```auto_field```, but it generate following error with ```auto_field```.

```
AttributeError: 'SQLAlchemyAutoField' object has no attribute '_creation_index'
```

Is there any solution for this?

Discussion:

Patch:
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 1f8c8be..252f378 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -35,4 +35,4 @@ Contributors
 - Pierre Verkest `@petrus-v <https://github.com/petrus-v>`_
 - Erik Cederstrand `@ecederstrand <https://github.com/ecederstrand>`_
 - Daven Quinn `@davenquinn <https://github.com/davenquinn>`_
-- Peter Schutt `@5uper5hoot <https://github.com/5uper5hoot>`_
+- Peter Schutt `@peterschutt <https://github.com/peterschutt>`_
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index b4bac3b..3bb1106 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,14 @@
 Changelog
 ---------
 
+0.23.0 (unreleased)
++++++++++++++++++++
+
+Bug fixes:
+
+* Fix data keys when using ``Related`` with a ``Column`` that is named differently
+  from its attribute (:issue:`299`). Thanks :user:`peterschutt` for the catch and patch.
+
 0.22.3 (2020-03-01)
 +++++++++++++++++++
 
diff --git a/setup.py b/setup.py
index 781210f..22b74a5 100644
--- a/setup.py
+++ b/setup.py
@@ -4,9 +4,9 @@ from setuptools import setup, find_packages
 
 INSTALL_REQUIRES = ("marshmallow>=2.15.2", "SQLAlchemy>=1.2.0")
 EXTRAS_REQUIRE = {
-    "tests": ["pytest", "pytest-lazy-fixture", "mock"],
+    "tests": ["pytest", "pytest-lazy-fixture"],
     "lint": ["flake8==3.7.9", "flake8-bugbear==20.1.4", "pre-commit~=2.0"],
-    "docs": ["sphinx==2.4.4", "alabaster==0.7.12", "sphinx-issues==1.2.0"],
+    "docs": ["sphinx==3.0.2", "alabaster==0.7.12", "sphinx-issues==1.2.0"],
 }
 EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"]
 
diff --git a/src/marshmallow_sqlalchemy/fields.py b/src/marshmallow_sqlalchemy/fields.py
index df13028..bc16a37 100644
--- a/src/marshmallow_sqlalchemy/fields.py
+++ b/src/marshmallow_sqlalchemy/fields.py
@@ -1,6 +1,7 @@
 from marshmallow import fields
 from marshmallow.utils import is_iterable_but_not_string
 
+from sqlalchemy import inspect
 from sqlalchemy.orm.exc import NoResultFound
 
 
@@ -58,9 +59,8 @@ class Related(fields.Field):
     @property
     def related_keys(self):
         if self.columns:
-            return [
-                self.related_model.__mapper__.columns[column] for column in self.columns
-            ]
+            insp = inspect(self.related_model)
+            return [insp.attrs[column] for column in self.columns]
         return get_primary_keys(self.related_model)
 
     @property
diff --git a/src/marshmallow_sqlalchemy/schema/sqlalchemy_schema.py b/src/marshmallow_sqlalchemy/schema/sqlalchemy_schema.py
index d397cd8..dad0923 100644
--- a/src/marshmallow_sqlalchemy/schema/sqlalchemy_schema.py
+++ b/src/marshmallow_sqlalchemy/schema/sqlalchemy_schema.py
@@ -1,4 +1,4 @@
-from marshmallow.fields import FieldABC
+from marshmallow.fields import Field
 from marshmallow.schema import Schema, SchemaMeta, SchemaOpts
 import sqlalchemy as sa
 from sqlalchemy.ext.declarative import DeclarativeMeta
@@ -10,8 +10,10 @@ from .load_instance_mixin import LoadInstanceMixin
 
 # This isn't really a field; it's a placeholder for the metaclass.
 # This should be considered private API.
-class SQLAlchemyAutoField(FieldABC):
+class SQLAlchemyAutoField(Field):
     def __init__(self, *, column_name=None, model=None, table=None, field_kwargs):
+        super().__init__()
+
         if model and table:
             raise ValueError("Cannot pass both `model` and `table` options.")
 


Test patch:
diff --git a/tests/test_sqlalchemy_schema.py b/tests/test_sqlalchemy_schema.py
index da4f827..c3b0003 100644
--- a/tests/test_sqlalchemy_schema.py
+++ b/tests/test_sqlalchemy_schema.py
@@ -6,6 +6,7 @@ import sqlalchemy as sa
 
 from marshmallow_sqlalchemy import SQLAlchemySchema, SQLAlchemyAutoSchema, auto_field
 from marshmallow_sqlalchemy.exceptions import IncorrectSchemaTypeError
+from marshmallow_sqlalchemy.fields import Related
 from .utils import unpack, MARSHMALLOW_VERSION_INFO
 
 
@@ -244,6 +245,21 @@ def test_auto_field_works_with_synonym(models):
     assert "curr_school_id" in schema.fields
 
 
+# Regresion test https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues/306
+def test_auto_field_works_with_ordered_flag(models):
+    class StudentSchema(SQLAlchemyAutoSchema):
+        class Meta:
+            model = models.Student
+            ordered = True
+            strict = True  # marshmallow 2 compat
+
+        full_name = auto_field()
+
+    schema = StudentSchema()
+    # Declared fields precede auto-generated fields
+    assert tuple(schema.fields.keys()) == ("full_name", "id", "dob", "date_created")
+
+
 class TestAliasing:
     @pytest.fixture
     def aliased_schema(self, models):
@@ -359,3 +375,23 @@ class TestModelInstanceDeserialization:
         assert isinstance(load_data, models.Teacher)
         state = sa.inspect(load_data)
         assert state.transient
+
+
+def test_related_when_model_attribute_name_distinct_from_column_name(
+    models, session, teacher,
+):
+    class TeacherSchema(SQLAlchemyAutoSchema):
+        class Meta:
+            model = models.Teacher
+            load_instance = True
+            sqla_session = session
+            strict = True  # marshmallow 2 compat
+
+        current_school = Related(["id", "name"])
+
+    dump_data = unpack(TeacherSchema().dump(teacher))
+    assert "school_id" not in dump_data["current_school"]
+    assert dump_data["current_school"]["id"] == teacher.current_school.id
+    new_teacher = unpack(TeacherSchema().load(dump_data, transient=True))
+    assert new_teacher.current_school.id == teacher.current_school.id
+    assert unpack(TeacherSchema().load(dump_data)) is teacher