Skip to content

Commit 7fc4d68

Browse files
committed
feat: method added
1 parent 815b341 commit 7fc4d68

10 files changed

Lines changed: 207 additions & 43 deletions

File tree

example/config-app/uv.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/database-app/uv.lock

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/connection.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,7 @@ async def insert(self, query: str, bindings: list | None = None) -> int | None:
104104

105105
async def insert_get_id(self, query: str, bindings: list | None = None) -> int | None:
106106
result = await self.execute(query, bindings)
107-
last_insert_id = getattr(result, "lastrowid", None)
108-
if not last_insert_id:
109-
row = result.fetchone()
110-
last_insert_id = row[0] if row else None
111-
return last_insert_id
107+
return getattr(result, "lastrowid", None)
112108

113109
async def update(self, query: str, bindings: list | None = None) -> int:
114110
result = await self.execute(query, bindings)

fastapi_startkit/src/fastapi_startkit/masoniteorm/connections/postgres_connection.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
class PostgresConnection(Connection):
88
"""Async PostgreSQL connection backed by asyncpg via SQLAlchemy."""
99

10+
async def insert_get_id(self, query: str, bindings: list | None = None) -> int | None:
11+
result = await self.run(query, bindings)
12+
row = result.fetchone()
13+
if not self.transactions:
14+
conn = await self.get_connection()
15+
await conn.commit()
16+
return row[0] if row is not None else None
17+
1018
@classmethod
1119
def get_query_grammar(cls):
1220
return PostgresGrammar

fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -218,26 +218,27 @@ def distinct(self) -> "QueryBuilder":
218218
self._distinct = True
219219
return self
220220

221-
def aggregate(self, aggregate_type: str, column: str, alias: str = None) -> "QueryBuilder":
222-
if alias:
223-
column = f"{column} as {alias}"
224-
self._aggregates += (AggregateExpression(aggregate_type, column),)
225-
return self
221+
async def aggregate(self, function: str, column: str):
222+
self._aggregates += (AggregateExpression(function, column),)
223+
row = await self.connection.select_one(self.to_qmark(), self.get_bindings())
224+
if row is None:
225+
return None
226+
return next(iter(row.values()))
226227

227-
def count(self, column: str = "*") -> "QueryBuilder":
228-
return self.aggregate("COUNT", column)
228+
async def count(self, column: str = "*"):
229+
return await self.aggregate("COUNT", column)
229230

230-
def sum(self, column: str) -> "QueryBuilder":
231-
return self.aggregate("SUM", column)
231+
async def sum(self, column: str):
232+
return await self.aggregate("SUM", column)
232233

233-
def max(self, column: str) -> "QueryBuilder":
234-
return self.aggregate("MAX", column)
234+
async def max(self, column: str):
235+
return await self.aggregate("MAX", column)
235236

236-
def min(self, column: str) -> "QueryBuilder":
237-
return self.aggregate("MIN", column)
237+
async def min(self, column: str):
238+
return await self.aggregate("MIN", column)
238239

239-
def avg(self, column: str) -> "QueryBuilder":
240-
return self.aggregate("AVG", column)
240+
async def avg(self, column: str):
241+
return await self.aggregate("AVG", column)
241242

242243
async def delete(self, column=None, value=None):
243244
if column is not None:
@@ -259,6 +260,15 @@ async def first_or_create(self, search: dict, attributes: dict | None = None):
259260

260261
return await self.create({**(attributes or {}), **search})
261262

263+
async def update_or_create(self, search: dict, attributes: dict | None = None):
264+
instance = await self.where(search).first()
265+
if instance is not None:
266+
if attributes:
267+
await instance.update(attributes)
268+
return instance
269+
270+
return await self.create({**(attributes or {}), **search})
271+
262272
async def insert(self, values: dict | list) -> int | None:
263273
self.set_action("bulk_create")
264274

@@ -302,9 +312,7 @@ async def paginate(self, per_page: int = 15, page: int = 1):
302312
count_builder._wheres = list(self._wheres)
303313
count_builder._joins = self._joins
304314
count_builder._global_scopes = self._global_scopes
305-
count_builder.count()
306-
count_result = await self.connection.select(count_builder.to_qmark(), count_builder.get_bindings())
307-
total = list(count_result[0].values())[0] if count_result else 0
315+
total = await count_builder.count() or 0
308316

309317
offset = (page - 1) * per_page
310318
results = await self.limit(per_page).offset(offset).get()

fastapi_startkit/src/fastapi_startkit/masoniteorm/models/model.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ def on(cls, connection: str):
119119
async def all(cls):
120120
return await cls.query().get()
121121

