kodit 0.5.3__py3-none-any.whl → 0.5.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kodit might be problematic. Click here for more details.

Files changed (55) hide show
  1. kodit/_version.py +2 -2
  2. kodit/application/factories/server_factory.py +54 -32
  3. kodit/application/services/code_search_application_service.py +89 -12
  4. kodit/application/services/commit_indexing_application_service.py +314 -195
  5. kodit/application/services/enrichment_query_service.py +274 -43
  6. kodit/application/services/indexing_worker_service.py +1 -1
  7. kodit/application/services/queue_service.py +15 -10
  8. kodit/application/services/sync_scheduler.py +2 -1
  9. kodit/domain/enrichments/architecture/architecture.py +1 -1
  10. kodit/domain/enrichments/architecture/physical/physical.py +1 -1
  11. kodit/domain/enrichments/development/development.py +1 -1
  12. kodit/domain/enrichments/development/snippet/snippet.py +12 -5
  13. kodit/domain/enrichments/enrichment.py +31 -4
  14. kodit/domain/enrichments/usage/api_docs.py +1 -1
  15. kodit/domain/enrichments/usage/usage.py +1 -1
  16. kodit/domain/entities/git.py +30 -25
  17. kodit/domain/factories/git_repo_factory.py +20 -5
  18. kodit/domain/protocols.py +56 -125
  19. kodit/domain/services/embedding_service.py +14 -16
  20. kodit/domain/services/git_repository_service.py +60 -38
  21. kodit/domain/services/git_service.py +18 -11
  22. kodit/domain/tracking/resolution_service.py +6 -16
  23. kodit/domain/value_objects.py +2 -9
  24. kodit/infrastructure/api/v1/dependencies.py +12 -3
  25. kodit/infrastructure/api/v1/query_params.py +27 -0
  26. kodit/infrastructure/api/v1/routers/commits.py +91 -85
  27. kodit/infrastructure/api/v1/routers/repositories.py +53 -37
  28. kodit/infrastructure/api/v1/routers/search.py +1 -1
  29. kodit/infrastructure/api/v1/schemas/enrichment.py +14 -0
  30. kodit/infrastructure/api/v1/schemas/repository.py +1 -1
  31. kodit/infrastructure/providers/litellm_provider.py +23 -1
  32. kodit/infrastructure/slicing/api_doc_extractor.py +0 -2
  33. kodit/infrastructure/sqlalchemy/embedding_repository.py +44 -34
  34. kodit/infrastructure/sqlalchemy/enrichment_association_repository.py +73 -0
  35. kodit/infrastructure/sqlalchemy/enrichment_v2_repository.py +116 -97
  36. kodit/infrastructure/sqlalchemy/entities.py +12 -116
  37. kodit/infrastructure/sqlalchemy/git_branch_repository.py +52 -244
  38. kodit/infrastructure/sqlalchemy/git_commit_repository.py +35 -324
  39. kodit/infrastructure/sqlalchemy/git_file_repository.py +70 -0
  40. kodit/infrastructure/sqlalchemy/git_repository.py +60 -230
  41. kodit/infrastructure/sqlalchemy/git_tag_repository.py +53 -240
  42. kodit/infrastructure/sqlalchemy/query.py +331 -0
  43. kodit/infrastructure/sqlalchemy/repository.py +203 -0
  44. kodit/infrastructure/sqlalchemy/task_repository.py +79 -58
  45. kodit/infrastructure/sqlalchemy/task_status_repository.py +45 -52
  46. kodit/migrations/versions/4b1a3b2c8fa5_refactor_git_tracking.py +190 -0
  47. {kodit-0.5.3.dist-info → kodit-0.5.5.dist-info}/METADATA +1 -1
  48. {kodit-0.5.3.dist-info → kodit-0.5.5.dist-info}/RECORD +51 -49
  49. kodit/infrastructure/mappers/enrichment_mapper.py +0 -83
  50. kodit/infrastructure/mappers/git_mapper.py +0 -193
  51. kodit/infrastructure/mappers/snippet_mapper.py +0 -104
  52. kodit/infrastructure/sqlalchemy/snippet_v2_repository.py +0 -479
  53. {kodit-0.5.3.dist-info → kodit-0.5.5.dist-info}/WHEEL +0 -0
  54. {kodit-0.5.3.dist-info → kodit-0.5.5.dist-info}/entry_points.txt +0 -0
  55. {kodit-0.5.3.dist-info → kodit-0.5.5.dist-info}/licenses/LICENSE +0 -0
@@ -1,15 +1,15 @@
1
1
  """SQLAlchemy implementation of GitRepoRepository."""
2
2
 
3
3
  from collections.abc import Callable
4
+ from typing import Any, override
4
5
 
5
6
  from pydantic import AnyUrl
6
- from sqlalchemy import delete, select
7
7
  from sqlalchemy.ext.asyncio import AsyncSession
8
8
 
9
- from kodit.domain.entities.git import GitRepo
9
+ from kodit.domain.entities.git import GitRepo, TrackingConfig, TrackingType
10
10
  from kodit.domain.protocols import GitRepoRepository
11
- from kodit.infrastructure.mappers.git_mapper import GitMapper
12
11
  from kodit.infrastructure.sqlalchemy import entities as db_entities
12
+ from kodit.infrastructure.sqlalchemy.repository import SqlAlchemyRepository
13
13
  from kodit.infrastructure.sqlalchemy.unit_of_work import SqlAlchemyUnitOfWork
14
14
 
15
15
 
@@ -20,243 +20,73 @@ def create_git_repo_repository(
20
20
  return SqlAlchemyGitRepoRepository(session_factory=session_factory)
21
21
 
22
22
 
23
- class SqlAlchemyGitRepoRepository(GitRepoRepository):
24
- """SQLAlchemy implementation of GitRepoRepository.
23
+ class SqlAlchemyGitRepoRepository(
24
+ SqlAlchemyRepository[GitRepo, db_entities.GitRepo], GitRepoRepository
25
+ ):
26
+ """SQLAlchemy implementation of GitRepoRepository."""
25
27
 
26
- This repository manages the GitRepo aggregate, including:
27
- - GitRepo entity
28
- - GitBranch entities
29
- - GitTag entities
30
-
31
- Note: Commits are now managed by the separate GitCommitRepository.
32
- """
33
-
34
- def __init__(self, session_factory: Callable[[], AsyncSession]) -> None:
35
- """Initialize the repository."""
36
- self.session_factory = session_factory
28
+ def _get_id(self, entity: GitRepo) -> Any:
29
+ return entity.id
37
30
 
38
31
  @property
39
- def _mapper(self) -> GitMapper:
40
- return GitMapper()
32
+ def db_entity_type(self) -> type[db_entities.GitRepo]:
33
+ """The SQLAlchemy model type."""
34
+ return db_entities.GitRepo
41
35
 
42
- async def save(self, repo: GitRepo) -> GitRepo:
43
- """Save or update a repository with all its branches, commits, and tags."""
36
+ @override
37
+ async def save(self, entity: GitRepo) -> GitRepo:
38
+ """Save entity (create new or update existing)."""
44
39
  async with SqlAlchemyUnitOfWork(self.session_factory) as session:
45
- # 1. Save or update the GitRepo entity
46
- # Check if repo exists by URI (for new repos from domain)
47
- existing_repo_stmt = select(db_entities.GitRepo).where(
48
- db_entities.GitRepo.sanitized_remote_uri
49
- == str(repo.sanitized_remote_uri)
40
+ entity_id = self._get_id(entity)
41
+ # Skip session.get if entity_id is None (new entity not yet persisted)
42
+ existing_db_entity = (
43
+ await session.get(self.db_entity_type, entity_id)
44
+ if entity_id is not None
45
+ else None
50
46
  )
51
- existing_repo = await session.scalar(existing_repo_stmt)
52
47
 
53
- if existing_repo:
54
- # Update existing repo found by URI
55
- existing_repo.remote_uri = str(repo.remote_uri)
56
- existing_repo.cloned_path = repo.cloned_path
57
- existing_repo.last_scanned_at = repo.last_scanned_at
58
- existing_repo.num_commits = repo.num_commits
59
- existing_repo.num_branches = repo.num_branches
60
- existing_repo.num_tags = repo.num_tags
61
- db_repo = existing_repo
62
- repo.id = existing_repo.id # Set the domain ID
48
+ if existing_db_entity:
49
+ # Update existing entity
50
+ new_db_entity = self.to_db(entity)
51
+ self._update_db_entity(existing_db_entity, new_db_entity)
52
+ db_entity = existing_db_entity
63
53
  else:
64
- # Create new repo
65
- db_repo = db_entities.GitRepo(
66
- sanitized_remote_uri=str(repo.sanitized_remote_uri),
67
- remote_uri=str(repo.remote_uri),
68
- cloned_path=repo.cloned_path,
69
- last_scanned_at=repo.last_scanned_at,
70
- num_commits=repo.num_commits,
71
- num_branches=repo.num_branches,
72
- num_tags=repo.num_tags,
73
- )
74
- session.add(db_repo)
75
- await session.flush() # Get the new ID
76
- repo.id = db_repo.id # Set the domain ID
77
-
78
- # 2. Save tracking branch
79
- await self._save_tracking_branch(session, repo)
54
+ # Create new entity
55
+ db_entity = self.to_db(entity)
56
+ session.add(db_entity)
80
57
 
81
58
  await session.flush()
82
- return repo
83
-
84
-
85
-
86
- async def _save_tracking_branch(self, session: AsyncSession, repo: GitRepo) -> None:
87
- """Save tracking branch if it doesn't exist."""
88
- if not repo.tracking_branch:
89
- return
90
-
91
- existing_tracking_branch = await session.get(
92
- db_entities.GitTrackingBranch, [repo.id, repo.tracking_branch.name]
93
- )
94
- if not existing_tracking_branch and repo.id is not None:
95
- db_tracking_branch = db_entities.GitTrackingBranch(
96
- repo_id=repo.id,
97
- name=repo.tracking_branch.name,
98
- )
99
- session.add(db_tracking_branch)
100
-
101
-
102
- async def get_by_id(self, repo_id: int) -> GitRepo:
103
- """Get repository by ID with all associated data."""
104
- async with SqlAlchemyUnitOfWork(self.session_factory) as session:
105
- db_repo = await session.get(db_entities.GitRepo, repo_id)
106
- if not db_repo:
107
- raise ValueError(f"Repository with ID {repo_id} not found")
108
-
109
- return await self._load_complete_repo(session, db_repo)
110
-
111
- async def get_by_uri(self, sanitized_uri: AnyUrl) -> GitRepo:
112
- """Get repository by sanitized URI with all associated data."""
113
- async with SqlAlchemyUnitOfWork(self.session_factory) as session:
114
- stmt = select(db_entities.GitRepo).where(
115
- db_entities.GitRepo.sanitized_remote_uri == str(sanitized_uri)
116
- )
117
- db_repo = await session.scalar(stmt)
118
- if not db_repo:
119
- raise ValueError(f"Repository with URI {sanitized_uri} not found")
120
-
121
- return await self._load_complete_repo(session, db_repo)
122
-
123
- async def get_by_commit(self, commit_sha: str) -> GitRepo:
124
- """Get repository by commit SHA with all associated data."""
125
- async with SqlAlchemyUnitOfWork(self.session_factory) as session:
126
- # Find the commit first
127
- stmt = select(db_entities.GitCommit).where(
128
- db_entities.GitCommit.commit_sha == commit_sha
129
- )
130
- db_commit = await session.scalar(stmt)
131
- if not db_commit:
132
- raise ValueError(f"Commit with SHA {commit_sha} not found")
133
-
134
- # Get the repo
135
- db_repo = await session.get(db_entities.GitRepo, db_commit.repo_id)
136
- if not db_repo:
137
- raise ValueError(f"Repository with commit SHA {commit_sha} not found")
138
-
139
- return await self._load_complete_repo(session, db_repo)
140
-
141
- async def get_all(self) -> list[GitRepo]:
142
- """Get all repositories."""
143
- async with SqlAlchemyUnitOfWork(self.session_factory) as session:
144
- stmt = select(db_entities.GitRepo)
145
- db_repos = (await session.scalars(stmt)).all()
146
-
147
- repos = []
148
- for db_repo in db_repos:
149
- repo = await self._load_complete_repo(session, db_repo)
150
- repos.append(repo)
151
-
152
- return repos
153
-
154
- async def delete(self, sanitized_uri: AnyUrl) -> bool:
155
- """Delete only the repository entity itself.
156
-
157
- According to DDD principles, this repository should only delete
158
- the GitRepo entity it directly controls. Related entities (commits,
159
- branches, tags, snippets) should be deleted by their respective
160
- repositories before calling this method.
161
- """
162
- async with SqlAlchemyUnitOfWork(self.session_factory) as session:
163
- # Find the repo
164
- stmt = select(db_entities.GitRepo).where(
165
- db_entities.GitRepo.sanitized_remote_uri == str(sanitized_uri)
166
- )
167
- db_repo = await session.scalar(stmt)
168
- if not db_repo:
169
- return False
170
-
171
- # Delete tracking branches first (they reference the repo)
172
- del_tracking_branches_stmt = delete(db_entities.GitTrackingBranch).where(
173
- db_entities.GitTrackingBranch.repo_id == db_repo.id
174
- )
175
- await session.execute(del_tracking_branches_stmt)
176
-
177
- # Delete only the repo entity itself
178
- # Foreign key constraints will prevent deletion if related entities exist
179
- del_stmt = delete(db_entities.GitRepo).where(
180
- db_entities.GitRepo.id == db_repo.id
181
- )
182
- await session.execute(del_stmt)
183
- return True
184
-
185
-
186
- async def _load_complete_repo(
187
- self, session: AsyncSession, db_repo: db_entities.GitRepo
188
- ) -> GitRepo:
189
- """Load a complete repo with all its associations."""
190
- all_tags = list(
191
- (
192
- await session.scalars(
193
- select(db_entities.GitTag).where(
194
- db_entities.GitTag.repo_id == db_repo.id
195
- )
196
- )
197
- ).all()
59
+ return self.to_domain(db_entity)
60
+
61
+ @staticmethod
62
+ def to_domain(db_entity: db_entities.GitRepo) -> GitRepo:
63
+ """Map database entity to domain entity."""
64
+ return GitRepo(
65
+ id=db_entity.id,
66
+ sanitized_remote_uri=AnyUrl(db_entity.sanitized_remote_uri),
67
+ remote_uri=AnyUrl(db_entity.remote_uri),
68
+ cloned_path=db_entity.cloned_path,
69
+ last_scanned_at=db_entity.last_scanned_at,
70
+ num_commits=db_entity.num_commits,
71
+ num_branches=db_entity.num_branches,
72
+ num_tags=db_entity.num_tags,
73
+ tracking_config=TrackingConfig(
74
+ type=TrackingType(db_entity.tracking_type),
75
+ name=db_entity.tracking_name,
76
+ ),
198
77
  )
199
- tracking_branch = await session.scalar(
200
- select(db_entities.GitTrackingBranch).where(
201
- db_entities.GitTrackingBranch.repo_id == db_repo.id
202
- )
203
- )
204
-
205
- # Get tracking branch from branches table if needed
206
- db_tracking_branch_entity = None
207
- if tracking_branch:
208
- db_tracking_branch_entity = await session.scalar(
209
- select(db_entities.GitBranch).where(
210
- db_entities.GitBranch.repo_id == db_repo.id,
211
- db_entities.GitBranch.name == tracking_branch.name,
212
- )
213
- )
214
-
215
- # Get only commits needed for tags and tracking branch
216
- referenced_commit_shas = set()
217
- for tag in all_tags:
218
- referenced_commit_shas.add(tag.target_commit_sha)
219
- if db_tracking_branch_entity:
220
- referenced_commit_shas.add(db_tracking_branch_entity.head_commit_sha)
221
-
222
- # Load only the referenced commits in chunks to avoid parameter limits
223
- referenced_commits = []
224
- referenced_files = []
225
- if referenced_commit_shas:
226
- commit_shas_list = list(referenced_commit_shas)
227
- chunk_size = 1000
228
-
229
- for i in range(0, len(commit_shas_list), chunk_size):
230
- chunk = commit_shas_list[i : i + chunk_size]
231
- chunk_commits = list(
232
- (
233
- await session.scalars(
234
- select(db_entities.GitCommit).where(
235
- db_entities.GitCommit.commit_sha.in_(chunk)
236
- )
237
- )
238
- ).all()
239
- )
240
- referenced_commits.extend(chunk_commits)
241
-
242
- for i in range(0, len(commit_shas_list), chunk_size):
243
- chunk = commit_shas_list[i : i + chunk_size]
244
- chunk_files = list(
245
- (
246
- await session.scalars(
247
- select(db_entities.GitCommitFile).where(
248
- db_entities.GitCommitFile.commit_sha.in_(chunk)
249
- )
250
- )
251
- ).all()
252
- )
253
- referenced_files.extend(chunk_files)
254
78
 
255
- return self._mapper.to_domain_git_repo(
256
- db_repo=db_repo,
257
- db_tracking_branch_entity=db_tracking_branch_entity,
258
- db_commits=referenced_commits,
259
- db_tags=all_tags,
260
- db_commit_files=referenced_files,
261
- db_tracking_branch=tracking_branch,
79
+ @staticmethod
80
+ def to_db(domain_entity: GitRepo) -> db_entities.GitRepo:
81
+ """Map domain entity to database entity."""
82
+ return db_entities.GitRepo(
83
+ sanitized_remote_uri=str(domain_entity.sanitized_remote_uri),
84
+ remote_uri=str(domain_entity.remote_uri),
85
+ cloned_path=domain_entity.cloned_path,
86
+ last_scanned_at=domain_entity.last_scanned_at,
87
+ num_commits=domain_entity.num_commits,
88
+ num_branches=domain_entity.num_branches,
89
+ num_tags=domain_entity.num_tags,
90
+ tracking_type=domain_entity.tracking_config.type,
91
+ tracking_name=domain_entity.tracking_config.name,
262
92
  )
@@ -2,13 +2,13 @@
2
2
 
3
3
  from collections.abc import Callable
4
4
 
5
- from sqlalchemy import delete, func, insert, select
6
5
  from sqlalchemy.ext.asyncio import AsyncSession
7
6
 
8
- from kodit.domain.entities.git import GitCommit, GitFile, GitTag
7
+ from kodit.domain.entities.git import GitTag
9
8
  from kodit.domain.protocols import GitTagRepository
10
9
  from kodit.infrastructure.sqlalchemy import entities as db_entities
11
- from kodit.infrastructure.sqlalchemy.unit_of_work import SqlAlchemyUnitOfWork
10
+ from kodit.infrastructure.sqlalchemy.query import FilterOperator, QueryBuilder
11
+ from kodit.infrastructure.sqlalchemy.repository import SqlAlchemyRepository
12
12
 
13
13
 
14
14
  def create_git_tag_repository(
@@ -18,251 +18,64 @@ def create_git_tag_repository(
18
18
  return SqlAlchemyGitTagRepository(session_factory=session_factory)
19
19
 
20
20
 
21
- class SqlAlchemyGitTagRepository(GitTagRepository):
21
+ class SqlAlchemyGitTagRepository(
22
+ SqlAlchemyRepository[GitTag, db_entities.GitTag], GitTagRepository
23
+ ):
22
24
  """SQLAlchemy implementation of GitTagRepository."""
23
25
 
24
- def __init__(self, session_factory: Callable[[], AsyncSession]) -> None:
25
- """Initialize the repository."""
26
- self.session_factory = session_factory
26
+ def _get_id(self, entity: GitTag) -> tuple[int, str]:
27
+ """Get the ID of a tag."""
28
+ if entity.repo_id is None:
29
+ raise ValueError("Repository ID is required")
30
+ return (entity.repo_id, entity.name)
31
+
32
+ @property
33
+ def db_entity_type(self) -> type[db_entities.GitTag]:
34
+ """The SQLAlchemy model type."""
35
+ return db_entities.GitTag
36
+
37
+ @staticmethod
38
+ def to_domain(db_entity: db_entities.GitTag) -> GitTag:
39
+ """Map a SQLAlchemy GitTag to a domain GitTag."""
40
+ return GitTag(
41
+ created_at=db_entity.created_at,
42
+ updated_at=db_entity.updated_at,
43
+ repo_id=db_entity.repo_id,
44
+ name=db_entity.name,
45
+ target_commit_sha=db_entity.target_commit_sha,
46
+ )
47
+
48
+ @staticmethod
49
+ def to_db(domain_entity: GitTag) -> db_entities.GitTag:
50
+ """Map a domain GitTag to a SQLAlchemy GitTag."""
51
+ if domain_entity.repo_id is None:
52
+ raise ValueError("Repository ID is required")
53
+ return db_entities.GitTag(
54
+ repo_id=domain_entity.repo_id,
55
+ name=domain_entity.name,
56
+ target_commit_sha=domain_entity.target_commit_sha,
57
+ )
27
58
 
28
59
  async def get_by_name(self, tag_name: str, repo_id: int) -> GitTag:
29
60
  """Get a tag by name and repository ID."""
30
- async with SqlAlchemyUnitOfWork(self.session_factory) as session:
31
- # Get the tag
32
- stmt = select(db_entities.GitTag).where(
33
- db_entities.GitTag.name == tag_name,
34
- db_entities.GitTag.repo_id == repo_id,
35
- )
36
- db_tag = await session.scalar(stmt)
37
- if not db_tag:
38
- raise ValueError(f"Tag {tag_name} not found in repo {repo_id}")
39
-
40
- # Get the target commit
41
- commit_stmt = select(db_entities.GitCommit).where(
42
- db_entities.GitCommit.commit_sha == db_tag.target_commit_sha
43
- )
44
- db_commit = await session.scalar(commit_stmt)
45
- if not db_commit:
46
- raise ValueError(f"Target commit {db_tag.target_commit_sha} not found")
47
-
48
- # Get files for the target commit
49
- files_stmt = select(db_entities.GitCommitFile).where(
50
- db_entities.GitCommitFile.commit_sha == db_tag.target_commit_sha
51
- )
52
- db_files = (await session.scalars(files_stmt)).all()
53
-
54
- domain_files = []
55
- for db_file in db_files:
56
- domain_file = GitFile(
57
- blob_sha=db_file.blob_sha,
58
- path=db_file.path,
59
- mime_type=db_file.mime_type,
60
- size=db_file.size,
61
- extension=db_file.extension,
62
- created_at=db_file.created_at,
63
- )
64
- domain_files.append(domain_file)
65
-
66
- target_commit = GitCommit(
67
- commit_sha=db_commit.commit_sha,
68
- date=db_commit.date,
69
- message=db_commit.message,
70
- parent_commit_sha=db_commit.parent_commit_sha,
71
- files=domain_files,
72
- author=db_commit.author,
73
- created_at=db_commit.created_at,
74
- updated_at=db_commit.updated_at,
75
- )
76
-
77
- return GitTag(
78
- repo_id=db_tag.repo_id,
79
- name=db_tag.name,
80
- target_commit=target_commit,
81
- created_at=db_tag.created_at,
82
- updated_at=db_tag.updated_at,
83
- )
61
+ query = (
62
+ QueryBuilder()
63
+ .filter("name", FilterOperator.EQ, tag_name)
64
+ .filter("repo_id", FilterOperator.EQ, repo_id)
65
+ )
66
+ tags = await self.find(query)
67
+ if not tags:
68
+ raise ValueError(f"Tag {tag_name} not found in repo {repo_id}")
69
+ return tags[0]
84
70
 
85
71
  async def get_by_repo_id(self, repo_id: int) -> list[GitTag]:
86
72
  """Get all tags for a repository."""
87
- async with SqlAlchemyUnitOfWork(self.session_factory) as session:
88
- # Get all tags for the repo
89
- tags_stmt = select(db_entities.GitTag).where(
90
- db_entities.GitTag.repo_id == repo_id
91
- )
92
- db_tags = (await session.scalars(tags_stmt)).all()
93
-
94
- if not db_tags:
95
- return []
96
-
97
- commit_shas = [tag.target_commit_sha for tag in db_tags]
98
-
99
- # Get all target commits for these tags in chunks
100
- # to avoid parameter limits
101
- db_commits: list[db_entities.GitCommit] = []
102
- chunk_size = 1000
103
- for i in range(0, len(commit_shas), chunk_size):
104
- chunk = commit_shas[i : i + chunk_size]
105
- commits_stmt = select(db_entities.GitCommit).where(
106
- db_entities.GitCommit.commit_sha.in_(chunk)
107
- )
108
- chunk_commits = (await session.scalars(commits_stmt)).all()
109
- db_commits.extend(chunk_commits)
110
-
111
- # Get all files for these commits in chunks
112
- # to avoid parameter limits
113
- db_files: list[db_entities.GitCommitFile] = []
114
- for i in range(0, len(commit_shas), chunk_size):
115
- chunk = commit_shas[i : i + chunk_size]
116
- files_stmt = select(db_entities.GitCommitFile).where(
117
- db_entities.GitCommitFile.commit_sha.in_(chunk)
118
- )
119
- chunk_files = (await session.scalars(files_stmt)).all()
120
- db_files.extend(chunk_files)
121
-
122
- # Group files by commit SHA
123
- files_by_commit: dict[str, list[GitFile]] = {}
124
- for db_file in db_files:
125
- if db_file.commit_sha not in files_by_commit:
126
- files_by_commit[db_file.commit_sha] = []
127
-
128
- domain_file = GitFile(
129
- blob_sha=db_file.blob_sha,
130
- path=db_file.path,
131
- mime_type=db_file.mime_type,
132
- size=db_file.size,
133
- extension=db_file.extension,
134
- created_at=db_file.created_at,
135
- )
136
- files_by_commit[db_file.commit_sha].append(domain_file)
137
-
138
- # Create commit lookup
139
- commits_by_sha = {commit.commit_sha: commit for commit in db_commits}
140
-
141
- # Create domain tags
142
- domain_tags = []
143
- for db_tag in db_tags:
144
- db_commit = commits_by_sha.get(db_tag.target_commit_sha)
145
- if not db_commit:
146
- continue
147
-
148
- commit_files = files_by_commit.get(db_tag.target_commit_sha, [])
149
- target_commit = GitCommit(
150
- commit_sha=db_commit.commit_sha,
151
- date=db_commit.date,
152
- message=db_commit.message,
153
- parent_commit_sha=db_commit.parent_commit_sha,
154
- files=commit_files,
155
- author=db_commit.author,
156
- created_at=db_commit.created_at,
157
- updated_at=db_commit.updated_at,
158
- )
159
-
160
- domain_tag = GitTag(
161
- repo_id=db_tag.repo_id,
162
- name=db_tag.name,
163
- target_commit=target_commit,
164
- created_at=db_tag.created_at,
165
- updated_at=db_tag.updated_at,
166
- )
167
- domain_tags.append(domain_tag)
168
-
169
- return domain_tags
170
-
171
- async def save(self, tag: GitTag, repo_id: int) -> GitTag:
172
- """Save a tag to a repository."""
173
- async with SqlAlchemyUnitOfWork(self.session_factory) as session:
174
- # Set repo_id on the tag
175
- tag.repo_id = repo_id
176
-
177
- # Check if tag already exists
178
- existing_tag = await session.get(
179
- db_entities.GitTag, (repo_id, tag.name)
180
- )
181
-
182
- if existing_tag:
183
- # Update existing tag
184
- existing_tag.target_commit_sha = tag.target_commit.commit_sha
185
- if tag.updated_at:
186
- existing_tag.updated_at = tag.updated_at
187
- else:
188
- # Create new tag
189
- db_tag = db_entities.GitTag(
190
- repo_id=repo_id,
191
- name=tag.name,
192
- target_commit_sha=tag.target_commit.commit_sha,
193
- )
194
- session.add(db_tag)
195
-
196
- return tag
197
-
198
- async def save_bulk(self, tags: list[GitTag], repo_id: int) -> None:
199
- """Bulk save tags to a repository."""
200
- if not tags:
201
- return
202
-
203
- async with SqlAlchemyUnitOfWork(self.session_factory) as session:
204
- # Get existing tags in bulk
205
- existing_tags_stmt = select(db_entities.GitTag).where(
206
- db_entities.GitTag.repo_id == repo_id,
207
- db_entities.GitTag.name.in_([tag.name for tag in tags]),
208
- )
209
- existing_tags = (await session.scalars(existing_tags_stmt)).all()
210
- existing_tag_names = {tag.name for tag in existing_tags}
211
-
212
- # Update existing tags
213
- for existing_tag in existing_tags:
214
- for tag in tags:
215
- if (
216
- tag.name == existing_tag.name
217
- and existing_tag.target_commit_sha
218
- != tag.target_commit.commit_sha
219
- ):
220
- existing_tag.target_commit_sha = tag.target_commit.commit_sha
221
- break
222
-
223
- # Prepare new tags for bulk insert
224
- new_tags_data = [
225
- {
226
- "repo_id": repo_id,
227
- "name": tag.name,
228
- "target_commit_sha": tag.target_commit.commit_sha,
229
- }
230
- for tag in tags
231
- if tag.name not in existing_tag_names
232
- ]
233
-
234
- # Bulk insert new tags in chunks to avoid parameter limits
235
- if new_tags_data:
236
- chunk_size = 1000 # Conservative chunk size for parameter limits
237
- for i in range(0, len(new_tags_data), chunk_size):
238
- chunk = new_tags_data[i : i + chunk_size]
239
- stmt = insert(db_entities.GitTag).values(chunk)
240
- await session.execute(stmt)
241
-
242
- async def exists(self, tag_name: str, repo_id: int) -> bool:
243
- """Check if a tag exists."""
244
- async with SqlAlchemyUnitOfWork(self.session_factory) as session:
245
- stmt = select(db_entities.GitTag.name).where(
246
- db_entities.GitTag.name == tag_name,
247
- db_entities.GitTag.repo_id == repo_id,
248
- )
249
- result = await session.scalar(stmt)
250
- return result is not None
73
+ return await self.find(
74
+ QueryBuilder().filter("repo_id", FilterOperator.EQ, repo_id)
75
+ )
251
76
 
252
77
  async def delete_by_repo_id(self, repo_id: int) -> None:
253
78
  """Delete all tags for a repository."""
254
- async with SqlAlchemyUnitOfWork(self.session_factory) as session:
255
- # Delete tags
256
- del_tags_stmt = delete(db_entities.GitTag).where(
257
- db_entities.GitTag.repo_id == repo_id
258
- )
259
- await session.execute(del_tags_stmt)
260
-
261
- async def count_by_repo_id(self, repo_id: int) -> int:
262
- """Count the number of tags for a repository."""
263
- async with SqlAlchemyUnitOfWork(self.session_factory) as session:
264
- stmt = select(func.count()).select_from(db_entities.GitTag).where(
265
- db_entities.GitTag.repo_id == repo_id
266
- )
267
- result = await session.scalar(stmt)
268
- return result or 0
79
+ await self.delete_by_query(
80
+ QueryBuilder().filter("repo_id", FilterOperator.EQ, repo_id)
81
+ )