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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user