sqlalchemy_api_workflow_run_repository.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. """
  2. SQLAlchemy API WorkflowRun Repository Implementation
  3. This module provides the SQLAlchemy-based implementation of the APIWorkflowRunRepository
  4. protocol. It handles service-layer WorkflowRun database operations using SQLAlchemy 2.0
  5. style queries with proper session management and multi-tenant data isolation.
  6. Key Features:
  7. - SQLAlchemy 2.0 style queries for modern database operations
  8. - Cursor-based pagination for efficient large dataset handling
  9. - Bulk operations with batch processing for performance
  10. - Multi-tenant data isolation and security
  11. - Proper session management with dependency injection
  12. Implementation Notes:
  13. - Uses sessionmaker for consistent session management
  14. - Implements cursor-based pagination using created_at timestamps
  15. - Provides efficient bulk deletion with batch processing
  16. - Maintains data consistency with proper transaction handling
  17. """
  18. import logging
  19. from collections.abc import Sequence
  20. from datetime import datetime
  21. from typing import Optional, cast
  22. from sqlalchemy import delete, select
  23. from sqlalchemy.orm import Session, sessionmaker
  24. from libs.infinite_scroll_pagination import InfiniteScrollPagination
  25. from models.workflow import WorkflowRun
  26. from repositories.api_workflow_run_repository import APIWorkflowRunRepository
  27. logger = logging.getLogger(__name__)
  28. class DifyAPISQLAlchemyWorkflowRunRepository(APIWorkflowRunRepository):
  29. """
  30. SQLAlchemy implementation of APIWorkflowRunRepository.
  31. Provides service-layer WorkflowRun database operations using SQLAlchemy 2.0
  32. style queries. Supports dependency injection through sessionmaker and
  33. maintains proper multi-tenant data isolation.
  34. Args:
  35. session_maker: SQLAlchemy sessionmaker instance for database connections
  36. """
  37. def __init__(self, session_maker: sessionmaker[Session]) -> None:
  38. """
  39. Initialize the repository with a sessionmaker.
  40. Args:
  41. session_maker: SQLAlchemy sessionmaker for database connections
  42. """
  43. self._session_maker = session_maker
  44. def get_paginated_workflow_runs(
  45. self,
  46. tenant_id: str,
  47. app_id: str,
  48. triggered_from: str,
  49. limit: int = 20,
  50. last_id: Optional[str] = None,
  51. ) -> InfiniteScrollPagination:
  52. """
  53. Get paginated workflow runs with filtering.
  54. Implements cursor-based pagination using created_at timestamps for
  55. efficient handling of large datasets. Filters by tenant, app, and
  56. trigger source for proper data isolation.
  57. """
  58. with self._session_maker() as session:
  59. # Build base query with filters
  60. base_stmt = select(WorkflowRun).where(
  61. WorkflowRun.tenant_id == tenant_id,
  62. WorkflowRun.app_id == app_id,
  63. WorkflowRun.triggered_from == triggered_from,
  64. )
  65. if last_id:
  66. # Get the last workflow run for cursor-based pagination
  67. last_run_stmt = base_stmt.where(WorkflowRun.id == last_id)
  68. last_workflow_run = session.scalar(last_run_stmt)
  69. if not last_workflow_run:
  70. raise ValueError("Last workflow run not exists")
  71. # Get records created before the last run's timestamp
  72. base_stmt = base_stmt.where(
  73. WorkflowRun.created_at < last_workflow_run.created_at,
  74. WorkflowRun.id != last_workflow_run.id,
  75. )
  76. # First page - get most recent records
  77. workflow_runs = session.scalars(base_stmt.order_by(WorkflowRun.created_at.desc()).limit(limit + 1)).all()
  78. # Check if there are more records for pagination
  79. has_more = len(workflow_runs) > limit
  80. if has_more:
  81. workflow_runs = workflow_runs[:-1]
  82. return InfiniteScrollPagination(data=workflow_runs, limit=limit, has_more=has_more)
  83. def get_workflow_run_by_id(
  84. self,
  85. tenant_id: str,
  86. app_id: str,
  87. run_id: str,
  88. ) -> Optional[WorkflowRun]:
  89. """
  90. Get a specific workflow run by ID with tenant and app isolation.
  91. """
  92. with self._session_maker() as session:
  93. stmt = select(WorkflowRun).where(
  94. WorkflowRun.tenant_id == tenant_id,
  95. WorkflowRun.app_id == app_id,
  96. WorkflowRun.id == run_id,
  97. )
  98. return cast(Optional[WorkflowRun], session.scalar(stmt))
  99. def get_expired_runs_batch(
  100. self,
  101. tenant_id: str,
  102. before_date: datetime,
  103. batch_size: int = 1000,
  104. ) -> Sequence[WorkflowRun]:
  105. """
  106. Get a batch of expired workflow runs for cleanup operations.
  107. """
  108. with self._session_maker() as session:
  109. stmt = (
  110. select(WorkflowRun)
  111. .where(
  112. WorkflowRun.tenant_id == tenant_id,
  113. WorkflowRun.created_at < before_date,
  114. )
  115. .limit(batch_size)
  116. )
  117. return cast(Sequence[WorkflowRun], session.scalars(stmt).all())
  118. def delete_runs_by_ids(
  119. self,
  120. run_ids: Sequence[str],
  121. ) -> int:
  122. """
  123. Delete workflow runs by their IDs using bulk deletion.
  124. """
  125. if not run_ids:
  126. return 0
  127. with self._session_maker() as session:
  128. stmt = delete(WorkflowRun).where(WorkflowRun.id.in_(run_ids))
  129. result = session.execute(stmt)
  130. session.commit()
  131. deleted_count = cast(int, result.rowcount)
  132. logger.info("Deleted %s workflow runs by IDs", deleted_count)
  133. return deleted_count
  134. def delete_runs_by_app(
  135. self,
  136. tenant_id: str,
  137. app_id: str,
  138. batch_size: int = 1000,
  139. ) -> int:
  140. """
  141. Delete all workflow runs for a specific app in batches.
  142. """
  143. total_deleted = 0
  144. while True:
  145. with self._session_maker() as session:
  146. # Get a batch of run IDs to delete
  147. stmt = (
  148. select(WorkflowRun.id)
  149. .where(
  150. WorkflowRun.tenant_id == tenant_id,
  151. WorkflowRun.app_id == app_id,
  152. )
  153. .limit(batch_size)
  154. )
  155. run_ids = session.scalars(stmt).all()
  156. if not run_ids:
  157. break
  158. # Delete the batch
  159. delete_stmt = delete(WorkflowRun).where(WorkflowRun.id.in_(run_ids))
  160. result = session.execute(delete_stmt)
  161. session.commit()
  162. batch_deleted = result.rowcount
  163. total_deleted += batch_deleted
  164. logger.info("Deleted batch of %s workflow runs for app %s", batch_deleted, app_id)
  165. # If we deleted fewer records than the batch size, we're done
  166. if batch_deleted < batch_size:
  167. break
  168. logger.info("Total deleted %s workflow runs for app %s", total_deleted, app_id)
  169. return total_deleted