SQL to Redis command translation utility
Project description
sql-redis
A proof-of-concept SQL-to-Redis translator that converts SQL SELECT statements into Redis FT.SEARCH and FT.AGGREGATE commands.
Status
This is an early POC demonstrating feasibility, not a production-ready library. The goal is to explore design decisions and validate the approach before committing to a full implementation.
Quick Example
from redis import Redis
from sql_redis import Translator
from sql_redis.schema import SchemaRegistry
from sql_redis.executor import Executor
client = Redis()
registry = SchemaRegistry(client)
registry.load_all() # Loads index schemas from Redis
executor = Executor(client, registry)
# Simple query
result = executor.execute("""
SELECT title, price
FROM products
WHERE category = 'electronics' AND price < 500
ORDER BY price ASC
LIMIT 10
""")
for row in result.rows:
print(row["title"], row["price"])
# Vector search with params
result = executor.execute("""
SELECT title, vector_distance(embedding, :vec) AS score
FROM products
LIMIT 5
""", params={"vec": vector_bytes})
Design Decisions
Why SQL instead of a pandas-like Python DSL?
We considered several interface options:
| Approach | Example | Trade-offs |
|---|---|---|
| SQL | SELECT * FROM products WHERE price > 100 |
Universal, well-understood, tooling exists |
| Pandas-like | df[df.price > 100] |
Pythonic but limited to Python, no standard |
| Builder pattern | query.select("*").where(price__gt=100) |
Type-safe but verbose, learning curve |
We chose SQL because:
- Universality — SQL is the lingua franca of data. Developers, analysts, and tools all speak it.
- No new DSL to learn — Users already know SQL. A pandas-like API requires learning our specific dialect.
- Tooling compatibility — SQL strings can be generated by ORMs, query builders, or AI assistants.
- Clear mapping — SQL semantics map reasonably well to RediSearch operations (SELECT→LOAD, WHERE→filter, GROUP BY→GROUPBY).
The downside is losing Python's type checking and IDE support, but for a query interface, the universality trade-off is worth it.
Why sqlglot instead of writing a custom parser?
Options considered:
- Custom parser (regex, hand-rolled recursive descent)
- PLY/Lark (parser generators)
- sqlglot (production SQL parser)
- sqlparse (tokenizer, not a full parser)
We chose sqlglot because:
- Battle-tested — Used in production by companies like Tobiko (SQLMesh). Handles edge cases we'd miss.
- Full AST — Provides a complete abstract syntax tree, not just tokens. We can traverse and analyze queries properly.
- Dialect support — Handles SQL variations. Users can write MySQL-style or PostgreSQL-style queries.
- Active maintenance — Regular releases, responsive maintainers, good documentation.
The alternative was writing a custom parser, which would be error-prone and time-consuming for a POC. sqlglot lets us focus on the translation logic rather than parsing edge cases.
Why schema-aware translation?
Redis field types determine query syntax:
| Field Type | Redis Syntax | Example |
|---|---|---|
| TEXT | @field:term |
@title:laptop |
| NUMERIC | @field:[min max] |
@price:[100 500] |
| TAG | @field:{value} |
@category:{books} |
Without schema knowledge, we can't translate category = 'books' correctly — it could be @category:books (TEXT search) or @category:{books} (TAG exact match).
Our approach: The SchemaRegistry fetches index schemas via FT.INFO at startup. The translator uses this to generate correct syntax per field type.
This adds a Redis round-trip at initialization but ensures correct query generation.
Architecture: Why this layered design?
SQL String
↓
┌─────────────────┐
│ SQLParser │ Parse SQL → ParsedQuery dataclass
└────────┬────────┘
↓
┌─────────────────┐
│ SchemaRegistry │ Load field types from Redis
└────────┬────────┘
↓
┌─────────────────┐
│ Analyzer │ Classify conditions by field type
└────────┬────────┘
↓
┌─────────────────┐
│ QueryBuilder │ Generate RediSearch syntax per type
└────────┬────────┘
↓
┌─────────────────┐
│ Translator │ Orchestrate pipeline, build command
└────────┬────────┘
↓
┌─────────────────┐
│ Executor │ Execute command, parse results
└────────┬────────┘
↓
QueryResult(rows, count)
Why separate components?
- Testability — Each layer has focused unit tests. 100% coverage is achievable because responsibilities are clear.
- Single responsibility — Parser doesn't know about Redis. QueryBuilder doesn't know about SQL. Changes are localized.
- Extensibility — Adding a new field type (e.g., GEO) means updating Analyzer and QueryBuilder, not rewriting everything.
Why not a single monolithic translator?
Early prototypes combined parsing and translation. This led to:
- Tests that required Redis connections for simple SQL parsing tests
- Difficulty testing edge cases in isolation
- Tangled code that was hard to modify
The layered approach emerged from TDD — writing tests first revealed natural boundaries.
What's Implemented
- Basic SELECT with field selection
- WHERE with TEXT, NUMERIC, TAG field types
- Comparison operators:
=,!=,<,<=,>,>=,BETWEEN,IN - Boolean operators:
AND,OR - Aggregations:
COUNT,SUM,AVG,MIN,MAX -
GROUP BYwith multiple aggregations -
ORDER BYwith ASC/DESC -
LIMITandOFFSETpagination - Computed fields:
price * 0.9 AS discounted - Vector KNN search:
vector_distance(field, :param) - Hybrid search (filters + vector)
- Full-text search:
LIKE 'prefix%'(prefix),fulltext(field, 'terms')function
What's Not Implemented (Yet...)
- JOINs (Redis doesn't support cross-index joins)
- Subqueries
- HAVING clause
- DISTINCT
- GEO field queries
- Index creation from SQL (CREATE INDEX)
Development
# Install dependencies
uv sync --all-extras
# Run tests (requires Docker for testcontainers)
uv run pytest
# Run with coverage
uv run pytest --cov=sql_redis --cov-report=html
Testing Philosophy
This project uses strict TDD with 100% test coverage as a hard requirement. The approach:
- Write failing tests first — Define expected behavior before implementation
- One test at a time — Implement just enough to pass each test
- No untestable code — If we can't test it, we don't write it
- Integration tests mirror raw Redis —
test_sql_queries.pyverifies SQL produces same results as equivalentFT.AGGREGATEcommands intest_redis_queries.py
Coverage is enforced in CI. Pragmas (# pragma: no cover) are forbidden — if code can't be tested, it shouldn't exist.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sql_redis-0.1.0.tar.gz.
File metadata
- Download URL: sql_redis-0.1.0.tar.gz
- Upload date:
- Size: 103.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
293700ea4dc80ffa2cce3f602c28ab179fa9a81f46007b686d02b8786306895d
|
|
| MD5 |
9c2d45dfcdd1565ac85d82bc6ae8556b
|
|
| BLAKE2b-256 |
53a7a024e535886e198b49258e9b78431fe7545ead54e94d9dc8bd4b5ea9fed1
|
File details
Details for the file sql_redis-0.1.0-py3-none-any.whl.
File metadata
- Download URL: sql_redis-0.1.0-py3-none-any.whl
- Upload date:
- Size: 17.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ed386d06965a597ed6ab6634f5a9e02093efbd9d9ea3543ce6ed166ce2ab5383
|
|
| MD5 |
d6bc9d107314ac3784f42c60b978b792
|
|
| BLAKE2b-256 |
4480c209fd20c1fbce6c02a1567a0a58c5832864876ab19037860a4108d7eba3
|