122+
@classmethod
123+
async def count(cls, column: str = "*"):
124+
return await cls.query().count(column)
125+
122126
def set_connection(self, connection: str):
123127
self.connection = connection
124128

@@ -175,6 +179,12 @@ async def first_or_create(
175179
) -> "Model":
176180
return await cls.query().first_or_create(search, attributes)
177181

182+
@classmethod
183+
async def update_or_create(
184+
cls, search: dict, attributes: dict | None = None
185+
) -> "Model":
186+
return await cls.query().update_or_create(search, attributes)
187+
178188
@classmethod
179189
async def create(cls, attributes: dict):
180190
instance = cls().new_model_instance(attributes)

fastapi_startkit/src/fastapi_startkit/masoniteorm/query/grammars/PostgresGrammar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def bulk_insert_format(self):
5353
return f"INSERT INTO {{table}} ({{columns}}) VALUES {{values}} RETURNING {self._returning}"
5454

5555
def delete_format(self):
56-
return "DELETE FROM {TABLE} {wheres}"
56+
return "DELETE FROM {table} {wheres}"
5757

5858
def aggregate_string_with_alias(self):
5959
return "{aggregate_function}({column}) AS {alias}"
Lines changed: 125 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,135 @@
1-
from unittest import IsolatedAsyncioTestCase
2-
3-
from fastapi_startkit.masoniteorm.models.model import Model
4-
from fastapi_startkit.masoniteorm.testing.transaction import RefreshDatabase
5-
from ..fixtures.db import DB, schema
6-
from ...fixtures.migration import migrate, wipe
1+
from ..test_case import TestCase
72
from ...fixtures.model import User
83

94

10-
class TestPostGresModel(RefreshDatabase, IsolatedAsyncioTestCase):
11-
async def asyncSetUp(self):
12-
Model.db_manager = DB
13-
await DB.clear()
14-
await wipe(schema)
15-
await migrate(schema)
16-
17-
async def asyncTearDown(self):
18-
await wipe(schema)
19-
await DB.clear()
20-
5+
class TestPostGresModel(TestCase):
216
async def test_can_create_and_find_user(self):
227
user = await User.create({"name": "Alice", "email": "alice@example.com", "is_admin": False})
238
self.assertIsNotNone(user.id)
249

