Skip to content

Conversation

Copy link

Copilot AI commented Oct 15, 2025

ORM Integration for TimeTracker - COMPLETE ✅

Summary

Successfully replaced raw SQLite queries with SQLAlchemy ORM, providing type safety, better developer experience, and support for future database migrations.

Implementation Complete

This PR transforms the database layer from raw SQLite to SQLAlchemy ORM while maintaining 100% backward compatibility and improving code quality.

Key Changes

1. Dependencies (pyproject.toml)

  • Added SQLAlchemy >=2.0.0 for ORM functionality
  • Added Alembic >=1.13.0 for future migration support

2. ORM Models (src/models.py)

  • Event: Start/stop tracking events (ISO string format)
  • Pause: Daily pause times (ISO date string)
  • Vacation: Vacation days (ISO date string)
  • Base class for all models
  • All dates stored as strings for consistent ISO format and proper SQLite ordering

3. Database Controller (src/database_controller.py)

  • Removed DatabaseHandler abstraction layer - merged into DatabaseController
  • Using context manager pattern (session_scope) instead of try/finally blocks
  • Added proper cleanup with __del__ method
  • Using scoped_session for thread safety with expire_on_commit=False
  • Direct engine and session management
  • All database operations use the context manager
  • Cleaner, more maintainable code

Date Format Fix

Issue: SQLAlchemy's DateTime type was storing dates in format 2025-10-15 11:26:02.000000 (space separator) instead of the original ISO format 2025-12-30T14:30:00 (T separator), breaking string-based ordering in SQLite.

Solution: Changed Event model to store dates as strings using ISO format with T separator, consistent with original implementation:

  • Event.Date: String type storing ISO format YYYY-MM-DDTHH:MM:SS
  • Pause.Date: String type storing ISO format YYYY-MM-DD
  • Vacation.Date: String type storing ISO format YYYY-MM-DD

Benefits:

  • ✅ Consistent date format across all tables
  • ✅ String-based ordering works correctly in SQLite
  • ✅ Backward compatible with existing database files
  • ✅ ISO format ensures proper chronological ordering

Architecture Changes

Before:

DatabaseControllerDatabaseHandlersession_factorySession
(with try/finally in every method)

After:

DatabaseControllerscoped_sessionSession (via context manager)
(using session_scope() context manager)

Benefits

Simplified Architecture: One class instead of two
Cleaner Code: Context manager handles commit/rollback/close
Better Error Handling: Automatic rollback on exceptions
Proper Cleanup: __del__ method ensures resources are freed
Thread Safety: scoped_session ensures thread-local sessions
Type Safety: Full type hints throughout
No Resource Leaks: Context manager guarantees session cleanup
Consistent Date Format: ISO strings with proper ordering

Testing

✅ All 9 comprehensive tests pass
✅ Date format verified (ISO with T separator)
✅ String-based ordering works correctly
✅ Backward compatibility maintained
✅ Context manager handles all scenarios
✅ Exception handling works correctly

Code Quality

  • No more repetitive try/finally blocks
  • Centralized session management
  • Follows best practices from similar projects
  • Clean separation of concerns
  • Consistent date storage across all tables
Original prompt

Use an ORM for Python SQLite.

This project currently just uses the sqlite library to connect to the local database. To get more type safety/support, we want to switch to an orm. INvestigate which orm is good in python and use the best fitting one for developer experience, as well as integration with database migration tools (will use them in the future).
This includes changing the database methods so they will use the orm and also see how the tests are mocked (fixture) and still will work with a mocked database.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 6 commits October 15, 2025 06:57
…ller

Co-authored-by: AndreWohnsland <50302161+AndreWohnsland@users.noreply.github.com>
Co-authored-by: AndreWohnsland <50302161+AndreWohnsland@users.noreply.github.com>
Co-authored-by: AndreWohnsland <50302161+AndreWohnsland@users.noreply.github.com>
Co-authored-by: AndreWohnsland <50302161+AndreWohnsland@users.noreply.github.com>
Co-authored-by: AndreWohnsland <50302161+AndreWohnsland@users.noreply.github.com>
Co-authored-by: AndreWohnsland <50302161+AndreWohnsland@users.noreply.github.com>
Copilot AI changed the title [WIP] Switch from sqlite library to ORM for SQLite Integrate SQLAlchemy ORM for type-safe database operations Oct 15, 2025
Copilot AI requested a review from AndreWohnsland October 15, 2025 07:17
@AndreWohnsland
Copy link
Owner

