basic-memory 0.2.12__py3-none-any.whl → 0.16.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 basic-memory might be problematic. Click here for more details.
- basic_memory/__init__.py +5 -1
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +27 -3
- basic_memory/alembic/migrations.py +4 -9
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +104 -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/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +100 -0
- basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
- basic_memory/api/app.py +63 -31
- basic_memory/api/routers/__init__.py +4 -1
- 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 +165 -28
- basic_memory/api/routers/management_router.py +80 -0
- basic_memory/api/routers/memory_router.py +28 -67
- basic_memory/api/routers/project_router.py +406 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/resource_router.py +219 -14
- basic_memory/api/routers/search_router.py +21 -13
- basic_memory/api/routers/utils.py +130 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/cli/app.py +52 -1
- basic_memory/cli/auth.py +277 -0
- basic_memory/cli/commands/__init__.py +13 -2
- 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 +301 -0
- basic_memory/cli/commands/cloud/rclone_config.py +110 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +249 -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 +51 -0
- basic_memory/cli/commands/db.py +26 -7
- basic_memory/cli/commands/import_chatgpt.py +83 -0
- basic_memory/cli/commands/import_claude_conversations.py +86 -0
- basic_memory/cli/commands/import_claude_projects.py +85 -0
- basic_memory/cli/commands/import_memory_json.py +35 -92
- basic_memory/cli/commands/mcp.py +84 -10
- basic_memory/cli/commands/project.py +876 -0
- basic_memory/cli/commands/status.py +47 -30
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/main.py +13 -6
- basic_memory/config.py +481 -22
- basic_memory/db.py +192 -32
- basic_memory/deps.py +252 -22
- basic_memory/file_utils.py +113 -58
- 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 +177 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +108 -0
- basic_memory/importers/utils.py +58 -0
- basic_memory/markdown/entity_parser.py +143 -23
- basic_memory/markdown/markdown_processor.py +3 -3
- basic_memory/markdown/plugins.py +39 -21
- basic_memory/markdown/schemas.py +1 -1
- basic_memory/markdown/utils.py +28 -13
- basic_memory/mcp/async_client.py +134 -4
- 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 +7 -13
- basic_memory/mcp/tools/__init__.py +33 -21
- basic_memory/mcp/tools/build_context.py +120 -0
- basic_memory/mcp/tools/canvas.py +130 -0
- basic_memory/mcp/tools/chatgpt_tools.py +187 -0
- basic_memory/mcp/tools/delete_note.py +225 -0
- basic_memory/mcp/tools/edit_note.py +320 -0
- basic_memory/mcp/tools/list_directory.py +167 -0
- basic_memory/mcp/tools/move_note.py +545 -0
- basic_memory/mcp/tools/project_management.py +200 -0
- basic_memory/mcp/tools/read_content.py +271 -0
- basic_memory/mcp/tools/read_note.py +255 -0
- basic_memory/mcp/tools/recent_activity.py +534 -0
- basic_memory/mcp/tools/search.py +369 -14
- basic_memory/mcp/tools/utils.py +374 -16
- basic_memory/mcp/tools/view_note.py +77 -0
- basic_memory/mcp/tools/write_note.py +207 -0
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +67 -15
- basic_memory/models/project.py +87 -0
- basic_memory/models/search.py +10 -6
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +229 -7
- basic_memory/repository/observation_repository.py +35 -3
- basic_memory/repository/project_info_repository.py +10 -0
- basic_memory/repository/project_repository.py +103 -0
- basic_memory/repository/relation_repository.py +21 -2
- basic_memory/repository/repository.py +147 -29
- basic_memory/repository/search_repository.py +437 -59
- basic_memory/schemas/__init__.py +22 -9
- basic_memory/schemas/base.py +97 -8
- basic_memory/schemas/cloud.py +50 -0
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +35 -0
- basic_memory/schemas/memory.py +188 -23
- basic_memory/schemas/project_info.py +211 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +57 -3
- basic_memory/schemas/response.py +9 -1
- basic_memory/schemas/search.py +33 -35
- basic_memory/schemas/sync_report.py +72 -0
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +251 -106
- basic_memory/services/directory_service.py +295 -0
- basic_memory/services/entity_service.py +595 -60
- basic_memory/services/exceptions.py +21 -0
- basic_memory/services/file_service.py +284 -30
- basic_memory/services/initialization.py +191 -0
- basic_memory/services/link_resolver.py +50 -56
- basic_memory/services/project_service.py +863 -0
- basic_memory/services/search_service.py +172 -34
- basic_memory/sync/__init__.py +3 -2
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/sync_service.py +1176 -96
- basic_memory/sync/watch_service.py +412 -135
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +388 -28
- basic_memory-0.16.1.dist-info/METADATA +493 -0
- basic_memory-0.16.1.dist-info/RECORD +148 -0
- {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/entry_points.txt +1 -0
- basic_memory/alembic/README +0 -1
- basic_memory/cli/commands/sync.py +0 -203
- basic_memory/mcp/tools/knowledge.py +0 -56
- basic_memory/mcp/tools/memory.py +0 -151
- basic_memory/mcp/tools/notes.py +0 -122
- basic_memory/schemas/discovery.py +0 -28
- basic_memory/sync/file_change_scanner.py +0 -158
- basic_memory/sync/utils.py +0 -34
- basic_memory-0.2.12.dist-info/METADATA +0 -291
- basic_memory-0.2.12.dist-info/RECORD +0 -78
- {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/WHEEL +0 -0
- {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/licenses/LICENSE +0 -0
basic_memory/__init__.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Package version - updated by release automation
|
|
4
|
+
__version__ = "0.16.1"
|
|
5
|
+
|
|
6
|
+
# API version for FastAPI - independent of package version
|
|
7
|
+
__api_version__ = "v0"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# A generic, single database configuration.
|
|
2
|
+
|
|
3
|
+
[alembic]
|
|
4
|
+
# path to migration scripts
|
|
5
|
+
# Use forward slashes (/) also on windows to provide an os agnostic path
|
|
6
|
+
script_location = .
|
|
7
|
+
|
|
8
|
+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
|
9
|
+
# Uncomment the line below if you want the files to be prepended with date and time
|
|
10
|
+
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
|
11
|
+
# for all available tokens
|
|
12
|
+
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
|
13
|
+
|
|
14
|
+
# sys.path path, will be prepended to sys.path if present.
|
|
15
|
+
# defaults to the current working directory.
|
|
16
|
+
prepend_sys_path = .
|
|
17
|
+
|
|
18
|
+
# timezone to use when rendering the date within the migration file
|
|
19
|
+
# as well as the filename.
|
|
20
|
+
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
|
|
21
|
+
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
|
22
|
+
# string value is passed to ZoneInfo()
|
|
23
|
+
# leave blank for localtime
|
|
24
|
+
# timezone =
|
|
25
|
+
|
|
26
|
+
# max length of characters to apply to the "slug" field
|
|
27
|
+
# truncate_slug_length = 40
|
|
28
|
+
|
|
29
|
+
# set to 'true' to run the environment during
|
|
30
|
+
# the 'revision' command, regardless of autogenerate
|
|
31
|
+
# revision_environment = false
|
|
32
|
+
|
|
33
|
+
# set to 'true' to allow .pyc and .pyo files without
|
|
34
|
+
# a source .py file to be detected as revisions in the
|
|
35
|
+
# versions/ directory
|
|
36
|
+
# sourceless = false
|
|
37
|
+
|
|
38
|
+
# version location specification; This defaults
|
|
39
|
+
# to migrations/versions. When using multiple version
|
|
40
|
+
# directories, initial revisions must be specified with --version-path.
|
|
41
|
+
# The path separator used here should be the separator specified by "version_path_separator" below.
|
|
42
|
+
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
|
|
43
|
+
|
|
44
|
+
# version path separator; As mentioned above, this is the character used to split
|
|
45
|
+
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
|
46
|
+
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
|
47
|
+
# Valid values for version_path_separator are:
|
|
48
|
+
#
|
|
49
|
+
# version_path_separator = :
|
|
50
|
+
# version_path_separator = ;
|
|
51
|
+
# version_path_separator = space
|
|
52
|
+
# version_path_separator = newline
|
|
53
|
+
#
|
|
54
|
+
# Use os.pathsep. Default configuration used for new projects.
|
|
55
|
+
version_path_separator = os
|
|
56
|
+
|
|
57
|
+
# set to 'true' to search source files recursively
|
|
58
|
+
# in each "version_locations" directory
|
|
59
|
+
# new in Alembic version 1.10
|
|
60
|
+
# recursive_version_locations = false
|
|
61
|
+
|
|
62
|
+
# the output encoding used when revision files
|
|
63
|
+
# are written from script.py.mako
|
|
64
|
+
# output_encoding = utf-8
|
|
65
|
+
|
|
66
|
+
sqlalchemy.url = driver://user:pass@localhost/dbname
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
[post_write_hooks]
|
|
70
|
+
# post_write_hooks defines scripts or Python functions that are run
|
|
71
|
+
# on newly generated revision scripts. See the documentation for further
|
|
72
|
+
# detail and examples
|
|
73
|
+
|
|
74
|
+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
|
75
|
+
# hooks = black
|
|
76
|
+
# black.type = console_scripts
|
|
77
|
+
# black.entrypoint = black
|
|
78
|
+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
|
79
|
+
|
|
80
|
+
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
|
81
|
+
# hooks = ruff
|
|
82
|
+
# ruff.type = exec
|
|
83
|
+
# ruff.executable = %(here)s/.venv/bin/ruff
|
|
84
|
+
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
|
85
|
+
|
|
86
|
+
# Logging configuration
|
|
87
|
+
[loggers]
|
|
88
|
+
keys = root,sqlalchemy,alembic
|
|
89
|
+
|
|
90
|
+
[handlers]
|
|
91
|
+
keys = console
|
|
92
|
+
|
|
93
|
+
[formatters]
|
|
94
|
+
keys = generic
|
|
95
|
+
|
|
96
|
+
[logger_root]
|
|
97
|
+
level = WARNING
|
|
98
|
+
handlers = console
|
|
99
|
+
qualname =
|
|
100
|
+
|
|
101
|
+
[logger_sqlalchemy]
|
|
102
|
+
level = WARNING
|
|
103
|
+
handlers =
|
|
104
|
+
qualname = sqlalchemy.engine
|
|
105
|
+
|
|
106
|
+
[logger_alembic]
|
|
107
|
+
level = INFO
|
|
108
|
+
handlers =
|
|
109
|
+
qualname = alembic
|
|
110
|
+
|
|
111
|
+
[handler_console]
|
|
112
|
+
class = StreamHandler
|
|
113
|
+
args = (sys.stderr,)
|
|
114
|
+
level = NOTSET
|
|
115
|
+
formatter = generic
|
|
116
|
+
|
|
117
|
+
[formatter_generic]
|
|
118
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
119
|
+
datefmt = %H:%M:%S
|
basic_memory/alembic/env.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Alembic environment configuration."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from logging.config import fileConfig
|
|
4
5
|
|
|
5
6
|
from sqlalchemy import engine_from_config
|
|
@@ -7,17 +8,25 @@ from sqlalchemy import pool
|
|
|
7
8
|
|
|
8
9
|
from alembic import context
|
|
9
10
|
|
|
10
|
-
from basic_memory.
|
|
11
|
-
|
|
11
|
+
from basic_memory.config import ConfigManager
|
|
12
|
+
|
|
13
|
+
# set config.env to "test" for pytest to prevent logging to file in utils.setup_logging()
|
|
14
|
+
os.environ["BASIC_MEMORY_ENV"] = "test"
|
|
15
|
+
|
|
16
|
+
# Import after setting environment variable # noqa: E402
|
|
17
|
+
from basic_memory.models import Base # noqa: E402
|
|
12
18
|
|
|
13
19
|
# this is the Alembic Config object, which provides
|
|
14
20
|
# access to the values within the .ini file in use.
|
|
15
21
|
config = context.config
|
|
16
22
|
|
|
23
|
+
app_config = ConfigManager().config
|
|
17
24
|
# Set the SQLAlchemy URL from our app config
|
|
18
25
|
sqlalchemy_url = f"sqlite:///{app_config.database_path}"
|
|
19
26
|
config.set_main_option("sqlalchemy.url", sqlalchemy_url)
|
|
20
27
|
|
|
28
|
+
# print(f"Using SQLAlchemy URL: {sqlalchemy_url}")
|
|
29
|
+
|
|
21
30
|
# Interpret the config file for Python logging.
|
|
22
31
|
if config.config_file_name is not None:
|
|
23
32
|
fileConfig(config.config_file_name)
|
|
@@ -27,6 +36,14 @@ if config.config_file_name is not None:
|
|
|
27
36
|
target_metadata = Base.metadata
|
|
28
37
|
|
|
29
38
|
|
|
39
|
+
# Add this function to tell Alembic what to include/exclude
|
|
40
|
+
def include_object(object, name, type_, reflected, compare_to):
|
|
41
|
+
# Ignore SQLite FTS tables
|
|
42
|
+
if type_ == "table" and name.startswith("search_index"):
|
|
43
|
+
return False
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
|
|
30
47
|
def run_migrations_offline() -> None:
|
|
31
48
|
"""Run migrations in 'offline' mode.
|
|
32
49
|
|
|
@@ -44,6 +61,8 @@ def run_migrations_offline() -> None:
|
|
|
44
61
|
target_metadata=target_metadata,
|
|
45
62
|
literal_binds=True,
|
|
46
63
|
dialect_opts={"paramstyle": "named"},
|
|
64
|
+
include_object=include_object,
|
|
65
|
+
render_as_batch=True,
|
|
47
66
|
)
|
|
48
67
|
|
|
49
68
|
with context.begin_transaction():
|
|
@@ -63,7 +82,12 @@ def run_migrations_online() -> None:
|
|
|
63
82
|
)
|
|
64
83
|
|
|
65
84
|
with connectable.connect() as connection:
|
|
66
|
-
context.configure(
|
|
85
|
+
context.configure(
|
|
86
|
+
connection=connection,
|
|
87
|
+
target_metadata=target_metadata,
|
|
88
|
+
include_object=include_object,
|
|
89
|
+
render_as_batch=True,
|
|
90
|
+
)
|
|
67
91
|
|
|
68
92
|
with context.begin_transaction():
|
|
69
93
|
context.run_migrations()
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Functions for managing database migrations."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
from loguru import logger
|
|
6
5
|
from alembic.config import Config
|
|
@@ -10,20 +9,16 @@ from alembic import command
|
|
|
10
9
|
def get_alembic_config() -> Config: # pragma: no cover
|
|
11
10
|
"""Get alembic config with correct paths."""
|
|
12
11
|
migrations_path = Path(__file__).parent
|
|
13
|
-
alembic_ini = migrations_path
|
|
12
|
+
alembic_ini = migrations_path / "alembic.ini"
|
|
14
13
|
|
|
15
14
|
config = Config(alembic_ini)
|
|
16
15
|
config.set_main_option("script_location", str(migrations_path))
|
|
17
16
|
return config
|
|
18
17
|
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
def reset_database(): # pragma: no cover
|
|
21
20
|
"""Drop and recreate all tables."""
|
|
22
21
|
logger.info("Resetting database...")
|
|
23
22
|
config = get_alembic_config()
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
command.downgrade(cfg, "base")
|
|
27
|
-
command.upgrade(cfg, "head")
|
|
28
|
-
|
|
29
|
-
await asyncio.get_event_loop().run_in_executor(None, _reset, config)
|
|
23
|
+
command.downgrade(config, "base")
|
|
24
|
+
command.upgrade(config, "head")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""remove required from entity.permalink
|
|
2
|
+
|
|
3
|
+
Revision ID: 502b60eaa905
|
|
4
|
+
Revises: b3c3938bacdb
|
|
5
|
+
Create Date: 2025-02-24 13:33:09.790951
|
|
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 = "502b60eaa905"
|
|
17
|
+
down_revision: Union[str, None] = "b3c3938bacdb"
|
|
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.alter_column("permalink", existing_type=sa.VARCHAR(), nullable=True)
|
|
26
|
+
batch_op.drop_index("ix_entity_permalink")
|
|
27
|
+
batch_op.create_index(batch_op.f("ix_entity_permalink"), ["permalink"], unique=False)
|
|
28
|
+
batch_op.drop_constraint("uix_entity_permalink", type_="unique")
|
|
29
|
+
batch_op.create_index(
|
|
30
|
+
"uix_entity_permalink",
|
|
31
|
+
["permalink"],
|
|
32
|
+
unique=True,
|
|
33
|
+
sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# ### end Alembic commands ###
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def downgrade() -> None:
|
|
40
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
41
|
+
with op.batch_alter_table("entity", schema=None) as batch_op:
|
|
42
|
+
batch_op.drop_index(
|
|
43
|
+
"uix_entity_permalink",
|
|
44
|
+
sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
|
45
|
+
)
|
|
46
|
+
batch_op.create_unique_constraint("uix_entity_permalink", ["permalink"])
|
|
47
|
+
batch_op.drop_index(batch_op.f("ix_entity_permalink"))
|
|
48
|
+
batch_op.create_index("ix_entity_permalink", ["permalink"], unique=1)
|
|
49
|
+
batch_op.alter_column("permalink", existing_type=sa.VARCHAR(), nullable=False)
|
|
50
|
+
|
|
51
|
+
# ### end Alembic commands ###
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
op.create_table(
|
|
25
|
+
"project",
|
|
26
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
|
27
|
+
sa.Column("name", sa.String(), nullable=False),
|
|
28
|
+
sa.Column("description", sa.Text(), nullable=True),
|
|
29
|
+
sa.Column("permalink", sa.String(), nullable=False),
|
|
30
|
+
sa.Column("path", sa.String(), nullable=False),
|
|
31
|
+
sa.Column("is_active", sa.Boolean(), nullable=False),
|
|
32
|
+
sa.Column("is_default", sa.Boolean(), nullable=True),
|
|
33
|
+
sa.Column("created_at", sa.DateTime(), nullable=False),
|
|
34
|
+
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
|
35
|
+
sa.PrimaryKeyConstraint("id"),
|
|
36
|
+
sa.UniqueConstraint("is_default"),
|
|
37
|
+
sa.UniqueConstraint("name"),
|
|
38
|
+
sa.UniqueConstraint("permalink"),
|
|
39
|
+
if_not_exists=True,
|
|
40
|
+
)
|
|
41
|
+
with op.batch_alter_table("project", schema=None) as batch_op:
|
|
42
|
+
batch_op.create_index(
|
|
43
|
+
"ix_project_created_at", ["created_at"], unique=False, if_not_exists=True
|
|
44
|
+
)
|
|
45
|
+
batch_op.create_index("ix_project_name", ["name"], unique=True, if_not_exists=True)
|
|
46
|
+
batch_op.create_index("ix_project_path", ["path"], unique=False, if_not_exists=True)
|
|
47
|
+
batch_op.create_index(
|
|
48
|
+
"ix_project_permalink", ["permalink"], unique=True, if_not_exists=True
|
|
49
|
+
)
|
|
50
|
+
batch_op.create_index(
|
|
51
|
+
"ix_project_updated_at", ["updated_at"], unique=False, if_not_exists=True
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
with op.batch_alter_table("entity", schema=None) as batch_op:
|
|
55
|
+
batch_op.add_column(sa.Column("project_id", sa.Integer(), nullable=False))
|
|
56
|
+
batch_op.drop_index(
|
|
57
|
+
"uix_entity_permalink",
|
|
58
|
+
sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
|
59
|
+
)
|
|
60
|
+
batch_op.drop_index("ix_entity_file_path")
|
|
61
|
+
batch_op.create_index(batch_op.f("ix_entity_file_path"), ["file_path"], unique=False)
|
|
62
|
+
batch_op.create_index("ix_entity_project_id", ["project_id"], unique=False)
|
|
63
|
+
batch_op.create_index(
|
|
64
|
+
"uix_entity_file_path_project", ["file_path", "project_id"], unique=True
|
|
65
|
+
)
|
|
66
|
+
batch_op.create_index(
|
|
67
|
+
"uix_entity_permalink_project",
|
|
68
|
+
["permalink", "project_id"],
|
|
69
|
+
unique=True,
|
|
70
|
+
sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
|
71
|
+
)
|
|
72
|
+
batch_op.create_foreign_key("fk_entity_project_id", "project", ["project_id"], ["id"])
|
|
73
|
+
|
|
74
|
+
# drop the search index table. it will be recreated
|
|
75
|
+
op.drop_table("search_index")
|
|
76
|
+
|
|
77
|
+
# ### end Alembic commands ###
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def downgrade() -> None:
|
|
81
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
82
|
+
with op.batch_alter_table("entity", schema=None) as batch_op:
|
|
83
|
+
batch_op.drop_constraint("fk_entity_project_id", type_="foreignkey")
|
|
84
|
+
batch_op.drop_index(
|
|
85
|
+
"uix_entity_permalink_project",
|
|
86
|
+
sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
|
87
|
+
)
|
|
88
|
+
batch_op.drop_index("uix_entity_file_path_project")
|
|
89
|
+
batch_op.drop_index("ix_entity_project_id")
|
|
90
|
+
batch_op.drop_index(batch_op.f("ix_entity_file_path"))
|
|
91
|
+
batch_op.create_index("ix_entity_file_path", ["file_path"], unique=1)
|
|
92
|
+
batch_op.create_index(
|
|
93
|
+
"uix_entity_permalink",
|
|
94
|
+
["permalink"],
|
|
95
|
+
unique=1,
|
|
96
|
+
sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
|
97
|
+
)
|
|
98
|
+
batch_op.drop_column("project_id")
|
|
99
|
+
|
|
100
|
+
with op.batch_alter_table("project", schema=None) as batch_op:
|
|
101
|
+
batch_op.drop_index("ix_project_updated_at")
|
|
102
|
+
batch_op.drop_index("ix_project_permalink")
|
|
103
|
+
batch_op.drop_index("ix_project_path")
|
|
104
|
+
batch_op.drop_index("ix_project_name")
|
|
105
|
+
batch_op.drop_index("ix_project_created_at")
|
|
106
|
+
|
|
107
|
+
op.drop_table("project")
|
|
108
|
+
# ### end Alembic commands ###
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
Since SQLite doesn't support dropping specific constraints easily, we'll
|
|
29
|
+
recreate the table without the problematic constraint.
|
|
30
|
+
"""
|
|
31
|
+
# For SQLite, we need to recreate the table without the UNIQUE constraint
|
|
32
|
+
# Create a new table without the UNIQUE constraint on is_default
|
|
33
|
+
op.create_table(
|
|
34
|
+
"project_new",
|
|
35
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
|
36
|
+
sa.Column("name", sa.String(), nullable=False),
|
|
37
|
+
sa.Column("description", sa.Text(), nullable=True),
|
|
38
|
+
sa.Column("permalink", sa.String(), nullable=False),
|
|
39
|
+
sa.Column("path", sa.String(), nullable=False),
|
|
40
|
+
sa.Column("is_active", sa.Boolean(), nullable=False),
|
|
41
|
+
sa.Column("is_default", sa.Boolean(), nullable=True), # No UNIQUE constraint!
|
|
42
|
+
sa.Column("created_at", sa.DateTime(), nullable=False),
|
|
43
|
+
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
|
44
|
+
sa.PrimaryKeyConstraint("id"),
|
|
45
|
+
sa.UniqueConstraint("name"),
|
|
46
|
+
sa.UniqueConstraint("permalink"),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Copy data from old table to new table
|
|
50
|
+
op.execute("INSERT INTO project_new SELECT * FROM project")
|
|
51
|
+
|
|
52
|
+
# Drop the old table
|
|
53
|
+
op.drop_table("project")
|
|
54
|
+
|
|
55
|
+
# Rename the new table
|
|
56
|
+
op.rename_table("project_new", "project")
|
|
57
|
+
|
|
58
|
+
# Recreate the indexes
|
|
59
|
+
with op.batch_alter_table("project", schema=None) as batch_op:
|
|
60
|
+
batch_op.create_index("ix_project_created_at", ["created_at"], unique=False)
|
|
61
|
+
batch_op.create_index("ix_project_name", ["name"], unique=True)
|
|
62
|
+
batch_op.create_index("ix_project_path", ["path"], unique=False)
|
|
63
|
+
batch_op.create_index("ix_project_permalink", ["permalink"], unique=True)
|
|
64
|
+
batch_op.create_index("ix_project_updated_at", ["updated_at"], unique=False)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def downgrade() -> None:
|
|
68
|
+
"""Add back the UNIQUE constraint on is_default column.
|
|
69
|
+
|
|
70
|
+
WARNING: This will break project creation again if multiple projects
|
|
71
|
+
have is_default=FALSE.
|
|
72
|
+
"""
|
|
73
|
+
# Recreate the table with the UNIQUE constraint
|
|
74
|
+
op.create_table(
|
|
75
|
+
"project_old",
|
|
76
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
|
77
|
+
sa.Column("name", sa.String(), nullable=False),
|
|
78
|
+
sa.Column("description", sa.Text(), nullable=True),
|
|
79
|
+
sa.Column("permalink", sa.String(), nullable=False),
|
|
80
|
+
sa.Column("path", sa.String(), nullable=False),
|
|
81
|
+
sa.Column("is_active", sa.Boolean(), nullable=False),
|
|
82
|
+
sa.Column("is_default", sa.Boolean(), nullable=True),
|
|
83
|
+
sa.Column("created_at", sa.DateTime(), nullable=False),
|
|
84
|
+
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
|
85
|
+
sa.PrimaryKeyConstraint("id"),
|
|
86
|
+
sa.UniqueConstraint("is_default"), # Add back the problematic constraint
|
|
87
|
+
sa.UniqueConstraint("name"),
|
|
88
|
+
sa.UniqueConstraint("permalink"),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Copy data (this may fail if multiple FALSE values exist)
|
|
92
|
+
op.execute("INSERT INTO project_old SELECT * FROM project")
|
|
93
|
+
|
|
94
|
+
# Drop the current table and rename
|
|
95
|
+
op.drop_table("project")
|
|
96
|
+
op.rename_table("project_old", "project")
|
|
97
|
+
|
|
98
|
+
# Recreate indexes
|
|
99
|
+
with op.batch_alter_table("project", schema=None) as batch_op:
|
|
100
|
+
batch_op.create_index("ix_project_created_at", ["created_at"], unique=False)
|
|
101
|
+
batch_op.create_index("ix_project_name", ["name"], unique=True)
|
|
102
|
+
batch_op.create_index("ix_project_path", ["path"], unique=False)
|
|
103
|
+
batch_op.create_index("ix_project_permalink", ["permalink"], unique=True)
|
|
104
|
+
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,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"])
|