Skip to content

Commit dab5dbb

Browse files
author
Eugene Shershen
committed
make execute and executemany async; add update tests and benchmarking; update version to 0.1.0a11
1 parent 8bcd9e7 commit dab5dbb

6 files changed

Lines changed: 251 additions & 20 deletions

File tree

Makefile

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: help docker-up docker-down docker-logs test test-db test-no-db clean install lint format
1+
.PHONY: help docker-up docker-down docker-logs test test-db test-no-db clean install lint format benchmark
22

33
help: ## Show this help message
44
@echo "Available commands:"
@@ -81,3 +81,30 @@ dev-setup: install docker-up ## Complete development setup
8181
@echo "Development environment is ready!"
8282
@echo "Run 'make test' to run tests with PostgreSQL"
8383
@echo "Run 'make docker-down' to stop PostgreSQL when done"
84+
85+
benchmark: ## Run performance comparison between psqlpy-sqlalchemy and asyncpg
86+
@echo "🚀 Starting performance benchmark..."
87+
@echo "Checking if PostgreSQL is available..."
88+
@if ! docker exec psqlpy-postgres pg_isready -U postgres >/dev/null 2>&1; then \
89+
echo "PostgreSQL not running, starting Docker container..."; \
90+
$(MAKE) docker-up; \
91+
DOCKER_STARTED=1; \
92+
else \
93+
echo "PostgreSQL is already running"; \
94+
DOCKER_STARTED=0; \
95+
fi; \
96+
echo "Ensuring dependencies are installed..."; \
97+
pip install -e ".[dev]" >/dev/null 2>&1 || echo "Dependencies already installed"; \
98+
echo "Running performance comparison test..."; \
99+
python performance_comparison.py; \
100+
BENCHMARK_EXIT_CODE=$$?; \
101+
if [ "$$DOCKER_STARTED" = "1" ]; then \
102+
echo "Stopping Docker container that was started for benchmark..."; \
103+
$(MAKE) docker-down; \
104+
fi; \
105+
if [ $$BENCHMARK_EXIT_CODE -eq 0 ]; then \
106+
echo "✅ Benchmark completed successfully!"; \
107+
else \
108+
echo "❌ Benchmark failed with exit code $$BENCHMARK_EXIT_CODE"; \
109+
fi; \
110+
exit $$BENCHMARK_EXIT_CODE

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,29 @@ with engine.connect() as conn:
211211
print("Connection successful:", result.fetchone())
212212
```
213213

214+
### Performance Benchmarking
215+
216+
The project includes a comprehensive performance comparison test between psqlpy-sqlalchemy and asyncpg:
217+
218+
```bash
219+
# Run performance benchmark (recommended)
220+
make benchmark
221+
```
222+
223+
This command will:
224+
- Automatically start PostgreSQL if not running
225+
- Install required dependencies
226+
- Run performance comparison tests across multiple scenarios
227+
- Clean up resources after completion
228+
229+
The benchmark tests various operations including:
230+
- Simple SELECT queries
231+
- Single and bulk INSERT operations
232+
- Complex queries with aggregations
233+
- Concurrent operations
234+
235+
For detailed benchmark configuration and results interpretation, see [PERFORMANCE_TEST_README.md](PERFORMANCE_TEST_README.md).
236+
214237
## Architecture
215238

216239
The dialect consists of several key components:

psqlpy_sqlalchemy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
PsqlpyDialect = PSQLPyAsyncDialect
44

5-
__version__ = "0.1.0a10"
5+
__version__ = "0.1.0a11"
66
__all__ = ["PsqlpyDialect", "PSQLPyAsyncDialect"]

psqlpy_sqlalchemy/connection.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -350,19 +350,19 @@ async def _executemany(
350350
True,
351351
)
352352

353-
def execute(
353+
async def execute(
354354
self,
355355
operation: t.Any,
356356
parameters: t.Union[
357357
t.Sequence[t.Any], t.Mapping[str, Any], None
358358
] = None,
359359
) -> None:
360-
await_only(self._prepare_execute(operation, parameters))
360+
await self._prepare_execute(operation, parameters)
361361

362-
def executemany(
362+
async def executemany(
363363
self, operation: t.Any, seq_of_parameters: t.Sequence[t.Any]
364364
) -> None:
365-
return await_only(self._executemany(operation, seq_of_parameters))
365+
await self._executemany(operation, seq_of_parameters)
366366

367367
def setinputsizes(self, *inputsizes: t.Any) -> None:
368368
raise NotImplementedError

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "psqlpy-sqlalchemy"
7-
version = "0.1.0a10"
7+
version = "0.1.0a11"
88
description = "SQLAlchemy dialect for psqlpy PostgreSQL driver"
99
readme = "README.md"
1010
license = {text = "MIT"}
@@ -44,6 +44,7 @@ dev = [
4444
"fastapi>=0.68.0",
4545
"starlette>=0.14.0",
4646
"fastapi-async-sqlalchemy>=0.3.0",
47+
"asyncpg>=0.28.0",
4748
]
4849
fastapi = [
4950
"fastapi>=0.68.0",

tests/test_connection.py

Lines changed: 193 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -208,25 +208,35 @@ def test_properties(self):
208208
self.cursor.arraysize = 10
209209
self.assertEqual(self.cursor.arraysize, 10)
210210

211-
@patch("psqlpy_sqlalchemy.connection.await_only")
212-
def test_execute(self, mock_await_only):
211+
@patch.object(
212+
AsyncAdapt_psqlpy_cursor, "_prepare_execute", new_callable=AsyncMock
213+
)
214+
def test_execute(self, mock_prepare_execute):
213215
"""Test cursor execute method"""
214216
operation = "SELECT * FROM table"
215217
parameters = {"id": 123}
216218

217-
self.cursor.execute(operation, parameters)
219+
# Since execute is now async, we need to run it in an async context
220+
import asyncio
218221

219-
mock_await_only.assert_called_once()
222+
asyncio.run(self.cursor.execute(operation, parameters))
220223

221-
@patch("psqlpy_sqlalchemy.connection.await_only")
222-
def test_executemany(self, mock_await_only):
224+
mock_prepare_execute.assert_called_once_with(operation, parameters)
225+
226+
@patch.object(
227+
AsyncAdapt_psqlpy_cursor, "_executemany", new_callable=AsyncMock
228+
)
229+
def test_executemany(self, mock_executemany):
223230
"""Test cursor executemany method"""
224231
operation = "INSERT INTO table VALUES ($1, $2)"
225232
seq_of_parameters = [[1, "a"], [2, "b"]]
226233

227-
self.cursor.executemany(operation, seq_of_parameters)
234+
# Since executemany is now async, we need to run it in an async context
235+
import asyncio
236+
237+
asyncio.run(self.cursor.executemany(operation, seq_of_parameters))
228238

229-
mock_await_only.assert_called_once()
239+
mock_executemany.assert_called_once_with(operation, seq_of_parameters)
230240

231241
def test_setinputsizes(self):
232242
"""Test setinputsizes method raises NotImplementedError"""
@@ -288,15 +298,185 @@ def test_convert_named_params_remaining_matches(self):
288298

289299
self.assertIn("Conversion incomplete", str(cm.exception))
290300

291-
def test_executemany_coverage(self):
301+
@patch.object(
302+
AsyncAdapt_psqlpy_cursor, "_executemany", new_callable=AsyncMock
303+
)
304+
def test_executemany_coverage(self, mock_executemany):
292305
"""Test executemany method for coverage"""
293306
operation = "INSERT INTO test VALUES ($1, $2)"
294307
seq_of_parameters = [[1, "a"], [2, "b"]]
295308

296-
# Test the sync wrapper by mocking await_only
297-
with patch("psqlpy_sqlalchemy.connection.await_only") as mock_await:
298-
self.cursor.executemany(operation, seq_of_parameters)
299-
mock_await.assert_called_once()
309+
# Since executemany is now async, we need to run it in an async context
310+
import asyncio
311+
312+
asyncio.run(self.cursor.executemany(operation, seq_of_parameters))
313+
314+
mock_executemany.assert_called_once_with(operation, seq_of_parameters)
315+
316+
# UPDATE operation tests
317+
@patch.object(
318+
AsyncAdapt_psqlpy_cursor, "_prepare_execute", new_callable=AsyncMock
319+
)
320+
def test_execute_update_basic(self, mock_prepare_execute):
321+
"""Test basic UPDATE operation with execute method"""
322+
operation = "UPDATE users SET name = 'John' WHERE id = 1"
323+
324+
import asyncio
325+
326+
asyncio.run(self.cursor.execute(operation))
327+
328+
mock_prepare_execute.assert_called_once_with(operation, None)
329+
330+
@patch.object(
331+
AsyncAdapt_psqlpy_cursor, "_prepare_execute", new_callable=AsyncMock
332+
)
333+
def test_execute_update_with_named_parameters(self, mock_prepare_execute):
334+
"""Test UPDATE operation with named parameters"""
335+
operation = (
336+
"UPDATE users SET name = :name, email = :email WHERE id = :id"
337+
)
338+
parameters = {"name": "John Doe", "email": "john@example.com", "id": 1}
339+
340+
import asyncio
341+
342+
asyncio.run(self.cursor.execute(operation, parameters))
343+
344+
mock_prepare_execute.assert_called_once_with(operation, parameters)
345+
346+
@patch.object(
347+
AsyncAdapt_psqlpy_cursor, "_prepare_execute", new_callable=AsyncMock
348+
)
349+
def test_execute_update_with_positional_parameters(
350+
self, mock_prepare_execute
351+
):
352+
"""Test UPDATE operation with positional parameters"""
353+
operation = "UPDATE users SET name = $1, email = $2 WHERE id = $3"
354+
parameters = ["John Doe", "john@example.com", 1]
355+
356+
import asyncio
357+
358+
asyncio.run(self.cursor.execute(operation, parameters))
359+
360+
mock_prepare_execute.assert_called_once_with(operation, parameters)
361+
362+
@patch.object(
363+
AsyncAdapt_psqlpy_cursor, "_prepare_execute", new_callable=AsyncMock
364+
)
365+
def test_execute_update_multiple_columns(self, mock_prepare_execute):
366+
"""Test UPDATE operation with multiple columns"""
367+
operation = "UPDATE users SET name = :name, email = :email, age = :age, updated_at = NOW() WHERE id = :id"
368+
parameters = {
369+
"name": "Jane Smith",
370+
"email": "jane@example.com",
371+
"age": 30,
372+
"id": 2,
373+
}
374+
375+
import asyncio
376+
377+
asyncio.run(self.cursor.execute(operation, parameters))
378+
379+
mock_prepare_execute.assert_called_once_with(operation, parameters)
380+
381+
@patch.object(
382+
AsyncAdapt_psqlpy_cursor, "_prepare_execute", new_callable=AsyncMock
383+
)
384+
def test_execute_update_with_where_clause(self, mock_prepare_execute):
385+
"""Test UPDATE operation with complex WHERE clause"""
386+
operation = "UPDATE users SET status = :status WHERE age > :min_age AND created_at < :date"
387+
parameters = {"status": "active", "min_age": 18, "date": "2023-01-01"}
388+
389+
import asyncio
390+
391+
asyncio.run(self.cursor.execute(operation, parameters))
392+
393+
mock_prepare_execute.assert_called_once_with(operation, parameters)
394+
395+
@patch.object(
396+
AsyncAdapt_psqlpy_cursor, "_executemany", new_callable=AsyncMock
397+
)
398+
def test_executemany_update_operations(self, mock_executemany):
399+
"""Test UPDATE operations with executemany method"""
400+
operation = "UPDATE users SET name = $1, email = $2 WHERE id = $3"
401+
seq_of_parameters = [
402+
["John Doe", "john@example.com", 1],
403+
["Jane Smith", "jane@example.com", 2],
404+
["Bob Johnson", "bob@example.com", 3],
405+
]
406+
407+
import asyncio
408+
409+
asyncio.run(self.cursor.executemany(operation, seq_of_parameters))
410+
411+
mock_executemany.assert_called_once_with(operation, seq_of_parameters)
412+
413+
@patch.object(
414+
AsyncAdapt_psqlpy_cursor, "_executemany", new_callable=AsyncMock
415+
)
416+
def test_executemany_update_with_dict_parameters(self, mock_executemany):
417+
"""Test UPDATE operations with executemany using dict parameters"""
418+
operation = "UPDATE users SET name = :name WHERE id = :id"
419+
seq_of_parameters = [
420+
{"name": "John Updated", "id": 1},
421+
{"name": "Jane Updated", "id": 2},
422+
]
423+
424+
import asyncio
425+
426+
asyncio.run(self.cursor.executemany(operation, seq_of_parameters))
427+
428+
mock_executemany.assert_called_once_with(operation, seq_of_parameters)
429+
430+
@patch.object(
431+
AsyncAdapt_psqlpy_cursor, "_prepare_execute", new_callable=AsyncMock
432+
)
433+
def test_execute_update_with_uuid_parameter(self, mock_prepare_execute):
434+
"""Test UPDATE operation with UUID parameter (tests the async fix)"""
435+
test_uuid = uuid.uuid4()
436+
operation = "UPDATE users SET profile_id = :profile_id WHERE id = :id"
437+
parameters = {"profile_id": test_uuid, "id": 1}
438+
439+
import asyncio
440+
441+
asyncio.run(self.cursor.execute(operation, parameters))
442+
443+
mock_prepare_execute.assert_called_once_with(operation, parameters)
444+
445+
@patch.object(
446+
AsyncAdapt_psqlpy_cursor, "_prepare_execute", new_callable=AsyncMock
447+
)
448+
def test_execute_update_async_greenlet_fix(self, mock_prepare_execute):
449+
"""Test that UPDATE operations work with the async/greenlet fix"""
450+
# This test specifically verifies that the async fix works for UPDATE operations
451+
# that were causing the original greenlet switching issue
452+
operation = "UPDATE test_table SET name = :name WHERE id = :id"
453+
parameters = {"name": "test_update", "id": 1}
454+
455+
import asyncio
456+
457+
# This should not raise any greenlet-related errors
458+
asyncio.run(self.cursor.execute(operation, parameters))
459+
460+
mock_prepare_execute.assert_called_once_with(operation, parameters)
461+
462+
# Verify that the method was called without await_only issues
463+
self.assertTrue(mock_prepare_execute.called)
464+
465+
@patch.object(
466+
AsyncAdapt_psqlpy_cursor, "_prepare_execute", new_callable=AsyncMock
467+
)
468+
def test_execute_update_with_null_values(self, mock_prepare_execute):
469+
"""Test UPDATE operation with NULL values"""
470+
operation = (
471+
"UPDATE users SET email = :email, phone = :phone WHERE id = :id"
472+
)
473+
parameters = {"email": None, "phone": None, "id": 1}
474+
475+
import asyncio
476+
477+
asyncio.run(self.cursor.execute(operation, parameters))
478+
479+
mock_prepare_execute.assert_called_once_with(operation, parameters)
300480

301481

302482
class TestAsyncAdaptPsqlpySSCursor(unittest.TestCase):

0 commit comments

Comments
 (0)