basic-memory 0.17.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.
Files changed (171) hide show
  1. basic_memory/__init__.py +7 -0
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +185 -0
  4. basic_memory/alembic/migrations.py +24 -0
  5. basic_memory/alembic/script.py.mako +26 -0
  6. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  7. basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -0
  8. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  9. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
  10. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
  11. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  12. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  13. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  14. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  15. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
  16. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  17. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  18. basic_memory/api/__init__.py +5 -0
  19. basic_memory/api/app.py +131 -0
  20. basic_memory/api/routers/__init__.py +11 -0
  21. basic_memory/api/routers/directory_router.py +84 -0
  22. basic_memory/api/routers/importer_router.py +152 -0
  23. basic_memory/api/routers/knowledge_router.py +318 -0
  24. basic_memory/api/routers/management_router.py +80 -0
  25. basic_memory/api/routers/memory_router.py +90 -0
  26. basic_memory/api/routers/project_router.py +448 -0
  27. basic_memory/api/routers/prompt_router.py +260 -0
  28. basic_memory/api/routers/resource_router.py +249 -0
  29. basic_memory/api/routers/search_router.py +36 -0
  30. basic_memory/api/routers/utils.py +169 -0
  31. basic_memory/api/template_loader.py +292 -0
  32. basic_memory/api/v2/__init__.py +35 -0
  33. basic_memory/api/v2/routers/__init__.py +21 -0
  34. basic_memory/api/v2/routers/directory_router.py +93 -0
  35. basic_memory/api/v2/routers/importer_router.py +182 -0
  36. basic_memory/api/v2/routers/knowledge_router.py +413 -0
  37. basic_memory/api/v2/routers/memory_router.py +130 -0
  38. basic_memory/api/v2/routers/project_router.py +342 -0
  39. basic_memory/api/v2/routers/prompt_router.py +270 -0
  40. basic_memory/api/v2/routers/resource_router.py +286 -0
  41. basic_memory/api/v2/routers/search_router.py +73 -0
  42. basic_memory/cli/__init__.py +1 -0
  43. basic_memory/cli/app.py +84 -0
  44. basic_memory/cli/auth.py +277 -0
  45. basic_memory/cli/commands/__init__.py +18 -0
  46. basic_memory/cli/commands/cloud/__init__.py +6 -0
  47. basic_memory/cli/commands/cloud/api_client.py +112 -0
  48. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  49. basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
  50. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  51. basic_memory/cli/commands/cloud/rclone_commands.py +371 -0
  52. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  53. basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
  54. basic_memory/cli/commands/cloud/upload.py +233 -0
  55. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  56. basic_memory/cli/commands/command_utils.py +77 -0
  57. basic_memory/cli/commands/db.py +44 -0
  58. basic_memory/cli/commands/format.py +198 -0
  59. basic_memory/cli/commands/import_chatgpt.py +84 -0
  60. basic_memory/cli/commands/import_claude_conversations.py +87 -0
  61. basic_memory/cli/commands/import_claude_projects.py +86 -0
  62. basic_memory/cli/commands/import_memory_json.py +87 -0
  63. basic_memory/cli/commands/mcp.py +76 -0
  64. basic_memory/cli/commands/project.py +889 -0
  65. basic_memory/cli/commands/status.py +174 -0
  66. basic_memory/cli/commands/telemetry.py +81 -0
  67. basic_memory/cli/commands/tool.py +341 -0
  68. basic_memory/cli/main.py +28 -0
  69. basic_memory/config.py +616 -0
  70. basic_memory/db.py +394 -0
  71. basic_memory/deps.py +705 -0
  72. basic_memory/file_utils.py +478 -0
  73. basic_memory/ignore_utils.py +297 -0
  74. basic_memory/importers/__init__.py +27 -0
  75. basic_memory/importers/base.py +79 -0
  76. basic_memory/importers/chatgpt_importer.py +232 -0
  77. basic_memory/importers/claude_conversations_importer.py +180 -0
  78. basic_memory/importers/claude_projects_importer.py +148 -0
  79. basic_memory/importers/memory_json_importer.py +108 -0
  80. basic_memory/importers/utils.py +61 -0
  81. basic_memory/markdown/__init__.py +21 -0
  82. basic_memory/markdown/entity_parser.py +279 -0
  83. basic_memory/markdown/markdown_processor.py +160 -0
  84. basic_memory/markdown/plugins.py +242 -0
  85. basic_memory/markdown/schemas.py +70 -0
  86. basic_memory/markdown/utils.py +117 -0
  87. basic_memory/mcp/__init__.py +1 -0
  88. basic_memory/mcp/async_client.py +139 -0
  89. basic_memory/mcp/project_context.py +141 -0
  90. basic_memory/mcp/prompts/__init__.py +19 -0
  91. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  92. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  93. basic_memory/mcp/prompts/recent_activity.py +188 -0
  94. basic_memory/mcp/prompts/search.py +57 -0
  95. basic_memory/mcp/prompts/utils.py +162 -0
  96. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  97. basic_memory/mcp/resources/project_info.py +71 -0
  98. basic_memory/mcp/server.py +81 -0
  99. basic_memory/mcp/tools/__init__.py +48 -0
  100. basic_memory/mcp/tools/build_context.py +120 -0
  101. basic_memory/mcp/tools/canvas.py +152 -0
  102. basic_memory/mcp/tools/chatgpt_tools.py +190 -0
  103. basic_memory/mcp/tools/delete_note.py +242 -0
  104. basic_memory/mcp/tools/edit_note.py +324 -0
  105. basic_memory/mcp/tools/list_directory.py +168 -0
  106. basic_memory/mcp/tools/move_note.py +551 -0
  107. basic_memory/mcp/tools/project_management.py +201 -0
  108. basic_memory/mcp/tools/read_content.py +281 -0
  109. basic_memory/mcp/tools/read_note.py +267 -0
  110. basic_memory/mcp/tools/recent_activity.py +534 -0
  111. basic_memory/mcp/tools/search.py +385 -0
  112. basic_memory/mcp/tools/utils.py +540 -0
  113. basic_memory/mcp/tools/view_note.py +78 -0
  114. basic_memory/mcp/tools/write_note.py +230 -0
  115. basic_memory/models/__init__.py +15 -0
  116. basic_memory/models/base.py +10 -0
  117. basic_memory/models/knowledge.py +226 -0
  118. basic_memory/models/project.py +87 -0
  119. basic_memory/models/search.py +85 -0
  120. basic_memory/repository/__init__.py +11 -0
  121. basic_memory/repository/entity_repository.py +503 -0
  122. basic_memory/repository/observation_repository.py +73 -0
  123. basic_memory/repository/postgres_search_repository.py +379 -0
  124. basic_memory/repository/project_info_repository.py +10 -0
  125. basic_memory/repository/project_repository.py +128 -0
  126. basic_memory/repository/relation_repository.py +146 -0
  127. basic_memory/repository/repository.py +385 -0
  128. basic_memory/repository/search_index_row.py +95 -0
  129. basic_memory/repository/search_repository.py +94 -0
  130. basic_memory/repository/search_repository_base.py +241 -0
  131. basic_memory/repository/sqlite_search_repository.py +439 -0
  132. basic_memory/schemas/__init__.py +86 -0
  133. basic_memory/schemas/base.py +297 -0
  134. basic_memory/schemas/cloud.py +50 -0
  135. basic_memory/schemas/delete.py +37 -0
  136. basic_memory/schemas/directory.py +30 -0
  137. basic_memory/schemas/importer.py +35 -0
  138. basic_memory/schemas/memory.py +285 -0
  139. basic_memory/schemas/project_info.py +212 -0
  140. basic_memory/schemas/prompt.py +90 -0
  141. basic_memory/schemas/request.py +112 -0
  142. basic_memory/schemas/response.py +229 -0
  143. basic_memory/schemas/search.py +117 -0
  144. basic_memory/schemas/sync_report.py +72 -0
  145. basic_memory/schemas/v2/__init__.py +27 -0
  146. basic_memory/schemas/v2/entity.py +129 -0
  147. basic_memory/schemas/v2/resource.py +46 -0
  148. basic_memory/services/__init__.py +8 -0
  149. basic_memory/services/context_service.py +601 -0
  150. basic_memory/services/directory_service.py +308 -0
  151. basic_memory/services/entity_service.py +864 -0
  152. basic_memory/services/exceptions.py +37 -0
  153. basic_memory/services/file_service.py +541 -0
  154. basic_memory/services/initialization.py +216 -0
  155. basic_memory/services/link_resolver.py +121 -0
  156. basic_memory/services/project_service.py +880 -0
  157. basic_memory/services/search_service.py +404 -0
  158. basic_memory/services/service.py +15 -0
  159. basic_memory/sync/__init__.py +6 -0
  160. basic_memory/sync/background_sync.py +26 -0
  161. basic_memory/sync/sync_service.py +1259 -0
  162. basic_memory/sync/watch_service.py +510 -0
  163. basic_memory/telemetry.py +249 -0
  164. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  165. basic_memory/templates/prompts/search.hbs +101 -0
  166. basic_memory/utils.py +468 -0
  167. basic_memory-0.17.1.dist-info/METADATA +617 -0
  168. basic_memory-0.17.1.dist-info/RECORD +171 -0
  169. basic_memory-0.17.1.dist-info/WHEEL +4 -0
  170. basic_memory-0.17.1.dist-info/entry_points.txt +3 -0
  171. basic_memory-0.17.1.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,120 @@
