chore: PostgreSQL migration compatibility and infrastructure improvements

Database & Migrations:
- Update all Alembic migrations for PostgreSQL compatibility
- Remove SQLite-specific syntax (AUTOINCREMENT, etc.)
- Add database utility helpers for PostgreSQL operations
- Fix services to use PostgreSQL-compatible queries

Documentation:
- Add comprehensive Docker deployment guide
- Add production deployment documentation
- Add infrastructure architecture documentation
- Update database setup guide for PostgreSQL-only
- Expand troubleshooting guide

Architecture & Validation:
- Add migration.yaml rules for SQL compatibility checking
- Enhance validate_architecture.py with migration validation
- Update architecture rules to validate Alembic migrations

Development:
- Fix duplicate install-all target in Makefile
- Add Celery/Redis validation to install.py script
- Add docker-compose.test.yml for CI testing
- Add squash_migrations.py utility script
- Update tests for PostgreSQL compatibility
- Improve test fixtures in conftest.py

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-11 17:52:28 +01:00
parent 2792414395
commit 3614d448e4
45 changed files with 3179 additions and 507 deletions

View File

@@ -380,6 +380,12 @@ class ArchitectureValidator:
suggestion="Keep SQLAlchemy models and Pydantic models separate",
)
# Alembic migrations
elif "/alembic/versions/" in file_path_str or "\\alembic\\versions\\" in file_path_str:
print("🔄 Validating as Alembic migration...")
self._check_migration_batch_mode(file_path, content, lines)
self._check_migration_constraint_names(file_path, content, lines)
# Generic Python file - check exception handling
print("⚠️ Validating exception handling...")
for i, line in enumerate(lines, 1):
@@ -3760,6 +3766,126 @@ class ArchitectureValidator:
suggestion="Fix JSON syntax error (check for trailing commas, missing quotes)",
)
def _check_migration_batch_mode(
self, file_path: Path, content: str, lines: list[str]
):
"""MIG-001: Check that alter_column, drop_constraint, create_foreign_key use batch mode"""
# Track if we're inside a batch_alter_table context
in_batch_context = False
batch_indent = 0
for i, line in enumerate(lines, 1):
stripped = line.strip()
# Track batch_alter_table context entry
if "batch_alter_table(" in line or "with op.batch_alter_table" in line:
in_batch_context = True
# Get indent level of the 'with' statement
batch_indent = len(line) - len(line.lstrip())
continue
# Track batch_alter_table context exit (dedent)
if in_batch_context and stripped and not stripped.startswith("#"):
current_indent = len(line) - len(line.lstrip())
# If we're back at or before the 'with' indent level, we've exited
if current_indent <= batch_indent and not line.strip().startswith(
"with"
):
in_batch_context = False
# Skip comments
if stripped.startswith("#"):
continue
# Check for direct op.alter_column (not batch_op.alter_column)
if re.search(r"\bop\.alter_column\(", line):
self._add_violation(
rule_id="MIG-001",
rule_name="Use batch_alter_table for column modifications",
severity=Severity.ERROR,
file_path=file_path,
line_number=i,
message="op.alter_column() not supported by SQLite - use batch mode",
context=stripped[:100],
suggestion="Use: with op.batch_alter_table('table') as batch_op: batch_op.alter_column(...)",
)
# Check for direct op.drop_constraint (not batch_op.drop_constraint)
if re.search(r"\bop\.drop_constraint\(", line):
self._add_violation(
rule_id="MIG-001",
rule_name="Use batch_alter_table for constraint modifications",
severity=Severity.ERROR,
file_path=file_path,
line_number=i,
message="op.drop_constraint() not supported by SQLite - use batch mode",
context=stripped[:100],
suggestion="Use: with op.batch_alter_table('table') as batch_op: batch_op.drop_constraint(...)",
)
# Check for direct op.create_foreign_key (not batch_op.create_foreign_key)
if re.search(r"\bop\.create_foreign_key\(", line):
self._add_violation(
rule_id="MIG-001",
rule_name="Use batch_alter_table for foreign key creation",
severity=Severity.ERROR,
file_path=file_path,
line_number=i,
message="op.create_foreign_key() not supported by SQLite - use batch mode",
context=stripped[:100],
suggestion="Use: with op.batch_alter_table('table') as batch_op: batch_op.create_foreign_key(...)",
)
def _check_migration_constraint_names(
self, file_path: Path, content: str, lines: list[str]
):
"""MIG-002: Check that constraints have explicit names (not None)"""
for i, line in enumerate(lines, 1):
stripped = line.strip()
# Skip comments
if stripped.startswith("#"):
continue
# Check for create_foreign_key(None, ...)
if re.search(r"create_foreign_key\s*\(\s*None\s*,", line):
self._add_violation(
rule_id="MIG-002",
rule_name="Constraints must have explicit names",
severity=Severity.ERROR,
file_path=file_path,
line_number=i,
message="Foreign key constraint must have an explicit name, not None",
context=stripped[:100],
suggestion="Use: create_foreign_key('fk_table_column', ...)",
)
# Check for create_unique_constraint(None, ...)
if re.search(r"create_unique_constraint\s*\(\s*None\s*,", line):
self._add_violation(
rule_id="MIG-002",
rule_name="Constraints must have explicit names",
severity=Severity.ERROR,
file_path=file_path,
line_number=i,
message="Unique constraint must have an explicit name, not None",
context=stripped[:100],
suggestion="Use: create_unique_constraint('uq_table_columns', ...)",
)
# Check for drop_constraint(None, ...)
if re.search(r"drop_constraint\s*\(\s*None\s*,", line):
self._add_violation(
rule_id="MIG-002",
rule_name="Constraints must have explicit names",
severity=Severity.ERROR,
file_path=file_path,
line_number=i,
message="Cannot drop constraint with None name",
context=stripped[:100],
suggestion="Specify the constraint name to drop",
)
def _get_rule(self, rule_id: str) -> dict[str, Any]:
"""Get rule configuration by ID"""
# Look in different rule categories
@@ -3772,6 +3898,7 @@ class ArchitectureValidator:
"template_rules",
"frontend_component_rules",
"language_rules",
"migration_rules",
]:
rules = self.config.get(category, [])
for rule in rules: