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.
@@ -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
+
@@ -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
+
@@ -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
+