From 596d2815f811da5e5649bfef9a8bfee4dee1443f Mon Sep 17 00:00:00 2001 From: Dries Peeters Date: Mon, 5 Jan 2026 20:50:24 +0100 Subject: [PATCH 1/2] Create 102_add_missing_quotes_template_id.py --- .../102_add_missing_quotes_template_id.py | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 migrations/versions/102_add_missing_quotes_template_id.py diff --git a/migrations/versions/102_add_missing_quotes_template_id.py b/migrations/versions/102_add_missing_quotes_template_id.py new file mode 100644 index 00000000..e18f750c --- /dev/null +++ b/migrations/versions/102_add_missing_quotes_template_id.py @@ -0,0 +1,200 @@ +"""Add missing template_id column to quotes table + +Revision ID: 102_add_missing_quotes_template_id +Revises: 101_add_issues_table +Create Date: 2026-01-05 + +This migration adds the missing template_id column to the quotes table. +The column was supposed to be added in migration 051, but some databases +may have missed it due to migration execution order or partial failures. +This migration is idempotent and safe to run multiple times. +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy import inspect + +# revision identifiers, used by Alembic. +revision = '102_add_missing_quotes_template_id' +down_revision = '101_add_issues_table' +branch_labels = None +depends_on = None + + +def _has_table(inspector, name: str) -> bool: + """Check if a table exists""" + try: + return name in inspector.get_table_names() + except Exception: + return False + + +def _has_column(inspector, table_name: str, column_name: str) -> bool: + """Check if a column exists in a table""" + try: + return column_name in {c["name"] for c in inspector.get_columns(table_name)} + except Exception: + return False + + +def _has_index(inspector, table_name: str, index_name: str) -> bool: + """Check if an index exists""" + try: + indexes = inspector.get_indexes(table_name) + return any((idx.get("name") or "") == index_name for idx in indexes) + except Exception: + return False + + +def _has_foreign_key(inspector, table_name: str, fk_name: str) -> bool: + """Check if a foreign key constraint exists""" + try: + fks = inspector.get_foreign_keys(table_name) + return any((fk.get("name") or "") == fk_name for fk in fks) + except Exception: + return False + + +def upgrade(): + """Add template_id column to quotes table if it doesn't exist""" + bind = op.get_bind() + inspector = inspect(bind) + + dialect_name = bind.dialect.name if bind else 'generic' + print(f"[Migration 102] Running on {dialect_name} database") + + # Check if quotes table exists + if not _has_table(inspector, 'quotes'): + print("[Migration 102] ⊘ Quotes table does not exist, skipping") + return + + # Check if template_id column already exists + if _has_column(inspector, 'quotes', 'template_id'): + print("[Migration 102] ✓ template_id column already exists in quotes table") + + # Verify index exists + if not _has_index(inspector, 'quotes', 'ix_quotes_template_id'): + print("[Migration 102] Creating missing index ix_quotes_template_id...") + try: + op.create_index('ix_quotes_template_id', 'quotes', ['template_id']) + print("[Migration 102] ✓ Index created") + except Exception as e: + print(f"[Migration 102] ⚠ Warning: Could not create index: {e}") + + # Verify foreign key exists (only if quote_pdf_templates table exists) + if _has_table(inspector, 'quote_pdf_templates'): + if not _has_foreign_key(inspector, 'quotes', 'fk_quotes_template_id'): + print("[Migration 102] Creating missing foreign key fk_quotes_template_id...") + try: + if dialect_name == 'sqlite': + # SQLite requires batch operations for foreign keys + with op.batch_alter_table('quotes', schema=None) as batch_op: + batch_op.create_foreign_key( + 'fk_quotes_template_id', + 'quote_pdf_templates', + ['template_id'], + ['id'] + ) + else: + # PostgreSQL and others + op.create_foreign_key( + 'fk_quotes_template_id', + 'quotes', + 'quote_pdf_templates', + ['template_id'], + ['id'], + ondelete='SET NULL' + ) + print("[Migration 102] ✓ Foreign key created") + except Exception as e: + print(f"[Migration 102] ⚠ Warning: Could not create foreign key: {e}") + + return + + # Column doesn't exist, add it + print("[Migration 102] Adding template_id column to quotes table...") + try: + op.add_column('quotes', + sa.Column('template_id', sa.Integer(), nullable=True) + ) + print("[Migration 102] ✓ Column added") + except Exception as e: + print(f"[Migration 102] ✗ Error adding column: {e}") + raise + + # Create index + print("[Migration 102] Creating index ix_quotes_template_id...") + try: + op.create_index('ix_quotes_template_id', 'quotes', ['template_id']) + print("[Migration 102] ✓ Index created") + except Exception as e: + print(f"[Migration 102] ⚠ Warning: Could not create index: {e}") + + # Create foreign key if quote_pdf_templates table exists + if _has_table(inspector, 'quote_pdf_templates'): + print("[Migration 102] Creating foreign key fk_quotes_template_id...") + try: + if dialect_name == 'sqlite': + # SQLite requires batch operations for foreign keys + with op.batch_alter_table('quotes', schema=None) as batch_op: + batch_op.create_foreign_key( + 'fk_quotes_template_id', + 'quote_pdf_templates', + ['template_id'], + ['id'] + ) + else: + # PostgreSQL and others + op.create_foreign_key( + 'fk_quotes_template_id', + 'quotes', + 'quote_pdf_templates', + ['template_id'], + ['id'], + ondelete='SET NULL' + ) + print("[Migration 102] ✓ Foreign key created") + except Exception as e: + print(f"[Migration 102] ⚠ Warning: Could not create foreign key: {e}") + else: + print("[Migration 102] ⚠ quote_pdf_templates table does not exist, skipping foreign key") + + print("[Migration 102] ✓ Migration completed successfully") + + +def downgrade(): + """Remove template_id column from quotes table""" + bind = op.get_bind() + inspector = inspect(bind) + + if not _has_table(inspector, 'quotes'): + print("[Migration 102] ⊘ Quotes table does not exist, skipping downgrade") + return + + if not _has_column(inspector, 'quotes', 'template_id'): + print("[Migration 102] ⊘ template_id column does not exist, skipping downgrade") + return + + print("[Migration 102] Removing template_id column from quotes table...") + try: + # Drop foreign key first + if _has_foreign_key(inspector, 'quotes', 'fk_quotes_template_id'): + try: + op.drop_constraint('fk_quotes_template_id', 'quotes', type_='foreignkey') + print("[Migration 102] ✓ Foreign key dropped") + except Exception as e: + print(f"[Migration 102] ⚠ Warning: Could not drop foreign key: {e}") + + # Drop index + if _has_index(inspector, 'quotes', 'ix_quotes_template_id'): + try: + op.drop_index('ix_quotes_template_id', table_name='quotes') + print("[Migration 102] ✓ Index dropped") + except Exception as e: + print(f"[Migration 102] ⚠ Warning: Could not drop index: {e}") + + # Drop column + op.drop_column('quotes', 'template_id') + print("[Migration 102] ✓ Column dropped") + except Exception as e: + print(f"[Migration 102] ✗ Error during downgrade: {e}") + raise From 7f3c8a88199ac6565c6672493a5127d54ea87a8d Mon Sep 17 00:00:00 2001 From: Dries Peeters Date: Mon, 5 Jan 2026 20:54:59 +0100 Subject: [PATCH 2/2] Fix quotes template --- docs/TROUBLESHOOTING_QUOTES_TEMPLATE_ID.md | 132 +++++++++++++++ scripts/fix_quotes_template_id.py | 180 +++++++++++++++++++++ 2 files changed, 312 insertions(+) create mode 100644 docs/TROUBLESHOOTING_QUOTES_TEMPLATE_ID.md create mode 100644 scripts/fix_quotes_template_id.py diff --git a/docs/TROUBLESHOOTING_QUOTES_TEMPLATE_ID.md b/docs/TROUBLESHOOTING_QUOTES_TEMPLATE_ID.md new file mode 100644 index 00000000..b55d2cf1 --- /dev/null +++ b/docs/TROUBLESHOOTING_QUOTES_TEMPLATE_ID.md @@ -0,0 +1,132 @@ +# Troubleshooting: Missing quotes.template_id Column + +## Problem + +You're seeing this error in your database logs: + +``` +ERROR: column quotes.template_id does not exist at character 2057 +``` + +This occurs when the database schema is missing the `template_id` column in the `quotes` table, even though the application code expects it to exist. + +## Root Cause + +The `template_id` column should have been added by migration `051_rename_offers_to_quotes_and_add_features.py` or migration `102_add_missing_quotes_template_id.py`. If this column is missing, it means: + +1. The migrations haven't been run yet, or +2. A migration failed partway through, or +3. The database was created before these migrations existed + +## Solution + +### Option 1: Run Database Migrations (Recommended) + +The proper way to fix this is to run the pending migrations: + +```bash +# Check current migration status +flask db current + +# Apply all pending migrations +flask db upgrade +``` + +If you're using Docker, migrations should run automatically on container startup. If they didn't, you can manually trigger them: + +```bash +# Inside the container or with docker exec +docker exec -it timetracker-app flask db upgrade +``` + +### Option 2: Quick Fix Script + +If you cannot run migrations for some reason, you can use the quick fix script: + +```bash +# Set your DATABASE_URL environment variable +export DATABASE_URL="postgresql+psycopg2://user:password@host:port/database" + +# Run the fix script +python scripts/fix_quotes_template_id.py +``` + +Or with Docker: + +```bash +docker exec -it timetracker-app python /app/scripts/fix_quotes_template_id.py +``` + +### Option 3: Manual SQL Fix + +If you have direct database access, you can run this SQL: + +```sql +-- Add the column +ALTER TABLE quotes ADD COLUMN template_id INTEGER; + +-- Create index +CREATE INDEX ix_quotes_template_id ON quotes (template_id); + +-- Add foreign key (if quote_pdf_templates table exists) +ALTER TABLE quotes +ADD CONSTRAINT fk_quotes_template_id +FOREIGN KEY (template_id) +REFERENCES quote_pdf_templates(id) +ON DELETE SET NULL; +``` + +## Verification + +After applying the fix, verify the column exists: + +```sql +-- PostgreSQL +SELECT column_name, data_type, is_nullable +FROM information_schema.columns +WHERE table_name = 'quotes' AND column_name = 'template_id'; + +-- Or check via Python +python -c " +from app import create_app, db +from sqlalchemy import inspect +app = create_app() +with app.app_context(): + inspector = inspect(db.engine) + columns = [c['name'] for c in inspector.get_columns('quotes')] + print('template_id' in columns) +" +``` + +## Prevention + +To prevent this issue in the future: + +1. **Always run migrations** after pulling code updates: + ```bash + flask db upgrade + ``` + +2. **Check migration status** before deploying: + ```bash + flask db current + flask db history + ``` + +3. **Use the comprehensive schema verification** script: + ```bash + python scripts/verify_and_fix_schema.py + ``` + +## Related Files + +- Migration: `migrations/versions/102_add_missing_quotes_template_id.py` +- Model: `app/models/quote.py` (line 63) +- Fix Script: `scripts/fix_quotes_template_id.py` +- Schema Verification: `scripts/verify_and_fix_schema.py` + +## Additional Notes + +- The `template_id` column is nullable, so existing quotes won't be affected +- The column references `quote_pdf_templates.id` for PDF template selection +- Migration 102 is idempotent and safe to run multiple times diff --git a/scripts/fix_quotes_template_id.py b/scripts/fix_quotes_template_id.py new file mode 100644 index 00000000..23d18f7b --- /dev/null +++ b/scripts/fix_quotes_template_id.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +""" +Quick fix script for missing quotes.template_id column. + +This script adds the missing template_id column to the quotes table. +This is a workaround for cases where migration 102 hasn't been applied yet. + +Usage: + python scripts/fix_quotes_template_id.py +""" + +import os +import sys +from sqlalchemy import create_engine, inspect, text +from sqlalchemy.exc import OperationalError + +# Add parent directory to path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Get database URL from environment +DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+psycopg2://timetracker:timetracker@localhost:5432/timetracker") + + +def has_table(inspector, name: str) -> bool: + """Check if a table exists""" + try: + return name in inspector.get_table_names() + except Exception: + return False + + +def has_column(inspector, table_name: str, column_name: str) -> bool: + """Check if a column exists in a table""" + try: + columns = [col['name'] for col in inspector.get_columns(table_name)] + return column_name in columns + except Exception: + return False + + +def has_index(inspector, table_name: str, index_name: str) -> bool: + """Check if an index exists""" + try: + indexes = inspector.get_indexes(table_name) + return any((idx.get("name") or "") == index_name for idx in indexes) + except Exception: + return False + + +def has_foreign_key(inspector, table_name: str, fk_name: str) -> bool: + """Check if a foreign key constraint exists""" + try: + fks = inspector.get_foreign_keys(table_name) + return any((fk.get("name") or "") == fk_name for fk in fks) + except Exception: + return False + + +def main(): + """Main function to fix missing template_id column""" + print("=" * 60) + print("TimeTracker - Fix Missing quotes.template_id Column") + print("=" * 60) + print() + + try: + engine = create_engine(DATABASE_URL) + inspector = inspect(engine) + + # Test connection + with engine.connect() as conn: + conn.execute(text("SELECT 1")) + print("✓ Database connection successful") + print() + + # Check if quotes table exists + if not has_table(inspector, 'quotes'): + print("✗ 'quotes' table does not exist. Please run migrations first.") + return 1 + + # Check if column already exists + if has_column(inspector, 'quotes', 'template_id'): + print("✓ Column 'template_id' already exists in 'quotes' table") + + # Verify index exists + if not has_index(inspector, 'quotes', 'ix_quotes_template_id'): + print("Creating missing index ix_quotes_template_id...") + try: + with engine.connect() as conn: + conn.execute(text("CREATE INDEX ix_quotes_template_id ON quotes (template_id)")) + conn.commit() + print("✓ Index created") + except Exception as e: + print(f"⚠ Warning: Could not create index: {e}") + + # Verify foreign key exists (only if quote_pdf_templates table exists) + if has_table(inspector, 'quote_pdf_templates'): + if not has_foreign_key(inspector, 'quotes', 'fk_quotes_template_id'): + print("Creating missing foreign key fk_quotes_template_id...") + try: + with engine.connect() as conn: + conn.execute(text( + "ALTER TABLE quotes " + "ADD CONSTRAINT fk_quotes_template_id " + "FOREIGN KEY (template_id) " + "REFERENCES quote_pdf_templates(id) " + "ON DELETE SET NULL" + )) + conn.commit() + print("✓ Foreign key created") + except Exception as e: + print(f"⚠ Warning: Could not create foreign key: {e}") + + print() + print("=" * 60) + print("✓ All required columns and constraints already exist") + print("=" * 60) + return 0 + + # Column doesn't exist, add it + print("Adding template_id column to quotes table...") + try: + with engine.connect() as conn: + # Add column + conn.execute(text("ALTER TABLE quotes ADD COLUMN template_id INTEGER")) + conn.commit() + print("✓ Column added") + except Exception as e: + print(f"✗ Failed to add column: {e}") + return 1 + + # Create index + print("Creating index ix_quotes_template_id...") + try: + with engine.connect() as conn: + conn.execute(text("CREATE INDEX ix_quotes_template_id ON quotes (template_id)")) + conn.commit() + print("✓ Index created") + except Exception as e: + print(f"⚠ Warning: Could not create index: {e}") + + # Create foreign key if quote_pdf_templates table exists + if has_table(inspector, 'quote_pdf_templates'): + print("Creating foreign key fk_quotes_template_id...") + try: + with engine.connect() as conn: + conn.execute(text( + "ALTER TABLE quotes " + "ADD CONSTRAINT fk_quotes_template_id " + "FOREIGN KEY (template_id) " + "REFERENCES quote_pdf_templates(id) " + "ON DELETE SET NULL" + )) + conn.commit() + print("✓ Foreign key created") + except Exception as e: + print(f"⚠ Warning: Could not create foreign key: {e}") + else: + print("⚠ quote_pdf_templates table does not exist, skipping foreign key") + + print() + print("=" * 60) + print("✓ Successfully fixed quotes.template_id column") + print("=" * 60) + print() + print("Note: This is a quick fix. For proper migration management,") + print(" you should run: flask db upgrade") + print() + + return 0 + + except Exception as e: + print(f"✗ Error: {e}") + import traceback + traceback.print_exc() + return 1 + + +if __name__ == "__main__": + sys.exit(main())