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.
- basic_memory/__init__.py +7 -0
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +185 -0
- basic_memory/alembic/migrations.py +24 -0
- basic_memory/alembic/script.py.mako +26 -0
- basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
- basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -0
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
- basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
- basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
- basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
- basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
- basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
- basic_memory/api/__init__.py +5 -0
- basic_memory/api/app.py +131 -0
- basic_memory/api/routers/__init__.py +11 -0
- basic_memory/api/routers/directory_router.py +84 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +318 -0
- basic_memory/api/routers/management_router.py +80 -0
- basic_memory/api/routers/memory_router.py +90 -0
- basic_memory/api/routers/project_router.py +448 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/resource_router.py +249 -0
- basic_memory/api/routers/search_router.py +36 -0
- basic_memory/api/routers/utils.py +169 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/api/v2/__init__.py +35 -0
- basic_memory/api/v2/routers/__init__.py +21 -0
- basic_memory/api/v2/routers/directory_router.py +93 -0
- basic_memory/api/v2/routers/importer_router.py +182 -0
- basic_memory/api/v2/routers/knowledge_router.py +413 -0
- basic_memory/api/v2/routers/memory_router.py +130 -0
- basic_memory/api/v2/routers/project_router.py +342 -0
- basic_memory/api/v2/routers/prompt_router.py +270 -0
- basic_memory/api/v2/routers/resource_router.py +286 -0
- basic_memory/api/v2/routers/search_router.py +73 -0
- basic_memory/cli/__init__.py +1 -0
- basic_memory/cli/app.py +84 -0
- basic_memory/cli/auth.py +277 -0
- basic_memory/cli/commands/__init__.py +18 -0
- basic_memory/cli/commands/cloud/__init__.py +6 -0
- basic_memory/cli/commands/cloud/api_client.py +112 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
- basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
- basic_memory/cli/commands/cloud/core_commands.py +195 -0
- basic_memory/cli/commands/cloud/rclone_commands.py +371 -0
- basic_memory/cli/commands/cloud/rclone_config.py +110 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
- basic_memory/cli/commands/cloud/upload.py +233 -0
- basic_memory/cli/commands/cloud/upload_command.py +124 -0
- basic_memory/cli/commands/command_utils.py +77 -0
- basic_memory/cli/commands/db.py +44 -0
- basic_memory/cli/commands/format.py +198 -0
- basic_memory/cli/commands/import_chatgpt.py +84 -0
- basic_memory/cli/commands/import_claude_conversations.py +87 -0
- basic_memory/cli/commands/import_claude_projects.py +86 -0
- basic_memory/cli/commands/import_memory_json.py +87 -0
- basic_memory/cli/commands/mcp.py +76 -0
- basic_memory/cli/commands/project.py +889 -0
- basic_memory/cli/commands/status.py +174 -0
- basic_memory/cli/commands/telemetry.py +81 -0
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/main.py +28 -0
- basic_memory/config.py +616 -0
- basic_memory/db.py +394 -0
- basic_memory/deps.py +705 -0
- basic_memory/file_utils.py +478 -0
- basic_memory/ignore_utils.py +297 -0
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +232 -0
- basic_memory/importers/claude_conversations_importer.py +180 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +108 -0
- basic_memory/importers/utils.py +61 -0
- basic_memory/markdown/__init__.py +21 -0
- basic_memory/markdown/entity_parser.py +279 -0
- basic_memory/markdown/markdown_processor.py +160 -0
- basic_memory/markdown/plugins.py +242 -0
- basic_memory/markdown/schemas.py +70 -0
- basic_memory/markdown/utils.py +117 -0
- basic_memory/mcp/__init__.py +1 -0
- basic_memory/mcp/async_client.py +139 -0
- basic_memory/mcp/project_context.py +141 -0
- basic_memory/mcp/prompts/__init__.py +19 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
- basic_memory/mcp/prompts/continue_conversation.py +62 -0
- basic_memory/mcp/prompts/recent_activity.py +188 -0
- basic_memory/mcp/prompts/search.py +57 -0
- basic_memory/mcp/prompts/utils.py +162 -0
- basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
- basic_memory/mcp/resources/project_info.py +71 -0
- basic_memory/mcp/server.py +81 -0
- basic_memory/mcp/tools/__init__.py +48 -0
- basic_memory/mcp/tools/build_context.py +120 -0
- basic_memory/mcp/tools/canvas.py +152 -0
- basic_memory/mcp/tools/chatgpt_tools.py +190 -0
- basic_memory/mcp/tools/delete_note.py +242 -0
- basic_memory/mcp/tools/edit_note.py +324 -0
- basic_memory/mcp/tools/list_directory.py +168 -0
- basic_memory/mcp/tools/move_note.py +551 -0
- basic_memory/mcp/tools/project_management.py +201 -0
- basic_memory/mcp/tools/read_content.py +281 -0
- basic_memory/mcp/tools/read_note.py +267 -0
- basic_memory/mcp/tools/recent_activity.py +534 -0
- basic_memory/mcp/tools/search.py +385 -0
- basic_memory/mcp/tools/utils.py +540 -0
- basic_memory/mcp/tools/view_note.py +78 -0
- basic_memory/mcp/tools/write_note.py +230 -0
- basic_memory/models/__init__.py +15 -0
- basic_memory/models/base.py +10 -0
- basic_memory/models/knowledge.py +226 -0
- basic_memory/models/project.py +87 -0
- basic_memory/models/search.py +85 -0
- basic_memory/repository/__init__.py +11 -0
- basic_memory/repository/entity_repository.py +503 -0
- basic_memory/repository/observation_repository.py +73 -0
- basic_memory/repository/postgres_search_repository.py +379 -0
- basic_memory/repository/project_info_repository.py +10 -0
- basic_memory/repository/project_repository.py +128 -0
- basic_memory/repository/relation_repository.py +146 -0
- basic_memory/repository/repository.py +385 -0
- basic_memory/repository/search_index_row.py +95 -0
- basic_memory/repository/search_repository.py +94 -0
- basic_memory/repository/search_repository_base.py +241 -0
- basic_memory/repository/sqlite_search_repository.py +439 -0
- basic_memory/schemas/__init__.py +86 -0
- basic_memory/schemas/base.py +297 -0
- basic_memory/schemas/cloud.py +50 -0
- basic_memory/schemas/delete.py +37 -0
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +35 -0
- basic_memory/schemas/memory.py +285 -0
- basic_memory/schemas/project_info.py +212 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +112 -0
- basic_memory/schemas/response.py +229 -0
- basic_memory/schemas/search.py +117 -0
- basic_memory/schemas/sync_report.py +72 -0
- basic_memory/schemas/v2/__init__.py +27 -0
- basic_memory/schemas/v2/entity.py +129 -0
- basic_memory/schemas/v2/resource.py +46 -0
- basic_memory/services/__init__.py +8 -0
- basic_memory/services/context_service.py +601 -0
- basic_memory/services/directory_service.py +308 -0
- basic_memory/services/entity_service.py +864 -0
- basic_memory/services/exceptions.py +37 -0
- basic_memory/services/file_service.py +541 -0
- basic_memory/services/initialization.py +216 -0
- basic_memory/services/link_resolver.py +121 -0
- basic_memory/services/project_service.py +880 -0
- basic_memory/services/search_service.py +404 -0
- basic_memory/services/service.py +15 -0
- basic_memory/sync/__init__.py +6 -0
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/sync_service.py +1259 -0
- basic_memory/sync/watch_service.py +510 -0
- basic_memory/telemetry.py +249 -0
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +468 -0
- basic_memory-0.17.1.dist-info/METADATA +617 -0
- basic_memory-0.17.1.dist-info/RECORD +171 -0
- basic_memory-0.17.1.dist-info/WHEEL +4 -0
- basic_memory-0.17.1.dist-info/entry_points.txt +3 -0
- 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 ###
|