sql-redis 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sql_redis-0.1.0/.github/workflows/lint.yml +57 -0
- sql_redis-0.1.0/.github/workflows/release.yml +83 -0
- sql_redis-0.1.0/.github/workflows/test.yml +48 -0
- sql_redis-0.1.0/.gitignore +55 -0
- sql_redis-0.1.0/.pre-commit-config.yaml +17 -0
- sql_redis-0.1.0/Makefile +67 -0
- sql_redis-0.1.0/PKG-INFO +211 -0
- sql_redis-0.1.0/README.md +191 -0
- sql_redis-0.1.0/pyproject.toml +78 -0
- sql_redis-0.1.0/sql_redis/__init__.py +5 -0
- sql_redis-0.1.0/sql_redis/analyzer.py +133 -0
- sql_redis-0.1.0/sql_redis/executor.py +83 -0
- sql_redis-0.1.0/sql_redis/parser.py +440 -0
- sql_redis-0.1.0/sql_redis/query_builder.py +270 -0
- sql_redis-0.1.0/sql_redis/schema.py +142 -0
- sql_redis-0.1.0/sql_redis/translator.py +324 -0
- sql_redis-0.1.0/tests/__init__.py +1 -0
- sql_redis-0.1.0/tests/conftest.py +356 -0
- sql_redis-0.1.0/tests/test_analyzer.py +286 -0
- sql_redis-0.1.0/tests/test_executor.py +322 -0
- sql_redis-0.1.0/tests/test_query_builder.py +310 -0
- sql_redis-0.1.0/tests/test_redis_queries.py +354 -0
- sql_redis-0.1.0/tests/test_schema_registry.py +642 -0
- sql_redis-0.1.0/tests/test_sql_parser.py +746 -0
- sql_redis-0.1.0/tests/test_sql_queries.py +342 -0
- sql_redis-0.1.0/tests/test_translator.py +436 -0
- sql_redis-0.1.0/uv.lock +1266 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: Lint
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
|
|
9
|
+
env:
|
|
10
|
+
UV_VERSION: "0.7.13"
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
check:
|
|
14
|
+
name: Style-check ${{ matrix.python-version }}
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
matrix:
|
|
18
|
+
python-version:
|
|
19
|
+
- "3.9"
|
|
20
|
+
- "3.11"
|
|
21
|
+
- "3.13"
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- name: Check out repository
|
|
25
|
+
uses: actions/checkout@v6
|
|
26
|
+
|
|
27
|
+
- name: Install Python
|
|
28
|
+
uses: actions/setup-python@v6
|
|
29
|
+
with:
|
|
30
|
+
python-version: ${{ matrix.python-version }}
|
|
31
|
+
|
|
32
|
+
- name: Install uv
|
|
33
|
+
uses: astral-sh/setup-uv@v6
|
|
34
|
+
with:
|
|
35
|
+
version: ${{ env.UV_VERSION }}
|
|
36
|
+
enable-cache: true
|
|
37
|
+
python-version: ${{ matrix.python-version }} # sets UV_PYTHON
|
|
38
|
+
cache-dependency-glob: |
|
|
39
|
+
pyproject.toml
|
|
40
|
+
uv.lock
|
|
41
|
+
|
|
42
|
+
- name: Install dependencies
|
|
43
|
+
run: |
|
|
44
|
+
uv sync --frozen
|
|
45
|
+
|
|
46
|
+
- name: check-sort-import
|
|
47
|
+
run: |
|
|
48
|
+
make check-sort-imports
|
|
49
|
+
|
|
50
|
+
- name: check-black-format
|
|
51
|
+
run: |
|
|
52
|
+
make check-format
|
|
53
|
+
|
|
54
|
+
- name: check-mypy
|
|
55
|
+
run: |
|
|
56
|
+
make check-types
|
|
57
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
name: Publish Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
env:
|
|
8
|
+
PYTHON_VERSION: "3.11"
|
|
9
|
+
UV_VERSION: "0.7.13"
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- name: Check out repository
|
|
17
|
+
uses: actions/checkout@v6
|
|
18
|
+
|
|
19
|
+
- name: Install Python
|
|
20
|
+
uses: actions/setup-python@v6
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ env.PYTHON_VERSION }}
|
|
23
|
+
|
|
24
|
+
- name: Install uv
|
|
25
|
+
uses: astral-sh/setup-uv@v6
|
|
26
|
+
with:
|
|
27
|
+
version: ${{ env.UV_VERSION }}
|
|
28
|
+
enable-cache: true
|
|
29
|
+
python-version: ${{ env.PYTHON_VERSION }} # sets UV_PYTHON
|
|
30
|
+
cache-dependency-glob: |
|
|
31
|
+
pyproject.toml
|
|
32
|
+
uv.lock
|
|
33
|
+
|
|
34
|
+
- name: Install dependencies
|
|
35
|
+
run: |
|
|
36
|
+
uv sync --frozen
|
|
37
|
+
|
|
38
|
+
- name: Build package
|
|
39
|
+
run: uv build
|
|
40
|
+
|
|
41
|
+
- name: Upload build
|
|
42
|
+
uses: actions/upload-artifact@v4
|
|
43
|
+
with:
|
|
44
|
+
name: dist
|
|
45
|
+
path: dist/
|
|
46
|
+
|
|
47
|
+
publish:
|
|
48
|
+
needs: build
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
|
|
51
|
+
steps:
|
|
52
|
+
- name: Check out repository
|
|
53
|
+
uses: actions/checkout@v6
|
|
54
|
+
|
|
55
|
+
- name: Install Python
|
|
56
|
+
uses: actions/setup-python@v6
|
|
57
|
+
with:
|
|
58
|
+
python-version: ${{ env.PYTHON_VERSION }}
|
|
59
|
+
|
|
60
|
+
- name: Install uv
|
|
61
|
+
uses: astral-sh/setup-uv@v6
|
|
62
|
+
with:
|
|
63
|
+
version: ${{ env.UV_VERSION }}
|
|
64
|
+
enable-cache: true
|
|
65
|
+
python-version: ${{ env.PYTHON_VERSION }} # sets UV_PYTHON
|
|
66
|
+
cache-dependency-glob: |
|
|
67
|
+
pyproject.toml
|
|
68
|
+
uv.lock
|
|
69
|
+
|
|
70
|
+
- name: Install dependencies
|
|
71
|
+
run: |
|
|
72
|
+
uv sync --frozen
|
|
73
|
+
|
|
74
|
+
- name: Download build artifacts
|
|
75
|
+
uses: actions/download-artifact@v4
|
|
76
|
+
with:
|
|
77
|
+
name: dist
|
|
78
|
+
path: dist/
|
|
79
|
+
|
|
80
|
+
- name: Publish to PyPI
|
|
81
|
+
env:
|
|
82
|
+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI }}
|
|
83
|
+
run: uv publish
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Test Suite
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
env:
|
|
11
|
+
UV_VERSION: "0.7.13"
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
name: Python ${{ matrix.python-version }}
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- name: Check out repository
|
|
24
|
+
uses: actions/checkout@v6
|
|
25
|
+
|
|
26
|
+
- name: Install Python
|
|
27
|
+
uses: actions/setup-python@v6
|
|
28
|
+
with:
|
|
29
|
+
python-version: ${{ matrix.python-version }}
|
|
30
|
+
|
|
31
|
+
- name: Install uv
|
|
32
|
+
uses: astral-sh/setup-uv@v6
|
|
33
|
+
with:
|
|
34
|
+
version: ${{ env.UV_VERSION }}
|
|
35
|
+
enable-cache: true
|
|
36
|
+
python-version: ${{ matrix.python-version }} # sets UV_PYTHON
|
|
37
|
+
cache-dependency-glob: |
|
|
38
|
+
pyproject.toml
|
|
39
|
+
uv.lock
|
|
40
|
+
|
|
41
|
+
- name: Install dependencies
|
|
42
|
+
run: |
|
|
43
|
+
uv sync
|
|
44
|
+
|
|
45
|
+
- name: Run tests
|
|
46
|
+
run: |
|
|
47
|
+
make test
|
|
48
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
.python-version
|
|
23
|
+
|
|
24
|
+
# Virtual environments
|
|
25
|
+
.venv/
|
|
26
|
+
venv/
|
|
27
|
+
env/
|
|
28
|
+
ENV/
|
|
29
|
+
|
|
30
|
+
# Testing
|
|
31
|
+
.pytest_cache/
|
|
32
|
+
.coverage
|
|
33
|
+
.coverage.*
|
|
34
|
+
htmlcov/
|
|
35
|
+
.tox/
|
|
36
|
+
.nox/
|
|
37
|
+
|
|
38
|
+
# Type checking
|
|
39
|
+
.mypy_cache/
|
|
40
|
+
.pytype/
|
|
41
|
+
|
|
42
|
+
# IDEs
|
|
43
|
+
.idea/
|
|
44
|
+
.vscode/
|
|
45
|
+
*.swp
|
|
46
|
+
*.swo
|
|
47
|
+
*~
|
|
48
|
+
|
|
49
|
+
# OS
|
|
50
|
+
.DS_Store
|
|
51
|
+
Thumbs.db
|
|
52
|
+
|
|
53
|
+
# Project specific
|
|
54
|
+
.ai/
|
|
55
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: local
|
|
3
|
+
hooks:
|
|
4
|
+
- id: code-quality-checks
|
|
5
|
+
name: Run pre-commit checks (format, sort-imports, check-mypy)
|
|
6
|
+
entry: bash -c 'make format && make check-sort-imports && make check-types'
|
|
7
|
+
language: system
|
|
8
|
+
pass_filenames: false
|
|
9
|
+
- repo: https://github.com/codespell-project/codespell
|
|
10
|
+
rev: v2.2.6
|
|
11
|
+
hooks:
|
|
12
|
+
- id: codespell
|
|
13
|
+
name: Check spelling
|
|
14
|
+
args:
|
|
15
|
+
- --write-changes
|
|
16
|
+
- --skip=*.pyc,*.pyo,*.lock,*.git,*.mypy_cache,__pycache__,*.egg-info,.pytest_cache,env,venv,.venv
|
|
17
|
+
|
sql_redis-0.1.0/Makefile
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
.PHONY: install format lint test clean check-types check-format check-sort-imports sort-imports build help
|
|
2
|
+
.DEFAULT_GOAL := help
|
|
3
|
+
|
|
4
|
+
# Allow passing arguments to make targets (e.g., make test ARGS="...")
|
|
5
|
+
ARGS ?=
|
|
6
|
+
|
|
7
|
+
install: ## Install the project and all dependencies
|
|
8
|
+
@echo "🚀 Installing project dependencies with uv"
|
|
9
|
+
uv sync
|
|
10
|
+
|
|
11
|
+
format: ## Format code with isort and black
|
|
12
|
+
@echo "🎨 Formatting code"
|
|
13
|
+
uv run isort ./sql_redis ./tests/ --profile black
|
|
14
|
+
uv run black ./sql_redis ./tests/
|
|
15
|
+
|
|
16
|
+
check-format: ## Check code formatting
|
|
17
|
+
@echo "🔍 Checking code formatting"
|
|
18
|
+
uv run black --check ./sql_redis ./tests/
|
|
19
|
+
|
|
20
|
+
sort-imports: ## Sort imports with isort
|
|
21
|
+
@echo "📦 Sorting imports"
|
|
22
|
+
uv run isort ./sql_redis ./tests/ --profile black
|
|
23
|
+
|
|
24
|
+
check-sort-imports: ## Check import sorting
|
|
25
|
+
@echo "🔍 Checking import sorting"
|
|
26
|
+
uv run isort ./sql_redis ./tests/ --check-only --profile black
|
|
27
|
+
|
|
28
|
+
check-types: ## Run mypy type checking
|
|
29
|
+
@echo "🔍 Running mypy type checking"
|
|
30
|
+
uv run python -m mypy ./sql_redis
|
|
31
|
+
|
|
32
|
+
lint: format check-types ## Run all linting (format + type check)
|
|
33
|
+
|
|
34
|
+
test: ## Run tests (pass extra args with ARGS="...")
|
|
35
|
+
@echo "🧪 Running tests"
|
|
36
|
+
uv run python -m pytest $(ARGS)
|
|
37
|
+
|
|
38
|
+
test-verbose: ## Run tests with verbose output
|
|
39
|
+
@echo "🧪 Running tests (verbose)"
|
|
40
|
+
uv run python -m pytest -vv -s $(ARGS)
|
|
41
|
+
|
|
42
|
+
test-cov: ## Run tests with coverage report
|
|
43
|
+
@echo "🧪 Running tests with coverage"
|
|
44
|
+
uv run python -m pytest --cov=sql_redis --cov-report=term-missing --cov-report=html $(ARGS)
|
|
45
|
+
|
|
46
|
+
check: lint test ## Run all checks (lint + test)
|
|
47
|
+
|
|
48
|
+
build: ## Build wheel and source distribution
|
|
49
|
+
@echo "🏗️ Building distribution packages"
|
|
50
|
+
uv build
|
|
51
|
+
|
|
52
|
+
clean: ## Clean up build artifacts and caches
|
|
53
|
+
@echo "🧹 Cleaning up directory"
|
|
54
|
+
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
|
55
|
+
find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
|
|
56
|
+
find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true
|
|
57
|
+
find . -type d -name ".coverage" -delete 2>/dev/null || true
|
|
58
|
+
find . -type d -name "htmlcov" -exec rm -rf {} + 2>/dev/null || true
|
|
59
|
+
find . -type d -name "dist" -exec rm -rf {} + 2>/dev/null || true
|
|
60
|
+
find . -type d -name "build" -exec rm -rf {} + 2>/dev/null || true
|
|
61
|
+
find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true
|
|
62
|
+
find . -type f -name "*.log" -exec rm -rf {} + 2>/dev/null || true
|
|
63
|
+
|
|
64
|
+
help: ## Show this help message
|
|
65
|
+
@echo "Available commands:"
|
|
66
|
+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
|
67
|
+
|
sql_redis-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sql-redis
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SQL to Redis command translation utility
|
|
5
|
+
Project-URL: Homepage, https://github.com/redis/sql-redis
|
|
6
|
+
Project-URL: Repository, https://github.com/redis/sql-redis
|
|
7
|
+
Author-email: "Redis Inc." <applied.ai@redis.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: query-translation,redis,redis-client,sql
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Python: <3.14,>=3.9
|
|
17
|
+
Requires-Dist: redis>=5.0.0
|
|
18
|
+
Requires-Dist: sqlglot>=26.0.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# sql-redis
|
|
22
|
+
|
|
23
|
+
A proof-of-concept SQL-to-Redis translator that converts SQL SELECT statements into Redis `FT.SEARCH` and `FT.AGGREGATE` commands.
|
|
24
|
+
|
|
25
|
+
## Status
|
|
26
|
+
|
|
27
|
+
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.
|
|
28
|
+
|
|
29
|
+
## Quick Example
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from redis import Redis
|
|
33
|
+
from sql_redis import Translator
|
|
34
|
+
from sql_redis.schema import SchemaRegistry
|
|
35
|
+
from sql_redis.executor import Executor
|
|
36
|
+
|
|
37
|
+
client = Redis()
|
|
38
|
+
registry = SchemaRegistry(client)
|
|
39
|
+
registry.load_all() # Loads index schemas from Redis
|
|
40
|
+
|
|
41
|
+
executor = Executor(client, registry)
|
|
42
|
+
|
|
43
|
+
# Simple query
|
|
44
|
+
result = executor.execute("""
|
|
45
|
+
SELECT title, price
|
|
46
|
+
FROM products
|
|
47
|
+
WHERE category = 'electronics' AND price < 500
|
|
48
|
+
ORDER BY price ASC
|
|
49
|
+
LIMIT 10
|
|
50
|
+
""")
|
|
51
|
+
|
|
52
|
+
for row in result.rows:
|
|
53
|
+
print(row["title"], row["price"])
|
|
54
|
+
|
|
55
|
+
# Vector search with params
|
|
56
|
+
result = executor.execute("""
|
|
57
|
+
SELECT title, vector_distance(embedding, :vec) AS score
|
|
58
|
+
FROM products
|
|
59
|
+
LIMIT 5
|
|
60
|
+
""", params={"vec": vector_bytes})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Design Decisions
|
|
64
|
+
|
|
65
|
+
### Why SQL instead of a pandas-like Python DSL?
|
|
66
|
+
|
|
67
|
+
We considered several interface options:
|
|
68
|
+
|
|
69
|
+
| Approach | Example | Trade-offs |
|
|
70
|
+
|----------|---------|------------|
|
|
71
|
+
| **SQL** | `SELECT * FROM products WHERE price > 100` | Universal, well-understood, tooling exists |
|
|
72
|
+
| **Pandas-like** | `df[df.price > 100]` | Pythonic but limited to Python, no standard |
|
|
73
|
+
| **Builder pattern** | `query.select("*").where(price__gt=100)` | Type-safe but verbose, learning curve |
|
|
74
|
+
|
|
75
|
+
**We chose SQL because:**
|
|
76
|
+
|
|
77
|
+
1. **Universality** — SQL is the lingua franca of data. Developers, analysts, and tools all speak it.
|
|
78
|
+
2. **No new DSL to learn** — Users already know SQL. A pandas-like API requires learning our specific dialect.
|
|
79
|
+
3. **Tooling compatibility** — SQL strings can be generated by ORMs, query builders, or AI assistants.
|
|
80
|
+
4. **Clear mapping** — SQL semantics map reasonably well to RediSearch operations (SELECT→LOAD, WHERE→filter, GROUP BY→GROUPBY).
|
|
81
|
+
|
|
82
|
+
The downside is losing Python's type checking and IDE support, but for a query interface, the universality trade-off is worth it.
|
|
83
|
+
|
|
84
|
+
### Why sqlglot instead of writing a custom parser?
|
|
85
|
+
|
|
86
|
+
**Options considered:**
|
|
87
|
+
- **Custom parser** (regex, hand-rolled recursive descent)
|
|
88
|
+
- **PLY/Lark** (parser generators)
|
|
89
|
+
- **sqlglot** (production SQL parser)
|
|
90
|
+
- **sqlparse** (tokenizer, not a full parser)
|
|
91
|
+
|
|
92
|
+
**We chose sqlglot because:**
|
|
93
|
+
|
|
94
|
+
1. **Battle-tested** — Used in production by companies like Tobiko (SQLMesh). Handles edge cases we'd miss.
|
|
95
|
+
2. **Full AST** — Provides a complete abstract syntax tree, not just tokens. We can traverse and analyze queries properly.
|
|
96
|
+
3. **Dialect support** — Handles SQL variations. Users can write MySQL-style or PostgreSQL-style queries.
|
|
97
|
+
4. **Active maintenance** — Regular releases, responsive maintainers, good documentation.
|
|
98
|
+
|
|
99
|
+
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.
|
|
100
|
+
|
|
101
|
+
### Why schema-aware translation?
|
|
102
|
+
|
|
103
|
+
Redis field types determine query syntax:
|
|
104
|
+
|
|
105
|
+
| Field Type | Redis Syntax | Example |
|
|
106
|
+
|------------|--------------|---------|
|
|
107
|
+
| TEXT | `@field:term` | `@title:laptop` |
|
|
108
|
+
| NUMERIC | `@field:[min max]` | `@price:[100 500]` |
|
|
109
|
+
| TAG | `@field:{value}` | `@category:{books}` |
|
|
110
|
+
|
|
111
|
+
**Without schema knowledge**, we can't translate `category = 'books'` correctly — it could be `@category:books` (TEXT search) or `@category:{books}` (TAG exact match).
|
|
112
|
+
|
|
113
|
+
**Our approach:** The `SchemaRegistry` fetches index schemas via `FT.INFO` at startup. The translator uses this to generate correct syntax per field type.
|
|
114
|
+
|
|
115
|
+
This adds a Redis round-trip at initialization but ensures correct query generation.
|
|
116
|
+
|
|
117
|
+
### Architecture: Why this layered design?
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
SQL String
|
|
121
|
+
↓
|
|
122
|
+
┌─────────────────┐
|
|
123
|
+
│ SQLParser │ Parse SQL → ParsedQuery dataclass
|
|
124
|
+
└────────┬────────┘
|
|
125
|
+
↓
|
|
126
|
+
┌─────────────────┐
|
|
127
|
+
│ SchemaRegistry │ Load field types from Redis
|
|
128
|
+
└────────┬────────┘
|
|
129
|
+
↓
|
|
130
|
+
┌─────────────────┐
|
|
131
|
+
│ Analyzer │ Classify conditions by field type
|
|
132
|
+
└────────┬────────┘
|
|
133
|
+
↓
|
|
134
|
+
┌─────────────────┐
|
|
135
|
+
│ QueryBuilder │ Generate RediSearch syntax per type
|
|
136
|
+
└────────┬────────┘
|
|
137
|
+
↓
|
|
138
|
+
┌─────────────────┐
|
|
139
|
+
│ Translator │ Orchestrate pipeline, build command
|
|
140
|
+
└────────┬────────┘
|
|
141
|
+
↓
|
|
142
|
+
┌─────────────────┐
|
|
143
|
+
│ Executor │ Execute command, parse results
|
|
144
|
+
└────────┬────────┘
|
|
145
|
+
↓
|
|
146
|
+
QueryResult(rows, count)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Why separate components?**
|
|
150
|
+
|
|
151
|
+
1. **Testability** — Each layer has focused unit tests. 100% coverage is achievable because responsibilities are clear.
|
|
152
|
+
2. **Single responsibility** — Parser doesn't know about Redis. QueryBuilder doesn't know about SQL. Changes are localized.
|
|
153
|
+
3. **Extensibility** — Adding a new field type (e.g., GEO) means updating Analyzer and QueryBuilder, not rewriting everything.
|
|
154
|
+
|
|
155
|
+
**Why not a single monolithic translator?**
|
|
156
|
+
|
|
157
|
+
Early prototypes combined parsing and translation. This led to:
|
|
158
|
+
- Tests that required Redis connections for simple SQL parsing tests
|
|
159
|
+
- Difficulty testing edge cases in isolation
|
|
160
|
+
- Tangled code that was hard to modify
|
|
161
|
+
|
|
162
|
+
The layered approach emerged from TDD — writing tests first revealed natural boundaries.
|
|
163
|
+
|
|
164
|
+
## What's Implemented
|
|
165
|
+
|
|
166
|
+
- [x] Basic SELECT with field selection
|
|
167
|
+
- [x] WHERE with TEXT, NUMERIC, TAG field types
|
|
168
|
+
- [x] Comparison operators: `=`, `!=`, `<`, `<=`, `>`, `>=`, `BETWEEN`, `IN`
|
|
169
|
+
- [x] Boolean operators: `AND`, `OR`
|
|
170
|
+
- [x] Aggregations: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`
|
|
171
|
+
- [x] `GROUP BY` with multiple aggregations
|
|
172
|
+
- [x] `ORDER BY` with ASC/DESC
|
|
173
|
+
- [x] `LIMIT` and `OFFSET` pagination
|
|
174
|
+
- [x] Computed fields: `price * 0.9 AS discounted`
|
|
175
|
+
- [x] Vector KNN search: `vector_distance(field, :param)`
|
|
176
|
+
- [x] Hybrid search (filters + vector)
|
|
177
|
+
- [x] Full-text search: `LIKE 'prefix%'` (prefix), `fulltext(field, 'terms')` function
|
|
178
|
+
|
|
179
|
+
## What's Not Implemented (Yet...)
|
|
180
|
+
|
|
181
|
+
- [ ] JOINs (Redis doesn't support cross-index joins)
|
|
182
|
+
- [ ] Subqueries
|
|
183
|
+
- [ ] HAVING clause
|
|
184
|
+
- [ ] DISTINCT
|
|
185
|
+
- [ ] GEO field queries
|
|
186
|
+
- [ ] Index creation from SQL (CREATE INDEX)
|
|
187
|
+
|
|
188
|
+
## Development
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# Install dependencies
|
|
192
|
+
uv sync --all-extras
|
|
193
|
+
|
|
194
|
+
# Run tests (requires Docker for testcontainers)
|
|
195
|
+
uv run pytest
|
|
196
|
+
|
|
197
|
+
# Run with coverage
|
|
198
|
+
uv run pytest --cov=sql_redis --cov-report=html
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Testing Philosophy
|
|
202
|
+
|
|
203
|
+
This project uses strict TDD with 100% test coverage as a hard requirement. The approach:
|
|
204
|
+
|
|
205
|
+
1. **Write failing tests first** — Define expected behavior before implementation
|
|
206
|
+
2. **One test at a time** — Implement just enough to pass each test
|
|
207
|
+
3. **No untestable code** — If we can't test it, we don't write it
|
|
208
|
+
4. **Integration tests mirror raw Redis** — `test_sql_queries.py` verifies SQL produces same results as equivalent `FT.AGGREGATE` commands in `test_redis_queries.py`
|
|
209
|
+
|
|
210
|
+
Coverage is enforced in CI. Pragmas (`# pragma: no cover`) are forbidden — if code can't be tested, it shouldn't exist.
|
|
211
|
+
|