Type: Reverse-engineered Status: Draft Last synced with code: 2026-04-10 Hexagonal scope: Domain + Infrastructure + Presentation Parent module: ../spec.md Related plan: ./plan.md
Read-only SQL executor over the cache_* tables populated by the connectors. Enforces safety via 3-layer validation (regex forbidden keywords, allowlist over SELECT/WITH on cache_* tables, sqlglot AST parsing) and runs queries in a ThreadPoolExecutor(max_workers=2) with a DB-level statement_timeout. Exposes raw-SQL HTTP endpoints. Consumed by the NL2SQL sub-module (010b-nl2sql) and by the query pipeline connectors.
| Term | Definition |
|---|---|
| Sandbox | Read-only environment with table allowlist and SQL validation. |
| Allowlist | Only tables with the cache_ prefix in the public schema. |
| SandboxResult | Typed return with columns, rows, row_count, truncated, error. |
As an expert developer, I want to send SELECT directly to the sandbox and get rows.
As a developer, I want to see which cache_* tables are available with their columns.
- FR-001: MUST expose the
ISQLSandboxport with:execute_readonly,list_cached_tables,get_column_types. - FR-002:
execute_readonlyMUST validate SQL in 3 layers:- Regex forbidden keywords (INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, etc.)
- Allowlist: only SELECT/WITH, only FROM/JOIN over
cache_*tables in thepublicschema - sqlglot AST parsing: reject CTEs with DML/DDL, validate table refs
- FR-003: MUST execute in a
ThreadPoolExecutor(max_workers=2)with a default 10sstatement_timeout. - FR-004: MUST truncate results to a maximum of 1000 rows (
MAX_ROWS). - FR-005: MUST return
SandboxResult(columns, rows, row_count, truncated, error). - FR-006:
list_cached_tablesMUST returnCachedTableInfo(table_name, dataset_id, row_count, columns)for allcache_*. - FR-008: MUST apply rate limiting (SlowAPI: 10/min) to the endpoints (
/api/v1/sandbox/query).
- SC-001: SQL execution responds in <3 seconds (p95) including validation.
- SC-003: Zero successful SQL injections (the 3 layers block them).
- sqlglot parses the PostgreSQL dialect correctly.
statement_timeoutis respected at the DB level.- Users do not need writes (read-only is sufficient).
- Writes — reads only.
- DDL — creating/altering tables is not allowed.
- Tables outside the
publicschema — blocked. - Tables without the
cache_prefix — blocked. - Explicit user transactions.
- LLM-based SQL generation — lives in
010b-nl2sql.
- [RESOLVED CL-001] — Mechanism = PostgreSQL-level cancel only, no asyncio cancellation.
execute_readonlywraps inrun_in_executor()(pg_sandbox_adapter.py:242-249) and setsSET statement_timeout = {ms}(_execute_sync:194). PG cancels the query at the DB level, but if the post-fetch thread is CPU-bound in Python, there is no thread kill. Structural debt: the timeout may not effectively interrupt in all cases (slow DB queries yes; post-result Python loops no). - [RESOLVED CL-002] — SQL error messages exposed to the user are acceptable. OpenArg is open source: the schema of
cache_*tables is public, there is no sensitive information to leak. Sandbox errors help the user correct their SQL with no security risk. - [NEEDS CLARIFICATION CL-003] — The
max_workers=2of the ThreadPoolExecutor is a known bottleneck under high load.
- [DEBT-001] — Static ThreadPoolExecutor
max_workers=2— does not scale with load. - [DEBT-003] — Exposed SQL error messages in the response — potential schema info leak.
- [DEBT-004] — No structured logging of executed SQL for auditing.
- [DEBT-005] — No metrics for success/failure per table.
End of spec.md