Skip to content

Commit 3f0669e

Browse files
authored
updates for 0.6.0 (#621)
This closes open customer feedback address with latest release https://github.com/redis-developer/sql-redis/releases/tag/v0.6.0 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Changes are dependency pins, broader error-string matching in MCP, and test coverage; no core query or runtime logic beyond MCP index detection. > > **Overview** > This PR aligns **redisvl** with **sql-redis 0.6.0** and locks related optional deps: `sql-redis` is raised from `>=0.5.0` to `>=0.6.0` (including the `all` extra), and **mistralai** is capped at `<2` in both `mistralai` and `all` extras. > > **MCP** startup now treats more RediSearch “missing index” error strings as absent indexes (`search_index_not_found`, `index not found`), so misconfigured index names fail with a clear error across RediSearch versions. > > **Tests** drop the `xfail` on numeric `IN` in hash and JSON SQL integration suites, and add `TestSQLRedis060Regression` on hash storage—end-to-end coverage for ten customer-reported sql-redis fixes (negated filters, tag pipes, numeric `IN`, invalid `LIKE`/`BETWEEN`, double-quoted literals, `DISTINCT`, multi-column `ORDER BY`). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 3687385f7da7ab0a0ce504619a98eb43946f3951. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 1d8d095 commit 3f0669e

5 files changed

Lines changed: 3807 additions & 2761 deletions

File tree

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ mcp = [
3838
"fastmcp>=2.0.0",
3939
"pydantic-settings>=2.0",
4040
]
41-
mistralai = ["mistralai>=1.0.0"]
41+
mistralai = ["mistralai>=1.0.0,<2"]
4242
openai = ["openai>=1.1.0"]
4343
nltk = ["nltk>=3.8.1,<4"]
4444
cohere = ["cohere>=4.44"]
@@ -57,10 +57,10 @@ pillow = [
5757
"pillow>=11.3.0",
5858
]
5959
sql-redis = [
60-
"sql-redis>=0.5.0",
60+
"sql-redis>=0.6.0",
6161
]
6262
all = [
63-
"mistralai>=1.0.0",
63+
"mistralai>=1.0.0,<2",
6464
"openai>=1.1.0",
6565
"nltk>=3.8.1,<4",
6666
"cohere>=4.44",
@@ -72,7 +72,7 @@ all = [
7272
"boto3>=1.36.0,<2",
7373
"urllib3<2.2.0",
7474
"pillow>=11.3.0",
75-
"sql-redis>=0.5.0",
75+
"sql-redis>=0.6.0",
7676
"ollama>=0.5.4",
7777
]
7878
ollama = [

redisvl/mcp/server.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,19 @@ def _register_tools(self, schema: IndexSchema) -> None:
204204

205205
@staticmethod
206206
def _is_missing_index_error(exc: RedisSearchError) -> bool:
207-
"""Detect the Redis search errors that mean the configured index is absent."""
207+
"""Detect the Redis search errors that mean the configured index is absent.
208+
209+
Different RediSearch versions phrase the error differently
210+
(``unknown index name``, ``no such index``, or ``SEARCH_INDEX_NOT_FOUND
211+
Index not found``), so check for each known wording.
212+
"""
208213
message = str(exc).lower()
209-
return "unknown index name" in message or "no such index" in message
214+
return (
215+
"unknown index name" in message
216+
or "no such index" in message
217+
or "search_index_not_found" in message
218+
or "index not found" in message
219+
)
210220

211221
@asynccontextmanager
212222
async def _server_lifespan(self, _server: Any):

tests/integration/test_sql_redis_hash.py

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,6 @@ def test_numeric_not_equals(self, sql_index):
462462
for result in results:
463463
assert float(result["price"]) != 45
464464

465-
@pytest.mark.xfail(reason="Numeric IN operator not yet supported in sql-redis")
466465
def test_numeric_in(self, sql_index):
467466
"""Test numeric IN operator."""
468467
sql_query = SQLQuery(
@@ -1585,3 +1584,177 @@ def test_group_by_year(self, date_index):
15851584
counts = {str(r["year"]): int(r["count"]) for r in results}
15861585
assert counts.get("2023") == 1
15871586
assert counts.get("2024") == 4
1587+
1588+
1589+
class TestSQLRedis060Regression:
1590+
"""Regression tests for the 10 trial-user findings fixed in sql-redis 0.6.0.
1591+
1592+
Each test exercises one of the defects end-to-end through SQLQuery +
1593+
SearchIndex. Assertions are behavior-only (result-set membership or
1594+
ValueError on inputs that previously emitted garbage queries) so they
1595+
do not depend on the exact RediSearch query-string formatting.
1596+
"""
1597+
1598+
def test_not_in_tag_excludes_listed_values(self, sql_index):
1599+
"""Case 1: NOT IN on TAG must exclude the listed values, not match them."""
1600+
results = sql_index.query(
1601+
SQLQuery(
1602+
f"SELECT title, category FROM {sql_index.name} "
1603+
f"WHERE category NOT IN ('electronics', 'books')"
1604+
)
1605+
)
1606+
1607+
assert len(results) > 0
1608+
for r in results:
1609+
assert r["category"] not in ("electronics", "books")
1610+
1611+
def test_not_between_numeric_excludes_range(self, sql_index):
1612+
"""Case 2: NOT BETWEEN on NUMERIC must exclude the range."""
1613+
results = sql_index.query(
1614+
SQLQuery(
1615+
f"SELECT title, price FROM {sql_index.name} "
1616+
f"WHERE price NOT BETWEEN 40 AND 60"
1617+
)
1618+
)
1619+
1620+
assert len(results) > 0
1621+
for r in results:
1622+
price = float(r["price"])
1623+
assert price < 40 or price > 60
1624+
1625+
def test_not_comparison_numeric_is_negated(self, sql_index):
1626+
"""Case 3: NOT price > 100 must return only prices <= 100."""
1627+
results = sql_index.query(
1628+
SQLQuery(
1629+
f"SELECT title, price FROM {sql_index.name} " f"WHERE NOT price > 100"
1630+
)
1631+
)
1632+
1633+
assert len(results) > 0
1634+
for r in results:
1635+
assert float(r["price"]) <= 100
1636+
1637+
def test_not_equals_on_tag_is_negated(self, sql_index):
1638+
"""Case 3 (TAG side): NOT category = 'electronics' must exclude electronics."""
1639+
results = sql_index.query(
1640+
SQLQuery(
1641+
f"SELECT title, category FROM {sql_index.name} "
1642+
f"WHERE NOT category = 'electronics'"
1643+
)
1644+
)
1645+
1646+
assert len(results) > 0
1647+
for r in results:
1648+
assert r["category"] != "electronics"
1649+
1650+
def test_literal_pipe_in_tag_value_is_not_a_separator(self, sql_index):
1651+
"""Case 4: a literal '|' inside a TAG value must not split the value.
1652+
1653+
No product has the literal tag 'bestseller|featured'. Pre-0.6.0 the
1654+
pipe was read as the IN-list separator, so this matched every row
1655+
tagged 'bestseller' OR 'featured'. Post-fix it matches nothing.
1656+
"""
1657+
results = sql_index.query(
1658+
SQLQuery(
1659+
f"SELECT title FROM {sql_index.name} "
1660+
f"WHERE tags = 'bestseller|featured'"
1661+
)
1662+
)
1663+
1664+
assert results == []
1665+
1666+
def test_numeric_in_does_not_crash(self, sql_index):
1667+
"""Case 5: IN on NUMERIC used to crash with TypeError: float([...])."""
1668+
results = sql_index.query(
1669+
SQLQuery(
1670+
f"SELECT title, price FROM {sql_index.name} "
1671+
f"WHERE price IN (45, 55, 65)"
1672+
)
1673+
)
1674+
1675+
assert sorted(float(r["price"]) for r in results) == [45.0, 55.0, 65.0]
1676+
1677+
def test_empty_like_raises_value_error(self, sql_index):
1678+
"""Case 6: LIKE '' is rejected with a clear ValueError."""
1679+
sql_query = SQLQuery(f"SELECT title FROM {sql_index.name} WHERE title LIKE ''")
1680+
1681+
with pytest.raises(ValueError):
1682+
sql_index.query(sql_query)
1683+
1684+
def test_between_on_tag_raises_value_error(self, sql_index):
1685+
"""Case 7: BETWEEN on TAG is rejected with a clear ValueError."""
1686+
sql_query = SQLQuery(
1687+
f"SELECT title FROM {sql_index.name} " f"WHERE category BETWEEN 'a' AND 'z'"
1688+
)
1689+
1690+
with pytest.raises(ValueError):
1691+
sql_index.query(sql_query)
1692+
1693+
def test_double_quoted_value_treated_as_literal(self, sql_index):
1694+
"""Case 8: `category = "books"` must be a value match, not @category:{None}."""
1695+
results = sql_index.query(
1696+
SQLQuery(
1697+
f"SELECT title, category FROM {sql_index.name} "
1698+
f'WHERE category = "books"'
1699+
)
1700+
)
1701+
1702+
assert len(results) > 0
1703+
for r in results:
1704+
assert r["category"] == "books"
1705+
1706+
def test_double_quoted_numeric_value_does_not_crash(self, sql_index):
1707+
"""Case 8 (NUMERIC side): `price = "45"` used to crash with float(None)."""
1708+
results = sql_index.query(
1709+
SQLQuery(f'SELECT title, price FROM {sql_index.name} WHERE price = "45"')
1710+
)
1711+
1712+
assert len(results) >= 1
1713+
for r in results:
1714+
assert float(r["price"]) == 45.0
1715+
1716+
def test_select_distinct_returns_unique_rows(self, sql_index):
1717+
"""Case 9: SELECT DISTINCT must deduplicate; was previously ignored."""
1718+
results = sql_index.query(
1719+
SQLQuery(f"SELECT DISTINCT category FROM {sql_index.name}")
1720+
)
1721+
1722+
categories = [r["category"] for r in results]
1723+
assert sorted(categories) == sorted(set(categories))
1724+
assert set(categories) == {
1725+
"electronics",
1726+
"books",
1727+
"accessories",
1728+
"stationery",
1729+
}
1730+
1731+
def test_select_distinct_star_raises_value_error(self, sql_index):
1732+
"""Case 9: DISTINCT * has no clean RediSearch mapping; must raise."""
1733+
sql_query = SQLQuery(f"SELECT DISTINCT * FROM {sql_index.name}")
1734+
1735+
with pytest.raises(ValueError):
1736+
sql_index.query(sql_query)
1737+
1738+
def test_multi_key_order_by_breaks_ties_with_secondary_key(self, sql_index):
1739+
"""Case 10: pre-0.6.0 silently dropped every ORDER BY key after the first.
1740+
1741+
Two rows tie at rating 4.7 (Python Programming $45, Laptop and Keyboard
1742+
Bundle $999) and two at rating 4.6 (Redis in Action $55, Mechanical
1743+
Keyboard $149). With the fix the secondary `price ASC` key breaks the
1744+
tie deterministically.
1745+
"""
1746+
results = sql_index.query(
1747+
SQLQuery(
1748+
f"SELECT title, rating, price FROM {sql_index.name} "
1749+
f"WHERE rating IN (4.7, 4.6) "
1750+
f"ORDER BY rating DESC, price ASC"
1751+
)
1752+
)
1753+
1754+
titles = [r["title"] for r in results]
1755+
assert titles == [
1756+
"Python Programming",
1757+
"Laptop and Keyboard Bundle",
1758+
"Redis in Action",
1759+
"Mechanical Keyboard",
1760+
]

tests/integration/test_sql_redis_json.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,6 @@ def test_numeric_not_equals(self, sql_index):
545545
for result in results:
546546
assert float(result["price"]) != 45
547547

548-
@pytest.mark.xfail(reason="Numeric IN operator not yet supported in sql-redis")
549548
def test_numeric_in(self, sql_index):
550549
"""Test numeric IN operator."""
551550
sql_query = SQLQuery(

0 commit comments

Comments
 (0)