Skip to content

Commit 3f59d10

Browse files
fix: Handle null, empty, and string 'None' title in markdown frontmatter (#387)
This fixes a bug where entities with invalid title values in frontmatter caused SQL INSERT failures due to NOT NULL constraint violations. The parser now properly falls back to the filename stem when title is: - None/null (YAML null value) - Empty string - The string "None" (common in user templates) This matches the existing pattern used for type field validation. Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
1 parent 32236cd commit 3f59d10

2 files changed

Lines changed: 85 additions & 2 deletions

File tree

src/basic_memory/markdown/entity_parser.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,13 @@ async def parse_file_content(self, absolute_path, file_content):
129129
file_stats = absolute_path.stat()
130130
metadata = post.metadata
131131

132-
# Ensure required fields have defaults (issue #184)
133-
metadata["title"] = post.metadata.get("title", absolute_path.stem)
132+
# Ensure required fields have defaults (issue #184, #387)
133+
# Handle title - use default if missing, None/null, empty, or string "None"
134+
title = post.metadata.get("title")
135+
if not title or title == "None":
136+
metadata["title"] = absolute_path.stem
137+
else:
138+
metadata["title"] = title
134139
# Handle type - use default if missing OR explicitly set to None/null
135140
entity_type = post.metadata.get("type")
136141
metadata["type"] = entity_type if entity_type is not None else "note"

tests/markdown/test_entity_parser_error_handling.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,84 @@ async def test_parse_file_with_null_entity_type(tmp_path):
183183
assert result.frontmatter.title == "Test File"
184184

185185

186+
@pytest.mark.asyncio
187+
async def test_parse_file_with_null_title(tmp_path):
188+
"""Test that files with explicit null title get default from filename (issue #387)."""
189+
# Create a file with null title
190+
test_file = tmp_path / "null_title.md"
191+
content = dedent(
192+
"""
193+
---
194+
title: null
195+
type: note
196+
---
197+
# Content
198+
"""
199+
).strip()
200+
test_file.write_text(content)
201+
202+
# Parse the file
203+
parser = EntityParser(tmp_path)
204+
result = await parser.parse_file(test_file)
205+
206+
# Should have default title from filename even when explicitly set to null
207+
assert result is not None
208+
assert result.frontmatter.title == "null_title" # Default from filename
209+
assert result.frontmatter.type == "note"
210+
211+
212+
@pytest.mark.asyncio
213+
async def test_parse_file_with_empty_title(tmp_path):
214+
"""Test that files with empty title get default from filename (issue #387)."""
215+
# Create a file with empty title
216+
test_file = tmp_path / "empty_title.md"
217+
content = dedent(
218+
"""
219+
---
220+
title:
221+
type: note
222+
---
223+
# Content
224+
"""
225+
).strip()
226+
test_file.write_text(content)
227+
228+
# Parse the file
229+
parser = EntityParser(tmp_path)
230+
result = await parser.parse_file(test_file)
231+
232+
# Should have default title from filename when title is empty
233+
assert result is not None
234+
assert result.frontmatter.title == "empty_title" # Default from filename
235+
assert result.frontmatter.type == "note"
236+
237+
238+
@pytest.mark.asyncio
239+
async def test_parse_file_with_string_none_title(tmp_path):
240+
"""Test that files with string 'None' title get default from filename (issue #387)."""
241+
# Create a file with string "None" as title (common in templates)
242+
test_file = tmp_path / "template_file.md"
243+
content = dedent(
244+
"""
245+
---
246+
title: "None"
247+
type: note
248+
---
249+
# Content
250+
"""
251+
).strip()
252+
test_file.write_text(content)
253+
254+
# Parse the file
255+
parser = EntityParser(tmp_path)
256+
result = await parser.parse_file(test_file)
257+
258+
# Should have default title from filename when title is string "None"
259+
assert result is not None
260+
assert result.frontmatter.title == "template_file" # Default from filename
261+
assert result.frontmatter.type == "note"
262+
263+
186264
@pytest.mark.asyncio
187265
async def test_parse_valid_file_still_works(tmp_path):
188266
"""Test that valid files with proper frontmatter still parse correctly."""

0 commit comments

Comments
 (0)