db-graphql-gateway 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.
- db_graphql_gateway-0.1.0/.github/workflows/ci.yml +32 -0
- db_graphql_gateway-0.1.0/.github/workflows/conformance.yml +33 -0
- db_graphql_gateway-0.1.0/.github/workflows/docs.yml +31 -0
- db_graphql_gateway-0.1.0/.github/workflows/integration.yml +30 -0
- db_graphql_gateway-0.1.0/.gitignore +57 -0
- db_graphql_gateway-0.1.0/.pre-commit-config.yaml +12 -0
- db_graphql_gateway-0.1.0/CONTRIBUTING_ADAPTER.md +94 -0
- db_graphql_gateway-0.1.0/LICENSE +21 -0
- db_graphql_gateway-0.1.0/Makefile +15 -0
- db_graphql_gateway-0.1.0/PKG-INFO +48 -0
- db_graphql_gateway-0.1.0/README.md +13 -0
- db_graphql_gateway-0.1.0/STATE.md +27 -0
- db_graphql_gateway-0.1.0/docs/ARCHITECTURE.md +229 -0
- db_graphql_gateway-0.1.0/docs/BENCHMARKS.md +38 -0
- db_graphql_gateway-0.1.0/docs/CHANGELOG.md +30 -0
- db_graphql_gateway-0.1.0/docs/CONTRIBUTING_ADAPTER.md +108 -0
- db_graphql_gateway-0.1.0/docs/FAQ.md +62 -0
- db_graphql_gateway-0.1.0/docs/SECURITY.md +74 -0
- db_graphql_gateway-0.1.0/docs/cli.md +127 -0
- db_graphql_gateway-0.1.0/docs/index.md +133 -0
- db_graphql_gateway-0.1.0/docs/quickstart.md +167 -0
- db_graphql_gateway-0.1.0/docs/stylesheets/custom.css +58 -0
- db_graphql_gateway-0.1.0/integration_tests/docker-compose.override.yml +14 -0
- db_graphql_gateway-0.1.0/integration_tests/docker-compose.yml +22 -0
- db_graphql_gateway-0.1.0/integration_tests/engine.py +65 -0
- db_graphql_gateway-0.1.0/integration_tests/level1_basic.py +61 -0
- db_graphql_gateway-0.1.0/integration_tests/level2_medium.py +99 -0
- db_graphql_gateway-0.1.0/integration_tests/level3_advanced.py +132 -0
- db_graphql_gateway-0.1.0/integration_tests/run_all.sh +33 -0
- db_graphql_gateway-0.1.0/integration_tests/run_integration.py +67 -0
- db_graphql_gateway-0.1.0/integration_tests/schema.sql +53 -0
- db_graphql_gateway-0.1.0/integration_tests/seed.py +102 -0
- db_graphql_gateway-0.1.0/integration_tests/server.py +101 -0
- db_graphql_gateway-0.1.0/integration_tests/sgql.yaml +25 -0
- db_graphql_gateway-0.1.0/mkdocs.yml +95 -0
- db_graphql_gateway-0.1.0/pyproject.toml +100 -0
- db_graphql_gateway-0.1.0/sgql.yaml +11 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/__init__.py +8 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/auth/__init__.py +10 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/auth/authorization.py +62 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/auth/interfaces.py +15 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/auth/jwt_provider.py +78 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/auth/middleware.py +10 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/cli/__init__.py +1 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/cli/main.py +241 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/__init__.py +1 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/__init__.py +1 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/base_compiler.py +285 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/interfaces.py +135 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/mysql/__init__.py +1 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/mysql/adapter.py +216 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/mysql/compiler.py +24 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/mysql/inspector.py +177 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/mysql/mapper.py +61 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/postgres/__init__.py +1 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/postgres/adapter.py +85 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/postgres/compiler.py +22 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/postgres/inspector.py +198 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/postgres/mapper.py +27 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/sqlite/__init__.py +1 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/sqlite/adapter.py +186 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/sqlite/compiler.py +26 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/sqlite/inspector.py +257 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/sqlite/mapper.py +38 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/models/__init__.py +1 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/models/schema.py +72 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/graphql/__init__.py +1 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/graphql/builder.py +588 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/graphql/dataloader.py +95 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/graphql/filter_builder.py +168 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/graphql/mutation_builder.py +49 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/graphql/pagination.py +41 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/graphql/query_planner.py +33 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/integrations/fastapi_integration.py +48 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/integrations/sqlalchemy_integration.py +58 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/py.typed +0 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/schema/__init__.py +1 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/schema/config.py +19 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/schema/ir/__init__.py +1 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/schema/ir/builder.py +160 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/schema/ir/models.py +76 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/security/complexity.py +60 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/security/error_masking.py +15 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/security/validation.py +101 -0
- db_graphql_gateway-0.1.0/src/db_graphql_gateway/version.py +1 -0
- db_graphql_gateway-0.1.0/tests/__init__.py +0 -0
- db_graphql_gateway-0.1.0/tests/conformance/conftest.py +212 -0
- db_graphql_gateway-0.1.0/tests/conformance/test_authorization.py +76 -0
- db_graphql_gateway-0.1.0/tests/conformance/test_mutations.py +85 -0
- db_graphql_gateway-0.1.0/tests/conformance/test_queries.py +56 -0
- db_graphql_gateway-0.1.0/tests/conformance/test_relationships.py +56 -0
- db_graphql_gateway-0.1.0/tests/integration/__init__.py +0 -0
- db_graphql_gateway-0.1.0/tests/integration/conftest.py +49 -0
- db_graphql_gateway-0.1.0/tests/integration/test_authorization.py +126 -0
- db_graphql_gateway-0.1.0/tests/integration/test_filtering_pagination.py +198 -0
- db_graphql_gateway-0.1.0/tests/integration/test_final_acceptance.py +148 -0
- db_graphql_gateway-0.1.0/tests/integration/test_graphql_execution.py +55 -0
- db_graphql_gateway-0.1.0/tests/integration/test_mutations.py +145 -0
- db_graphql_gateway-0.1.0/tests/integration/test_mysql_integration.py +281 -0
- db_graphql_gateway-0.1.0/tests/integration/test_postgres_introspection.py +47 -0
- db_graphql_gateway-0.1.0/tests/integration/test_query_planner.py +100 -0
- db_graphql_gateway-0.1.0/tests/integration/test_relationships_dataloader.py +131 -0
- db_graphql_gateway-0.1.0/tests/integration/test_sqlite_integration.py +300 -0
- db_graphql_gateway-0.1.0/tests/unit/__init__.py +0 -0
- db_graphql_gateway-0.1.0/tests/unit/test_authentication.py +204 -0
- db_graphql_gateway-0.1.0/tests/unit/test_base_compiler.py +343 -0
- db_graphql_gateway-0.1.0/tests/unit/test_cli.py +77 -0
- db_graphql_gateway-0.1.0/tests/unit/test_integrations.py +52 -0
- db_graphql_gateway-0.1.0/tests/unit/test_interfaces.py +11 -0
- db_graphql_gateway-0.1.0/tests/unit/test_ir_builder.py +143 -0
- db_graphql_gateway-0.1.0/tests/unit/test_mysql_compiler.py +228 -0
- db_graphql_gateway-0.1.0/tests/unit/test_security_hardening.py +180 -0
- db_graphql_gateway-0.1.0/tests/unit/test_sqlite_adapter.py +386 -0
- db_graphql_gateway-0.1.0/uv.lock +1940 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v5
|
|
17
|
+
with:
|
|
18
|
+
enable-cache: true
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version-file: "pyproject.toml"
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: uv sync --all-extras --dev
|
|
27
|
+
|
|
28
|
+
- name: Run pre-commit
|
|
29
|
+
run: uv run pre-commit run --all-files
|
|
30
|
+
|
|
31
|
+
- name: Run tests
|
|
32
|
+
run: uv run pytest tests/ -v
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Conformance Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ "main" ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ "main" ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
conformance:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
adapter: [postgres, mysql, sqlite]
|
|
15
|
+
fail-fast: false
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Install uv
|
|
21
|
+
uses: astral-sh/setup-uv@v2
|
|
22
|
+
with:
|
|
23
|
+
enable-cache: true
|
|
24
|
+
cache-dependency-glob: "pyproject.toml"
|
|
25
|
+
|
|
26
|
+
- name: Set up Python
|
|
27
|
+
run: uv python install 3.12
|
|
28
|
+
|
|
29
|
+
- name: Install Dependencies
|
|
30
|
+
run: uv sync --all-extras --dev
|
|
31
|
+
|
|
32
|
+
- name: Run Conformance Suite
|
|
33
|
+
run: uv run pytest tests/conformance/ -v -k "[${{ matrix.adapter }}]"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Deploy docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
deploy:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Configure Git Credentials
|
|
18
|
+
run: |
|
|
19
|
+
git config user.name github-actions[bot]
|
|
20
|
+
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
|
21
|
+
|
|
22
|
+
- name: Setup Python
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: '3.12'
|
|
26
|
+
|
|
27
|
+
- name: Install MkDocs Material
|
|
28
|
+
run: pip install mkdocs-material
|
|
29
|
+
|
|
30
|
+
- name: Deploy Docs to GitHub Pages
|
|
31
|
+
run: mkdocs gh-deploy --force
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Integration Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
integration-test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v5
|
|
17
|
+
with:
|
|
18
|
+
enable-cache: true
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version-file: "pyproject.toml"
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: uv sync --all-extras --dev
|
|
27
|
+
|
|
28
|
+
- name: Run Integration Tests
|
|
29
|
+
run: |
|
|
30
|
+
./integration_tests/run_all.sh
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# Virtual environments
|
|
30
|
+
venv/
|
|
31
|
+
.venv/
|
|
32
|
+
env/
|
|
33
|
+
.env/
|
|
34
|
+
ENV/
|
|
35
|
+
|
|
36
|
+
# IDEs
|
|
37
|
+
.idea/
|
|
38
|
+
.vscode/
|
|
39
|
+
*.swp
|
|
40
|
+
|
|
41
|
+
# Testing and Linting
|
|
42
|
+
.mypy_cache/
|
|
43
|
+
.ruff_cache/
|
|
44
|
+
.pytest_cache/
|
|
45
|
+
.coverage
|
|
46
|
+
htmlcov/
|
|
47
|
+
.tox/
|
|
48
|
+
|
|
49
|
+
# MkDocs
|
|
50
|
+
site*
|
|
51
|
+
|
|
52
|
+
# Environment Variables
|
|
53
|
+
.env
|
|
54
|
+
|
|
55
|
+
# OS generated files
|
|
56
|
+
.DS_Store
|
|
57
|
+
Thumbs.db
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.4.4
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff
|
|
6
|
+
args: [ --fix ]
|
|
7
|
+
- id: ruff-format
|
|
8
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
9
|
+
rev: v1.10.0
|
|
10
|
+
hooks:
|
|
11
|
+
- id: mypy
|
|
12
|
+
additional_dependencies: ["types-PyYAML", "strawberry-graphql", "PyJWT", "testcontainers"]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Contributing a New Database Adapter
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in expanding the database support of `db-graphql-gateway`! Our goal is to maintain a truly database-agnostic GraphQL interface. This means that users should be able to swap adapters without modifying their GraphQL schemas, authorization policies, or application code.
|
|
4
|
+
|
|
5
|
+
This guide explains the `DatabaseAdapter` protocol, how to build a new adapter, and how to prove it works using the **Cross-Adapter Conformance Suite**.
|
|
6
|
+
|
|
7
|
+
## The `DatabaseAdapter` Protocol
|
|
8
|
+
|
|
9
|
+
To integrate a new database engine (e.g., SQL Server, DuckDB, CockroachDB), you must implement the `DatabaseAdapter` interface (found in `src/db_graphql_gateway/database/adapters/interfaces.py`).
|
|
10
|
+
|
|
11
|
+
The adapter acts as a bridge between the engine-agnostic core (`GraphQLSchemaBuilder`, `AuthorizationEngine`) and the underlying database dialect.
|
|
12
|
+
|
|
13
|
+
### Key Components
|
|
14
|
+
|
|
15
|
+
1. **`DatabaseAdapter`**: Manages the connection pool, executes queries, and returns raw data.
|
|
16
|
+
2. **`SchemaInspector`**: Introspects the database to dynamically generate the canonical schema.
|
|
17
|
+
3. **`TypeMapper`**: Translates dialect-specific types (e.g., MySQL's `TINYINT(1)`) into abstract GraphQL IR types (`GraphQLType.BOOLEAN`).
|
|
18
|
+
4. **`QueryCompiler`**: Usually inherits from `BaseQueryCompiler` and overrides dialect-specific AST-to-SQL logic.
|
|
19
|
+
|
|
20
|
+
### Capability Flags
|
|
21
|
+
|
|
22
|
+
When configuring your `QueryCompiler` or `Adapter`, you may need to define capability flags that instruct the core gateway on how to handle the dialect's quirks:
|
|
23
|
+
|
|
24
|
+
- `supports_returning` (bool): Set to `True` if your engine supports `RETURNING` clauses (like Postgres). If `False` (like SQLite/MySQL), the gateway will automatically use a "SELECT-after-write" pattern.
|
|
25
|
+
- `fetch_after_write` (bool): Flag emitted in the `MutationPlan` for engines lacking `RETURNING` to trigger the secondary select.
|
|
26
|
+
- `placeholder_style` (enum/string): e.g., `?` for SQLite, `%s` for MySQL, `$1` for Postgres. Ensures parameterized queries match the underlying driver's expectations.
|
|
27
|
+
|
|
28
|
+
## Building a New Adapter: Worked Example (SQLite)
|
|
29
|
+
|
|
30
|
+
Let's look at how SQLite was implemented.
|
|
31
|
+
|
|
32
|
+
### 1. Connection & Execution
|
|
33
|
+
```python
|
|
34
|
+
class SQLiteAdapter(DatabaseAdapter):
|
|
35
|
+
async def execute_query(self, query: str, params: list[Any]) -> list[dict[str, Any]]:
|
|
36
|
+
async with self.pool.acquire() as conn:
|
|
37
|
+
cursor = await conn.execute(query, params)
|
|
38
|
+
rows = await cursor.fetchall()
|
|
39
|
+
return [dict(row) for row in rows]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Query Compilation
|
|
43
|
+
Inherit from `BaseQueryCompiler` to get 90% of ANSI SQL behavior for free.
|
|
44
|
+
```python
|
|
45
|
+
class SQLiteQueryCompiler(BaseQueryCompiler):
|
|
46
|
+
def __init__(self):
|
|
47
|
+
super().__init__()
|
|
48
|
+
self.placeholder = "?"
|
|
49
|
+
self.supports_returning = False
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 3. Engine Gotchas (Checklist)
|
|
53
|
+
|
|
54
|
+
Before building, verify the following against your driver:
|
|
55
|
+
- [ ] **Placeholder Style**: Does the driver use `?`, `%s`, `:name`, or `$1`?
|
|
56
|
+
- [ ] **Identifier Quoting**: Does it use `""` (Postgres/SQLite) or `\`\`` (MySQL)? Override `quote_identifier()` in your compiler.
|
|
57
|
+
- [ ] **Boolean Types**: Does the engine lack a native `BOOLEAN`? Ensure your `TypeMapper` intercepts it (e.g., `TINYINT(1) -> BOOLEAN`).
|
|
58
|
+
- [ ] **Upsert Syntax**: Does it use `ON CONFLICT` or `ON DUPLICATE KEY UPDATE`?
|
|
59
|
+
|
|
60
|
+
### Explicit Non-Goals
|
|
61
|
+
The `DatabaseAdapter` protocol does **not** need to support highly proprietary, engine-specific extensions (e.g., Postgres PostGIS, Oracle XMLType) if there is no cross-engine equivalent. Expose these via adapter-level opt-in configuration, not by muddying the core protocol.
|
|
62
|
+
|
|
63
|
+
## Passing the Conformance Suite
|
|
64
|
+
|
|
65
|
+
Your PR **will not be merged** unless it passes the Conformance Suite. This suite runs identical GraphQL queries against all adapters to guarantee 100% behavioral parity (including pagination, relationships, and authorization pushdowns).
|
|
66
|
+
|
|
67
|
+
### How to hook into the Conformance Suite:
|
|
68
|
+
|
|
69
|
+
1. Open `tests/conformance/conftest.py`.
|
|
70
|
+
2. Add your engine to the parameter matrix:
|
|
71
|
+
```python
|
|
72
|
+
@pytest_asyncio.fixture(params=["sqlite", "mysql", "postgres", "your_engine"])
|
|
73
|
+
```
|
|
74
|
+
3. Update the `db_adapter` fixture to provision your engine (use `testcontainers` if it requires a daemon). The fixture must use the generic `get_ddl("your_engine")` string to instantiate the canonical schema:
|
|
75
|
+
```python
|
|
76
|
+
elif engine == "your_engine":
|
|
77
|
+
# Setup your engine here
|
|
78
|
+
for stmt in get_ddl("your_engine"):
|
|
79
|
+
await conn.execute(stmt)
|
|
80
|
+
```
|
|
81
|
+
4. Run the suite: `uv run pytest tests/conformance/ -k "db_adapter[your_engine]"`
|
|
82
|
+
|
|
83
|
+
If your compiler and type mapper are correct, all tests will pass without writing a single line of test code!
|
|
84
|
+
|
|
85
|
+
## Entry Points & Packaging
|
|
86
|
+
|
|
87
|
+
To keep the core package lightweight, third-party adapters should be published as separate packages (e.g., `db-graphql-gateway-duckdb`).
|
|
88
|
+
|
|
89
|
+
Register your adapter in your `pyproject.toml` so the gateway can auto-discover it:
|
|
90
|
+
```toml
|
|
91
|
+
[project.entry-points."db_graphql_gateway.adapters"]
|
|
92
|
+
duckdb = "my_duckdb_package.adapter:DuckDBAdapter"
|
|
93
|
+
```
|
|
94
|
+
Users will then configure the gateway with `engine="duckdb"` and the plugin system will handle the rest.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Author
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.PHONY: install lint typecheck test
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
uv venv
|
|
5
|
+
uv pip install -e ".[dev,postgres]"
|
|
6
|
+
|
|
7
|
+
lint:
|
|
8
|
+
uv run ruff check src/ tests/
|
|
9
|
+
uv run ruff format --check src/ tests/
|
|
10
|
+
|
|
11
|
+
typecheck:
|
|
12
|
+
uv run mypy src/ tests/
|
|
13
|
+
|
|
14
|
+
test:
|
|
15
|
+
uv run pytest
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: db-graphql-gateway
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Automatically generate a secure, production-ready GraphQL API from a database connection.
|
|
5
|
+
Author-email: Mukesh M Lohar <mukesh1lohar@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Requires-Dist: click>=8.1.0
|
|
14
|
+
Requires-Dist: pydantic>=2.0.0
|
|
15
|
+
Requires-Dist: pyjwt[crypto]>=2.8.0
|
|
16
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
17
|
+
Requires-Dist: strawberry-graphql>=0.200.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
20
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
24
|
+
Requires-Dist: testcontainers[postgres]>=4.4.1; extra == 'dev'
|
|
25
|
+
Provides-Extra: fastapi
|
|
26
|
+
Requires-Dist: fastapi>=0.141.1; extra == 'fastapi'
|
|
27
|
+
Requires-Dist: httpx>=0.28.1; extra == 'fastapi'
|
|
28
|
+
Provides-Extra: mysql
|
|
29
|
+
Requires-Dist: asyncmy>=0.2.14; extra == 'mysql'
|
|
30
|
+
Provides-Extra: postgres
|
|
31
|
+
Requires-Dist: asyncpg>=0.28.0; extra == 'postgres'
|
|
32
|
+
Provides-Extra: sqlite
|
|
33
|
+
Requires-Dist: aiosqlite>=0.22.1; extra == 'sqlite'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# db-graphql-gateway
|
|
37
|
+
|
|
38
|
+
Automatically generate a secure, production-ready GraphQL API from a database connection.
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
- **Schema Discovery:** Automatically inspects your database schema and generates corresponding GraphQL types.
|
|
42
|
+
- **Relationships:** Resolves foreign keys to automatically wire up GraphQL associations.
|
|
43
|
+
- **Pagination:** Built-in Relay-style cursor pagination.
|
|
44
|
+
- **Authorization & Security:** Configurable JWT/OIDC support and query complexity/depth protection.
|
|
45
|
+
- **Performance:** Built-in DataLoader support to protect against N+1 query problems.
|
|
46
|
+
|
|
47
|
+
## Getting Started
|
|
48
|
+
See documentation for full setup instructions.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# db-graphql-gateway
|
|
2
|
+
|
|
3
|
+
Automatically generate a secure, production-ready GraphQL API from a database connection.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- **Schema Discovery:** Automatically inspects your database schema and generates corresponding GraphQL types.
|
|
7
|
+
- **Relationships:** Resolves foreign keys to automatically wire up GraphQL associations.
|
|
8
|
+
- **Pagination:** Built-in Relay-style cursor pagination.
|
|
9
|
+
- **Authorization & Security:** Configurable JWT/OIDC support and query complexity/depth protection.
|
|
10
|
+
- **Performance:** Built-in DataLoader support to protect against N+1 query problems.
|
|
11
|
+
|
|
12
|
+
## Getting Started
|
|
13
|
+
See documentation for full setup instructions.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# STATE: db-graphql-gateway
|
|
2
|
+
|
|
3
|
+
## Current Status
|
|
4
|
+
- Phase 1 (Foundation) has been successfully completed.
|
|
5
|
+
- Phase 2 (PostgreSQL Introspection) has been successfully completed.
|
|
6
|
+
- Phase 3 (Schema IR) has been successfully completed.
|
|
7
|
+
- Phase 4 (Basic GraphQL Generation) has been successfully completed.
|
|
8
|
+
- Phase 5 (Filter, Sort, Pagination) has been successfully completed.
|
|
9
|
+
- Phase 6 (Relationships & DataLoader) has been successfully completed.
|
|
10
|
+
- Phase 7 (Query Planner) has been successfully completed.
|
|
11
|
+
- Phase 8 (Authentication) has been successfully completed.
|
|
12
|
+
- Phase 9 (Authorization) has been successfully completed.
|
|
13
|
+
- Phase 10 (Security Hardening) has been successfully completed.
|
|
14
|
+
- Phase 11 (Mutations) has been successfully completed.
|
|
15
|
+
- Phase 12 (FastAPI/ORM Integrations) has been successfully completed.
|
|
16
|
+
- Phase 13 (CLI Tooling) has been successfully completed.
|
|
17
|
+
- [x] Phase 14 (Final Testing & Documentation) has been successfully completed.
|
|
18
|
+
- Developed end-to-end acceptance tests.
|
|
19
|
+
- Written extensive MkDocs documentation covering `ARCHITECTURE.md`, `SECURITY.md`, `BENCHMARKS.md`, and more.
|
|
20
|
+
- Successfully addressed strict typing with `pre-commit --all-files`.
|
|
21
|
+
|
|
22
|
+
## Next Actions
|
|
23
|
+
- Project is effectively complete! All functionality, tests, and documentation are verified.
|
|
24
|
+
- Built a Docker-based integration test workflow with strict security and type checks, which runs 9 advanced test levels natively asserting against real postgres.
|
|
25
|
+
|
|
26
|
+
## Issues / Blockers
|
|
27
|
+
- None.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Architecture: db-graphql-gateway
|
|
2
|
+
|
|
3
|
+
The `db-graphql-gateway` is designed with a strict separation of concerns, decoupling the database
|
|
4
|
+
inspection and query compilation from the GraphQL presentation layer and authentication logic.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. Core Execution Pipeline
|
|
9
|
+
|
|
10
|
+
The life cycle of a GraphQL query from the HTTP request down to the database engine follows this
|
|
11
|
+
strict pipeline:
|
|
12
|
+
|
|
13
|
+
```mermaid
|
|
14
|
+
flowchart TB
|
|
15
|
+
classDef client fill:#e0f7fa,stroke:#006064,stroke-width:2px,color:#006064
|
|
16
|
+
classDef security fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#e65100
|
|
17
|
+
classDef core fill:#e8eaf6,stroke:#1a237e,stroke-width:2px,color:#1a237e
|
|
18
|
+
classDef db fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px,color:#1b5e20
|
|
19
|
+
|
|
20
|
+
Client([GraphQL Client]):::client -->|HTTP POST| Auth(Authentication Provider):::security
|
|
21
|
+
|
|
22
|
+
subgraph "GraphQL Gateway Pipeline"
|
|
23
|
+
Auth -->|Verified JWT| Context(Auth Context)
|
|
24
|
+
Context --> Resolver(Strawberry Resolver):::core
|
|
25
|
+
|
|
26
|
+
subgraph "Execution & Planning"
|
|
27
|
+
Resolver --> Limits{AST Limits}:::security
|
|
28
|
+
Limits --> Planner(Query Planner):::core
|
|
29
|
+
Planner --> DataLoader(DataLoader Registry):::core
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
subgraph "Security & Database Abstraction"
|
|
33
|
+
DataLoader --> AuthZ(Authorization Engine):::security
|
|
34
|
+
AuthZ --> |Inject SQL Policies| AdapterProtocol[[DatabaseAdapter Protocol]]:::core
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
AdapterProtocol --> PG[(PostgreSQL)]:::db
|
|
39
|
+
AdapterProtocol --> SQ[(SQLite)]:::db
|
|
40
|
+
AdapterProtocol --> MY[(MySQL/MariaDB)]:::db
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 2. Package Boundaries
|
|
46
|
+
|
|
47
|
+
The codebase is strictly separated into four functional areas:
|
|
48
|
+
|
|
49
|
+
<div class="grid cards" markdown>
|
|
50
|
+
|
|
51
|
+
- :material-database: **Database Abstraction**
|
|
52
|
+
---
|
|
53
|
+
Located in `database/`. Handles adapters (`PostgresAdapter`, `SQLiteAdapter`,
|
|
54
|
+
`MySQLAdapter`), schema inspection, normalised models, and dialect-parameterised
|
|
55
|
+
query compilers. **No dialect-specific logic leaks above this layer.**
|
|
56
|
+
|
|
57
|
+
- :material-transit-connection-variant: **Schema Intermediate Representation**
|
|
58
|
+
---
|
|
59
|
+
Located in `schema/ir/`. The source of truth mapping database structure to
|
|
60
|
+
GraphQL schemas. It is database-agnostic and GraphQL-agnostic.
|
|
61
|
+
|
|
62
|
+
- :material-graphql: **GraphQL Generation**
|
|
63
|
+
---
|
|
64
|
+
Located in `graphql/`. Converts the IR into a Strawberry GraphQL schema (types,
|
|
65
|
+
queries, mutations) and handles execution, filtering, sorting, pagination.
|
|
66
|
+
Talks only to the IR and the `DatabaseAdapter` protocol — never to a dialect.
|
|
67
|
+
|
|
68
|
+
- :material-security: **Security & Auth**
|
|
69
|
+
---
|
|
70
|
+
Located in `auth/` and `security/`. Validates callers (e.g., JWT), evaluates
|
|
71
|
+
contextual policies to generate SQL predicate trees (`FilterCondition` /
|
|
72
|
+
`FilterGroup`), and enforces query complexity budgets and AST limits. Produces
|
|
73
|
+
dialect-neutral predicate objects; placeholder style is owned by the compiler.
|
|
74
|
+
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 3. Multi-Dialect Adapter Architecture
|
|
80
|
+
|
|
81
|
+
### 3.1 `DatabaseAdapter` Protocol
|
|
82
|
+
|
|
83
|
+
Every adapter implements the same protocol from `database/adapters/interfaces.py`:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
class DatabaseAdapter(Protocol):
|
|
87
|
+
# ── Dialect capability flags ──────────────────────────────────────────
|
|
88
|
+
supports_returning: bool # RETURNING clause supported after DML
|
|
89
|
+
supports_upsert_on_conflict: bool # ON CONFLICT DO UPDATE / ON DUPLICATE KEY
|
|
90
|
+
placeholder_style: PlaceholderStyle # "numbered" | "qmark" | "named"
|
|
91
|
+
identifier_quote_char: str # '"' | '`' | '['
|
|
92
|
+
|
|
93
|
+
async def connect(self) -> None: ...
|
|
94
|
+
async def close(self) -> None: ...
|
|
95
|
+
async def execute(self, query: CompiledQuery) -> QueryResult: ...
|
|
96
|
+
async def execute_many(self, queries: list[CompiledQuery]) -> list[QueryResult]: ...
|
|
97
|
+
def inspector(self) -> SchemaInspector: ...
|
|
98
|
+
def compiler(self) -> QueryCompiler: ...
|
|
99
|
+
def type_mapper(self) -> TypeMapper: ...
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 3.2 `BaseQueryCompiler`
|
|
103
|
+
|
|
104
|
+
All SELECT / DML SQL generation lives in `database/adapters/base_compiler.py`.
|
|
105
|
+
Dialect subclasses only set three class-level attributes:
|
|
106
|
+
|
|
107
|
+
| Subclass | `placeholder_style` | `identifier_quote_char` | `supports_returning` |
|
|
108
|
+
|---|---|---|---|
|
|
109
|
+
| `PostgresQueryCompiler` | `"numbered"` (`$1, $2, …`) | `"` | `True` |
|
|
110
|
+
| `SQLiteQueryCompiler` | `"qmark"` (`?, ?, …`) | `"` | runtime-detected (≥ 3.35.0) |
|
|
111
|
+
| `MySQLQueryCompiler` | `"qmark"` (`?, ?, …`) | `` ` `` | `False` |
|
|
112
|
+
|
|
113
|
+
When `supports_returning = False`, `compile_mutation()` sets
|
|
114
|
+
`CompiledQuery.fetch_after_write = True` and the adapter's `execute()` issues a
|
|
115
|
+
follow-up `SELECT` using `cursor.lastrowid` (INSERT) or the known PK value
|
|
116
|
+
(UPDATE / DELETE). This pattern is called **SELECT-after-write**.
|
|
117
|
+
|
|
118
|
+
### 3.3 Introspection
|
|
119
|
+
|
|
120
|
+
Each adapter ships its own `SchemaInspector` that reads dialect-specific metadata:
|
|
121
|
+
|
|
122
|
+
| Adapter | Introspection source |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `PostgresSchemaInspector` | `pg_class`, `pg_attribute`, `pg_constraint`, `pg_enum` |
|
|
125
|
+
| `SQLiteSchemaInspector` | `PRAGMA table_info`, `PRAGMA foreign_key_list`, `sqlite_master` |
|
|
126
|
+
| `MySQLSchemaInspector` | `information_schema.columns`, `information_schema.key_column_usage` |
|
|
127
|
+
|
|
128
|
+
All three produce the **same** `DatabaseSchema → Table → Column` data model.
|
|
129
|
+
`IRBuilder.build()` is called identically regardless of which inspector ran.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 4. Schema Intermediate Representation (IR)
|
|
134
|
+
|
|
135
|
+
The Gateway decouples the database schema from the GraphQL schema using the IR
|
|
136
|
+
(`GraphQLTypeIR`, `GraphQLFieldIR`).
|
|
137
|
+
|
|
138
|
+
1. **Introspection (`inspector.py`)**: Reads dialect-specific system catalogs to
|
|
139
|
+
construct a `DatabaseSchema` with normalised `Column.type` strings.
|
|
140
|
+
2. **Type mapping (`TypeMapper`)**: Each adapter's `TypeMapper` converts
|
|
141
|
+
normalised column types to abstract GraphQL scalar names (`"Int"`, `"Float"`,
|
|
142
|
+
`"String"`, `"DateTime"`, `"Boolean"`, `"JSON"`). No raw DB type strings leak
|
|
143
|
+
past the `TypeMapper`.
|
|
144
|
+
3. **IR Build (`IRBuilder`)**: Converts the low-level schema into GraphQL-centric
|
|
145
|
+
constructs. During this phase, `GatewayConfig` overrides are applied.
|
|
146
|
+
**Sensitive fields** (e.g., `password`, `token`) are automatically redacted here
|
|
147
|
+
based on name-pattern matching — dialect-agnostically.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 5. GraphQL Schema Generation (Build-Time)
|
|
152
|
+
|
|
153
|
+
The `GraphQLSchemaBuilder` transforms the IR into a Strawberry GraphQL schema
|
|
154
|
+
dynamically using Python's `type` function. This happens exactly **once at startup**.
|
|
155
|
+
|
|
156
|
+
### Dynamic Annotations & Mypy
|
|
157
|
+
Because Strawberry relies heavily on Python's type hints (`__annotations__`) to
|
|
158
|
+
build the static GraphQL schema, the Builder must dynamically construct these
|
|
159
|
+
dictionaries for every resolver it generates.
|
|
160
|
+
|
|
161
|
+
```python title="Injecting annotations dynamically to satisfy Strawberry"
|
|
162
|
+
update_fn.__annotations__ = {
|
|
163
|
+
"info": Info,
|
|
164
|
+
"id": pk_type,
|
|
165
|
+
"input": update_input_type,
|
|
166
|
+
"expected_version": Optional[int],
|
|
167
|
+
"return": Optional[sb_type],
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
!!! note "Strict Typing"
|
|
172
|
+
This architecture cleanly separates the dynamic runtime from static analysis,
|
|
173
|
+
allowing the core engine to pass strict `mypy` checks.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 6. Execution & DataLoading (Per-Request)
|
|
178
|
+
|
|
179
|
+
When a resolver executes, it does not use an ORM. It builds a `QueryPlan` that is
|
|
180
|
+
compiled directly into SQL via the adapter's `QueryCompiler`.
|
|
181
|
+
|
|
182
|
+
### N+1 Query Elimination
|
|
183
|
+
Nested relationship fields use Strawberry DataLoaders that batch foreign keys at
|
|
184
|
+
execution time. The query compiler merges these batched IDs (e.g.,
|
|
185
|
+
`WHERE author_id IN ($1, $2)` or `WHERE author_id IN (?, ?)`) ensuring constant
|
|
186
|
+
$O(1)$ database execution regardless of the depth of the graph.
|
|
187
|
+
|
|
188
|
+
The `DataLoaderRegistry` receives a `schema_map` dict (`type_name → schema_name`)
|
|
189
|
+
from `GraphQLSchemaBuilder`, so relationship batch queries use the correct schema
|
|
190
|
+
name for each adapter (e.g., `"public"` for Postgres, `"main"` for SQLite).
|
|
191
|
+
|
|
192
|
+
### Optimistic Concurrency & Soft Deletes
|
|
193
|
+
The `IRBuilder` automatically detects `version` and `deleted_at` columns.
|
|
194
|
+
|
|
195
|
+
- **Soft Deletes**: List queries automatically append `deleted_at IS NULL` filters,
|
|
196
|
+
and `delete_` mutations are converted into `update_` operations that set the
|
|
197
|
+
deletion timestamp.
|
|
198
|
+
- **Optimistic Locking**: Mutations include an `expected_version` argument. If
|
|
199
|
+
provided, the update query asserts `version = <expected>` and increments it,
|
|
200
|
+
failing if another transaction modified the row concurrently.
|
|
201
|
+
|
|
202
|
+
Both features are detected from **IR field names** (`f.name == "deleted_at"`,
|
|
203
|
+
`f.name == "version"`) — never from raw schema or adapter code.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 7. Authorization Predicates
|
|
208
|
+
|
|
209
|
+
!!! danger "Zero Memory Filtering"
|
|
210
|
+
The Gateway **never** filters sensitive data in Python memory. All authorization
|
|
211
|
+
policies are pushed down to the database engine as SQL `WHERE` predicates,
|
|
212
|
+
regardless of which adapter is in use.
|
|
213
|
+
|
|
214
|
+
The `AuthorizationEngine` evaluates contextual policies and generates
|
|
215
|
+
`FilterCondition` / `FilterGroup` predicate trees. When the `QueryCompiler` builds
|
|
216
|
+
the final SQL string, it merges the basic user `WHERE` filters with the
|
|
217
|
+
authorization predicates.
|
|
218
|
+
|
|
219
|
+
The placeholder style (e.g., `$1` for Postgres, `?` for SQLite/MySQL) is owned
|
|
220
|
+
**exclusively by the compiler** — the auth engine produces dialect-neutral predicate
|
|
221
|
+
objects and has no knowledge of the underlying database engine.
|
|
222
|
+
|
|
223
|
+
For example, a policy defining `owner_id = $user_id` on the `tasks` table will
|
|
224
|
+
statically inject:
|
|
225
|
+
|
|
226
|
+
- `AND "owner_id" = $1` in Postgres SQL
|
|
227
|
+
- `AND "owner_id" = ?` in SQLite / MySQL SQL
|
|
228
|
+
|
|
229
|
+
In all cases, unauthorized rows are **never loaded into memory**.
|