Compare commits

..

5 Commits

Author SHA1 Message Date
dimgigov 4c6c72dc5b docs(plan): mark 9.1.2 as complete
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
2026-05-17 03:50:57 +03:00
dimgigov 2e1d982b9f fix(executor): include arguments in aggregate column names
Issue #6: getSelectColumns() produced count() for count(*)
and max() for max(id). Now uses exprToSql() on funcArgs
to generate consistent names matching the actual SQL expression.

Before: columns: @[count()], rows: @[{count(*): 2}]
After:  columns: @[count(*)], rows: @[{count(*): 2}]
2026-05-17 03:50:43 +03:00
dimgigov cd62f98641 docs(plan): mark 9.1.1 as complete 2026-05-17 03:47:30 +03:00
dimgigov 07a22d305d chore: fix all build warnings and add stabilization roadmap
- Fix 3× ResultShadowed in cypher.nim, recovery.nim, shell.nim
- Remove 6× UnusedImport in test files
- Build is now clean: 0 warnings, 294 tests pass
- Add 4-week stabilization plan to PLAN.md (Sessions 9.1–9.4)
2026-05-17 03:47:17 +03:00
dimgigov 5231724e77 fix(executor): handle subqueries in comparisons and per-row UPDATE expressions
- Add missing nkSubquery case in lowerExpr so scalar subqueries
  (e.g. budget > (SELECT AVG(budget) FROM projects)) are lowered
  correctly instead of being treated as NULL literals.
- Move UPDATE SET expression evaluation inside the row loop so
  column references (e.g. salary + 5000) are resolved against the
  current row, fixing type validation failures.
- Make irAdd/irSub/irMul/irDiv return integer strings when the
  result is a whole number, avoiding spurious float output for INT
  columns (e.g. MERGE UPDATE qty = 100 + 50 now yields 150).

These fix regressions introduced by switching NULL representation
from empty string to \N, which exposed the pre-existing bugs above.

