fix(db): prefer postgres config in production refs NOISSUE

This commit is contained in:
2026-04-10 20:37:31 +02:00
parent a357a307a7
commit 59a7e9787e
5 changed files with 89 additions and 12 deletions

View File

@@ -54,6 +54,15 @@ GITEA_OWNER=ai-software-factory
# Optional legacy fixed-repository mode. Leave empty to create one repo per project.
GITEA_REPO=
# Database
# In production, provide PostgreSQL settings. They take precedence over the SQLite default.
# Setting USE_SQLITE=false is still supported if you want to make the choice explicit.
POSTGRES_HOST=postgres.yourserver.com
POSTGRES_PORT=5432
POSTGRES_USER=ai_software_factory
POSTGRES_PASSWORD=change-me
POSTGRES_DB=ai_software_factory
# n8n
N8N_WEBHOOK_URL=http://n8n.yourserver.com/webhook/telegram
@@ -90,6 +99,8 @@ docker-compose up -d
Check your gitea repository for generated PRs
If you deploy the container with PostgreSQL environment variables set, the service now selects PostgreSQL automatically even though SQLite remains the default for local/test usage.
## API Endpoints
| Endpoint | Method | Description |

View File

@@ -30,6 +30,8 @@ TELEGRAM_BOT_TOKEN=your_telegram_bot_token
TELEGRAM_CHAT_ID=your_chat_id
# PostgreSQL
# In production, provide PostgreSQL settings below. They now take precedence over the SQLite default.
# You can also set USE_SQLITE=false explicitly if you want the intent to be obvious.
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_USER=ai_test

View File

