Skip to content

Commit 87d0f55

Browse files
authored
Merge pull request #50 from seapagan/fix-missing-commits
2 parents ffde9e9 + 9afc8e4 commit 87d0f55

File tree

6 files changed

+279
-6
lines changed

6 files changed

+279
-6
lines changed

TODO.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ Items marked with :fire: are high priority.
44

55
## General Plans and Ideas
66

7-
- :fire: add (optional) `created_at` and `updated_at` fields to the BaseDBModel class
8-
which will be automatically updated when a record is created or updated.
97
- add an 'execute' method to the main class to allow executing arbitrary SQL
108
queries which can be chained to the 'find_first' etc methods or just used
119
directly.
@@ -22,6 +20,12 @@ Items marked with :fire: are high priority.
2220
- :fire: support structures like, `list`, `dict`, `set` etc. in the model. These will
2321
need to be `pickled` first then stored as a BLOB in the database . Also
2422
support `date` which can be stored as a Unix timestamp in an integer field.
23+
- on update, check if the model has actually changed before sending the update
24+
to the database. This will prevent unnecessary updates and leave the
25+
`updated_at` correct. However, this will always require a query to the
26+
database to check the current values and so in large batch updates this could
27+
have a considerable performance impact. Probably best to gate this behind a
28+
flag.
2529

2630
## Housekeeping
2731

