Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,22 @@ def test_any_client_works(pglite_manager):

# Use with any PostgreSQL client
# conn = psycopg.connect(host=host, port=port, dbname=database)
# conn = await asyncpg.connect(host=host, port=port, database=database)
# engine = create_async_engine(f"postgresql+asyncpg://{host}:{port}/{database}")

# For asyncpg specifically, use TCP mode with proper configuration:
async def test_asyncpg_works(pglite_tcp_manager):
config = pglite_tcp_manager.config
conn = await asyncpg.connect(
host=config.tcp_host,
port=config.tcp_port,
user="postgres",
password="postgres",
database="postgres",
ssl=False,
server_settings={} # CRITICAL: Required for PGlite compatibility
)
result = await conn.fetchval("SELECT 1")
await conn.close()
```

---
Expand Down Expand Up @@ -243,6 +257,7 @@ with SQLAlchemyPGliteManager(config) as manager:
py-pglite supports both Unix domain sockets (default) and TCP sockets for different use cases:

### Unix Socket Mode (Default)

```python
# Default configuration - uses Unix domain socket for best performance
from py_pglite import PGliteManager
Expand All @@ -253,6 +268,7 @@ with PGliteManager() as db:
```

### TCP Socket Mode

```python
from py_pglite import PGliteConfig, PGliteManager

Expand All @@ -271,16 +287,20 @@ with PGliteManager(config) as db:
```

**When to use TCP mode:**

- Any TCP-only clients (doesn't support Unix sockets)
- Cloud-native testing environments
- Docker containers with network isolation
- Testing network-based database tools
- **Required for asyncpg**: asyncpg only works in TCP mode

**Important notes:**

- PGlite Socket supports only **one active connection** at a time
- SSL is not supported - always use `sslmode=disable`
- Unix sockets are faster for local testing (default)
- TCP mode binds to localhost by default for security
- **asyncpg requires `server_settings={}` to prevent hanging**

</details>

Expand All @@ -299,8 +319,20 @@ with SQLAlchemyPGliteManager() as manager:

# Examples for different clients:
# psycopg: psycopg.connect(host=host, port=port, dbname=database)
# asyncpg: await asyncpg.connect(host=host, port=port, database=database)
# Django: Uses custom py-pglite backend automatically

# asyncpg requires TCP mode and specific configuration:
config = PGliteConfig(use_tcp=True)
with PGliteManager(config) as manager:
conn = await asyncpg.connect(
host=config.tcp_host,
port=config.tcp_port,
user="postgres",
password="postgres",
database="postgres",
ssl=False,
server_settings={} # Required for PGlite compatibility
)
```

**Installation Matrix:**
Expand Down
172 changes: 172 additions & 0 deletions examples/quickstart/simple_asyncpg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""
🚀 Instant AsyncPG + PostgreSQL
================================

Zero-config asyncpg with real PostgreSQL in 30 seconds.
Shows the proper configuration for asyncpg with py-pglite.

Usage:
pip install py-pglite[asyncpg]
python simple_asyncpg.py