2510
found = await User.find(user.id)
2611
self.assertEqual(found.name, "Alice")
2712
self.assertEqual(found.email, "alice@example.com")
13+
14+
async def test_find_returns_none_for_missing_id(self):
15+
found = await User.find(99999)
16+
self.assertIsNone(found)
17+
18+
async def test_first_returns_first_record(self):
19+
await User.create({"name": "Bob", "email": "bob@example.com", "is_admin": False})
20+
await User.create({"name": "Carol", "email": "carol@example.com", "is_admin": True})
21+
22+
user = await User.first()
23+
self.assertIsNotNone(user)
24+
self.assertEqual(user.name, "Bob")
25+
26+
async def test_first_returns_none_when_table_is_empty(self):
27+
user = await User.first()
28+
self.assertIsNone(user)
29+
30+
async def test_update_changes_attributes(self):
31+
user = await User.create({"name": "Dave", "email": "dave@example.com", "is_admin": False})
32+
33+
await user.update({"name": "David"})
34+
35+
refreshed = await User.find(user.id)
36+
self.assertEqual(refreshed.name, "David")
37+
self.assertEqual(refreshed.email, "dave@example.com")
38+
39+
async def test_update_only_dirty_fields(self):
40+
user = await User.create({"name": "Eve", "email": "eve@example.com", "is_admin": False})
41+
42+
await user.update({"name": "Eve", "is_admin": True})
43+
44+
refreshed = await User.find(user.id)
45+
self.assertTrue(refreshed.is_admin)
46+
self.assertEqual(refreshed.name, "Eve")
47+
48+
async def test_delete_removes_record(self):
49+
user = await User.create({"name": "Frank", "email": "frank@example.com", "is_admin": False})
50+
user_id = user.id
51+
52+
await User.where("id", user_id).delete()
53+
54+
found = await User.find(user_id)
55+
self.assertIsNone(found)
56+
57+
async def test_delete_by_column_removes_matching_records(self):
58+
await User.create({"name": "Grace", "email": "grace@example.com", "is_admin": False})
59+
await User.create({"name": "Heidi", "email": "heidi@example.com", "is_admin": True})
60+
61+
await User.query().delete("is_admin", True)
62+
63+
admin = await User.where("is_admin", True).first()
64+
self.assertIsNone(admin)
65+
66+
non_admin = await User.where("is_admin", False).first()
67+
self.assertIsNotNone(non_admin)
68+
69+
async def test_where_filters_results(self):
70+
await User.create({"name": "Ivan", "email": "ivan@example.com", "is_admin": False})
71+
await User.create({"name": "Judy", "email": "judy@example.com", "is_admin": True})
72+
73+
admins = await User.where("is_admin", True).get()
74+
self.assertEqual(len(admins), 1)
75+
self.assertEqual(admins[0].name, "Judy")
76+
77+
async def test_first_or_create_creates_when_not_found(self):
78+
user = await User.first_or_create(
79+
{"email": "newuser@example.com"},
80+
{"name": "New User", "is_admin": False},
81+
)
82+
self.assertIsNotNone(user.id)
83+
self.assertEqual(user.email, "newuser@example.com")
84+
self.assertEqual(user.name, "New User")
85+
86+
async def test_first_or_create_returns_existing_when_found(self):
87+
existing = await User.create({"name": "Existing", "email": "existing@example.com", "is_admin": False})
88+
89+
user = await User.first_or_create(
90+
{"email": "existing@example.com"},
91+
{"name": "Should Not Be Created", "is_admin": True},
92+
)
93+
self.assertEqual(user.id, existing.id)
94+
self.assertEqual(user.name, "Existing")
95+
96+
# Confirm no duplicate was inserted
97+
all_users = await User.where("email", "existing@example.com").get()
98+
self.assertEqual(len(all_users), 1)
99+
100+
async def test_all_returns_all_records(self):
101+
await User.create({"name": "Karl", "email": "karl@example.com", "is_admin": False})
102+
await User.create({"name": "Laura", "email": "laura@example.com", "is_admin": False})
103+
104+
users = await User.all()
105+
self.assertEqual(len(users), 2)
106+
107+
async def test_update_or_create_creates_when_not_found(self):
108+
user = await User.update_or_create(
109+
{"email": "new@example.com"},
110+
{"name": "New User", "is_admin": False},
111+
)
112+
self.assertIsNotNone(user.id)
113+
self.assertEqual(user.email, "new@example.com")
114+
self.assertEqual(user.name, "New User")
115+
116+
async def test_update_or_create_updates_when_found(self):
117+
await User.create({"name": "Original", "email": "update@example.com", "is_admin": False})
118+
119+
user = await User.update_or_create(
120+
{"email": "update@example.com"},
121+
{"name": "Updated", "is_admin": True},
122+
)
123+
self.assertEqual(user.name, "Updated")
124+
self.assertTrue(user.is_admin)
125+
126+
# Confirm no duplicate was inserted
127+
count = await User.where("email", "update@example.com").count()
128+
self.assertEqual(count, 1)
129+
130+
async def test_count_returns_correct_number(self):
131+
await User.create({"name": "Mallory", "email": "mallory@example.com", "is_admin": False})
132+
await User.create({"name": "Niaj", "email": "niaj@example.com", "is_admin": False})
133+
134+
count = await User.count()
135+
self.assertEqual(count, 2)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from unittest import IsolatedAsyncioTestCase
2+
3+
from fastapi_startkit.masoniteorm import Model
4+
from fastapi_startkit.masoniteorm.testing.transaction import RefreshDatabase
5+
from .fixtures.db import DB, schema
6+
from ..fixtures.migration import migrate, wipe
7+
8+
9+
class TestCase(RefreshDatabase, IsolatedAsyncioTestCase):
10+
async def asyncSetUp(self):
11+
Model.db_manager = DB
12+
await DB.clear()
13+
await wipe(schema)
14+
await migrate(schema)
15+
16+
async def asyncTearDown(self):
17+
await wipe(schema)
18+
await DB.clear()

fastapi_startkit/tests/masoniteorm/sqlite/test_case.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from unittest import IsolatedAsyncioTestCase
22

33
from fastapi_startkit.masoniteorm.testing.transaction import RefreshDatabase
4+
from fastapi_startkit.masoniteorm import Model
45

56
from .fixtures.db import DB
67
from ..fixtures.migration import migrate, wipe
@@ -10,6 +11,7 @@
1011
class TestCase(RefreshDatabase, IsolatedAsyncioTestCase):
1112
async def asyncSetUp(self):
1213
self.db = DB
14+
Model.db_manager = DB
1315
self.schema = DB.get_schema_builder()
1416
await self.migrate_database()
1517

0 commit comments

Comments
 (0)