@@ -66,6 +66,32 @@ class Settings(BaseSettings):
DB_POOL_RECYCLE: int = 3600
DB_POOL_TIMEOUT: int = 30
@property
def postgres_url(self) -> str:
"""Get PostgreSQL URL with trimmed whitespace."""
return (self.POSTGRES_URL or "").strip()
@property
def postgres_env_configured(self) -> bool:
"""Whether PostgreSQL was explicitly configured via environment variables."""
if self.postgres_url:
return True
postgres_env_keys = (
"POSTGRES_HOST",
"POSTGRES_PORT",
"POSTGRES_USER",
"POSTGRES_PASSWORD",
"POSTGRES_DB",
)
return any(bool(os.environ.get(key, "").strip()) for key in postgres_env_keys)
@property
def use_sqlite(self) -> bool:
"""Whether SQLite should be used as the active database backend."""
if not self.USE_SQLITE:
return False
return not self.postgres_env_configured
@property
def pool(self) -> dict:
"""Get database pool configuration."""
@@ -79,8 +105,10 @@ class Settings(BaseSettings):
@property
def database_url(self) -> str:
"""Get database connection URL."""
if self.USE_SQLITE:
if self.use_sqlite:
return f"sqlite:///{self.SQLITE_DB_PATH}"
if self.postgres_url:
return self.postgres_url
return (
f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
f"@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
@@ -89,8 +117,10 @@ class Settings(BaseSettings):
@property
def test_database_url(self) -> str:
"""Get test database connection URL."""
if self.USE_SQLITE:
if self.use_sqlite:
return f"sqlite:///{self.SQLITE_DB_PATH}"
if self.postgres_url:
return self.postgres_url
return (
f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
f"@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_TEST_DB}"

View File

@@ -2,6 +2,7 @@
from collections.abc import Generator
from pathlib import Path
from urllib.parse import urlparse
from alembic import command
from alembic.config import Config
@@ -17,10 +18,31 @@ except ImportError:
from models import Base
def get_database_runtime_summary() -> dict[str, str]:
"""Return a human-readable summary of the effective database backend."""
if settings.use_sqlite:
db_path = str(Path(settings.SQLITE_DB_PATH or "/tmp/ai_software_factory_test.db").expanduser().resolve())
return {
"backend": "sqlite",
"target": db_path,
"database": db_path,
}
parsed = urlparse(settings.database_url)
database_name = parsed.path.lstrip("/") or "unknown"
host = parsed.hostname or "unknown-host"
port = str(parsed.port or 5432)
return {
"backend": parsed.scheme.split("+", 1)[0] or "postgresql",
"target": f"{host}:{port}/{database_name}",
"database": database_name,
}
def get_engine() -> Engine:
"""Create and return SQLAlchemy engine with connection pooling."""
# Use SQLite for tests, PostgreSQL for production
if settings.USE_SQLITE:
if settings.use_sqlite:
db_path = settings.SQLITE_DB_PATH or "/tmp/ai_software_factory_test.db"
Path(db_path).expanduser().resolve().parent.mkdir(parents=True, exist_ok=True)
db_url = f"sqlite:///{db_path}"
@@ -31,7 +53,7 @@ def get_engine() -> Engine:
echo=settings.LOG_LEVEL == "DEBUG"
)
else:
db_url = settings.POSTGRES_URL or settings.database_url
db_url = settings.database_url
# PostgreSQL-specific configuration
engine = create_engine(
db_url,
@@ -43,7 +65,7 @@ def get_engine() -> Engine:
)
# Event listener for connection checkout (PostgreSQL only)
if not settings.USE_SQLITE:
if not settings.use_sqlite:
@event.listens_for(engine, "checkout")
def receive_checkout(dbapi_connection, connection_record, connection_proxy):
"""Log connection checkout for audit purposes."""
@@ -100,7 +122,7 @@ def get_alembic_config(database_url: str | None = None) -> Config:
alembic_ini = package_root / "alembic.ini"
config = Config(str(alembic_ini))
config.set_main_option("script_location", str(package_root / "alembic"))
config.set_main_option("sqlalchemy.url", database_url or (settings.database_url if not settings.USE_SQLITE else f"sqlite:///{settings.SQLITE_DB_PATH or '/tmp/ai_software_factory_test.db'}"))
config.set_main_option("sqlalchemy.url", database_url or settings.database_url)
return config
@@ -116,7 +138,7 @@ def run_migrations(database_url: str | None = None) -> dict:
def init_db() -> dict:
"""Initialize database tables and database if needed."""
if settings.USE_SQLITE:
if settings.use_sqlite:
result = run_migrations()
if result["status"] == "success":
print("SQLite database migrations applied successfully.")
@@ -131,7 +153,7 @@ def init_db() -> dict:
return {'status': 'error', 'message': f'Error: {str(e)}'}
else:
# PostgreSQL
db_url = settings.POSTGRES_URL or settings.database_url
db_url = settings.database_url
db_name = db_url.split('/')[-1] if '/' in db_url else 'ai_software_factory'
try:
@@ -180,7 +202,7 @@ def init_db() -> dict:
def drop_db() -> dict:
"""Drop all database tables (use with caution!)."""
if settings.USE_SQLITE:
if settings.use_sqlite:
engine = get_engine()
try:
Base.metadata.drop_all(bind=engine)
@@ -190,7 +212,7 @@ def drop_db() -> dict:
print(f"Error dropping SQLite tables: {str(e)}")
return {'status': 'error', 'message': str(e)}
else:
db_url = settings.POSTGRES_URL or settings.database_url
db_url = settings.database_url
db_name = db_url.split('/')[-1] if '/' in db_url else 'ai_software_factory'
try:

View File

@@ -13,6 +13,7 @@ The NiceGUI frontend provides:
from __future__ import annotations
from contextlib import asynccontextmanager
import json
import re
from pathlib import Path
@@ -42,7 +43,18 @@ except ImportError:
__version__ = "0.0.1"
app = FastAPI()
@asynccontextmanager
async def lifespan(_app: FastAPI):
"""Log resolved runtime configuration when the app starts."""
runtime = database_module.get_database_runtime_summary()
print(
f"Runtime configuration: database_backend={runtime['backend']} target={runtime['target']}"
)
yield
app = FastAPI(lifespan=lifespan)
DbSession = Annotated[Session, Depends(database_module.get_db)]
PROJECT_ID_PATTERN = re.compile(r"[^a-z0-9]+")
@@ -180,7 +192,7 @@ def health_check():
"""Health check endpoint."""
return {
'status': 'healthy',
'database': 'sqlite' if database_module.settings.USE_SQLITE else 'postgresql',
'database': 'sqlite' if database_module.settings.use_sqlite else 'postgresql',
}