fix: stabilize metadata filters on postgres by phernandez · Pull Request #536 · basicmachines-co/basic-memory · GitHub
Skip to content

fix: stabilize metadata filters on postgres - #536

Merged
phernandez merged 2 commits into
mainfrom
fix/metadata-filter-postgres
Feb 1, 2026
Merged

fix: stabilize metadata filters on postgres#536
phernandez merged 2 commits into
mainfrom
fix/metadata-filter-postgres

Conversation

@phernandez

Copy link
Copy Markdown
Member

Summary

  • fix Postgres metadata tag filters by casting bind params to jsonb instead of leaving named placeholders in SQL
  • simplify numeric metadata comparisons to cast JSON values to double precision; keep numeric params as floats
  • adjust metadata filter parsing to preserve numeric types for comparison operators

Testing

  • uv run pytest tests/repository/test_search_repository.py -k "metadata_filters" -q
  • BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest tests/repository/test_search_repository.py -k "metadata_filters" -q

Signed-off-by: phernandez <paul@basicmachines.co>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4dbcc52e3e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 342 to +345

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore numeric-guard to avoid cast failures

Casting #>> directly to double precision will raise invalid input syntax for type double precision if any row has non-numeric text at that metadata path (e.g., "N/A"), causing the entire search query to error out even though the filter value is numeric. The previous CASE+regex avoided this by returning NULL for non-numeric values; with the new unconditional cast, numeric comparisons now fail for mixed-type metadata.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Feb 1, 2026

Copy link
Copy Markdown
Contributor

Claude finished @phernandez's task in 2m 19s —— View job


PR Review: Fix metadata filters for Postgres

I've reviewed this PR against the Basic Memory team checklist. Here's my analysis:


Summary of Changes

This PR fixes Postgres metadata filter handling by:

  1. metadata_filters.py (lines 86-108): Preserving numeric types (as float) for comparison operators instead of converting them to strings via _normalize_scalar()
  2. postgres_search_repository.py (lines 322-359):
    • Replacing ::jsonb cast syntax with CAST(... AS jsonb) for better param binding
    • Simplifying numeric comparison expressions by removing regex-based guards

Critical Issue: P1 Concern is Valid ⚠️

The codex review raises a legitimate concern about line 344. The previous implementation had safety guards:

# Previous (safe):
numeric_expr = (
    f"CASE WHEN ({text_expr}) ~ '^-?\\\\d+(\\\\.\\\\d+)?$' "
    f"THEN ({text_expr})::double precision END"
)
# Current (unsafe):
compare_expr = f"({metadata_expr} #>> '{path}')::double precision"

Problem: If any entity has non-numeric text (e.g., "N/A", "unknown", "pending") in a metadata field that another entity uses numerically, the entire query will fail with:

ERROR: invalid input syntax for type double precision: "N/A"

The previous CASE WHEN ... ~ regex ... THEN cast pattern safely returned NULL for non-numeric values, allowing the query to succeed and simply exclude those rows from numeric comparisons.

Why this matters: Metadata fields can legitimately have mixed types across entities. For example:

  • Entity A: {"priority": 1}
  • Entity B: {"priority": "high"}
  • Entity C: {"priority": "N/A"}

A search for {"priority": {"$gt": 0}} would crash instead of just matching Entity A.


Team Checklist Review

✅ Code Quality & Standards

  • Python 3.12+ type annotations: Proper typing throughout
  • Async patterns: Follows SQLAlchemy 2.0 async conventions
  • Line length: All lines under 100 characters
  • Formatting: Code follows ruff formatting standards
  • Repository pattern: Changes properly isolated to repository layer

⚠️ Testing & Documentation

  • Tests exist: test_search_metadata_filters_numeric_comparisons covers basic numeric filtering
  • Edge case coverage: Missing test for mixed-type metadata fields (numeric + text in same field across entities)
  • 100% coverage: Changes are covered, but edge case may not be
  • Documentation: No updates needed for internal repository changes

✅ Architecture

  • Local-first: No impact on architecture
  • Composition: Repository pattern maintained
  • SQLAlchemy patterns: Proper use of text() queries with params

⚠️ Security & Performance

  • Input validation: Handled by parse_metadata_filters
  • Error handling: Numeric cast can fail with invalid input syntax error (regression from previous safe implementation)
  • Performance: CAST vs :: notation has no performance difference

✅ Compatibility

  • Cross-platform: Postgres-specific code properly isolated
  • No unicode/emojis: Clean code

Recommendations

  1. Restore the numeric guard (CRITICAL):

    # For postgres_search_repository.py line 343-347
    if filt.comparison == "numeric":
        # Safe: Only cast if value matches numeric regex pattern
        compare_expr = (
            f"CASE WHEN ({metadata_expr} #>> '{path}') ~ '^-?\\d+(\\.\\d+)?$' "
            f"THEN ({metadata_expr} #>> '{path}')::double precision END"
        )
    else:
        compare_expr = text_expr
  2. Add edge case test:

    async def test_search_metadata_filters_numeric_with_mixed_types(search_repository, session_maker):
        """Test numeric filters when some entities have non-numeric values."""
        entity_numeric = await _index_entity_with_metadata(
            search_repository, session_maker, "Has Number", {"score": 0.8}
        )
        entity_text = await _index_entity_with_metadata(
            search_repository, session_maker, "Has Text", {"score": "N/A"}
        )
        
        # Should match only numeric entity, not crash
        results = await search_repository.search(
            metadata_filters={"score": {"$gt": 0.5}}
        )
        assert {result.id for result in results} == {entity_numeric.id}
  3. Simplification is good, but safety matters: The CAST syntax change (line 335) is fine, but removing the regex guard traded robustness for simplicity.


Verdict

Request Changes: The P1 concern about unsafe numeric casting is valid and represents a regression. While the metadata filter parsing improvements are good, the removal of the safety guard in postgres_search_repository.py line 343-347 needs to be reverted.

The previous implementation's CASE WHEN ... ~ regex pattern was not just defensive programming—it was necessary to handle real-world scenarios where metadata fields contain mixed types across entities.

Fix this issue →


@phernandez phernandez changed the title Fix metadata filters for Postgres fix: stabilize metadata filters on postgres Feb 1, 2026
Signed-off-by: phernandez <paul@basicmachines.co>
@github-actions

github-actions Bot commented Feb 1, 2026

Copy link
Copy Markdown
Contributor

@phernandez
phernandez merged commit 009e849 into main Feb 1, 2026
24 of 28 checks passed
@phernandez
phernandez deleted the fix/metadata-filter-postgres branch February 1, 2026 04:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant