basic-memory 0.13.0b3__py3-none-any.whl → 0.13.0b4__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.

@@ -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)
@@ -111,10 +111,9 @@ async def add_project(
111
111
  Response confirming the project was added
112
112
  """
113
113
  try: # pragma: no cover
114
- await project_service.add_project(project_data.name, project_data.path)
115
-
116
- if project_data.set_default: # pragma: no cover
117
- await project_service.set_default_project(project_data.name)
114
+ await project_service.add_project(
115
+ project_data.name, project_data.path, set_default=project_data.set_default
116
+ )
118
117
 
119
118
  return ProjectStatusResponse( # pyright: ignore [reportCallIssue]
120
119
  message=f"Project '{project_data.name}' added successfully",
@@ -49,9 +49,7 @@ class Project(Base):
49
49
 
50
50
  # Status flags
51
51
  is_active: Mapped[bool] = mapped_column(Boolean, default=True)
52
- is_default: Mapped[Optional[bool]] = mapped_column(
53
- Boolean, default=None, unique=True, nullable=True
54
- )
52
+ is_default: Mapped[Optional[bool]] = mapped_column(Boolean, default=None, nullable=True)
55
53
 
56
54
  # Timestamps
57
55
  created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
@@ -67,12 +67,13 @@ class ProjectService:
67
67
  """Get the file path for a project by name."""
68
68
  return await self.repository.get_by_name(name)
69
69
 
70
- async def add_project(self, name: str, path: str) -> None:
70
+ async def add_project(self, name: str, path: str, set_default: bool = False) -> None:
71
71
  """Add a new project to the configuration and database.
72
72
 
73
73
  Args:
74
74
  name: The name of the project
75
75
  path: The file path to the project directory
76
+ set_default: Whether to set this project as the default
76
77
 
77
78
  Raises:
78
79
  ValueError: If the project already exists
@@ -92,9 +93,16 @@ class ProjectService:
92
93
  "path": resolved_path,
93
94
  "permalink": generate_permalink(project_config.name),
94
95
  "is_active": True,
95
- "is_default": False,
96
+ # Don't set is_default=False to avoid UNIQUE constraint issues
97
+ # Let it default to NULL, only set to True when explicitly making default
96
98
  }
97
- await self.repository.create(project_data)
99
+ created_project = await self.repository.create(project_data)
100
+
101
+ # If this should be the default project, ensure only one default exists
102
+ if set_default:
103
+ await self.repository.set_as_default(created_project.id)
104
+ config_manager.set_default_project(name)
105
+ logger.info(f"Project '{name}' set as default")
98
106
 
99
107
  logger.info(f"Project '{name}' added at {resolved_path}")
100
108
 
@@ -144,6 +152,45 @@ class ProjectService:
144
152
 
145
153
  logger.info(f"Project '{name}' set as default in configuration and database")
146
154
 
155
+ async def _ensure_single_default_project(self) -> None:
156
+ """Ensure only one project has is_default=True.
157
+
158
+ This method validates the database state and fixes any issues where
159
+ multiple projects might have is_default=True or no project is marked as default.
160
+ """
161
+ if not self.repository:
162
+ raise ValueError("Repository is required for _ensure_single_default_project") # pragma: no cover
163
+
164
+ # Get all projects with is_default=True
165
+ db_projects = await self.repository.find_all()
166
+ default_projects = [p for p in db_projects if p.is_default is True]
167
+
168
+ if len(default_projects) > 1: # pragma: no cover
169
+ # Multiple defaults found - fix by keeping the first one and clearing others
170
+ # This is defensive code that should rarely execute due to business logic enforcement
171
+ logger.warning( # pragma: no cover
172
+ f"Found {len(default_projects)} projects with is_default=True, fixing..."
173
+ )
174
+ keep_default = default_projects[0] # pragma: no cover
175
+
176
+ # Clear all defaults first, then set only the first one as default
177
+ await self.repository.set_as_default(keep_default.id) # pragma: no cover
178
+
179
+ logger.info(
180
+ f"Fixed default project conflicts, kept '{keep_default.name}' as default"
181
+ ) # pragma: no cover
182
+
183
+ elif len(default_projects) == 0: # pragma: no cover
184
+ # No default project - set the config default as default
185
+ # This is defensive code for edge cases where no default exists
186
+ config_default = config_manager.default_project # pragma: no cover
187
+ config_project = await self.repository.get_by_name(config_default) # pragma: no cover
188
+ if config_project: # pragma: no cover
189
+ await self.repository.set_as_default(config_project.id) # pragma: no cover
190
+ logger.info(
191
+ f"Set '{config_default}' as default project (was missing)"
192
+ ) # pragma: no cover
193
+
147
194
  async def synchronize_projects(self) -> None: # pragma: no cover
