kodit 0.4.3__py3-none-any.whl → 0.5.1__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 (135) hide show
  1. kodit/_version.py +2 -2
  2. kodit/app.py +51 -23
  3. kodit/application/factories/reporting_factory.py +6 -2
  4. kodit/application/factories/server_factory.py +353 -0
  5. kodit/application/services/code_search_application_service.py +144 -0
  6. kodit/application/services/commit_indexing_application_service.py +700 -0
  7. kodit/application/services/indexing_worker_service.py +13 -44
  8. kodit/application/services/queue_service.py +24 -3
  9. kodit/application/services/reporting.py +0 -2
  10. kodit/application/services/sync_scheduler.py +15 -31
  11. kodit/cli.py +2 -753
  12. kodit/cli_utils.py +2 -9
  13. kodit/config.py +4 -97
  14. kodit/database.py +38 -1
  15. kodit/domain/enrichments/__init__.py +1 -0
  16. kodit/domain/enrichments/architecture/__init__.py +1 -0
  17. kodit/domain/enrichments/architecture/architecture.py +20 -0
  18. kodit/domain/enrichments/architecture/physical/__init__.py +1 -0
  19. kodit/domain/enrichments/architecture/physical/discovery_notes.py +14 -0
  20. kodit/domain/enrichments/architecture/physical/formatter.py +11 -0
  21. kodit/domain/enrichments/architecture/physical/physical.py +17 -0
  22. kodit/domain/enrichments/development/__init__.py +1 -0
  23. kodit/domain/enrichments/development/development.py +18 -0
  24. kodit/domain/enrichments/development/snippet/__init__.py +1 -0
  25. kodit/domain/enrichments/development/snippet/snippet.py +21 -0
  26. kodit/domain/enrichments/enricher.py +17 -0
  27. kodit/domain/enrichments/enrichment.py +39 -0
  28. kodit/domain/enrichments/request.py +12 -0
  29. kodit/domain/enrichments/response.py +11 -0
  30. kodit/domain/enrichments/usage/__init__.py +1 -0
  31. kodit/domain/enrichments/usage/api_docs.py +19 -0
  32. kodit/domain/enrichments/usage/usage.py +18 -0
  33. kodit/domain/{entities.py → entities/__init__.py} +50 -195
  34. kodit/domain/entities/git.py +190 -0
  35. kodit/domain/factories/__init__.py +1 -0
  36. kodit/domain/factories/git_repo_factory.py +76 -0
  37. kodit/domain/protocols.py +264 -64
  38. kodit/domain/services/bm25_service.py +5 -1
  39. kodit/domain/services/embedding_service.py +3 -0
  40. kodit/domain/services/enrichment_service.py +9 -30
  41. kodit/domain/services/git_repository_service.py +429 -0
  42. kodit/domain/services/git_service.py +300 -0
  43. kodit/domain/services/physical_architecture_service.py +182 -0
  44. kodit/domain/services/task_status_query_service.py +2 -2
  45. kodit/domain/value_objects.py +87 -135
  46. kodit/infrastructure/api/client/__init__.py +0 -2
  47. kodit/infrastructure/api/v1/__init__.py +0 -4
  48. kodit/infrastructure/api/v1/dependencies.py +92 -46
  49. kodit/infrastructure/api/v1/routers/__init__.py +0 -6
  50. kodit/infrastructure/api/v1/routers/commits.py +352 -0
  51. kodit/infrastructure/api/v1/routers/queue.py +2 -2
  52. kodit/infrastructure/api/v1/routers/repositories.py +282 -0
  53. kodit/infrastructure/api/v1/routers/search.py +31 -14
  54. kodit/infrastructure/api/v1/schemas/__init__.py +0 -24
  55. kodit/infrastructure/api/v1/schemas/commit.py +96 -0
  56. kodit/infrastructure/api/v1/schemas/context.py +2 -0
  57. kodit/infrastructure/api/v1/schemas/enrichment.py +29 -0
  58. kodit/infrastructure/api/v1/schemas/repository.py +128 -0
  59. kodit/infrastructure/api/v1/schemas/search.py +12 -9
  60. kodit/infrastructure/api/v1/schemas/snippet.py +58 -0
  61. kodit/infrastructure/api/v1/schemas/tag.py +31 -0
  62. kodit/infrastructure/api/v1/schemas/task_status.py +2 -0
  63. kodit/infrastructure/bm25/local_bm25_repository.py +16 -4
  64. kodit/infrastructure/bm25/vectorchord_bm25_repository.py +68 -52
  65. kodit/infrastructure/cloning/git/git_python_adaptor.py +534 -0
  66. kodit/infrastructure/cloning/git/working_copy.py +1 -1
  67. kodit/infrastructure/embedding/embedding_factory.py +3 -2
  68. kodit/infrastructure/embedding/local_vector_search_repository.py +1 -1
  69. kodit/infrastructure/embedding/vectorchord_vector_search_repository.py +111 -84
  70. kodit/infrastructure/enricher/__init__.py +1 -0
  71. kodit/infrastructure/enricher/enricher_factory.py +53 -0
  72. kodit/infrastructure/{enrichment/litellm_enrichment_provider.py → enricher/litellm_enricher.py} +36 -56
  73. kodit/infrastructure/{enrichment/local_enrichment_provider.py → enricher/local_enricher.py} +19 -24
  74. kodit/infrastructure/enricher/null_enricher.py +36 -0
  75. kodit/infrastructure/indexing/fusion_service.py +1 -1
  76. kodit/infrastructure/mappers/enrichment_mapper.py +83 -0
  77. kodit/infrastructure/mappers/git_mapper.py +193 -0
  78. kodit/infrastructure/mappers/snippet_mapper.py +104 -0
  79. kodit/infrastructure/mappers/task_mapper.py +5 -44
  80. kodit/infrastructure/physical_architecture/__init__.py +1 -0
  81. kodit/infrastructure/physical_architecture/detectors/__init__.py +1 -0
  82. kodit/infrastructure/physical_architecture/detectors/docker_compose_detector.py +336 -0
  83. kodit/infrastructure/physical_architecture/formatters/__init__.py +1 -0
  84. kodit/infrastructure/physical_architecture/formatters/narrative_formatter.py +149 -0
  85. kodit/infrastructure/reporting/log_progress.py +8 -5
  86. kodit/infrastructure/reporting/telemetry_progress.py +21 -0
  87. kodit/infrastructure/slicing/api_doc_extractor.py +836 -0
  88. kodit/infrastructure/slicing/ast_analyzer.py +1128 -0
  89. kodit/infrastructure/slicing/slicer.py +87 -421
  90. kodit/infrastructure/sqlalchemy/embedding_repository.py +43 -23
  91. kodit/infrastructure/sqlalchemy/enrichment_v2_repository.py +118 -0
  92. kodit/infrastructure/sqlalchemy/entities.py +402 -158
  93. kodit/infrastructure/sqlalchemy/git_branch_repository.py +274 -0
  94. kodit/infrastructure/sqlalchemy/git_commit_repository.py +346 -0
  95. kodit/infrastructure/sqlalchemy/git_repository.py +262 -0
  96. kodit/infrastructure/sqlalchemy/git_tag_repository.py +268 -0
  97. kodit/infrastructure/sqlalchemy/snippet_v2_repository.py +479 -0
  98. kodit/infrastructure/sqlalchemy/task_repository.py +29 -23
  99. kodit/infrastructure/sqlalchemy/task_status_repository.py +24 -12
  100. kodit/infrastructure/sqlalchemy/unit_of_work.py +10 -14
  101. kodit/mcp.py +12 -30
  102. kodit/migrations/env.py +1 -0
  103. kodit/migrations/versions/04b80f802e0c_foreign_key_review.py +100 -0
  104. kodit/migrations/versions/19f8c7faf8b9_add_generic_enrichment_type.py +260 -0
  105. kodit/migrations/versions/7f15f878c3a1_add_new_git_entities.py +690 -0
  106. kodit/migrations/versions/f9e5ef5e688f_add_git_commits_number.py +43 -0
  107. kodit/py.typed +0 -0
  108. kodit/utils/dump_config.py +361 -0
  109. kodit/utils/dump_openapi.py +6 -4
  110. kodit/utils/path_utils.py +29 -0
  111. {kodit-0.4.3.dist-info → kodit-0.5.1.dist-info}/METADATA +3 -3
  112. kodit-0.5.1.dist-info/RECORD +168 -0
  113. kodit/application/factories/code_indexing_factory.py +0 -195
  114. kodit/application/services/auto_indexing_service.py +0 -99
  115. kodit/application/services/code_indexing_application_service.py +0 -410
  116. kodit/domain/services/index_query_service.py +0 -70
  117. kodit/domain/services/index_service.py +0 -269
  118. kodit/infrastructure/api/client/index_client.py +0 -57
  119. kodit/infrastructure/api/v1/routers/indexes.py +0 -164
  120. kodit/infrastructure/api/v1/schemas/index.py +0 -101
  121. kodit/infrastructure/bm25/bm25_factory.py +0 -28
  122. kodit/infrastructure/cloning/__init__.py +0 -1
  123. kodit/infrastructure/cloning/metadata.py +0 -98
  124. kodit/infrastructure/enrichment/__init__.py +0 -1
  125. kodit/infrastructure/enrichment/enrichment_factory.py +0 -52
  126. kodit/infrastructure/enrichment/null_enrichment_provider.py +0 -19
  127. kodit/infrastructure/mappers/index_mapper.py +0 -345
  128. kodit/infrastructure/reporting/tdqm_progress.py +0 -38
  129. kodit/infrastructure/slicing/language_detection_service.py +0 -18
  130. kodit/infrastructure/sqlalchemy/index_repository.py +0 -646
  131. kodit-0.4.3.dist-info/RECORD +0 -125
  132. /kodit/infrastructure/{enrichment → enricher}/utils.py +0 -0
  133. {kodit-0.4.3.dist-info → kodit-0.5.1.dist-info}/WHEEL +0 -0
  134. {kodit-0.4.3.dist-info → kodit-0.5.1.dist-info}/entry_points.txt +0 -0
  135. {kodit-0.4.3.dist-info → kodit-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -14,17 +14,10 @@ class SqlAlchemyUnitOfWork:
14
14
  self._session_factory = session_factory
15
15
  self._session: AsyncSession | None = None
16
16
 
17
- @property
18
- def session(self) -> AsyncSession:
19
- """Get the current session."""
20
- if self._session is None:
21
- raise RuntimeError("UnitOfWork must be used within async context")
22
- return self._session
23
-
24
- async def __aenter__(self) -> "SqlAlchemyUnitOfWork":
17
+ async def __aenter__(self) -> "AsyncSession":
25
18
  """Enter the unit of work context."""
26
19
  self._session = self._session_factory()
27
- return self
20
+ return self._session
28
21
 
29
22
  async def __aexit__(
30
23
  self,
@@ -34,11 +27,14 @@ class SqlAlchemyUnitOfWork:
34
27
  ) -> None:
35
28
  """Exit the unit of work context."""
36
29
  if self._session:
37
- if exc_type is not None:
38
- await self._session.rollback()
39
- await self._session.commit()
40
- await self._session.close()
41
- self._session = None
30
+ try:
31
+ if exc_type is None: # Only commit if no exception
32
+ await self._session.commit()
33
+ else:
34
+ await self._session.rollback()
35
+ finally:
36
+ await self._session.close()
37
+ self._session = None
42
38
 
43
39
  async def commit(self) -> None:
44
40
  """Commit the current transaction."""
kodit/mcp.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """MCP server for kodit."""
2
2
 
3
- from collections.abc import AsyncIterator, Callable
3
+ from collections.abc import AsyncIterator
4
4
  from contextlib import asynccontextmanager
5
5
  from dataclasses import dataclass
6
6
  from pathlib import Path
@@ -9,35 +9,27 @@ from typing import Annotated
9
9
  import structlog
10
10
  from fastmcp import Context, FastMCP
11
11
  from pydantic import Field
12
- from sqlalchemy.ext.asyncio import AsyncSession
13
12
 
14
13
  from kodit._version import version
15
- from kodit.application.factories.code_indexing_factory import (
16
- create_code_indexing_application_service,
17
- )
18
- from kodit.application.factories.reporting_factory import create_server_operation
14
+ from kodit.application.factories.server_factory import ServerFactory
15
+ from kodit.application.services.code_search_application_service import MultiSearchResult
19
16
  from kodit.config import AppContext
20
17
  from kodit.database import Database
21
18
  from kodit.domain.value_objects import (
22
19
  MultiSearchRequest,
23
- MultiSearchResult,
24
20
  SnippetSearchFilters,
25
21
  )
26
- from kodit.infrastructure.sqlalchemy.task_status_repository import (
27
- create_task_status_repository,
28
- )
29
22
 
30
23
  # Global database connection for MCP server
31
24
  _mcp_db: Database | None = None
25
+ _mcp_server_factory: ServerFactory | None = None
32
26
 
33
27
 
34
28
  @dataclass
35
29
  class MCPContext:
36
30
  """Context for the MCP server."""
37
31
 
38
- session: AsyncSession
39
- session_factory: Callable[[], AsyncSession]
40
- app_context: AppContext
32
+ server_factory: ServerFactory
41
33
 
42
34
 
43
35
  @asynccontextmanager
@@ -55,16 +47,12 @@ async def mcp_lifespan(_: FastMCP) -> AsyncIterator[MCPContext]:
55
47
  Since they don't provide a good way to handle global state, we must use a
56
48
  global variable to store the database connection.
57
49
  """
58
- global _mcp_db # noqa: PLW0603
59
- app_context = AppContext()
60
- if _mcp_db is None:
61
- _mcp_db = await app_context.get_db()
62
- async with _mcp_db.session_factory() as session:
63
- yield MCPContext(
64
- session=session,
65
- app_context=app_context,
66
- session_factory=_mcp_db.session_factory,
67
- )
50
+ global _mcp_server_factory # noqa: PLW0603
51
+ if _mcp_server_factory is None:
52
+ app_context = AppContext()
53
+ db = await app_context.get_db()
54
+ _mcp_server_factory = ServerFactory(app_context, db.session_factory)
55
+ yield MCPContext(_mcp_server_factory)
68
56
 
69
57
 
70
58
  def create_mcp_server(name: str, instructions: str | None = None) -> FastMCP:
@@ -180,13 +168,7 @@ def register_mcp_tools(mcp_server: FastMCP) -> None:
180
168
  mcp_context: MCPContext = ctx.request_context.lifespan_context
181
169
 
182
170
  # Use the unified application service
183
- service = create_code_indexing_application_service(
184
- app_context=mcp_context.app_context,
185
- session_factory=mcp_context.session_factory,
186
- operation=create_server_operation(
187
- create_task_status_repository(mcp_context.session_factory)
188
- ),
189
- )
171
+ service = mcp_context.server_factory.code_search_application_service()
190
172
 
191
173
  log.debug("Searching for snippets")
192
174
 
kodit/migrations/env.py CHANGED
@@ -41,6 +41,7 @@ def run_migrations_offline() -> None:
41
41
  target_metadata=target_metadata,
42
42
  literal_binds=True,
43
43
  dialect_opts={"paramstyle": "named"},
44
+ render_as_batch=True,
44
45
  )
45
46
 
46
47
  with context.begin_transaction():
@@ -0,0 +1,100 @@
1
+ # ruff: noqa
2
+ """foreign key review
3
+
4
+ Revision ID: 04b80f802e0c
5
+ Revises: 7f15f878c3a1
6
+ Create Date: 2025-09-22 11:21:43.432880
7
+
8
+ """
9
+
10
+ from typing import Sequence, Union
11
+
12
+ from alembic import op
13
+ import sqlalchemy as sa
14
+
15
+
16
+ # revision identifiers, used by Alembic.
17
+ revision: str = "04b80f802e0c"
18
+ down_revision: Union[str, None] = "7f15f878c3a1"
19
+ branch_labels: Union[str, Sequence[str], None] = None
20
+ depends_on: Union[str, Sequence[str], None] = None
21
+
22
+
23
+ def upgrade() -> None:
24
+ """Upgrade schema."""
25
+ # SQLite doesn't support complex constraint alterations, so we'll drop and recreate tables
26
+
27
+ # Drop and recreate commit_indexes table with commit_sha as primary key
28
+ op.drop_table("commit_indexes")
29
+ op.create_table(
30
+ "commit_indexes",
31
+ sa.Column("commit_sha", sa.String(64), nullable=False),
32
+ sa.Column("status", sa.String(255), nullable=False),
33
+ sa.Column("indexed_at", sa.DateTime(), nullable=True),
34
+ sa.Column("error_message", sa.UnicodeText(), nullable=True),
35
+ sa.Column("files_processed", sa.Integer(), nullable=False, default=0),
36
+ sa.Column("processing_time_seconds", sa.Float(), nullable=False, default=0.0),
37
+ sa.Column("created_at", sa.DateTime(), nullable=False),
38
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
39
+ sa.PrimaryKeyConstraint("commit_sha", name="pk_commit_indexes"),
40
+ )
41
+ op.create_index("ix_commit_indexes_status", "commit_indexes", ["status"])
42
+
43
+ # Drop and recreate git_tracking_branches table with proper constraints
44
+ op.drop_table("git_tracking_branches")
45
+ op.create_table(
46
+ "git_tracking_branches",
47
+ sa.Column("repo_id", sa.Integer(), nullable=False),
48
+ sa.Column("name", sa.String(255), nullable=False),
49
+ sa.Column("created_at", sa.DateTime(), nullable=False),
50
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
51
+ sa.ForeignKeyConstraint(
52
+ ["repo_id"], ["git_repos.id"], name="fk_tracking_branch_repo"
53
+ ),
54
+ sa.PrimaryKeyConstraint("repo_id", "name", name="pk_git_tracking_branches"),
55
+ )
56
+ op.create_index("ix_git_tracking_branches_name", "git_tracking_branches", ["name"])
57
+ op.create_index(
58
+ "ix_git_tracking_branches_repo_id", "git_tracking_branches", ["repo_id"]
59
+ )
60
+
61
+
62
+ def downgrade() -> None:
63
+ """Downgrade schema."""
64
+ # Recreate the original tables
65
+
66
+ # Recreate commit_indexes table with id-based primary key (original structure)
67
+ op.drop_table("commit_indexes")
68
+ op.create_table(
69
+ "commit_indexes",
70
+ sa.Column("id", sa.Integer(), nullable=False),
71
+ sa.Column("commit_sha", sa.String(64), nullable=False),
72
+ sa.Column("status", sa.String(255), nullable=False),
73
+ sa.Column("indexed_at", sa.DateTime(), nullable=True),
74
+ sa.Column("error_message", sa.UnicodeText(), nullable=True),
75
+ sa.Column("files_processed", sa.Integer(), nullable=False, default=0),
76
+ sa.Column("processing_time_seconds", sa.Float(), nullable=False, default=0.0),
77
+ sa.Column("created_at", sa.DateTime(), nullable=False),
78
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
79
+ sa.PrimaryKeyConstraint("id"),
80
+ )
81
+ op.create_index("ix_commit_indexes_status", "commit_indexes", ["status"])
82
+
83
+ # Recreate git_tracking_branches table with original structure
84
+ op.drop_table("git_tracking_branches")
85
+ op.create_table(
86
+ "git_tracking_branches",
87
+ sa.Column("repo_id", sa.Integer(), nullable=False),
88
+ sa.Column("name", sa.String(255), nullable=False),
89
+ sa.Column("created_at", sa.DateTime(), nullable=False),
90
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
91
+ sa.ForeignKeyConstraint(
92
+ ["repo_id", "name"], ["git_branches.repo_id", "git_branches.name"]
93
+ ),
94
+ sa.PrimaryKeyConstraint("repo_id"),
95
+ sa.UniqueConstraint("repo_id", "name", name="uix_repo_tracking_branch"),
96
+ )
97
+ op.create_index("ix_git_tracking_branches_name", "git_tracking_branches", ["name"])
98
+ op.create_index(
99
+ "ix_git_tracking_branches_repo_id", "git_tracking_branches", ["repo_id"]
100
+ )
@@ -0,0 +1,260 @@
1
+ # ruff: noqa
2
+ """add generic enrichment type
3
+
4
+ Revision ID: 19f8c7faf8b9
5
+ Revises: f9e5ef5e688f
6
+ Create Date: 2025-09-29 21:38:19.093821
7
+
8
+ """
9
+
10
+ from typing import Sequence, Union
11
+
12
+ from alembic import op
13
+ import sqlalchemy as sa
14
+
15
+ from kodit.domain.enrichments.development.development import ENRICHMENT_TYPE_DEVELOPMENT
16
+ from kodit.domain.enrichments.development.snippet.snippet import (
17
+ ENRICHMENT_SUBTYPE_SNIPPET_SUMMARY,
18
+ )
19
+
20
+
21
+ # revision identifiers, used by Alembic.
22
+ revision: str = "19f8c7faf8b9"
23
+ down_revision: Union[str, None] = "f9e5ef5e688f"
24
+ branch_labels: Union[str, Sequence[str], None] = None
25
+ depends_on: Union[str, Sequence[str], None] = None
26
+
27
+
28
+ def upgrade() -> None:
29
+ """Upgrade schema."""
30
+ # ### commands auto generated by Alembic - please adjust! ###
31
+ op.create_table(
32
+ "enrichments_v2",
33
+ sa.Column("content", sa.UnicodeText(), nullable=False),
34
+ sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
35
+ sa.Column("type", sa.String(length=255), nullable=False),
36
+ sa.Column("subtype", sa.String(length=255), nullable=False),
37
+ sa.Column(
38
+ "created_at",
39
+ sa.DateTime(timezone=True),
40
+ nullable=False,
41
+ ),
42
+ sa.Column(
43
+ "updated_at",
44
+ sa.DateTime(timezone=True),
45
+ nullable=False,
46
+ ),
47
+ sa.PrimaryKeyConstraint("id"),
48
+ )
49
+ op.create_index(
50
+ "idx_type_subtype", "enrichments_v2", ["type", "subtype"], unique=False
51
+ )
52
+ op.create_index(
53
+ op.f("ix_enrichments_v2_type"), "enrichments_v2", ["type"], unique=False
54
+ )
55
+ op.create_index(
56
+ op.f("ix_enrichments_v2_subtype"), "enrichments_v2", ["subtype"], unique=False
57
+ )
58
+ op.create_table(
59
+ "enrichment_associations",
60
+ sa.Column("enrichment_id", sa.Integer(), nullable=False),
61
+ sa.Column("entity_type", sa.String(length=50), nullable=False),
62
+ sa.Column("entity_id", sa.String(length=255), nullable=False),
63
+ sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
64
+ sa.Column(
65
+ "created_at",
66
+ sa.DateTime(timezone=True),
67
+ nullable=False,
68
+ ),
69
+ sa.Column(
70
+ "updated_at",
71
+ sa.DateTime(timezone=True),
72
+ nullable=False,
73
+ ),
74
+ sa.ForeignKeyConstraint(
75
+ ["enrichment_id"], ["enrichments_v2.id"], ondelete="CASCADE"
76
+ ),
77
+ sa.PrimaryKeyConstraint("id"),
78
+ sa.UniqueConstraint(
79
+ "entity_type", "entity_id", "enrichment_id", name="uix_entity_enrichment"
80
+ ),
81
+ sqlite_autoincrement=True,
82
+ )
83
+ op.create_index(
84
+ "idx_entity_lookup",
85
+ "enrichment_associations",
86
+ ["entity_type", "entity_id"],
87
+ unique=False,
88
+ )
89
+ op.create_index(
90
+ op.f("ix_enrichment_associations_enrichment_id"),
91
+ "enrichment_associations",
92
+ ["enrichment_id"],
93
+ unique=False,
94
+ )
95
+ op.create_index(
96
+ op.f("ix_enrichment_associations_entity_id"),
97
+ "enrichment_associations",
98
+ ["entity_id"],
99
+ unique=False,
100
+ )
101
+ op.create_index(
102
+ op.f("ix_enrichment_associations_entity_type"),
103
+ "enrichment_associations",
104
+ ["entity_type"],
105
+ unique=False,
106
+ )
107
+
108
+ # Migrate existing enrichments from old table to new structure
109
+ # All old enrichments are snippet enrichments
110
+ connection = op.get_bind()
111
+
112
+ # Check if old enrichments table exists
113
+ inspector = sa.inspect(connection)
114
+ if "enrichments" in inspector.get_table_names():
115
+ # Copy enrichments to new table
116
+ old_enrichments = connection.execute(
117
+ sa.text(
118
+ "SELECT id, content, created_at, updated_at, snippet_sha FROM enrichments"
119
+ )
120
+ ).fetchall()
121
+
122
+ for old_enrichment in old_enrichments:
123
+ # Check if database supports RETURNING (PostgreSQL) or use lastrowid (SQLite)
124
+ dialect_name = connection.dialect.name
125
+
126
+ if dialect_name == "postgresql":
127
+ result = connection.execute(
128
+ sa.text(
129
+ "INSERT INTO enrichments_v2 (type, subtype, content, created_at, updated_at) "
130
+ "VALUES (:type, :subtype, :content, :created_at, :updated_at) "
131
+ "RETURNING id"
132
+ ),
133
+ {
134
+ "type": ENRICHMENT_TYPE_DEVELOPMENT,
135
+ "subtype": ENRICHMENT_SUBTYPE_SNIPPET_SUMMARY,
136
+ "content": old_enrichment[1],
137
+ "created_at": old_enrichment[2],
138
+ "updated_at": old_enrichment[3],
139
+ },
140
+ )
141
+ row = result.fetchone()
142
+ if row is None:
143
+ raise RuntimeError("Failed to insert enrichment")
144
+ new_enrichment_id = row[0]
145
+ else:
146
+ # SQLite and other databases
147
+ result = connection.execute(
148
+ sa.text(
149
+ "INSERT INTO enrichments_v2 (type, subtype, content, created_at, updated_at) "
150
+ "VALUES (:type, :subtype, :content, :created_at, :updated_at)"
151
+ ),
152
+ {
153
+ "type": ENRICHMENT_TYPE_DEVELOPMENT,
154
+ "subtype": ENRICHMENT_SUBTYPE_SNIPPET_SUMMARY,
155
+ "content": old_enrichment[1],
156
+ "created_at": old_enrichment[2],
157
+ "updated_at": old_enrichment[3],
158
+ },
159
+ )
160
+ new_enrichment_id = result.lastrowid
161
+
162
+ # Insert association (snippet_v2 is the entity type for snippets)
163
+ connection.execute(
164
+ sa.text(
165
+ "INSERT INTO enrichment_associations "
166
+ "(enrichment_id, entity_type, entity_id, created_at, updated_at) "
167
+ "VALUES (:enrichment_id, :entity_type, :entity_id, :created_at, :updated_at)"
168
+ ),
169
+ {
170
+ "enrichment_id": new_enrichment_id,
171
+ "entity_type": "snippet_v2",
172
+ "entity_id": old_enrichment[4], # snippet_sha
173
+ "created_at": old_enrichment[2],
174
+ "updated_at": old_enrichment[3],
175
+ },
176
+ )
177
+
178
+ # Drop old enrichments table
179
+ op.drop_table("enrichments")
180
+
181
+ # ### end Alembic commands ###
182
+
183
+
184
+ def downgrade() -> None:
185
+ """Downgrade schema."""
186
+ # ### commands auto generated by Alembic - please adjust! ###
187
+ # Recreate old enrichments table
188
+ op.create_table(
189
+ "enrichments",
190
+ sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
191
+ sa.Column(
192
+ "created_at",
193
+ sa.DateTime(timezone=True),
194
+ nullable=False,
195
+ ),
196
+ sa.Column(
197
+ "updated_at",
198
+ sa.DateTime(timezone=True),
199
+ nullable=False,
200
+ ),
201
+ sa.Column("snippet_sha", sa.String(length=64), nullable=False),
202
+ sa.Column("type", sa.String(length=50), nullable=False),
203
+ sa.Column("content", sa.UnicodeText(), nullable=False),
204
+ sa.PrimaryKeyConstraint("id"),
205
+ sa.ForeignKeyConstraint(["snippet_sha"], ["snippets_v2.sha"]),
206
+ )
207
+ op.create_index(
208
+ op.f("ix_enrichments_snippet_sha"), "enrichments", ["snippet_sha"], unique=False
209
+ )
210
+ op.create_index(op.f("ix_enrichments_type"), "enrichments", ["type"], unique=False)
211
+
212
+ # Migrate data back from new structure to old table
213
+ connection = op.get_bind()
214
+
215
+ # Get enrichments with their associations
216
+ enrichments_with_associations = connection.execute(
217
+ sa.text(
218
+ "SELECT e.id, e.content, e.created_at, e.updated_at, "
219
+ "a.entity_id, a.entity_type "
220
+ "FROM enrichments_v2 e "
221
+ "JOIN enrichment_associations a ON e.id = a.enrichment_id "
222
+ "WHERE a.entity_type = 'snippet_v2'"
223
+ )
224
+ ).fetchall()
225
+
226
+ for enrichment in enrichments_with_associations:
227
+ connection.execute(
228
+ sa.text(
229
+ "INSERT INTO enrichments "
230
+ "(content, created_at, updated_at, snippet_sha, type) "
231
+ "VALUES (:content, :created_at, :updated_at, :snippet_sha, :type)"
232
+ ),
233
+ {
234
+ "content": enrichment[1],
235
+ "created_at": enrichment[2],
236
+ "updated_at": enrichment[3],
237
+ "snippet_sha": enrichment[4],
238
+ "type": "summarization", # Default to summarization type
239
+ },
240
+ )
241
+
242
+ op.drop_index(
243
+ op.f("ix_enrichment_associations_entity_type"),
244
+ table_name="enrichment_associations",
245
+ )
246
+ op.drop_index(
247
+ op.f("ix_enrichment_associations_entity_id"),
248
+ table_name="enrichment_associations",
249
+ )
250
+ op.drop_index(
251
+ op.f("ix_enrichment_associations_enrichment_id"),
252
+ table_name="enrichment_associations",
253
+ )
254
+ op.drop_index("idx_entity_lookup", table_name="enrichment_associations")
255
+ op.drop_table("enrichment_associations")
256
+ op.drop_index(op.f("ix_enrichments_v2_subtype"), table_name="enrichments_v2")
257
+ op.drop_index(op.f("ix_enrichments_v2_type"), table_name="enrichments_v2")
258
+ op.drop_index("idx_type_subtype", table_name="enrichments_v2")
259
+ op.drop_table("enrichments_v2")
260
+ # ### end Alembic commands ###