Skip to content

Commit cb51d70

Browse files
authored
Merge pull request #19 from redis-developer/feat/RAAE-1542/schema_lazy_load
executor factory pattern
2 parents 91f30e3 + a093820 commit cb51d70

3 files changed

Lines changed: 159 additions & 3 deletions

File tree

sql_redis/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
"""SQL to Redis command translation utility."""
22

3-
from sql_redis.executor import AsyncExecutor, Executor, QueryResult
3+
from sql_redis.executor import (
4+
AsyncExecutor,
5+
Executor,
6+
QueryResult,
7+
SchemaCacheStrategy,
8+
create_async_executor,
9+
create_executor,
10+
)
411
from sql_redis.schema import AsyncSchemaRegistry, SchemaRegistry
512
from sql_redis.translator import TranslatedQuery, Translator
613
from sql_redis.version import __version__
@@ -12,6 +19,9 @@
1219
"AsyncSchemaRegistry",
1320
"Executor",
1421
"AsyncExecutor",
22+
"create_executor",
23+
"create_async_executor",
24+
"SchemaCacheStrategy",
1525
"QueryResult",
1626
"__version__",
1727
]

sql_redis/executor.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import re
66
from dataclasses import dataclass
7-
from typing import TYPE_CHECKING, Any
7+
from typing import TYPE_CHECKING, Any, Literal, cast
88

99
import redis
1010

@@ -15,6 +15,18 @@
1515
import redis.asyncio as async_redis
1616

1717