1
+ """add projects table
2
+
3
+ Revision ID: 5fe1ab1ccebe
4
+ Revises: cc7172b46608
5
+ Create Date: 2025-05-14 09:05:18.214357
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ from alembic import op
12
+ import sqlalchemy as sa
13
+
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = "5fe1ab1ccebe"
17
+ down_revision: Union[str, None] = "cc7172b46608"
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+
25
+ # SQLite FTS5 virtual table handling is SQLite-specific
26
+ # For Postgres, search_index is a regular table managed by ORM
27
+ connection = op.get_bind()
28
+ is_sqlite = connection.dialect.name == "sqlite"
29
+
30
+ op.create_table(
31
+ "project",
32
+ sa.Column("id", sa.Integer(), nullable=False),
33
+ sa.Column("name", sa.String(), nullable=False),
34
+ sa.Column("description", sa.Text(), nullable=True),
35
+ sa.Column("permalink", sa.String(), nullable=False),
36
+ sa.Column("path", sa.String(), nullable=False),
37
+ sa.Column("is_active", sa.Boolean(), nullable=False),
38
+ sa.Column("is_default", sa.Boolean(), nullable=True),
39
+ sa.Column("created_at", sa.DateTime(), nullable=False),
40
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
41
+ sa.PrimaryKeyConstraint("id"),
42
+ sa.UniqueConstraint("is_default"),
43
+ sa.UniqueConstraint("name"),
44
+ sa.UniqueConstraint("permalink"),
45
+ if_not_exists=True,
46
+ )
47
+ with op.batch_alter_table("project", schema=None) as batch_op:
48
+ batch_op.create_index(
49
+ "ix_project_created_at", ["created_at"], unique=False, if_not_exists=True
50
+ )
51
+ batch_op.create_index("ix_project_name", ["name"], unique=True, if_not_exists=True)
52
+ batch_op.create_index("ix_project_path", ["path"], unique=False, if_not_exists=True)
53
+ batch_op.create_index(
54
+ "ix_project_permalink", ["permalink"], unique=True, if_not_exists=True
55
+ )
56
+ batch_op.create_index(
57
+ "ix_project_updated_at", ["updated_at"], unique=False, if_not_exists=True
58
+ )
59
+
60
+ with op.batch_alter_table("entity", schema=None) as batch_op:
61
+ batch_op.add_column(sa.Column("project_id", sa.Integer(), nullable=False))
62
+ batch_op.drop_index(
63
+ "uix_entity_permalink",
64
+ sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL")
65
+ if is_sqlite
66
+ else None,
67
+ )
68
+ batch_op.drop_index("ix_entity_file_path")
69
+ batch_op.create_index(batch_op.f("ix_entity_file_path"), ["file_path"], unique=False)
70
+ batch_op.create_index("ix_entity_project_id", ["project_id"], unique=False)
71
+ batch_op.create_index(
72
+ "uix_entity_file_path_project", ["file_path", "project_id"], unique=True
73
+ )
74
+ batch_op.create_index(
75
+ "uix_entity_permalink_project",
76
+ ["permalink", "project_id"],
77
+ unique=True,
78
+ sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL")
79
+ if is_sqlite
80
+ else None,
81
+ )
82
+ batch_op.create_foreign_key("fk_entity_project_id", "project", ["project_id"], ["id"])
83
+
84
+ # drop the search index table. it will be recreated
85
+ # Only drop for SQLite - Postgres uses regular table managed by ORM
86
+ if is_sqlite:
87
+ op.drop_table("search_index")
88
+
89
+ # ### end Alembic commands ###
90
+
91
+
92
+ def downgrade() -> None:
93
+ # ### commands auto generated by Alembic - please adjust! ###
94
+ with op.batch_alter_table("entity", schema=None) as batch_op:
95
+ batch_op.drop_constraint("fk_entity_project_id", type_="foreignkey")
96
+ batch_op.drop_index(
97
+ "uix_entity_permalink_project",
98
+ sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
99
+ )
100
+ batch_op.drop_index("uix_entity_file_path_project")
101
+ batch_op.drop_index("ix_entity_project_id")
102
+ batch_op.drop_index(batch_op.f("ix_entity_file_path"))
103
+ batch_op.create_index("ix_entity_file_path", ["file_path"], unique=1)
104
+ batch_op.create_index(
105
+ "uix_entity_permalink",
106
+ ["permalink"],
107
+ unique=1,
108
+ sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
109
+ )
110
+ batch_op.drop_column("project_id")
111
+
112
+ with op.batch_alter_table("project", schema=None) as batch_op:
113
+ batch_op.drop_index("ix_project_updated_at")
114
+ batch_op.drop_index("ix_project_permalink")
115
+ batch_op.drop_index("ix_project_path")
116
+ batch_op.drop_index("ix_project_name")
117
+ batch_op.drop_index("ix_project_created_at")
118
+
119
+ op.drop_table("project")
120
+ # ### end Alembic commands ###
@@ -0,0 +1,112 @@
1
+ """project constraint fix
2
+
3
+ Revision ID: 647e7a75e2cd
4
+ Revises: 5fe1ab1ccebe
5
+ Create Date: 2025-06-03 12:48:30.162566
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ from alembic import op
12
+ import sqlalchemy as sa
13
+
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = "647e7a75e2cd"
17
+ down_revision: Union[str, None] = "5fe1ab1ccebe"
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ """Remove the problematic UNIQUE constraint on is_default column.
24
+
25
+ The UNIQUE constraint prevents multiple projects from having is_default=FALSE,
26
+ which breaks project creation when the service sets is_default=False.
27
+
28
+ SQLite: Recreate the table without the constraint (no ALTER TABLE support)
29
+ Postgres: Use ALTER TABLE to drop the constraint directly
30
+ """
31
+ connection = op.get_bind()
32
+ is_sqlite = connection.dialect.name == "sqlite"
33
+
34
+ if is_sqlite:
35
+ # For SQLite, we need to recreate the table without the UNIQUE constraint
36
+ # Create a new table without the UNIQUE constraint on is_default
37
+ op.create_table(
38
+ "project_new",
39
+ sa.Column("id", sa.Integer(), nullable=False),
40
+ sa.Column("name", sa.String(), nullable=False),
41
+ sa.Column("description", sa.Text(), nullable=True),
42
+ sa.Column("permalink", sa.String(), nullable=False),
43
+ sa.Column("path", sa.String(), nullable=False),
44
+ sa.Column("is_active", sa.Boolean(), nullable=False),
45
+ sa.Column("is_default", sa.Boolean(), nullable=True), # No UNIQUE constraint!
46
+ sa.Column("created_at", sa.DateTime(), nullable=False),
47
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
48
+ sa.PrimaryKeyConstraint("id"),
49
+ sa.UniqueConstraint("name"),
50
+ sa.UniqueConstraint("permalink"),
51
+ )
52
+
53
+ # Copy data from old table to new table
54
+ op.execute("INSERT INTO project_new SELECT * FROM project")
55
+
56
+ # Drop the old table
57
+ op.drop_table("project")
58
+
59
+ # Rename the new table
60
+ op.rename_table("project_new", "project")
61
+
62
+ # Recreate the indexes
63
+ with op.batch_alter_table("project", schema=None) as batch_op:
64
+ batch_op.create_index("ix_project_created_at", ["created_at"], unique=False)
65
+ batch_op.create_index("ix_project_name", ["name"], unique=True)
66
+ batch_op.create_index("ix_project_path", ["path"], unique=False)
67
+ batch_op.create_index("ix_project_permalink", ["permalink"], unique=True)
68
+ batch_op.create_index("ix_project_updated_at", ["updated_at"], unique=False)
69
+ else:
70
+ # For Postgres, we can simply drop the constraint
71
+ with op.batch_alter_table("project", schema=None) as batch_op:
72
+ batch_op.drop_constraint("project_is_default_key", type_="unique")
73
+
74
+
75
+ def downgrade() -> None:
76
+ """Add back the UNIQUE constraint on is_default column.
77
+
78
+ WARNING: This will break project creation again if multiple projects
79
+ have is_default=FALSE.
80
+ """
81
+ # Recreate the table with the UNIQUE constraint
82
+ op.create_table(
83
+ "project_old",
84
+ sa.Column("id", sa.Integer(), nullable=False),
85
+ sa.Column("name", sa.String(), nullable=False),
86
+ sa.Column("description", sa.Text(), nullable=True),
87
+ sa.Column("permalink", sa.String(), nullable=False),
88
+ sa.Column("path", sa.String(), nullable=False),
89
+ sa.Column("is_active", sa.Boolean(), nullable=False),
90
+ sa.Column("is_default", sa.Boolean(), nullable=True),
91
+ sa.Column("created_at", sa.DateTime(), nullable=False),
92
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
93
+ sa.PrimaryKeyConstraint("id"),
94
+ sa.UniqueConstraint("is_default"), # Add back the problematic constraint
95
+ sa.UniqueConstraint("name"),
96
+ sa.UniqueConstraint("permalink"),
97
+ )
98
+
99
+ # Copy data (this may fail if multiple FALSE values exist)
100
+ op.execute("INSERT INTO project_old SELECT * FROM project")
101
+
102
+ # Drop the current table and rename
103
+ op.drop_table("project")
104
+ op.rename_table("project_old", "project")
105
+
106
+ # Recreate indexes
107
+ with op.batch_alter_table("project", schema=None) as batch_op:
108
+ batch_op.create_index("ix_project_created_at", ["created_at"], unique=False)
109
+ batch_op.create_index("ix_project_name", ["name"], unique=True)
110
+ batch_op.create_index("ix_project_path", ["path"], unique=False)
111
+ batch_op.create_index("ix_project_permalink", ["permalink"], unique=True)
112
+ batch_op.create_index("ix_project_updated_at", ["updated_at"], unique=False)
@@ -0,0 +1,49 @@
1
+ """Add mtime and size columns to Entity for sync optimization
2
+
3
+ Revision ID: 9d9c1cb7d8f5
4
+ Revises: a1b2c3d4e5f6
5
+ Create Date: 2025-10-20 05:07:55.173849
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ from alembic import op
12
+ import sqlalchemy as sa
13
+
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = "9d9c1cb7d8f5"
17
+ down_revision: Union[str, None] = "a1b2c3d4e5f6"
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ with op.batch_alter_table("entity", schema=None) as batch_op:
25
+ batch_op.add_column(sa.Column("mtime", sa.Float(), nullable=True))
26
+ batch_op.add_column(sa.Column("size", sa.Integer(), nullable=True))
27
+ batch_op.drop_constraint(batch_op.f("fk_entity_project_id"), type_="foreignkey")
28
+ batch_op.create_foreign_key(
29
+ batch_op.f("fk_entity_project_id"), "project", ["project_id"], ["id"]
30
+ )
31
+
32
+ # ### end Alembic commands ###
33
+
34
+
35
+ def downgrade() -> None:
36
+ # ### commands auto generated by Alembic - please adjust! ###
37
+ with op.batch_alter_table("entity", schema=None) as batch_op:
38
+ batch_op.drop_constraint(batch_op.f("fk_entity_project_id"), type_="foreignkey")
39
+ batch_op.create_foreign_key(
40
+ batch_op.f("fk_entity_project_id"),
41
+ "project",
42
+ ["project_id"],
43
+ ["id"],
44
+ ondelete="CASCADE",
45
+ )
46
+ batch_op.drop_column("size")
47
+ batch_op.drop_column("mtime")
48
+
49
+ # ### end Alembic commands ###
@@ -0,0 +1,49 @@
1
+ """fix project foreign keys
2
+
3
+ Revision ID: a1b2c3d4e5f6
4
+ Revises: 647e7a75e2cd
5
+ Create Date: 2025-08-19 22:06:00.000000
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ from alembic import op
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = "a1b2c3d4e5f6"
16
+ down_revision: Union[str, None] = "647e7a75e2cd"
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Re-establish foreign key constraints that were lost during project table recreation.
23
+
24
+ The migration 647e7a75e2cd recreated the project table but did not re-establish
25
+ the foreign key constraint from entity.project_id to project.id, causing
26
+ foreign key constraint failures when trying to delete projects with related entities.
27
+ """
28
+ # SQLite doesn't allow adding foreign key constraints to existing tables easily
29
+ # We need to be careful and handle the case where the constraint might already exist
30
+
31
+ with op.batch_alter_table("entity", schema=None) as batch_op:
32
+ # Try to drop existing foreign key constraint (may not exist)
33
+ try:
34
+ batch_op.drop_constraint("fk_entity_project_id", type_="foreignkey")
35
+ except Exception:
36
+ # Constraint may not exist, which is fine - we'll create it next
37
+ pass
38
+
39
+ # Add the foreign key constraint with CASCADE DELETE
40
+ # This ensures that when a project is deleted, all related entities are also deleted
41
+ batch_op.create_foreign_key(
42
+ "fk_entity_project_id", "project", ["project_id"], ["id"], ondelete="CASCADE"
43
+ )
44
+
45
+
46
+ def downgrade() -> None:
47
+ """Remove the foreign key constraint."""
48
+ with op.batch_alter_table("entity", schema=None) as batch_op:
49
+ batch_op.drop_constraint("fk_entity_project_id", type_="foreignkey")
@@ -0,0 +1,56 @@
1
+ """Add cascade delete FK from search_index to entity
2
+
3
+ Revision ID: a2b3c4d5e6f7
4
+ Revises: f8a9b2c3d4e5
5
+ Create Date: 2025-12-02 07:00:00.000000
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ from alembic import op
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = "a2b3c4d5e6f7"
16
+ down_revision: Union[str, None] = "f8a9b2c3d4e5"
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Add FK with CASCADE delete from search_index.entity_id to entity.id.
23
+
24
+ This migration is Postgres-only because:
25
+ - SQLite uses FTS5 virtual tables which don't support foreign keys
26
+ - The FK enables automatic cleanup of search_index entries when entities are deleted
27
+ """
28
+ connection = op.get_bind()
29
+ dialect = connection.dialect.name
30
+
31
+ if dialect == "postgresql":
32
+ # First, clean up any orphaned search_index entries where entity no longer exists
33
+ op.execute("""
34
+ DELETE FROM search_index
35
+ WHERE entity_id IS NOT NULL
36
+ AND entity_id NOT IN (SELECT id FROM entity)
37
+ """)
38
+
39
+ # Add FK with CASCADE - nullable FK allows search_index entries without entity_id
40
+ op.create_foreign_key(
41
+ "fk_search_index_entity_id",
42
+ "search_index",
43
+ "entity",
44
+ ["entity_id"],
45
+ ["id"],
46
+ ondelete="CASCADE",
47
+ )
48
+
49
+
50
+ def downgrade() -> None:
51
+ """Remove the FK constraint."""
52
+ connection = op.get_bind()
53
+ dialect = connection.dialect.name
54
+
55
+ if dialect == "postgresql":
56
+ op.drop_constraint("fk_search_index_entity_id", "search_index", type_="foreignkey")
@@ -0,0 +1,44 @@
1
+ """relation to_name unique index
2
+
3
+ Revision ID: b3c3938bacdb
4
+ Revises: 3dae7c7b1564
5
+ Create Date: 2025-02-22 14:59:30.668466
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ from alembic import op
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = "b3c3938bacdb"
16
+ down_revision: Union[str, None] = "3dae7c7b1564"
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ # SQLite doesn't support constraint changes through ALTER
23
+ # Need to recreate table with desired constraints
24
+ with op.batch_alter_table("relation") as batch_op:
25
+ # Drop existing unique constraint
26
+ batch_op.drop_constraint("uix_relation", type_="unique")
27
+
28
+ # Add new constraints
29
+ batch_op.create_unique_constraint(
30
+ "uix_relation_from_id_to_id", ["from_id", "to_id", "relation_type"]
31
+ )
32
+ batch_op.create_unique_constraint(
33
+ "uix_relation_from_id_to_name", ["from_id", "to_name", "relation_type"]
34
+ )
35
+
36
+
37
+ def downgrade() -> None:
38
+ with op.batch_alter_table("relation") as batch_op:
39
+ # Drop new constraints
40
+ batch_op.drop_constraint("uix_relation_from_id_to_name", type_="unique")
41
+ batch_op.drop_constraint("uix_relation_from_id_to_id", type_="unique")
42
+
43
+ # Restore original constraint
44
+ batch_op.create_unique_constraint("uix_relation", ["from_id", "to_id", "relation_type"])
@@ -0,0 +1,113 @@
1
+ """Update search index schema
2
+
3
+ Revision ID: cc7172b46608
4
+ Revises: 502b60eaa905
5
+ Create Date: 2025-02-28 18:48:23.244941
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ from alembic import op
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = "cc7172b46608"
16
+ down_revision: Union[str, None] = "502b60eaa905"
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade database schema to use new search index with content_stems and content_snippet."""
23
+
24
+ # This migration is SQLite-specific (FTS5 virtual tables)
25
+ # For Postgres, the search_index table is created via ORM models
26
+ connection = op.get_bind()
27
+ if connection.dialect.name != "sqlite":
28
+ return
29
+
30
+ # First, drop the existing search_index table
31
+ op.execute("DROP TABLE IF EXISTS search_index")
32
+
33
+ # Create new search_index with updated schema
34
+ op.execute("""
35
+ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
36
+ -- Core entity fields
37
+ id UNINDEXED, -- Row ID
38
+ title, -- Title for searching
39
+ content_stems, -- Main searchable content split into stems
40
+ content_snippet, -- File content snippet for display
41
+ permalink, -- Stable identifier (now indexed for path search)
42
+ file_path UNINDEXED, -- Physical location
43
+ type UNINDEXED, -- entity/relation/observation
44
+
45
+ -- Relation fields
46
+ from_id UNINDEXED, -- Source entity
47
+ to_id UNINDEXED, -- Target entity
48
+ relation_type UNINDEXED, -- Type of relation
49
+
50
+ -- Observation fields
51
+ entity_id UNINDEXED, -- Parent entity
52
+ category UNINDEXED, -- Observation category
53
+
54
+ -- Common fields
55
+ metadata UNINDEXED, -- JSON metadata
56
+ created_at UNINDEXED, -- Creation timestamp
57
+ updated_at UNINDEXED, -- Last update
58
+
59
+ -- Configuration
60
+ tokenize='unicode61 tokenchars 0x2F', -- Hex code for /
61
+ prefix='1,2,3,4' -- Support longer prefixes for paths
62
+ );
63
+ """)
64
+
65
+
66
+ def downgrade() -> None:
67
+ """Downgrade database schema to use old search index."""
68
+
69
+ # This migration is SQLite-specific (FTS5 virtual tables)
70
+ # For Postgres, the search_index table is managed via ORM models
71
+ connection = op.get_bind()
72
+ if connection.dialect.name != "sqlite":
73
+ return
74
+
75
+ # Drop the updated search_index table
76
+ op.execute("DROP TABLE IF EXISTS search_index")
77
+
78
+ # Recreate the original search_index schema
79
+ op.execute("""
80
+ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
81
+ -- Core entity fields
82
+ id UNINDEXED, -- Row ID
83
+ title, -- Title for searching
84
+ content, -- Main searchable content
85
+ permalink, -- Stable identifier (now indexed for path search)
86
+ file_path UNINDEXED, -- Physical location
87
+ type UNINDEXED, -- entity/relation/observation
88
+
89
+ -- Relation fields
90
+ from_id UNINDEXED, -- Source entity
91
+ to_id UNINDEXED, -- Target entity
92
+ relation_type UNINDEXED, -- Type of relation
93
+
94
+ -- Observation fields
95
+ entity_id UNINDEXED, -- Parent entity
96
+ category UNINDEXED, -- Observation category
97
+
98
+ -- Common fields
99
+ metadata UNINDEXED, -- JSON metadata
100
+ created_at UNINDEXED, -- Creation timestamp
101
+ updated_at UNINDEXED, -- Last update
102
+
103
+ -- Configuration
104
+ tokenize='unicode61 tokenchars 0x2F', -- Hex code for /
105
+ prefix='1,2,3,4' -- Support longer prefixes for paths
106
+ );
107
+ """)
108
+
109
+ # Print instruction to manually reindex after migration
110
+ print("\n------------------------------------------------------------------")
111
+ print("IMPORTANT: After downgrade completes, manually run the reindex command:")
112
+ print("basic-memory sync")
113
+ print("------------------------------------------------------------------\n")
@@ -0,0 +1,37 @@
1
+ """Add scan watermark tracking to Project
2
+
3
+ Revision ID: e7e1f4367280
4
+ Revises: 9d9c1cb7d8f5
5
+ Create Date: 2025-10-20 16:42:46.625075
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ from alembic import op
12
+ import sqlalchemy as sa
13
+
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = "e7e1f4367280"
17
+ down_revision: Union[str, None] = "9d9c1cb7d8f5"
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ with op.batch_alter_table("project", schema=None) as batch_op:
25
+ batch_op.add_column(sa.Column("last_scan_timestamp", sa.Float(), nullable=True))
26
+ batch_op.add_column(sa.Column("last_file_count", sa.Integer(), nullable=True))
27
+
28
+ # ### end Alembic commands ###
29
+
30
+
31
+ def downgrade() -> None:
32
+ # ### commands auto generated by Alembic - please adjust! ###
33
+ with op.batch_alter_table("project", schema=None) as batch_op:
34
+ batch_op.drop_column("last_file_count")
35
+ batch_op.drop_column("last_scan_timestamp")
36
+
37
+ # ### end Alembic commands ###