Skip to content

SQLAlchemy Metadata Column Fix Summary

Issue Overview

CI/CD pipeline was failing with the following error:

sqlalchemy.exc.InvalidRequestError: Attribute name 'metadata' is reserved when using the Declarative API

Root Cause

In SQLAlchemy's Declarative API, the attribute name metadata is reserved for the table metadata object. Our SessionRecord model was using a column named metadata, which conflicted with SQLAlchemy's internal attribute.

Additionally, we encountered datetime serialization issues when saving session data to Redis (JSON) and PostgreSQL (JSONB columns).

Changes Made

1. Database Schema Changes

fiml/sessions/db.py

  • Line 45: Renamed column from metadata to session_metadata

    # Before:
    metadata = Column(JSON, nullable=False, default=dict)
    
    # After:
    session_metadata = Column(JSON, nullable=False, default=dict)
    

  • Lines 90-150: Updated CREATE_TABLES_SQL to use session_metadata column name

scripts/init-db.sql

  • Renamed metadata column to session_metadata in table creation script

2. Session Store Updates

fiml/sessions/store.py

  • Line 444: Updated _record_from_session to use session_metadata

    session_metadata=session.state.metadata,
    

  • Line 446: Fixed datetime serialization in history queries

    # Before:
    history_queries=[q.model_dump() for q in session.state.history.queries],
    
    # After:
    history_queries=[q.model_dump(mode='json') for q in session.state.history.queries],
    

  • Line 470: Updated _session_from_record to use session_metadata

    metadata=record.session_metadata,
    

3. Session Models Updates

fiml/sessions/models.py

  • Line 157: Fixed datetime serialization in to_dict() method
    # Before:
    "state": self.state.model_dump(),
    
    # After:
    "state": self.state.model_dump(mode='json'),
    

4. Test Infrastructure Updates

tests/conftest.py

  • Added init_session_db fixture with CASCADE drops to ensure clean schema:

    @pytest_asyncio.fixture(scope="session")
    async def init_session_db():
        """Initialize session database tables before any session tests"""
        async with get_async_engine().begin() as conn:
            # Drop existing tables first (with CASCADE to handle dependencies)
            await conn.execute(text("DROP TABLE IF EXISTS session_metrics CASCADE"))
            await conn.execute(text("DROP TABLE IF EXISTS sessions CASCADE"))
    
            # Create tables with new schema
            for statement in CREATE_TABLES_SQL.split(";"):
                if statement.strip():
                    await conn.execute(text(statement))
    

  • Updated session tests to depend on init_session_db fixture

Technical Details

Why model_dump(mode='json')?

Pydantic v2's model_dump() method has multiple modes: - Default mode: Returns Python objects (datetime, UUID, etc.) as-is - mode='json': Serializes all values to JSON-compatible types (datetime → ISO string)

When serializing sessions containing QueryRecord objects with timestamp fields, we need mode='json' to convert datetime objects to ISO format strings for: 1. Redis storage (requires JSON-serializable data) 2. PostgreSQL JSONB columns (requires JSON-compatible types)

Database Migration Notes

For production environments with existing data: 1. Test environments: Use DROP CASCADE approach (implemented in fixture) 2. Production: Need migration script:

ALTER TABLE sessions RENAME COLUMN metadata TO session_metadata;

Test Results

All 19 session tests now pass:

tests/test_sessions.py::TestSessionModels::test_query_record_creation PASSED
tests/test_sessions.py::TestSessionModels::test_analysis_history PASSED
tests/test_sessions.py::TestSessionModels::test_session_state PASSED
tests/test_sessions.py::TestSessionModels::test_session_creation PASSED
tests/test_sessions.py::TestSessionModels::test_session_expiration PASSED
tests/test_sessions.py::TestSessionModels::test_session_extend PASSED
tests/test_sessions.py::TestSessionModels::test_session_touch PASSED
tests/test_sessions.py::TestSessionModels::test_session_add_query PASSED
tests/test_sessions.py::TestSessionModels::test_session_serialization PASSED
tests/test_sessions.py::TestSessionStore::test_create_session PASSED
tests/test_sessions.py::TestSessionStore::test_get_session PASSED
tests/test_sessions.py::TestSessionStore::test_get_nonexistent_session PASSED
tests/test_sessions.py::TestSessionStore::test_update_session PASSED
tests/test_sessions.py::TestSessionStore::test_delete_session PASSED
tests/test_sessions.py::TestSessionStore::test_extend_session PASSED
tests/test_sessions.py::TestSessionStore::test_list_user_sessions PASSED
tests/test_sessions.py::TestSessionStore::test_archive_session PASSED
tests/test_sessions.py::TestSessionStore::test_cleanup_expired_sessions PASSED
tests/test_sessions.py::TestSessionIntegration::test_multi_query_session_workflow PASSED

=========================================================================== 19 passed, 155 warnings in 3.45s ===========================================================================

Files Modified

  1. fiml/sessions/db.py - Column name and SQL schema
  2. fiml/sessions/store.py - Column references and datetime serialization
  3. fiml/sessions/models.py - JSON serialization mode
  4. scripts/init-db.sql - SQL schema
  5. tests/conftest.py - Test fixture with schema initialization

References

  • SQLAlchemy Reserved Attributes: https://docs.sqlalchemy.org/en/20/orm/mapping_api.html#sqlalchemy.orm.DeclarativeBase.metadata
  • Pydantic v2 Serialization: https://docs.pydantic.dev/2.12/concepts/serialization/