docs/guide/data-ops.md renamed to docs/guide/data-operations.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,31 @@ print(f"New record inserted with primary key: {result.pk}")
2020
print(f"Name: {result.name}, Age: {result.age}, Email: {result.email}")
2121
```
2222

23+
### Overriding the Timestamps
24+
25+
By default, SQLiter will automatically set the `created_at` and `updated_at`
26+
fields to the current Unix timestamp in UTC when a record is inserted. If you
27+
want to override this behavior, you can set the `created_at` and `updated_at`
28+
fields manually before calling `insert()`:
29+
30+
```python
31+
import time
32+
33+
user.created_at = int(time.time())
34+
user.updated_at = int(time.time())
35+
```
36+
37+
However, by default **this is disabled**. Any model passed to `insert()` will
38+
have the `created_at` and `updated_at` fields set automatically and ignore any
39+
values passed in these 2 fields.
40+
41+
If you want to enable this feature, you can set the `timestamp_override` flag to `True`
42+
when inserting the record:
43+
44+
```python
45+
result = db.insert(user, timestamp_override=True)
46+
```
47+
2348
> [!IMPORTANT]
2449
>
2550
> The `insert()` method will raise a `RecordInsertionError` if you try to insert
@@ -74,6 +99,16 @@ db.update(user)
7499
> You can also set the primary key value on the model instance manually before
75100
> calling `update()` if you have that.
76101
102+
On suffescul update, the `updated_at` field will be set to the current Unix
103+
timestamp in UTC by default.
104+
105+
> [!WARNING]
106+
>
107+
> Unlike with the `insert()` method, you **CANNOT** override the `updated_at`
108+
> field when calling `update()`. It will always be set to the current Unix
109+
> timestamp in UTC. This is to ensure that the `updated_at` field is always
110+
> accurate.
111+
77112
## Deleting Records
78113

79114
To delete a record from the database, you need to pass the model class and the

docs/guide/models.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ the table.
2828
> - Type-hints are **REQUIRED** for each field in the model.
2929
> - The Model **automatically** creates an **auto-incrementing integer primary
3030
> key** for each table called `pk`, you do not need to define it yourself.
31+
> - The Model **automatically** creates a `created_at` and `updated_at` field
32+
> which is an integer Unix timestamp **IN UTC** when the record was created or
33+
> last updated. You can convert this timestamp to any format and timezone that
34+
> you need.
3135
3236
### Adding Indexes
3337

mkdocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ nav:
7474
- Connect to a Database: guide/connecting.md
7575
- Properties: guide/properties.md
7676
- Table Operations: guide/tables.md
77-
- Data Operations: guide/data-ops.md
77+
- Data Operations: guide/data-operations.md
7878
- Transactions: guide/transactions.md
7979
- Filtering Results: guide/filtering.md
8080
- Ordering: guide/ordering.md

sqliter/sqliter.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -430,11 +430,18 @@ def _maybe_commit(self) -> None:
430430
if not self._in_transaction and self.auto_commit and self.conn:
431431
self.conn.commit()
432432

433-
def insert(self, model_instance: T) -> T:
433+
def insert(
434+
self, model_instance: T, *, timestamp_override: bool = False
435+
) -> T:
434436
"""Insert a new record into the database.
435437
436438
Args:
437439
model_instance: The instance of the model class to insert.
440+
timestamp_override: If True, override the created_at and updated_at
441+
timestamps with provided values. Default is False. If the values
442+
are not provided, they will be set to the current time as
443+
normal. Without this flag, the timestamps will always be set to
444+
the current time, even if provided.
438445
439446
Returns:
440447
The updated model instance with the primary key (pk) set.
@@ -447,8 +454,18 @@ def insert(self, model_instance: T) -> T:
447454

448455
# Always set created_at and updated_at timestamps
449456
current_timestamp = int(time.time())
450-
model_instance.created_at = current_timestamp
451-
model_instance.updated_at = current_timestamp
457+
458+
# Handle the case where timestamp_override is False
459+
if not timestamp_override:
460+
# Always override both timestamps with the current time
461+
model_instance.created_at = current_timestamp
462+
model_instance.updated_at = current_timestamp
463+
else:
464+
# Respect provided values, but set to current time if they are 0
465+
if model_instance.created_at == 0:
466+
model_instance.created_at = current_timestamp
467+
if model_instance.updated_at == 0:
468+
model_instance.updated_at = current_timestamp
452469

453470
# Get the data from the model
454471
data = model_instance.model_dump()

tests/test_timestamps.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Class to test the `created_at` and `updated_at` timestamps."""
22

3+
from datetime import datetime, timezone
4+
35
from tests.conftest import ExampleModel
46

57

@@ -48,3 +50,214 @@ def test_update_timestamps(self, db_mock, mocker) -> None:
4850
assert (
4951
returned_instance.updated_at == 1234567891
5052
) # Should be updated to the new timestamp
53+
54+
def test_insert_with_provided_timestamps(self, db_mock, mocker) -> None:
55+
"""Test that user-provided timestamps are respected on insert."""
56+
# Mock time.time() to return a fixed timestamp
57+
mocker.patch("time.time", return_value=1234567890)
58+
59+
# User-provided timestamps
60+
new_instance = ExampleModel(
61+
slug="test",
62+
name="Test",
63+
content="Test content",
64+
created_at=1111111111, # User-provided
65+
updated_at=1111111111, # User-provided
66+
)
67+
68+
# Perform the insert operation
69+
returned_instance = db_mock.insert(
70+
new_instance, timestamp_override=True
71+
)
72+
73+
# Assert that the user-provided timestamps are respected
74+
assert returned_instance.created_at == 1111111111
75+
assert returned_instance.updated_at == 1111111111
76+
77+
def test_insert_with_default_timestamps(self, db_mock, mocker) -> None:
78+
"""Test that timestamps are set when created_at and updated_at are 0."""
79+
# Mock time.time() to return a fixed timestamp
80+
mocker.patch("time.time", return_value=1234567890)
81+
82+
# Create instance with default (0) timestamps
83+
new_instance = ExampleModel(
84+
slug="test",
85+
name="Test",
86+
content="Test content",
87+
created_at=0,
88+
updated_at=0,
89+
)
90+
91+
# Perform the insert operation
92+
returned_instance = db_mock.insert(new_instance)
93+
94+
# Assert that timestamps are set to the mocked time
95+
assert returned_instance.created_at == 1234567890
96+
assert returned_instance.updated_at == 1234567890
97+
98+
def test_insert_with_mixed_timestamps(self, db_mock, mocker) -> None:
99+
"""Test a mix of user-provided and default timestamps work on insert."""
100+
# Mock time.time() to return a fixed timestamp
101+
mocker.patch("time.time", return_value=1234567890)
102+
103+
# Provide only created_at, leave updated_at as 0
104+
new_instance = ExampleModel(
105+
slug="test",
106+
name="Test",
107+
content="Test content",
108+
created_at=1111111111, # User-provided
109+
updated_at=0, # Default to current time
110+
)
111+
112+
# Perform the insert operation
113+
returned_instance = db_mock.insert(
114+
new_instance, timestamp_override=True
115+
)
116+
117+
# Assert that created_at is respected, and updated_at is set to the
118+
# current time
119+
assert returned_instance.created_at == 1111111111
120+
assert returned_instance.updated_at == 1234567890
121+
122+
def test_update_timestamps_on_change(self, db_mock, mocker) -> None:
123+
"""Test that only `updated_at` changes on update."""
124+
# Mock time.time() to return a fixed timestamp for the insert
125+
mocker.patch("time.time", return_value=1234567890)
126+
127+
# Insert a new record
128+
new_instance = ExampleModel(
129+
slug="test", name="Test", content="Test content"
130+
)
131+
returned_instance = db_mock.insert(new_instance)
132+
133+
# Mock time.time() to return a new timestamp for the update
134+
mocker.patch("time.time", return_value=1234567891)
135+
136+
# Update the record
137+
returned_instance.name = "Updated Test"
138+
db_mock.update(returned_instance)
139+
140+
# Assert that created_at stays the same and updated_at is changed
141+
assert returned_instance.created_at == 1234567890
142+
assert returned_instance.updated_at == 1234567891
143+
144+
def test_no_change_if_timestamps_already_set(self, db_mock, mocker) -> None:
145+
"""Test timestamps are not modified if already set during insert."""
146+
# Mock time.time() to return a fixed timestamp
147+
mocker.patch("time.time", return_value=1234567890)
148+
149+
# User provides both timestamps
150+
new_instance = ExampleModel(
151+
slug="test",
152+
name="Test",
153+
content="Test content",
154+
created_at=1111111111, # Already set
155+
updated_at=1111111111, # Already set
156+
)
157+
158+
# Perform the insert operation
159+
returned_instance = db_mock.insert(
160+
new_instance, timestamp_override=True
161+
)
162+
163+
# Assert that timestamps are not modified
164+
assert returned_instance.created_at == 1111111111
165+
assert returned_instance.updated_at == 1111111111
166+
167+
def test_override_but_no_timestamps_provided(self, db_mock, mocker) -> None:
168+
"""Test missing timestamps always set to current time.
169+
170+
Even with `timestamp_override=True`.
171+
"""
172+
# Mock time.time() to return a fixed timestamp
173+
mocker.patch("time.time", return_value=1234567890)
174+
175+
# User provides `0` for both timestamps, expecting them to be overridden
176+
new_instance = ExampleModel(
177+
slug="test",
178+
name="Test",
179+
content="Test content",
180+
created_at=0, # Should default to current time
181+
updated_at=0, # Should default to current time
182+
)
183+
184+
# Perform the insert with timestamp_override=True
185+
returned_instance = db_mock.insert(
186+
new_instance, timestamp_override=True
187+
)
188+
189+
# Assert that both timestamps are set to the current time, ignoring the
190+
# `0`
191+
assert returned_instance.created_at == 1234567890
192+
assert returned_instance.updated_at == 1234567890
193+
194+
def test_partial_override_with_zero(self, db_mock, mocker) -> None:
195+
"""Test changing `updated_at` only on create.
196+
197+
When `timestamp_override=True
198+
"""
199+
# Mock time.time() to return a fixed timestamp
200+
mocker.patch("time.time", return_value=1234567890)
201+
202+
# User provides `created_at`, but leaves `updated_at` as 0
203+
new_instance = ExampleModel(
204+
slug="test",
205+
name="Test",
206+
content="Test content",
207+
created_at=1111111111, # Provided by the user
208+
updated_at=0, # Should be set to current time
209+
)
210+
211+
# Perform the insert operation with timestamp_override=True
212+
returned_instance = db_mock.insert(
213+
new_instance, timestamp_override=True
214+
)
215+
216+
# Assert that `created_at` is respected, and `updated_at` is set to the
217+
# current time
218+
assert returned_instance.created_at == 1111111111
219+
assert returned_instance.updated_at == 1234567890
220+
221+
def test_insert_with_override_disabled(self, db_mock, mocker) -> None:
222+
"""Test that timestamp_override=False ignores provided timestamps."""
223+
# Mock time.time() to return a fixed timestamp
224+
mocker.patch("time.time", return_value=1234567890)
225+
226+
# User provides both timestamps, but they should be ignored
227+
new_instance = ExampleModel(
228+
slug="test",
229+
name="Test",
230+
content="Test content",
231+
created_at=1111111111, # Should be ignored
232+
updated_at=1111111111, # Should be ignored
233+
)
234+
235+
# Perform the insert with timestamp_override=False (default)
236+
returned_instance = db_mock.insert(
237+
new_instance, timestamp_override=False
238+
)
239+
240+
# Assert that both timestamps are set to the mocked current time
241+
assert returned_instance.created_at == 1234567890
242+
assert returned_instance.updated_at == 1234567890
243+
244+
def test_time_is_in_utc(self, db_mock, mocker) -> None:
245+
"""Test that timestamps generated with time.time() are in UTC."""
246+
# Mock time.time() to return a fixed timestamp
247+
mocker.patch("time.time", return_value=1234567890)
248+
249+
# Insert a new instance
250+
new_instance = ExampleModel(
251+
slug="test", name="Test", content="Test content"
252+
)
253+
returned_instance = db_mock.insert(new_instance)
254+
255+
# Convert created_at to UTC datetime and verify the conversion
256+
created_at_utc = datetime.fromtimestamp(
257+
returned_instance.created_at, tz=timezone.utc
258+
)
259+
260+
# Assert that the datetime is correctly interpreted as UTC
261+
assert created_at_utc == datetime(
262+
2009, 2, 13, 23, 31, 30, tzinfo=timezone.utc
263+
)

0 commit comments

Comments
 (0)