From pgdbm
Use before implementing pgdbm patterns to avoid common mistakes - provides rationalization table and red flags that prevent pool multiplication, schema errors, and template syntax violations
How this skill is triggered — by the user, by Claude, or both
Slash command
/pgdbm:common-mistakesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
**Core Principle:** Most pgdbm mistakes come from fighting the library's design instead of using it.
Core Principle: Most pgdbm mistakes come from fighting the library's design instead of using it.
This skill provides explicit counters for common rationalizations that lead to bugs.
Violating these = your code is wrong:
drop_test_database() in a finally block| Excuse | Reality | Fix |
|---|---|---|
| "Each service needs different pool sizes" | Shared pool allocates dynamically. Pre-sizing is guessing. | ONE pool with total max |
| "Separate pools give better isolation" | Schema isolation is enough. Separate pools waste connections. | Schema-isolated managers |
| "It's simpler to just write the schema name" | Breaks portability. Code only works in one deployment mode. | Use {{tables.}} always |
| "I'll use search_path instead of templates" | Doesn't work with shared pools. Leads to race conditions. | Use {{tables.}} syntax |
| "module_name seems optional" | Causes migration conflicts when multiple modules share DB. | Always specify unique name |
| "I can switch schema at runtime for tenants" | Race conditions. Same manager used by concurrent requests. | Create manager per schema |
| "I'll close the db_manager in my library" | Closes parent app's pool. Crashes everything. | Check _external_db flag |
| "{{tables.}} is too verbose, I'll skip it" | Works until you use shared pools or change schemas. Then breaks. | Use always, no exceptions |
| "Cleanup doesn't need try/finally" | If test fails, cleanup never runs. Databases leak forever. | ALWAYS use try/finally |
| "I'll silence cleanup errors with except pass" | Hides failures. Databases accumulate silently for months. | Let cleanup errors propagate |
If you're about to do ANY of these, you're making a mistake:
# WRONG
service1_db = AsyncDatabaseManager(DatabaseConfig(connection_string="postgresql://localhost/app"))
service2_db = AsyncDatabaseManager(DatabaseConfig(connection_string="postgresql://localhost/app"))
What happens:
"⚠️ Creating another connection pool to..."Fix:
# CORRECT
pool = await AsyncDatabaseManager.create_shared_pool(config)
service1_db = AsyncDatabaseManager(pool=pool, schema="service1")
service2_db = AsyncDatabaseManager(pool=pool, schema="service2")
# WRONG
await db.execute('INSERT INTO "myschema".users (email) VALUES ($1)', email)
await db.execute('INSERT INTO users (email) VALUES ($1)', email)
What happens:
Fix:
# CORRECT
await db.execute('INSERT INTO {{tables.users}} (email) VALUES ($1)', email)
# WRONG
migrations = AsyncMigrationManager(db, "migrations")
# Uses "default" module name - conflicts with other modules!
What happens:
Fix:
# CORRECT
migrations = AsyncMigrationManager(db, "migrations", module_name="myservice")
# WRONG
migrations = AsyncMigrationManager(
db,
"migrations",
schema="myschema" # This parameter doesn't exist!
)
What happens:
Fix:
# CORRECT - schema comes from db
db = AsyncDatabaseManager(pool=pool, schema="myschema")
migrations = AsyncMigrationManager(db, "migrations", module_name="myservice")
# WRONG
db = AsyncDatabaseManager(pool=pool, schema="tenant1")
# Later...
db.schema = "tenant2" # Don't do this!
await db.execute("INSERT INTO {{tables.data}} ...")
What happens:
Fix:
# CORRECT - create manager per schema
tenant1_db = AsyncDatabaseManager(pool=pool, schema="tenant1")
tenant2_db = AsyncDatabaseManager(pool=pool, schema="tenant2")
# WRONG
db = AsyncDatabaseManager(pool=shared_pool, schema="myservice")
await db.connect() # ERROR!
What happens:
Fix:
# CORRECT - don't call connect() when using external pool
db = AsyncDatabaseManager(pool=shared_pool, schema="myservice")
# Just use it - no connect() needed
# WRONG in library
class MyLibrary:
async def close(self):
# Always disconnects, even if didn't create connection
await self.db.disconnect()
What happens:
Fix:
# CORRECT - conditional cleanup
class MyLibrary:
async def close(self):
if self.db and not self._external_db:
await self.db.disconnect()
# WRONG - inconsistent
await db.execute('CREATE TABLE {{tables.users}} (...)')
await db.execute('INSERT INTO users (email) VALUES ($1)', email)
What happens:
Fix:
# CORRECT - use templates everywhere
await db.execute('CREATE TABLE {{tables.users}} (...)')
await db.execute('INSERT INTO {{tables.users}} (email) VALUES ($1)', email)
# WRONG - cleanup never runs if test fails
@pytest_asyncio.fixture
async def test_db():
test_database = AsyncTestDatabase(TEST_CONFIG)
await test_database.create_test_database()
async with test_database.get_test_db_manager(schema="myapp") as db:
yield db
await test_database.drop_test_database() # ← NEVER RUNS IF TEST FAILS
What happens:
test_* databases accumulate (thousands over time)Fix:
# CORRECT - cleanup in finally block
@pytest_asyncio.fixture
async def test_db():
test_database = AsyncTestDatabase(TEST_CONFIG)
await test_database.create_test_database()
try:
async with test_database.get_test_db_manager(schema="myapp") as db:
yield db
finally:
await test_database.drop_test_database() # ← ALWAYS RUNS
Even better - use provided fixtures:
# BEST - just import and use pgdbm fixtures
# tests/conftest.py
from pgdbm.fixtures.conftest import *
# No manual cleanup needed - fixtures handle it
# WRONG - silently ignores cleanup failure
finally:
try:
await test_db.drop_test_database()
except Exception:
pass # Database leaks silently!
What happens:
Fix:
# CORRECT - let cleanup failures be visible
finally:
await test_db.drop_test_database() # Failure will be reported
# WRONG - duplicating fixture logic in every test
@pytest.mark.asyncio
async def test_something():
test_db = AsyncTestDatabase(config)
await test_db.create_test_database()
try:
# ... test code ...
finally:
await test_db.drop_test_database()
What happens:
Fix:
# CORRECT - use fixtures
@pytest.mark.asyncio
async def test_something(test_db): # Fixture handles everything
# ... test code ...
# WRONG
migrations = AsyncMigrationManager(db1, "migrations", module_name="mylib")
migrations = AsyncMigrationManager(db2, "migrations", module_name="mylib")
# Both use same module_name but different schemas!
What happens:
Fix:
# CORRECT - include schema in module_name
migrations = AsyncMigrationManager(db1, "migrations", module_name=f"mylib_{schema1}")
migrations = AsyncMigrationManager(db2, "migrations", module_name=f"mylib_{schema2}")
Possible causes:
Debug checklist:
# Count orphaned databases
psql -U postgres -t -c "SELECT COUNT(*) FROM pg_database WHERE datname ~ '^test_[0-9a-f]{8}'"
# If count > 0, you have a cleanup problem
Fix:
except Exception: pass from cleanup codetest_db_isolated fixture (uses rollback, no database created)Clean up orphaned databases:
psql -U postgres -t -c \
"SELECT 'DROP DATABASE IF EXISTS \"' || datname || '\";' FROM pg_database WHERE datname ~ '^test_[0-9a-f]{8}'" \
| psql -U postgres
Possible causes:
{{tables.}} syntaxDebug checklist:
# Check schema configuration
print(f"Configured schema: {db.schema}") # Should match where tables are
# Debug template expansion
print(db.prepare_query("SELECT * FROM {{tables.users}}"))
# Shows: 'SELECT * FROM "myschema".users' or 'SELECT * FROM users'
# Check query uses templates
query = "SELECT * FROM {{tables.users}}" # ✅
query = "SELECT * FROM users" # ❌
# Verify schema exists
schemas = await db.fetch_all(
"SELECT schema_name FROM information_schema.schemata"
)
print([s["schema_name"] for s in schemas])
# Check migrations ran
applied = await migrations.get_applied_migrations()
print(f"Applied migrations: {applied}")
Possible causes:
Debug checklist:
# Check for multiple pools
# Look for this warning in logs:
"⚠️ Creating another connection pool to..."
# Check pool stats
stats = await pool.get_pool_stats()
print(f"Used: {stats['used_size']}/{stats['size']}")
# Verify cleanup in shutdown
# Make sure you have:
await pool.close() # Or await db.disconnect()
Possible causes:
module_nameDebug checklist:
# Check module_name is unique
migrations = AsyncMigrationManager(
db,
"migrations",
module_name="myservice" # Should be unique per service/schema
)
# For dual-mode libraries
module_name = f"mylib_{schema}" # Include schema in name
Run through this before implementing pgdbm:
AsyncDatabaseManager(DatabaseConfig(...)) to same database?{{tables.tablename}} in ALL queries and migrations?module_name for each service/schema?If you answered YES to first question or NO to any others: Review the pattern skills.
Quick self-test: What's wrong with each?
# 1. What's wrong?
db1 = AsyncDatabaseManager(DatabaseConfig(connection_string="postgresql://localhost/app"))
db2 = AsyncDatabaseManager(DatabaseConfig(connection_string="postgresql://localhost/app"))
# 2. What's wrong?
await db.execute("INSERT INTO users (email) VALUES ($1)", email)
# 3. What's wrong?
migrations = AsyncMigrationManager(db, "migrations")
# 4. What's wrong?
db = AsyncDatabaseManager(pool=pool, schema="service1")
await db.connect()
# 5. What's wrong?
db.schema = "different_schema"
Answers:
create_shared_pool(){{tables.users}}module_name="myservice"If pgdbm is fighting you, you're using it wrong.
The library is designed for specific patterns:
Follow these patterns and pgdbm works smoothly. Fight them and you get errors, warnings, and bugs.
pgdbm:using-pgdbmpgdbm:choosing-patternpgdbm:shared-pool-pattern, pgdbm:dual-mode-librarynpx claudepluginhub juanre/ai-tools --plugin pgdbmProvides patterns for Alembic migrations, SQL/NoSQL schema design, normalization, versioning, and zero-downtime migrations in SQLAlchemy apps. Use for schema drift, data backfills, and database selection.
Guides schema design, migration safety, and ORM analysis. Detects database engine and ORM, evaluates normalization and indexing, and validates backward compatibility.
Provides database migration best practices for schema changes, data migrations, rollbacks, and zero-downtime deployments across PostgreSQL, MySQL, and ORMs like Prisma, Drizzle, and Django.