diff --git a/CHANGELOG.md b/CHANGELOG.md index 6934b936..3db9588e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **Every fresh install broke on the first query (`redis` 8.1.0)** — the + SDK imports `redis.asyncio` directly but never declared `redis` as a + dependency, so pip took `falkordb`'s `redis>=7.1.0` range and resolved + to the newest release. redis 8.1.0 injects an internal + `himport_registry` key into `ConnectionPool.connection_kwargs`, which + `falkordb`'s `Is_Cluster()` forwards verbatim to the synchronous + `redis.Redis()` constructor — raising + `TypeError: Redis.__init__() got an unexpected keyword argument + 'himport_registry'`, surfaced as `DatabaseUnavailableError`. `ping()` + still succeeded, so health checks passed and the failure only appeared + on real work. `redis` is now declared explicitly as `>=7.2,<8.1`. The + lower bound is a second fix: `falkordb` 1.6 imports + `redis.driver_info`, which does not exist before redis 7.2, so its + declared `>=7.1.0` floor was also unusable. + ## [1.4.0] - 2026-08-10 Chunk-level extraction cache for `update()` (#288): re-ingesting a diff --git a/graphrag_sdk/pyproject.toml b/graphrag_sdk/pyproject.toml index 45fe9af6..5d9a1b47 100644 --- a/graphrag_sdk/pyproject.toml +++ b/graphrag_sdk/pyproject.toml @@ -34,6 +34,13 @@ classifiers = [ dependencies = [ "pydantic>=2.0,<3.0", "falkordb>=1.0,<2", + # We import redis.asyncio directly, so pin it explicitly rather than + # inheriting falkordb's range. Upper bound: redis 8.1 injects + # "himport_registry" into ConnectionPool.connection_kwargs, which + # falkordb's Is_Cluster() forwards to the sync redis.Redis() constructor + # -> TypeError on the first query. Lower bound: falkordb 1.6 imports + # redis.driver_info, which only exists from redis 7.2 on. + "redis>=7.2,<8.1", "numpy>=1.24,<3", "python-dotenv>=1.0", "tiktoken>=0.5,<1.0", diff --git a/graphrag_sdk/tests/test_connection.py b/graphrag_sdk/tests/test_connection.py index b113def0..c04e5380 100644 --- a/graphrag_sdk/tests/test_connection.py +++ b/graphrag_sdk/tests/test_connection.py @@ -322,3 +322,37 @@ async def test_ping_reports_unreachable_instead_of_raising(self): patch("falkordb.asyncio.FalkorDB") as mock_falkor: mock_falkor.side_effect = RedisConnectionError("Connection refused") assert await conn.ping() is False + + +class TestRedisVersionCompat: + """Guards the redis-py range declared in pyproject.toml. + + falkordb's ``Is_Cluster()`` copies ``pool.connection_kwargs`` off the + async pool and hands them straight to the *synchronous* + ``redis.Redis()`` constructor. Any key redis-py adds to its async pool + kwargs but does not accept on ``Redis.__init__`` therefore breaks the + first query with a TypeError. redis 8.1.0 did exactly that with + ``himport_registry``, so assert the invariant rather than the version + number — this fails on any future release that reintroduces the shape. + """ + + def _pool_kwargs(self, cfg: ConnectionConfig) -> dict: + """Build the pool the way _ensure_client() does, capturing its kwargs.""" + conn = FalkorDBConnection(cfg) + with patch("falkordb.asyncio.FalkorDB", side_effect=RuntimeError("stop after pool")): + with pytest.raises(Exception): + conn._ensure_client() + assert conn._pool is not None, "pool was not constructed" + return conn._pool.connection_kwargs.copy() + + @pytest.mark.parametrize("ssl", [False, True]) + def test_sync_redis_accepts_async_pool_kwargs(self, ssl): + import redis as sync_redis + import redis.asyncio as async_redis + + kwargs = self._pool_kwargs(ConnectionConfig(ssl=ssl)) + # Mirror Is_Cluster()'s own fixups. + kwargs["ssl"] = kwargs.pop("connection_class", None) is async_redis.SSLConnection + + # Constructing does not open a socket, so no live server is needed. + sync_redis.Redis(**kwargs)