18+
SchemaCacheStrategy = Literal["lazy", "load_all"]
19+
20+
21+
def _validate_schema_cache_strategy(
22+
schema_cache_strategy: str,
23+
) -> SchemaCacheStrategy:
24+
"""Validate and normalize the schema cache strategy."""
25+
if schema_cache_strategy not in {"lazy", "load_all"}:
26+
raise ValueError("schema_cache_strategy must be one of: 'lazy', 'load_all'")
27+
return cast(SchemaCacheStrategy, schema_cache_strategy)
28+
29+
1830
def _substitute_params(sql: str, params: dict[str, Any]) -> str:
1931
"""Substitute parameter placeholders in SQL with actual values.
2032
@@ -270,3 +282,51 @@ async def execute(self, sql: str, *, params: dict | None = None) -> QueryResult:
270282
rows.append(row)
271283

272284
return QueryResult(rows=rows, count=count)
285+
286+
287+
def create_executor(
288+
client: redis.Redis,
289+
*,
290+
schema_registry: SchemaRegistry | None = None,
291+
schema_cache_strategy: SchemaCacheStrategy = "lazy",
292+
) -> Executor:
293+
"""Create a sync SQL executor with the requested schema cache strategy.
294+
295+
Args:
296+
client: Redis client used by the executor.
297+
schema_registry: Optional existing registry to reuse.
298+
schema_cache_strategy: Schema loading strategy. ``"lazy"`` defers
299+
``FT.INFO`` calls until a referenced index is needed. ``"load_all"``
300+
preserves the historical eager behavior by preloading all schemas.
301+
"""
302+
schema_cache_strategy = _validate_schema_cache_strategy(schema_cache_strategy)
303+
304+
registry = schema_registry or SchemaRegistry(client)
305+
if schema_cache_strategy == "load_all":
306+
registry.load_all()
307+
308+
return Executor(client, registry)
309+
310+
311+
async def create_async_executor(
312+
client: "async_redis.Redis",
313+
*,
314+
schema_registry: AsyncSchemaRegistry | None = None,
315+
schema_cache_strategy: SchemaCacheStrategy = "lazy",
316+
) -> AsyncExecutor:
317+
"""Create an async SQL executor with the requested schema cache strategy.
318+
319+
Args:
320+
client: Async Redis client used by the executor.
321+
schema_registry: Optional existing async registry to reuse.
322+
schema_cache_strategy: Schema loading strategy. ``"lazy"`` defers
323+
``FT.INFO`` calls until a referenced index is needed. ``"load_all"``
324+
preserves the historical eager behavior by preloading all schemas.
325+
"""
326+
schema_cache_strategy = _validate_schema_cache_strategy(schema_cache_strategy)
327+
328+
registry = schema_registry or AsyncSchemaRegistry(client)
329+
if schema_cache_strategy == "load_all":
330+
await registry.load_all()
331+
332+
return AsyncExecutor(client, registry)

tests/test_schema_caching.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
import redis
1717
import redis.asyncio as async_redis
1818

19-
from sql_redis.executor import AsyncExecutor, Executor
19+
from sql_redis.executor import (
20+
AsyncExecutor,
21+
Executor,
22+
create_async_executor,
23+
create_executor,
24+
)
2025
from sql_redis.schema import AsyncSchemaRegistry, SchemaRegistry
2126

2227
# ---------------------------------------------------------------------------
@@ -322,6 +327,42 @@ def test_executor_multiple_queries_reuse_cache(
322327
assert schema_after_first is schema_after_second
323328

324329

330+
class TestExecutorFactories:
331+
"""Factory helpers configure schema loading strategies correctly."""
332+
333+
def test_create_executor_defaults_to_lazy(
334+
self, redis_client: redis.Redis, caching_indexes: list[str]
335+
):
336+
"""Default factory behavior should not preload schemas."""
337+
executor = create_executor(redis_client)
338+
339+
assert isinstance(executor, Executor)
340+
assert executor._schema_registry._schemas == {}
341+
342+
def test_create_executor_load_all_preloads_schemas(
343+
self, redis_client: redis.Redis, caching_indexes: list[str]
344+
):
345+
"""load_all strategy preserves eager schema loading."""
346+
executor = create_executor(redis_client, schema_cache_strategy="load_all")
347+
348+
assert set(executor._schema_registry._schemas) == set(caching_indexes)
349+
350+
def test_create_executor_reuses_registry(
351+
self, redis_client: redis.Redis, caching_indexes: list[str]
352+
):
353+
"""Factories should preserve a caller-provided registry instance."""
354+
registry = SchemaRegistry(redis_client)
355+
356+
executor = create_executor(
357+
redis_client,
358+
schema_registry=registry,
359+
schema_cache_strategy="load_all",
360+
)
361+
362+
assert executor._schema_registry is registry
363+
assert set(registry._schemas) == set(caching_indexes)
364+
365+
325366
# ---------------------------------------------------------------------------
326367
# Tests: Negative caching (sync)
327368
# ---------------------------------------------------------------------------
@@ -767,3 +808,48 @@ async def test_async_executor_multiple_queries_reuse_cache(
767808
assert result2.count == 1
768809
# Same dict object — was not re-fetched
769810
assert schema_after_first is schema_after_second
811+
812+
813+
class TestAsyncExecutorFactories:
814+
"""Async factory helpers configure schema loading strategies correctly."""
815+
816+
async def test_create_async_executor_defaults_to_lazy(
817+
self,
818+
async_caching_client: async_redis.Redis,
819+
async_caching_indexes: list[str],
820+
):
821+
"""Default async factory behavior should not preload schemas."""
822+
executor = await create_async_executor(async_caching_client)
823+
824+
assert isinstance(executor, AsyncExecutor)
825+
assert executor._schema_registry._schemas == {}
826+
827+
async def test_create_async_executor_load_all_preloads_schemas(
828+
self,
829+
async_caching_client: async_redis.Redis,
830+
async_caching_indexes: list[str],
831+
):
832+
"""Async load_all strategy preserves eager schema loading."""
833+
executor = await create_async_executor(
834+
async_caching_client,
835+
schema_cache_strategy="load_all",
836+
)
837+
838+
assert set(executor._schema_registry._schemas) == set(async_caching_indexes)
839+
840+
async def test_create_async_executor_reuses_registry(
841+
self,
842+
async_caching_client: async_redis.Redis,
843+
async_caching_indexes: list[str],
844+
):
845+
"""Async factories should preserve a caller-provided registry instance."""
846+
registry = AsyncSchemaRegistry(async_caching_client)
847+
848+
executor = await create_async_executor(
849+
async_caching_client,
850+
schema_registry=registry,
851+
schema_cache_strategy="load_all",
852+
)
853+
854+
assert executor._schema_registry is registry
855+
assert set(registry._schemas) == set(async_caching_indexes)

0 commit comments

Comments
 (0)