Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(client): Add support for interactive transactions #613

Merged
merged 42 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f8f7d0f
feat(client): add support for interactive transactions
RobertCraigie Jan 21, 2023
04c0c55
Add missing dep
RobertCraigie Jan 22, 2023
eb46991
Fix test
RobertCraigie Jan 22, 2023
575b8a5
databases: add missing dep
RobertCraigie Jan 22, 2023
bb1de54
Try fix tests
RobertCraigie Jan 22, 2023
f122f85
Fix tests & bump typing-extensions version
RobertCraigie Jan 22, 2023
9d566f5
Fix client logging
RobertCraigie Jan 22, 2023
92d89fd
Add type annotation and improve debug messages
RobertCraigie Jan 22, 2023
b803d89
Document `timeout` and `max_wait` arguments
RobertCraigie Jan 22, 2023
9113251
Merge branch 'main' into feat/transactions
RobertCraigie Feb 3, 2023
ccb41f8
Fix mypy
RobertCraigie Feb 3, 2023
9fbe6ee
xfail broken tests
RobertCraigie Feb 3, 2023
26d21fe
Updates
RobertCraigie Mar 18, 2023
727f954
Merge branch 'main' into feat/transactions
RobertCraigie May 13, 2023
5c24f24
Merge branch 'main' into feat/transactions
RobertCraigie May 14, 2023
53fcf0c
Remove engine reference counting
RobertCraigie Jun 10, 2023
28f4e1c
Merge branch 'main' into feat/transactions
RobertCraigie Jun 10, 2023
c59d52b
Missed reference to `new_ref()`
RobertCraigie Jun 11, 2023
e63ab30
Skip broken test for SQLite + improve debug logs
RobertCraigie Jun 11, 2023
cf40527
chore(pre-commit.ci): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 11, 2023
71c83f5
docs
RobertCraigie Jun 11, 2023
2fdaa90
chore(pre-commit.ci): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 11, 2023
6354e91
debug
RobertCraigie Jun 11, 2023
a00291c
debug
RobertCraigie Jun 11, 2023
71ef8b8
Try fix
RobertCraigie Jun 11, 2023
addbf6b
Try fix
RobertCraigie Jun 11, 2023
1c858fd
Try fix
RobertCraigie Jun 11, 2023
554c444
Fix sync tests
RobertCraigie Jun 11, 2023
00a28ef
Attempt fix
RobertCraigie Jun 11, 2023
3713e02
chore(pre-commit.ci): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 11, 2023
7b4c40d
Increase some timeouts
RobertCraigie Jun 11, 2023
ba635dd
Commit debug stuff in case it is useful later
RobertCraigie Jun 11, 2023
f942b1f
Revert "Commit debug stuff in case it is useful later"
RobertCraigie Jun 11, 2023
92fae9b
Add sync tests + fix timeout tests
RobertCraigie Jun 12, 2023
c9311c0
Skip transactions for CockroachDB for now
RobertCraigie Jun 12, 2023
22d17a2
Remove some debug logs
RobertCraigie Jun 12, 2023
dda4bee
Remove unused typevar
RobertCraigie Jun 12, 2023
7151fbd
Make tx_id a required argument
RobertCraigie Jun 12, 2023
8a6b625
Add debug logging to transaction manager
RobertCraigie Jun 12, 2023
eb97a3e
Add test case for error pass-through
RobertCraigie Jun 12, 2023
f785125
Minor cleanups
RobertCraigie Jun 12, 2023
8e1fbd4
Slight improvement to client copy test
RobertCraigie Jun 12, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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