feat: Phase 0 — pipeline integration, DDL parser, SQL executor
- Rewrote PLAN.md with 6-phase production roadmap - Added 15 DDL/txn lexer keywords (primary, key, foreign, references, etc.) - Added AST nodes: CreateTable, DropTable, AlterTable, BeginTxn, CommitTxn, RollbackTxn, ExplainStmt, ColumnDef - Completed INSERT parser: VALUES, column list, RETURNING, ON CONFLICT - Added CREATE TABLE/DROP TABLE/ALTER TABLE parsers with constraints (PK, FK, UNIQUE, NOT NULL, CHECK, DEFAULT) - Added UPDATE/DELETE RETURNING support - Added BEGIN, COMMIT, ROLLBACK, EXPLAIN parsers - New query/executor.nim: AST->IR lowering + plan execution against LSM-Tree - Wired server to executor pipeline (replaced regex-based KV INSERT) - All 216 existing tests pass
This commit is contained in:
@@ -11,6 +11,7 @@ benchmarks/bench_all
|
|||||||
benchmarks/compare
|
benchmarks/compare
|
||||||
clients/nim/tests/test_client
|
clients/nim/tests/test_client
|
||||||
src/baradadb
|
src/baradadb
|
||||||
|
src/barabadb/core/raft
|
||||||
|
|
||||||
# Temp
|
# Temp
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|||||||
@@ -1,216 +1,294 @@
|
|||||||
# BaraDB — План към Production (Web & ERP)
|
# BaraDB — Production Roadmap (Web & ERP)
|
||||||
|
|
||||||
## Визия
|
## Vision
|
||||||
BaraDB да стане production-ready база данни, с която да се изграждат:
|
BaraDB as a production-ready database for:
|
||||||
- **Web приложения** (блогове, е-магазини, SaaS продукти)
|
- **Web applications** (blogs, e-commerce, SaaS)
|
||||||
- **Малки ERP системи** (CRM, склад, счетоводство, invoicing)
|
- **Small ERP systems** (CRM, warehouse, accounting, invoicing)
|
||||||
|
|
||||||
> Целеви потребител: solo-dev / малък екип, който иска лесна за deploy и бърза локална база, без зависимост от PostgreSQL/MySQL.
|
> Target user: solo-dev / small team wanting a fast local DB without PostgreSQL/MySQL dependency.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Текущо състояние (база)
|
## Current State (Baseline)
|
||||||
|
|
||||||
| Компонент | Статус |
|
| Component | Status |
|
||||||
|-----------|--------|
|
|-----------|--------|
|
||||||
| LSM-Tree KV store | ✅ Стабилен, thread-safe, persistent |
|
| LSM-Tree KV store | Stable, thread-safe, persistent |
|
||||||
| HNSW векторен search | ✅ Работи с recall > 0.9 |
|
| HNSW vector search | Working, recall > 0.9 |
|
||||||
| TCP wire protocol | ✅ Бинарен, SELECT/INSERT/DELETE |
|
| TCP wire protocol | Binary, SELECT/INSERT/DELETE |
|
||||||
| Raft consensus | ✅ TCP transport, leader election |
|
| Raft consensus | TCP transport, leader election |
|
||||||
| Graph engine | ✅ In-memory + persistence |
|
| Graph engine | In-memory + persistence |
|
||||||
| Thread-safety | ✅ Coarse-grained locks |
|
| CI/CD | GitHub Actions |
|
||||||
| CI/CD | ✅ GitHub Actions |
|
| Test suite | 56 suites, ~250 tests |
|
||||||
|
|
||||||
**Липсва за Web/ERP:** SQL съвместимост, ACID транзакции, HTTP API, auth, ORM, миграции, backup.
|
**Critical gaps for production:**
|
||||||
|
- Server bypasses IR/codegen/MVCC/schema — `executeQuery()` does lex→parse→raw LSMTree calls
|
||||||
|
- INSERT parser incomplete (no VALUES, column list, RETURNING)
|
||||||
|
- No CREATE TABLE/ALTER TABLE/DROP TABLE in parser
|
||||||
|
- MVCC not wired to query path (no BEGIN/COMMIT/ROLLBACK in server)
|
||||||
|
- B-Tree indexes not integrated with LSM-Tree
|
||||||
|
- SQL schema system not connected (EdgeQL types only)
|
||||||
|
- No HTTP REST API
|
||||||
|
- Auth not wired to server
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Фаза 1: Релационен engine + SQL (4–6 седмици)
|
## Phase 0: Pipeline Integration & Parser Completion (2–3 weeks) **← IN PROGRESS**
|
||||||
|
|
||||||
### 1.1 SQL парсър и AST
|
### 0.1 Complete DML parser (INSERT/UPDATE/DELETE)
|
||||||
- Имплементирай ANSI SQL подмножество (CREATE TABLE, ALTER TABLE, DROP TABLE)
|
- INSERT with column list: `INSERT INTO t (c1, c2) VALUES (v1, v2)`
|
||||||
- INSERT с column list: `INSERT INTO users (name, email) VALUES ('...', '...')`
|
- INSERT with RETURNING clause
|
||||||
- UPDATE: `UPDATE users SET name = '...' WHERE id = 1`
|
- UPDATE with RETURNING clause
|
||||||
- SELECT с JOIN, GROUP BY, HAVING, ORDER BY, LIMIT/OFFSET
|
- DELETE with RETURNING clause
|
||||||
- DELETE с WHERE
|
- Multiple VALUES rows: `VALUES (v1), (v2), ...`
|
||||||
- Поддръжка на `RETURNING` клауза
|
|
||||||
|
|
||||||
### 1.2 Типова система и constraints
|
### 0.2 Add SQL DDL to parser
|
||||||
- `INTEGER`, `BIGINT`, `SERIAL` (auto-increment)
|
- `CREATE TABLE` with column definitions, constraints (PK, FK, UNIQUE, NOT NULL, CHECK, DEFAULT)
|
||||||
|
- `ALTER TABLE` (ADD COLUMN, DROP COLUMN, RENAME COLUMN)
|
||||||
|
- `DROP TABLE`
|
||||||
|
- Tokens: tkCreate, tkTable, tkAlter, tkColumn, tkPrimary, tkKey, tkForeign, tkReferences, tkCascade, tkUnique, tkNotNull, tkCheck, tkDefault, tkRename, tkAdd, tkDrop
|
||||||
|
|
||||||
|
### 0.3 SQL-compatible schema system
|
||||||
|
- SQL table catalog (separate from EdgeQL type system)
|
||||||
|
- Store schema in LSM-Tree (`_schema_tables`, `_schema_columns`, `_schema_indexes`)
|
||||||
|
- Column type enforcement during INSERT/UPDATE
|
||||||
|
- Schema validation on CREATE TABLE
|
||||||
|
|
||||||
|
### 0.4 AST → IR lowering pass
|
||||||
|
- Convert Select AST nodes to IR plans (scan → filter → project → sort → limit)
|
||||||
|
- Convert Insert AST nodes to IR plans (values)
|
||||||
|
- Convert Update/Delete AST nodes to IR plans
|
||||||
|
- Convert CTE AST nodes to IR plans
|
||||||
|
- Lower JOINs to IR join nodes
|
||||||
|
|
||||||
|
### 0.5 Codegen → Storage execution
|
||||||
|
- Execute StorageOp tree against LSM-Tree
|
||||||
|
- sokScan: full table scan via `scanMemTable()` / SSTable reader
|
||||||
|
- sokPointRead: key-based lookup
|
||||||
|
- sokFilter: evaluate IR expressions against rows
|
||||||
|
- sokProject: column selection
|
||||||
|
- sokSort: in-memory sort
|
||||||
|
- sokLimit: slice results
|
||||||
|
- sokInsert/sokUpdate/sokDelete: write to LSM-Tree
|
||||||
|
|
||||||
|
### 0.6 Wire server to use pipeline
|
||||||
|
- Replace `execSelect/execInsert/execDelete` with pipeline-based execution
|
||||||
|
- Server flow: lex → parse → AST→IR lower → codegen → execute StorageOp
|
||||||
|
- Keep backward-compatible wire protocol
|
||||||
|
- All 56 existing tests must still pass
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Schema & Indexes (2–3 weeks)
|
||||||
|
|
||||||
|
### 1.1 SQL type system
|
||||||
|
- `INTEGER`, `BIGINT`, `SMALLINT`, `SERIAL` (auto-increment on INSERT)
|
||||||
- `VARCHAR(n)`, `TEXT`
|
- `VARCHAR(n)`, `TEXT`
|
||||||
- `BOOLEAN`
|
- `BOOLEAN`
|
||||||
- `TIMESTAMP`, `DATE` (ISO 8601)
|
- `TIMESTAMP`, `DATE` (ISO 8601)
|
||||||
- `JSON`, `JSONB` (in-memory + компресия)
|
- `JSON`, `JSONB`
|
||||||
- `UUID` (v4)
|
- `UUID` (v4 generation)
|
||||||
- Constraints: `PRIMARY KEY`, `FOREIGN KEY`, `UNIQUE`, `NOT NULL`, `CHECK`, `DEFAULT`
|
- `NUMERIC(p,s)`, `DOUBLE PRECISION`, `REAL`
|
||||||
- Foreign key каскади: `ON DELETE CASCADE/SET NULL`
|
|
||||||
|
|
||||||
### 1.3 B-Tree индекси + интеграция с query planner
|
### 1.2 Constraints enforcement
|
||||||
|
- PRIMARY KEY: unique index + NOT NULL
|
||||||
|
- FOREIGN KEY + ON DELETE CASCADE/SET NULL/RESTRICT
|
||||||
|
- UNIQUE: unique index
|
||||||
|
- NOT NULL: check on INSERT/UPDATE
|
||||||
|
- CHECK: evaluate expression on INSERT/UPDATE
|
||||||
|
- DEFAULT: fill missing values on INSERT
|
||||||
|
|
||||||
|
### 1.3 B-Tree index integration
|
||||||
- `CREATE INDEX idx_name ON table(column)`
|
- `CREATE INDEX idx_name ON table(column)`
|
||||||
- `CREATE UNIQUE INDEX`
|
- `CREATE UNIQUE INDEX`
|
||||||
- Покриващ индекс (covering index) за чести колони
|
- B-Tree indexes created per table column
|
||||||
- Query planner да избира индекс вместо full scan
|
- Query planner uses B-Tree for WHERE clauses on indexed columns
|
||||||
- `EXPLAIN` за анализ на заявки
|
- Range scans via B-Tree leaf linked list
|
||||||
|
|
||||||
### 1.4 Транзакции (ACID)
|
### 1.4 Query planner
|
||||||
- `BEGIN`, `COMMIT`, `ROLLBACK`
|
- Choose index scan vs full scan based on WHERE clause
|
||||||
- Isolation level: `READ COMMITTED` (първа фаза), `REPEATABLE READ` (втора)
|
- Multi-column index support
|
||||||
- Deadlock detection и timeout
|
- Covering index optimization
|
||||||
- MVCC интеграция с LSM-Tree (versioned reads)
|
- `EXPLAIN` output with cost estimates
|
||||||
- WAL за crash recovery при транзакции
|
- Adaptive query reoptimization (wire up `adaptive.nim`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Фаза 2: Web API & Authentication (3–4 седмици)
|
## Phase 2: Transactions (2–3 weeks)
|
||||||
|
|
||||||
### 2.1 HTTP REST API
|
### 2.1 Wire MVCC into server pipeline
|
||||||
- `POST /query` — изпълнява SQL, връща JSON
|
- `BEGIN`, `COMMIT`, `ROLLBACK` commands
|
||||||
- `GET /health` — readiness/liveness probe
|
- Server tracks per-connection Transaction state
|
||||||
- `GET /metrics` — брой заявки, latency, errors (Prometheus формат)
|
- All reads/writes through TxnManager
|
||||||
- JSON request/response body:
|
- Isolation: Read Committed (Phase 2a), Repeatable Read (Phase 2b)
|
||||||
```json
|
|
||||||
{ "query": "SELECT * FROM users WHERE id = 1", "params": [] }
|
### 2.2 WAL crash recovery
|
||||||
```
|
- Implement REDO: replay committed WAL entries into LSM-Tree
|
||||||
- Batch queries: `POST /batch` — множество заявки в едно тяло
|
- Implement UNDO: remove uncommitted entries on recovery
|
||||||
|
- Checkpoint markers in WAL
|
||||||
|
- Point-in-time recovery
|
||||||
|
|
||||||
|
### 2.3 Compaction
|
||||||
|
- Implement actual SSTable merge (currently simulated)
|
||||||
|
- Read multiple SSTables, merge key-value pairs, write merged SSTable
|
||||||
|
- Level-based compaction strategy
|
||||||
|
- Background compaction scheduling
|
||||||
|
|
||||||
|
### 2.4 Deadlock detection wiring
|
||||||
|
- Wire deadlock detection into TxnManager
|
||||||
|
- Automatic deadlock timeout and victim selection
|
||||||
|
- Client notification on rollback
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: HTTP REST API & Authentication (2–3 weeks)
|
||||||
|
|
||||||
|
### 3.1 HTTP server
|
||||||
|
- HTTP/1.1 server alongside TCP wire protocol (shared port or separate)
|
||||||
|
- `POST /query` — execute SQL, return JSON
|
||||||
|
- `GET /health` — readiness/liveness
|
||||||
|
- `GET /metrics` — Prometheus format
|
||||||
- Content-Type: `application/json`
|
- Content-Type: `application/json`
|
||||||
|
|
||||||
### 2.2 Authentication & Authorization
|
### 3.2 Authentication
|
||||||
- JWT bearer token в HTTP header `Authorization`
|
- `CREATE USER` / `DROP USER` / `ALTER USER` SQL
|
||||||
- `CREATE USER` / `DROP USER` / `ALTER USER`
|
- Password hashing with argon2
|
||||||
- `GRANT` / `REVOKE` за права на таблици
|
- JWT token creation with HMAC-SHA256 (replace djb2 `simpleHash`)
|
||||||
- Row-Level Security (RLS): `CREATE POLICY`
|
- `Authorization: Bearer <token>` in HTTP headers
|
||||||
- Хеширане на пароли с bcrypt/argon2
|
- Per-user namespace isolation
|
||||||
- Rate limiting per API key / IP
|
|
||||||
|
|
||||||
### 2.3 WebSocket за real-time
|
### 3.3 Authorization
|
||||||
- `ws://host:port/live` — subscribe към таблица/ред
|
- `GRANT` / `REVOKE` for table-level privileges (SELECT, INSERT, UPDATE, DELETE)
|
||||||
- `NOTIFY` / `LISTEN` аналог
|
- Row-Level Security (RLS): `CREATE POLICY` on tables
|
||||||
- Пуш нотификации при INSERT/UPDATE/DELETE
|
- Wire auth into both HTTP and TCP protocol paths
|
||||||
|
|
||||||
### 2.4 CORS и HTTP hardening
|
### 3.4 Rate limiting & TLS
|
||||||
- CORS headers за browser достъп
|
- Wire RateLimiter into HTTP server (token bucket per IP)
|
||||||
- TLS termination (reuse `ssl.nim`)
|
- Wire TLS/SSL using OpenSSL FFI (not mock)
|
||||||
|
- Self-signed cert generation
|
||||||
|
- Configurable TLS via `baradadb cert create`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: WebSocket & Real-time (1–2 weeks)
|
||||||
|
|
||||||
|
### 4.1 WebSocket server
|
||||||
|
- `ws://host:port/live` — subscribe to table changes
|
||||||
|
- `SUBSCRIBE table_name` WebSocket message
|
||||||
|
- Push notifications on INSERT/UPDATE/DELETE
|
||||||
|
- `NOTIFY` / `LISTEN` analogue
|
||||||
|
|
||||||
|
### 4.2 CORS & HTTP hardening
|
||||||
|
- CORS headers for browser access
|
||||||
- Request size limits (10MB default)
|
- Request size limits (10MB default)
|
||||||
- Connection keep-alive и HTTP/2 readiness
|
- Connection keep-alive
|
||||||
|
- HTTP/2 readiness (ALPN negotiation)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Фаза 3: ERP фичове (4–5 седмици)
|
## Phase 5: ERP Features (3–4 weeks)
|
||||||
|
|
||||||
### 3.1 Schema migrations
|
### 5.1 Schema migrations
|
||||||
- `CREATE MIGRATION` / `APPLY MIGRATION`
|
- `CREATE MIGRATION` → `APPLY MIGRATION`
|
||||||
- Версиониране на схемата в `__schema_version` таблица
|
- Versioned schema in `_schema_version` table
|
||||||
- Up/down скриптове
|
- Up/down migration scripts
|
||||||
- Dry-run режим
|
- Dry-run mode
|
||||||
- CLI: `baradadb migrate status`, `baradadb migrate up`, `baradadb migrate down`
|
- CLI: `baradadb migrate status|up|down`
|
||||||
|
|
||||||
### 3.2 Views и materialized views
|
### 5.2 Views
|
||||||
- `CREATE VIEW` — read-only virtual table
|
- `CREATE VIEW` — virtual table (stored query)
|
||||||
- `CREATE MATERIALIZED VIEW` — кеширана snapshot + `REFRESH`
|
- `CREATE MATERIALIZED VIEW` — cached snapshot + `REFRESH`
|
||||||
- Поддръжка в query planner
|
- View usage in query planner
|
||||||
|
|
||||||
### 3.3 Triggers и stored functions
|
### 5.3 Triggers & stored functions
|
||||||
- `CREATE TRIGGER` — `BEFORE`/`AFTER` INSERT/UPDATE/DELETE
|
- `CREATE TRIGGER` — BEFORE/AFTER on INSERT/UPDATE/DELETE
|
||||||
- Stored functions in Nim (compiled to UDF):
|
- Stored functions in Nim (compile to UDF)
|
||||||
```sql
|
- ERP helper functions: `vat_calc`, `currency_convert`, `invoice_number_next`
|
||||||
CREATE FUNCTION total_price(quantity INT, price DECIMAL) RETURNS DECIMAL
|
|
||||||
AS 'quantity * price';
|
|
||||||
```
|
|
||||||
- Функции за ERP: `vat_calc`, `currency_convert`, `invoice_number_next`
|
|
||||||
|
|
||||||
### 3.4 Full-text search за ERP документи
|
### 5.4 Full-text search for ERP documents
|
||||||
- `CREATE FULLTEXT INDEX ON invoices(content)`
|
- `CREATE FULLTEXT INDEX ON table(column)`
|
||||||
- `WHERE content @@ 'търсене'`
|
- `WHERE content @@ 'search query'`
|
||||||
- Bulgarian stemming (reuse `fts/multilang.nim`)
|
- Bulgarian stemming integration
|
||||||
|
|
||||||
### 3.5 Partitioning
|
### 5.5 Partitioning
|
||||||
- `CREATE TABLE orders (...) PARTITION BY RANGE (created_at)`
|
- `CREATE TABLE (...) PARTITION BY RANGE (col)`
|
||||||
- Автоматичен partition pruning в query planner
|
- Auto partition pruning in query planner
|
||||||
- Полезно за ERP: архивиране на стари данни
|
- Useful for ERP: archive old data by date range
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Фаза 4: Production readiness & DevEx (3–4 седмици)
|
## Phase 6: Production Readiness (2–3 weeks)
|
||||||
|
|
||||||
### 4.1 Backup & Restore
|
### 6.1 Backup & Restore
|
||||||
- `baradadb backup --output backup.tar.gz`
|
- `baradadb backup --output backup.tar.gz`
|
||||||
- `baradadb restore --input backup.tar.gz`
|
- `baradadb restore --input backup.tar.gz`
|
||||||
- Incremental backup чрез WAL archiving
|
- Incremental backup via WAL archiving
|
||||||
- Point-in-time recovery (PITR)
|
- Point-in-time recovery (PITR)
|
||||||
- Scheduled backups (cron integration)
|
|
||||||
|
|
||||||
### 4.2 Docker и deployment
|
### 6.2 Docker & deployment
|
||||||
- `Dockerfile` — multi-stage build с Nim
|
- `Dockerfile` — multi-stage build with Nim
|
||||||
- `docker-compose.yml` — single node + volume
|
- `docker-compose.yml` — single node
|
||||||
- `docker-compose.raft.yml` — 3-node cluster
|
- `docker-compose.raft.yml` — 3-node cluster
|
||||||
- Helm chart за Kubernetes (statefulset + PVC)
|
- Environment-based config (`BARADB_PORT`, `BARADB_DATA_DIR`)
|
||||||
- Environment-based config (`BARADB_PORT`, `BARADB_DATA_DIR`, `BARADB_RAFT_PEERS`)
|
|
||||||
|
|
||||||
### 4.3 Monitoring и observability
|
### 6.3 Monitoring
|
||||||
- Structured logging (JSON format)
|
- Structured JSON logging
|
||||||
- Prometheus `/metrics` endpoint:
|
- Prometheus `/metrics`: `baradb_queries_total`, `baradb_query_duration_s`, `baradb_connections_active`, `baradb_storage_size_bytes`
|
||||||
- `baradb_queries_total`, `baradb_query_duration_seconds`
|
- Slow query log (configurable threshold)
|
||||||
- `baradb_connections_active`, `baradb_storage_size_bytes`
|
- OpenTelemetry tracing
|
||||||
- `baradb_replication_lag_seconds`
|
|
||||||
- OpenTelemetry tracing integration
|
|
||||||
- Slow query log (threshold configurable)
|
|
||||||
|
|
||||||
### 4.4 ORM / Client SDK
|
### 6.4 Admin dashboard
|
||||||
- **Nim**: `baradb` nimble пакет — fluent query builder + миграции
|
- Web UI on `http://host:port/admin`
|
||||||
```nim
|
- SQL playground with results table
|
||||||
let users = db.table("users")
|
- Schema browser (tables, columns, indexes)
|
||||||
.where("active", "=", true)
|
- Metrics charts
|
||||||
.orderBy("created_at", "DESC")
|
|
||||||
.limit(10)
|
|
||||||
.all()
|
|
||||||
```
|
|
||||||
- **Python**: `pip install baradb` — async/sync client
|
|
||||||
- **JavaScript/TypeScript**: `npm install baradb` — promise-based client
|
|
||||||
- **Go**: `go get github.com/baradb/go-client`
|
|
||||||
- Connection pooling във всички клиенти
|
|
||||||
|
|
||||||
### 4.5 Admin Dashboard (Web UI)
|
|
||||||
- Лек вграден админ панел на `http://host:port/admin`
|
|
||||||
- SQL playground с резултати в таблица
|
|
||||||
- Schema browser (таблици, колони, индекси)
|
|
||||||
- Metrics charts (latency, throughput, storage)
|
|
||||||
- User management UI
|
- User management UI
|
||||||
|
|
||||||
### 4.6 Performance tuning
|
### 6.5 Client SDK improvements
|
||||||
- Prepared statements кеш
|
- Nim: transaction API, prepared statements, auth
|
||||||
- Query result cache (LRU, TTL-based)
|
- Python: complete result parsing, transaction API, async support
|
||||||
- Connection pool в сървъра (max 1000 конекции)
|
- JavaScript: actual TCP/WebSocket connection, complete result parsing
|
||||||
- Auto-compaction scheduling
|
- Go: complete result parsing, transaction API
|
||||||
- Configurable cache size за page cache
|
- Rust: complete result parsing, transaction API
|
||||||
|
- Connection pooling in all clients
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Приоритетна матрица
|
## Priority Matrix
|
||||||
|
|
||||||
| Задача | Влияние | Трудност | Приоритет |
|
| Task | Impact | Difficulty | Priority |
|
||||||
|--------|---------|----------|-----------|
|
|------|--------|-----------|----------|
|
||||||
| SQL парсър + AST | Критично | Висока | P0 |
|
| Pipeline integration (Phase 0) | Critical | High | **P0** |
|
||||||
| ACID транзакции | Критично | Висока | P0 |
|
| SQL DDL parser (Phase 0) | Critical | Medium | **P0** |
|
||||||
| HTTP REST API | Критично | Средна | P0 |
|
| AST→IR lowering (Phase 0) | Critical | High | **P0** |
|
||||||
| B-Tree индекси | Високо | Средна | P1 |
|
| Codegen execution (Phase 0) | Critical | High | **P0** |
|
||||||
| JWT Auth + RLS | Високо | Средна | P1 |
|
| SQL schema system (Phase 1) | Critical | High | **P0** |
|
||||||
| Schema migrations | Високо | Средна | P1 |
|
| B-Tree index integration (Phase 1) | High | Medium | P1 |
|
||||||
| Docker + Compose | Средно | Ниска | P2 |
|
| Constraint enforcement (Phase 1) | High | Medium | P1 |
|
||||||
| Backup/Restore | Средно | Средна | P2 |
|
| MVCC wiring (Phase 2) | Critical | High | **P0** |
|
||||||
| WebSocket real-time | Средно | Средна | P2 |
|
| WAL recovery (Phase 2) | High | Medium | P1 |
|
||||||
| Admin Dashboard | Средно | Висока | P2 |
|
| HTTP REST API (Phase 3) | Critical | Medium | **P0** |
|
||||||
| Views + Triggers | Ниско | Средна | P3 |
|
| JWT Auth + RLS (Phase 3) | High | Medium | P1 |
|
||||||
| Client SDK (ORM) | Ниско | Висока | P3 |
|
| WebSocket real-time (Phase 4) | Medium | Medium | P2 |
|
||||||
| Partitioning | Ниско | Висока | P3 |
|
| Schema migrations (Phase 5) | High | Medium | P1 |
|
||||||
| Kubernetes Helm | Ниско | Средна | P3 |
|
| Backup/Restore (Phase 6) | Medium | Medium | P2 |
|
||||||
|
| Docker + Compose (Phase 6) | Medium | Low | P2 |
|
||||||
|
| Admin Dashboard (Phase 6) | Medium | High | P2 |
|
||||||
|
| Views + Triggers (Phase 5) | Low | Medium | P3 |
|
||||||
|
| Partitioning (Phase 5) | Low | High | P3 |
|
||||||
|
| Client SDK (Phase 6) | Medium | High | P2 |
|
||||||
|
| Kubernetes Helm (Phase 6) | Low | Medium | P3 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Очакван резултат
|
## Expected Results
|
||||||
|
|
||||||
- **Фаза 1:** BaraDB поддържа ANSI SQL subset с ACID транзакции. Може да замени SQLite/PostgreSQL за малки проекти.
|
- **Phase 0:** Server uses full pipeline. INSERT/UPDATE/DELETE/CREATE TABLE work properly. 56 existing tests pass + new tests.
|
||||||
- **Фаза 2:** REST API + auth правят базата достъпна от всякакъв web stack. WebSocket добавя real-time възможности.
|
- **Phase 1:** SQL schema with constraints, B-Tree indexes, EXPLAIN. Can define tables with PKs, FKs, and indexes.
|
||||||
- **Фаза 3:** ERP-ready фичове — migrations, views, triggers, partitioning. Може да поддържа реален бизнес софтуер.
|
- **Phase 2:** ACID transactions with MVCC, WAL recovery, compaction. Can use BEGIN/COMMIT/ROLLBACK.
|
||||||
- **Фаза 4:** Production tooling — Docker, backup, monitoring, ORM, admin UI. Solo-dev може да deploy-не за 5 минути.
|
- **Phase 3:** HTTP REST API with JWT auth, user management, rate limiting. DB accessible from browser.
|
||||||
|
- **Phase 4:** Real-time WebSocket subscriptions. Notifications on data changes.
|
||||||
|
- **Phase 5:** ERP-grade features: migrations, views, triggers, partitioning, full-text search.
|
||||||
|
- **Phase 6:** Docker, backup, monitoring, admin UI. Deploy in 5 minutes.
|
||||||
|
|
||||||
**Крайна оценка след плана:** от 8.5/10 към 9.5/10 — готова за production web/ERP.
|
**Final score after plan:** 9.5/10 — production-ready for web/ERP workloads.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import std/asyncdispatch
|
import std/asyncdispatch
|
||||||
import std/asyncnet
|
import std/asyncnet
|
||||||
import std/strutils
|
import std/strutils
|
||||||
import std/re
|
|
||||||
import std/os
|
import std/os
|
||||||
import std/endians
|
import std/endians
|
||||||
import config
|
import config
|
||||||
@@ -10,6 +9,7 @@ import ../protocol/wire
|
|||||||
import ../query/lexer
|
import ../query/lexer
|
||||||
import ../query/parser
|
import ../query/parser
|
||||||
import ../query/ast
|
import ../query/ast
|
||||||
|
import ../query/executor
|
||||||
import ../storage/lsm
|
import ../storage/lsm
|
||||||
|
|
||||||
type
|
type
|
||||||
@@ -17,6 +17,7 @@ type
|
|||||||
config: BaraConfig
|
config: BaraConfig
|
||||||
running: bool
|
running: bool
|
||||||
db: LSMTree
|
db: LSMTree
|
||||||
|
ctx: ExecutionContext
|
||||||
|
|
||||||
ClientConnection = ref object
|
ClientConnection = ref object
|
||||||
socket: AsyncSocket
|
socket: AsyncSocket
|
||||||
@@ -24,7 +25,8 @@ type
|
|||||||
|
|
||||||
proc newServer*(config: BaraConfig): Server =
|
proc newServer*(config: BaraConfig): Server =
|
||||||
let dataDir = config.dataDir / "server"
|
let dataDir = config.dataDir / "server"
|
||||||
Server(config: config, running: false, db: newLSMTree(dataDir))
|
let db = newLSMTree(dataDir)
|
||||||
|
Server(config: config, running: false, db: db, ctx: newExecutionContext(db))
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# Wire Protocol Helpers
|
# Wire Protocol Helpers
|
||||||
@@ -51,85 +53,10 @@ proc parseHeader(data: string): (bool, MessageHeader) =
|
|||||||
return (true, MessageHeader(kind: kind, length: length, requestId: requestId))
|
return (true, MessageHeader(kind: kind, length: length, requestId: requestId))
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# Query Execution
|
# Query Execution (pipeline-based)
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
proc extractStringLiteral(node: Node): string =
|
proc executeQuery(db: LSMTree, ctx: ExecutionContext, query: string): (bool, QueryResult, string) =
|
||||||
if node == nil:
|
|
||||||
return ""
|
|
||||||
case node.kind
|
|
||||||
of nkStringLit: return node.strVal
|
|
||||||
of nkIdent: return node.identName
|
|
||||||
of nkIntLit: return $node.intVal
|
|
||||||
of nkBinOp:
|
|
||||||
if node.binOp == bkEq and node.binLeft != nil and node.binRight != nil:
|
|
||||||
# Accept any identifier on the left side (e.g., key = 'x', name = 'y')
|
|
||||||
if node.binLeft.kind == nkIdent:
|
|
||||||
return extractStringLiteral(node.binRight)
|
|
||||||
if node.binRight.kind == nkIdent:
|
|
||||||
return extractStringLiteral(node.binLeft)
|
|
||||||
return ""
|
|
||||||
else: return ""
|
|
||||||
|
|
||||||
proc execSelect(db: LSMTree, astNode: Node): QueryResult =
|
|
||||||
result = QueryResult(columns: @["key", "value"], rows: @[])
|
|
||||||
|
|
||||||
var keyFilter = ""
|
|
||||||
if astNode.selWhere != nil and astNode.selWhere.whereExpr != nil:
|
|
||||||
let whereExpr = astNode.selWhere.whereExpr
|
|
||||||
keyFilter = extractStringLiteral(whereExpr)
|
|
||||||
|
|
||||||
if keyFilter != "":
|
|
||||||
# Point read
|
|
||||||
let (found, val) = db.get(keyFilter)
|
|
||||||
if found:
|
|
||||||
var row: seq[WireValue] = @[]
|
|
||||||
row.add(WireValue(kind: fkString, strVal: keyFilter))
|
|
||||||
row.add(WireValue(kind: fkBytes, bytesVal: val))
|
|
||||||
result.rows.add(row)
|
|
||||||
result.rowCount = 1
|
|
||||||
else:
|
|
||||||
# Full scan of memory tables
|
|
||||||
for entry in db.scanMemTable():
|
|
||||||
if entry.deleted:
|
|
||||||
continue
|
|
||||||
var row: seq[WireValue] = @[]
|
|
||||||
row.add(WireValue(kind: fkString, strVal: entry.key))
|
|
||||||
row.add(WireValue(kind: fkBytes, bytesVal: entry.value))
|
|
||||||
result.rows.add(row)
|
|
||||||
result.rowCount = result.rows.len
|
|
||||||
|
|
||||||
proc execInsert(db: LSMTree, query: string): QueryResult =
|
|
||||||
result = QueryResult()
|
|
||||||
# Manual parsing for simple INSERT: INSERT table { field := 'value' }
|
|
||||||
# We use the value as the key for simple KV semantics
|
|
||||||
let pattern = re"INSERT\s+(\w+)\s*\{\s*(\w+)\s*:=\s*'([^']+)'\s*\}"
|
|
||||||
var matches: array[3, string]
|
|
||||||
if query.match(pattern, matches):
|
|
||||||
let key = matches[2] # use the value as key
|
|
||||||
let value = matches[2]
|
|
||||||
db.put(key, cast[seq[byte]](value))
|
|
||||||
result.affectedRows = 1
|
|
||||||
else:
|
|
||||||
# Try simpler pattern: INSERT table { field := value }
|
|
||||||
let pattern2 = re"INSERT\s+(\w+)\s*\{\s*(\w+)\s*:=\s*(\w+)\s*\}"
|
|
||||||
var matches2: array[3, string]
|
|
||||||
if query.match(pattern2, matches2):
|
|
||||||
let key = matches2[2]
|
|
||||||
let value = matches2[2]
|
|
||||||
db.put(key, cast[seq[byte]](value))
|
|
||||||
result.affectedRows = 1
|
|
||||||
|
|
||||||
proc execDelete(db: LSMTree, astNode: Node): QueryResult =
|
|
||||||
result = QueryResult()
|
|
||||||
var keyFilter = ""
|
|
||||||
if astNode.delWhere != nil and astNode.delWhere.whereExpr != nil:
|
|
||||||
keyFilter = extractStringLiteral(astNode.delWhere.whereExpr)
|
|
||||||
if keyFilter != "":
|
|
||||||
db.delete(keyFilter)
|
|
||||||
result.affectedRows = 1
|
|
||||||
|
|
||||||
proc executeQuery(db: LSMTree, query: string): (bool, QueryResult, string) =
|
|
||||||
try:
|
try:
|
||||||
let tokens = tokenize(query)
|
let tokens = tokenize(query)
|
||||||
let astNode = parse(tokens)
|
let astNode = parse(tokens)
|
||||||
@@ -139,15 +66,17 @@ proc executeQuery(db: LSMTree, query: string): (bool, QueryResult, string) =
|
|||||||
|
|
||||||
let stmt = astNode.stmts[0]
|
let stmt = astNode.stmts[0]
|
||||||
case stmt.kind
|
case stmt.kind
|
||||||
of nkSelect:
|
of nkSelect, nkInsert, nkUpdate, nkDelete, nkCreateTable, nkDropTable,
|
||||||
let qr = execSelect(db, stmt)
|
nkCreateType, nkBeginTxn, nkCommitTxn, nkRollbackTxn, nkExplainStmt:
|
||||||
return (true, qr, "")
|
let (success, errMsg, affectedRows) = executor.executeQuery(ctx, astNode)
|
||||||
of nkInsert:
|
if success:
|
||||||
let qr = execInsert(db, query)
|
var qr = QueryResult(affectedRows: affectedRows, rowCount: affectedRows)
|
||||||
return (true, qr, "")
|
if stmt.kind == nkSelect:
|
||||||
of nkDelete:
|
qr.columns = @["key", "value"]
|
||||||
let qr = execDelete(db, stmt)
|
qr.rows = @[]
|
||||||
return (true, qr, "")
|
return (true, qr, "")
|
||||||
|
else:
|
||||||
|
return (false, QueryResult(), errMsg)
|
||||||
else:
|
else:
|
||||||
return (false, QueryResult(), "Unsupported statement type: " & $stmt.kind)
|
return (false, QueryResult(), "Unsupported statement type: " & $stmt.kind)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -237,7 +166,7 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
|
|||||||
let queryStr = readString(cast[seq[byte]](payload), pos)
|
let queryStr = readString(cast[seq[byte]](payload), pos)
|
||||||
echo "[", clientId, "] Query: ", queryStr
|
echo "[", clientId, "] Query: ", queryStr
|
||||||
|
|
||||||
let (success, result, errorMsg) = executeQuery(server.db, queryStr)
|
let (success, result, errorMsg) = executeQuery(server.db, server.ctx, queryStr)
|
||||||
if success:
|
if success:
|
||||||
if result.rows.len > 0:
|
if result.rows.len > 0:
|
||||||
let dataMsg = serializeResult(result, header.requestId)
|
let dataMsg = serializeResult(result, header.requestId)
|
||||||
|
|||||||
@@ -11,8 +11,15 @@ type
|
|||||||
nkCreateType
|
nkCreateType
|
||||||
nkDropType
|
nkDropType
|
||||||
nkAlterType
|
nkAlterType
|
||||||
|
nkCreateTable
|
||||||
|
nkDropTable
|
||||||
|
nkAlterTable
|
||||||
nkCreateIndex
|
nkCreateIndex
|
||||||
nkDropIndex
|
nkDropIndex
|
||||||
|
nkBeginTxn
|
||||||
|
nkCommitTxn
|
||||||
|
nkRollbackTxn
|
||||||
|
nkExplainStmt
|
||||||
|
|
||||||
# Clauses
|
# Clauses
|
||||||
nkFrom
|
nkFrom
|
||||||
@@ -63,10 +70,11 @@ type
|
|||||||
# Join
|
# Join
|
||||||
nkJoin
|
nkJoin
|
||||||
|
|
||||||
# Type definitions
|
# Type definitions / DDL
|
||||||
nkPropertyDef
|
nkPropertyDef
|
||||||
nkLinkDef
|
nkLinkDef
|
||||||
nkIndexDef
|
nkIndexDef
|
||||||
|
nkColumnDef
|
||||||
nkConstraintDef
|
nkConstraintDef
|
||||||
|
|
||||||
# Top-level
|
# Top-level
|
||||||
@@ -157,6 +165,41 @@ type
|
|||||||
of nkAlterType:
|
of nkAlterType:
|
||||||
atName*: string
|
atName*: string
|
||||||
atOps*: seq[Node]
|
atOps*: seq[Node]
|
||||||
|
of nkCreateTable:
|
||||||
|
crtName*: string
|
||||||
|
crtColumns*: seq[Node]
|
||||||
|
crtConstraints*: seq[Node]
|
||||||
|
crtIfNotExists*: bool
|
||||||
|
of nkDropTable:
|
||||||
|
drtName*: string
|
||||||
|
drtIfExists*: bool
|
||||||
|
of nkAlterTable:
|
||||||
|
altName*: string
|
||||||
|
altOps*: seq[Node]
|
||||||
|
of nkColumnDef:
|
||||||
|
cdName*: string
|
||||||
|
cdType*: string
|
||||||
|
cdConstraints*: seq[Node]
|
||||||
|
of nkConstraintDef:
|
||||||
|
cstName*: string
|
||||||
|
cstType*: string
|
||||||
|
cstExpr*: Node
|
||||||
|
cstColumns*: seq[string]
|
||||||
|
cstRefTable*: string
|
||||||
|
cstRefColumns*: seq[string]
|
||||||
|
cstOnDelete*: string
|
||||||
|
cstOnUpdate*: string
|
||||||
|
cstCheck*: Node
|
||||||
|
cstDefault*: Node
|
||||||
|
of nkBeginTxn:
|
||||||
|
btxnMode*: string
|
||||||
|
of nkCommitTxn:
|
||||||
|
ctxnChain*: bool
|
||||||
|
of nkRollbackTxn:
|
||||||
|
rtxnChain*: bool
|
||||||
|
of nkExplainStmt:
|
||||||
|
expStmt*: Node
|
||||||
|
expAnalyze*: bool
|
||||||
of nkCreateIndex:
|
of nkCreateIndex:
|
||||||
ciTarget*: string
|
ciTarget*: string
|
||||||
ciName*: string
|
ciName*: string
|
||||||
@@ -301,9 +344,6 @@ type
|
|||||||
idName*: string
|
idName*: string
|
||||||
idExpr*: Node
|
idExpr*: Node
|
||||||
idKind*: IndexKind
|
idKind*: IndexKind
|
||||||
of nkConstraintDef:
|
|
||||||
cdName*: string
|
|
||||||
cdExpr*: Node
|
|
||||||
of nkStatementList:
|
of nkStatementList:
|
||||||
stmts*: seq[Node]
|
stmts*: seq[Node]
|
||||||
|
|
||||||
@@ -314,5 +354,9 @@ proc newNode*(kind: NodeKind, line, col: int = 0): Node =
|
|||||||
of nkInsert: result.insFields = @[]; result.insValues = @[]
|
of nkInsert: result.insFields = @[]; result.insValues = @[]
|
||||||
of nkUpdate: result.updSet = @[]
|
of nkUpdate: result.updSet = @[]
|
||||||
of nkDelete: discard
|
of nkDelete: discard
|
||||||
|
of nkCreateTable: result.crtColumns = @[]; result.crtConstraints = @[]
|
||||||
|
of nkAlterTable: result.altOps = @[]
|
||||||
|
of nkColumnDef: result.cdConstraints = @[]
|
||||||
|
of nkConstraintDef: result.cstColumns = @[]; result.cstRefColumns = @[]
|
||||||
of nkStatementList: result.stmts = @[]
|
of nkStatementList: result.stmts = @[]
|
||||||
else: discard
|
else: discard
|
||||||
|
|||||||
@@ -0,0 +1,398 @@
|
|||||||
|
## BaraQL Executor — AST lowering, IR compilation, and execution
|
||||||
|
import std/strutils
|
||||||
|
import std/tables
|
||||||
|
import ast
|
||||||
|
import ir
|
||||||
|
import ../core/types
|
||||||
|
import ../storage/lsm
|
||||||
|
|
||||||
|
type
|
||||||
|
ExecutionContext* = ref object
|
||||||
|
db*: LSMTree
|
||||||
|
tables*: Table[string, TableDef] # table name -> definition
|
||||||
|
|
||||||
|
TableDef* = object
|
||||||
|
name*: string
|
||||||
|
columns*: seq[ColumnDef]
|
||||||
|
pkColumns*: seq[string]
|
||||||
|
|
||||||
|
ColumnDef* = object
|
||||||
|
name*: string
|
||||||
|
colType*: string
|
||||||
|
isPk*: bool
|
||||||
|
isNotNull*: bool
|
||||||
|
isUnique*: bool
|
||||||
|
defaultVal*: string
|
||||||
|
|
||||||
|
Row = Table[string, string]
|
||||||
|
|
||||||
|
proc newExecutionContext*(db: LSMTree): ExecutionContext =
|
||||||
|
ExecutionContext(db: db, tables: initTable[string, TableDef]())
|
||||||
|
|
||||||
|
proc execScan(ctx: ExecutionContext, table: string): seq[Row] =
|
||||||
|
## Full table scan via LSM-Tree memtable scan.
|
||||||
|
## Rows are stored as: "{table}.{key}" -> value
|
||||||
|
## For simple KV: key is the PK value, value is the serialized row
|
||||||
|
result = @[]
|
||||||
|
let prefix = table & "."
|
||||||
|
for entry in ctx.db.scanMemTable():
|
||||||
|
if entry.deleted:
|
||||||
|
continue
|
||||||
|
if not entry.key.startsWith(prefix):
|
||||||
|
continue
|
||||||
|
let rest = entry.key[prefix.len..^1]
|
||||||
|
var row: Table[string, string]
|
||||||
|
row["$key"] = rest
|
||||||
|
row["$value"] = cast[string](entry.value)
|
||||||
|
result.add(row)
|
||||||
|
|
||||||
|
proc execPointRead(ctx: ExecutionContext, table: string, key: string): seq[Row] =
|
||||||
|
## Point read from LSM-Tree
|
||||||
|
let fullKey = table & "." & key
|
||||||
|
let (found, val) = ctx.db.get(fullKey)
|
||||||
|
if found:
|
||||||
|
var row: Table[string, string]
|
||||||
|
row["$key"] = key
|
||||||
|
row["$value"] = cast[string](val)
|
||||||
|
return @[row]
|
||||||
|
return @[]
|
||||||
|
|
||||||
|
proc execInsert(ctx: ExecutionContext, table: string, fields: seq[string], values: seq[seq[string]]): int =
|
||||||
|
## Insert rows into LSM-Tree.
|
||||||
|
## Each row is stored as key=table.pk_value, value=<serialized row>
|
||||||
|
var count = 0
|
||||||
|
for rowVals in values:
|
||||||
|
var key = ""
|
||||||
|
var valStr = ""
|
||||||
|
for i, f in fields:
|
||||||
|
if i < rowVals.len:
|
||||||
|
if key == "":
|
||||||
|
key = f & "=" & rowVals[i]
|
||||||
|
else:
|
||||||
|
valStr &= f & "=" & rowVals[i]
|
||||||
|
if i < rowVals.len - 1:
|
||||||
|
valStr &= ","
|
||||||
|
let fullKey = table & "." & key
|
||||||
|
ctx.db.put(fullKey, cast[seq[byte]](valStr))
|
||||||
|
inc count
|
||||||
|
return count
|
||||||
|
|
||||||
|
proc execDelete(ctx: ExecutionContext, table: string, key: string): int =
|
||||||
|
let fullKey = table & "." & key
|
||||||
|
let (found, _) = ctx.db.get(fullKey)
|
||||||
|
if found:
|
||||||
|
ctx.db.delete(fullKey)
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# AST → IR Lowering
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc lowerExpr(node: Node): IRExpr =
|
||||||
|
if node == nil:
|
||||||
|
return nil
|
||||||
|
case node.kind
|
||||||
|
of nkIntLit:
|
||||||
|
result = IRExpr(kind: irekLiteral)
|
||||||
|
result.literal = IRLiteral(kind: vkInt64, int64Val: node.intVal)
|
||||||
|
of nkFloatLit:
|
||||||
|
result = IRExpr(kind: irekLiteral)
|
||||||
|
result.literal = IRLiteral(kind: vkFloat64, float64Val: node.floatVal)
|
||||||
|
of nkStringLit:
|
||||||
|
result = IRExpr(kind: irekLiteral)
|
||||||
|
result.literal = IRLiteral(kind: vkString, strVal: node.strVal)
|
||||||
|
of nkBoolLit:
|
||||||
|
result = IRExpr(kind: irekLiteral)
|
||||||
|
result.literal = IRLiteral(kind: vkBool, boolVal: node.boolVal)
|
||||||
|
of nkNullLit:
|
||||||
|
result = IRExpr(kind: irekLiteral)
|
||||||
|
result.literal = IRLiteral(kind: vkNull)
|
||||||
|
of nkIdent:
|
||||||
|
result = IRExpr(kind: irekField)
|
||||||
|
result.fieldPath = @[node.identName]
|
||||||
|
of nkPath:
|
||||||
|
result = IRExpr(kind: irekField)
|
||||||
|
result.fieldPath = node.pathParts
|
||||||
|
of nkBinOp:
|
||||||
|
result = IRExpr(kind: irekBinary)
|
||||||
|
var irOp: IROperator
|
||||||
|
case node.binOp
|
||||||
|
of bkAdd: irOp = irAdd
|
||||||
|
of bkSub: irOp = irSub
|
||||||
|
of bkMul: irOp = irMul
|
||||||
|
of bkDiv: irOp = irDiv
|
||||||
|
of bkMod: irOp = irMod
|
||||||
|
of bkEq: irOp = irEq
|
||||||
|
of bkNotEq: irOp = irNeq
|
||||||
|
of bkLt: irOp = irLt
|
||||||
|
of bkLtEq: irOp = irLte
|
||||||
|
of bkGt: irOp = irGt
|
||||||
|
of bkGtEq: irOp = irGte
|
||||||
|
of bkAnd: irOp = irAnd
|
||||||
|
of bkOr: irOp = irOr
|
||||||
|
else: irOp = irEq
|
||||||
|
result.binOp = irOp
|
||||||
|
result.binLeft = lowerExpr(node.binLeft)
|
||||||
|
result.binRight = lowerExpr(node.binRight)
|
||||||
|
of nkUnaryOp:
|
||||||
|
result = IRExpr(kind: irekUnary)
|
||||||
|
result.unOp = if node.unOp == ukNot: irNot else: irNot
|
||||||
|
result.unExpr = lowerExpr(node.unOperand)
|
||||||
|
of nkFuncCall:
|
||||||
|
result = IRExpr(kind: irekAggregate)
|
||||||
|
case node.funcName.toLower()
|
||||||
|
of "count": result.aggOp = irCount
|
||||||
|
of "sum": result.aggOp = irSum
|
||||||
|
of "avg": result.aggOp = irAvg
|
||||||
|
of "min": result.aggOp = irMin
|
||||||
|
of "max": result.aggOp = irMax
|
||||||
|
else: result = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkNull))
|
||||||
|
result.aggArgs = @[]
|
||||||
|
for arg in node.funcArgs:
|
||||||
|
result.aggArgs.add(lowerExpr(arg))
|
||||||
|
of nkIsExpr:
|
||||||
|
result = IRExpr(kind: irekUnary)
|
||||||
|
result.unOp = if node.isNegated: irIsNotNull else: irIsNull
|
||||||
|
result.unExpr = lowerExpr(node.isExpr)
|
||||||
|
of nkLikeExpr:
|
||||||
|
result = IRExpr(kind: irekBinary)
|
||||||
|
result.binOp = if node.likeCaseInsensitive: irILike else: irLike
|
||||||
|
result.binLeft = lowerExpr(node.likeExpr)
|
||||||
|
result.binRight = lowerExpr(node.likePattern)
|
||||||
|
of nkBetweenExpr:
|
||||||
|
result = IRExpr(kind: irekBinary)
|
||||||
|
result.binOp = irBetween
|
||||||
|
result.binLeft = lowerExpr(node.betweenExpr)
|
||||||
|
result.binRight = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkString, strVal: ""))
|
||||||
|
of nkInExpr:
|
||||||
|
result = IRExpr(kind: irekBinary)
|
||||||
|
result.binOp = irIn
|
||||||
|
result.binLeft = lowerExpr(node.inLeft)
|
||||||
|
result.binRight = lowerExpr(node.inRight)
|
||||||
|
of nkExists:
|
||||||
|
result = IRExpr(kind: irekExists)
|
||||||
|
else:
|
||||||
|
result = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkNull))
|
||||||
|
|
||||||
|
proc lowerSelect(node: Node): IRPlan =
|
||||||
|
result = IRPlan(kind: irpkScan)
|
||||||
|
if node.selFrom != nil and node.selFrom.fromTable.len > 0:
|
||||||
|
result.scanTable = node.selFrom.fromTable
|
||||||
|
result.scanAlias = node.selFrom.fromAlias
|
||||||
|
|
||||||
|
# WHERE → Filter
|
||||||
|
if node.selWhere != nil and node.selWhere.whereExpr != nil:
|
||||||
|
let filterPlan = IRPlan(kind: irpkFilter)
|
||||||
|
filterPlan.filterSource = result
|
||||||
|
filterPlan.filterCond = lowerExpr(node.selWhere.whereExpr)
|
||||||
|
result = filterPlan
|
||||||
|
|
||||||
|
# GROUP BY
|
||||||
|
if node.selGroupBy.len > 0:
|
||||||
|
let groupPlan = IRPlan(kind: irpkGroupBy)
|
||||||
|
groupPlan.groupSource = result
|
||||||
|
groupPlan.groupKeys = @[]
|
||||||
|
for g in node.selGroupBy:
|
||||||
|
groupPlan.groupKeys.add(lowerExpr(g))
|
||||||
|
groupPlan.groupAggs = @[]
|
||||||
|
if node.selHaving != nil:
|
||||||
|
groupPlan.groupHaving = lowerExpr(node.selHaving.havingExpr)
|
||||||
|
result = groupPlan
|
||||||
|
|
||||||
|
# SELECT → Project
|
||||||
|
let projectPlan = IRPlan(kind: irpkProject)
|
||||||
|
projectPlan.projectSource = result
|
||||||
|
projectPlan.projectExprs = @[]
|
||||||
|
projectPlan.projectAliases = @[]
|
||||||
|
for e in node.selResult:
|
||||||
|
projectPlan.projectExprs.add(lowerExpr(e))
|
||||||
|
if e.kind == nkIdent:
|
||||||
|
projectPlan.projectAliases.add(e.identName)
|
||||||
|
else:
|
||||||
|
projectPlan.projectAliases.add("")
|
||||||
|
result = projectPlan
|
||||||
|
|
||||||
|
# ORDER BY → Sort
|
||||||
|
if node.selOrderBy.len > 0:
|
||||||
|
let sortPlan = IRPlan(kind: irpkSort)
|
||||||
|
sortPlan.sortSource = result
|
||||||
|
sortPlan.sortExprs = @[]
|
||||||
|
sortPlan.sortDirs = @[]
|
||||||
|
for o in node.selOrderBy:
|
||||||
|
sortPlan.sortExprs.add(lowerExpr(o.orderByExpr))
|
||||||
|
sortPlan.sortDirs.add(o.orderByDir == sdAsc)
|
||||||
|
result = sortPlan
|
||||||
|
|
||||||
|
# LIMIT/OFFSET
|
||||||
|
if node.selLimit != nil or node.selOffset != nil:
|
||||||
|
let limitPlan = IRPlan(kind: irpkLimit)
|
||||||
|
limitPlan.limitSource = result
|
||||||
|
limitPlan.limitCount = if node.selLimit != nil and node.selLimit.limitExpr.kind == nkIntLit:
|
||||||
|
node.selLimit.limitExpr.intVal else: 0
|
||||||
|
limitPlan.limitOffset = if node.selOffset != nil and node.selOffset.offsetExpr.kind == nkIntLit:
|
||||||
|
node.selOffset.offsetExpr.intVal else: 0
|
||||||
|
result = limitPlan
|
||||||
|
|
||||||
|
proc lowerInsert(node: Node): IRPlan =
|
||||||
|
result = IRPlan(kind: irpkInsert)
|
||||||
|
result.insertTable = node.insTarget
|
||||||
|
result.insertFields = @[]
|
||||||
|
for f in node.insFields:
|
||||||
|
if f.kind == nkIdent:
|
||||||
|
result.insertFields.add(f.identName)
|
||||||
|
else:
|
||||||
|
result.insertFields.add("")
|
||||||
|
result.insertValues = @[]
|
||||||
|
for rowNode in node.insValues:
|
||||||
|
var rowVals: seq[IRExpr] = @[]
|
||||||
|
if rowNode.kind == nkArrayLit:
|
||||||
|
for v in rowNode.arrayElems:
|
||||||
|
rowVals.add(lowerExpr(v))
|
||||||
|
else:
|
||||||
|
rowVals.add(lowerExpr(rowNode))
|
||||||
|
result.insertValues.add(rowVals)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# IR Plan Execution
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc executePlan(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
|
||||||
|
if plan == nil:
|
||||||
|
return @[]
|
||||||
|
|
||||||
|
case plan.kind
|
||||||
|
of irpkScan:
|
||||||
|
result = execScan(ctx, plan.scanTable)
|
||||||
|
|
||||||
|
of irpkFilter:
|
||||||
|
let sourceRows = executePlan(ctx, plan.filterSource)
|
||||||
|
result = sourceRows # TODO: actual filter evaluation
|
||||||
|
|
||||||
|
of irpkProject:
|
||||||
|
let sourceRows = executePlan(ctx, plan.projectSource)
|
||||||
|
result = sourceRows
|
||||||
|
|
||||||
|
of irpkSort:
|
||||||
|
let sourceRows = executePlan(ctx, plan.sortSource)
|
||||||
|
result = sourceRows
|
||||||
|
|
||||||
|
of irpkLimit:
|
||||||
|
let sourceRows = executePlan(ctx, plan.limitSource)
|
||||||
|
var start = int(plan.limitOffset)
|
||||||
|
if start > sourceRows.len: start = sourceRows.len
|
||||||
|
var endIdx = start + int(plan.limitCount)
|
||||||
|
if endIdx > sourceRows.len or plan.limitCount == 0:
|
||||||
|
endIdx = sourceRows.len
|
||||||
|
result = sourceRows[start..<endIdx]
|
||||||
|
|
||||||
|
of irpkGroupBy:
|
||||||
|
result = executePlan(ctx, plan.groupSource)
|
||||||
|
|
||||||
|
of irpkJoin:
|
||||||
|
result = executePlan(ctx, plan.joinLeft)
|
||||||
|
|
||||||
|
else:
|
||||||
|
result = @[]
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# High-level execute function
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc executeQuery*(ctx: ExecutionContext, astNode: Node): (bool, string, int) =
|
||||||
|
## Execute a parsed AST statement against the execution context.
|
||||||
|
## Returns (success, errorMessage, affectedRows)
|
||||||
|
if astNode == nil or astNode.stmts.len == 0:
|
||||||
|
return (true, "", 0)
|
||||||
|
|
||||||
|
let stmt = astNode.stmts[0]
|
||||||
|
case stmt.kind
|
||||||
|
of nkSelect:
|
||||||
|
let plan = lowerSelect(stmt)
|
||||||
|
let rows = executePlan(ctx, plan)
|
||||||
|
return (true, "", rows.len)
|
||||||
|
|
||||||
|
of nkInsert:
|
||||||
|
var fields: seq[string] = @[]
|
||||||
|
for f in stmt.insFields:
|
||||||
|
if f.kind == nkIdent:
|
||||||
|
fields.add(f.identName)
|
||||||
|
else:
|
||||||
|
fields.add("")
|
||||||
|
|
||||||
|
var values: seq[seq[string]] = @[]
|
||||||
|
for rowNode in stmt.insValues:
|
||||||
|
var row: seq[string] = @[]
|
||||||
|
if rowNode.kind == nkArrayLit:
|
||||||
|
for v in rowNode.arrayElems:
|
||||||
|
if v.kind == nkStringLit: row.add(v.strVal)
|
||||||
|
elif v.kind == nkIntLit: row.add($v.intVal)
|
||||||
|
elif v.kind == nkFloatLit: row.add($v.floatVal)
|
||||||
|
elif v.kind == nkBoolLit: row.add($v.boolVal)
|
||||||
|
elif v.kind == nkNullLit: row.add("NULL")
|
||||||
|
else: row.add("")
|
||||||
|
else:
|
||||||
|
if rowNode.kind == nkStringLit: row.add(rowNode.strVal)
|
||||||
|
elif rowNode.kind == nkIntLit: row.add($rowNode.intVal)
|
||||||
|
else: row.add("")
|
||||||
|
values.add(row)
|
||||||
|
|
||||||
|
let count = execInsert(ctx, stmt.insTarget, fields, values)
|
||||||
|
return (true, "", count)
|
||||||
|
|
||||||
|
of nkUpdate:
|
||||||
|
return (true, "", 0)
|
||||||
|
|
||||||
|
of nkDelete:
|
||||||
|
var key = ""
|
||||||
|
if stmt.delWhere != nil and stmt.delWhere.whereExpr != nil:
|
||||||
|
# Extract simple WHERE key = 'value'
|
||||||
|
let w = stmt.delWhere.whereExpr
|
||||||
|
if w.kind == nkBinOp and w.binOp == bkEq:
|
||||||
|
if w.binLeft.kind == nkIdent and w.binRight.kind == nkStringLit:
|
||||||
|
key = w.binLeft.identName & "=" & w.binRight.strVal
|
||||||
|
let count = execDelete(ctx, stmt.delTarget, key)
|
||||||
|
return (true, "", count)
|
||||||
|
|
||||||
|
of nkCreateTable:
|
||||||
|
var tbl = TableDef(name: stmt.crtName, columns: @[], pkColumns: @[])
|
||||||
|
for col in stmt.crtColumns:
|
||||||
|
if col.kind == nkColumnDef:
|
||||||
|
var colDef = ColumnDef(name: col.cdName, colType: col.cdType)
|
||||||
|
for cst in col.cdConstraints:
|
||||||
|
if cst.kind == nkConstraintDef:
|
||||||
|
case cst.cstType
|
||||||
|
of "pkey":
|
||||||
|
colDef.isPk = true
|
||||||
|
tbl.pkColumns.add(col.cdName)
|
||||||
|
of "notnull": colDef.isNotNull = true
|
||||||
|
of "unique": colDef.isUnique = true
|
||||||
|
of "default":
|
||||||
|
if cst.cstDefault != nil and cst.cstDefault.kind == nkStringLit:
|
||||||
|
colDef.defaultVal = cst.cstDefault.strVal
|
||||||
|
else: discard
|
||||||
|
tbl.columns.add(colDef)
|
||||||
|
ctx.tables[stmt.crtName] = tbl
|
||||||
|
return (true, "", 0)
|
||||||
|
|
||||||
|
of nkDropTable:
|
||||||
|
ctx.tables.del(stmt.drtName)
|
||||||
|
return (true, "", 0)
|
||||||
|
|
||||||
|
of nkBeginTxn:
|
||||||
|
return (true, "", 0)
|
||||||
|
|
||||||
|
of nkCommitTxn:
|
||||||
|
return (true, "", 0)
|
||||||
|
|
||||||
|
of nkRollbackTxn:
|
||||||
|
return (true, "", 0)
|
||||||
|
|
||||||
|
of nkCreateType:
|
||||||
|
return (true, "", 0)
|
||||||
|
|
||||||
|
of nkExplainStmt:
|
||||||
|
return (true, "", 0)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return (false, "Unsupported statement type: " & $stmt.kind, 0)
|
||||||
@@ -75,6 +75,21 @@ type
|
|||||||
tkLike
|
tkLike
|
||||||
tkILike
|
tkILike
|
||||||
tkReturning
|
tkReturning
|
||||||
|
tkPrimary
|
||||||
|
tkKey
|
||||||
|
tkForeign
|
||||||
|
tkReferences
|
||||||
|
tkCascade
|
||||||
|
tkUnique
|
||||||
|
tkCheck
|
||||||
|
tkDefault
|
||||||
|
tkAdd
|
||||||
|
tkColumn
|
||||||
|
tkRename
|
||||||
|
tkBegin
|
||||||
|
tkCommit
|
||||||
|
tkRollback
|
||||||
|
tkExplain
|
||||||
tkCount
|
tkCount
|
||||||
tkSum
|
tkSum
|
||||||
tkAvg
|
tkAvg
|
||||||
@@ -203,6 +218,21 @@ const keywords*: Table[string, TokenKind] = {
|
|||||||
"like": tkLike,
|
"like": tkLike,
|
||||||
"ilike": tkILike,
|
"ilike": tkILike,
|
||||||
"returning": tkReturning,
|
"returning": tkReturning,
|
||||||
|
"primary": tkPrimary,
|
||||||
|
"key": tkKey,
|
||||||
|
"foreign": tkForeign,
|
||||||
|
"references": tkReferences,
|
||||||
|
"cascade": tkCascade,
|
||||||
|
"unique": tkUnique,
|
||||||
|
"check": tkCheck,
|
||||||
|
"default": tkDefault,
|
||||||
|
"add": tkAdd,
|
||||||
|
"column": tkColumn,
|
||||||
|
"rename": tkRename,
|
||||||
|
"begin": tkBegin,
|
||||||
|
"commit": tkCommit,
|
||||||
|
"rollback": tkRollback,
|
||||||
|
"explain": tkExplain,
|
||||||
"count": tkCount,
|
"count": tkCount,
|
||||||
"sum": tkSum,
|
"sum": tkSum,
|
||||||
"avg": tkAvg,
|
"avg": tkAvg,
|
||||||
|
|||||||
+267
-12
@@ -36,6 +36,8 @@ proc match(p: var Parser, kind: TokenKind): bool =
|
|||||||
|
|
||||||
proc parseExpr(p: var Parser): Node
|
proc parseExpr(p: var Parser): Node
|
||||||
proc parseSelect(p: var Parser): Node
|
proc parseSelect(p: var Parser): Node
|
||||||
|
proc parseStatement*(p: var Parser): Node
|
||||||
|
proc parseCreateType(p: var Parser): Node
|
||||||
|
|
||||||
proc parsePrimary(p: var Parser): Node =
|
proc parsePrimary(p: var Parser): Node =
|
||||||
let tok = p.peek()
|
let tok = p.peek()
|
||||||
@@ -405,8 +407,50 @@ proc parseSelect(p: var Parser): Node =
|
|||||||
|
|
||||||
proc parseInsert(p: var Parser): Node =
|
proc parseInsert(p: var Parser): Node =
|
||||||
let tok = p.expect(tkInsert)
|
let tok = p.expect(tkInsert)
|
||||||
|
discard p.match(tkInto) # optional INTO
|
||||||
let target = p.expect(tkIdent).value
|
let target = p.expect(tkIdent).value
|
||||||
result = Node(kind: nkInsert, insTarget: target, line: tok.line, col: tok.col)
|
result = Node(kind: nkInsert, insTarget: target, line: tok.line, col: tok.col)
|
||||||
|
result.insFields = @[]
|
||||||
|
result.insValues = @[]
|
||||||
|
result.insReturning = @[]
|
||||||
|
|
||||||
|
# Parse column list: (col1, col2, ...)
|
||||||
|
if p.peek().kind == tkLParen:
|
||||||
|
discard p.advance()
|
||||||
|
result.insFields.add(p.parseExpr())
|
||||||
|
while p.match(tkComma):
|
||||||
|
result.insFields.add(p.parseExpr())
|
||||||
|
discard p.expect(tkRParen)
|
||||||
|
|
||||||
|
# Parse VALUES
|
||||||
|
if p.match(tkValues):
|
||||||
|
while true:
|
||||||
|
var row: seq[Node] = @[]
|
||||||
|
if p.peek().kind == tkLParen:
|
||||||
|
discard p.advance()
|
||||||
|
row.add(p.parseExpr())
|
||||||
|
while p.match(tkComma):
|
||||||
|
row.add(p.parseExpr())
|
||||||
|
discard p.expect(tkRParen)
|
||||||
|
else:
|
||||||
|
row.add(p.parseExpr())
|
||||||
|
result.insValues.add(Node(kind: nkArrayLit, arrayElems: row))
|
||||||
|
if p.peek().kind != tkComma:
|
||||||
|
break
|
||||||
|
discard p.advance()
|
||||||
|
|
||||||
|
# Parse ON CONFLICT
|
||||||
|
if p.peek().kind == tkOn:
|
||||||
|
discard p.advance()
|
||||||
|
if p.peek().kind == tkIdent and p.peek().value.toLower() == "conflict":
|
||||||
|
discard p.advance()
|
||||||
|
result.insConflict = p.parseExpr()
|
||||||
|
|
||||||
|
# Parse RETURNING
|
||||||
|
if p.match(tkReturning):
|
||||||
|
result.insReturning.add(p.parseExpr())
|
||||||
|
while p.match(tkComma):
|
||||||
|
result.insReturning.add(p.parseExpr())
|
||||||
|
|
||||||
proc parseUpdate(p: var Parser): Node =
|
proc parseUpdate(p: var Parser): Node =
|
||||||
let tok = p.expect(tkUpdate)
|
let tok = p.expect(tkUpdate)
|
||||||
@@ -429,6 +473,11 @@ proc parseUpdate(p: var Parser): Node =
|
|||||||
binRight: v))
|
binRight: v))
|
||||||
if p.match(tkWhere):
|
if p.match(tkWhere):
|
||||||
result.updWhere = Node(kind: nkWhere, whereExpr: p.parseExpr())
|
result.updWhere = Node(kind: nkWhere, whereExpr: p.parseExpr())
|
||||||
|
if p.match(tkReturning):
|
||||||
|
result.updReturning = @[]
|
||||||
|
result.updReturning.add(p.parseExpr())
|
||||||
|
while p.match(tkComma):
|
||||||
|
result.updReturning.add(p.parseExpr())
|
||||||
|
|
||||||
proc parseDelete(p: var Parser): Node =
|
proc parseDelete(p: var Parser): Node =
|
||||||
let tok = p.expect(tkDelete)
|
let tok = p.expect(tkDelete)
|
||||||
@@ -437,25 +486,25 @@ proc parseDelete(p: var Parser): Node =
|
|||||||
result = Node(kind: nkDelete, delTarget: target, line: tok.line, col: tok.col)
|
result = Node(kind: nkDelete, delTarget: target, line: tok.line, col: tok.col)
|
||||||
if p.match(tkWhere):
|
if p.match(tkWhere):
|
||||||
result.delWhere = Node(kind: nkWhere, whereExpr: p.parseExpr())
|
result.delWhere = Node(kind: nkWhere, whereExpr: p.parseExpr())
|
||||||
|
if p.match(tkReturning):
|
||||||
|
result.delReturning = @[]
|
||||||
|
result.delReturning.add(p.parseExpr())
|
||||||
|
while p.match(tkComma):
|
||||||
|
result.delReturning.add(p.parseExpr())
|
||||||
|
|
||||||
proc parseCreateType(p: var Parser): Node =
|
proc parseCreateType(p: var Parser): Node =
|
||||||
let tok = p.expect(tkCreate)
|
let tok = p.expect(tkCreate)
|
||||||
discard p.expect(tkType)
|
discard p.expect(tkType)
|
||||||
let name = p.expect(tkIdent).value
|
let name = p.expect(tkIdent).value
|
||||||
result = Node(kind: nkCreateType, ctName: name, line: tok.line, col: tok.col)
|
result = Node(kind: nkCreateType, ctName: name, line: tok.line, col: tok.col)
|
||||||
# Parse bases (EXTENDING)
|
|
||||||
result.ctBases = @[]
|
result.ctBases = @[]
|
||||||
if p.match(tkIdent): # "extending" keyword mapped to ident
|
if p.match(tkIdent):
|
||||||
# Check if the ident is "extending"
|
|
||||||
# For now, just accept bases in braces
|
|
||||||
discard
|
discard
|
||||||
# Parse body
|
|
||||||
if p.match(tkLBrace):
|
if p.match(tkLBrace):
|
||||||
result.ctProperties = @[]
|
result.ctProperties = @[]
|
||||||
result.ctLinks = @[]
|
result.ctLinks = @[]
|
||||||
while p.peek().kind != tkRbrace and p.peek().kind != tkEof:
|
while p.peek().kind != tkRbrace and p.peek().kind != tkEof:
|
||||||
discard p.match(tkComma) # optional comma separator
|
discard p.match(tkComma)
|
||||||
# Parse property or link
|
|
||||||
var isRequired = false
|
var isRequired = false
|
||||||
var isMulti = false
|
var isMulti = false
|
||||||
if p.peek().kind == tkRequired:
|
if p.peek().kind == tkRequired:
|
||||||
@@ -464,11 +513,9 @@ proc parseCreateType(p: var Parser): Node =
|
|||||||
if p.peek().kind == tkMulti:
|
if p.peek().kind == tkMulti:
|
||||||
discard p.advance()
|
discard p.advance()
|
||||||
isMulti = true
|
isMulti = true
|
||||||
|
|
||||||
let fieldTok = p.expect(tkIdent)
|
let fieldTok = p.expect(tkIdent)
|
||||||
# Check for link or property
|
if p.peek().kind == tkArrow:
|
||||||
if p.peek().kind == tkArrow: # -> means link
|
discard p.advance()
|
||||||
discard p.advance() # consume ->
|
|
||||||
let target = p.expect(tkIdent).value
|
let target = p.expect(tkIdent).value
|
||||||
result.ctLinks.add(Node(kind: nkLinkDef,
|
result.ctLinks.add(Node(kind: nkLinkDef,
|
||||||
ldName: fieldTok.value, ldTarget: target,
|
ldName: fieldTok.value, ldTarget: target,
|
||||||
@@ -484,13 +531,221 @@ proc parseCreateType(p: var Parser): Node =
|
|||||||
discard p.match(tkSemicolon)
|
discard p.match(tkSemicolon)
|
||||||
discard p.expect(tkRbrace)
|
discard p.expect(tkRbrace)
|
||||||
|
|
||||||
|
proc parseCreateTable(p: var Parser): Node =
|
||||||
|
let tok = p.expect(tkCreate)
|
||||||
|
result = Node(kind: nkCreateTable, line: tok.line, col: tok.col)
|
||||||
|
result.crtColumns = @[]
|
||||||
|
result.crtConstraints = @[]
|
||||||
|
|
||||||
|
# Optional IF NOT EXISTS
|
||||||
|
if p.peek().kind == tkIf:
|
||||||
|
discard p.advance()
|
||||||
|
discard p.expect(tkNot)
|
||||||
|
discard p.expect(tkExists)
|
||||||
|
result.crtIfNotExists = true
|
||||||
|
|
||||||
|
discard p.expect(tkTable)
|
||||||
|
if p.peek().kind == tkIdent and p.peek().value.toLower() == "if":
|
||||||
|
discard p.advance() # if
|
||||||
|
discard p.expect(tkNot)
|
||||||
|
discard p.expect(tkExists)
|
||||||
|
result.crtIfNotExists = true
|
||||||
|
|
||||||
|
result.crtName = p.expect(tkIdent).value
|
||||||
|
discard p.expect(tkLParen)
|
||||||
|
|
||||||
|
while p.peek().kind != tkRParen and p.peek().kind != tkEof:
|
||||||
|
discard p.match(tkComma)
|
||||||
|
|
||||||
|
# Check if this is a table-level constraint
|
||||||
|
if p.peek().kind in {tkPrimary, tkForeign, tkUnique, tkCheck}:
|
||||||
|
let cst = Node(kind: nkConstraintDef)
|
||||||
|
cst.cstColumns = @[]; cst.cstRefColumns = @[]
|
||||||
|
if p.match(tkPrimary):
|
||||||
|
discard p.expect(tkKey)
|
||||||
|
cst.cstType = "pkey"
|
||||||
|
if p.peek().kind == tkLParen:
|
||||||
|
discard p.advance()
|
||||||
|
cst.cstColumns.add(p.expect(tkIdent).value)
|
||||||
|
while p.match(tkComma):
|
||||||
|
cst.cstColumns.add(p.expect(tkIdent).value)
|
||||||
|
discard p.expect(tkRParen)
|
||||||
|
elif p.match(tkForeign):
|
||||||
|
discard p.expect(tkKey)
|
||||||
|
cst.cstType = "fkey"
|
||||||
|
if p.peek().kind == tkLParen:
|
||||||
|
discard p.advance()
|
||||||
|
cst.cstColumns.add(p.expect(tkIdent).value)
|
||||||
|
while p.match(tkComma):
|
||||||
|
cst.cstColumns.add(p.expect(tkIdent).value)
|
||||||
|
discard p.expect(tkRParen)
|
||||||
|
discard p.expect(tkReferences)
|
||||||
|
cst.cstRefTable = p.expect(tkIdent).value
|
||||||
|
if p.peek().kind == tkLParen:
|
||||||
|
discard p.advance()
|
||||||
|
cst.cstRefColumns.add(p.expect(tkIdent).value)
|
||||||
|
while p.match(tkComma):
|
||||||
|
cst.cstRefColumns.add(p.expect(tkIdent).value)
|
||||||
|
discard p.expect(tkRParen)
|
||||||
|
if p.peek().kind == tkOn:
|
||||||
|
discard p.advance()
|
||||||
|
if p.peek().kind == tkDelete:
|
||||||
|
discard p.advance()
|
||||||
|
if p.peek().kind == tkCascade:
|
||||||
|
discard p.advance()
|
||||||
|
cst.cstOnDelete = "CASCADE"
|
||||||
|
elif p.peek().kind == tkSet:
|
||||||
|
discard p.advance()
|
||||||
|
discard p.match(tkNull)
|
||||||
|
cst.cstOnDelete = "SET NULL"
|
||||||
|
elif p.match(tkUnique):
|
||||||
|
cst.cstType = "unique"
|
||||||
|
if p.peek().kind == tkLParen:
|
||||||
|
discard p.advance()
|
||||||
|
cst.cstColumns.add(p.expect(tkIdent).value)
|
||||||
|
while p.match(tkComma):
|
||||||
|
cst.cstColumns.add(p.expect(tkIdent).value)
|
||||||
|
discard p.expect(tkRParen)
|
||||||
|
elif p.match(tkCheck):
|
||||||
|
cst.cstType = "check"
|
||||||
|
discard p.expect(tkLParen)
|
||||||
|
cst.cstCheck = p.parseExpr()
|
||||||
|
discard p.expect(tkRParen)
|
||||||
|
result.crtConstraints.add(cst)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parse column definition
|
||||||
|
let colName = p.expect(tkIdent).value
|
||||||
|
var colType = ""
|
||||||
|
if p.peek().kind == tkIdent:
|
||||||
|
colType = p.advance().value.toUpper()
|
||||||
|
if p.peek().kind == tkLParen:
|
||||||
|
discard p.advance()
|
||||||
|
let size = p.expect(tkIntLit).value
|
||||||
|
colType &= "(" & size & ")"
|
||||||
|
discard p.expect(tkRParen)
|
||||||
|
|
||||||
|
let colDef = Node(kind: nkColumnDef, cdName: colName, cdType: colType)
|
||||||
|
colDef.cdConstraints = @[]
|
||||||
|
|
||||||
|
# Parse column constraints
|
||||||
|
while p.peek().kind in {tkPrimary, tkNot, tkNull, tkUnique, tkCheck, tkDefault, tkReferences}:
|
||||||
|
let cst = Node(kind: nkConstraintDef)
|
||||||
|
cst.cstColumns = @[colName]; cst.cstRefColumns = @[]
|
||||||
|
if p.match(tkPrimary):
|
||||||
|
discard p.expect(tkKey)
|
||||||
|
cst.cstType = "pkey"
|
||||||
|
elif p.match(tkNot):
|
||||||
|
discard p.expect(tkNull)
|
||||||
|
cst.cstType = "notnull"
|
||||||
|
elif p.match(tkNull):
|
||||||
|
cst.cstType = "null"
|
||||||
|
elif p.match(tkUnique):
|
||||||
|
cst.cstType = "unique"
|
||||||
|
elif p.match(tkCheck):
|
||||||
|
cst.cstType = "check"
|
||||||
|
discard p.expect(tkLParen)
|
||||||
|
cst.cstCheck = p.parseExpr()
|
||||||
|
discard p.expect(tkRParen)
|
||||||
|
elif p.match(tkDefault):
|
||||||
|
cst.cstType = "default"
|
||||||
|
cst.cstDefault = p.parseExpr()
|
||||||
|
elif p.match(tkReferences):
|
||||||
|
cst.cstType = "fkey"
|
||||||
|
cst.cstRefTable = p.expect(tkIdent).value
|
||||||
|
if p.peek().kind == tkLParen:
|
||||||
|
discard p.advance()
|
||||||
|
cst.cstRefColumns.add(p.expect(tkIdent).value)
|
||||||
|
while p.match(tkComma):
|
||||||
|
cst.cstRefColumns.add(p.expect(tkIdent).value)
|
||||||
|
discard p.expect(tkRParen)
|
||||||
|
if p.peek().kind == tkOn:
|
||||||
|
discard p.advance()
|
||||||
|
if p.peek().kind == tkDelete:
|
||||||
|
discard p.advance()
|
||||||
|
if p.peek().kind == tkCascade:
|
||||||
|
discard p.advance()
|
||||||
|
cst.cstOnDelete = "CASCADE"
|
||||||
|
colDef.cdConstraints.add(cst)
|
||||||
|
result.crtColumns.add(colDef)
|
||||||
|
|
||||||
|
discard p.expect(tkRParen)
|
||||||
|
|
||||||
|
proc parseDropTable(p: var Parser): Node =
|
||||||
|
let tok = p.expect(tkDrop)
|
||||||
|
result = Node(kind: nkDropTable, line: tok.line, col: tok.col)
|
||||||
|
if p.peek().kind == tkTable:
|
||||||
|
discard p.advance()
|
||||||
|
if p.peek().kind == tkIdent and p.peek().value.toLower() == "if":
|
||||||
|
discard p.advance()
|
||||||
|
discard p.expect(tkExists)
|
||||||
|
result.drtIfExists = true
|
||||||
|
result.drtName = p.expect(tkIdent).value
|
||||||
|
|
||||||
|
proc parseAlterTable(p: var Parser): Node =
|
||||||
|
let tok = p.expect(tkAlter)
|
||||||
|
discard p.expect(tkTable)
|
||||||
|
result = Node(kind: nkAlterTable, line: tok.line, col: tok.col)
|
||||||
|
result.altName = p.expect(tkIdent).value
|
||||||
|
result.altOps = @[]
|
||||||
|
discard p.match(tkAdd) # or tkRename
|
||||||
|
|
||||||
|
proc parseBeginTxn(p: var Parser): Node =
|
||||||
|
let tok = p.expect(tkBegin)
|
||||||
|
result = Node(kind: nkBeginTxn, line: tok.line, col: tok.col)
|
||||||
|
discard p.match(tkIdent) # optional TRANSACTION / WORK
|
||||||
|
result.btxnMode = ""
|
||||||
|
|
||||||
|
proc parseCommitTxn(p: var Parser): Node =
|
||||||
|
let tok = p.expect(tkCommit)
|
||||||
|
result = Node(kind: nkCommitTxn, line: tok.line, col: tok.col)
|
||||||
|
discard p.match(tkIdent) # optional TRANSACTION / WORK
|
||||||
|
|
||||||
|
proc parseRollbackTxn(p: var Parser): Node =
|
||||||
|
let tok = p.expect(tkRollback)
|
||||||
|
result = Node(kind: nkRollbackTxn, line: tok.line, col: tok.col)
|
||||||
|
discard p.match(tkIdent) # optional TRANSACTION / WORK
|
||||||
|
|
||||||
|
proc parseExplain(p: var Parser): Node =
|
||||||
|
let tok = p.expect(tkExplain)
|
||||||
|
result = Node(kind: nkExplainStmt, line: tok.line, col: tok.col)
|
||||||
|
if p.peek().kind == tkIdent and p.peek().value.toLower() == "analyze":
|
||||||
|
discard p.advance()
|
||||||
|
result.expAnalyze = true
|
||||||
|
result.expStmt = p.parseStatement()
|
||||||
|
|
||||||
proc parseStatement*(p: var Parser): Node =
|
proc parseStatement*(p: var Parser): Node =
|
||||||
case p.peek().kind
|
case p.peek().kind
|
||||||
of tkWith, tkSelect: p.parseSelect()
|
of tkWith, tkSelect: p.parseSelect()
|
||||||
of tkInsert: p.parseInsert()
|
of tkInsert: p.parseInsert()
|
||||||
of tkUpdate: p.parseUpdate()
|
of tkUpdate: p.parseUpdate()
|
||||||
of tkDelete: p.parseDelete()
|
of tkDelete: p.parseDelete()
|
||||||
of tkCreate: p.parseCreateType()
|
of tkCreate:
|
||||||
|
if p.pos + 1 < p.tokens.len:
|
||||||
|
let next = p.tokens[p.pos + 1]
|
||||||
|
if next.kind == tkTable or
|
||||||
|
(next.kind == tkIdent and next.value.toLower() == "if"):
|
||||||
|
p.parseCreateTable()
|
||||||
|
else:
|
||||||
|
p.parseCreateType()
|
||||||
|
else:
|
||||||
|
p.parseCreateType()
|
||||||
|
of tkDrop:
|
||||||
|
if p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind == tkTable:
|
||||||
|
p.parseDropTable()
|
||||||
|
else:
|
||||||
|
let tok = p.advance()
|
||||||
|
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||||
|
of tkAlter:
|
||||||
|
if p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind == tkTable:
|
||||||
|
p.parseAlterTable()
|
||||||
|
else:
|
||||||
|
let tok = p.advance()
|
||||||
|
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||||
|
of tkBegin: p.parseBeginTxn()
|
||||||
|
of tkCommit: p.parseCommitTxn()
|
||||||
|
of tkRollback: p.parseRollbackTxn()
|
||||||
|
of tkExplain: p.parseExplain()
|
||||||
else:
|
else:
|
||||||
let tok = p.advance()
|
let tok = p.advance()
|
||||||
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||||
|
|||||||
Reference in New Issue
Block a user