@copilot i've copied this section from another project, which is the sort of direction, i want to use the DB controller. Take note that this might be more complex, than we need it, but i want especially get rid of the try ... finally blocks in each function and rater use the context manager. I also want to have a proper cleanup function. I also do not think we need this abstraction layer between handler and controller anymore. Like in the snippet below. It was migrated into one class. Please adjust the new implementation according to this guideline.

class DatabaseCommander:
    """Commander Class to execute queries and return the results as lists."""

    database_path = DATABASE_PATH
    database_path_default = DEFAULT_DATABASE_PATH

    def __init__(self, use_default: bool = False, db_url: str | None = None) -> None:
        if not self.database_path.exists():
            time_print("Copying default database for maker usage")
            self.copy_default_database()
        if db_url is None:
            self.db_url = f"sqlite:///{self.database_path_default if use_default else self.database_path}"
        else:
            self.db_url = db_url
        self.engine = create_engine(self.db_url, echo=False)
        Base.metadata.create_all(self.engine)
        self.Session = scoped_session(sessionmaker(bind=self.engine, expire_on_commit=False))

    def __del__(self) -> None:
        """Close the session when the object is deleted."""
        self.Session.remove()
        self.engine.dispose()

    @contextmanager
    def session_scope(self) -> Generator[Session, None, None]:
        """Provide a transactional scope around a series of operations."""
        session = self.Session()
        try:
            yield session
            session.commit()
        except Exception as e:
            session.rollback()
            raise e
        finally:
            session.close()
           

Co-authored-by: AndreWohnsland <50302161+AndreWohnsland@users.noreply.github.com>
Copy link
Author

Copilot AI commented Oct 15, 2025

@copilot i've copied this section from another project, which is the sort of direction, i want to use the DB controller. Take note that this might be more complex, than we need it, but i want especial...

Refactored according to your guidelines in commit 3bb0f72^..HEAD:

✅ Removed the DatabaseHandler abstraction layer
✅ Merged everything into a single DatabaseController class
✅ Implemented session_scope() context manager pattern
✅ Added del cleanup method
✅ Using scoped_session with expire_on_commit=False
✅ Removed all try/finally blocks from methods

The code now follows the same pattern as your example - much cleaner and more maintainable. All existing tests still pass.

@AndreWohnsland AndreWohnsland added the dependencies Upgrade or downgrade of project dependencies. label Oct 15, 2025
@AndreWohnsland
Copy link
Owner

@copilot i am running into some issues with existing database data now. While the date is now "2025-10-15 11:26:02.000000", it was before "2025-12-30T14:30:00" -> what caused this change, and is there a way to fix this? I noticed this is causing issues in case i get the returned date in ordered by date, because i think sqlite just uses the string to order it.

Co-authored-by: AndreWohnsland <50302161+AndreWohnsland@users.noreply.github.com>
Copy link
Author

Copilot AI commented Oct 15, 2025

@copilot i am running into some issues with existing database data now. While the date is now "2025-10-15 11:26:02.000000", it was before "2025-12-30T14:30:00" -> what caused this change, and is there...

Fixed in commit cdd6503^..HEAD. The issue was that I initially used SQLAlchemy's DateTime column type for Events, which stores dates in SQLite format (2025-10-15 11:26:02.000000 with space separator) instead of ISO format (2025-12-30T14:30:00 with T separator).

Solution: Changed Event.Date to String type to store ISO format consistently:

  • Dates now stored as: YYYY-MM-DDTHH:MM:SS (with T separator)
  • String-based ordering works correctly in SQLite
  • Backward compatible with existing database data
  • Consistent with how Pause and Vacation dates are stored

All tests pass and dates are now in the correct format for proper ordering.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Upgrade or downgrade of project dependencies.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants