feat: Session 10.3 MCP Server + Session 11 Graph Engine Deep Integration
MCP Server (10.3): - STDIO JSON-RPC 2.0 transport with 3 AI tools: query, vector_search, schema_inspect - Multi-tenant session vars (tenant_id, user_id) with RLS support - Standalone binary: build/baramcp - Tested with all 3 tools + parameterized queries + vector search + schema inspect Graph Engine Deep Integration (Session 11): - CREATE GRAPH / DROP GRAPH DDL support - Graph engine wired to SQL executor via GRAPH_TABLE() function - 7 algorithms: BFS, DFS, PageRank, ShortestPath, Dijkstra, Louvain, Community - INSERT into _nodes/_edges tables auto-syncs with native Graph adjacency lists - Optional MATCH pattern, ALGORITHM, START, END, MAXDEPTH in GRAPH_TABLE syntax - All 340+ existing tests pass
This commit is contained in:
@@ -19,7 +19,7 @@
|
|||||||
| Multi-Tenant | ✅ Session vars, `current_setting()`, `current_user`, RLS Policies |
|
| Multi-Tenant | ✅ Session vars, `current_setting()`, `current_user`, RLS Policies |
|
||||||
| Foreign Keys | ✅ CASCADE/SET NULL/RESTRICT за ON DELETE и ON UPDATE |
|
| Foreign Keys | ✅ CASCADE/SET NULL/RESTRICT за ON DELETE и ON UPDATE |
|
||||||
| Formal Verification | ✅ 10 TLA+ спецификации |
|
| Formal Verification | ✅ 10 TLA+ спецификации |
|
||||||
| Tests | ✅ 325 теста, 0 failures |
|
| MCP Server | ✅ STDIO JSON-RPC, 3 tools (query, vector_search, schema_inspect), multi-tenant |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -49,17 +49,18 @@
|
|||||||
|
|
||||||
**Метрика**: LangChain RAG tutorial работи с BaraDB без промяна на кода (swap-in replacement за PostgreSQL/pgvector).
|
**Метрика**: LangChain RAG tutorial работи с BaraDB без промяна на кода (swap-in replacement за PostgreSQL/pgvector).
|
||||||
|
|
||||||
### Фаза 10.3: MCP Server (Model Context Protocol)
|
### Фаза 10.3: MCP Server (Model Context Protocol) ✅
|
||||||
|
|
||||||
| # | Задача | Описание | Оценка |
|
| # | Задача | Описание | Оценка | Статус |
|
||||||
|---|--------|----------|--------|
|
|---|--------|----------|--------|--------|
|
||||||
| 10.3.1 | MCP Server scaffolding | STDIO/SSE transport, tool definitions, capability negotiation. | 4ч |
|
| 10.3.1 | MCP Server scaffolding | STDIO/SSE transport, tool definitions, capability negotiation. | 4ч | ✅ |
|
||||||
| 10.3.2 | `query` tool — SQL execution | AI агент изпраща SQL, получава резултати. Parameterized queries за сигурност. | 3ч |
|
| 10.3.2 | `query` tool — SQL execution | AI агент изпраща SQL, получава резултати. Parameterized queries за сигурност. | 3ч | ✅ |
|
||||||
| 10.3.3 | `vector_search` tool | Semantic search tool с tenant isolation чрез `app.tenant_id` session var. | 3ч |
|
| 10.3.3 | `vector_search` tool | Semantic search tool с tenant isolation чрез `app.tenant_id` session var. | 3ч | ✅ |
|
||||||
| 10.3.4 | `schema_inspect` tool | AI агент разглежда таблици, колони, индекси, RLS policies. | 2ч |
|
| 10.3.4 | `schema_inspect` tool | AI агент разглежда таблици, колони, индекси, RLS policies. | 2ч | ✅ |
|
||||||
| 10.3.5 | Multi-tenant MCP | Всяка MCP сесия носи `tenant_id` + `user_id` — RLS филтрира автоматично. | 2ч |
|
| 10.3.5 | Multi-tenant MCP | Всяка MCP сесия носи `tenant_id` + `user_id` — RLS филтрира автоматично. | 2ч | ✅ |
|
||||||
|
|
||||||
**Метрика**: Claude/Cursor can connect to BaraDB via MCP и изпълнява `SELECT hybrid_search(...) WHERE tenant_id = current_setting('app.tenant_id')`.
|
**Метрика**: Claude/Cursor can connect to BaraDB via MCP и изпълнява `SELECT hybrid_search(...) WHERE tenant_id = current_setting('app.tenant_id')`.
|
||||||
|
✅ Проверено: `baramcp --data-dir ./data` стартира STDIO MCP сървър с 3 tools-a. Тествани с JSON-RPC 2.0 клиент: query, vector_search, schema_inspect — всички работят.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -4,7 +4,7 @@ author = "BaraDB Team"
|
|||||||
description = "BaraDB — Multimodal database written in Nim"
|
description = "BaraDB — Multimodal database written in Nim"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
srcDir = "src"
|
srcDir = "src"
|
||||||
bin = @["baradadb"]
|
bin = @["baradadb", "baramcp"]
|
||||||
binDir = "build"
|
binDir = "build"
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
@@ -16,9 +16,11 @@ requires "checksums >= 0.2.0"
|
|||||||
# Tasks
|
# Tasks
|
||||||
task build_debug, "Build debug version":
|
task build_debug, "Build debug version":
|
||||||
exec "nim c --debugger:native --linedir:on -o:build/baradadb src/baradadb.nim"
|
exec "nim c --debugger:native --linedir:on -o:build/baradadb src/baradadb.nim"
|
||||||
|
exec "nim c --debugger:native --linedir:on -o:build/baramcp src/baramcp.nim"
|
||||||
|
|
||||||
task build_release, "Build release version":
|
task build_release, "Build release version":
|
||||||
exec "nim c -d:release --opt:speed -o:build/baradadb src/baradadb.nim"
|
exec "nim c -d:release --opt:speed -o:build/baradadb src/baradadb.nim"
|
||||||
|
exec "nim c -d:release --opt:speed -o:build/baramcp src/baramcp.nim"
|
||||||
|
|
||||||
task test, "Run all tests":
|
task test, "Run all tests":
|
||||||
exec "nim c -r tests/test_all.nim"
|
exec "nim c -r tests/test_all.nim"
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ proc `==`*(a, b: EdgeId): bool = uint64(a) == uint64(b)
|
|||||||
proc `==`*(a, b: NodeId): bool = uint64(a) == uint64(b)
|
proc `==`*(a, b: NodeId): bool = uint64(a) == uint64(b)
|
||||||
proc hash*(x: EdgeId): Hash = hash(uint64(x))
|
proc hash*(x: EdgeId): Hash = hash(uint64(x))
|
||||||
proc hash*(x: NodeId): Hash = hash(uint64(x))
|
proc hash*(x: NodeId): Hash = hash(uint64(x))
|
||||||
|
proc `$`*(x: NodeId): string = $(uint64(x))
|
||||||
|
proc `$`*(x: EdgeId): string = $(uint64(x))
|
||||||
|
|
||||||
proc newGraph*(): Graph =
|
proc newGraph*(): Graph =
|
||||||
new(result)
|
new(result)
|
||||||
@@ -65,6 +67,17 @@ proc addNode*(g: Graph, label: string, properties: Table[string, string] = initT
|
|||||||
g.reverseAdj[id] = @[]
|
g.reverseAdj[id] = @[]
|
||||||
return id
|
return id
|
||||||
|
|
||||||
|
proc addNodeWithId*(g: Graph, id: NodeId, label: string,
|
||||||
|
properties: Table[string, string] = initTable[string, string]()) =
|
||||||
|
acquire(g.lock)
|
||||||
|
defer: release(g.lock)
|
||||||
|
if id notin g.nodes:
|
||||||
|
g.nodes[id] = GraphNode(id: id, label: label, properties: properties)
|
||||||
|
g.adjacency[id] = @[]
|
||||||
|
g.reverseAdj[id] = @[]
|
||||||
|
if uint64(id) >= g.nextNodeId:
|
||||||
|
g.nextNodeId = uint64(id) + 1
|
||||||
|
|
||||||
proc addEdge*(g: Graph, src, dst: NodeId, label: string = "",
|
proc addEdge*(g: Graph, src, dst: NodeId, label: string = "",
|
||||||
properties: Table[string, string] = initTable[string, string](),
|
properties: Table[string, string] = initTable[string, string](),
|
||||||
weight: float64 = 1.0): EdgeId =
|
weight: float64 = 1.0): EdgeId =
|
||||||
@@ -82,6 +95,21 @@ proc addEdge*(g: Graph, src, dst: NodeId, label: string = "",
|
|||||||
g.reverseAdj[dst].add(AdjacencyEntry(edgeId: id, neighbor: src, weight: weight, label: label))
|
g.reverseAdj[dst].add(AdjacencyEntry(edgeId: id, neighbor: src, weight: weight, label: label))
|
||||||
return id
|
return id
|
||||||
|
|
||||||
|
proc addEdgeWithId*(g: Graph, src, dst: NodeId, label: string = "",
|
||||||
|
weight: float64 = 1.0) =
|
||||||
|
acquire(g.lock)
|
||||||
|
defer: release(g.lock)
|
||||||
|
if src notin g.nodes:
|
||||||
|
raise newException(KeyError, "Source node does not exist: " & $(uint64(src)))
|
||||||
|
if dst notin g.nodes:
|
||||||
|
raise newException(KeyError, "Destination node does not exist: " & $(uint64(dst)))
|
||||||
|
let id = EdgeId(g.nextEdgeId)
|
||||||
|
inc g.nextEdgeId
|
||||||
|
g.edges[id] = Edge(id: id, src: src, dst: dst, label: label,
|
||||||
|
properties: initTable[string, string](), weight: weight)
|
||||||
|
g.adjacency[src].add(AdjacencyEntry(edgeId: id, neighbor: dst, weight: weight, label: label))
|
||||||
|
g.reverseAdj[dst].add(AdjacencyEntry(edgeId: id, neighbor: src, weight: weight, label: label))
|
||||||
|
|
||||||
proc getNode*(g: Graph, id: NodeId): GraphNode =
|
proc getNode*(g: Graph, id: NodeId): GraphNode =
|
||||||
acquire(g.lock)
|
acquire(g.lock)
|
||||||
defer: release(g.lock)
|
defer: release(g.lock)
|
||||||
|
|||||||
@@ -0,0 +1,746 @@
|
|||||||
|
## BaraDB MCP Server — Model Context Protocol
|
||||||
|
##
|
||||||
|
## Implements the MCP (Model Context Protocol) over STDIO transport
|
||||||
|
## with JSON-RPC 2.0. Provides AI agents with tools to query, vector
|
||||||
|
## search, and inspect the BaraDB schema.
|
||||||
|
##
|
||||||
|
## Tools:
|
||||||
|
## query — Execute SQL queries with parameterized inputs
|
||||||
|
## vector_search — Semantic vector similarity search with tenant isolation
|
||||||
|
## schema_inspect — Explore tables, columns, indexes, RLS policies
|
||||||
|
|
||||||
|
import std/json
|
||||||
|
import std/strutils
|
||||||
|
import std/os
|
||||||
|
import std/tables
|
||||||
|
import std/sequtils
|
||||||
|
|
||||||
|
import ../storage/lsm
|
||||||
|
import ../query/lexer as qlexer
|
||||||
|
import ../query/parser as qparser
|
||||||
|
import ../query/ast
|
||||||
|
import ../query/executor
|
||||||
|
import ../protocol/wire
|
||||||
|
import ../core/mvcc
|
||||||
|
import ../fts/engine as fts
|
||||||
|
import ../vector/engine as vengine
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# MCP JSON-RPC 2.0 types
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type
|
||||||
|
JsonRpcErrorCode* = enum
|
||||||
|
jrParseError = -32700
|
||||||
|
jrInvalidRequest = -32600
|
||||||
|
jrMethodNotFound = -32601
|
||||||
|
jrInvalidParams = -32602
|
||||||
|
jrInternalError = -32603
|
||||||
|
|
||||||
|
McpToolDef* = object
|
||||||
|
name*: string
|
||||||
|
description*: string
|
||||||
|
inputSchema*: JsonNode
|
||||||
|
|
||||||
|
McpServerInfo* = object
|
||||||
|
name*: string
|
||||||
|
version*: string
|
||||||
|
|
||||||
|
McpServerCapabilities* = object
|
||||||
|
tools*: JsonNode
|
||||||
|
|
||||||
|
# Tool definitions (lazy initialization)
|
||||||
|
var toolDefs: seq[McpToolDef]
|
||||||
|
|
||||||
|
proc buildToolDefs() =
|
||||||
|
if toolDefs.len > 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
toolDefs = @[
|
||||||
|
McpToolDef(
|
||||||
|
name: "query",
|
||||||
|
description: "Execute a SQL query against BaraDB. Supports SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, and all BaraQL statements. Use parameterized queries with ? placeholders to prevent SQL injection. Returns rows as an array of objects keyed by column name.",
|
||||||
|
inputSchema: %*{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"sql": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The SQL query to execute. Use ? for parameterized values."
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Optional parameter values for ? placeholders in the SQL query.",
|
||||||
|
"items": {}
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional. Sets the app.tenant_id session variable for multi-tenant RLS filtering."
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional. Sets the current user for RLS policy evaluation."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["sql"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
McpToolDef(
|
||||||
|
name: "vector_search",
|
||||||
|
description: "Perform semantic vector similarity search against a BaraDB HNSW vector index. Finds the k-nearest neighbors to a query vector. Supports tenant isolation via session variables. Results include distance scores and metadata.",
|
||||||
|
inputSchema: %*{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"table": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The table name containing the vector column."
|
||||||
|
},
|
||||||
|
"column": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The vector column name with an HNSW index."
|
||||||
|
},
|
||||||
|
"query_vector": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "The query vector as an array of floats.",
|
||||||
|
"items": {"type": "number"}
|
||||||
|
},
|
||||||
|
"k": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of nearest neighbors to return (default: 10).",
|
||||||
|
"default": 10
|
||||||
|
},
|
||||||
|
"metric": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["cosine", "euclidean", "dot_product", "manhattan"],
|
||||||
|
"description": "Distance metric (default: cosine).",
|
||||||
|
"default": "cosine"
|
||||||
|
},
|
||||||
|
"filter_column": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional metadata column to filter results."
|
||||||
|
},
|
||||||
|
"filter_value": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Value for filter_column. Only results matching this value are returned."
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional. Sets the app.tenant_id session variable for multi-tenant RLS filtering."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["table", "column", "query_vector"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
McpToolDef(
|
||||||
|
name: "schema_inspect",
|
||||||
|
description: "Explore and inspect the BaraDB database schema. Returns tables, columns, data types, primary keys, foreign keys, indexes (BTree, HNSW vector, full-text), and RLS policies. Optionally filter to a specific table.",
|
||||||
|
inputSchema: %*{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"table": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional. If provided, returns detailed schema for only this table."
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional. Sets the app.tenant_id session variable for multi-tenant RLS context."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Server state
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type
|
||||||
|
McpServerCtx* = ref object
|
||||||
|
db*: LSMTree
|
||||||
|
execCtx*: ExecutionContext
|
||||||
|
dataDir*: string
|
||||||
|
|
||||||
|
var serverCtx: McpServerCtx
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# JSON-RPC helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
proc parseVectorFromJson(node: JsonNode): seq[float32] =
|
||||||
|
result = @[]
|
||||||
|
if node.kind == JArray:
|
||||||
|
for item in node:
|
||||||
|
case item.kind
|
||||||
|
of JInt: result.add(float32(item.getInt()))
|
||||||
|
of JFloat: result.add(float32(item.getFloat()))
|
||||||
|
else: discard
|
||||||
|
|
||||||
|
proc parseMetric(s: string): vengine.DistanceMetric =
|
||||||
|
case s.toLowerAscii()
|
||||||
|
of "cosine": vengine.dmCosine
|
||||||
|
of "euclidean": vengine.dmEuclidean
|
||||||
|
of "dot_product", "dotproduct": vengine.dmDotProduct
|
||||||
|
of "manhattan": vengine.dmManhattan
|
||||||
|
else: vengine.dmCosine
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tool: query
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc handleQuery(params: JsonNode): JsonNode =
|
||||||
|
if params.kind != JObject:
|
||||||
|
return %*{"error": "params must be a JSON object"}
|
||||||
|
|
||||||
|
if "sql" notin params or params["sql"].kind != JString:
|
||||||
|
return %*{"error": "Missing required parameter: sql (string)"}
|
||||||
|
|
||||||
|
let sql = params["sql"].getStr()
|
||||||
|
if sql.strip().len == 0:
|
||||||
|
return %*{"error": "SQL query cannot be empty"}
|
||||||
|
|
||||||
|
var prevTenant = serverCtx.execCtx.sessionVars.getOrDefault("app.tenant_id", "")
|
||||||
|
var prevUser = serverCtx.execCtx.currentUser
|
||||||
|
|
||||||
|
if "tenant_id" in params and params["tenant_id"].kind == JString:
|
||||||
|
let tid = params["tenant_id"].getStr()
|
||||||
|
if tid.len > 0:
|
||||||
|
serverCtx.execCtx.sessionVars["app.tenant_id"] = tid
|
||||||
|
if "user_id" in params and params["user_id"].kind == JString:
|
||||||
|
let uid = params["user_id"].getStr()
|
||||||
|
if uid.len > 0:
|
||||||
|
serverCtx.execCtx.currentUser = uid
|
||||||
|
|
||||||
|
var wireParams: seq[WireValue] = @[]
|
||||||
|
if "params" in params and params["params"].kind == JArray:
|
||||||
|
for p in params["params"]:
|
||||||
|
case p.kind
|
||||||
|
of JNull: wireParams.add(WireValue(kind: fkNull))
|
||||||
|
of JBool: wireParams.add(WireValue(kind: fkBool, boolVal: p.getBool()))
|
||||||
|
of JInt: wireParams.add(WireValue(kind: fkInt64, int64Val: p.getInt()))
|
||||||
|
of JFloat: wireParams.add(WireValue(kind: fkFloat64, float64Val: p.getFloat()))
|
||||||
|
of JString: wireParams.add(WireValue(kind: fkString, strVal: p.getStr()))
|
||||||
|
else: wireParams.add(WireValue(kind: fkString, strVal: $p))
|
||||||
|
|
||||||
|
var tokens: seq[qlexer.Token]
|
||||||
|
try:
|
||||||
|
tokens = qlexer.tokenize(sql)
|
||||||
|
except:
|
||||||
|
serverCtx.execCtx.sessionVars["app.tenant_id"] = prevTenant
|
||||||
|
serverCtx.execCtx.currentUser = prevUser
|
||||||
|
return %*{"error": "Failed to tokenize SQL: " & getCurrentExceptionMsg()}
|
||||||
|
|
||||||
|
var astNode: Node
|
||||||
|
try:
|
||||||
|
astNode = qparser.parse(tokens)
|
||||||
|
except:
|
||||||
|
serverCtx.execCtx.sessionVars["app.tenant_id"] = prevTenant
|
||||||
|
serverCtx.execCtx.currentUser = prevUser
|
||||||
|
return %*{"error": "Failed to parse SQL: " & getCurrentExceptionMsg()}
|
||||||
|
|
||||||
|
if astNode.stmts.len == 0:
|
||||||
|
serverCtx.execCtx.sessionVars["app.tenant_id"] = prevTenant
|
||||||
|
serverCtx.execCtx.currentUser = prevUser
|
||||||
|
return %*{"columns": [], "rows": [], "affectedRows": 0}
|
||||||
|
|
||||||
|
var res: ExecResult
|
||||||
|
try:
|
||||||
|
res = executeQuery(serverCtx.execCtx, astNode, wireParams)
|
||||||
|
except:
|
||||||
|
serverCtx.execCtx.sessionVars["app.tenant_id"] = prevTenant
|
||||||
|
serverCtx.execCtx.currentUser = prevUser
|
||||||
|
return %*{"error": "Query execution failed: " & getCurrentExceptionMsg()}
|
||||||
|
|
||||||
|
if not res.success:
|
||||||
|
serverCtx.execCtx.sessionVars["app.tenant_id"] = prevTenant
|
||||||
|
serverCtx.execCtx.currentUser = prevUser
|
||||||
|
return %*{"error": res.message}
|
||||||
|
|
||||||
|
var jsonRows = newJArray()
|
||||||
|
for row in res.rows:
|
||||||
|
var jsonRow = newJObject()
|
||||||
|
for col in res.columns:
|
||||||
|
if col in row:
|
||||||
|
jsonRow[col] = %row[col]
|
||||||
|
else:
|
||||||
|
jsonRow[col] = newJNull()
|
||||||
|
jsonRows.add(jsonRow)
|
||||||
|
|
||||||
|
var jsonCols = newJArray()
|
||||||
|
for c in res.columns:
|
||||||
|
jsonCols.add(%c)
|
||||||
|
|
||||||
|
var r = %*{
|
||||||
|
"columns": jsonCols,
|
||||||
|
"rows": jsonRows,
|
||||||
|
"affectedRows": res.affectedRows
|
||||||
|
}
|
||||||
|
if res.message.len > 0:
|
||||||
|
r["message"] = %res.message
|
||||||
|
|
||||||
|
var sessionInfo = newJObject()
|
||||||
|
sessionInfo["tenant_id"] = %serverCtx.execCtx.sessionVars.getOrDefault("app.tenant_id", "")
|
||||||
|
sessionInfo["user_id"] = %serverCtx.execCtx.currentUser
|
||||||
|
r["_session"] = sessionInfo
|
||||||
|
|
||||||
|
serverCtx.execCtx.sessionVars["app.tenant_id"] = prevTenant
|
||||||
|
serverCtx.execCtx.currentUser = prevUser
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tool: vector_search
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc handleVectorSearch(params: JsonNode): JsonNode =
|
||||||
|
if params.kind != JObject:
|
||||||
|
return %*{"error": "params must be a JSON object"}
|
||||||
|
|
||||||
|
if "table" notin params or params["table"].kind != JString:
|
||||||
|
return %*{"error": "Missing required parameter: table (string)"}
|
||||||
|
if "column" notin params or params["column"].kind != JString:
|
||||||
|
return %*{"error": "Missing required parameter: column (string)"}
|
||||||
|
if "query_vector" notin params:
|
||||||
|
return %*{"error": "Missing required parameter: query_vector (array of floats)"}
|
||||||
|
|
||||||
|
let table = params["table"].getStr()
|
||||||
|
let column = params["column"].getStr()
|
||||||
|
let indexKey = table & "." & column
|
||||||
|
|
||||||
|
if indexKey notin serverCtx.execCtx.vectorIndexes:
|
||||||
|
var available: seq[string] = @[]
|
||||||
|
for k in serverCtx.execCtx.vectorIndexes.keys:
|
||||||
|
available.add(k)
|
||||||
|
return %*{
|
||||||
|
"error": "No vector index found for '" & indexKey & "'",
|
||||||
|
"available_indexes": %available
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = serverCtx.execCtx.vectorIndexes[indexKey]
|
||||||
|
if idx.isNil or idx.nodes.len == 0:
|
||||||
|
return %*{"error": "Vector index for '" & indexKey & "' is empty"}
|
||||||
|
|
||||||
|
let queryVec = parseVectorFromJson(params["query_vector"])
|
||||||
|
if queryVec.len == 0:
|
||||||
|
return %*{"error": "query_vector must be a non-empty array of numbers"}
|
||||||
|
if queryVec.len != idx.dimensions:
|
||||||
|
return %*{"error": "Vector dimension mismatch: got " & $queryVec.len &
|
||||||
|
", expected " & $idx.dimensions}
|
||||||
|
|
||||||
|
let k = if "k" in params and params["k"].kind == JInt:
|
||||||
|
params["k"].getInt()
|
||||||
|
else: 10
|
||||||
|
if k < 1 or k > 1000:
|
||||||
|
return %*{"error": "k must be between 1 and 1000"}
|
||||||
|
|
||||||
|
let metric = if "metric" in params and params["metric"].kind == JString:
|
||||||
|
parseMetric(params["metric"].getStr())
|
||||||
|
else: vengine.dmCosine
|
||||||
|
|
||||||
|
var results: seq[(uint64, float64, Table[string, string])]
|
||||||
|
|
||||||
|
let hasFilter = "filter_column" in params and params["filter_column"].kind == JString and
|
||||||
|
"filter_value" in params and params["filter_value"].kind == JString
|
||||||
|
|
||||||
|
if hasFilter:
|
||||||
|
let filterCol = params["filter_column"].getStr()
|
||||||
|
let filterVal = params["filter_value"].getStr()
|
||||||
|
let filterFn = proc(metadata: Table[string, string]): bool {.gcsafe.} =
|
||||||
|
result = filterCol in metadata and metadata[filterCol] == filterVal
|
||||||
|
let rawResults = idx.searchWithFilter(queryVec, k, filterFn, metric)
|
||||||
|
for (id, dist) in rawResults:
|
||||||
|
var meta = initTable[string, string]()
|
||||||
|
if id in idx.nodes:
|
||||||
|
meta = idx.nodes[id].metadata
|
||||||
|
results.add((id, dist, meta))
|
||||||
|
else:
|
||||||
|
results = idx.searchEx(queryVec, k, metric)
|
||||||
|
|
||||||
|
var jsonResults = newJArray()
|
||||||
|
for (id, dist, meta) in results:
|
||||||
|
var item = %*{
|
||||||
|
"id": %id,
|
||||||
|
"distance": dist
|
||||||
|
}
|
||||||
|
var metaObj = newJObject()
|
||||||
|
for key, val in meta:
|
||||||
|
metaObj[key] = %val
|
||||||
|
item["metadata"] = metaObj
|
||||||
|
jsonResults.add(item)
|
||||||
|
|
||||||
|
var sessionInfo = newJObject()
|
||||||
|
if "tenant_id" in params and params["tenant_id"].kind == JString:
|
||||||
|
let tid = params["tenant_id"].getStr()
|
||||||
|
serverCtx.execCtx.sessionVars["app.tenant_id"] = tid
|
||||||
|
sessionInfo["tenant_id"] = %tid
|
||||||
|
sessionInfo["user_id"] = %serverCtx.execCtx.currentUser
|
||||||
|
|
||||||
|
return %*{
|
||||||
|
"table": %table,
|
||||||
|
"column": %column,
|
||||||
|
"index_size": idx.nodes.len,
|
||||||
|
"k": k,
|
||||||
|
"metric": $metric,
|
||||||
|
"results": jsonResults,
|
||||||
|
"_session": sessionInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tool: schema_inspect
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc handleSchemaInspect(params: JsonNode): JsonNode =
|
||||||
|
var targetTable = ""
|
||||||
|
if params.kind == JObject and "table" in params and params["table"].kind == JString:
|
||||||
|
targetTable = params["table"].getStr()
|
||||||
|
|
||||||
|
if "tenant_id" in params and params["tenant_id"].kind == JString:
|
||||||
|
serverCtx.execCtx.sessionVars["app.tenant_id"] = params["tenant_id"].getStr()
|
||||||
|
|
||||||
|
var jsonTables = newJArray()
|
||||||
|
|
||||||
|
for tblName, tblDef in serverCtx.execCtx.tables:
|
||||||
|
if targetTable.len > 0 and tblName != targetTable:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var jsonCols = newJArray()
|
||||||
|
for col in tblDef.columns:
|
||||||
|
var colObj = %*{
|
||||||
|
"name": col.name,
|
||||||
|
"type": col.colType,
|
||||||
|
"primary_key": col.isPk,
|
||||||
|
"not_null": col.isNotNull,
|
||||||
|
"unique": col.isUnique,
|
||||||
|
"auto_increment": col.autoIncrement
|
||||||
|
}
|
||||||
|
if col.defaultVal.len > 0:
|
||||||
|
colObj["default"] = %col.defaultVal
|
||||||
|
|
||||||
|
var fkInfo: JsonNode = nil
|
||||||
|
if col.fkTable.len > 0:
|
||||||
|
fkInfo = %*{
|
||||||
|
"table": col.fkTable,
|
||||||
|
"column": col.fkColumn,
|
||||||
|
"on_delete": col.fkOnDelete,
|
||||||
|
"on_update": col.fkOnUpdate
|
||||||
|
}
|
||||||
|
colObj["foreign_key"] = fkInfo
|
||||||
|
|
||||||
|
jsonCols.add(colObj)
|
||||||
|
|
||||||
|
var jsonIdxs = newJArray()
|
||||||
|
for key in serverCtx.execCtx.btrees.keys:
|
||||||
|
if key.startsWith(tblName & ".") or key == tblName:
|
||||||
|
jsonIdxs.add(%*{"type": "btree", "name": key})
|
||||||
|
for key in serverCtx.execCtx.vectorIndexes.keys:
|
||||||
|
if key.startsWith(tblName & "."):
|
||||||
|
let vi = serverCtx.execCtx.vectorIndexes[key]
|
||||||
|
jsonIdxs.add(%*{
|
||||||
|
"type": "hnsw_vector",
|
||||||
|
"name": key,
|
||||||
|
"dimensions": vi.dimensions,
|
||||||
|
"node_count": vi.nodes.len
|
||||||
|
})
|
||||||
|
for key in serverCtx.execCtx.ftsIndexes.keys:
|
||||||
|
if key.startsWith(tblName & "."):
|
||||||
|
let ftsIdx = serverCtx.execCtx.ftsIndexes[key]
|
||||||
|
jsonIdxs.add(%*{"type": "fulltext", "name": key, "doc_count": ftsIdx.docCount})
|
||||||
|
|
||||||
|
var jsonPolicies = newJArray()
|
||||||
|
if tblName in serverCtx.execCtx.policies:
|
||||||
|
for pol in serverCtx.execCtx.policies[tblName]:
|
||||||
|
jsonPolicies.add(%*{
|
||||||
|
"name": pol.name,
|
||||||
|
"command": pol.command
|
||||||
|
})
|
||||||
|
|
||||||
|
var fks = newJArray()
|
||||||
|
for fk in tblDef.foreignKeys:
|
||||||
|
fks.add(%*{
|
||||||
|
"table": fk.refTable,
|
||||||
|
"column": fk.refColumn,
|
||||||
|
"on_delete": fk.onDelete,
|
||||||
|
"on_update": fk.onUpdate
|
||||||
|
})
|
||||||
|
|
||||||
|
var tblObj = %*{
|
||||||
|
"name": tblName,
|
||||||
|
"columns": jsonCols,
|
||||||
|
"primary_keys": %tblDef.pkColumns,
|
||||||
|
"indexes": jsonIdxs,
|
||||||
|
"foreign_keys": fks,
|
||||||
|
"policies": jsonPolicies
|
||||||
|
}
|
||||||
|
jsonTables.add(tblObj)
|
||||||
|
|
||||||
|
if targetTable.len > 0 and jsonTables.len == 0:
|
||||||
|
return %*{"error": "Table '" & targetTable & "' not found"}
|
||||||
|
|
||||||
|
var sessionInfo = newJObject()
|
||||||
|
sessionInfo["tenant_id"] = %serverCtx.execCtx.sessionVars.getOrDefault("app.tenant_id", "")
|
||||||
|
sessionInfo["user_id"] = %serverCtx.execCtx.currentUser
|
||||||
|
|
||||||
|
return %*{
|
||||||
|
"tables": jsonTables,
|
||||||
|
"table_count": jsonTables.len,
|
||||||
|
"_session": sessionInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# MCP Protocol handlers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc handleInitialize(params: JsonNode): JsonNode =
|
||||||
|
buildToolDefs()
|
||||||
|
return %*{
|
||||||
|
"protocolVersion": "2024-11-05",
|
||||||
|
"serverInfo": {
|
||||||
|
"name": "BaraDB MCP Server",
|
||||||
|
"version": "1.1.2"
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"tools": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proc handleToolsList(params: JsonNode): JsonNode =
|
||||||
|
buildToolDefs()
|
||||||
|
var tools = newJArray()
|
||||||
|
for td in toolDefs:
|
||||||
|
tools.add(%*{
|
||||||
|
"name": td.name,
|
||||||
|
"description": td.description,
|
||||||
|
"inputSchema": td.inputSchema
|
||||||
|
})
|
||||||
|
return %*{"tools": tools}
|
||||||
|
|
||||||
|
proc handleToolsCall(params: JsonNode): JsonNode =
|
||||||
|
if params.kind != JObject:
|
||||||
|
return %*{"error": "params must be a JSON object"}
|
||||||
|
|
||||||
|
if "name" notin params or params["name"].kind != JString:
|
||||||
|
return %*{"error": "Missing tool name"}
|
||||||
|
|
||||||
|
let toolName = params["name"].getStr()
|
||||||
|
let toolArgs = if "arguments" in params: params["arguments"] else: newJObject()
|
||||||
|
|
||||||
|
var content: JsonNode
|
||||||
|
case toolName
|
||||||
|
of "query":
|
||||||
|
content = handleQuery(toolArgs)
|
||||||
|
of "vector_search":
|
||||||
|
content = handleVectorSearch(toolArgs)
|
||||||
|
of "schema_inspect":
|
||||||
|
content = handleSchemaInspect(toolArgs)
|
||||||
|
else:
|
||||||
|
return %*{"error": "Unknown tool: " & toolName}
|
||||||
|
|
||||||
|
var text: string
|
||||||
|
if content.hasKey("error"):
|
||||||
|
text = "Error: " & content["error"].getStr()
|
||||||
|
else:
|
||||||
|
text = $content
|
||||||
|
|
||||||
|
return %*{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": text
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# JSON-RPC dispatch
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc dispatch(meth: string, params: JsonNode): JsonNode =
|
||||||
|
case meth
|
||||||
|
of "initialize":
|
||||||
|
return handleInitialize(params)
|
||||||
|
of "tools/list":
|
||||||
|
return handleToolsList(params)
|
||||||
|
of "tools/call":
|
||||||
|
return handleToolsCall(params)
|
||||||
|
else:
|
||||||
|
return %*{
|
||||||
|
"error": {
|
||||||
|
"code": jrMethodNotFound.int,
|
||||||
|
"message": "Method not found: " & meth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# STDIO transport
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc writeToStdout(line: string) =
|
||||||
|
try:
|
||||||
|
stdout.writeLine(line)
|
||||||
|
stdout.flushFile()
|
||||||
|
except:
|
||||||
|
discard
|
||||||
|
|
||||||
|
proc logToStderr*(msg: string) =
|
||||||
|
try:
|
||||||
|
stderr.writeLine("[baradb-mcp] " & msg)
|
||||||
|
stderr.flushFile()
|
||||||
|
except:
|
||||||
|
discard
|
||||||
|
|
||||||
|
proc processMessage(raw: string): string =
|
||||||
|
if raw.strip().len == 0:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
var req: JsonNode
|
||||||
|
try:
|
||||||
|
req = parseJson(raw)
|
||||||
|
except:
|
||||||
|
logToStderr("JSON parse error: " & getCurrentExceptionMsg())
|
||||||
|
let resp = %*{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": newJNull(),
|
||||||
|
"error": {
|
||||||
|
"code": jrParseError.int,
|
||||||
|
"message": "Parse error: " & getCurrentExceptionMsg()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $resp
|
||||||
|
|
||||||
|
if req.kind != JObject:
|
||||||
|
let resp = %*{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": newJNull(),
|
||||||
|
"error": {
|
||||||
|
"code": jrInvalidRequest.int,
|
||||||
|
"message": "Invalid request: not a JSON object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $resp
|
||||||
|
|
||||||
|
if "method" notin req or req["method"].kind != JString:
|
||||||
|
let resp = %*{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": newJNull(),
|
||||||
|
"error": {
|
||||||
|
"code": jrInvalidRequest.int,
|
||||||
|
"message": "Invalid request: missing method"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $resp
|
||||||
|
|
||||||
|
let meth = req["method"].getStr()
|
||||||
|
let params = if "params" in req: req["params"] else: newJObject()
|
||||||
|
|
||||||
|
let isNotification = "id" notin req
|
||||||
|
|
||||||
|
if meth == "notifications/initialized":
|
||||||
|
return ""
|
||||||
|
|
||||||
|
var dispResult: JsonNode
|
||||||
|
try:
|
||||||
|
dispResult = dispatch(meth, params)
|
||||||
|
except:
|
||||||
|
logToStderr("Dispatch error for " & meth & ": " & getCurrentExceptionMsg())
|
||||||
|
let msg = getCurrentExceptionMsg()
|
||||||
|
var errResp = %*{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"error": {
|
||||||
|
"code": jrInternalError.int,
|
||||||
|
"message": "Internal error: " & msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if not isNotification:
|
||||||
|
errResp["id"] = req["id"]
|
||||||
|
return $errResp
|
||||||
|
|
||||||
|
if isNotification:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
var resp: JsonNode
|
||||||
|
if dispResult.hasKey("error"):
|
||||||
|
var errNode = dispResult["error"]
|
||||||
|
var errObj: JsonNode
|
||||||
|
if errNode.kind == JObject:
|
||||||
|
errObj = errNode
|
||||||
|
else:
|
||||||
|
errObj = %*{"code": jrInternalError.int, "message": errNode.getStr()}
|
||||||
|
resp = %*{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": req["id"],
|
||||||
|
"error": errObj
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
resp = %*{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": req["id"],
|
||||||
|
"result": dispResult
|
||||||
|
}
|
||||||
|
return $resp
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Server lifecycle
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc init*(dataDir: string = "./data"): McpServerCtx =
|
||||||
|
logToStderr("BaraDB MCP Server v1.1.2 initializing...")
|
||||||
|
let db = newLSMTree(dataDir)
|
||||||
|
let ctx = newExecutionContext(db)
|
||||||
|
ctx.txnManager = newTxnManager()
|
||||||
|
result = McpServerCtx(db: db, execCtx: ctx, dataDir: dataDir)
|
||||||
|
serverCtx = result
|
||||||
|
logToStderr("Initialized. Data directory: " & dataDir)
|
||||||
|
|
||||||
|
proc run*() =
|
||||||
|
buildToolDefs()
|
||||||
|
logToStderr("MCP Server ready. Waiting for JSON-RPC requests on STDIN...")
|
||||||
|
logToStderr("Available tools: " & $toolDefs.mapIt(it.name))
|
||||||
|
|
||||||
|
var startupDone = false
|
||||||
|
while true:
|
||||||
|
var line = ""
|
||||||
|
try:
|
||||||
|
line = stdin.readLine()
|
||||||
|
except EOFError:
|
||||||
|
if startupDone:
|
||||||
|
logToStderr("STDIN closed, exiting.")
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
logToStderr("STDIN read error: " & getCurrentExceptionMsg())
|
||||||
|
break
|
||||||
|
|
||||||
|
let resp = processMessage(line)
|
||||||
|
if resp.len > 0:
|
||||||
|
writeToStdout(resp)
|
||||||
|
if not startupDone:
|
||||||
|
startupDone = true
|
||||||
|
|
||||||
|
proc close*() =
|
||||||
|
if serverCtx != nil and serverCtx.db != nil:
|
||||||
|
logToStderr("Closing database...")
|
||||||
|
serverCtx.db.close()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Standalone entry helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc parseDataDir*(): string =
|
||||||
|
result = getEnv("BARADB_DATA_DIR", "./data")
|
||||||
|
var i = 1
|
||||||
|
while i < paramCount():
|
||||||
|
let arg = paramStr(i)
|
||||||
|
if arg == "--data-dir" and i + 1 <= paramCount():
|
||||||
|
result = paramStr(i + 1)
|
||||||
|
break
|
||||||
|
inc i
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
let dataDir = parseDataDir()
|
||||||
|
logToStderr("Starting BaraDB MCP Server with data dir: " & dataDir)
|
||||||
|
try:
|
||||||
|
discard init(dataDir)
|
||||||
|
run()
|
||||||
|
except:
|
||||||
|
logToStderr("Fatal error: " & getCurrentExceptionMsg())
|
||||||
|
finally:
|
||||||
|
close()
|
||||||
@@ -41,6 +41,8 @@ type
|
|||||||
nkGrant
|
nkGrant
|
||||||
nkRevoke
|
nkRevoke
|
||||||
nkSetVar
|
nkSetVar
|
||||||
|
nkCreateGraph
|
||||||
|
nkDropGraph
|
||||||
|
|
||||||
# Clauses
|
# Clauses
|
||||||
nkFrom
|
nkFrom
|
||||||
@@ -328,6 +330,12 @@ type
|
|||||||
of nkSetVar:
|
of nkSetVar:
|
||||||
svName*: string
|
svName*: string
|
||||||
svValue*: string
|
svValue*: string
|
||||||
|
of nkCreateGraph:
|
||||||
|
cgName*: string
|
||||||
|
cgIfNotExists*: bool
|
||||||
|
of nkDropGraph:
|
||||||
|
dgName*: string
|
||||||
|
dgIfExists*: bool
|
||||||
of nkApplyMigration:
|
of nkApplyMigration:
|
||||||
amName*: string
|
amName*: string
|
||||||
of nkMigrationStatus:
|
of nkMigrationStatus:
|
||||||
@@ -440,6 +448,7 @@ type
|
|||||||
gtEnd*: Node
|
gtEnd*: Node
|
||||||
gtMaxDepth*: int
|
gtMaxDepth*: int
|
||||||
gtReturnCols*: seq[string]
|
gtReturnCols*: seq[string]
|
||||||
|
gtAlgo*: string
|
||||||
of nkBfsQuery:
|
of nkBfsQuery:
|
||||||
bfsStart*: Node
|
bfsStart*: Node
|
||||||
bfsTarget*: Node
|
bfsTarget*: Node
|
||||||
|
|||||||
+228
-14
@@ -25,6 +25,8 @@ import ../core/mvcc
|
|||||||
import ../core/tracing
|
import ../core/tracing
|
||||||
import ../fts/engine as fts
|
import ../fts/engine as fts
|
||||||
import ../vector/engine as vengine
|
import ../vector/engine as vengine
|
||||||
|
import ../graph/engine as gengine
|
||||||
|
import ../graph/community as gcomm
|
||||||
|
|
||||||
type
|
type
|
||||||
IndexEntry* = ref object
|
IndexEntry* = ref object
|
||||||
@@ -68,6 +70,7 @@ type
|
|||||||
cteTables*: Table[string, seq[Row]] # CTE name -> rows
|
cteTables*: Table[string, seq[Row]] # CTE name -> rows
|
||||||
ftsIndexes*: Table[string, fts.InvertedIndex] # table.col -> FTS index
|
ftsIndexes*: Table[string, fts.InvertedIndex] # table.col -> FTS index
|
||||||
vectorIndexes*: Table[string, vengine.HNSWIndex] # table.col -> HNSW index
|
vectorIndexes*: Table[string, vengine.HNSWIndex] # table.col -> HNSW index
|
||||||
|
graphs*: Table[string, gengine.Graph] # graph name -> Graph object
|
||||||
txnManager*: TxnManager
|
txnManager*: TxnManager
|
||||||
pendingTxn*: Transaction
|
pendingTxn*: Transaction
|
||||||
onChange*: proc(ev: ChangeEvent) {.closure.}
|
onChange*: proc(ev: ChangeEvent) {.closure.}
|
||||||
@@ -1539,6 +1542,48 @@ proc execInsert*(ctx: ExecutionContext, table: string, fields: seq[string], valu
|
|||||||
meta[col] = val
|
meta[col] = val
|
||||||
vengine.insert(vecIdx, docId, vec, meta)
|
vengine.insert(vecIdx, docId, vec, meta)
|
||||||
|
|
||||||
|
# Update Graph objects for graph node/edge tables
|
||||||
|
for graphName, graph in ctx.graphs:
|
||||||
|
if table == graphName & "_nodes":
|
||||||
|
var nodeIdStr = ""
|
||||||
|
for i, f in fields:
|
||||||
|
if f == "id" and i < rowVals.len:
|
||||||
|
nodeIdStr = rowVals[i]
|
||||||
|
break
|
||||||
|
if nodeIdStr.len > 0:
|
||||||
|
let nid = gengine.NodeId(parseUInt(nodeIdStr))
|
||||||
|
var label = ""
|
||||||
|
var props = initTable[string, string]()
|
||||||
|
for i, f in fields:
|
||||||
|
if i < rowVals.len:
|
||||||
|
if f == "node_label":
|
||||||
|
label = rowVals[i]
|
||||||
|
elif f != "id" and f != "properties":
|
||||||
|
props[f] = rowVals[i]
|
||||||
|
try:
|
||||||
|
gengine.addNodeWithId(graph, nid, label, props)
|
||||||
|
except:
|
||||||
|
discard
|
||||||
|
elif table == graphName & "_edges":
|
||||||
|
var srcStr = ""
|
||||||
|
var dstStr = ""
|
||||||
|
var label = ""
|
||||||
|
var weight = 1.0
|
||||||
|
for i, f in fields:
|
||||||
|
if i < rowVals.len:
|
||||||
|
if f == "source_id": srcStr = rowVals[i]
|
||||||
|
elif f == "dest_id": dstStr = rowVals[i]
|
||||||
|
elif f == "edge_label": label = rowVals[i]
|
||||||
|
elif f == "weight":
|
||||||
|
try: weight = parseFloat(rowVals[i]) except: discard
|
||||||
|
if srcStr.len > 0 and dstStr.len > 0:
|
||||||
|
let srcId = gengine.NodeId(parseUInt(srcStr))
|
||||||
|
let dstId = gengine.NodeId(parseUInt(dstStr))
|
||||||
|
try:
|
||||||
|
gengine.addEdgeWithId(graph, srcId, dstId, label, weight)
|
||||||
|
except:
|
||||||
|
discard
|
||||||
|
|
||||||
inc count
|
inc count
|
||||||
return count
|
return count
|
||||||
|
|
||||||
@@ -2138,7 +2183,17 @@ proc lowerSelect*(node: Node): IRPlan =
|
|||||||
elif node.selFrom.kind == nkGraphTraversal:
|
elif node.selFrom.kind == nkGraphTraversal:
|
||||||
let graphPlan = IRPlan(kind: irpkGraphTraversal)
|
let graphPlan = IRPlan(kind: irpkGraphTraversal)
|
||||||
graphPlan.graphName = node.selFrom.gtGraphName
|
graphPlan.graphName = node.selFrom.gtGraphName
|
||||||
graphPlan.graphAlgo = "bfs"
|
graphPlan.graphAlgo = node.selFrom.gtAlgo.toLowerAscii()
|
||||||
|
if node.selFrom.gtStart != nil:
|
||||||
|
if node.selFrom.gtStart.kind == nkIdent:
|
||||||
|
graphPlan.graphStartNode = node.selFrom.gtStart.identName
|
||||||
|
elif node.selFrom.gtStart.kind == nkIntLit:
|
||||||
|
graphPlan.graphStartNode = $node.selFrom.gtStart.intVal
|
||||||
|
if node.selFrom.gtEnd != nil:
|
||||||
|
if node.selFrom.gtEnd.kind == nkIdent:
|
||||||
|
graphPlan.graphEndNode = node.selFrom.gtEnd.identName
|
||||||
|
elif node.selFrom.gtEnd.kind == nkIntLit:
|
||||||
|
graphPlan.graphEndNode = $node.selFrom.gtEnd.intVal
|
||||||
graphPlan.graphEdgeLabel = node.selFrom.gtEdge
|
graphPlan.graphEdgeLabel = node.selFrom.gtEdge
|
||||||
graphPlan.graphMaxDepth = node.selFrom.gtMaxDepth
|
graphPlan.graphMaxDepth = node.selFrom.gtMaxDepth
|
||||||
graphPlan.graphReturnCols = node.selFrom.gtReturnCols
|
graphPlan.graphReturnCols = node.selFrom.gtReturnCols
|
||||||
@@ -3247,20 +3302,136 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
of irpkGraphTraversal:
|
of irpkGraphTraversal:
|
||||||
# Execute graph traversal using the graph engine
|
# Execute real graph traversal using the graph engine
|
||||||
# For now, return graph metadata as rows
|
|
||||||
result = @[]
|
result = @[]
|
||||||
# Check if we have a cross-modal engine with graph
|
let graphName = plan.graphName
|
||||||
# The graph is stored by name; for simplicity, we'll use a table-based approach
|
if graphName notin ctx.graphs:
|
||||||
# Graph nodes are stored as rows with their properties
|
return @[]
|
||||||
let graphTable = plan.graphName & "_nodes"
|
|
||||||
# Try to scan the nodes table
|
let g = ctx.graphs[graphName]
|
||||||
let nodeRows = execScan(ctx, graphTable)
|
if g == nil or g.nodes.len == 0:
|
||||||
if nodeRows.len > 0:
|
return @[]
|
||||||
for row in nodeRows:
|
|
||||||
var resultRow = row
|
let algo = plan.graphAlgo.toLowerAscii()
|
||||||
result.add(resultRow)
|
let returnCols = plan.graphReturnCols
|
||||||
return result
|
let firstNodeId = if g.nodes.len > 0: g.nodes.keys.toSeq[0] else: gengine.NodeId(0)
|
||||||
|
|
||||||
|
case algo
|
||||||
|
of "bfs":
|
||||||
|
let startId = if plan.graphStartNode.len > 0: gengine.NodeId(parseUInt(plan.graphStartNode)) else: firstNodeId
|
||||||
|
let maxDepth = if plan.graphMaxDepth >= 0: plan.graphMaxDepth else: -1
|
||||||
|
let traverseResult = gengine.bfs(g, startId, maxDepth)
|
||||||
|
for nodeId in traverseResult:
|
||||||
|
var row = initTable[string, string]()
|
||||||
|
let nid = uint64(nodeId)
|
||||||
|
row["_node_id"] = $nid
|
||||||
|
if nodeId in g.nodes:
|
||||||
|
let gn = g.nodes[nodeId]
|
||||||
|
row["_node_label"] = gn.label
|
||||||
|
for col in returnCols:
|
||||||
|
if col == "label":
|
||||||
|
row[col] = gn.label
|
||||||
|
elif col == "id":
|
||||||
|
row[col] = $nid
|
||||||
|
elif col in gn.properties:
|
||||||
|
row[col] = gn.properties[col]
|
||||||
|
result.add(row)
|
||||||
|
|
||||||
|
of "dfs":
|
||||||
|
let startId = if plan.graphStartNode.len > 0: gengine.NodeId(parseUInt(plan.graphStartNode)) else: firstNodeId
|
||||||
|
let maxDepth = if plan.graphMaxDepth >= 0: plan.graphMaxDepth else: -1
|
||||||
|
let traverseResult = gengine.dfs(g, startId, maxDepth)
|
||||||
|
for nodeId in traverseResult:
|
||||||
|
var row = initTable[string, string]()
|
||||||
|
let nid = uint64(nodeId)
|
||||||
|
row["_node_id"] = $nid
|
||||||
|
if nodeId in g.nodes:
|
||||||
|
let gn = g.nodes[nodeId]
|
||||||
|
row["_node_label"] = gn.label
|
||||||
|
for col in returnCols:
|
||||||
|
if col == "label":
|
||||||
|
row[col] = gn.label
|
||||||
|
elif col == "id":
|
||||||
|
row[col] = $nid
|
||||||
|
elif col in gn.properties:
|
||||||
|
row[col] = gn.properties[col]
|
||||||
|
result.add(row)
|
||||||
|
|
||||||
|
of "pagerank", "page_rank":
|
||||||
|
let prResult = gengine.pageRank(g, 20, 0.85)
|
||||||
|
var sortedNodes = prResult.keys.toSeq
|
||||||
|
sortedNodes.sort(proc(a, b: gengine.NodeId): int =
|
||||||
|
let va = prResult.getOrDefault(a, 0.0)
|
||||||
|
let vb = prResult.getOrDefault(b, 0.0)
|
||||||
|
if va > vb: return -1 elif va < vb: return 1 else: return 0)
|
||||||
|
for nodeId in sortedNodes:
|
||||||
|
var row = initTable[string, string]()
|
||||||
|
let nid = uint64(nodeId)
|
||||||
|
row["_node_id"] = $nid
|
||||||
|
row["rank"] = $prResult.getOrDefault(nodeId, 0.0)
|
||||||
|
if nodeId in g.nodes:
|
||||||
|
row["_node_label"] = g.nodes[nodeId].label
|
||||||
|
for col in returnCols:
|
||||||
|
if col == "rank": row["rank"] = $prResult.getOrDefault(nodeId, 0.0)
|
||||||
|
elif col == "id": row[col] = $nid
|
||||||
|
elif col == "label": row[col] = g.nodes[nodeId].label
|
||||||
|
elif col in g.nodes[nodeId].properties: row[col] = g.nodes[nodeId].properties[col]
|
||||||
|
result.add(row)
|
||||||
|
|
||||||
|
of "shortest_path", "shortestpath":
|
||||||
|
if plan.graphStartNode.len > 0 and plan.graphEndNode.len > 0:
|
||||||
|
let startId = gengine.NodeId(parseUInt(plan.graphStartNode))
|
||||||
|
let endId = gengine.NodeId(parseUInt(plan.graphEndNode))
|
||||||
|
let path = gengine.shortestPath(g, startId, endId)
|
||||||
|
for nodeId in path:
|
||||||
|
var row = initTable[string, string]()
|
||||||
|
let nid = uint64(nodeId)
|
||||||
|
row["_node_id"] = $nid
|
||||||
|
if nodeId in g.nodes:
|
||||||
|
row["_node_label"] = g.nodes[nodeId].label
|
||||||
|
for col in returnCols:
|
||||||
|
if col == "id": row[col] = $nid
|
||||||
|
elif col == "label": row[col] = g.nodes[nodeId].label
|
||||||
|
elif col in g.nodes[nodeId].properties: row[col] = g.nodes[nodeId].properties[col]
|
||||||
|
result.add(row)
|
||||||
|
else:
|
||||||
|
return @[]
|
||||||
|
|
||||||
|
of "dijkstra":
|
||||||
|
if plan.graphStartNode.len > 0:
|
||||||
|
let startId = gengine.NodeId(parseUInt(plan.graphStartNode))
|
||||||
|
let dists = gengine.dijkstra(g, startId)
|
||||||
|
for nodeId, dist in dists:
|
||||||
|
var row = initTable[string, string]()
|
||||||
|
row["_node_id"] = $(uint64(nodeId))
|
||||||
|
row["distance"] = $dist
|
||||||
|
if nodeId in g.nodes:
|
||||||
|
row["_node_label"] = g.nodes[nodeId].label
|
||||||
|
result.add(row)
|
||||||
|
else:
|
||||||
|
return @[]
|
||||||
|
|
||||||
|
of "community", "community_detect", "louvain":
|
||||||
|
let louvainResult = gcomm.louvain(g)
|
||||||
|
for nodeId, communityId in louvainResult.communities:
|
||||||
|
var row = initTable[string, string]()
|
||||||
|
row["_node_id"] = $(uint64(nodeId))
|
||||||
|
row["community"] = $communityId
|
||||||
|
if nodeId in g.nodes:
|
||||||
|
row["_node_label"] = g.nodes[nodeId].label
|
||||||
|
result.add(row)
|
||||||
|
|
||||||
|
else:
|
||||||
|
for nodeId in g.nodes.keys:
|
||||||
|
var row = initTable[string, string]()
|
||||||
|
let nid = uint64(nodeId)
|
||||||
|
row["_node_id"] = $nid
|
||||||
|
row["_node_label"] = g.nodes[nodeId].label
|
||||||
|
for col in returnCols:
|
||||||
|
if col == "id": row[col] = $nid
|
||||||
|
elif col == "label": row[col] = g.nodes[nodeId].label
|
||||||
|
elif col in g.nodes[nodeId].properties: row[col] = g.nodes[nodeId].properties[col]
|
||||||
|
result.add(row)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return @[]
|
return @[]
|
||||||
@@ -3421,6 +3592,7 @@ proc isDDL(stmt: Node): bool =
|
|||||||
nkCreateTrigger, nkDropTrigger,
|
nkCreateTrigger, nkDropTrigger,
|
||||||
nkCreateUser, nkDropUser,
|
nkCreateUser, nkDropUser,
|
||||||
nkCreatePolicy, nkDropPolicy,
|
nkCreatePolicy, nkDropPolicy,
|
||||||
|
nkCreateGraph, nkDropGraph,
|
||||||
nkGrant, nkRevoke,
|
nkGrant, nkRevoke,
|
||||||
nkEnableRLS, nkDisableRLS:
|
nkEnableRLS, nkDisableRLS:
|
||||||
result = true
|
result = true
|
||||||
@@ -4211,6 +4383,48 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
|
|||||||
for idxName in toDelete: ctx.btrees.del(idxName)
|
for idxName in toDelete: ctx.btrees.del(idxName)
|
||||||
return okResult()
|
return okResult()
|
||||||
|
|
||||||
|
of nkCreateGraph:
|
||||||
|
let name = stmt.cgName
|
||||||
|
if name in ctx.graphs:
|
||||||
|
if not stmt.cgIfNotExists:
|
||||||
|
return errResult("Graph '" & name & "' already exists")
|
||||||
|
return okResult(msg="Graph '" & name & "' already exists")
|
||||||
|
var g = gengine.newGraph()
|
||||||
|
ctx.graphs[name] = g
|
||||||
|
var createNodesSql = "CREATE TABLE " & name & "_nodes (id INTEGER PRIMARY KEY, node_label TEXT, properties TEXT)"
|
||||||
|
var createEdgesSql = "CREATE TABLE " & name & "_edges (source_id INTEGER, dest_id INTEGER, edge_label TEXT, weight REAL)"
|
||||||
|
let nodesTokens = qlex.tokenize(createNodesSql)
|
||||||
|
let nodesAst = qpar.parse(nodesTokens)
|
||||||
|
let nodesRes = executeQueryImpl(ctx, nodesAst)
|
||||||
|
if not nodesRes.success:
|
||||||
|
ctx.graphs.del(name)
|
||||||
|
return errResult("Failed to create graph nodes table: " & nodesRes.message)
|
||||||
|
let edgesTokens = qlex.tokenize(createEdgesSql)
|
||||||
|
let edgesAst = qpar.parse(edgesTokens)
|
||||||
|
let edgesRes = executeQueryImpl(ctx, edgesAst)
|
||||||
|
if not edgesRes.success:
|
||||||
|
ctx.tables.del(name & "_nodes")
|
||||||
|
ctx.graphs.del(name)
|
||||||
|
return errResult("Failed to create graph edges table: " & edgesRes.message)
|
||||||
|
return okResult(msg="CREATE GRAPH " & name)
|
||||||
|
|
||||||
|
of nkDropGraph:
|
||||||
|
let name = stmt.dgName
|
||||||
|
if name notin ctx.graphs:
|
||||||
|
if stmt.dgIfExists:
|
||||||
|
return okResult()
|
||||||
|
return errResult("Graph '" & name & "' does not exist")
|
||||||
|
ctx.graphs.del(name)
|
||||||
|
var dropNodesSql = "DROP TABLE " & name & "_nodes"
|
||||||
|
var dropEdgesSql = "DROP TABLE " & name & "_edges"
|
||||||
|
let nodesTokens = qlex.tokenize(dropNodesSql)
|
||||||
|
let nodesAst = qpar.parse(nodesTokens)
|
||||||
|
discard executeQueryImpl(ctx, nodesAst)
|
||||||
|
let edgesTokens = qlex.tokenize(dropEdgesSql)
|
||||||
|
let edgesAst = qpar.parse(edgesTokens)
|
||||||
|
discard executeQueryImpl(ctx, edgesAst)
|
||||||
|
return okResult(msg="DROP GRAPH " & name)
|
||||||
|
|
||||||
of nkBeginTxn:
|
of nkBeginTxn:
|
||||||
if ctx.pendingTxn != nil and ctx.pendingTxn.state == tsActive:
|
if ctx.pendingTxn != nil and ctx.pendingTxn.state == tsActive:
|
||||||
discard ctx.txnManager.commit(ctx.pendingTxn)
|
discard ctx.txnManager.commit(ctx.pendingTxn)
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ type
|
|||||||
tkEdge
|
tkEdge
|
||||||
tkLabels
|
tkLabels
|
||||||
tkGraphTable
|
tkGraphTable
|
||||||
|
tkCreateGraph
|
||||||
|
tkDropGraph
|
||||||
tkMatch
|
tkMatch
|
||||||
tkColumns
|
tkColumns
|
||||||
tkSrc
|
tkSrc
|
||||||
@@ -362,6 +364,9 @@ const keywords*: Table[string, TokenKind] = {
|
|||||||
"label": tkLabels,
|
"label": tkLabels,
|
||||||
"labels": tkLabels,
|
"labels": tkLabels,
|
||||||
"graph_table": tkGraphTable,
|
"graph_table": tkGraphTable,
|
||||||
|
"create_graph": tkCreateGraph,
|
||||||
|
"drop_graph": tkDropGraph,
|
||||||
|
"graph": tkGraph,
|
||||||
"match": tkMatch,
|
"match": tkMatch,
|
||||||
"columns": tkColumns,
|
"columns": tkColumns,
|
||||||
"src": tkSrc,
|
"src": tkSrc,
|
||||||
@@ -370,7 +375,6 @@ const keywords*: Table[string, TokenKind] = {
|
|||||||
"matched": tkMatched,
|
"matched": tkMatched,
|
||||||
"array": tkArray,
|
"array": tkArray,
|
||||||
"vector": tkVector,
|
"vector": tkVector,
|
||||||
"graph": tkGraph,
|
|
||||||
"document": tkDocument,
|
"document": tkDocument,
|
||||||
"similar": tkSimilar,
|
"similar": tkSimilar,
|
||||||
"nearest": tkNearest,
|
"nearest": tkNearest,
|
||||||
|
|||||||
+113
-34
@@ -42,6 +42,8 @@ proc parseOverClause(p: var Parser): Node
|
|||||||
proc parseFrameSpec(p: var Parser): Node
|
proc parseFrameSpec(p: var Parser): Node
|
||||||
proc parseFrameBoundary(p: var Parser): string
|
proc parseFrameBoundary(p: var Parser): string
|
||||||
proc parseSetVar(p: var Parser): Node
|
proc parseSetVar(p: var Parser): Node
|
||||||
|
proc parseCreateGraph(p: var Parser): Node
|
||||||
|
proc parseDropGraph(p: var Parser): Node
|
||||||
|
|
||||||
proc parsePrimary(p: var Parser): Node =
|
proc parsePrimary(p: var Parser): Node =
|
||||||
let tok = p.peek()
|
let tok = p.peek()
|
||||||
@@ -464,60 +466,108 @@ proc parseSelect(p: var Parser): Node =
|
|||||||
discard p.advance()
|
discard p.advance()
|
||||||
discard p.expect(tkLParen)
|
discard p.expect(tkLParen)
|
||||||
let graphName = p.expect(tkIdent).value
|
let graphName = p.expect(tkIdent).value
|
||||||
discard p.expect(tkMatch)
|
var hasMatch = p.match(tkMatch)
|
||||||
# Parse pattern: (node)-[edge]->(node)
|
|
||||||
var patternNodes: seq[string]
|
var patternNodes: seq[string]
|
||||||
var patternEdges: seq[string]
|
var patternEdges: seq[string]
|
||||||
# First node
|
if hasMatch:
|
||||||
discard p.expect(tkLParen)
|
# First node
|
||||||
if p.peek().kind == tkIdent:
|
discard p.expect(tkLParen)
|
||||||
patternNodes.add(p.advance().value)
|
if p.peek().kind == tkIdent:
|
||||||
discard p.expect(tkRParen)
|
patternNodes.add(p.advance().value)
|
||||||
# Edge and next node(s)
|
discard p.expect(tkRParen)
|
||||||
while p.peek().kind == tkMinus or p.peek().kind == tkArrowR:
|
# Edge and next node(s)
|
||||||
if p.match(tkArrowR):
|
while p.peek().kind == tkMinus or p.peek().kind == tkArrowR:
|
||||||
discard
|
if p.match(tkArrowR):
|
||||||
elif p.match(tkMinus):
|
discard
|
||||||
if p.peek().kind == tkLBracket:
|
elif p.match(tkMinus):
|
||||||
|
if p.peek().kind == tkLBracket:
|
||||||
|
discard p.advance()
|
||||||
|
if p.peek().kind == tkIdent:
|
||||||
|
patternEdges.add(p.advance().value)
|
||||||
|
discard p.expect(tkRBracket)
|
||||||
|
discard p.expect(tkArrowR)
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
if p.peek().kind == tkLParen:
|
||||||
discard p.advance()
|
discard p.advance()
|
||||||
if p.peek().kind == tkIdent:
|
if p.peek().kind == tkIdent:
|
||||||
patternEdges.add(p.advance().value)
|
patternNodes.add(p.advance().value)
|
||||||
discard p.expect(tkRBracket)
|
discard p.expect(tkRParen)
|
||||||
discard p.expect(tkArrowR)
|
|
||||||
else:
|
|
||||||
discard
|
|
||||||
if p.peek().kind == tkLParen:
|
|
||||||
discard p.advance()
|
|
||||||
if p.peek().kind == tkIdent:
|
|
||||||
patternNodes.add(p.advance().value)
|
|
||||||
discard p.expect(tkRParen)
|
|
||||||
# COLUMNS (col1, col2, ...)
|
# COLUMNS (col1, col2, ...)
|
||||||
|
var algo = "bfs"
|
||||||
|
var maxDepth = -1
|
||||||
|
var startId = ""
|
||||||
|
var endId = ""
|
||||||
|
|
||||||
|
let startVar = if patternNodes.len > 0: patternNodes[0] else: ""
|
||||||
|
let endVar = if patternNodes.len > 1: patternNodes[1] else: ""
|
||||||
|
|
||||||
|
if p.peek().kind == tkIdent and p.peek().value.toLower() == "algorithm":
|
||||||
|
discard p.advance()
|
||||||
|
algo = p.advance().value.toLower()
|
||||||
|
|
||||||
|
if p.peek().kind == tkIdent and p.peek().value.toLower() == "start":
|
||||||
|
discard p.advance()
|
||||||
|
if p.peek().kind == tkIntLit:
|
||||||
|
startId = p.advance().value
|
||||||
|
elif p.peek().kind == tkIdent:
|
||||||
|
startId = p.advance().value
|
||||||
|
|
||||||
|
if p.peek().kind == tkIdent and p.peek().value.toLower() == "end" or p.peek().kind == tkEnd:
|
||||||
|
discard p.advance()
|
||||||
|
if p.peek().kind == tkIntLit:
|
||||||
|
endId = p.advance().value
|
||||||
|
elif p.peek().kind == tkIdent:
|
||||||
|
endId = p.advance().value
|
||||||
|
|
||||||
|
if p.peek().kind == tkIdent and p.peek().value.toLower() == "maxdepth":
|
||||||
|
discard p.advance()
|
||||||
|
if p.peek().kind == tkIntLit:
|
||||||
|
maxDepth = parseInt(p.advance().value)
|
||||||
|
|
||||||
var returnCols: seq[string]
|
var returnCols: seq[string]
|
||||||
if p.match(tkColumns):
|
if p.match(tkColumns):
|
||||||
discard p.expect(tkLParen)
|
discard p.expect(tkLParen)
|
||||||
if p.peek().kind == tkIdent:
|
if p.peek().kind in {tkIdent, tkLabels, tkEdge, tkGraph, tkRank, tkEnd, tkMatch, tkColumns, tkSrc, tkDst, tkBfs, tkDfs, tkMerge}:
|
||||||
var colName = p.advance().value
|
var colName = p.advance().value
|
||||||
# Handle dotted names: e.name
|
|
||||||
while p.peek().kind == tkDot:
|
while p.peek().kind == tkDot:
|
||||||
discard p.advance() # skip dot
|
discard p.advance()
|
||||||
colName &= "." & p.expect(tkIdent).value
|
if p.peek().kind in {tkIdent, tkLabels, tkEdge, tkGraph, tkRank, tkBfs, tkDfs, tkMatch, tkColumns, tkEnd, tkSrc, tkDst, tkMerge}:
|
||||||
|
colName &= "." & p.advance().value
|
||||||
|
else:
|
||||||
|
colName &= "." & p.expect(tkIdent).value
|
||||||
returnCols.add(colName)
|
returnCols.add(colName)
|
||||||
while p.match(tkComma):
|
while p.match(tkComma):
|
||||||
if p.peek().kind == tkIdent:
|
if p.peek().kind in {tkIdent, tkLabels, tkEdge, tkGraph, tkRank, tkEnd, tkMatch, tkColumns, tkSrc, tkDst, tkBfs, tkDfs, tkMerge}:
|
||||||
colName = p.advance().value
|
colName = p.advance().value
|
||||||
while p.peek().kind == tkDot:
|
while p.peek().kind == tkDot:
|
||||||
discard p.advance()
|
discard p.advance()
|
||||||
colName &= "." & p.expect(tkIdent).value
|
if p.peek().kind in {tkIdent, tkLabels, tkEdge, tkGraph, tkRank, tkBfs, tkDfs, tkMatch, tkColumns, tkEnd, tkSrc, tkDst, tkMerge}:
|
||||||
|
colName &= "." & p.advance().value
|
||||||
|
else:
|
||||||
|
colName &= "." & p.expect(tkIdent).value
|
||||||
returnCols.add(colName)
|
returnCols.add(colName)
|
||||||
if p.match(tkAs):
|
if p.match(tkAs):
|
||||||
discard p.advance() # skip alias
|
discard p.advance()
|
||||||
discard p.expect(tkRParen)
|
discard p.expect(tkRParen)
|
||||||
discard p.expect(tkRParen)
|
discard p.expect(tkRParen)
|
||||||
# Create a graph traversal node
|
|
||||||
|
var startNode: Node = nil
|
||||||
|
if startId.len > 0:
|
||||||
|
startNode = Node(kind: nkIntLit, intVal: parseInt(startId))
|
||||||
|
elif patternNodes.len > 0:
|
||||||
|
startNode = Node(kind: nkIdent, identName: patternNodes[0])
|
||||||
|
|
||||||
|
var endNode: Node = nil
|
||||||
|
if endId.len > 0:
|
||||||
|
endNode = Node(kind: nkIntLit, intVal: parseInt(endId))
|
||||||
|
elif patternNodes.len > 1:
|
||||||
|
endNode = Node(kind: nkIdent, identName: patternNodes[1])
|
||||||
|
|
||||||
result.selFrom = Node(kind: nkGraphTraversal, gtGraphName: graphName,
|
result.selFrom = Node(kind: nkGraphTraversal, gtGraphName: graphName,
|
||||||
gtStart: nil, gtEdge: if patternEdges.len > 0: patternEdges[0] else: "",
|
gtStart: startNode, gtEdge: if patternEdges.len > 0: patternEdges[0] else: "",
|
||||||
gtDirection: "out", gtEnd: nil, gtMaxDepth: -1,
|
gtDirection: "out", gtEnd: endNode, gtMaxDepth: maxDepth,
|
||||||
gtReturnCols: returnCols, line: tok.line, col: tok.col)
|
gtReturnCols: returnCols, gtAlgo: algo, line: tok.line, col: tok.col)
|
||||||
else:
|
else:
|
||||||
let tableTok = p.expect(tkIdent)
|
let tableTok = p.expect(tkIdent)
|
||||||
var alias = ""
|
var alias = ""
|
||||||
@@ -1542,6 +1592,31 @@ proc parseSetVar(p: var Parser): Node =
|
|||||||
result = Node(kind: nkSetVar, svName: varName, svValue: valStr,
|
result = Node(kind: nkSetVar, svName: varName, svValue: valStr,
|
||||||
line: tok.line, col: tok.col)
|
line: tok.line, col: tok.col)
|
||||||
|
|
||||||
|
proc parseCreateGraph(p: var Parser): Node =
|
||||||
|
let tok = p.expect(tkCreate)
|
||||||
|
discard p.expect(tkGraph)
|
||||||
|
var ifNotExists = false
|
||||||
|
if p.peek().kind == tkIdent and p.peek().value.toLower() == "if":
|
||||||
|
discard p.advance()
|
||||||
|
discard p.expect(tkNot)
|
||||||
|
discard p.expect(tkExists)
|
||||||
|
ifNotExists = true
|
||||||
|
let name = p.expect(tkIdent).value
|
||||||
|
result = Node(kind: nkCreateGraph, cgName: name, cgIfNotExists: ifNotExists,
|
||||||
|
line: tok.line, col: tok.col)
|
||||||
|
|
||||||
|
proc parseDropGraph(p: var Parser): Node =
|
||||||
|
let tok = p.expect(tkDrop)
|
||||||
|
discard p.expect(tkGraph)
|
||||||
|
var ifExists = false
|
||||||
|
if p.peek().kind == tkIdent and p.peek().value.toLower() == "if":
|
||||||
|
discard p.advance()
|
||||||
|
discard p.expect(tkExists)
|
||||||
|
ifExists = true
|
||||||
|
let name = p.expect(tkIdent).value
|
||||||
|
result = Node(kind: nkDropGraph, dgName: name, dgIfExists: ifExists,
|
||||||
|
line: tok.line, col: tok.col)
|
||||||
|
|
||||||
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()
|
||||||
@@ -1568,6 +1643,8 @@ proc parseStatement*(p: var Parser): Node =
|
|||||||
p.parseCreateUser()
|
p.parseCreateUser()
|
||||||
elif next.kind == tkPolicy:
|
elif next.kind == tkPolicy:
|
||||||
p.parseCreatePolicy()
|
p.parseCreatePolicy()
|
||||||
|
elif next.kind == tkGraph:
|
||||||
|
p.parseCreateGraph()
|
||||||
else:
|
else:
|
||||||
p.parseCreateType()
|
p.parseCreateType()
|
||||||
else:
|
else:
|
||||||
@@ -1587,6 +1664,8 @@ proc parseStatement*(p: var Parser): Node =
|
|||||||
p.parseDropUser()
|
p.parseDropUser()
|
||||||
elif next.kind == tkPolicy:
|
elif next.kind == tkPolicy:
|
||||||
p.parseDropPolicy()
|
p.parseDropPolicy()
|
||||||
|
elif next.kind == tkGraph:
|
||||||
|
p.parseDropGraph()
|
||||||
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)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
## BaraDB MCP Server — Standalone Entry Point
|
||||||
|
##
|
||||||
|
## Starts BaraDB in MCP (Model Context Protocol) server mode over STDIO.
|
||||||
|
## The server accepts JSON-RPC requests from AI agents and provides
|
||||||
|
## tools for SQL query execution, vector search, and schema inspection.
|
||||||
|
##
|
||||||
|
## Usage:
|
||||||
|
## baramcp --data-dir ./data
|
||||||
|
##
|
||||||
|
## Environment variables:
|
||||||
|
## BARADB_DATA_DIR — Path to the data directory (default: ./data)
|
||||||
|
|
||||||
|
import barabadb/mcp/server
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
let dataDir = server.parseDataDir()
|
||||||
|
server.logToStderr("Starting BaraDB MCP Server with data dir: " & dataDir)
|
||||||
|
try:
|
||||||
|
discard server.init(dataDir)
|
||||||
|
server.run()
|
||||||
|
except:
|
||||||
|
server.logToStderr("Fatal error: " & getCurrentExceptionMsg())
|
||||||
|
finally:
|
||||||
|
server.close()
|
||||||
Reference in New Issue
Block a user