Skip to content

Commit 8aef600

Browse files
committed
feat: QueryForge distributed SQL engine — predicate pushdown, partial aggregation, fault recovery, EXPLAIN endpoint, 1.70x speedup on 2M rows
0 parents  commit 8aef600

60 files changed

Lines changed: 4799 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Coordinator
2+
DATABASE_URL=postgresql://postgres:password@postgres:5432/queryforge
3+
MINIO_ENDPOINT=minio
4+
MINIO_PORT=9000
5+
MINIO_ACCESS_KEY=minioadmin
6+
MINIO_SECRET_KEY=minioadmin
7+
WORKER_ADDRESSES=worker-1:50051,worker-2:50051,worker-3:50051
8+
9+
# Workers
10+
WORKER_ID=worker-1
11+
WORKER_PORT=50051
12+
COORDINATOR_ADDRESS=coordinator:50050
13+
14+
# Frontend
15+
VITE_COORDINATOR_URL=http://localhost:3000
16+
VITE_WS_URL=ws://localhost:3000

.github/workflows/ci.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v4
16+
17+
- name: Build all Docker images
18+
run: docker compose build
19+
20+
- name: Start services
21+
run: docker compose up -d
22+
23+
- name: Wait for coordinator to be healthy
24+
run: |
25+
for i in $(seq 1 30); do
26+
if curl -sf http://localhost:3000/api/health; then
27+
echo "Coordinator is healthy"
28+
exit 0
29+
fi
30+
echo "Waiting... ($i/30)"
31+
sleep 5
32+
done
33+
echo "Coordinator did not become healthy in time"
34+
docker compose logs coordinator
35+
exit 1
36+
37+
- name: Verify workers registered
38+
run: |
39+
sleep 15
40+
WORKERS=$(curl -sf http://localhost:3000/api/workers | python3 -c "import sys,json; data=json.load(sys.stdin); print(len([w for w in data if w.get('liveStatus')=='active']))")
41+
echo "Active workers: $WORKERS"
42+
if [ "$WORKERS" -lt "1" ]; then
43+
echo "No active workers registered"
44+
docker compose logs worker-1 worker-2 worker-3
45+
exit 1
46+
fi
47+
48+
- name: Tear down
49+
if: always()
50+
run: docker compose down -v

.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
node_modules/
2+
.env
3+
*.csv
4+
/tmp/
5+
benchmarks/benchmark_data.csv
6+
7+
# Docker volumes
8+
postgres_data/
9+
minio_data/
10+
grafana_data/
11+
12+
# Logs
13+
*.log
14+
npm-debug.log*
15+
16+
# OS
17+
.DS_Store
18+
Thumbs.db

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{
2+
}

README.md

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
# QueryForge — Distributed SQL Query Engine
2+
3+
> A production-grade distributed SQL engine that processes million-row CSV datasets across parallel worker nodes. Upload a dataset, write SQL, and QueryForge distributes execution across 3 workers — returning results faster than any single-machine setup.
4+
>
5+
> **Architected after AWS Athena · Google BigQuery · Apache Drill**
6+
7+
---
8+
9+
## Benchmark Results
10+
11+
| Metric | Value |
12+
|--------|-------|
13+
| Dataset size | 2,000,000 rows |
14+
| Single-machine time | 5,958ms |
15+
| Distributed (3 workers) | 3,512ms |
16+
| **Speedup** | **1.70x faster** |
17+
| Throughput | ~570,000 rows/sec |
18+
| Max tested | 5,000,000 rows ✓ |
19+
20+
Query: `SELECT department, COUNT(*), AVG(salary), MAX(salary), MIN(salary), SUM(salary) FROM employees WHERE age > 20 GROUP BY department ORDER BY total DESC`
21+
22+
---
23+
24+
## Architecture
25+
26+
```
27+
┌─────────────────────────────────────────────────────────────────────┐
28+
│ Frontend (React + Vite) │
29+
│ Upload · SQL Editor · ⚡ Explain · Live Worker Dashboard │
30+
└─────────────────────────┬───────────────────────────────────────────┘
31+
│ REST + WebSocket
32+
┌─────────────────────────▼───────────────────────────────────────────┐
33+
│ Coordinator (Node.js) │
34+
│ │
35+
│ SQL Parser → Execution Plan → Job Manager → Result Merger │
36+
│ ⚡ EXPLAIN endpoint · Fault Monitor · WebSocket broadcaster │
37+
│ CoordinatorService gRPC server (workers register here) │
38+
└──────────┬──────────────────┬───────────────────┬────────────────────┘
39+
│ gRPC │ gRPC │ gRPC
40+
┌──────▼──────┐ ┌───────▼──────┐ ┌───────▼──────┐
41+
│ Worker 1 │ │ Worker 2 │ │ Worker 3 │
42+
│ │ │ │ │ │
43+
│ ① Download │ │ ① Download │ │ ① Download │
44+
│ partition │ │ partition │ │ partition │
45+
│ ② Filter │ │ ② Filter │ │ ② Filter │
46+
│ (WHERE) │ │ (WHERE) │ │ (WHERE) │
47+
│ ③ Local │ │ ③ Local │ │ ③ Local │
48+
│ GROUP BY │ │ GROUP BY │ │ GROUP BY │
49+
│ ④ Stream │ │ ④ Stream │ │ ④ Stream │
50+
│ results │ │ results │ │ results │
51+
└──────┬──────┘ └───────┬──────┘ └───────┬──────┘
52+
└──────────────────┼───────────────────┘
53+
│ All read from
54+
┌──────────▼──────────┐
55+
│ MinIO (S3) │
56+
│ datasets/ │
57+
│ partitions/ │
58+
└─────────────────────┘
59+
PostgreSQL ── metadata, jobs, tasks, workers
60+
Prometheus + Grafana ── metrics, dashboards
61+
```
62+
63+
---
64+
65+
## Key Features
66+
67+
### ① Predicate Pushdown
68+
WHERE filters applied **row-by-row during CSV streaming**, before any rows enter memory. Non-matching rows are discarded immediately — never transferred to coordinator.
69+
70+
```
71+
Worker reads CSV:
72+
row 1: age=22 → WHERE age > 25 → ✗ discarded (never in memory)
73+
row 2: age=31 → WHERE age > 25 → ✓ kept
74+
row 3: age=19 → WHERE age > 25 → ✗ discarded
75+
```
76+
77+
### ② Partial Aggregation (MapReduce-style)
78+
For GROUP BY queries, each worker builds a **local hash map** on its partition. Coordinator receives 3 compact maps and merges them — not millions of raw rows.
79+
80+
```
81+
Worker 1 sends: { Engineering: { count:180000, sum:14B } } ← 8 objects
82+
Worker 2 sends: { Engineering: { count:181000, sum:14.1B } } ← 8 objects
83+
Worker 3 sends: { Engineering: { count:180667, sum:14B } } ← 8 objects
84+
85+
Coordinator merges → final AVG = total_sum / total_count
86+
(NOT average of averages — mathematically correct)
87+
```
88+
89+
### ③ Automatic Fault Recovery
90+
Workers send heartbeats every 5 seconds. If a worker misses 3 heartbeats (15s), coordinator marks it dead and reassigns its partition to a healthy worker. Max 3 reassignment attempts per partition.
91+
92+
### ④ EXPLAIN Endpoint (like PostgreSQL's EXPLAIN)
93+
`POST /api/explain` returns the full execution plan before running — shows which predicates are pushed down, partition assignment per worker, aggregation strategy per function.
94+
95+
### ⑤ OpenTelemetry Observability
96+
Every coordinator and worker exposes metrics on `:9464/metrics`. Custom spans on `query.plan`, `job.execute`, `task.execute` with `rows.scanned` vs `rows.passed_filter` attributes.
97+
98+
---
99+
100+
## Quick Start
101+
102+
```bash
103+
git clone https://github.com/vipulpandey7917/queryforge
104+
cd queryforge
105+
docker compose up --build
106+
```
107+
108+
**That's it.** All 9 services start automatically. No manual setup.
109+
110+
| Service | URL |
111+
|---------|-----|
112+
| **Frontend** | http://localhost:5173 |
113+
| **Coordinator API** | http://localhost:3000 |
114+
| **MinIO Console** | http://localhost:9001 (minioadmin / minioadmin) |
115+
| **Prometheus** | http://localhost:9090/targets |
116+
| **Grafana** | http://localhost:3001 (admin / admin) |
117+
118+
---
119+
120+
## SQL Support
121+
122+
```sql
123+
-- Filtered scan with predicate pushdown
124+
SELECT name, salary FROM employees WHERE salary > 60000
125+
126+
-- GROUP BY with multiple aggregations
127+
SELECT department,
128+
COUNT(*) as total,
129+
AVG(salary) as avg_sal,
130+
MAX(salary) as max_sal,
131+
MIN(salary) as min_sal,
132+
SUM(salary) as total_sal
133+
FROM employees
134+
WHERE age > 25
135+
GROUP BY department
136+
ORDER BY total DESC
137+
138+
-- COUNT with filter
139+
SELECT COUNT(*) as total FROM employees WHERE city = 'Mumbai'
140+
141+
-- ORDER BY + LIMIT
142+
SELECT name, salary FROM employees ORDER BY salary DESC LIMIT 10
143+
```
144+
145+
Supported aggregations: `COUNT`, `SUM`, `AVG`, `MAX`, `MIN`
146+
147+
---
148+
149+
## Query Execution — 20-Step Flow
150+
151+
```
152+
1. POST /api/query { sql, datasetId }
153+
2. node-sql-parser → AST
154+
3. Extract: predicates, GROUP BY, aggregations, ORDER BY, LIMIT
155+
4. Look up dataset + 3 partitions in PostgreSQL
156+
5. Create Job + 3 Tasks in PostgreSQL
157+
6. Dispatch 3 gRPC ExecuteTask calls in parallel (Promise.all)
158+
7. Each worker: getObject(MinIO) → write to /tmp/{taskId}.csv
159+
8. Each worker: stream CSV row-by-row → apply WHERE predicates
160+
9. Each worker: build local GROUP BY hash map
161+
10. Each worker: stream AggregationGroup messages → coordinator
162+
11. Coordinator: merge 3 hash maps (SUM totals, MAX of MAXes, COUNT sums)
163+
12. Coordinator: compute final AVG = total_sum / total_count
164+
13. Coordinator: apply ORDER BY on merged result
165+
14. Coordinator: apply LIMIT
166+
15. Coordinator: stream rows via WebSocket → frontend
167+
16. Frontend: render rows as they arrive
168+
17. Coordinator: UPDATE jobs SET status='completed'
169+
18. WebSocket: { type: 'complete', totalRows, executionTimeMs }
170+
19. OTel spans closed with row counts
171+
20. Prometheus metrics updated
172+
```
173+
174+
---
175+
176+
## Fault Recovery Demo
177+
178+
While a query is running:
179+
180+
```bash
181+
docker compose stop worker-2
182+
```
183+
184+
Coordinator detects missing heartbeat → reassigns partition → query completes with 2 workers.
185+
186+
```bash
187+
docker compose start worker-2 # brings it back online
188+
```
189+
190+
---
191+
192+
## Running the Benchmark
193+
194+
```bash
195+
cd benchmarks
196+
npm install
197+
node run_benchmark.js
198+
```
199+
200+
Generates 2M rows → uploads → runs distributed query → runs same query single-machine → prints speedup.
201+
202+
---
203+
204+
## API Reference
205+
206+
| Method | Endpoint | Description |
207+
|--------|----------|-------------|
208+
| `POST` | `/api/datasets/upload` | Upload CSV, returns `{ datasetId, rowCount, schema }` |
209+
| `GET` | `/api/datasets` | List all datasets |
210+
| `GET` | `/api/datasets/:id` | Dataset + partition details |
211+
| `POST` | `/api/query` | Submit SQL, returns `{ jobId }` immediately |
212+
| `GET` | `/api/query/jobs/:id` | Job status + per-task metrics |
213+
| `POST` | `/api/explain` | Execution plan JSON (no query executed) |
214+
| `GET` | `/api/workers` | Live worker registry with heartbeat status |
215+
| `GET` | `/api/health` | Coordinator health check |
216+
217+
**WebSocket:** `ws://localhost:3000/ws`
218+
```json
219+
// Subscribe
220+
{ "type": "subscribe", "jobId": "..." }
221+
222+
// Receive
223+
{ "type": "row", "data": { "department": "Engineering", "total": 541667 } }
224+
{ "type": "progress", "completedTasks": 2, "totalTasks": 3 }
225+
{ "type": "complete", "totalRows": 8, "executionTimeMs": 3512 }
226+
{ "type": "error", "message": "..." }
227+
```
228+
229+
---
230+
231+
## Tech Stack
232+
233+
| Layer | Technology |
234+
|-------|-----------|
235+
| Coordinator | Node.js 20, Express 4, gRPC (`@grpc/grpc-js`), WebSocket (`ws`) |
236+
| Workers | Node.js 20, gRPC server-side streaming |
237+
| SQL Parsing | `node-sql-parser` (PostgreSQL dialect) |
238+
| Object Storage | MinIO (S3-compatible) |
239+
| Metadata | PostgreSQL 15 |
240+
| Observability | OpenTelemetry SDK, Prometheus, Grafana |
241+
| Containerisation | Docker Compose (9 services) |
242+
| Frontend | React 18, Vite 5, Tailwind CSS 3 |
243+
244+
---
245+
246+
## Project Structure
247+
248+
```
249+
queryforge/
250+
├── coordinator/ # Coordinator node
251+
│ ├── src/
252+
│ │ ├── grpc/ # CoordinatorService server + WorkerService client
253+
│ │ ├── routes/ # datasets, query, workers, explain
254+
│ │ ├── services/ # queryPlanner, partitioner, jobManager,
255+
│ │ │ # resultMerger, faultMonitor
256+
│ │ ├── websocket/ # WebSocket server (ping/pong + job subscriptions)
257+
│ │ └── db/ # PostgreSQL connection pool
258+
│ ├── schema.sql
259+
│ └── tracing.js # OTel SDK (loaded before index.js via -r flag)
260+
├── worker/ # Worker node
261+
│ ├── src/
262+
│ │ ├── grpc/ # WorkerService server + CoordinatorService client
263+
│ │ └── services/ # taskExecutor, predicateEvaluator,
264+
│ │ # aggregator, minioClient
265+
│ └── tracing.js
266+
├── frontend/ # React + Tailwind UI
267+
│ └── src/components/ # DatasetUploader, SQLEditor, ExplainPanel,
268+
│ # WorkerDashboard, ResultsTable
269+
├── proto/
270+
│ └── queryforge.proto # gRPC service definitions
271+
├── monitoring/
272+
│ ├── prometheus.yml
273+
│ └── provisioning/ # Grafana auto-provisioned datasource + dashboard
274+
├── benchmarks/
275+
│ └── run_benchmark.js # 2M row benchmark script
276+
└── docker-compose.yml # 9 services, zero manual setup
277+
```
278+
279+
---
280+
281+
## Architecture Decisions (Interview Q&A)
282+
283+
**Why gRPC between coordinator and workers, not REST?**
284+
gRPC supports server-side streaming natively — workers stream partial results back as they process, without buffering everything first. REST would require workers to finish completely before sending anything, removing the streaming benefit.
285+
286+
**Why MinIO and not shared filesystem?**
287+
Shared filesystem doesn't work across distributed nodes. MinIO gives each worker independent object access — worker-1 reads partition-0, worker-2 reads partition-1, both simultaneously, with no coordination needed.
288+
289+
**Why partial aggregation instead of sending all rows?**
290+
For a GROUP BY query on 2M rows with 8 groups, sending raw rows means 666,667 rows per worker × 3 workers = 2M rows through the coordinator. Partial aggregation sends 8 hash map entries per worker = 24 objects total. The network difference is ~100MB vs ~200 bytes.
291+
292+
**What's the bottleneck right now?**
293+
Coordinator is a single point of failure and also the merge bottleneck. For production I'd shard the merge across multiple coordinator instances, similar to how Presto uses a coordinator cluster.
294+
295+
**How would you scale beyond 3 workers?**
296+
Partition count = worker count. Dynamic partitioning would split the dataset into N chunks at upload time based on registered workers. The current round-robin assignment already handles N workers — changing `partitionCount` from 3 to N is the only required change.
297+
298+
**Why 3 partitions specifically?**
299+
Matched to the 3 workers in this deployment. Coordinator assigns partition[i] to worker[i % workerCount], so adding a 4th worker automatically gets partition-3 if it exists.
300+
301+
---
302+
303+
*Built by [Vipul Pandey](https://github.com/vipulpandey7917) — GSoC 2026 Contributor (Learning Unlimited) · LeetCode Knight (1881) · B.Tech IT + MBA, IIITM Gwalior*

0 commit comments

Comments
 (0)