Recent findings: asyncpg DOES work with PGlite TCP mode when configured properly!
"""

import asyncio
import json
import logging

from py_pglite import PGliteConfig
from py_pglite import PGliteManager


logger = logging.getLogger(__name__)


try:
import asyncpg
except ImportError:
logger.info(
"❌ asyncpg not available. Install with: pip install py-pglite[asyncpg]"
)
exit(1)


async def main():
"""⚡ Instant PostgreSQL with asyncpg - proper configuration!"""

# print("🚀 Starting py-pglite with asyncpg...")

# Enable TCP mode (required for asyncpg)
config = PGliteConfig(use_tcp=True, tcp_port=5432)

with PGliteManager(config):
logger.info(f"✅ PGlite started on {config.tcp_host}:{config.tcp_port}")

# Connect with asyncpg using the CRITICAL configuration discovered
# Key finding: server_settings={} prevents hanging!
conn = await asyncio.wait_for(
asyncpg.connect(
host=config.tcp_host,
port=config.tcp_port,
user="postgres",
password="postgres",
database="postgres",
ssl=False,
server_settings={}, # CRITICAL: Empty server_settings prevents hanging
),
timeout=10.0,
)

try:
logger.info("✅ Connected to PostgreSQL via asyncpg!")

# Test 1: Basic query
result = await conn.fetchval("SELECT version()")
logger.info(f"📊 PostgreSQL Version: {result[:50]}...")

# Test 2: Create table with advanced types
await conn.execute("""
CREATE TABLE async_demo (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
data JSONB,
tags TEXT[],
created TIMESTAMP DEFAULT NOW()
)
""")
logger.info("✅ Table created with JSONB and array support!")

# Test 3: Insert with prepared statements
stmt = await conn.prepare("""
INSERT INTO async_demo (name, data, tags)
VALUES ($1, $2, $3) RETURNING id
""")

user_id = await stmt.fetchval(
"Alice",
json.dumps({"role": "admin", "score": 95}),
["python", "asyncpg", "postgresql"],
)
logger.info(f"✅ Inserted user with ID: {user_id}")

# Test 4: Complex query with JSON operations
row = await conn.fetchrow(
"""
SELECT
name,
data->>'role' as role,
data->>'score' as score,
array_length(tags, 1) as tag_count,
created
FROM async_demo
WHERE id = $1
""",
user_id,
)

logger.info("✅ Query result:")
logger.info(f" Name: {row['name']}")
logger.info(f" Role: {row['role']}")
logger.info(f" Score: {row['score']}")
logger.info(f" Tags: {row['tag_count']} tags")
logger.info(f" Created: {row['created']}")

# Test 5: Transaction support
async with conn.transaction():
await conn.execute("""
INSERT INTO async_demo (name, data, tags) VALUES
('Bob', '{"role": "user"}', ARRAY['beginner']),
('Carol', '{"role": "moderator"}', ARRAY['advanced', 'helper'])
""")
count = await conn.fetchval("SELECT COUNT(*) FROM async_demo")
logger.info(f"✅ Transaction: {count} total records")

# Test 6: Batch operations
batch_data = [
(f"User{i}", json.dumps({"level": i}), [f"tag{i}", "batch"])
for i in range(1, 4)
]

await conn.executemany(
"""
INSERT INTO async_demo (name, data, tags) VALUES ($1, $2, $3)
""",
batch_data,
)

final_count = await conn.fetchval("SELECT COUNT(*) FROM async_demo")
logger.info(f"✅ Batch insert completed: {final_count} total records")

# Test 7: Advanced PostgreSQL features
stats = await conn.fetchrow("""
SELECT
COUNT(*) as total_users,
COUNT(*) FILTER (WHERE data->>'role' = 'admin') as admins,
AVG((data->>'score')::int) FILTER (WHERE data ? 'score') as avg_score
FROM async_demo
""")

# print("✅ Advanced query:")
logger.info(f" Total users: {stats['total_users']}")
logger.info(f" Admins: {stats['admins']}")
logger.info(f" Avg score: {stats['avg_score']}")

finally:
# Handle connection cleanup with timeout (addresses hanging issue)
try:
await asyncio.wait_for(conn.close(), timeout=5.0)
logger.info("✅ Connection closed cleanly")
except asyncio.TimeoutError:
logger.info("⚠️ Connection cleanup timed out (known limitation)")
# This is not a failure - all operations completed successfully

logger.info("🎉 asyncpg + py-pglite demo completed successfully!")
logger.info(
"💡 Key finding: server_settings={} is critical for asyncpg compatibility"
)


if __name__ == "__main__":
asyncio.run(main())
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Database",
"Topic :: Software Development :: Testing",
"Topic :: Software Development :: Libraries :: Python Modules",
Expand Down
10 changes: 5 additions & 5 deletions src/py_pglite/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def get_connection_string(self) -> str:
if self.use_tcp:
# TCP connection string
return f"postgresql+psycopg://postgres:postgres@{self.tcp_host}:{self.tcp_port}/postgres?sslmode=disable"

# For SQLAlchemy with Unix domain sockets, we need to specify the directory
# and use the standard PostgreSQL socket naming convention
socket_dir = str(Path(self.socket_path).parent)
Expand All @@ -108,7 +108,7 @@ def get_psycopg_uri(self) -> str:
if self.use_tcp:
# TCP URI
return f"postgresql://postgres:postgres@{self.tcp_host}:{self.tcp_port}/postgres?sslmode=disable"

socket_dir = str(Path(self.socket_path).parent)
# Use standard PostgreSQL URI format for psycopg
return f"postgresql://postgres:postgres@/postgres?host={socket_dir}"
Expand All @@ -118,21 +118,21 @@ def get_dsn(self) -> str:
if self.use_tcp:
# TCP DSN
return f"host={self.tcp_host} port={self.tcp_port} dbname=postgres user=postgres password=postgres sslmode=disable"

socket_dir = str(Path(self.socket_path).parent)
# Use key-value format for psycopg DSN, including password
return f"host={socket_dir} dbname=postgres user=postgres password=postgres"

def get_asyncpg_uri(self) -> str:
"""Get PostgreSQL URI for asyncpg usage.

Returns:
PostgreSQL URI string compatible with asyncpg.connect()
"""
if self.use_tcp:
# TCP URI (asyncpg doesn't support sslmode parameter)
return f"postgresql://postgres:postgres@{self.tcp_host}:{self.tcp_port}/postgres"

# Unix socket URI
socket_dir = str(Path(self.socket_path).parent)
return f"postgresql://postgres:postgres@/postgres?host={socket_dir}"
23 changes: 18 additions & 5 deletions src/py_pglite/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import sys
import tempfile
import time

from pathlib import Path
from textwrap import dedent
from typing import Any
Expand Down Expand Up @@ -385,16 +386,23 @@ def start(self) -> None:
if self.config.use_tcp:
# TCP readiness check
if not ready_logged:
self.logger.info(f"Waiting for TCP server on {self.config.tcp_host}:{self.config.tcp_port}...")
self.logger.info(
f"Waiting for TCP server on {self.config.tcp_host}:{self.config.tcp_port}..."
)
ready_logged = True

try:
import socket

test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
test_socket.settimeout(1)
test_socket.connect((self.config.tcp_host, self.config.tcp_port))
test_socket.connect(
(self.config.tcp_host, self.config.tcp_port)
)
test_socket.close()
self.logger.info(f"PGlite TCP server started successfully on {self.config.tcp_host}:{self.config.tcp_port}")
self.logger.info(
f"PGlite TCP server started successfully on {self.config.tcp_host}:{self.config.tcp_port}"
)
break
except (ImportError, OSError):
# TCP port not ready yet, continue waiting
Expand All @@ -403,13 +411,18 @@ def start(self) -> None:
# Unix socket readiness check
socket_path = Path(self.config.socket_path)
if socket_path.exists() and not ready_logged:
self.logger.info("PGlite socket created, server should be ready...")
self.logger.info(
"PGlite socket created, server should be ready..."
)
ready_logged = True

# Test basic connectivity to ensure it's really ready
try:
import socket
test_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)

test_socket = socket.socket(
socket.AF_UNIX, socket.SOCK_STREAM
)
test_socket.settimeout(1)
test_socket.connect(str(socket_path))
test_socket.close()
Expand Down
Loading
Loading