diff --git a/README.md b/README.md
index fac2862..14a297f 100644
--- a/README.md
+++ b/README.md
@@ -79,7 +79,27 @@ Ormar is built with:
As I write open-source code to solve everyday problems in my work or to promote and build strong python
community you can say thank you and buy me a coffee or sponsor me with a monthly amount to help ensure my work remains free and maintained.
-
+
+
+
### Migrating from `sqlalchemy` and existing databases
@@ -176,6 +196,7 @@ class BaseMeta(ormar.ModelMeta):
# id = ormar.Integer(primary_key=True) # <= notice no field types
# name = ormar.String(max_length=100)
+
class Author(ormar.Model):
class Meta(BaseMeta):
tablename = "authors"
@@ -210,15 +231,9 @@ async def create():
# Create some records to work with through QuerySet.create method.
# Note that queryset is exposed on each Model's class as objects
tolkien = await Author.objects.create(name="J.R.R. Tolkien")
- await Book.objects.create(author=tolkien,
- title="The Hobbit",
- year=1937)
- await Book.objects.create(author=tolkien,
- title="The Lord of the Rings",
- year=1955)
- await Book.objects.create(author=tolkien,
- title="The Silmarillion",
- year=1977)
+ await Book.objects.create(author=tolkien, title="The Hobbit", year=1937)
+ await Book.objects.create(author=tolkien, title="The Lord of the Rings", year=1955)
+ await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
# alternative creation of object divided into 2 steps
sapkowski = Author(name="Andrzej Sapkowski")
@@ -317,9 +332,7 @@ async def delete():
# note that despite the fact that record no longer exists in database
# the object above is still accessible and you can use it (and i.e. save()) again.
tolkien = silmarillion.author
- await Book.objects.create(author=tolkien,
- title="The Silmarillion",
- year=1977)
+ await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
async def joins():
@@ -371,11 +384,17 @@ async def filter_and_sort():
# to sort decreasing use hyphen before the field name
# same as with filter you can use double underscores to access related fields
# Django style
- books = await Book.objects.filter(author__name__icontains="tolkien").order_by(
- "-year").all()
+ books = (
+ await Book.objects.filter(author__name__icontains="tolkien")
+ .order_by("-year")
+ .all()
+ )
# python style
- books = await Book.objects.filter(Book.author.name.icontains("tolkien")).order_by(
- Book.year.desc()).all()
+ books = (
+ await Book.objects.filter(Book.author.name.icontains("tolkien"))
+ .order_by(Book.year.desc())
+ .all()
+ )
assert len(books) == 3
assert books[0].title == "The Silmarillion"
assert books[2].title == "The Hobbit"
@@ -448,25 +467,68 @@ async def aggregations():
# count:
assert 2 == await Author.objects.count()
- # exists:
+ # exists
assert await Book.objects.filter(title="The Hobbit").exists()
- # max:
+ # maximum
assert 1990 == await Book.objects.max(columns=["year"])
- # min:
+ # minimum
assert 1937 == await Book.objects.min(columns=["year"])
- # avg:
+ # average
assert 1964.75 == await Book.objects.avg(columns=["year"])
- # sum:
+ # sum
assert 7859 == await Book.objects.sum(columns=["year"])
# to read more about aggregated functions
# visit: https://collerek.github.io/ormar/queries/aggregations/
-
+
+async def raw_data():
+ # extract raw data in a form of dicts or tuples
+ # note that this skips the validation(!) as models are
+ # not created from parsed data
+
+ # get list of objects as dicts
+ assert await Book.objects.values() == [
+ {"id": 1, "author": 1, "title": "The Hobbit", "year": 1937},
+ {"id": 2, "author": 1, "title": "The Lord of the Rings", "year": 1955},
+ {"id": 4, "author": 2, "title": "The Witcher", "year": 1990},
+ {"id": 5, "author": 1, "title": "The Silmarillion", "year": 1977},
+ ]
+
+ # get list of objects as tuples
+ assert await Book.objects.values_list() == [
+ (1, 1, "The Hobbit", 1937),
+ (2, 1, "The Lord of the Rings", 1955),
+ (4, 2, "The Witcher", 1990),
+ (5, 1, "The Silmarillion", 1977),
+ ]
+
+ # filter data - note how you always get a list
+ assert await Book.objects.filter(title="The Hobbit").values() == [
+ {"id": 1, "author": 1, "title": "The Hobbit", "year": 1937}
+ ]
+
+ # select only wanted fields
+ assert await Book.objects.filter(title="The Hobbit").values(["id", "title"]) == [
+ {"id": 1, "title": "The Hobbit"}
+ ]
+
+ # if you select only one column you could flatten it with values_list
+ assert await Book.objects.values_list("title", flatten=True) == [
+ "The Hobbit",
+ "The Lord of the Rings",
+ "The Witcher",
+ "The Silmarillion",
+ ]
+
+ # to read more about extracting raw values
+ # visit: https://collerek.github.io/ormar/queries/aggregations/
+
+
async def with_connect(function):
# note that for any other backend than sqlite you actually need to
# connect to the database to perform db operations
@@ -477,15 +539,25 @@ async def with_connect(function):
# in your endpoints but have a global connection pool
# check https://collerek.github.io/ormar/fastapi/ and section with db connection
+
# gather and execute all functions
# note - normally import should be at the beginning of the file
import asyncio
# note that normally you use gather() function to run several functions
# concurrently but we actually modify the data and we rely on the order of functions
-for func in [create, read, update, delete, joins,
- filter_and_sort, subset_of_columns,
- pagination, aggregations]:
+for func in [
+ create,
+ read,
+ update,
+ delete,
+ joins,
+ filter_and_sort,
+ subset_of_columns,
+ pagination,
+ aggregations,
+ raw_data,
+]:
print(f"Executing: {func.__name__}")
asyncio.run(with_connect(func))
@@ -523,6 +595,8 @@ metadata.drop_all(engine)
* `fields(columns: Union[List, str, set, dict]) -> QuerySet`
* `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet`
* `order_by(columns:Union[List, str]) -> QuerySet`
+* `values(fields: Union[List, str, Set, Dict])`
+* `values_list(fields: Union[List, str, Set, Dict])`
#### Relation types
@@ -584,6 +658,10 @@ Signals allow to trigger your function for a given event on a given Model.
* `post_update`
* `pre_delete`
* `post_delete`
+ * `pre_relation_add`
+ * `post_relation_add`
+ * `pre_relation_remove`
+ * `post_relation_remove`
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
diff --git a/docs/index.md b/docs/index.md
index fac2862..c215fe5 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -79,7 +79,27 @@ Ormar is built with:
As I write open-source code to solve everyday problems in my work or to promote and build strong python
community you can say thank you and buy me a coffee or sponsor me with a monthly amount to help ensure my work remains free and maintained.
-
+
+
+
### Migrating from `sqlalchemy` and existing databases
@@ -176,6 +196,7 @@ class BaseMeta(ormar.ModelMeta):
# id = ormar.Integer(primary_key=True) # <= notice no field types
# name = ormar.String(max_length=100)
+
class Author(ormar.Model):
class Meta(BaseMeta):
tablename = "authors"
@@ -210,15 +231,9 @@ async def create():
# Create some records to work with through QuerySet.create method.
# Note that queryset is exposed on each Model's class as objects
tolkien = await Author.objects.create(name="J.R.R. Tolkien")
- await Book.objects.create(author=tolkien,
- title="The Hobbit",
- year=1937)
- await Book.objects.create(author=tolkien,
- title="The Lord of the Rings",
- year=1955)
- await Book.objects.create(author=tolkien,
- title="The Silmarillion",
- year=1977)
+ await Book.objects.create(author=tolkien, title="The Hobbit", year=1937)
+ await Book.objects.create(author=tolkien, title="The Lord of the Rings", year=1955)
+ await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
# alternative creation of object divided into 2 steps
sapkowski = Author(name="Andrzej Sapkowski")
@@ -317,27 +332,43 @@ async def delete():
# note that despite the fact that record no longer exists in database
# the object above is still accessible and you can use it (and i.e. save()) again.
tolkien = silmarillion.author
- await Book.objects.create(author=tolkien,
- title="The Silmarillion",
- year=1977)
+ await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
async def joins():
# Tho join two models use select_related
+
+ # Django style
book = await Book.objects.select_related("author").get(title="The Hobbit")
+ # Python style
+ book = await Book.objects.select_related(Book.author).get(
+ Book.title == "The Hobbit"
+ )
+
# now the author is already prefetched
assert book.author.name == "J.R.R. Tolkien"
# By default you also get a second side of the relation
# constructed as lowercase source model name +'s' (books in this case)
# you can also provide custom name with parameter related_name
+
+ # Django style
author = await Author.objects.select_related("books").all(name="J.R.R. Tolkien")
+ # Python style
+ author = await Author.objects.select_related(Author.books).all(
+ Author.name == "J.R.R. Tolkien"
+ )
assert len(author[0].books) == 3
# for reverse and many to many relations you can also prefetch_related
# that executes a separate query for each of related models
+ # Django style
author = await Author.objects.prefetch_related("books").get(name="J.R.R. Tolkien")
+ # Python style
+ author = await Author.objects.prefetch_related(Author.books).get(
+ Author.name == "J.R.R. Tolkien"
+ )
assert len(author.books) == 3
# to read more about relations
@@ -371,11 +402,17 @@ async def filter_and_sort():
# to sort decreasing use hyphen before the field name
# same as with filter you can use double underscores to access related fields
# Django style
- books = await Book.objects.filter(author__name__icontains="tolkien").order_by(
- "-year").all()
+ books = (
+ await Book.objects.filter(author__name__icontains="tolkien")
+ .order_by("-year")
+ .all()
+ )
# python style
- books = await Book.objects.filter(Book.author.name.icontains("tolkien")).order_by(
- Book.year.desc()).all()
+ books = (
+ await Book.objects.filter(Book.author.name.icontains("tolkien"))
+ .order_by(Book.year.desc())
+ .all()
+ )
assert len(books) == 3
assert books[0].title == "The Silmarillion"
assert books[2].title == "The Hobbit"
@@ -448,25 +485,68 @@ async def aggregations():
# count:
assert 2 == await Author.objects.count()
- # exists:
+ # exists
assert await Book.objects.filter(title="The Hobbit").exists()
- # max:
+ # maximum
assert 1990 == await Book.objects.max(columns=["year"])
- # min:
+ # minimum
assert 1937 == await Book.objects.min(columns=["year"])
- # avg:
+ # average
assert 1964.75 == await Book.objects.avg(columns=["year"])
- # sum:
+ # sum
assert 7859 == await Book.objects.sum(columns=["year"])
# to read more about aggregated functions
# visit: https://collerek.github.io/ormar/queries/aggregations/
-
+
+async def raw_data():
+ # extract raw data in a form of dicts or tuples
+ # note that this skips the validation(!) as models are
+ # not created from parsed data
+
+ # get list of objects as dicts
+ assert await Book.objects.values() == [
+ {"id": 1, "author": 1, "title": "The Hobbit", "year": 1937},
+ {"id": 2, "author": 1, "title": "The Lord of the Rings", "year": 1955},
+ {"id": 4, "author": 2, "title": "The Witcher", "year": 1990},
+ {"id": 5, "author": 1, "title": "The Silmarillion", "year": 1977},
+ ]
+
+ # get list of objects as tuples
+ assert await Book.objects.values_list() == [
+ (1, 1, "The Hobbit", 1937),
+ (2, 1, "The Lord of the Rings", 1955),
+ (4, 2, "The Witcher", 1990),
+ (5, 1, "The Silmarillion", 1977),
+ ]
+
+ # filter data - note how you always get a list
+ assert await Book.objects.filter(title="The Hobbit").values() == [
+ {"id": 1, "author": 1, "title": "The Hobbit", "year": 1937}
+ ]
+
+ # select only wanted fields
+ assert await Book.objects.filter(title="The Hobbit").values(["id", "title"]) == [
+ {"id": 1, "title": "The Hobbit"}
+ ]
+
+ # if you select only one column you could flatten it with values_list
+ assert await Book.objects.values_list("title", flatten=True) == [
+ "The Hobbit",
+ "The Lord of the Rings",
+ "The Witcher",
+ "The Silmarillion",
+ ]
+
+ # to read more about extracting raw values
+ # visit: https://collerek.github.io/ormar/queries/aggregations/
+
+
async def with_connect(function):
# note that for any other backend than sqlite you actually need to
# connect to the database to perform db operations
@@ -477,15 +557,25 @@ async def with_connect(function):
# in your endpoints but have a global connection pool
# check https://collerek.github.io/ormar/fastapi/ and section with db connection
+
# gather and execute all functions
# note - normally import should be at the beginning of the file
import asyncio
# note that normally you use gather() function to run several functions
# concurrently but we actually modify the data and we rely on the order of functions
-for func in [create, read, update, delete, joins,
- filter_and_sort, subset_of_columns,
- pagination, aggregations]:
+for func in [
+ create,
+ read,
+ update,
+ delete,
+ joins,
+ filter_and_sort,
+ subset_of_columns,
+ pagination,
+ aggregations,
+ raw_data,
+]:
print(f"Executing: {func.__name__}")
asyncio.run(with_connect(func))
@@ -523,6 +613,8 @@ metadata.drop_all(engine)
* `fields(columns: Union[List, str, set, dict]) -> QuerySet`
* `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet`
* `order_by(columns:Union[List, str]) -> QuerySet`
+* `values(fields: Union[List, str, Set, Dict])`
+* `values_list(fields: Union[List, str, Set, Dict])`
#### Relation types
@@ -584,6 +676,10 @@ Signals allow to trigger your function for a given event on a given Model.
* `post_update`
* `pre_delete`
* `post_delete`
+ * `pre_relation_add`
+ * `post_relation_add`
+ * `pre_relation_remove`
+ * `post_relation_remove`
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
diff --git a/docs/queries/joins-and-subqueries.md b/docs/queries/joins-and-subqueries.md
index fc26518..cf7fef7 100644
--- a/docs/queries/joins-and-subqueries.md
+++ b/docs/queries/joins-and-subqueries.md
@@ -69,11 +69,16 @@ class Track(ormar.Model):
```
```python
+# Django style
album = await Album.objects.select_related("tracks").all()
+
+# Python style
+album = await Album.objects.select_related(Album.tracks).all()
+
# will return album will all columns tracks
```
-You can provide a string or a list of strings
+You can provide a string or a list of strings (or a field/ list of fields)
```python
class SchoolClass(ormar.Model):
@@ -122,8 +127,14 @@ class Teacher(ormar.Model):
```
```python
+# Django style
classes = await SchoolClass.objects.select_related(
["teachers__category", "students"]).all()
+
+# Python style
+classes = await SchoolClass.objects.select_related(
+ [SchoolClass.teachers.category, SchoolClass.students]).all()
+
# will return classes with teachers and teachers categories
# as well as classes students
```
@@ -276,7 +287,13 @@ class Track(ormar.Model):
```
```python
+# Django style
album = await Album.objects.prefetch_related("tracks").all()
+
+# Python style
+album = await Album.objects.prefetch_related(Album.tracks).all()
+
+
# will return album will all columns tracks
```
@@ -329,8 +346,13 @@ class Teacher(ormar.Model):
```
```python
+# Django style
classes = await SchoolClass.objects.prefetch_related(
["teachers__category", "students"]).all()
+
+# Python style
+classes = await SchoolClass.objects.prefetch_related(
+ [SchoolClass.teachers.category, SchoolClass.students]).all()
# will return classes with teachers and teachers categories
# as well as classes students
```
diff --git a/docs/releases.md b/docs/releases.md
index bdf2453..30185c2 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -1,3 +1,23 @@
+# 0.10.13
+
+## ✨ Features
+
+* Allow passing field accessors in `select_related` and `prefetch_related` aka. python style `select_related` [#225](https://github.com/collerek/ormar/issues/225).
+ * Previously:
+ ```python
+ await Post.objects.select_related(["author", "categories"]).get()
+ await Author.objects.prefetch_related("posts__categories").get()
+ ```
+ * Now also:
+ ```python
+ await Post.objects.select_related([Post.author, Post.categories]).get()
+ await Author.objects.prefetch_related(Author.posts.categories).get()
+ ```
+
+## 🐛 Fixes
+
+* Fix overwriting default value for inherited primary key [#253](https://github.com/collerek/ormar/issues/253)
+
# 0.10.12
## 🐛 Fixes
diff --git a/examples/script_from_readme.py b/examples/script_from_readme.py
index cb1083d..95041c4 100644
--- a/examples/script_from_readme.py
+++ b/examples/script_from_readme.py
@@ -169,20 +169,37 @@ async def delete():
async def joins():
# Tho join two models use select_related
+
+ # Django style
book = await Book.objects.select_related("author").get(title="The Hobbit")
+ # Python style
+ book = await Book.objects.select_related(Book.author).get(
+ Book.title == "The Hobbit"
+ )
+
# now the author is already prefetched
assert book.author.name == "J.R.R. Tolkien"
# By default you also get a second side of the relation
# constructed as lowercase source model name +'s' (books in this case)
# you can also provide custom name with parameter related_name
+ # Django style
author = await Author.objects.select_related("books").all(name="J.R.R. Tolkien")
+ # Python style
+ author = await Author.objects.select_related(Author.books).all(
+ Author.name == "J.R.R. Tolkien"
+ )
assert len(author[0].books) == 3
# for reverse and many to many relations you can also prefetch_related
# that executes a separate query for each of related models
+ # Django style
author = await Author.objects.prefetch_related("books").get(name="J.R.R. Tolkien")
+ # Python style
+ author = await Author.objects.prefetch_related(Author.books).get(
+ Author.name == "J.R.R. Tolkien"
+ )
assert len(author.books) == 3
# to read more about relations
@@ -302,13 +319,13 @@ async def aggregations():
# exists
assert await Book.objects.filter(title="The Hobbit").exists()
- # max
+ # maximum
assert 1990 == await Book.objects.max(columns=["year"])
- # min
+ # minimum
assert 1937 == await Book.objects.min(columns=["year"])
- # avg
+ # average
assert 1964.75 == await Book.objects.avg(columns=["year"])
# sum
@@ -318,6 +335,49 @@ async def aggregations():
# visit: https://collerek.github.io/ormar/queries/aggregations/
+async def raw_data():
+ # extract raw data in a form of dicts or tuples
+ # note that this skips the validation(!) as models are
+ # not created from parsed data
+
+ # get list of objects as dicts
+ assert await Book.objects.values() == [
+ {"id": 1, "author": 1, "title": "The Hobbit", "year": 1937},
+ {"id": 2, "author": 1, "title": "The Lord of the Rings", "year": 1955},
+ {"id": 4, "author": 2, "title": "The Witcher", "year": 1990},
+ {"id": 5, "author": 1, "title": "The Silmarillion", "year": 1977},
+ ]
+
+ # get list of objects as tuples
+ assert await Book.objects.values_list() == [
+ (1, 1, "The Hobbit", 1937),
+ (2, 1, "The Lord of the Rings", 1955),
+ (4, 2, "The Witcher", 1990),
+ (5, 1, "The Silmarillion", 1977),
+ ]
+
+ # filter data - note how you always get a list
+ assert await Book.objects.filter(title="The Hobbit").values() == [
+ {"id": 1, "author": 1, "title": "The Hobbit", "year": 1937}
+ ]
+
+ # select only wanted fields
+ assert await Book.objects.filter(title="The Hobbit").values(["id", "title"]) == [
+ {"id": 1, "title": "The Hobbit"}
+ ]
+
+ # if you select only one column you could flatten it with values_list
+ assert await Book.objects.values_list("title", flatten=True) == [
+ "The Hobbit",
+ "The Lord of the Rings",
+ "The Witcher",
+ "The Silmarillion",
+ ]
+
+ # to read more about extracting raw values
+ # visit: https://collerek.github.io/ormar/queries/aggregations/
+
+
async def with_connect(function):
# note that for any other backend than sqlite you actually need to
# connect to the database to perform db operations
@@ -345,6 +405,7 @@ for func in [
subset_of_columns,
pagination,
aggregations,
+ raw_data,
]:
print(f"Executing: {func.__name__}")
asyncio.run(with_connect(func))
diff --git a/ormar/__init__.py b/ormar/__init__.py
index ca414c8..8b86efb 100644
--- a/ormar/__init__.py
+++ b/ormar/__init__.py
@@ -76,7 +76,7 @@ class UndefinedType: # pragma no cover
Undefined = UndefinedType()
-__version__ = "0.10.12"
+__version__ = "0.10.13"
__all__ = [
"Integer",
"BigInteger",
diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py
index c0d752f..88f5681 100644
--- a/ormar/models/metaclass.py
+++ b/ormar/models/metaclass.py
@@ -584,7 +584,11 @@ class ModelMetaclass(pydantic.main.ModelMetaclass):
register_relation_in_alias_manager(field=field)
add_field_descriptor(name=name, field=field, new_model=new_model)
- if new_model.Meta.pkname not in attrs["__annotations__"]:
+ if (
+ new_model.Meta.pkname
+ and new_model.Meta.pkname not in attrs["__annotations__"]
+ and new_model.Meta.pkname not in new_model.__fields__
+ ):
field_name = new_model.Meta.pkname
attrs["__annotations__"][field_name] = Optional[int] # type: ignore
attrs[field_name] = None
diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py
index ef4b434..5c93173 100644
--- a/ormar/queryset/queryset.py
+++ b/ormar/queryset/queryset.py
@@ -21,7 +21,7 @@ from sqlalchemy import bindparam
import ormar # noqa I100
from ormar import MultipleMatches, NoMatch
from ormar.exceptions import ModelPersistenceError, QueryDefinitionError
-from ormar.queryset import FilterQuery, SelectAction
+from ormar.queryset import FieldAccessor, FilterQuery, SelectAction
from ormar.queryset.actions.order_action import OrderAction
from ormar.queryset.clause import FilterGroup, QueryClause
from ormar.queryset.prefetch_query import PrefetchQuery
@@ -353,7 +353,7 @@ class QuerySet(Generic[T]):
"""
return self.filter(_exclude=True, *args, **kwargs)
- def select_related(self, related: Union[List, str]) -> "QuerySet[T]":
+ def select_related(self, related: Union[List, str, FieldAccessor]) -> "QuerySet[T]":
"""
Allows to prefetch related models during the same query.
@@ -372,6 +372,10 @@ class QuerySet(Generic[T]):
"""
if not isinstance(related, list):
related = [related]
+ related = [
+ rel._access_chain if isinstance(rel, FieldAccessor) else rel
+ for rel in related
+ ]
related = sorted(list(set(list(self._select_related) + related)))
return self.rebuild_self(select_related=related,)
@@ -402,7 +406,9 @@ class QuerySet(Generic[T]):
relations = self.model._iterate_related_models()
return self.rebuild_self(select_related=relations,)
- def prefetch_related(self, related: Union[List, str]) -> "QuerySet[T]":
+ def prefetch_related(
+ self, related: Union[List, str, FieldAccessor]
+ ) -> "QuerySet[T]":
"""
Allows to prefetch related models during query - but opposite to
`select_related` each subsequent model is fetched in a separate database query.
@@ -422,6 +428,10 @@ class QuerySet(Generic[T]):
"""
if not isinstance(related, list):
related = [related]
+ related = [
+ rel._access_chain if isinstance(rel, FieldAccessor) else rel
+ for rel in related
+ ]
related = list(set(list(self._prefetch_related) + related))
return self.rebuild_self(prefetch_related=related,)
diff --git a/tests/test_inheritance_and_pydantic_generation/test_inheritance_with_default.py b/tests/test_inheritance_and_pydantic_generation/test_inheritance_with_default.py
new file mode 100644
index 0000000..1c61e41
--- /dev/null
+++ b/tests/test_inheritance_and_pydantic_generation/test_inheritance_with_default.py
@@ -0,0 +1,64 @@
+import datetime
+import uuid
+
+import databases
+import pytest
+import sqlalchemy
+
+import ormar
+from tests.settings import DATABASE_URL
+
+metadata = sqlalchemy.MetaData()
+database = databases.Database(DATABASE_URL)
+
+
+class BaseMeta(ormar.ModelMeta):
+ database = database
+ metadata = metadata
+
+
+class BaseModel(ormar.Model):
+ class Meta(ormar.ModelMeta):
+ abstract = True
+
+ id: uuid.UUID = ormar.UUID(
+ primary_key=True, default=uuid.uuid4, uuid_format="string"
+ )
+ created_at: datetime.datetime = ormar.DateTime(default=datetime.datetime.utcnow())
+ updated_at: datetime.datetime = ormar.DateTime(default=datetime.datetime.utcnow())
+
+
+class Member(BaseModel):
+ class Meta(BaseMeta):
+ tablename = "members"
+
+ first_name: str = ormar.String(max_length=50)
+ last_name: str = ormar.String(max_length=50)
+
+
+@pytest.fixture(autouse=True, scope="module")
+def create_test_database():
+ engine = sqlalchemy.create_engine(DATABASE_URL)
+ metadata.drop_all(engine)
+ metadata.create_all(engine)
+ yield
+ metadata.drop_all(engine)
+
+
+def test_model_structure():
+ assert "id" in BaseModel.__fields__
+ assert "id" in BaseModel.Meta.model_fields
+ assert BaseModel.Meta.model_fields["id"].has_default()
+ assert BaseModel.__fields__["id"].default_factory is not None
+
+ assert "id" in Member.__fields__
+ assert "id" in Member.Meta.model_fields
+ assert Member.Meta.model_fields["id"].has_default()
+ assert Member.__fields__["id"].default_factory is not None
+
+
+@pytest.mark.asyncio
+async def test_fields_inherited_with_default():
+ async with database:
+ await Member(first_name="foo", last_name="bar").save()
+ await Member.objects.create(first_name="foo", last_name="bar")
diff --git a/tests/test_relations/test_python_style_relations.py b/tests/test_relations/test_python_style_relations.py
new file mode 100644
index 0000000..61824f2
--- /dev/null
+++ b/tests/test_relations/test_python_style_relations.py
@@ -0,0 +1,100 @@
+from typing import List, Optional
+
+import databases
+import pytest
+import sqlalchemy
+
+import ormar
+from tests.settings import DATABASE_URL
+
+database = databases.Database(DATABASE_URL, force_rollback=True)
+metadata = sqlalchemy.MetaData()
+
+
+class Author(ormar.Model):
+ class Meta:
+ tablename = "authors"
+ database = database
+ metadata = metadata
+
+ id: int = ormar.Integer(primary_key=True)
+ first_name: str = ormar.String(max_length=80)
+ last_name: str = ormar.String(max_length=80)
+
+
+class Category(ormar.Model):
+ class Meta:
+ tablename = "categories"
+ database = database
+ metadata = metadata
+
+ id: int = ormar.Integer(primary_key=True)
+ name: str = ormar.String(max_length=40)
+
+
+class Post(ormar.Model):
+ class Meta:
+ tablename = "posts"
+ database = database
+ metadata = metadata
+
+ id: int = ormar.Integer(primary_key=True)
+ title: str = ormar.String(max_length=200)
+ categories: Optional[List[Category]] = ormar.ManyToMany(Category)
+ author: Optional[Author] = ormar.ForeignKey(Author, related_name="author_posts")
+
+
+@pytest.fixture(autouse=True, scope="module")
+def create_test_database():
+ engine = sqlalchemy.create_engine(DATABASE_URL)
+ metadata.create_all(engine)
+ yield
+ metadata.drop_all(engine)
+
+
+@pytest.fixture(scope="function")
+async def cleanup():
+ yield
+ async with database:
+ PostCategory = Post.Meta.model_fields["categories"].through
+ await PostCategory.objects.delete(each=True)
+ await Post.objects.delete(each=True)
+ await Category.objects.delete(each=True)
+ await Author.objects.delete(each=True)
+
+
+@pytest.mark.asyncio
+async def test_selecting_related(cleanup):
+ async with database:
+ guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum")
+ post = await Post.objects.create(title="Hello, M2M", author=guido)
+ news = await Category.objects.create(name="News")
+ recent = await Category.objects.create(name="Recent")
+ await post.categories.add(news)
+ await post.categories.add(recent)
+ assert len(await post.categories.all()) == 2
+ # Loads categories and posts (2 queries) and perform the join in Python.
+ categories = await Category.objects.select_related(Category.posts).all()
+ assert len(categories) == 2
+ assert categories[0].name == "News"
+
+ news_posts = await news.posts.select_related(Post.author).all()
+ assert news_posts[0].author == guido
+ assert (await post.categories.limit(1).all())[0] == news
+ assert (await post.categories.offset(1).limit(1).all())[0] == recent
+ assert await post.categories.first() == news
+ assert await post.categories.exists()
+
+ author = await Author.objects.prefetch_related(
+ Author.author_posts.categories
+ ).get()
+ assert len(author.author_posts) == 1
+ assert author.author_posts[0].title == "Hello, M2M"
+ assert author.author_posts[0].categories[0].name == "News"
+ assert author.author_posts[0].categories[1].name == "Recent"
+
+ post = await Post.objects.select_related([Post.author, Post.categories]).get()
+ assert len(post.categories) == 2
+ assert post.categories[0].name == "News"
+ assert post.categories[1].name == "Recent"
+ assert post.author.first_name == "Guido"