Skip to content

Commit

Permalink
feat(client): Add support for interactive transactions (#613)
Browse files Browse the repository at this point in the history
## Change Summary

closes #53

This PR also bumps the `typing-extensions` version for `NewType`
support.

TODO:
- [x] tests for behaviour wrt rollback
- [x] Check what `max_wait` does and document both options
- [x] Check if we need to bump the `typing-extensions` dep
- [x] Support for model based access
- [x] Fix synchronous tests
- [x] Docs (include timeout information) 

## Checklist

- [ ] Unit tests for the changes exist
- [ ] Tests pass without significant drop in coverage
- [ ] Documentation reflects changes where applicable
- [ ] Test snapshots have been
[updated](https://prisma-client-py.readthedocs.io/en/latest/contributing/contributing/#snapshot-tests)
if applicable

## Agreement

By submitting this pull request, I confirm that you can use, modify,
copy and redistribute this contribution, under the terms of your choice.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
RobertCraigie and pre-commit-ci[bot] committed Jun 12, 2023
1 parent 50f01ed commit cca0e85
Show file tree
Hide file tree
Showing 33 changed files with 1,311 additions and 84 deletions.
1 change: 0 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ jobs:
run: |
nox -s typesafety-mypy
lint:
name: lint
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
bootstrap:
pip install -U wheel
pip install -U -e .
pip install -U -r pipelines/requirements/lint.txt
pip install -U -r pipelines/requirements/all.txt
python -m prisma_cleanup
prisma db push --schema=tests/data/schema.prisma
cp tests/data/dev.db dev.db
Expand Down
2 changes: 2 additions & 0 deletions databases/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def _fromdir(path: str) -> list[str]:
unsupported_features={
'json_arrays',
'array_push',
'transactions',
},
),
'sqlite': DatabaseConfig(
Expand Down Expand Up @@ -102,6 +103,7 @@ def _fromdir(path: str) -> list[str]:
'arrays': [*_fromdir('arrays'), *_fromdir('types/raw_queries/arrays')],
'array_push': _fromdir('arrays/push'),
'json_arrays': ['arrays/test_json.py', 'arrays/push/test_json.py'],
'transactions': ['test_transactions.py'],
# not yet implemented
'date': [],
'create_many': ['test_create_many.py'],
Expand Down
1 change: 1 addition & 0 deletions databases/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ distro

-r ../pipelines/requirements/deps/pyright.txt
-r ../pipelines/requirements/deps/pytest.txt
-r ../pipelines/requirements/deps/pytest-mock.txt
-r ../pipelines/requirements/deps/pytest-asyncio.txt
-r ../pipelines/requirements/deps/syrupy.txt
177 changes: 177 additions & 0 deletions databases/sync_tests/test_transactions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import time
from typing import Optional

import pytest

import prisma
from prisma import Prisma
from prisma.models import User
from ..utils import CURRENT_DATABASE


def test_context_manager(client: Prisma) -> None:
"""Basic usage within a context manager"""
with client.tx(timeout=10 * 100) as transaction:
user = transaction.user.create({'name': 'Robert'})
assert user.name == 'Robert'

# ensure not commited outside transaction
assert client.user.count() == 0

transaction.profile.create(
{
'description': 'Hello, there!',
'country': 'Scotland',
'user': {
'connect': {
'id': user.id,
},
},
},
)

found = client.user.find_unique(
where={'id': user.id}, include={'profile': True}
)
assert found is not None
assert found.name == 'Robert'
assert found.profile is not None
assert found.profile.description == 'Hello, there!'


def test_context_manager_auto_rollback(client: Prisma) -> None:
"""An error being thrown when within a context manager causes the transaction to be rolled back"""
user: Optional[User] = None

with pytest.raises(RuntimeError) as exc:
with client.tx() as tx:
user = tx.user.create({'name': 'Tegan'})
raise RuntimeError('Error ocurred mid transaction.')

assert exc.match('Error ocurred mid transaction.')

assert user is not None
found = client.user.find_unique(where={'id': user.id})
assert found is None


def test_batch_within_transaction(client: Prisma) -> None:
"""Query batching can be used within transactions"""
with client.tx(timeout=10000) as transaction:
with transaction.batch_() as batcher:
batcher.user.create({'name': 'Tegan'})
batcher.user.create({'name': 'Robert'})

assert client.user.count() == 0
assert transaction.user.count() == 2

assert client.user.count() == 2


def test_timeout(client: Prisma) -> None:
"""A `TransactionExpiredError` is raised when the transaction times out."""
# this outer block is necessary becuse to the context manager it appears that no error
# ocurred so it will attempt to commit the transaction, triggering the expired error again
with pytest.raises(prisma.errors.TransactionExpiredError):
with client.tx(timeout=50) as transaction:
time.sleep(0.05)

with pytest.raises(prisma.errors.TransactionExpiredError) as exc:
transaction.user.create({'name': 'Robert'})

raise exc.value


@pytest.mark.skipif(
CURRENT_DATABASE == 'sqlite', reason='This is currently broken...'
)
def test_concurrent_transactions(client: Prisma) -> None:
"""Two separate transactions can be used independently of each other at the same time"""
timeout = 15000
with client.tx(timeout=timeout) as tx1, client.tx(timeout=timeout) as tx2:
user1 = tx1.user.create({'name': 'Tegan'})
user2 = tx2.user.create({'name': 'Robert'})

assert tx1.user.find_first(where={'name': 'Robert'}) is None
assert tx2.user.find_first(where={'name': 'Tegan'}) is None

found = tx1.user.find_first(where={'name': 'Tegan'})
assert found is not None
assert found.id == user1.id

found = tx2.user.find_first(where={'name': 'Robert'})
assert found is not None
assert found.id == user2.id

# ensure not leaked
assert client.user.count() == 0
assert (tx1.user.find_first(where={'name': user2.name})) is None
assert (tx2.user.find_first(where={'name': user1.name})) is None

assert client.user.count() == 2


def test_transaction_raises_original_error(client: Prisma) -> None:
"""If an error is raised during the execution of the transaction, it is raised"""
with pytest.raises(RuntimeError, match=r'Test error!'):
with client.tx():
raise RuntimeError('Test error!')


def test_transaction_within_transaction_warning(client: Prisma) -> None:
"""A warning is raised if a transaction is started from another transaction client"""
tx1 = client.tx().start()
with pytest.warns(UserWarning) as warnings:
tx1.tx().start()

assert len(warnings) == 1
record = warnings[0]
assert not isinstance(record.message, str)
assert (
record.message.args[0]
== 'The current client is already in a transaction. This can lead to surprising behaviour.'
)
assert record.filename == __file__


def test_transaction_within_transaction_context_warning(
client: Prisma,
) -> None:
"""A warning is raised if a transaction is started from another transaction client"""
with client.tx() as tx1:
with pytest.warns(UserWarning) as warnings:
with tx1.tx():
pass

assert len(warnings) == 1
record = warnings[0]
assert not isinstance(record.message, str)
assert (
record.message.args[0]
== 'The current client is already in a transaction. This can lead to surprising behaviour.'
)
assert record.filename == __file__


def test_transaction_not_started(client: Prisma) -> None:
"""A `TransactionNotStartedError` is raised when attempting to call `commit()` or `rollback()`
on a transaction that hasn't been started yet.
"""
tx = client.tx()

with pytest.raises(prisma.errors.TransactionNotStartedError):
tx.commit()

with pytest.raises(prisma.errors.TransactionNotStartedError):
tx.rollback()


def test_transaction_already_closed(client: Prisma) -> None:
"""Attempting to use a transaction outside of the context block raises an error"""
with client.tx() as transaction:
pass

with pytest.raises(prisma.errors.TransactionExpiredError) as exc:
transaction.user.delete_many()

assert exc.match('Transaction already closed')
Loading

0 comments on commit cca0e85

Please sign in to comment.