Refs: issue #9, issue #10
2026-05-17 03:29:25 +03:00
8 changed files with 127 additions and 33 deletions
+79
View File
@@ -123,3 +123,82 @@
**BaraDB v1.0.0 е production-ready за blogs, e-commerce и small ERP системи.**
**Всички distributed gaps са запълнени: replication, gossip transport, sharding migration, inter-module wiring.**
---
## 🆕 Сесия 9 — Stabilization Sprint (май 2026)
> **Цел:** Да махнем всички workaround-и от `BARADB_DEFICIENCIES.md`, да почистим build-а и да подготвим почвата за типова система.
> **Принцип:** Без нови светове — само stabilizaция на съществуващото.
### Седмица 1: Deficiency Hunt + Build Cleanup
| # | Задача | Оценка | Статус |
|---|--------|--------|--------|
| 9.1.1 | Почистване на 9-те build warnings (ResultShadowed + UnusedImport) | 1ч | ✅ |
| 9.1.2 | Issue #6: Aggregate column names (`count(*)``count(*)`, `max(id)``max(id)`) | 2ч | ✅ |
| 9.1.3 | Issue #5: GROUP BY bare columns — първи ред от групата за non-aggregated колони | 4-6ч | 🔄 |
| 9.1.4 | Issue #7+8: Решение за async vs sync client + thread safety | 2ч | 🔄 |
| 9.1.5 | Regression тестове за всички 10 deficiencies | 2ч | 🔄 |
**Метрика:** NimForum миграционният код маха всички `DISTINCT` workaround-и за GROUP BY.
---
### Седмица 2: Type Safety in Execution Layer
| # | Задача | Оценка | Статус |
|---|--------|--------|--------|
| 9.2.1 | `IRExpr` носи `expectedType` — всеки AST node знае дали е INT, FLOAT, TEXT, NULL | 4-6ч | 🔄 |
| 9.2.2 | `evalExpr` връща discriminated union (`Value(kind: vkInt64/Float64/String/Null)`) вместо само `string` | 6-8ч | 🔄 |
| 9.2.3 | `irAdd`/`irSub`/`irMul`/`irDiv` използват типовата информация (INT+INT → INT, INT+FLOAT → FLOAT) | 3ч | 🔄 |
| 9.2.4 | `validateType` използва `Value.kind` вместо `parseInt`/`parseFloat` на string | 2ч | 🔄 |
**Метрика:** Премахваме всички `try: parseFloat catch: return fallback` евристики от `evalExpr`.
---
### Седмица 3: JOIN Performance
| # | Задача | Оценка | Статус |
|---|--------|--------|--------|
| 9.3.1 | Hash Join: `ON a.col = b.col` с hash table върху по-малката страна | 6ч | 🔄 |
| 9.3.2 | Index Nested Loop Join: ако има B-Tree индекс на join колоната | 4ч | 🔄 |
| 9.3.3 | Benchmark: `thread JOIN category` с 10K/100K/1M редове | 2ч | 🔄 |
| 9.3.4 | Query planner избира между Nested Loop / Hash / Index въз основа на cardinality | 4ч | 🔄 |
**Метрика:** JOIN с 100K редове е под 100ms.
---
### Седмица 4: Production Hardening
| # | Задача | Оценка | Статус |
|---|--------|--------|--------|
| 9.4.1 | Property-based tests за `evalExpr` — случайни AST-та, проверка на invariant-и | 4ч | 🔄 |
| 9.4.2 | Fuzz test за wire protocol — случайни байтове в `_requestQueue` на JS client-а | 3ч | 🔄 |
| 9.4.3 | Thread safety audit: `execInsert`/`execUpdate`/`execDelete` с shared `ExecutionContext` | 3ч | 🔄 |
| 9.4.4 | Final integration test: NimForum login + thread list + post create с BaraDB | 4ч | 🔄 |
**Метрика:** 48 часа continuous fuzzing без crash. NimForum smoke test минава end-to-end.
---
### Рискове и митигации
| Риск | Митигация |
|------|-----------|
| GROUP BY bare columns е по-сложен от очакваното | Fallback: SQLite behavior (първи ред), не PostgreSQL (грешка) |
| Type safety рефакторингът чупи 294 теста | Правим го на отделен branch, не в `main` |
| Hash Join изисква памет proportional на N | Добавяме `work_mem` лимит като PostgreSQL |
---
### Текущи метрики (преди сесия 9)
| Метрика | Стойност |
|---------|----------|
| **Тестове** | 294 — 0 failures |
| **Build warnings** | 9 (3× ResultShadowed + 6× UnusedImport) |
| **BARADB_DEFICIENCIES** | 4 непоправени (#5, #6, #7, #8) |
| **Workaround-и в NimForum** | 2 (GROUP BY → DISTINCT, aggregate positional access) |
+3 -3
View File
@@ -164,10 +164,10 @@ proc processCommand*(state: var CliState, input: string): string =
of "history", "\\history":
if state.history.len == 0:
return "(no history)"
var result = ""
var res = ""
for i, h in state.history:
result &= " " & $(i + 1) & ": " & h & "\n"
return result
res &= " " & $(i + 1) & ": " & h & "\n"
return res
of "verbose":
state.verbose = not state.verbose
return "Verbose mode: " & (if state.verbose: "ON" else: "OFF")
-1
View File
@@ -212,7 +212,6 @@ proc executeCypher*(g: Graph, query: CypherQuery): CypherResult =
proc toCypher*(query: string): string =
## Convert basic BaraQL to Cypher-like syntax for graph queries
var result = ""
let upper = query.toUpper()
if upper.startsWith("SELECT") and upper.contains("MATCH"):
# Already Cypher-like
+39 -17
View File
@@ -581,18 +581,34 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
if left == "true" or right == "true": return "true"
return "false"
of irAdd:
try: return $(parseFloat(left) + parseFloat(right))
try:
let sum = parseFloat(left) + parseFloat(right)
if sum == float(int(sum)):
return $int(sum)
return $sum
except: return left & right
of irSub:
try: return $(parseFloat(left) - parseFloat(right))
try:
let diff = parseFloat(left) - parseFloat(right)
if diff == float(int(diff)):
return $int(diff)
return $diff
except: return "0"
of irMul:
try: return $(parseFloat(left) * parseFloat(right))
try:
let prod = parseFloat(left) * parseFloat(right)
if prod == float(int(prod)):
return $int(prod)
return $prod
except: return "0"
of irDiv:
try:
let r = parseFloat(right)
if r != 0: return $(parseFloat(left) / r)
if r != 0:
let quot = parseFloat(left) / r
if quot == float(int(quot)):
return $int(quot)
return $quot
return "0"
except: return "0"
of irMod:
@@ -1528,6 +1544,9 @@ proc lowerExpr*(node: Node): IRExpr =
result.binRight = lowerExpr(node.inRight)
of nkExists:
result = IRExpr(kind: irekExists)
of nkSubquery:
result = IRExpr(kind: irekSubquery)
result.subqueryPlan = lowerSelect(node.subQuery)
of nkStar:
result = IRExpr(kind: irekStar)
of nkWindowExpr:
@@ -2659,7 +2678,10 @@ proc getSelectColumns(stmt: Node): seq[string] =
elif e.kind == nkPath and e.pathParts.len > 0:
result.add(e.pathParts[^1])
elif e.kind == nkFuncCall:
result.add(e.funcName & "()")
var aliasArgs: seq[string] = @[]
for arg in e.funcArgs:
aliasArgs.add(exprToSql(arg))
result.add(e.funcName & "(" & aliasArgs.join(", ") & ")")
elif e.kind == nkStar:
result.add("*")
else:
@@ -3166,23 +3188,23 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
of nkUpdate:
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
elif s.binRight.kind == nkBoolLit: $s.binRight.boolVal
elif s.binRight.kind == nkNullLit: "\\N"
else: evalNodeToString(s.binRight)
sets[s.binLeft.identName] = val
# Scan and apply
let rows = execScan(ctx, stmt.updTarget)
var count = 0
var kvPairs: seq[(string, seq[byte])]
for row in rows:
# Compute sets for this row (expressions may reference columns)
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
elif s.binRight.kind == nkBoolLit: $s.binRight.boolVal
elif s.binRight.kind == nkNullLit: "\\N"
else: evalExpr(lowerExpr(s.binRight), row, ctx)
sets[s.binLeft.identName] = val
# Check WHERE
if stmt.updWhere != nil and stmt.updWhere.whereExpr != nil:
let whereExpr = lowerExpr(stmt.updWhere.whereExpr)
+5 -5
View File
@@ -136,19 +136,19 @@ proc analyze*(rec: CrashRecovery): RecoveryResult =
return rec.result
proc recover*(rec: CrashRecovery, db: LSMTree = nil): RecoveryResult =
let result = rec.analyze()
let analysis = rec.analyze()
if not result.applied:
return result
if not analysis.applied:
return analysis
if db == nil:
return result
return analysis
# REDO: apply committed entries (txnId < lastCommitted) to LSM-Tree
var redoCount = 0
var undoCount = 0
for entry in rec.entries:
if entry.txnId < result.lastTxn:
if entry.txnId < analysis.lastTxn:
# Committed — redo
if entry.isDelete:
db.delete(entry.key)
-3
View File
@@ -1,13 +1,10 @@
import std/unittest
import std/os
import std/strutils
import std/times
import std/monotimes
import std/tables
import barabadb/core/types
import barabadb/query/executor as qexec
import barabadb/query/parser
import barabadb/query/ast
import barabadb/storage/lsm
proc execSql(ctx: qexec.ExecutionContext, sql: string): qexec.ExecResult =
+1 -3
View File
@@ -4,8 +4,6 @@ import std/tables
import std/strutils
import std/os
import std/asyncdispatch
import std/times
import std/random
import std/monotimes
import std/base64
@@ -2997,7 +2995,7 @@ suite "MERGE Statement":
check r.success
check r.affectedRows == 1
let verify = qexec.executeQuery(ctx, parse("SELECT * FROM inventory WHERE sku = 'SKU001'"))
check verify.rows[0]["qty"] == "150.0"
check verify.rows[0]["qty"] == "150"
test "MERGE WHEN NOT MATCHED INSERT":
let r = qexec.executeQuery(ctx, parse("""
-1
View File
@@ -4,7 +4,6 @@
import std/unittest
import std/tables
import std/sets
import std/sequtils
import std/strformat