148
195
  """Synchronize projects between database and configuration.
149
196
 
@@ -172,7 +219,7 @@ class ProjectService:
172
219
  "path": path,
173
220
  "permalink": name.lower().replace(" ", "-"),
174
221
  "is_active": True,
175
- "is_default": (name == config_manager.default_project),
222
+ # Don't set is_default here - let the enforcement logic handle it
176
223
  }
177
224
  await self.repository.create(project_data)
178
225
 
@@ -182,19 +229,23 @@ class ProjectService:
182
229
  logger.info(f"Adding project '{name}' to configuration")
183
230
  config_manager.add_project(name, project.path)
184
231
 
185
- # Make sure default project is synchronized
186
- db_default = next((p for p in db_projects if p.is_default), None)
232
+ # Ensure database default project state is consistent
233
+ await self._ensure_single_default_project()
234
+
235
+ # Make sure default project is synchronized between config and database
236
+ db_default = await self.repository.get_default_project()
187
237
  config_default = config_manager.default_project
188
238
 
189
239
  if db_default and db_default.name != config_default:
190
240
  # Update config to match DB default
191
241
  logger.info(f"Updating default project in config to '{db_default.name}'")
192
242
  config_manager.set_default_project(db_default.name)
193
- elif not db_default and config_default in db_projects_by_name:
194
- # Update DB to match config default
195
- logger.info(f"Updating default project in database to '{config_default}'")
196
- project = db_projects_by_name[config_default]
197
- await self.repository.set_as_default(project.id)
243
+ elif not db_default and config_default:
244
+ # Update DB to match config default (if the project exists)
245
+ project = await self.repository.get_by_name(config_default)
246
+ if project:
247
+ logger.info(f"Updating default project in database to '{config_default}'")
248
+ await self.repository.set_as_default(project.id)
198
249
 
199
250
  logger.info("Project synchronization complete")
200
251
 
@@ -546,4 +597,4 @@ class ProjectService:
546
597
  database_size=db_size_readable,
547
598
  watch_status=watch_status,
548
599
  timestamp=datetime.now(),
549
- )
600
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: basic-memory
3
- Version: 0.13.0b3
3
+ Version: 0.13.0b4
4
4
  Summary: Local-first knowledge management combining Zettelkasten with knowledge graphs
5
5
  Project-URL: Homepage, https://github.com/basicmachines-co/basic-memory
6
6
  Project-URL: Repository, https://github.com/basicmachines-co/basic-memory
@@ -11,6 +11,7 @@ basic_memory/alembic/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl
11
11
  basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py,sha256=lTbWlAnd1es7xU99DoJgfaRe1_Kte8TL98riqeKGV80,4363
12
12
  basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py,sha256=k6xYTmYPM9Ros-7CA7BwZBKYwoK_gmVdC-2n8FAjdoE,1840
13
13
  basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py,sha256=2CCY9ayjzbraYMcinqSsJi9Sc0nu2e-ehkUJus-sz34,4379
14
+ basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py,sha256=YErFkIpZdicvUil4ZtE6uxSpk5BZCTXZ_TfPE-MgSfo,4210
14
15
  basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py,sha256=RsGymQzfRXV1LSNKiyi0lMilTxW1NgwS9jR67ye2apI,1428
15
16
  basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py,sha256=kDavR9Qqx9wSu5P3qd4SZq_waIsDG1UMTg2SmDoktMU,3679
16
17
  basic_memory/api/__init__.py,sha256=wCpj-21j1D0KzKl9Ql6unLBVFY0K1uGp_FeSZRKtqpk,72
@@ -22,7 +23,7 @@ basic_memory/api/routers/importer_router.py,sha256=xFUCorkPWo8AF0ya0UrcLmXNf8CjP
22
23
  basic_memory/api/routers/knowledge_router.py,sha256=sdRGWqBmAjELkJ7ZXyZB6qTU1CVH4EXpKOyDDVGV4yU,9133
23
24
  basic_memory/api/routers/management_router.py,sha256=INT5PzUXhfBH2546CTZKqZnRuECYIA4Ypfgdf6JNyu0,2686
24
25
  basic_memory/api/routers/memory_router.py,sha256=M344_tHIFzgi-i5PqltaPHdZaut_bMejqktmzLKto0c,2995
25
- basic_memory/api/routers/project_router.py,sha256=TMREu8qVnTvx1y9crdx6UsfY8OtaXfpG4KhgJ9O1Gs0,8246
26
+ basic_memory/api/routers/project_router.py,sha256=ZlKcrA7lThWcm7-IdFMBtxMMd7IoV03GRfSH1zVs5Ug,8175
26
27
  basic_memory/api/routers/prompt_router.py,sha256=LzZAerGtDLPdyTKjThQciVGPUkjbIc3dmdJHX-ievSo,9913
27
28
  basic_memory/api/routers/resource_router.py,sha256=WEJEqEaY_yTKj5-U-rW4kXQKUcJflykgwI6_g_R41ck,8058
28
29
  basic_memory/api/routers/search_router.py,sha256=GD62jlCQTiL_VNsdibi-b1f6H40KCWo9SX2Cl7YH4QU,1226
@@ -87,7 +88,7 @@ basic_memory/mcp/tools/write_note.py,sha256=yGPb9HGB5BFnjBD1lFNsZ7wF4ItY2nOxaE05
87
88
  basic_memory/models/__init__.py,sha256=j0C4dtFi-FOEaQKR8dQWEG-dJtdQ15NBTiJg4nbIXNU,333
88
89
  basic_memory/models/base.py,sha256=4hAXJ8CE1RnjKhb23lPd-QM7G_FXIdTowMJ9bRixspU,225
89
90
  basic_memory/models/knowledge.py,sha256=AFxfKS8fRa43Kq3EjJCAufpte4VNC7fs9YfshDrB4o0,7087
90
- basic_memory/models/project.py,sha256=S3PvS2o8dFLOo8qiSdI12CG29dTvHw2JIpYJLZ0UFmg,2800
91
+ basic_memory/models/project.py,sha256=oUrQaUOu7_muSl-i38Dh0HzmCFrMAtwgxALDUTt9k5c,2773
91
92
  basic_memory/models/search.py,sha256=PhQ8w4taApSvjh1DpPhB4cH9GTt2E2po-DFZzhnoZkY,1300
92
93
  basic_memory/repository/__init__.py,sha256=MWK-o8QikqzOpe5SyPbKQ2ioB5BWA0Upz65tgg-E0DU,327
93
94
  basic_memory/repository/entity_repository.py,sha256=larjP7r6Sc7YyPD1BloC_m96McYsjHTf6doUQy3gSY4,3776
@@ -116,7 +117,7 @@ basic_memory/services/exceptions.py,sha256=oVjQr50XQqnFq1-MNKBilI2ShtHDxypavyDk1
116
117
  basic_memory/services/file_service.py,sha256=jCrmnEkTQ4t9HF7L_M6BL7tdDqjjzty9hpTo9AzwhvM,10059
117
118
  basic_memory/services/initialization.py,sha256=1sPz0dAsFVJOaiWBG6FgxKmwLNrMtBrmX4I4qVTI0aY,7786
118
119
  basic_memory/services/link_resolver.py,sha256=8KgozYS8reQaihzDMS8_0tDKo-BdqHjhECIM39I_wis,4078
119
- basic_memory/services/project_service.py,sha256=7P3fCmS5YOmD64vN_WKpJJ_V52ca600uBnlqf264_2Y,20520
120
+ basic_memory/services/project_service.py,sha256=mntHwPwypgec45vsB5ASzTybA_UGAf4lA9cRKw4sl8E,23310
120
121
  basic_memory/services/search_service.py,sha256=c5Ky0ufz7YPFgHhVzNRQ4OecF_JUrt7nALzpMjobW4M,12782
121
122
  basic_memory/services/service.py,sha256=V-d_8gOV07zGIQDpL-Ksqs3ZN9l3qf3HZOK1f_YNTag,336
122
123
  basic_memory/sync/__init__.py,sha256=CVHguYH457h2u2xoM8KvOilJC71XJlZ-qUh8lHcjYj4,156
@@ -125,8 +126,8 @@ basic_memory/sync/sync_service.py,sha256=7ccn4_YWcmC9VSEgm6diosRAibn4LxBS3lWb8vc
125
126
  basic_memory/sync/watch_service.py,sha256=JAumrHUjV1lF9NtEK32jgg0myWBfLXotNXxONeIV9SM,15316
126
127
  basic_memory/templates/prompts/continue_conversation.hbs,sha256=begMFHOPN3aCm5sHz5PlKMLOfZ8hlpFxFJ-hgy0T9K4,3075
127
128
  basic_memory/templates/prompts/search.hbs,sha256=H1cCIsHKp4VC1GrH2KeUB8pGe5vXFPqb2VPotypmeCA,3098
128
- basic_memory-0.13.0b3.dist-info/METADATA,sha256=9PsyggvtWB9fpo9h4RfUOfy4g4JVqrRYbeOjGWiIwz0,15468
129
- basic_memory-0.13.0b3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
130
- basic_memory-0.13.0b3.dist-info/entry_points.txt,sha256=wvE2mRF6-Pg4weIYcfQ-86NOLZD4WJg7F7TIsRVFLb8,90
131
- basic_memory-0.13.0b3.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
132
- basic_memory-0.13.0b3.dist-info/RECORD,,
129
+ basic_memory-0.13.0b4.dist-info/METADATA,sha256=Anht8NA7a-vnCuyqZQfv1quuEHqerGWwg26uegkhUZo,15468
130
+ basic_memory-0.13.0b4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
131
+ basic_memory-0.13.0b4.dist-info/entry_points.txt,sha256=wvE2mRF6-Pg4weIYcfQ-86NOLZD4WJg7F7TIsRVFLb8,90
132
+ basic_memory-0.13.0b4.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
133
+ basic_memory-0.13.0b4.dist-info/RECORD,,