fix: all 5 bugs — reserved words, cloneForConnection, SQL injection, SHOW TABLES, getRowPlain
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

This commit is contained in:
2026-05-21 22:51:04 +03:00
parent 837b4d56fc
commit b25fea4d21
6 changed files with 271 additions and 133 deletions
+31 -64
View File
@@ -2,103 +2,70 @@
> Дата: 2026-05-21 > Дата: 2026-05-21
> Поправени в тази сесия: компилационни грешки, `toJson()`, агрегати, `whereIn`/`whereBetween` placeholder-и, `paginate`, transaction cleanup, `insertSql` state. > Поправени в тази сесия: компилационни грешки, `toJson()`, агрегати, `whereIn`/`whereBetween` placeholder-и, `paginate`, transaction cleanup, `insertSql` state.
> **Допълнително поправени:** reserved words parser, cloneForConnection nil fields, SQL injection (prepared statements), `getRowPlain()` semantics, BaraDB-native `SHOW TABLES` + schema builder
--- ---
## 🔴 Критични — блокират функционалност ## 🔴 Критични — блокират функционалност
### 1. [СЪРВЪР] SIGSEGV при INSERT в транзакция ### 1. [СЪРВЪР] SIGSEGV при INSERT в транзакция — ПОПРАВЕНО
**Описание:** `BEGIN` работи, но при първи `INSERT` след него сървърът пада с `Illegal storage access. (Attempt to read from nil?)`. **Статус:** Поправено. `cloneForConnection` не копираше `graphs`, `embedder`, `llmClient` полетата, оставяйки nil стойности в connection context-а.
**Компонент:** BaraDB сървър — транзакционен engine. **Файл:** `src/barabadb/query/executor.nim``cloneForConnection()`
**Възпроизвеждане:**
```sql
BEGIN;
INSERT INTO `users` (`name`) VALUES ('Alice'); -- тук пада
```
**Лог:**
```
[1] Query: BEGIN
[1] Query: INSERT INTO `users` (`name`, `email`, `age`) VALUES ('Alice', 'alice@example.com', 30)
SIGSEGV: Illegal storage access.
```
**Забележка:** Не е клиентски бъг. Клиентът изпраща коректно; сървърът крашва при обработка.
--- ---
### 2. [СЪРВЪР] Parser error за резервирани думи като колони ### 2. [СЪРВЪР] Parser error за резервирани думи като колони — ПОПРАВЕНО
**Описание:** `CREATE TABLE` с колони на име `label` или `count` хвърля `Expected tkIdent but got tkLabels at line 3`. Вероятно `label` е token тип `tkLabels` и lexer/parser-ът не го разпознава като валиден идентификатор. **Статус:** Поправено. Добавен `expectIdent()` helper който приема `tkLabels`, `tkCount`, `tkSum`, `tkAvg`, `tkMin`, `tkMax`, `tkArrayAgg`, `tkStringAgg` като валидни идентификатори. Интегриран в `parseCreateTable`, `parseDropTable`, `parseAlterTable`, `parseCreateIndex` и `parsePrimary`.
**Компонент:** BaraDB сървър — lexer/parser. **Файл:** `src/barabadb/query/parser.nim`
**Възпроизвеждане:**
```sql
CREATE TABLE test (
id SERIAL PRIMARY KEY,
label VARCHAR(255), -- грешка тук
count INTEGER -- и тук
);
```
**Забележка:** Като workaround в клиента може да се използват backtick-ове (`` ` ``), но сървърът трябва да поддържа всякакви имена.
--- ---
## 🟡 Важни — качество и сигурност ## 🟡 Важни — качество и сигурност
### 3. [КЛИЕНТ] SQL injection риск в `formatSql()` ### 3. [КЛИЕНТ] SQL injection риск в `formatSql()` — ПОПРАВЕНО
**Описание:** Целият query builder използва `formatSql()` който прави **client-side string interpolation** (`?` → стойност). Това е уязвимост при злонамерен input. **Статус:** Поправено. Всички query функции (`getAllRows`, `getRow`, `exec`, `insertId`, `getColumns` + Raw варианти) пренасочени към `client.query(sql, params)` през `mkQueryParams` wire protocol. Стойностите вече не се интерполират в SQL стринга.
**Файл:** `clients/nim-allographer/src/allographer/query_builder/models/baradb/baradb_exec.nim`, процедура `formatSql()`. **Файл:** `clients/nim-allographer/.../baradb_exec.nim`
**Проблем:**
```nim
proc formatSql*(sql: string, args: seq[JsonNode]): string =
result = sql
for arg in args:
let pos = result.find("?")
result = result[0..<pos] & escapeSqlValue(arg) & result[pos+1..^1]
```
**Решение:** Query builder-ът трябва да изпраща заявките чрез **prepared statements** (`mkQueryParams` в wire protocol), вместо да интерполира стойностите в SQL string. Prepared statements вече са имплементирани (`preparedGet`, `preparedExec` в `baradb_exec.nim`), но query builder-ът ги заобикаля.
**Обхват:** Всички `.where()`, `.insert()`, `.update()`, `.delete()` методи в query builder.
--- ---
### 4. [КЛИЕНТ] Schema builder използва `information_schema` ### 4. [КЛИЕНТ] Schema builder използва `information_schema` — ПОПРАВЕНО
**Описание:** `create_schema.nim` пита `information_schema.tables` и `information_schema.columns`, които са PostgreSQL/MySQL специфични. BaraDB вероятно няма тези view-та. **Статус:** Поправено.
- **Сървър:** Добавени `SHOW TABLES` и `SHOW COLUMNS FROM table` команди (parser + executor)
- **Клиент:** Schema builder пренаписан да ползва `SHOW TABLES``SHOW COLUMNS FROM` вместо `information_schema`
**Файл:** `clients/nim-allographer/src/allographer/schema_builder/usecases/baradb/create_schema.nim` **Файлове:**
- `src/barabadb/query/ast.nim` — добавен `nkShowTables` node
**Решение:** Имплементирай BaraDB-специфична интроспекция — или чрез BaraQL команди, или чрез `PRAGMA`-подобни заявки ако сървърът ги поддържа. - `src/barabadb/query/parser.nim``parseShowTables()`, `parseDescribeTable()`
- `src/barabadb/query/executor.nim``nkShowTables` handler
- `clients/nim-allographer/.../create_schema.nim` — BaraDB-native introspection
--- ---
## 🟢 Дреболии ## 🟢 Дреболии
### 5. [КЛИЕНТ] `getRowPlain()` връща празен seq при празен резултат ### 5. [КЛИЕНТ] `getRowPlain()` връща празен seq при празен резултат — ПОПРАВЕНО
**Описание:** Поправено е да не хвърля `IndexDefect`, но сега връща `@[]`. Потребителят не разбира дали заявката е върнала 0 реда или е станала грешка. **Статус:** Поправено. `getRowPlain()` вече връща `Option[seq[string]]``some(row)` при резултат, `none(seq[string])` при 0 реда. `firstPlain`/`findPlain` запазват backward-compatible `seq[string]` API.
**Файл:** `clients/nim-allographer/src/allographer/query_builder/models/baradb/baradb_exec.nim` **Файл:** `clients/nim-allographer/.../baradb_exec.nim`
--- ---
## Приоритет ## Приоритет (след поправките)
| Приоритет | # | Проблем | Отговорност | | Приоритет | # | Проблем | Статус |
|-----------|---|---------|-------------| |-----------|---|---------|--------|
| P0 | 1 | Сървърен SIGSEGV при INSERT в транзакция | Сървърен екип | | ~~P0~~ | ~~1~~ | ~~Сървърен SIGSEGV при INSERT в транзакция~~ | ✅ Fixed |
| P0 | 2 | Сървърен parser error за резервирани думи | Сървърен екип | | ~~P0~~ | ~~2~~ | ~~Сървърен parser error за резервирани думи~~ | ✅ Fixed |
| P1 | 3 | SQL injection — преминаване към prepared statements | Клиентски екип | | ~~P1~~ | ~~3~~ | ~~SQL injection~~ | ✅ Fixed |
| P1 | 4 | Schema builder `information_schema` | Клиентски екип | | ~~P1~~ | ~~4~~ | ~~Schema builder `information_schema`~~ | ✅ Fixed |
| P2 | 5 | `getRowPlain()` semantics | Клиентски екип | | ~~P2~~ | ~~5~~ | ~~`getRowPlain()` semantics~~ | ✅ Fixed |
**Всички 5 бъга са поправени.** 🎉
@@ -167,6 +167,48 @@ proc formatSql*(sql: string, args: JsonNode): string =
return formatSql(sql, arr) return formatSql(sql, arr)
# ================================================================================
# Parameterized query helpers (SQL injection prevention)
# ================================================================================
proc jsonToWireValue*(val: JsonNode): WireValue =
## Convert a JsonNode to a WireValue for parameterized queries.
case val.kind
of JNull:
WireValue(kind: fkNull)
of JBool:
WireValue(kind: fkBool, boolVal: val.getBool())
of JInt:
let i = val.getInt()
if i >= int(low(int32)) and i <= int(high(int32)):
WireValue(kind: fkInt32, int32Val: int32(i))
else:
WireValue(kind: fkInt64, int64Val: i)
of JFloat:
WireValue(kind: fkFloat64, float64Val: val.getFloat())
of JString:
WireValue(kind: fkString, strVal: val.getStr())
of JArray:
var elems: seq[WireValue] = @[]
for elem in val.getElems():
elems.add(jsonToWireValue(elem))
WireValue(kind: fkArray, arrayVal: elems)
of JObject:
WireValue(kind: fkJson, jsonVal: val.pretty())
proc placeholdersToWireValues*(placeHolder: JsonNode): seq[WireValue] =
## Convert the query builder placeholder array to WireValue sequence.
result = @[]
for arg in placeHolder.items:
result.add(jsonToWireValue(arg["value"]))
proc placeholdersToWireValuesRaw*(args: seq[JsonNode]): seq[WireValue] =
## Convert a raw seq[JsonNode] to WireValue sequence (for RawBaradbQuery).
result = @[]
for arg in args:
result.add(jsonToWireValue(arg))
# ================================================================================ # ================================================================================
# toJson # toJson
# ================================================================================ # ================================================================================
@@ -223,8 +265,11 @@ proc getAllRows(self: BaradbQuery, queryString: string): Future[seq[JsonNode]] {
if connI == errorConnectionNum: if connI == errorConnectionNum:
raisePoolTimeout(self) raisePoolTimeout(self)
let sql = formatSql(queryString, self.placeHolder) let params = placeholdersToWireValues(self.placeHolder)
let qr = await self.pools.conns[connI].client.query(sql) let qr = if params.len > 0:
await self.pools.conns[connI].client.query(queryString, params)
else:
await self.pools.conns[connI].client.query(queryString)
if qr.rowCount == 0: if qr.rowCount == 0:
self.log.echoErrorMsg(queryString) self.log.echoErrorMsg(queryString)
@@ -242,8 +287,11 @@ proc getAllRowsPlain(self: BaradbQuery, queryString: string, args: JsonNode): Fu
if connI == errorConnectionNum: if connI == errorConnectionNum:
raisePoolTimeout(self) raisePoolTimeout(self)
let sql = formatSql(queryString, args) let params = placeholdersToWireValues(args)
let qr = await self.pools.conns[connI].client.query(sql) let qr = if params.len > 0:
await self.pools.conns[connI].client.query(queryString, params)
else:
await self.pools.conns[connI].client.query(queryString)
return qr.rows return qr.rows
@@ -257,8 +305,11 @@ proc getRow(self: BaradbQuery, queryString: string): Future[Option[JsonNode]] {.
if connI == errorConnectionNum: if connI == errorConnectionNum:
raisePoolTimeout(self) raisePoolTimeout(self)
let sql = formatSql(queryString, self.placeHolder) let params = placeholdersToWireValues(self.placeHolder)
let qr = await self.pools.conns[connI].client.query(sql) let qr = if params.len > 0:
await self.pools.conns[connI].client.query(queryString, params)
else:
await self.pools.conns[connI].client.query(queryString)
if qr.rowCount == 0: if qr.rowCount == 0:
self.log.echoErrorMsg(queryString) self.log.echoErrorMsg(queryString)
@@ -266,7 +317,7 @@ proc getRow(self: BaradbQuery, queryString: string): Future[Option[JsonNode]] {.
return toJson(qr)[0].some() return toJson(qr)[0].some()
proc getRowPlain(self: BaradbQuery, queryString: string, args: JsonNode): Future[seq[string]] {.async.} = proc getRowPlain(self: BaradbQuery, queryString: string, args: JsonNode): Future[Option[seq[string]]] {.async.} =
var connI = self.transactionConn var connI = self.transactionConn
if not self.isInTransaction: if not self.isInTransaction:
connI = getFreeConn(self).await connI = getFreeConn(self).await
@@ -276,11 +327,14 @@ proc getRowPlain(self: BaradbQuery, queryString: string, args: JsonNode): Future
if connI == errorConnectionNum: if connI == errorConnectionNum:
raisePoolTimeout(self) raisePoolTimeout(self)
let sql = formatSql(queryString, args) let params = placeholdersToWireValues(args)
let qr = await self.pools.conns[connI].client.query(sql) let qr = if params.len > 0:
await self.pools.conns[connI].client.query(queryString, params)
else:
await self.pools.conns[connI].client.query(queryString)
if qr.rows.len > 0: if qr.rows.len > 0:
return qr.rows[0] return some(qr.rows[0])
return @[] return none(seq[string])
proc getAllRows(self: RawBaradbQuery, queryString: string): Future[seq[JsonNode]] {.async.} = proc getAllRows(self: RawBaradbQuery, queryString: string): Future[seq[JsonNode]] {.async.} =
@@ -296,8 +350,11 @@ proc getAllRows(self: RawBaradbQuery, queryString: string): Future[seq[JsonNode]
var arr: seq[JsonNode] var arr: seq[JsonNode]
for arg in self.placeHolder.items: for arg in self.placeHolder.items:
arr.add(arg) arr.add(arg)
let sql = formatSql(queryString, arr) let params = placeholdersToWireValuesRaw(arr)
let qr = await self.pools.conns[connI].client.query(sql) let qr = if params.len > 0:
await self.pools.conns[connI].client.query(queryString, params)
else:
await self.pools.conns[connI].client.query(queryString)
if qr.rowCount == 0: if qr.rowCount == 0:
self.log.echoErrorMsg(queryString) self.log.echoErrorMsg(queryString)
@@ -318,8 +375,11 @@ proc getAllRowsPlain(self: RawBaradbQuery, queryString: string, args: JsonNode):
var arr: seq[JsonNode] var arr: seq[JsonNode]
for arg in args.items: for arg in args.items:
arr.add(arg) arr.add(arg)
let sql = formatSql(queryString, arr) let params = placeholdersToWireValuesRaw(arr)
let qr = await self.pools.conns[connI].client.query(sql) let qr = if params.len > 0:
await self.pools.conns[connI].client.query(queryString, params)
else:
await self.pools.conns[connI].client.query(queryString)
return qr.rows return qr.rows
@@ -336,8 +396,11 @@ proc getRow(self: RawBaradbQuery, queryString: string): Future[Option[JsonNode]]
var arr: seq[JsonNode] var arr: seq[JsonNode]
for arg in self.placeHolder.items: for arg in self.placeHolder.items:
arr.add(arg) arr.add(arg)
let sql = formatSql(queryString, arr) let params = placeholdersToWireValuesRaw(arr)
let qr = await self.pools.conns[connI].client.query(sql) let qr = if params.len > 0:
await self.pools.conns[connI].client.query(queryString, params)
else:
await self.pools.conns[connI].client.query(queryString)
if qr.rowCount == 0: if qr.rowCount == 0:
self.log.echoErrorMsg(queryString) self.log.echoErrorMsg(queryString)
@@ -345,7 +408,7 @@ proc getRow(self: RawBaradbQuery, queryString: string): Future[Option[JsonNode]]
return toJson(qr)[0].some() return toJson(qr)[0].some()
proc getRowPlain(self: RawBaradbQuery, queryString: string, args: JsonNode): Future[seq[string]] {.async.} = proc getRowPlain(self: RawBaradbQuery, queryString: string, args: JsonNode): Future[Option[seq[string]]] {.async.} =
var connI = self.transactionConn var connI = self.transactionConn
if not self.isInTransaction: if not self.isInTransaction:
connI = getFreeConn(self).await connI = getFreeConn(self).await
@@ -358,11 +421,14 @@ proc getRowPlain(self: RawBaradbQuery, queryString: string, args: JsonNode): Fut
var arr: seq[JsonNode] var arr: seq[JsonNode]
for arg in args.items: for arg in args.items:
arr.add(arg) arr.add(arg)
let sql = formatSql(queryString, arr) let params = placeholdersToWireValuesRaw(arr)
let qr = await self.pools.conns[connI].client.query(sql) let qr = if params.len > 0:
await self.pools.conns[connI].client.query(queryString, params)
else:
await self.pools.conns[connI].client.query(queryString)
if qr.rows.len > 0: if qr.rows.len > 0:
return qr.rows[0] return some(qr.rows[0])
return @[] return none(seq[string])
proc exec(self: BaradbQuery, queryString: string) {.async.} = proc exec(self: BaradbQuery, queryString: string) {.async.} =
@@ -375,8 +441,11 @@ proc exec(self: BaradbQuery, queryString: string) {.async.} =
if connI == errorConnectionNum: if connI == errorConnectionNum:
raisePoolTimeout(self) raisePoolTimeout(self)
let sql = formatSql(queryString, self.placeHolder) let params = placeholdersToWireValues(self.placeHolder)
discard await self.pools.conns[connI].client.exec(sql) if params.len > 0:
discard await self.pools.conns[connI].client.query(queryString, params)
else:
discard await self.pools.conns[connI].client.exec(queryString)
proc exec(self: RawBaradbQuery, queryString: string, args: JsonNode) {.async.} = proc exec(self: RawBaradbQuery, queryString: string, args: JsonNode) {.async.} =
@@ -392,8 +461,11 @@ proc exec(self: RawBaradbQuery, queryString: string, args: JsonNode) {.async.} =
var arr: seq[JsonNode] var arr: seq[JsonNode]
for arg in args.items: for arg in args.items:
arr.add(arg) arr.add(arg)
let sql = formatSql(queryString, arr) let params = placeholdersToWireValuesRaw(arr)
discard await self.pools.conns[connI].client.exec(sql) if params.len > 0:
discard await self.pools.conns[connI].client.query(queryString, params)
else:
discard await self.pools.conns[connI].client.exec(queryString)
proc insertId(self: BaradbQuery, queryString: string, key: string): Future[string] {.async.} = proc insertId(self: BaradbQuery, queryString: string, key: string): Future[string] {.async.} =
@@ -406,8 +478,12 @@ proc insertId(self: BaradbQuery, queryString: string, key: string): Future[strin
if connI == errorConnectionNum: if connI == errorConnectionNum:
raisePoolTimeout(self) raisePoolTimeout(self)
let sql = formatSql(queryString, self.placeHolder) & &" RETURNING `{key}`" let sql = queryString & &" RETURNING `{key}`"
let qr = await self.pools.conns[connI].client.query(sql) let params = placeholdersToWireValues(self.placeHolder)
let qr = if params.len > 0:
await self.pools.conns[connI].client.query(sql, params)
else:
await self.pools.conns[connI].client.query(sql)
if qr.rowCount > 0 and qr.rows[0].len > 0: if qr.rowCount > 0 and qr.rows[0].len > 0:
return qr.rows[0][0] return qr.rows[0][0]
return "" return ""
@@ -423,11 +499,11 @@ proc getColumns(self: BaradbQuery, queryString: string, args = newJArray()): Fut
if connI == errorConnectionNum: if connI == errorConnectionNum:
raisePoolTimeout(self) raisePoolTimeout(self)
var arr: seq[JsonNode] let params = placeholdersToWireValues(args)
for arg in args.items: let qr = if params.len > 0:
arr.add(arg["value"]) await self.pools.conns[connI].client.query(queryString, params)
let sql = formatSql(queryString, arr) else:
let qr = await self.pools.conns[connI].client.query(sql) await self.pools.conns[connI].client.query(queryString)
if qr.rowCount > 0: if qr.rowCount > 0:
return qr.rows[0] return qr.rows[0]
return @[] return @[]
@@ -517,7 +593,9 @@ proc firstPlain*(self: BaradbQuery): Future[seq[string]] {.async.} =
let sql = self.selectFirstBuilder() let sql = self.selectFirstBuilder()
try: try:
self.log.logger(sql) self.log.logger(sql)
return await self.getRowPlain(sql, self.placeHolder) let row = await self.getRowPlain(sql, self.placeHolder)
if row.isSome: return row.get()
return @[]
except CatchableError: except CatchableError:
self.log.echoErrorMsg(sql) self.log.echoErrorMsg(sql)
self.log.echoErrorMsg(getCurrentExceptionMsg()) self.log.echoErrorMsg(getCurrentExceptionMsg())
@@ -529,7 +607,9 @@ proc findPlain*(self: BaradbQuery, id: string, key = "id"): Future[seq[string]]
let sql = self.selectFindBuilder(key) let sql = self.selectFindBuilder(key)
try: try:
self.log.logger(sql) self.log.logger(sql)
return await self.getRowPlain(sql, self.placeHolder) let row = await self.getRowPlain(sql, self.placeHolder)
if row.isSome: return row.get()
return @[]
except CatchableError: except CatchableError:
self.log.echoErrorMsg(sql) self.log.echoErrorMsg(sql)
self.log.echoErrorMsg(getCurrentExceptionMsg()) self.log.echoErrorMsg(getCurrentExceptionMsg())
@@ -10,15 +10,25 @@ import ../../../query_builder/models/baradb/baradb_query
import ../../../query_builder/models/baradb/baradb_exec import ../../../query_builder/models/baradb/baradb_exec
proc getTableInfo(rdb: BaradbConnections): Future[Table[string, seq[tuple[name: string, typ: string]]]] {.async.} = proc getTableInfo(rdb: BaradbConnections): Future[Table[string, seq[tuple[name: string, typ: string]]]] {.async.} =
## Introspect BaraDB tables using native SHOW TABLES / SHOW COLUMNS commands.
var tablesInfo = initTable[string, seq[tuple[name: string, typ: string]]]() var tablesInfo = initTable[string, seq[tuple[name: string, typ: string]]]()
let tables = await rdb.raw("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'").get()
# SHOW TABLES — get all table names from the server
let tables = await rdb.raw("SHOW TABLES").get()
for table in tables: for table in tables:
let tableName = table["table_name"].getStr() let tableName = table["name"].getStr()
let query = "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = ? ORDER BY ordinal_position" # Skip internal schema tables
let columns = await rdb.raw(query, %*[tableName]).get() if tableName.startsWith("_") and tableName != "_allographer_migrations":
continue
# SHOW COLUMNS FROM — get column definitions for this table
let descQuery = "SHOW COLUMNS FROM `" & tableName & "`"
let columns = await rdb.raw(descQuery).get()
var columnInfo: seq[tuple[name: string, typ: string]] var columnInfo: seq[tuple[name: string, typ: string]]
for col in columns: for col in columns:
columnInfo.add((name: col["column_name"].getStr(), typ: col["data_type"].getStr())) let colName = col["column_name"].getStr()
let colType = col["data_type"].getStr()
columnInfo.add((name: colName, typ: colType))
tablesInfo[tableName] = columnInfo tablesInfo[tableName] = columnInfo
return tablesInfo return tablesInfo
@@ -33,15 +43,18 @@ proc generateSchemaCode(tablesInfo: Table[string, seq[tuple[name: string, typ: s
for col in columns: for col in columns:
let nimType = let nimType =
case col.typ.toLower() case col.typ.toLower()
of "smallint", "integer", "bigint", "serial": of "smallint", "integer", "bigint", "serial", "int", "int8", "int16", "int32", "int64":
"int" "int"
of "character", "character varying", "text", "date", "timestamp without time zone", "time without time zone", "bytea", "varchar": of "character", "character varying", "text", "date",
"timestamp without time zone", "time without time zone", "bytea",
"varchar", "string", "fkstring":
"string" "string"
of "boolean": of "boolean", "bool":
"bool" "bool"
of "numeric", "double precision", "real": of "numeric", "double precision", "real", "float", "float32", "float64",
"double":
"float" "float"
of "json", "jsonb": of "json", "jsonb", "jsonnode":
"JsonNode" "JsonNode"
else: else:
"string" "string"
+3
View File
@@ -49,6 +49,7 @@ type
nkDropDatabase nkDropDatabase
nkUseDatabase nkUseDatabase
nkShowDatabases nkShowDatabases
nkShowTables
# Clauses # Clauses
nkFrom nkFrom
@@ -355,6 +356,8 @@ type
udDbName*: string udDbName*: string
of nkShowDatabases: of nkShowDatabases:
discard discard
of nkShowTables:
stTableName*: string # empty = list tables; non-empty = describe table
of nkApplyMigration: of nkApplyMigration:
amName*: string amName*: string
of nkMigrationStatus: of nkMigrationStatus:
+30
View File
@@ -1,4 +1,5 @@
## BaraQL Executor — AST lowering, IR compilation, and execution ## BaraQL Executor — AST lowering, IR compilation, and execution
import std/os
import std/strutils import std/strutils
import std/tables import std/tables
import std/hashes import std/hashes
@@ -421,6 +422,7 @@ proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext =
cteTables: initTable[string, seq[Row]](), cteTables: initTable[string, seq[Row]](),
ftsIndexes: ctx.ftsIndexes, ftsIndexes: ctx.ftsIndexes,
vectorIndexes: ctx.vectorIndexes, vectorIndexes: ctx.vectorIndexes,
graphs: ctx.graphs,
users: ctx.users, policies: ctx.policies, users: ctx.users, policies: ctx.policies,
txnManager: ctx.txnManager, txnManager: ctx.txnManager,
currentUser: ctx.currentUser, currentRole: ctx.currentRole, currentUser: ctx.currentUser, currentRole: ctx.currentRole,
@@ -428,6 +430,8 @@ proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext =
autoIncCounters: ctx.autoIncCounters, autoIncCounters: ctx.autoIncCounters,
sequences: ctx.sequences, sequences: ctx.sequences,
pendingTxn: nil, onChange: ctx.onChange, pendingTxn: nil, onChange: ctx.onChange,
embedder: ctx.embedder,
llmClient: ctx.llmClient,
currentDatabase: ctx.currentDatabase, currentDatabase: ctx.currentDatabase,
registry: ctx.registry) registry: ctx.registry)
result.sharedLock = ctx.sharedLock result.sharedLock = ctx.sharedLock
@@ -5630,6 +5634,32 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
rows.add(row) rows.add(row)
return okResult(rows, @["name"]) return okResult(rows, @["name"])
of nkShowTables:
if stmt.stTableName.len == 0:
# SHOW TABLES — list all tables
var rows: seq[Row] = @[]
for tableName in ctx.tables.keys:
var row = initTable[string, Value]()
row["name"] = tableName
rows.add(row)
return okResult(rows, @["name"])
else:
# SHOW COLUMNS FROM table — describe a specific table
var rows: seq[Row] = @[]
let tbl = ctx.getTableDef(stmt.stTableName)
for col in tbl.columns:
var row = initTable[string, Value]()
row["column_name"] = col.name
row["data_type"] = col.colType
row["is_nullable"] = if col.isNotNull: "NO" else: "YES"
row["is_primary_key"] = if col.isPk: "YES" else: "NO"
if col.defaultVal.len > 0:
row["column_default"] = col.defaultVal
else:
row["column_default"] = ""
rows.add(row)
return okResult(rows, @["column_name", "data_type", "is_nullable", "is_primary_key", "column_default"])
else: else:
return errResult("Unsupported statement type: " & $stmt.kind) return errResult("Unsupported statement type: " & $stmt.kind)
+69 -24
View File
@@ -34,6 +34,18 @@ proc match(p: var Parser, kind: TokenKind): bool =
return true return true
return false return false
# Token kinds that can also serve as identifiers in table/column name positions
const identLikeKinds = {tkIdent, tkLabels, tkCount, tkSum, tkAvg, tkMin, tkMax, tkArrayAgg, tkStringAgg}
proc expectIdent(p: var Parser): Token =
## Expect a token that can serve as an identifier (table name, column name, alias, etc.).
## Accepts tkIdent plus keyword tokens that are commonly used as names.
let tok = p.peek()
if tok.kind in identLikeKinds:
return p.advance()
raise newException(ValueError,
"Expected identifier but got " & $tok.kind & " at line " & $tok.line)
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 parseStatement*(p: var Parser): Node
@@ -69,7 +81,7 @@ proc parsePrimary(p: var Parser): Node =
of tkCurrentRole: of tkCurrentRole:
discard p.advance() discard p.advance()
Node(kind: nkCurrentRole, line: tok.line, col: tok.col) Node(kind: nkCurrentRole, line: tok.line, col: tok.col)
of tkIdent, tkRowNumber, tkRank, tkDenseRank, tkLead, tkLag, tkFirstValue, tkLastValue, tkNtile: of tkIdent, tkLabels, tkRowNumber, tkRank, tkDenseRank, tkLead, tkLag, tkFirstValue, tkLastValue, tkNtile:
discard p.advance() discard p.advance()
let funcName = tok.value let funcName = tok.value
# Check for function call: ident(...) # Check for function call: ident(...)
@@ -139,6 +151,17 @@ proc parsePrimary(p: var Parser): Node =
let operand = p.parsePrimary() let operand = p.parsePrimary()
Node(kind: nkUnaryOp, unOp: ukNeg, unOperand: operand, line: tok.line, col: tok.col) Node(kind: nkUnaryOp, unOp: ukNeg, unOperand: operand, line: tok.line, col: tok.col)
of tkCount, tkSum, tkAvg, tkMin, tkMax, tkArrayAgg, tkStringAgg: of tkCount, tkSum, tkAvg, tkMin, tkMax, tkArrayAgg, tkStringAgg:
# If not followed by '(', treat as a regular identifier (column reference)
if p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind != tkLParen:
discard p.advance()
let identName = tok.value
var parts = @[identName]
while p.peek().kind == tkDot:
discard p.advance()
parts.add(p.expectIdent().value)
if parts.len == 1:
return Node(kind: nkIdent, identName: identName, line: tok.line, col: tok.col)
return Node(kind: nkPath, pathParts: parts, line: tok.line, col: tok.col)
let funcName = tok.value let funcName = tok.value
discard p.advance() discard p.advance()
discard p.expect(tkLParen) discard p.expect(tkLParen)
@@ -1038,7 +1061,7 @@ proc parseCreateTable(p: var Parser): Node =
discard p.expect(tkExists) discard p.expect(tkExists)
result.crtIfNotExists = true result.crtIfNotExists = true
result.crtName = p.expect(tkIdent).value result.crtName = p.expectIdent().value
discard p.expect(tkLParen) discard p.expect(tkLParen)
while p.peek().kind != tkRParen and p.peek().kind != tkEof: while p.peek().kind != tkRParen and p.peek().kind != tkEof:
@@ -1053,26 +1076,26 @@ proc parseCreateTable(p: var Parser): Node =
cst.cstType = "pkey" cst.cstType = "pkey"
if p.peek().kind == tkLParen: if p.peek().kind == tkLParen:
discard p.advance() discard p.advance()
cst.cstColumns.add(p.expect(tkIdent).value) cst.cstColumns.add(p.expectIdent().value)
while p.match(tkComma): while p.match(tkComma):
cst.cstColumns.add(p.expect(tkIdent).value) cst.cstColumns.add(p.expectIdent().value)
discard p.expect(tkRParen) discard p.expect(tkRParen)
elif p.match(tkForeign): elif p.match(tkForeign):
discard p.expect(tkKey) discard p.expect(tkKey)
cst.cstType = "fkey" cst.cstType = "fkey"
if p.peek().kind == tkLParen: if p.peek().kind == tkLParen:
discard p.advance() discard p.advance()
cst.cstColumns.add(p.expect(tkIdent).value) cst.cstColumns.add(p.expectIdent().value)
while p.match(tkComma): while p.match(tkComma):
cst.cstColumns.add(p.expect(tkIdent).value) cst.cstColumns.add(p.expectIdent().value)
discard p.expect(tkRParen) discard p.expect(tkRParen)
discard p.expect(tkReferences) discard p.expect(tkReferences)
cst.cstRefTable = p.expect(tkIdent).value cst.cstRefTable = p.expectIdent().value
if p.peek().kind == tkLParen: if p.peek().kind == tkLParen:
discard p.advance() discard p.advance()
cst.cstRefColumns.add(p.expect(tkIdent).value) cst.cstRefColumns.add(p.expectIdent().value)
while p.match(tkComma): while p.match(tkComma):
cst.cstRefColumns.add(p.expect(tkIdent).value) cst.cstRefColumns.add(p.expectIdent().value)
discard p.expect(tkRParen) discard p.expect(tkRParen)
if p.peek().kind == tkOn: if p.peek().kind == tkOn:
discard p.advance() discard p.advance()
@@ -1116,9 +1139,9 @@ proc parseCreateTable(p: var Parser): Node =
cst.cstType = "unique" cst.cstType = "unique"
if p.peek().kind == tkLParen: if p.peek().kind == tkLParen:
discard p.advance() discard p.advance()
cst.cstColumns.add(p.expect(tkIdent).value) cst.cstColumns.add(p.expectIdent().value)
while p.match(tkComma): while p.match(tkComma):
cst.cstColumns.add(p.expect(tkIdent).value) cst.cstColumns.add(p.expectIdent().value)
discard p.expect(tkRParen) discard p.expect(tkRParen)
elif p.match(tkCheck): elif p.match(tkCheck):
cst.cstType = "check" cst.cstType = "check"
@@ -1129,7 +1152,7 @@ proc parseCreateTable(p: var Parser): Node =
continue continue
# Parse column definition # Parse column definition
let colName = p.expect(tkIdent).value let colName = p.expectIdent().value
var colType = "" var colType = ""
if p.peek().kind == tkIdent: if p.peek().kind == tkIdent:
colType = p.advance().value.toUpper() colType = p.advance().value.toUpper()
@@ -1185,12 +1208,12 @@ proc parseCreateTable(p: var Parser): Node =
cst.cstDefault = p.parseExpr() cst.cstDefault = p.parseExpr()
elif p.match(tkReferences): elif p.match(tkReferences):
cst.cstType = "fkey" cst.cstType = "fkey"
cst.cstRefTable = p.expect(tkIdent).value cst.cstRefTable = p.expectIdent().value
if p.peek().kind == tkLParen: if p.peek().kind == tkLParen:
discard p.advance() discard p.advance()
cst.cstRefColumns.add(p.expect(tkIdent).value) cst.cstRefColumns.add(p.expectIdent().value)
while p.match(tkComma): while p.match(tkComma):
cst.cstRefColumns.add(p.expect(tkIdent).value) cst.cstRefColumns.add(p.expectIdent().value)
discard p.expect(tkRParen) discard p.expect(tkRParen)
if p.peek().kind == tkOn: if p.peek().kind == tkOn:
discard p.advance() discard p.advance()
@@ -1232,12 +1255,12 @@ proc parseDropTable(p: var Parser): Node =
discard p.advance() discard p.advance()
discard p.expect(tkExists) discard p.expect(tkExists)
result.drtIfExists = true result.drtIfExists = true
result.drtName = p.expect(tkIdent).value result.drtName = p.expectIdent().value
proc parseAlterTable(p: var Parser): Node = proc parseAlterTable(p: var Parser): Node =
let tok = p.expect(tkAlter) let tok = p.expect(tkAlter)
discard p.expect(tkTable) discard p.expect(tkTable)
let tableName = p.expect(tkIdent).value let tableName = p.expectIdent().value
# Check for ENABLE/DISABLE ROW LEVEL SECURITY # Check for ENABLE/DISABLE ROW LEVEL SECURITY
if p.peek().kind == tkEnable: if p.peek().kind == tkEnable:
discard p.advance() discard p.advance()
@@ -1256,7 +1279,7 @@ proc parseAlterTable(p: var Parser): Node =
result.altOps = @[] result.altOps = @[]
if p.match(tkAdd): if p.match(tkAdd):
discard p.match(tkColumn) discard p.match(tkColumn)
let colName = p.expect(tkIdent).value let colName = p.expectIdent().value
var colType = "" var colType = ""
if p.peek().kind == tkIdent: if p.peek().kind == tkIdent:
colType = p.advance().value.toUpper() colType = p.advance().value.toUpper()
@@ -1273,16 +1296,16 @@ proc parseCreateIndex(p: var Parser): Node =
if p.peek().kind == tkIdent and p.peek().value.toLower() != "on": if p.peek().kind == tkIdent and p.peek().value.toLower() != "on":
idxName = p.advance().value idxName = p.advance().value
discard p.match(tkOn) discard p.match(tkOn)
let tableName = p.expect(tkIdent).value let tableName = p.expectIdent().value
discard p.match(tkLParen) discard p.match(tkLParen)
var colNames: seq[string] = @[] var colNames: seq[string] = @[]
colNames.add(p.expect(tkIdent).value) colNames.add(p.expectIdent().value)
while p.match(tkComma): while p.match(tkComma):
colNames.add(p.expect(tkIdent).value) colNames.add(p.expectIdent().value)
discard p.match(tkRParen) discard p.match(tkRParen)
var idxKind = ikBTree var idxKind = ikBTree
if p.match(tkUsing): if p.match(tkUsing):
let idxMethod = p.expect(tkIdent).value.toLower() let idxMethod = p.expectIdent().value.toLower()
if idxMethod == "fts" or idxMethod == "fulltext": if idxMethod == "fts" or idxMethod == "fulltext":
idxKind = ikFullText idxKind = ikFullText
elif idxMethod == "hnsw": elif idxMethod == "hnsw":
@@ -1754,6 +1777,19 @@ proc parseShowDatabases(p: var Parser): Node =
discard p.expect(tkDatabases) discard p.expect(tkDatabases)
result = Node(kind: nkShowDatabases, line: tok.line, col: tok.col) result = Node(kind: nkShowDatabases, line: tok.line, col: tok.col)
proc parseShowTables(p: var Parser): Node =
let tok = p.expect(tkShow)
discard p.expect(tkTable) # consume TABLES or TABLE
result = Node(kind: nkShowTables, line: tok.line, col: tok.col)
result.stTableName = ""
proc parseDescribeTable(p: var Parser): Node =
let tok = p.expect(tkShow)
discard p.expect(tkColumns)
discard p.expect(tkFrom)
result = Node(kind: nkShowTables, line: tok.line, col: tok.col)
result.stTableName = p.expectIdent().value
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()
@@ -1830,8 +1866,17 @@ proc parseStatement*(p: var Parser): Node =
of tkUse: of tkUse:
p.parseUseDatabase() p.parseUseDatabase()
of tkShow: of tkShow:
if p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind == tkDatabases: if p.pos + 1 < p.tokens.len:
p.parseShowDatabases() let next = p.tokens[p.pos + 1].kind
if next == tkDatabases:
p.parseShowDatabases()
elif next == tkTable:
p.parseShowTables()
elif next == tkColumns:
p.parseDescribeTable()
else:
let tok = p.advance()
Node(kind: nkNullLit, line: tok.line, col: tok.col)
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)