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
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:
+31
-64
@@ -2,103 +2,70 @@
|
||||
|
||||
> Дата: 2026-05-21
|
||||
> Поправени в тази сесия: компилационни грешки, `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.
|
||||
|
||||
**Възпроизвеждане:**
|
||||
```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.
|
||||
```
|
||||
|
||||
**Забележка:** Не е клиентски бъг. Клиентът изпраща коректно; сървърът крашва при обработка.
|
||||
**Файл:** `src/barabadb/query/executor.nim` — `cloneForConnection()`
|
||||
|
||||
---
|
||||
|
||||
### 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.
|
||||
|
||||
**Възпроизвеждане:**
|
||||
```sql
|
||||
CREATE TABLE test (
|
||||
id SERIAL PRIMARY KEY,
|
||||
label VARCHAR(255), -- грешка тук
|
||||
count INTEGER -- и тук
|
||||
);
|
||||
```
|
||||
|
||||
**Забележка:** Като workaround в клиента може да се използват backtick-ове (`` ` ``), но сървърът трябва да поддържа всякакви имена.
|
||||
**Файл:** `src/barabadb/query/parser.nim`
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Важни — качество и сигурност
|
||||
|
||||
### 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()`.
|
||||
|
||||
**Проблем:**
|
||||
```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.
|
||||
**Файл:** `clients/nim-allographer/.../baradb_exec.nim`
|
||||
|
||||
---
|
||||
|
||||
### 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`
|
||||
|
||||
**Решение:** Имплементирай BaraDB-специфична интроспекция — или чрез BaraQL команди, или чрез `PRAGMA`-подобни заявки ако сървърът ги поддържа.
|
||||
**Файлове:**
|
||||
- `src/barabadb/query/ast.nim` — добавен `nkShowTables` node
|
||||
- `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 | 2 | Сървърен parser error за резервирани думи | Сървърен екип |
|
||||
| P1 | 3 | SQL injection — преминаване към prepared statements | Клиентски екип |
|
||||
| P1 | 4 | Schema builder `information_schema` | Клиентски екип |
|
||||
| P2 | 5 | `getRowPlain()` semantics | Клиентски екип |
|
||||
| Приоритет | # | Проблем | Статус |
|
||||
|-----------|---|---------|--------|
|
||||
| ~~P0~~ | ~~1~~ | ~~Сървърен SIGSEGV при INSERT в транзакция~~ | ✅ Fixed |
|
||||
| ~~P0~~ | ~~2~~ | ~~Сървърен parser error за резервирани думи~~ | ✅ Fixed |
|
||||
| ~~P1~~ | ~~3~~ | ~~SQL injection~~ | ✅ Fixed |
|
||||
| ~~P1~~ | ~~4~~ | ~~Schema builder `information_schema`~~ | ✅ Fixed |
|
||||
| ~~P2~~ | ~~5~~ | ~~`getRowPlain()` semantics~~ | ✅ Fixed |
|
||||
|
||||
**Всички 5 бъга са поправени.** 🎉
|
||||
|
||||
+115
-35
@@ -167,6 +167,48 @@ proc formatSql*(sql: string, args: JsonNode): string =
|
||||
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
|
||||
# ================================================================================
|
||||
@@ -223,8 +265,11 @@ proc getAllRows(self: BaradbQuery, queryString: string): Future[seq[JsonNode]] {
|
||||
if connI == errorConnectionNum:
|
||||
raisePoolTimeout(self)
|
||||
|
||||
let sql = formatSql(queryString, self.placeHolder)
|
||||
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(queryString, params)
|
||||
else:
|
||||
await self.pools.conns[connI].client.query(queryString)
|
||||
|
||||
if qr.rowCount == 0:
|
||||
self.log.echoErrorMsg(queryString)
|
||||
@@ -242,8 +287,11 @@ proc getAllRowsPlain(self: BaradbQuery, queryString: string, args: JsonNode): Fu
|
||||
if connI == errorConnectionNum:
|
||||
raisePoolTimeout(self)
|
||||
|
||||
let sql = formatSql(queryString, args)
|
||||
let qr = await self.pools.conns[connI].client.query(sql)
|
||||
let params = placeholdersToWireValues(args)
|
||||
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
|
||||
|
||||
|
||||
@@ -257,8 +305,11 @@ proc getRow(self: BaradbQuery, queryString: string): Future[Option[JsonNode]] {.
|
||||
if connI == errorConnectionNum:
|
||||
raisePoolTimeout(self)
|
||||
|
||||
let sql = formatSql(queryString, self.placeHolder)
|
||||
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(queryString, params)
|
||||
else:
|
||||
await self.pools.conns[connI].client.query(queryString)
|
||||
|
||||
if qr.rowCount == 0:
|
||||
self.log.echoErrorMsg(queryString)
|
||||
@@ -266,7 +317,7 @@ proc getRow(self: BaradbQuery, queryString: string): Future[Option[JsonNode]] {.
|
||||
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
|
||||
if not self.isInTransaction:
|
||||
connI = getFreeConn(self).await
|
||||
@@ -276,11 +327,14 @@ proc getRowPlain(self: BaradbQuery, queryString: string, args: JsonNode): Future
|
||||
if connI == errorConnectionNum:
|
||||
raisePoolTimeout(self)
|
||||
|
||||
let sql = formatSql(queryString, args)
|
||||
let qr = await self.pools.conns[connI].client.query(sql)
|
||||
let params = placeholdersToWireValues(args)
|
||||
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:
|
||||
return qr.rows[0]
|
||||
return @[]
|
||||
return some(qr.rows[0])
|
||||
return none(seq[string])
|
||||
|
||||
|
||||
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]
|
||||
for arg in self.placeHolder.items:
|
||||
arr.add(arg)
|
||||
let sql = formatSql(queryString, arr)
|
||||
let qr = await self.pools.conns[connI].client.query(sql)
|
||||
let params = placeholdersToWireValuesRaw(arr)
|
||||
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:
|
||||
self.log.echoErrorMsg(queryString)
|
||||
@@ -318,8 +375,11 @@ proc getAllRowsPlain(self: RawBaradbQuery, queryString: string, args: JsonNode):
|
||||
var arr: seq[JsonNode]
|
||||
for arg in args.items:
|
||||
arr.add(arg)
|
||||
let sql = formatSql(queryString, arr)
|
||||
let qr = await self.pools.conns[connI].client.query(sql)
|
||||
let params = placeholdersToWireValuesRaw(arr)
|
||||
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
|
||||
|
||||
|
||||
@@ -336,8 +396,11 @@ proc getRow(self: RawBaradbQuery, queryString: string): Future[Option[JsonNode]]
|
||||
var arr: seq[JsonNode]
|
||||
for arg in self.placeHolder.items:
|
||||
arr.add(arg)
|
||||
let sql = formatSql(queryString, arr)
|
||||
let qr = await self.pools.conns[connI].client.query(sql)
|
||||
let params = placeholdersToWireValuesRaw(arr)
|
||||
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:
|
||||
self.log.echoErrorMsg(queryString)
|
||||
@@ -345,7 +408,7 @@ proc getRow(self: RawBaradbQuery, queryString: string): Future[Option[JsonNode]]
|
||||
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
|
||||
if not self.isInTransaction:
|
||||
connI = getFreeConn(self).await
|
||||
@@ -358,11 +421,14 @@ proc getRowPlain(self: RawBaradbQuery, queryString: string, args: JsonNode): Fut
|
||||
var arr: seq[JsonNode]
|
||||
for arg in args.items:
|
||||
arr.add(arg)
|
||||
let sql = formatSql(queryString, arr)
|
||||
let qr = await self.pools.conns[connI].client.query(sql)
|
||||
let params = placeholdersToWireValuesRaw(arr)
|
||||
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:
|
||||
return qr.rows[0]
|
||||
return @[]
|
||||
return some(qr.rows[0])
|
||||
return none(seq[string])
|
||||
|
||||
|
||||
proc exec(self: BaradbQuery, queryString: string) {.async.} =
|
||||
@@ -375,8 +441,11 @@ proc exec(self: BaradbQuery, queryString: string) {.async.} =
|
||||
if connI == errorConnectionNum:
|
||||
raisePoolTimeout(self)
|
||||
|
||||
let sql = formatSql(queryString, self.placeHolder)
|
||||
discard await self.pools.conns[connI].client.exec(sql)
|
||||
let params = placeholdersToWireValues(self.placeHolder)
|
||||
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.} =
|
||||
@@ -392,8 +461,11 @@ proc exec(self: RawBaradbQuery, queryString: string, args: JsonNode) {.async.} =
|
||||
var arr: seq[JsonNode]
|
||||
for arg in args.items:
|
||||
arr.add(arg)
|
||||
let sql = formatSql(queryString, arr)
|
||||
discard await self.pools.conns[connI].client.exec(sql)
|
||||
let params = placeholdersToWireValuesRaw(arr)
|
||||
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.} =
|
||||
@@ -406,8 +478,12 @@ proc insertId(self: BaradbQuery, queryString: string, key: string): Future[strin
|
||||
if connI == errorConnectionNum:
|
||||
raisePoolTimeout(self)
|
||||
|
||||
let sql = formatSql(queryString, self.placeHolder) & &" RETURNING `{key}`"
|
||||
let qr = await self.pools.conns[connI].client.query(sql)
|
||||
let sql = queryString & &" RETURNING `{key}`"
|
||||
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:
|
||||
return qr.rows[0][0]
|
||||
return ""
|
||||
@@ -423,11 +499,11 @@ proc getColumns(self: BaradbQuery, queryString: string, args = newJArray()): Fut
|
||||
if connI == errorConnectionNum:
|
||||
raisePoolTimeout(self)
|
||||
|
||||
var arr: seq[JsonNode]
|
||||
for arg in args.items:
|
||||
arr.add(arg["value"])
|
||||
let sql = formatSql(queryString, arr)
|
||||
let qr = await self.pools.conns[connI].client.query(sql)
|
||||
let params = placeholdersToWireValues(args)
|
||||
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:
|
||||
return qr.rows[0]
|
||||
return @[]
|
||||
@@ -517,7 +593,9 @@ proc firstPlain*(self: BaradbQuery): Future[seq[string]] {.async.} =
|
||||
let sql = self.selectFirstBuilder()
|
||||
try:
|
||||
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:
|
||||
self.log.echoErrorMsg(sql)
|
||||
self.log.echoErrorMsg(getCurrentExceptionMsg())
|
||||
@@ -529,7 +607,9 @@ proc findPlain*(self: BaradbQuery, id: string, key = "id"): Future[seq[string]]
|
||||
let sql = self.selectFindBuilder(key)
|
||||
try:
|
||||
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:
|
||||
self.log.echoErrorMsg(sql)
|
||||
self.log.echoErrorMsg(getCurrentExceptionMsg())
|
||||
|
||||
+23
-10
@@ -10,15 +10,25 @@ import ../../../query_builder/models/baradb/baradb_query
|
||||
import ../../../query_builder/models/baradb/baradb_exec
|
||||
|
||||
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]]]()
|
||||
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:
|
||||
let tableName = table["table_name"].getStr()
|
||||
let query = "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = ? ORDER BY ordinal_position"
|
||||
let columns = await rdb.raw(query, %*[tableName]).get()
|
||||
let tableName = table["name"].getStr()
|
||||
# Skip internal schema tables
|
||||
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]]
|
||||
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
|
||||
return tablesInfo
|
||||
|
||||
@@ -33,15 +43,18 @@ proc generateSchemaCode(tablesInfo: Table[string, seq[tuple[name: string, typ: s
|
||||
for col in columns:
|
||||
let nimType =
|
||||
case col.typ.toLower()
|
||||
of "smallint", "integer", "bigint", "serial":
|
||||
of "smallint", "integer", "bigint", "serial", "int", "int8", "int16", "int32", "int64":
|
||||
"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"
|
||||
of "boolean":
|
||||
of "boolean", "bool":
|
||||
"bool"
|
||||
of "numeric", "double precision", "real":
|
||||
of "numeric", "double precision", "real", "float", "float32", "float64",
|
||||
"double":
|
||||
"float"
|
||||
of "json", "jsonb":
|
||||
of "json", "jsonb", "jsonnode":
|
||||
"JsonNode"
|
||||
else:
|
||||
"string"
|
||||
|
||||
@@ -49,6 +49,7 @@ type
|
||||
nkDropDatabase
|
||||
nkUseDatabase
|
||||
nkShowDatabases
|
||||
nkShowTables
|
||||
|
||||
# Clauses
|
||||
nkFrom
|
||||
@@ -355,6 +356,8 @@ type
|
||||
udDbName*: string
|
||||
of nkShowDatabases:
|
||||
discard
|
||||
of nkShowTables:
|
||||
stTableName*: string # empty = list tables; non-empty = describe table
|
||||
of nkApplyMigration:
|
||||
amName*: string
|
||||
of nkMigrationStatus:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
## BaraQL Executor — AST lowering, IR compilation, and execution
|
||||
import std/os
|
||||
import std/strutils
|
||||
import std/tables
|
||||
import std/hashes
|
||||
@@ -421,6 +422,7 @@ proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext =
|
||||
cteTables: initTable[string, seq[Row]](),
|
||||
ftsIndexes: ctx.ftsIndexes,
|
||||
vectorIndexes: ctx.vectorIndexes,
|
||||
graphs: ctx.graphs,
|
||||
users: ctx.users, policies: ctx.policies,
|
||||
txnManager: ctx.txnManager,
|
||||
currentUser: ctx.currentUser, currentRole: ctx.currentRole,
|
||||
@@ -428,6 +430,8 @@ proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext =
|
||||
autoIncCounters: ctx.autoIncCounters,
|
||||
sequences: ctx.sequences,
|
||||
pendingTxn: nil, onChange: ctx.onChange,
|
||||
embedder: ctx.embedder,
|
||||
llmClient: ctx.llmClient,
|
||||
currentDatabase: ctx.currentDatabase,
|
||||
registry: ctx.registry)
|
||||
result.sharedLock = ctx.sharedLock
|
||||
@@ -5630,6 +5634,32 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
|
||||
rows.add(row)
|
||||
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:
|
||||
return errResult("Unsupported statement type: " & $stmt.kind)
|
||||
|
||||
|
||||
@@ -34,6 +34,18 @@ proc match(p: var Parser, kind: TokenKind): bool =
|
||||
return true
|
||||
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 parseSelect(p: var Parser): Node
|
||||
proc parseStatement*(p: var Parser): Node
|
||||
@@ -69,7 +81,7 @@ proc parsePrimary(p: var Parser): Node =
|
||||
of tkCurrentRole:
|
||||
discard p.advance()
|
||||
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()
|
||||
let funcName = tok.value
|
||||
# Check for function call: ident(...)
|
||||
@@ -139,6 +151,17 @@ proc parsePrimary(p: var Parser): Node =
|
||||
let operand = p.parsePrimary()
|
||||
Node(kind: nkUnaryOp, unOp: ukNeg, unOperand: operand, line: tok.line, col: tok.col)
|
||||
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
|
||||
discard p.advance()
|
||||
discard p.expect(tkLParen)
|
||||
@@ -1038,7 +1061,7 @@ proc parseCreateTable(p: var Parser): Node =
|
||||
discard p.expect(tkExists)
|
||||
result.crtIfNotExists = true
|
||||
|
||||
result.crtName = p.expect(tkIdent).value
|
||||
result.crtName = p.expectIdent().value
|
||||
discard p.expect(tkLParen)
|
||||
|
||||
while p.peek().kind != tkRParen and p.peek().kind != tkEof:
|
||||
@@ -1053,26 +1076,26 @@ proc parseCreateTable(p: var Parser): Node =
|
||||
cst.cstType = "pkey"
|
||||
if p.peek().kind == tkLParen:
|
||||
discard p.advance()
|
||||
cst.cstColumns.add(p.expect(tkIdent).value)
|
||||
cst.cstColumns.add(p.expectIdent().value)
|
||||
while p.match(tkComma):
|
||||
cst.cstColumns.add(p.expect(tkIdent).value)
|
||||
cst.cstColumns.add(p.expectIdent().value)
|
||||
discard p.expect(tkRParen)
|
||||
elif p.match(tkForeign):
|
||||
discard p.expect(tkKey)
|
||||
cst.cstType = "fkey"
|
||||
if p.peek().kind == tkLParen:
|
||||
discard p.advance()
|
||||
cst.cstColumns.add(p.expect(tkIdent).value)
|
||||
cst.cstColumns.add(p.expectIdent().value)
|
||||
while p.match(tkComma):
|
||||
cst.cstColumns.add(p.expect(tkIdent).value)
|
||||
cst.cstColumns.add(p.expectIdent().value)
|
||||
discard p.expect(tkRParen)
|
||||
discard p.expect(tkReferences)
|
||||
cst.cstRefTable = p.expect(tkIdent).value
|
||||
cst.cstRefTable = p.expectIdent().value
|
||||
if p.peek().kind == tkLParen:
|
||||
discard p.advance()
|
||||
cst.cstRefColumns.add(p.expect(tkIdent).value)
|
||||
cst.cstRefColumns.add(p.expectIdent().value)
|
||||
while p.match(tkComma):
|
||||
cst.cstRefColumns.add(p.expect(tkIdent).value)
|
||||
cst.cstRefColumns.add(p.expectIdent().value)
|
||||
discard p.expect(tkRParen)
|
||||
if p.peek().kind == tkOn:
|
||||
discard p.advance()
|
||||
@@ -1116,9 +1139,9 @@ proc parseCreateTable(p: var Parser): Node =
|
||||
cst.cstType = "unique"
|
||||
if p.peek().kind == tkLParen:
|
||||
discard p.advance()
|
||||
cst.cstColumns.add(p.expect(tkIdent).value)
|
||||
cst.cstColumns.add(p.expectIdent().value)
|
||||
while p.match(tkComma):
|
||||
cst.cstColumns.add(p.expect(tkIdent).value)
|
||||
cst.cstColumns.add(p.expectIdent().value)
|
||||
discard p.expect(tkRParen)
|
||||
elif p.match(tkCheck):
|
||||
cst.cstType = "check"
|
||||
@@ -1129,7 +1152,7 @@ proc parseCreateTable(p: var Parser): Node =
|
||||
continue
|
||||
|
||||
# Parse column definition
|
||||
let colName = p.expect(tkIdent).value
|
||||
let colName = p.expectIdent().value
|
||||
var colType = ""
|
||||
if p.peek().kind == tkIdent:
|
||||
colType = p.advance().value.toUpper()
|
||||
@@ -1185,12 +1208,12 @@ proc parseCreateTable(p: var Parser): Node =
|
||||
cst.cstDefault = p.parseExpr()
|
||||
elif p.match(tkReferences):
|
||||
cst.cstType = "fkey"
|
||||
cst.cstRefTable = p.expect(tkIdent).value
|
||||
cst.cstRefTable = p.expectIdent().value
|
||||
if p.peek().kind == tkLParen:
|
||||
discard p.advance()
|
||||
cst.cstRefColumns.add(p.expect(tkIdent).value)
|
||||
cst.cstRefColumns.add(p.expectIdent().value)
|
||||
while p.match(tkComma):
|
||||
cst.cstRefColumns.add(p.expect(tkIdent).value)
|
||||
cst.cstRefColumns.add(p.expectIdent().value)
|
||||
discard p.expect(tkRParen)
|
||||
if p.peek().kind == tkOn:
|
||||
discard p.advance()
|
||||
@@ -1232,12 +1255,12 @@ proc parseDropTable(p: var Parser): Node =
|
||||
discard p.advance()
|
||||
discard p.expect(tkExists)
|
||||
result.drtIfExists = true
|
||||
result.drtName = p.expect(tkIdent).value
|
||||
result.drtName = p.expectIdent().value
|
||||
|
||||
proc parseAlterTable(p: var Parser): Node =
|
||||
let tok = p.expect(tkAlter)
|
||||
discard p.expect(tkTable)
|
||||
let tableName = p.expect(tkIdent).value
|
||||
let tableName = p.expectIdent().value
|
||||
# Check for ENABLE/DISABLE ROW LEVEL SECURITY
|
||||
if p.peek().kind == tkEnable:
|
||||
discard p.advance()
|
||||
@@ -1256,7 +1279,7 @@ proc parseAlterTable(p: var Parser): Node =
|
||||
result.altOps = @[]
|
||||
if p.match(tkAdd):
|
||||
discard p.match(tkColumn)
|
||||
let colName = p.expect(tkIdent).value
|
||||
let colName = p.expectIdent().value
|
||||
var colType = ""
|
||||
if p.peek().kind == tkIdent:
|
||||
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":
|
||||
idxName = p.advance().value
|
||||
discard p.match(tkOn)
|
||||
let tableName = p.expect(tkIdent).value
|
||||
let tableName = p.expectIdent().value
|
||||
discard p.match(tkLParen)
|
||||
var colNames: seq[string] = @[]
|
||||
colNames.add(p.expect(tkIdent).value)
|
||||
colNames.add(p.expectIdent().value)
|
||||
while p.match(tkComma):
|
||||
colNames.add(p.expect(tkIdent).value)
|
||||
colNames.add(p.expectIdent().value)
|
||||
discard p.match(tkRParen)
|
||||
var idxKind = ikBTree
|
||||
if p.match(tkUsing):
|
||||
let idxMethod = p.expect(tkIdent).value.toLower()
|
||||
let idxMethod = p.expectIdent().value.toLower()
|
||||
if idxMethod == "fts" or idxMethod == "fulltext":
|
||||
idxKind = ikFullText
|
||||
elif idxMethod == "hnsw":
|
||||
@@ -1754,6 +1777,19 @@ proc parseShowDatabases(p: var Parser): Node =
|
||||
discard p.expect(tkDatabases)
|
||||
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 =
|
||||
case p.peek().kind
|
||||
of tkWith, tkSelect: p.parseSelect()
|
||||
@@ -1830,8 +1866,17 @@ proc parseStatement*(p: var Parser): Node =
|
||||
of tkUse:
|
||||
p.parseUseDatabase()
|
||||
of tkShow:
|
||||
if p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind == tkDatabases:
|
||||
p.parseShowDatabases()
|
||||
if p.pos + 1 < p.tokens.len:
|
||||
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:
|
||||
let tok = p.advance()
|
||||
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||
|
||||
Reference in New Issue
Block a user