diff --git a/PLAN.md b/PLAN.md index 02e8495..7b2252f 100644 --- a/PLAN.md +++ b/PLAN.md @@ -33,224 +33,206 @@ BaraDB as a production-ready database for: --- -## Phase 0: Pipeline Integration & Parser Completion (2–3 weeks) **← IN PROGRESS** +## Phase 0: Pipeline Integration & Parser Completion ✅ DONE ### 0.1 Complete DML parser (INSERT/UPDATE/DELETE) -- INSERT with column list: `INSERT INTO t (c1, c2) VALUES (v1, v2)` -- INSERT with RETURNING clause -- UPDATE with RETURNING clause -- DELETE with RETURNING clause -- Multiple VALUES rows: `VALUES (v1), (v2), ...` +- INSERT with column list: `INSERT INTO t (c1, c2) VALUES (v1, v2)` ✅ +- INSERT with RETURNING clause ✅ (parsed, executor returns data) +- UPDATE with RETURNING clause ✅ (parsed) +- DELETE with RETURNING clause ✅ (parsed) +- Multiple VALUES rows: `VALUES (v1), (v2), ...` ✅ ### 0.2 Add SQL DDL to parser -- `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 +- `CREATE TABLE` with column definitions, constraints (PK, FK, UNIQUE, NOT NULL, CHECK, DEFAULT) ✅ +- `ALTER TABLE` ❌ **STUB** — parsed but no operations populated, no executor +- `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 +- SQL table catalog (separate from EdgeQL type system) ✅ +- Store schema in LSM-Tree (`_schema:migrations:*`) ✅ +- Column type enforcement during INSERT ❌ **Types parsed but not enforced** +- 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 +- Convert Select AST nodes to IR plans (scan → filter → project → sort → limit) ✅ +- Convert Insert AST nodes to IR plans ✅ +- Convert Update/Delete AST nodes to IR plans ❌ **Bypassed — direct execution** +- Convert CTE AST nodes to IR plans ❌ **Lowering exists but CTE execution not wired** +- Lower JOINs to IR join nodes ❌ **Parsed but not lowered** ### 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 +- Execute StorageOp tree against LSM-Tree ✅ (via executePlan) +- sokScan: full table scan via `scanMemTable()` ✅ +- sokPointRead: key-based lookup ✅ +- sokFilter: evaluate IR expressions against rows ✅ **FIXED** +- sokProject: column selection ✅ +- sokSort: in-memory sort ✅ **FIXED** +- sokLimit: slice results ✅ ### 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 +- Replace `execSelect/execInsert/execDelete` with pipeline-based execution ✅ +- Server flow: lex → parse → AST→IR lower → executePlan → LSM ✅ +- Keep backward-compatible wire protocol ✅ +- All 56 existing tests still pass ✅ --- -## Phase 1: Schema & Indexes (2–3 weeks) +## Phase 1: Schema & Indexes ✅ MOSTLY DONE ### 1.1 SQL type system -- `INTEGER`, `BIGINT`, `SMALLINT`, `SERIAL` (auto-increment on INSERT) -- `VARCHAR(n)`, `TEXT` -- `BOOLEAN` -- `TIMESTAMP`, `DATE` (ISO 8601) -- `JSON`, `JSONB` -- `UUID` (v4 generation) -- `NUMERIC(p,s)`, `DOUBLE PRECISION`, `REAL` +- `INTEGER`, `BIGINT`, `SMALLINT`, `SERIAL` ❌ **Types stored as strings, not enforced** +- `VARCHAR(n)`, `TEXT` ❌ **Same** +- `BOOLEAN` ❌ **Same** +- `TIMESTAMP`, `DATE` (ISO 8601) ❌ **Same** +- `JSON`, `JSONB` ❌ **Same** +- `UUID` (v4 generation) ❌ **Same** +- `NUMERIC(p,s)`, `DOUBLE PRECISION`, `REAL` ❌ **Same** ### 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 +- PRIMARY KEY: unique index + NOT NULL ✅ +- FOREIGN KEY + ON DELETE CASCADE/SET NULL/RESTRICT ❌ **Parsed but not enforced** +- UNIQUE: unique index ✅ (uses B-Tree index) +- NOT NULL: check on INSERT ✅ +- CHECK: evaluate expression on INSERT/UPDATE ❌ **Parsed but not evaluated** +- DEFAULT: fill missing values on INSERT ✅ ### 1.3 B-Tree index integration -- `CREATE INDEX idx_name ON table(column)` -- `CREATE UNIQUE INDEX` -- B-Tree indexes created per table column -- Query planner uses B-Tree for WHERE clauses on indexed columns -- Range scans via B-Tree leaf linked list +- `CREATE INDEX idx_name ON table(column)` ❌ **No parser/executor** +- `CREATE UNIQUE INDEX` ❌ **No parser/executor** +- B-Tree indexes created per PK/UNIQUE column ✅ +- Query planner uses B-Tree for WHERE clauses ✅ (point reads) +- Range scans via B-Tree leaf linked list ❌ **Not implemented** ### 1.4 Query planner -- Choose index scan vs full scan based on WHERE clause -- Multi-column index support -- Covering index optimization -- `EXPLAIN` output with cost estimates -- Adaptive query reoptimization (wire up `adaptive.nim`) +- Choose index scan vs full scan based on WHERE clause ✅ +- Multi-column index support ❌ +- Covering index optimization ❌ +- `EXPLAIN` output with cost estimates ✅ **FIXED — now returns plan string** +- Adaptive query reoptimization ❌ **Module exists, not wired** --- -## Phase 2: Transactions (2–3 weeks) +## Phase 2: Transactions ✅ MOSTLY DONE ### 2.1 Wire MVCC into server pipeline -- `BEGIN`, `COMMIT`, `ROLLBACK` commands -- Server tracks per-connection Transaction state -- All reads/writes through TxnManager -- Isolation: Read Committed (Phase 2a), Repeatable Read (Phase 2b) +- `BEGIN`, `COMMIT`, `ROLLBACK` commands ✅ +- Server tracks per-connection Transaction state ✅ +- All reads/writes through TxnManager ✅ (INSERT/DELETE) +- Isolation: Read Committed ✅ ### 2.2 WAL crash recovery -- Implement REDO: replay committed WAL entries into LSM-Tree -- Implement UNDO: remove uncommitted entries on recovery -- Checkpoint markers in WAL -- Point-in-time recovery +- Implement REDO: replay committed WAL entries ❌ **Not implemented** +- Implement UNDO: remove uncommitted entries ❌ **Not implemented** +- 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 +- Implement actual SSTable merge ❌ **Stub — metadata shuffle only** +- 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 +- Wire deadlock detection into TxnManager ❌ **Module exists, never imported** --- -## Phase 3: HTTP REST API & Authentication (2–3 weeks) +## Phase 3: HTTP REST API & Authentication ✅ PARTIALLY DONE ### 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` +- HTTP/1.1 server alongside TCP wire protocol ✅ +- `POST /query` — execute SQL, return JSON ✅ **FIXED — returns actual rows** +- `GET /health` — readiness/liveness ✅ +- `GET /metrics` — Prometheus format ✅ (basic counters) ### 3.2 Authentication -- `CREATE USER` / `DROP USER` / `ALTER USER` SQL -- Password hashing with argon2 -- JWT token creation with HMAC-SHA256 (replace djb2 `simpleHash`) -- `Authorization: Bearer ` in HTTP headers -- Per-user namespace isolation +- `CREATE USER` / `DROP USER` / `ALTER USER` SQL ❌ **Not implemented** +- Password hashing with argon2 ❌ **Not implemented** +- JWT token creation with HMAC-SHA256 ⚠️ **Uses djb2 hash, not real HMAC** +- `Authorization: Bearer ` in HTTP headers ✅ +- Per-user namespace isolation ❌ ### 3.3 Authorization -- `GRANT` / `REVOKE` for table-level privileges (SELECT, INSERT, UPDATE, DELETE) -- Row-Level Security (RLS): `CREATE POLICY` on tables -- Wire auth into both HTTP and TCP protocol paths +- `GRANT` / `REVOKE` for table-level privileges ❌ **Not implemented** +- Row-Level Security (RLS) ❌ **Not implemented** +- Wire auth into both HTTP and TCP protocol paths ❌ **HTTP only, optional** ### 3.4 Rate limiting & TLS -- Wire RateLimiter into HTTP server (token bucket per IP) -- Wire TLS/SSL using OpenSSL FFI (not mock) -- Self-signed cert generation -- Configurable TLS via `baradadb cert create` +- Wire RateLimiter into HTTP server ❌ **Module exists, never imported** +- Wire TLS/SSL ❌ **Mock only, no OpenSSL FFI** +- Self-signed cert generation ✅ (shells to openssl CLI) --- -## Phase 4: WebSocket & Real-time (1–2 weeks) +## Phase 4: WebSocket & Real-time ✅ PARTIALLY DONE ### 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 +- `ws://host:port/live` — subscribe to table changes ✅ +- `SUBSCRIBE table_name` WebSocket message ✅ +- Push notifications on INSERT/UPDATE/DELETE ❌ **broadcastToTable exists but never called** +- `NOTIFY` / `LISTEN` analogue ❌ ### 4.2 CORS & HTTP hardening -- CORS headers for browser access -- Request size limits (10MB default) -- Connection keep-alive -- HTTP/2 readiness (ALPN negotiation) +- CORS headers for browser access ⚠️ **On WS upgrade only, not HTTP server** +- Request size limits ❌ +- Connection keep-alive ❌ +- HTTP/2 readiness ❌ --- -## Phase 5: ERP Features (3–4 weeks) +## Phase 5: ERP Features ❌ MOSTLY NOT DONE ### 5.1 Schema migrations -- `CREATE MIGRATION` → `APPLY MIGRATION` -- Versioned schema in `_schema_version` table -- Up/down migration scripts -- Dry-run mode -- CLI: `baradadb migrate status|up|down` +- `CREATE MIGRATION` → `APPLY MIGRATION` ❌ **No SQL syntax** +- Versioned schema in `_schema_version` table ❌ **Uses _schema:migrations: prefix** +- Up/down migration scripts ❌ +- Dry-run mode ❌ +- CLI: `baradadb migrate status|up|down` ❌ ### 5.2 Views -- `CREATE VIEW` — virtual table (stored query) -- `CREATE MATERIALIZED VIEW` — cached snapshot + `REFRESH` -- View usage in query planner +- `CREATE VIEW` — virtual table ❌ +- `CREATE MATERIALIZED VIEW` ❌ +- View usage in query planner ❌ ### 5.3 Triggers & stored functions -- `CREATE TRIGGER` — BEFORE/AFTER on INSERT/UPDATE/DELETE -- Stored functions in Nim (compile to UDF) -- ERP helper functions: `vat_calc`, `currency_convert`, `invoice_number_next` +- `CREATE TRIGGER` ❌ +- Stored functions ❌ +- ERP helper functions ❌ ### 5.4 Full-text search for ERP documents -- `CREATE FULLTEXT INDEX ON table(column)` -- `WHERE content @@ 'search query'` -- Bulgarian stemming integration +- `CREATE FULLTEXT INDEX ON table(column)` ❌ **FTS engine exists, not wired to SQL** +- `WHERE content @@ 'search query'` ❌ ### 5.5 Partitioning -- `CREATE TABLE (...) PARTITION BY RANGE (col)` -- Auto partition pruning in query planner -- Useful for ERP: archive old data by date range +- `CREATE TABLE (...) PARTITION BY RANGE (col)` ❌ --- -## Phase 6: Production Readiness (2–3 weeks) +## Phase 6: Production Readiness ⚠️ PARTIALLY DONE ### 6.1 Backup & Restore -- `baradadb backup --output backup.tar.gz` -- `baradadb restore --input backup.tar.gz` -- Incremental backup via WAL archiving -- Point-in-time recovery (PITR) +- `baradadb backup --output backup.tar.gz` ✅ +- `baradadb restore --input backup.tar.gz` ✅ +- Incremental backup via WAL archiving ❌ +- Point-in-time recovery (PITR) ❌ ### 6.2 Docker & deployment -- `Dockerfile` — multi-stage build with Nim -- `docker-compose.yml` — single node -- `docker-compose.raft.yml` — 3-node cluster -- Environment-based config (`BARADB_PORT`, `BARADB_DATA_DIR`) +- `Dockerfile` — multi-stage build with Nim ✅ +- `docker-compose.yml` — single node ✅ +- `docker-compose.raft.yml` — 3-node cluster ❌ +- Environment-based config ✅ ### 6.3 Monitoring -- Structured JSON logging -- Prometheus `/metrics`: `baradb_queries_total`, `baradb_query_duration_s`, `baradb_connections_active`, `baradb_storage_size_bytes` -- Slow query log (configurable threshold) -- OpenTelemetry tracing +- Structured JSON logging ❌ **Uses echo** +- Prometheus `/metrics` ✅ (basic counters) +- Slow query log ❌ +- OpenTelemetry tracing ❌ ### 6.4 Admin dashboard -- Web UI on `http://host:port/admin` -- SQL playground with results table -- Schema browser (tables, columns, indexes) -- Metrics charts -- User management UI +- Web UI ❌ **Does not exist** ### 6.5 Client SDK improvements -- Nim: transaction API, prepared statements, auth -- Python: complete result parsing, transaction API, async support -- JavaScript: actual TCP/WebSocket connection, complete result parsing -- Go: complete result parsing, transaction API -- Rust: complete result parsing, transaction API -- Connection pooling in all clients +- All clients: ❌ **Not improved** --- @@ -258,37 +240,60 @@ BaraDB as a production-ready database for: | Task | Impact | Difficulty | Priority | |------|--------|-----------|----------| -| Pipeline integration (Phase 0) | Critical | High | **P0** | -| SQL DDL parser (Phase 0) | Critical | Medium | **P0** | -| AST→IR lowering (Phase 0) | Critical | High | **P0** | -| Codegen execution (Phase 0) | Critical | High | **P0** | -| SQL schema system (Phase 1) | Critical | High | **P0** | -| B-Tree index integration (Phase 1) | High | Medium | P1 | -| Constraint enforcement (Phase 1) | High | Medium | P1 | -| MVCC wiring (Phase 2) | Critical | High | **P0** | -| WAL recovery (Phase 2) | High | Medium | P1 | -| HTTP REST API (Phase 3) | Critical | Medium | **P0** | -| JWT Auth + RLS (Phase 3) | High | Medium | P1 | -| WebSocket real-time (Phase 4) | Medium | Medium | P2 | -| Schema migrations (Phase 5) | High | Medium | P1 | -| 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 | +| Pipeline integration (Phase 0) | Critical | High | **P0 ✅** | +| SQL DDL parser (Phase 0) | Critical | Medium | **P0 ✅** | +| AST→IR lowering (Phase 0) | Critical | High | **P0 ✅** | +| Codegen execution (Phase 0) | Critical | High | **P0 ✅** | +| SQL schema system (Phase 1) | Critical | High | **P0 ✅** | +| B-Tree index integration (Phase 1) | High | Medium | **P1 ✅** | +| Constraint enforcement (Phase 1) | High | Medium | **P1 ✅ (NOT NULL, PK, UNIQUE, DEFAULT)** | +| MVCC wiring (Phase 2) | Critical | High | **P0 ✅** | +| WAL recovery (Phase 2) | High | Medium | P1 ❌ | +| HTTP REST API (Phase 3) | Critical | Medium | **P0 ✅** | +| JWT Auth + RLS (Phase 3) | High | Medium | P1 ⚠️ **JWT exists, RLS not** | +| WebSocket real-time (Phase 4) | Medium | Medium | **P2 ✅ (server works, executor not wired)** | +| Schema migrations (Phase 5) | High | Medium | P1 ❌ | +| 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 +## What Actually Works (Honest) -- **Phase 0:** Server uses full pipeline. INSERT/UPDATE/DELETE/CREATE TABLE work properly. 56 existing tests pass + new tests. -- **Phase 1:** SQL schema with constraints, B-Tree indexes, EXPLAIN. Can define tables with PKs, FKs, and indexes. -- **Phase 2:** ACID transactions with MVCC, WAL recovery, compaction. Can use BEGIN/COMMIT/ROLLBACK. -- **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. +**Production-ready NOW:** +- CREATE TABLE with PK, UNIQUE, NOT NULL, DEFAULT constraints +- INSERT INTO ... VALUES with column list and validation +- SELECT with WHERE filter, ORDER BY, LIMIT/OFFSET +- UPDATE with WHERE clause +- DELETE with WHERE clause +- BEGIN / COMMIT / ROLLBACK transactions +- B-Tree index point reads on indexed columns +- HTTP REST API: POST /query, GET /health, GET /metrics +- JWT authentication (optional) +- WebSocket SUBSCRIBE/UNSUBSCRIBE +- Schema persistence + auto-restore on restart +- Docker + docker-compose deployment +- Backup/restore via tar.gz -**Final score after plan:** 9.5/10 — production-ready for web/ERP workloads. +**Partially working:** +- EXPLAIN (returns plan description, not cost estimates) +- Auth (JWT exists but uses weak hash, no user management) + +**Not yet working:** +- ALTER TABLE, CREATE INDEX, CREATE VIEW +- FOREIGN KEY, CHECK constraints +- WAL crash recovery, compaction +- Rate limiting, TLS +- Admin dashboard +- Client SDK improvements +- Full-text search via SQL +- Type enforcement (INTEGER, VARCHAR, etc.) + +--- + +**Honest score: 8/10 — solid foundation, but significant gaps remain before production use.** diff --git a/docker-compose.yml b/docker-compose.yml index 9eb901b..6c9011f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: - BARADB_HTTP_PORT=8080 - BARADB_WS_PORT=8081 healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"] interval: 10s timeout: 5s retries: 3 diff --git a/src/barabadb/core/httpserver.nim b/src/barabadb/core/httpserver.nim index 0a8a7a8..6353c9d 100644 --- a/src/barabadb/core/httpserver.nim +++ b/src/barabadb/core/httpserver.nim @@ -103,18 +103,31 @@ proc handleRequest(server: HttpServer, req: Request) {.async, gcsafe.} = newHttpHeaders([("Content-Type", "application/json")])) return - let (success, errMsg, affectedRows) = executor.executeQuery(reqCtx, astNode) + let result = executor.executeQuery(reqCtx, astNode) - if success: + if result.success: inc server.metrics.selectCount + var jsonRows = newJArray() + for row in result.rows: + var jsonRow = newJObject() + for col, val in row: + jsonRow[col] = %val + jsonRows.add(jsonRow) + var jsonCols = newJArray() + for c in result.columns: + jsonCols.add(%c) + var msg: JsonNode = nil + if result.message.len > 0: + msg = %result.message await req.respond(Http200, $ %* { - "rows": newJArray(), - "affectedRows": affectedRows, - "columns": newJArray() + "rows": jsonRows, + "affectedRows": result.affectedRows, + "columns": jsonCols, + "message": if result.message.len > 0: %result.message else: newJNull() }, newHttpHeaders([("Content-Type", "application/json")])) else: inc server.metrics.queryErrors - await req.respond(Http400, $jsonError(400, errMsg), + await req.respond(Http400, $jsonError(400, result.message), newHttpHeaders([("Content-Type", "application/json")])) of "/health": diff --git a/src/barabadb/core/server.nim b/src/barabadb/core/server.nim index e79585e..cef772e 100644 --- a/src/barabadb/core/server.nim +++ b/src/barabadb/core/server.nim @@ -70,21 +70,22 @@ proc executeQuery(db: LSMTree, ctx: ExecutionContext, query: string): (bool, Que if astNode.stmts.len == 0: return (true, QueryResult(), "") - let stmt = astNode.stmts[0] - case stmt.kind - of nkSelect, nkInsert, nkUpdate, nkDelete, nkCreateTable, nkDropTable, - nkCreateType, nkBeginTxn, nkCommitTxn, nkRollbackTxn, nkExplainStmt: - let (success, errMsg, affectedRows) = executor.executeQuery(ctx, astNode) - if success: - var qr = QueryResult(affectedRows: affectedRows, rowCount: affectedRows) - if stmt.kind == nkSelect: - qr.columns = @["key", "value"] - qr.rows = @[] - return (true, qr, "") - else: - return (false, QueryResult(), errMsg) + let result = executor.executeQuery(ctx, astNode) + if result.success: + var qr = QueryResult(affectedRows: result.affectedRows, rowCount: result.rows.len) + qr.columns = result.columns + qr.rows = @[] + for row in result.rows: + var wireRow: seq[WireValue] = @[] + for col in result.columns: + if col in row: + wireRow.add(WireValue(kind: fkString, strVal: row[col])) + else: + wireRow.add(WireValue(kind: fkString, strVal: "")) + qr.rows.add(wireRow) + return (true, qr, result.message) else: - return (false, QueryResult(), "Unsupported statement type: " & $stmt.kind) + return (false, QueryResult(), result.message) except Exception as e: return (false, QueryResult(), e.msg) diff --git a/src/barabadb/query/executor.nim b/src/barabadb/query/executor.nim index 5234f01..a6fc0d4 100644 --- a/src/barabadb/query/executor.nim +++ b/src/barabadb/query/executor.nim @@ -3,6 +3,7 @@ import std/strutils import std/tables import std/hashes import std/sequtils +import std/algorithm import lexer as qlex import parser as qpar import ast @@ -13,16 +14,16 @@ import ../storage/btree import ../core/mvcc type - IndexEntry = ref object + IndexEntry* = ref object lsmKey*: string rowValue*: string ExecutionContext* = ref object db*: LSMTree - tables*: Table[string, TableDef] # table name -> definition - btrees*: Table[string, BTreeIndex[string, IndexEntry]] # index name -> btree + tables*: Table[string, TableDef] + btrees*: Table[string, BTreeIndex[string, IndexEntry]] txnManager*: TxnManager - pendingTxn*: Transaction # active transaction for this context + pendingTxn*: Transaction TableDef* = object name*: string @@ -37,7 +38,24 @@ type isUnique*: bool defaultVal*: string - Row = Table[string, string] + Row* = Table[string, string] + + ExecResult* = object + success*: bool + columns*: seq[string] + rows*: seq[Row] + affectedRows*: int + message*: string + +proc okResult*(rows: seq[Row] = @[], cols: seq[string] = @[], affected: int = 0, msg: string = ""): ExecResult = + ExecResult(success: true, columns: cols, rows: rows, affectedRows: affected, message: msg) + +proc errResult*(msg: string): ExecResult = + ExecResult(success: false, columns: @[], rows: @[], affectedRows: 0, message: msg) + +# ---------------------------------------------------------------------- +# Context management +# ---------------------------------------------------------------------- proc newExecutionContext*(db: LSMTree): ExecutionContext = result = ExecutionContext(db: db, tables: initTable[string, TableDef](), @@ -45,14 +63,12 @@ proc newExecutionContext*(db: LSMTree): ExecutionContext = restoreSchema(result) proc restoreSchema(ctx: ExecutionContext) = - ## Replay persisted migrations on startup let prefix = "_schema:migrations:" for entry in ctx.db.scanMemTable(): if entry.deleted: continue if not entry.key.startsWith(prefix): continue let ddl = cast[string](entry.value) if ddl.len == 0: continue - # Replay DDL let tokens = qlex.tokenize(ddl) let astNode = qpar.parse(tokens) if astNode.stmts.len > 0: @@ -69,36 +85,28 @@ proc restoreSchema(ctx: ExecutionContext) = of "pkey": colDef.isPk = true tbl.pkColumns.add(col.cdName) - let idxName = stmt.crtName & "." & col.cdName - var bt = newBTreeIndex[string, IndexEntry]() - ctx.btrees[idxName] = bt + ctx.btrees[stmt.crtName & "." & col.cdName] = newBTreeIndex[string, IndexEntry]() of "notnull": colDef.isNotNull = true of "unique": colDef.isUnique = true - let idxName2 = stmt.crtName & "." & col.cdName - var bt2 = newBTreeIndex[string, IndexEntry]() - ctx.btrees[idxName2] = bt2 + ctx.btrees[stmt.crtName & "." & col.cdName] = newBTreeIndex[string, IndexEntry]() else: discard tbl.columns.add(colDef) ctx.tables[stmt.crtName] = tbl else: discard proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext = - ## Create a per-connection context that shares the same data but has its own transaction. ExecutionContext(db: ctx.db, tables: ctx.tables, btrees: ctx.btrees, txnManager: ctx.txnManager, pendingTxn: nil) -proc getTableDef(ctx: ExecutionContext, tableName: string): TableDef = - if tableName in ctx.tables: - return ctx.tables[tableName] - var tbl = TableDef(name: tableName, columns: @[], pkColumns: @[]) - return tbl +# ---------------------------------------------------------------------- +# Helpers +# ---------------------------------------------------------------------- -proc getColumnDef(tbl: TableDef, colName: string): ColumnDef = - for col in tbl.columns: - if col.name.toLower() == colName.toLower(): - return col +proc getTableDef(ctx: ExecutionContext, tableName: string): TableDef = + if tableName in ctx.tables: return ctx.tables[tableName] + return TableDef(name: tableName, columns: @[], pkColumns: @[]) proc getValue(values: seq[string], fields: seq[string], colName: string): string = for i, f in fields: @@ -106,38 +114,152 @@ proc getValue(values: seq[string], fields: seq[string], colName: string): string return values[i] return "" -proc isNull(value: string): bool = - result = value.len == 0 or value.toLower() == "null" +proc isNull*(value: string): bool = + value.len == 0 or value.toLower() == "null" + +proc parseRowData(valStr: string): Table[string, string] = + ## Parse "col1=val1,col2=val2" into a table + result = initTable[string, string]() + for part in valStr.split(","): + let eqPos = part.find('=') + if eqPos >= 0: + let k = part[0.. 0: + let colName = expr.fieldPath[^1] + if colName in row: return row[colName] + if "$key" in row and row["$key"].startsWith(colName & "="): + return row["$key"][colName.len+1..^1] + if "$value" in row: + let parsed = parseRowData(row["$value"]) + if colName in parsed: return parsed[colName] + return "" + of irekBinary: + let left = evalExpr(expr.binLeft, row) + let right = evalExpr(expr.binRight, row) + case expr.binOp + of irEq: + if left == right: return "true" + # Try numeric comparison + try: + if parseFloat(left) == parseFloat(right): return "true" + except: discard + return "false" + of irNeq: return if left != right: "true" else: "false" + of irLt: + try: + return if parseFloat(left) < parseFloat(right): "true" else: "false" + except: return if left < right: "true" else: "false" + of irLte: + try: + return if parseFloat(left) <= parseFloat(right): "true" else: "false" + except: return if left <= right: "true" else: "false" + of irGt: + try: + return if parseFloat(left) > parseFloat(right): "true" else: "false" + except: return if left > right: "true" else: "false" + of irGte: + try: + return if parseFloat(left) >= parseFloat(right): "true" else: "false" + except: return if left >= right: "true" else: "false" + of irAnd: + if left == "true" and right == "true": return "true" + return "false" + of irOr: + if left == "true" or right == "true": return "true" + return "false" + of irAdd: + try: return $(parseFloat(left) + parseFloat(right)) + except: return left & right + of irSub: + try: return $(parseFloat(left) - parseFloat(right)) + except: return "0" + of irMul: + try: return $(parseFloat(left) * parseFloat(right)) + except: return "0" + of irDiv: + try: + let r = parseFloat(right) + if r != 0: return $(parseFloat(left) / r) + return "0" + except: return "0" + of irLike: + let pattern = right.replace("%", ".*").replace("_", ".") + try: + if left.match(pattern): return "true" + except: discard + return "false" + else: return "false" + of irekUnary: + case expr.unOp + of irNot: + let v = evalExpr(expr.unExpr, row) + return if v == "true": "false" else: "true" + of irIsNull: + let v = evalExpr(expr.unExpr, row) + return if isNull(v): "true" else: "false" + of irIsNotNull: + let v = evalExpr(expr.unExpr, row) + return if not isNull(v): "true" else: "false" + else: return "false" + of irekExists: return "false" + else: return "" + +# ---------------------------------------------------------------------- +# Table scan and storage +# ---------------------------------------------------------------------- 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 + 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) + let valStr = cast[string](entry.value) + row["$value"] = valStr + # Also parse individual columns + for k, v in parseRowData(valStr): + row[k] = v + # Extract PK value from key + let eqPos = rest.find('=') + if eqPos >= 0: + row[rest[0..= 0: + row[key[0.. 0 and not isNull(colVal): - let entry = IndexEntry(lsmKey: fullKey, rowValue: valStr) - ctx.btrees[colName].insert(colVal, entry) + ctx.btrees[colName].insert(colVal, IndexEntry(lsmKey: fullKey, rowValue: valStr)) inc count return count -proc execUpdateRow(ctx: ExecutionContext, table: string, key: string, sets: seq[(string, string)]): int = - let fullKey = table & "." & key - let (found, existing) = ctx.db.get(fullKey) - if not found: - return 0 - var valStr = cast[string](existing) - for (f, v) in sets: - let prefix = f & "=" - var newParts: seq[string] = @[] - for part in valStr.split(","): - if part.startsWith(prefix): - newParts.add(prefix & v) - else: - newParts.add(part) - # If field not in existing, append - var hasField = false - for part in valStr.split(","): - if part.startsWith(prefix): - hasField = true - break - if not hasField: - newParts.add(prefix & v) - valStr = newParts.join(",") - ctx.db.put(fullKey, cast[seq[byte]](valStr)) - return 1 - -# ---------------------------------------------------------------------- -# Constraint Validation -# ---------------------------------------------------------------------- - -proc validateConstraints*(ctx: ExecutionContext, tableName: string, - fields: seq[string], values: seq[seq[string]]): (bool, string) = - ## Validate INSERT/UPDATE constraints. - ## Returns (success, errorMessage). - - let tbl = ctx.getTableDef(tableName) - - for rowIdx, rowVals in values: - for col in tbl.columns: - let val = getValue(rowVals, fields, col.name) - - # NOT NULL check - if col.isNotNull and isNull(val): - return (false, "NOT NULL constraint violated for column '" & col.name & "'") - - # DEFAULT value - if isNull(val) and col.defaultVal.len > 0: - # We'll handle this in the insert loop - discard - - # PRIMARY KEY uniqueness check (against existing data) - if tbl.pkColumns.len > 0: - var pkVals: seq[string] = @[] - for pkCol in tbl.pkColumns: - pkVals.add(getValue(rowVals, fields, pkCol)) - let pkStr = pkVals.join("|") - let pkKey = tableName & "." & pkStr - let (exists, _) = ctx.db.get(pkKey) - if exists: - return (false, "UNIQUE constraint violated: duplicate key '" & pkStr & "' for table '" & tableName & "'") - - # UNIQUE constraint check - for col in tbl.columns: - if col.isUnique: - let uVal = getValue(rowVals, fields, col.name) - if not isNull(uVal): - # Check uniqueness by scanning existing rows - let prefix = tableName & "." - for entry in ctx.db.scanMemTable(): - if entry.deleted: continue - if entry.key.startsWith(prefix): - let existingVal = cast[string](entry.value) - let fieldPrefix = col.name & "=" - for part in existingVal.split(","): - if part.startsWith(fieldPrefix) and part[fieldPrefix.len..^1] == uVal: - return (false, "UNIQUE constraint violated: duplicate value '" & uVal & "' for column '" & col.name & "'") - - return (true, "") - -proc applyDefaultValues(tbl: TableDef, fields: var seq[string], values: var seq[seq[string]]) = - for col in tbl.columns: - if col.defaultVal.len == 0: continue - var hasField = false - for f in fields: - if f.toLower() == col.name.toLower(): - hasField = true - break - if not hasField: - fields.add(col.name) - for rowIdx in 0.. 0: + var pkVals: seq[string] = @[] + for pkCol in tbl.pkColumns: + pkVals.add(getValue(rowVals, fields, pkCol)) + let pkStr = pkVals.join("|") + let pkKey = tableName & "." & pkStr + let (exists, _) = ctx.db.get(pkKey) + if exists: + return (false, "UNIQUE constraint violated: duplicate key '" & pkStr & "' for table '" & tableName & "'") + + for col in tbl.columns: + if col.isUnique: + let uVal = getValue(rowVals, fields, col.name) + if not isNull(uVal): + let idxName = tableName & "." & col.name + if idxName in ctx.btrees: + if ctx.btrees[idxName].contains(uVal): + return (false, "UNIQUE constraint violated: duplicate value '" & uVal & "' for column '" & col.name & "'") + + # CHECK constraints (via table-level cstType="check") + for col in tbl.columns: + let fkIdx = tableName & "." & col.name + # FK check: verify referenced row exists + # (skipped for now — needs full table metadata) + + return (true, "") + +proc applyDefaultValues*(tbl: TableDef, fields: var seq[string], values: var seq[seq[string]]) = + for col in tbl.columns: + if col.defaultVal.len == 0: continue + var hasField = false + for f in fields: + if f.toLower() == col.name.toLower(): + hasField = true + break + if not hasField: + fields.add(col.name) + for rowIdx in 0.. 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)) + 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("") + 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 @@ -430,7 +511,6 @@ proc lowerSelect(node: Node): IRPlan = 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 @@ -440,48 +520,60 @@ proc lowerSelect(node: Node): IRPlan = 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 +# IR Plan Execution (with actual filter/sort/projection) # ---------------------------------------------------------------------- -proc executePlan(ctx: ExecutionContext, plan: IRPlan): seq[Row] = - if plan == nil: - return @[] +proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] = + if plan == nil: return @[] case plan.kind of irpkScan: - result = execScan(ctx, plan.scanTable) + return execScan(ctx, plan.scanTable) of irpkFilter: let sourceRows = executePlan(ctx, plan.filterSource) - result = sourceRows # TODO: actual filter evaluation + if plan.filterCond == nil: return sourceRows + result = @[] + for row in sourceRows: + let evalResult = evalExpr(plan.filterCond, row) + if evalResult == "true": + result.add(row) of irpkProject: let sourceRows = executePlan(ctx, plan.projectSource) - result = sourceRows + if plan.projectAliases.len == 0: return sourceRows + result = @[] + for row in sourceRows: + var newRow: Table[string, string] + for i, alias in plan.projectAliases: + if i < plan.projectExprs.len: + let val = evalExpr(plan.projectExprs[i], row) + if alias.len > 0: newRow[alias] = val + else: newRow["col" & $i] = val + if newRow.len > 0: + result.add(newRow) + else: + result.add(row) of irpkSort: - let sourceRows = executePlan(ctx, plan.sortSource) - result = sourceRows + var sourceRows = executePlan(ctx, plan.sortSource) + if plan.sortExprs.len == 0: return sourceRows + let sortExpr = plan.sortExprs[0] + let ascending = if plan.sortDirs.len > 0: plan.sortDirs[0] else: true + proc sortCmp(a, b: Row): int = + let va = evalExpr(sortExpr, a) + let vb = evalExpr(sortExpr, b) + try: + let fa = parseFloat(va) + let fb = parseFloat(vb) + if fa < fb: return -1 + if fa > fb: return 1 + return 0 + except: + return cmp(va, vb) + sourceRows.sort(sortCmp, if ascending: Ascending else: Descending) + return sourceRows of irpkLimit: let sourceRows = executePlan(ctx, plan.limitSource) @@ -490,33 +582,31 @@ proc executePlan(ctx: ExecutionContext, plan: IRPlan): seq[Row] = var endIdx = start + int(plan.limitCount) if endIdx > sourceRows.len or plan.limitCount == 0: endIdx = sourceRows.len - result = sourceRows[start.. 0: if stmt.selWhere != nil and stmt.selWhere.whereExpr != nil: let w = stmt.selWhere.whereExpr @@ -527,23 +617,29 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node): (bool, string, int) = if idxName in ctx.btrees: let entries = ctx.btrees[idxName].get(w.binRight.strVal) if entries.len > 0: - useIndex = true - idxKey = w.binRight.strVal + # Fetch actual row data from LSM + let rows = execPointRead(ctx, stmt.selFrom.fromTable, colName & "=" & w.binRight.strVal) + let tbl = ctx.getTableDef(stmt.selFrom.fromTable) + var cols: seq[string] = @[] + for c in tbl.columns: cols.add(c.name) + if cols.len == 0: cols = @["key", "value"] + return okResult(rows, cols) - if useIndex: - return (true, "", 1) # Point read via B-Tree (result count) - else: - let plan = lowerSelect(stmt) - let rows = executePlan(ctx, plan) - return (true, "", rows.len) + # Full pipeline execution + let plan = lowerSelect(stmt) + let rows = executePlan(ctx, plan) + let tbl = ctx.getTableDef(if stmt.selFrom != nil: stmt.selFrom.fromTable else: "") + var cols: seq[string] = @[] + for c in tbl.columns: cols.add(c.name) + if cols.len == 0 and rows.len > 0: + for k, _ in rows[0]: cols.add(k) + return okResult(rows, cols) of nkInsert: var fields: seq[string] = @[] for f in stmt.insFields: - if f.kind == nkIdent: - fields.add(f.identName) - else: - fields.add("") + if f.kind == nkIdent: fields.add(f.identName) + else: fields.add("") var values: seq[seq[string]] = @[] for rowNode in stmt.insValues: @@ -562,40 +658,58 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node): (bool, string, int) = else: row.add("") values.add(row) - # If no fields specified, use all columns from table definition if fields.len == 0: let tbl = ctx.getTableDef(stmt.insTarget) - if tbl.columns.len > 0: - for col in tbl.columns: - fields.add(col.name) + for col in tbl.columns: fields.add(col.name) - # Apply DEFAULT values let tbl = ctx.getTableDef(stmt.insTarget) var mutableFields = fields var mutableValues = values applyDefaultValues(tbl, mutableFields, mutableValues) - # Validate constraints let (valid, errMsg) = validateConstraints(ctx, stmt.insTarget, mutableFields, mutableValues) - if not valid: - return (false, errMsg, 0) + if not valid: return errResult(errMsg) let count = execInsert(ctx, stmt.insTarget, mutableFields, mutableValues) - return (true, "", count) + return okResult(affected=count) of nkUpdate: - return (true, "", 0) + if stmt.updSet.len == 0: return okResult() + # Simple UPDATE: scan table, filter by WHERE, apply SET + var sets = initTable[string, string]() + for s in stmt.updSet: + if s.kind == nkBinOp and s.binOp == bkAssign: + if s.binLeft.kind == nkIdent: + let val = if s.binRight.kind == nkStringLit: s.binRight.strVal + elif s.binRight.kind == nkIntLit: $s.binRight.intVal + elif s.binRight.kind == nkFloatLit: $s.binRight.floatVal + else: "" + sets[s.binLeft.identName] = val + + # Scan and apply + let rows = execScan(ctx, stmt.updTarget) + var count = 0 + for row in rows: + # Check WHERE + if stmt.updWhere != nil and stmt.updWhere.whereExpr != nil: + let whereExpr = lowerExpr(stmt.updWhere.whereExpr) + if evalExpr(whereExpr, row) != "true": continue + # Get key from row + if "$key" in row: + count += execUpdateRow(ctx, stmt.updTarget, row["$key"], sets) + return okResult(affected=count) 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) + # Delete all rows matching WHERE + let rows = execScan(ctx, stmt.delTarget) + var count = 0 + for row in rows: + if stmt.delWhere != nil and stmt.delWhere.whereExpr != nil: + let whereExpr = lowerExpr(stmt.delWhere.whereExpr) + if evalExpr(whereExpr, row) != "true": continue + if "$key" in row: + count += execDelete(ctx, stmt.delTarget, row["$key"]) + return okResult(affected=count) of nkCreateTable: var tbl = TableDef(name: stmt.crtName, columns: @[], pkColumns: @[]) @@ -608,25 +722,22 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node): (bool, string, int) = of "pkey": colDef.isPk = true tbl.pkColumns.add(col.cdName) - let idxName = stmt.crtName & "." & col.cdName - var bt = newBTreeIndex[string, IndexEntry]() - ctx.btrees[idxName] = bt + ctx.btrees[stmt.crtName & "." & col.cdName] = newBTreeIndex[string, IndexEntry]() of "notnull": colDef.isNotNull = true of "unique": colDef.isUnique = true - let idxName2 = stmt.crtName & "." & col.cdName - var bt2 = newBTreeIndex[string, IndexEntry]() - ctx.btrees[idxName2] = bt2 + ctx.btrees[stmt.crtName & "." & col.cdName] = newBTreeIndex[string, IndexEntry]() of "default": - if cst.cstDefault != nil and cst.cstDefault.kind == nkStringLit: - colDef.defaultVal = cst.cstDefault.strVal + if cst.cstDefault != nil: + if cst.cstDefault.kind == nkStringLit: colDef.defaultVal = cst.cstDefault.strVal + elif cst.cstDefault.kind == nkIntLit: colDef.defaultVal = $cst.cstDefault.intVal + elif cst.cstDefault.kind == nkBoolLit: colDef.defaultVal = $cst.cstDefault.boolVal + elif cst.cstDefault.kind == nkFloatLit: colDef.defaultVal = $cst.cstDefault.floatVal else: discard tbl.columns.add(colDef) ctx.tables[stmt.crtName] = tbl - # Persist schema as migration - let schemaKey = "_schema:migrations:" & $ctx.tables.len - let schemaDDL = "CREATE TABLE " & stmt.crtName & " (" + # Persist schema var colDefs: seq[string] = @[] for col in tbl.columns: var parts = @[col.name, col.colType] @@ -635,59 +746,50 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node): (bool, string, int) = if col.isUnique: parts.add("UNIQUE") if col.defaultVal.len > 0: parts.add("DEFAULT '" & col.defaultVal & "'") colDefs.add(parts.join(" ")) - ctx.db.put(schemaKey, cast[seq[byte]](colDefs.join(", ") & ")")) + let schemaKey = "_schema:migrations:" & $ctx.tables.len + ctx.db.put(schemaKey, cast[seq[byte]]("CREATE TABLE " & stmt.crtName & " (" & colDefs.join(", ") & ")")) - return (true, "", 0) + return okResult() of nkDropTable: ctx.tables.del(stmt.drtName) - # Remove associated B-Tree indexes var toDelete: seq[string] = @[] - for idxName, bt in ctx.btrees: - if idxName.startsWith(stmt.drtName & "."): - toDelete.add(idxName) - for idxName in toDelete: - ctx.btrees.del(idxName) - return (true, "", 0) + for idxName in ctx.btrees.keys.toSeq(): + if idxName.startsWith(stmt.drtName & "."): toDelete.add(idxName) + for idxName in toDelete: ctx.btrees.del(idxName) + return okResult() of nkBeginTxn: if ctx.pendingTxn != nil and ctx.pendingTxn.state == tsActive: - # Auto-commit previous pending transaction discard ctx.txnManager.commit(ctx.pendingTxn) ctx.pendingTxn = ctx.txnManager.beginTxn(ilReadCommitted) - return (true, "", 0) + return okResult(msg="Transaction started") of nkCommitTxn: if ctx.pendingTxn != nil and ctx.pendingTxn.state == tsActive: - # Flush write set to LSM-Tree for key, version in ctx.pendingTxn.writeSet: - if version.value == @[]: - ctx.db.delete(key) - else: - ctx.db.put(key, version.value) + if version.value == @[]: ctx.db.delete(key) + else: ctx.db.put(key, version.value) discard ctx.txnManager.commit(ctx.pendingTxn) ctx.pendingTxn = nil - return (true, "", 0) - else: - return (false, "No active transaction to commit", 0) + return okResult(msg="Transaction committed") + return errResult("No active transaction to commit") of nkRollbackTxn: if ctx.pendingTxn != nil: discard ctx.txnManager.abortTxn(ctx.pendingTxn) ctx.pendingTxn = nil - return (true, "", 0) - else: - return (false, "No active transaction to rollback", 0) + return okResult(msg="Transaction rolled back") + return errResult("No active transaction to rollback") of nkCreateType: - return (true, "", 0) + return okResult() of nkExplainStmt: if stmt.expStmt != nil and stmt.expStmt.kind == nkSelect: var planStr = "EXPLAIN " if stmt.expStmt.selFrom != nil: planStr &= "SELECT on " & stmt.expStmt.selFrom.fromTable - # Check if index can be used var indexUsed = false if stmt.expStmt.selFrom != nil and stmt.expStmt.selFrom.fromTable.len > 0: if stmt.expStmt.selWhere != nil and stmt.expStmt.selWhere.whereExpr != nil: @@ -698,10 +800,12 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node): (bool, string, int) = if idxName in ctx.btrees: planStr &= " (using B-Tree index on " & w.binLeft.identName & ")" indexUsed = true - if not indexUsed: - planStr &= " (full table scan)" - return (true, "", 0) - return (true, "", 0) + if not indexUsed: planStr &= " (full table scan)" + return okResult(msg=planStr) + return okResult(msg="EXPLAIN") + + of nkAlterTable: + return okResult(msg="ALTER TABLE not yet fully implemented") else: - return (false, "Unsupported statement type: " & $stmt.kind, 0) + return errResult("Unsupported statement type: " & $stmt.kind)