1ed97fb075
CI / test (push) Has been cancelled
CI / raft-e2e (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
Close the remaining 2026-08 findings: peer TLS on leader forward, disttxn SO_ERROR, compaction catalog order, OFFSET without LIMIT, window aggregates, WebSocket mask/size/auth, SCRAM timing and cbind, B-tree leaf left-max separators, and SQL three-valued NULL comparisons.
1076 lines
42 KiB
Nim
1076 lines
42 KiB
Nim
## IR plan execution (executePlan) — walks a lowered IRPlan tree and
|
|
## produces result rows: filters, projections, aggregates, grouping sets,
|
|
## joins (nested loop / hash / index nested loop / lateral), pivot/unpivot,
|
|
## and graph traversal. Extracted from `executor.nim` (Task 13 of the
|
|
## executor split). Pure code motion — no behavior changes.
|
|
import std/strutils
|
|
import std/tables
|
|
import std/sets
|
|
import std/sequtils
|
|
import std/algorithm
|
|
import ../ir
|
|
import ../../core/types
|
|
import ../../storage/btree
|
|
import ../../graph/engine as gengine
|
|
import ../../graph/community as gcomm
|
|
import types
|
|
import values
|
|
import helpers
|
|
import eval
|
|
import scan
|
|
import window
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Aggregate DISTINCT helpers
|
|
# ----------------------------------------------------------------------
|
|
|
|
proc shouldKeepDistinct(seen: var HashSet[string], s: string, doDistinct: bool): bool =
|
|
## Returns true if `s` should be counted/included (first occurrence when distinct).
|
|
if not doDistinct:
|
|
return true
|
|
if s in seen:
|
|
return false
|
|
seen.incl(s)
|
|
return true
|
|
|
|
# ----------------------------------------------------------------------
|
|
# IR Plan Execution (with actual filter/sort/projection)
|
|
# ----------------------------------------------------------------------
|
|
|
|
proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
|
|
if plan == nil: return @[]
|
|
|
|
case plan.kind
|
|
of irpkScan:
|
|
return execScan(ctx, plan.scanTable)
|
|
|
|
of irpkFilter:
|
|
let sourceRows = executePlan(ctx, plan.filterSource)
|
|
if plan.filterCond == nil: return sourceRows
|
|
result = @[]
|
|
for row in sourceRows:
|
|
let evalResult = evalExpr(plan.filterCond, row, ctx)
|
|
if valueToString(evalResult) == "true":
|
|
result.add(row)
|
|
|
|
of irpkProject:
|
|
var sourceRows = executePlan(ctx, plan.projectSource)
|
|
if plan.projectAliases.len == 0: return sourceRows
|
|
# Scalar SELECT (no FROM): create a dummy row so expressions can be evaluated
|
|
if sourceRows.len == 0 and plan.projectSource != nil and
|
|
plan.projectSource.kind == irpkScan and plan.projectSource.scanTable.len == 0:
|
|
sourceRows = @[initTable[string, Value]()]
|
|
|
|
# Check if this projection contains aggregates without GROUP BY
|
|
var hasAggs = false
|
|
let sourceIsGroupBy = plan.projectSource != nil and plan.projectSource.kind == irpkGroupBy
|
|
for expr in plan.projectExprs:
|
|
if expr != nil and expr.kind == irekAggregate:
|
|
# If source is GroupBy, aggregates are pre-computed in group rows
|
|
if not sourceIsGroupBy:
|
|
hasAggs = true
|
|
break
|
|
|
|
# Check if projection contains window functions
|
|
var hasWindowFuncs = false
|
|
for expr in plan.projectExprs:
|
|
if expr != nil and expr.kind == irekWindowFunc:
|
|
hasWindowFuncs = true
|
|
break
|
|
|
|
if hasWindowFuncs:
|
|
# Pre-compute window function values for all source rows
|
|
var winValues = newSeq[seq[string]](plan.projectExprs.len)
|
|
for i, expr in plan.projectExprs:
|
|
if expr != nil and expr.kind == irekWindowFunc:
|
|
winValues[i] = computeWindowValues(sourceRows, expr)
|
|
result = @[]
|
|
for rowIdx, row in sourceRows:
|
|
var newRow: Row
|
|
for i, alias in plan.projectAliases:
|
|
if i < plan.projectExprs.len:
|
|
let expr = plan.projectExprs[i]
|
|
if expr.kind == irekWindowFunc:
|
|
newRow[alias] = winValues[i][rowIdx]
|
|
elif expr.kind == irekStar:
|
|
newRow = expandStarRow(row)
|
|
else:
|
|
let val = evalExpr(expr, row, ctx)
|
|
if alias.len > 0: newRow[alias] = val
|
|
else: newRow["col" & $i] = val
|
|
if newRow.len > 0:
|
|
result.add(newRow)
|
|
else:
|
|
result.add(row)
|
|
return result
|
|
|
|
if hasAggs:
|
|
# Produce exactly one row with aggregate values
|
|
var newRow: Row
|
|
for i, alias in plan.projectAliases:
|
|
if i < plan.projectExprs.len:
|
|
let expr = plan.projectExprs[i]
|
|
if expr.kind == irekStar:
|
|
if sourceRows.len > 0:
|
|
newRow = expandStarRow(sourceRows[0])
|
|
elif expr.kind == irekAggregate:
|
|
# Apply FILTER (WHERE ...) if present
|
|
var filteredRows = sourceRows
|
|
if expr.aggFilter != nil:
|
|
filteredRows = @[]
|
|
for row in sourceRows:
|
|
if valueToString(evalExpr(expr.aggFilter, row, ctx)) == "true":
|
|
filteredRows.add(row)
|
|
case expr.aggOp
|
|
of irCount:
|
|
if expr.aggArgs.len == 0:
|
|
newRow[alias] = $filteredRows.len
|
|
else:
|
|
var count = 0
|
|
var seen: HashSet[string]
|
|
for row in filteredRows:
|
|
let v = evalExpr(expr.aggArgs[0], row, ctx)
|
|
if v.kind != vkNull:
|
|
let s = valueToString(v)
|
|
if shouldKeepDistinct(seen, s, expr.aggDistinct):
|
|
count += 1
|
|
newRow[alias] = $count
|
|
of irSum:
|
|
var sum = 0.0
|
|
var seen: HashSet[string]
|
|
for row in filteredRows:
|
|
let v = evalExpr(expr.aggArgs[0], row, ctx)
|
|
let s = valueToString(v)
|
|
if shouldKeepDistinct(seen, s, expr.aggDistinct):
|
|
try: sum += parseFloat(s) except CatchableError: discard
|
|
newRow[alias] = $sum
|
|
of irAvg:
|
|
var sum = 0.0
|
|
var count = 0
|
|
var seen: HashSet[string]
|
|
for row in filteredRows:
|
|
let v = evalExpr(expr.aggArgs[0], row, ctx)
|
|
let s = valueToString(v)
|
|
if shouldKeepDistinct(seen, s, expr.aggDistinct):
|
|
try: sum += parseFloat(s); count += 1 except CatchableError: discard
|
|
newRow[alias] = if count > 0: $(sum / float(count)) else: "0"
|
|
of irMin:
|
|
var minVal = ""
|
|
var seen: HashSet[string]
|
|
for row in filteredRows:
|
|
let v = evalExpr(expr.aggArgs[0], row, ctx)
|
|
if v.kind == vkNull: continue
|
|
let s = valueToString(v)
|
|
if shouldKeepDistinct(seen, s, expr.aggDistinct):
|
|
if minVal == "" or cmpMin(s, minVal): minVal = s
|
|
newRow[alias] = minVal
|
|
of irMax:
|
|
var maxVal = ""
|
|
var seen: HashSet[string]
|
|
for row in filteredRows:
|
|
let v = evalExpr(expr.aggArgs[0], row, ctx)
|
|
if v.kind == vkNull: continue
|
|
let s = valueToString(v)
|
|
if shouldKeepDistinct(seen, s, expr.aggDistinct):
|
|
if maxVal == "" or cmpMax(s, maxVal): maxVal = s
|
|
newRow[alias] = maxVal
|
|
of irArrayAgg:
|
|
var arr: seq[string]
|
|
var seen: HashSet[string]
|
|
for row in filteredRows:
|
|
if expr.aggArgs.len > 0:
|
|
let s = valueToString(evalExpr(expr.aggArgs[0], row, ctx))
|
|
if shouldKeepDistinct(seen, s, expr.aggDistinct):
|
|
arr.add(s)
|
|
newRow[alias] = "[" & arr.join(", ") & "]"
|
|
of irStringAgg:
|
|
var parts: seq[string]
|
|
var seen: HashSet[string]
|
|
let delim = if expr.aggArgs.len > 1: evalExpr(expr.aggArgs[1], initTable[string, Value](), ctx) else: Value(kind: vkString, strVal: ",")
|
|
for row in filteredRows:
|
|
if expr.aggArgs.len > 0:
|
|
let s = valueToString(evalExpr(expr.aggArgs[0], row, ctx))
|
|
if shouldKeepDistinct(seen, s, expr.aggDistinct):
|
|
parts.add(s)
|
|
newRow[alias] = parts.join(valueToString(delim))
|
|
else:
|
|
let val = evalExpr(expr, if sourceRows.len > 0: sourceRows[0] else: initTable[string, Value](), ctx)
|
|
if alias.len > 0: newRow[alias] = val
|
|
else: newRow["col" & $i] = val
|
|
result = @[newRow]
|
|
return result
|
|
|
|
result = @[]
|
|
for row in sourceRows:
|
|
var newRow: Row
|
|
for i, alias in plan.projectAliases:
|
|
if i < plan.projectExprs.len:
|
|
let expr = plan.projectExprs[i]
|
|
if expr.kind == irekStar:
|
|
newRow = expandStarRow(row)
|
|
elif expr.kind == irekAggregate and sourceIsGroupBy:
|
|
# Look up pre-computed aggregate from GroupBy row
|
|
let aggKey = "$agg_" & $expr.aggOp
|
|
var found = false
|
|
for k, v in row:
|
|
if k.startsWith(aggKey):
|
|
if alias.len > 0: newRow[alias] = v
|
|
else: newRow["col" & $i] = v
|
|
found = true
|
|
break
|
|
if not found:
|
|
if alias.len > 0: newRow[alias] = "0"
|
|
else: newRow["col" & $i] = "0"
|
|
else:
|
|
let val = evalExpr(expr, row, ctx)
|
|
if alias.len > 0: newRow[alias] = val
|
|
else: newRow["col" & $i] = val
|
|
if newRow.len > 0:
|
|
result.add(newRow)
|
|
else:
|
|
result.add(row)
|
|
|
|
of irpkSort:
|
|
var sourceRows = executePlan(ctx, plan.sortSource)
|
|
if plan.sortExprs.len == 0: return sourceRows
|
|
proc sortCmp(a, b: Row): int =
|
|
for i, sortExpr in plan.sortExprs:
|
|
let ascending = if i < plan.sortDirs.len: plan.sortDirs[i] else: true
|
|
let va = evalExpr(sortExpr, a, ctx)
|
|
let vb = evalExpr(sortExpr, b, ctx)
|
|
var cmpRes = 0
|
|
try:
|
|
let fa = parseFloat(valueToString(va))
|
|
let fb = parseFloat(valueToString(vb))
|
|
if fa < fb: cmpRes = -1
|
|
elif fa > fb: cmpRes = 1
|
|
except CatchableError:
|
|
cmpRes = cmp(valueToString(va), valueToString(vb))
|
|
if not ascending: cmpRes = -cmpRes
|
|
if cmpRes != 0: return cmpRes
|
|
return 0
|
|
sourceRows.sort(sortCmp, Ascending)
|
|
return sourceRows
|
|
|
|
of irpkLimit:
|
|
let sourceRows = executePlan(ctx, plan.limitSource)
|
|
var start = int(plan.limitOffset)
|
|
if start < 0: start = 0
|
|
if start > sourceRows.len: start = sourceRows.len
|
|
if plan.limitCount < 0:
|
|
# OFFSET without LIMIT — return the remainder.
|
|
return sourceRows[start ..< sourceRows.len]
|
|
if plan.limitCount == 0:
|
|
return @[]
|
|
var endIdx = start + int(plan.limitCount)
|
|
if endIdx > sourceRows.len:
|
|
endIdx = sourceRows.len
|
|
if endIdx < start:
|
|
endIdx = start
|
|
return sourceRows[start..<endIdx]
|
|
|
|
of irpkGroupBy:
|
|
let sourceRows = executePlan(ctx, plan.groupSource)
|
|
if plan.groupKeys.len == 0 and plan.groupingSetsKind == irgskNone: return sourceRows
|
|
|
|
# Generate grouping sets
|
|
var groupingSets: seq[seq[IRExpr]]
|
|
case plan.groupingSetsKind
|
|
of irgskNone:
|
|
groupingSets = @[plan.groupKeys]
|
|
of irgskGroupingSets:
|
|
groupingSets = plan.groupingSets
|
|
of irgskRollup:
|
|
# ROLLUP(a, b) => GROUPING SETS ((a, b), (a), ())
|
|
groupingSets = @[]
|
|
for i in countdown(plan.groupKeys.len, 0):
|
|
groupingSets.add(plan.groupKeys[0..<i])
|
|
of irgskCube:
|
|
# CUBE(a, b) => GROUPING SETS ((a, b), (a), (b), ())
|
|
groupingSets = @[@[]] # start with empty set
|
|
for key in plan.groupKeys:
|
|
var newSets: seq[seq[IRExpr]]
|
|
for s in groupingSets:
|
|
newSets.add(s)
|
|
var s2 = s
|
|
s2.add(key)
|
|
newSets.add(s2)
|
|
groupingSets = newSets
|
|
|
|
result = @[]
|
|
for gkeys in groupingSets:
|
|
# Group rows by this set's key values
|
|
var groups = initTable[string, seq[Row]]()
|
|
for row in sourceRows:
|
|
var groupKey = ""
|
|
for gk in gkeys:
|
|
groupKey &= valueToString(evalExpr(gk, row, ctx)) & "|"
|
|
if groupKey notin groups:
|
|
groups[groupKey] = @[]
|
|
groups[groupKey].add(row)
|
|
for gk, groupRows in groups:
|
|
var aggRow: Row
|
|
# Populate GROUP BY key columns
|
|
for gkExpr in gkeys:
|
|
if gkExpr.kind == irekField and gkExpr.fieldPath.len > 0:
|
|
aggRow[gkExpr.fieldPath[^1]] = evalExpr(gkExpr, groupRows[0], ctx)
|
|
# Populate non-aggregated columns from first row in group
|
|
if groupRows.len > 0:
|
|
for k, v in groupRows[0]:
|
|
if not k.startsWith("$") and k notin aggRow:
|
|
aggRow[k] = v
|
|
# Compute each aggregate expression
|
|
for aggExpr in plan.groupAggs:
|
|
let aggKey = "$agg_" & $aggExpr.aggOp & "_" & $plan.groupAggs.find(aggExpr)
|
|
var filteredRows = groupRows
|
|
if aggExpr.aggFilter != nil:
|
|
filteredRows = @[]
|
|
for row in groupRows:
|
|
if valueToString(evalExpr(aggExpr.aggFilter, row, ctx)) == "true":
|
|
filteredRows.add(row)
|
|
case aggExpr.aggOp
|
|
of irCount:
|
|
if aggExpr.aggArgs.len == 0:
|
|
aggRow[aggKey] = $filteredRows.len
|
|
else:
|
|
var count = 0
|
|
var seen: HashSet[string]
|
|
for row in filteredRows:
|
|
let v = evalExpr(aggExpr.aggArgs[0], row, ctx)
|
|
if v.kind != vkNull:
|
|
let s = valueToString(v)
|
|
if shouldKeepDistinct(seen, s, aggExpr.aggDistinct):
|
|
count += 1
|
|
aggRow[aggKey] = $count
|
|
of irSum:
|
|
var sum = 0.0
|
|
var seen: HashSet[string]
|
|
for row in filteredRows:
|
|
let v = evalExpr(aggExpr.aggArgs[0], row, ctx)
|
|
let s = valueToString(v)
|
|
if shouldKeepDistinct(seen, s, aggExpr.aggDistinct):
|
|
try: sum += parseFloat(s) except CatchableError: discard
|
|
aggRow[aggKey] = $sum
|
|
of irAvg:
|
|
var sum = 0.0
|
|
var count = 0
|
|
var seen: HashSet[string]
|
|
for row in filteredRows:
|
|
let v = evalExpr(aggExpr.aggArgs[0], row, ctx)
|
|
let s = valueToString(v)
|
|
if shouldKeepDistinct(seen, s, aggExpr.aggDistinct):
|
|
try: sum += parseFloat(s); count += 1 except CatchableError: discard
|
|
aggRow[aggKey] = if count > 0: $(sum / float(count)) else: "0"
|
|
of irMin:
|
|
var minVal = ""
|
|
var seen: HashSet[string]
|
|
for row in filteredRows:
|
|
let v = evalExpr(aggExpr.aggArgs[0], row, ctx)
|
|
if v.kind == vkNull: continue
|
|
let s = valueToString(v)
|
|
if shouldKeepDistinct(seen, s, aggExpr.aggDistinct):
|
|
if minVal == "" or cmpMin(s, minVal): minVal = s
|
|
aggRow[aggKey] = minVal
|
|
of irMax:
|
|
var maxVal = ""
|
|
var seen: HashSet[string]
|
|
for row in filteredRows:
|
|
let v = evalExpr(aggExpr.aggArgs[0], row, ctx)
|
|
if v.kind == vkNull: continue
|
|
let s = valueToString(v)
|
|
if shouldKeepDistinct(seen, s, aggExpr.aggDistinct):
|
|
if maxVal == "" or cmpMax(s, maxVal): maxVal = s
|
|
aggRow[aggKey] = maxVal
|
|
of irArrayAgg:
|
|
var arr: seq[string]
|
|
var seen: HashSet[string]
|
|
for row in filteredRows:
|
|
if aggExpr.aggArgs.len > 0:
|
|
let s = valueToString(evalExpr(aggExpr.aggArgs[0], row, ctx))
|
|
if shouldKeepDistinct(seen, s, aggExpr.aggDistinct):
|
|
arr.add(s)
|
|
aggRow[aggKey] = "[" & arr.join(", ") & "]"
|
|
of irStringAgg:
|
|
var parts: seq[string]
|
|
var seen: HashSet[string]
|
|
let delim = if aggExpr.aggArgs.len > 1: evalExpr(aggExpr.aggArgs[1], initTable[string, Value](), ctx) else: Value(kind: vkString, strVal: ",")
|
|
for row in filteredRows:
|
|
if aggExpr.aggArgs.len > 0:
|
|
let s = valueToString(evalExpr(aggExpr.aggArgs[0], row, ctx))
|
|
if shouldKeepDistinct(seen, s, aggExpr.aggDistinct):
|
|
parts.add(s)
|
|
aggRow[aggKey] = parts.join(valueToString(delim))
|
|
# Apply HAVING filter
|
|
if plan.groupHaving != nil:
|
|
if valueToString(evalExpr(plan.groupHaving, aggRow, ctx)) != "true":
|
|
continue
|
|
result.add(aggRow)
|
|
return result
|
|
|
|
of irpkJoin:
|
|
let leftRows = executePlan(ctx, plan.joinLeft)
|
|
result = @[]
|
|
|
|
proc getScanTable(p: IRPlan): string =
|
|
if p == nil: return ""
|
|
case p.kind
|
|
of irpkScan: p.scanTable
|
|
of irpkJoin: getScanTable(p.joinRight)
|
|
else: ""
|
|
|
|
proc getLeftmostScanTable(p: IRPlan): string =
|
|
if p == nil: return ""
|
|
case p.kind
|
|
of irpkScan: p.scanTable
|
|
of irpkJoin: getLeftmostScanTable(p.joinLeft)
|
|
else: ""
|
|
|
|
proc mergeRow(left, right: Row, leftAlias, rightAlias, leftTable, rightTable: string): Row =
|
|
result = initTable[string, Value]()
|
|
for k, v in left:
|
|
if not k.startsWith("$"):
|
|
result[k] = v
|
|
for k, v in right:
|
|
if not k.startsWith("$") and k notin result:
|
|
result[k] = v
|
|
let lq = if leftAlias.len > 0: leftAlias elif leftTable.len > 0: leftTable else: ""
|
|
if lq.len > 0:
|
|
for k, v in left:
|
|
if not k.startsWith("$"):
|
|
result[lq & "." & k] = v
|
|
let rq = if rightAlias.len > 0: rightAlias elif rightTable.len > 0: rightTable else: ""
|
|
if rq.len > 0:
|
|
for k, v in right:
|
|
if not k.startsWith("$"):
|
|
result[rq & "." & k] = v
|
|
|
|
let leftAlias = if plan.joinLeft != nil and plan.joinLeft.kind == irpkScan:
|
|
plan.joinLeft.scanAlias else: ""
|
|
let leftTable = getLeftmostScanTable(plan.joinLeft)
|
|
|
|
# LATERAL JOIN: for each left row, scan right, merge, then filter/sort/limit
|
|
if plan.joinLateral:
|
|
let rightAlias = plan.joinAlias
|
|
# Walk down right plan to extract filter, sort, limit
|
|
var rightFilter: IRExpr = nil
|
|
var rightSortExprs: seq[IRExpr]
|
|
var rightSortDirs: seq[bool]
|
|
var rightLimit: int = -1
|
|
var rightScanPlan: IRPlan = plan.joinRight
|
|
while rightScanPlan != nil:
|
|
case rightScanPlan.kind
|
|
of irpkScan: break
|
|
of irpkFilter:
|
|
if rightFilter == nil:
|
|
rightFilter = rightScanPlan.filterCond
|
|
else:
|
|
rightFilter = IRExpr(kind: irekBinary, binOp: irAnd,
|
|
binLeft: rightFilter, binRight: rightScanPlan.filterCond)
|
|
rightScanPlan = rightScanPlan.filterSource
|
|
of irpkSort:
|
|
rightSortExprs = rightScanPlan.sortExprs
|
|
rightSortDirs = rightScanPlan.sortDirs
|
|
rightScanPlan = rightScanPlan.sortSource
|
|
of irpkLimit:
|
|
rightLimit = rightScanPlan.limitCount
|
|
rightScanPlan = rightScanPlan.limitSource
|
|
of irpkProject:
|
|
rightScanPlan = rightScanPlan.projectSource
|
|
of irpkGroupBy:
|
|
rightScanPlan = rightScanPlan.groupSource
|
|
else: break
|
|
|
|
for l in leftRows:
|
|
var rawRightRows: seq[Row]
|
|
if rightScanPlan != nil and rightScanPlan.kind == irpkScan:
|
|
rawRightRows = execScan(ctx, rightScanPlan.scanTable)
|
|
else:
|
|
rawRightRows = @[]
|
|
|
|
# Merge, filter
|
|
var mergedRows: seq[Row]
|
|
for r in rawRightRows:
|
|
let merged = mergeRow(l, r, leftAlias, rightAlias, leftTable, getScanTable(plan.joinRight))
|
|
if rightFilter != nil and valueToString(evalExpr(rightFilter, merged, ctx)) != "true":
|
|
continue
|
|
if plan.joinCond != nil and valueToString(evalExpr(plan.joinCond, merged, ctx)) != "true":
|
|
continue
|
|
mergedRows.add(merged)
|
|
|
|
# Apply sort from subquery
|
|
if rightSortExprs.len > 0 and mergedRows.len > 1:
|
|
mergedRows.sort(proc(a, b: Row): int =
|
|
for i, sExpr in rightSortExprs:
|
|
let aVal = evalExpr(sExpr, a, ctx)
|
|
let bVal = evalExpr(sExpr, b, ctx)
|
|
let asc = if i < rightSortDirs.len: rightSortDirs[i] else: true
|
|
var cmp = 0
|
|
let aNum = parseFloat(valueToString(aVal))
|
|
let bNum = parseFloat(valueToString(bVal))
|
|
if aNum < bNum: cmp = -1
|
|
elif aNum > bNum: cmp = 1
|
|
if cmp != 0:
|
|
return if asc: cmp else: -cmp
|
|
return 0
|
|
)
|
|
|
|
# Apply limit from subquery
|
|
let limitRows = if rightLimit >= 0 and rightLimit < mergedRows.len:
|
|
mergedRows[0 ..< rightLimit]
|
|
else:
|
|
mergedRows
|
|
|
|
if limitRows.len > 0:
|
|
for row in limitRows:
|
|
result.add(row)
|
|
elif plan.joinKind == irjkLeft or plan.joinKind == irjkFull:
|
|
var rightCols: seq[string]
|
|
# Build rightCols from filtered rows if available, otherwise from raw rows
|
|
let sourceForCols = if mergedRows.len > 0: mergedRows else: rawRightRows
|
|
for r in sourceForCols:
|
|
for k, _ in r:
|
|
if not k.startsWith("$") and k notin rightCols:
|
|
rightCols.add(k)
|
|
# If still no right rows, get column names from table definition
|
|
if rightCols.len == 0 and rightScanPlan != nil and rightScanPlan.kind == irpkScan:
|
|
let rightTable = rightScanPlan.scanTable
|
|
if rightTable.len > 0:
|
|
let tbl = ctx.getTableDef(rightTable)
|
|
for col in tbl.columns:
|
|
rightCols.add(col.name)
|
|
var padded = initTable[string, Value]()
|
|
for k, v in l:
|
|
if not k.startsWith("$"):
|
|
padded[k] = v
|
|
for col in rightCols:
|
|
if col notin padded: padded[col] = Value(kind: vkNull)
|
|
if leftAlias.len > 0:
|
|
for k, v in l:
|
|
if not k.startsWith("$"):
|
|
padded[leftAlias & "." & k] = v
|
|
if rightAlias.len > 0:
|
|
for col in rightCols:
|
|
padded[rightAlias & "." & col] = Value(kind: vkNull)
|
|
result.add(padded)
|
|
return result
|
|
|
|
# Non-LATERAL: choose join strategy
|
|
chooseJoinStrategy(ctx, plan)
|
|
# Hash join and index nested loop are only safe for INNER JOIN
|
|
# (padding logic for outer joins is complex)
|
|
if plan.joinKind != irjkInner and plan.joinStrategy in {irjsHash, irjsIndexNestedLoop}:
|
|
plan.joinStrategy = irjsNestedLoop
|
|
let rightRows = executePlan(ctx, plan.joinRight)
|
|
|
|
# Collect all unique column names from each side (excluding internal $ keys)
|
|
var leftCols, rightCols: seq[string]
|
|
for l in leftRows:
|
|
for k, _ in l:
|
|
if not k.startsWith("$") and k notin leftCols:
|
|
leftCols.add(k)
|
|
for r in rightRows:
|
|
for k, _ in r:
|
|
if not k.startsWith("$") and k notin rightCols:
|
|
rightCols.add(k)
|
|
|
|
let rightAlias = if plan.joinRight != nil and plan.joinRight.kind == irpkScan:
|
|
plan.joinRight.scanAlias else: ""
|
|
let rightTable = getScanTable(plan.joinRight)
|
|
|
|
if plan.joinKind == irjkCross:
|
|
for l in leftRows:
|
|
for r in rightRows:
|
|
result.add(mergeRow(l, r, leftAlias, rightAlias, leftTable, rightTable))
|
|
return result
|
|
|
|
case plan.joinStrategy
|
|
of irjsHash:
|
|
# Hash Join: build hash table on the smaller side
|
|
let (leftCol, rightCol) = extractJoinEquality(plan.joinCond)
|
|
var buildRows, probeRows: seq[Row]
|
|
var buildCol, probeCol: string
|
|
var buildAlias, probeAlias: string
|
|
var buildIsLeft: bool
|
|
|
|
if leftRows.len <= rightRows.len:
|
|
buildRows = leftRows
|
|
buildCol = leftCol
|
|
buildAlias = leftAlias
|
|
probeRows = rightRows
|
|
probeCol = rightCol
|
|
probeAlias = rightAlias
|
|
buildIsLeft = true
|
|
else:
|
|
buildRows = rightRows
|
|
buildCol = rightCol
|
|
buildAlias = rightAlias
|
|
probeRows = leftRows
|
|
probeCol = leftCol
|
|
probeAlias = leftAlias
|
|
buildIsLeft = false
|
|
|
|
var hashTable = initTable[string, seq[Row]]()
|
|
for row in buildRows:
|
|
let key = if buildAlias.len > 0 and buildCol in row: valueToString(row[buildCol])
|
|
elif buildCol in row: valueToString(row[buildCol])
|
|
else: ""
|
|
if key.len > 0 :
|
|
if key notin hashTable: hashTable[key] = @[]
|
|
hashTable[key].add(row)
|
|
|
|
var matchedProbe = initTable[int, bool]()
|
|
for i, prow in probeRows:
|
|
let key = if probeAlias.len > 0 and probeCol in prow: valueToString(prow[probeCol])
|
|
elif probeCol in prow: valueToString(prow[probeCol])
|
|
else: ""
|
|
if key in hashTable:
|
|
matchedProbe[i] = true
|
|
for brow in hashTable[key]:
|
|
if buildIsLeft:
|
|
result.add(mergeRow(brow, prow, leftAlias, rightAlias, leftTable, rightTable))
|
|
else:
|
|
result.add(mergeRow(prow, brow, leftAlias, rightAlias, leftTable, rightTable))
|
|
|
|
# LEFT / RIGHT / FULL padding for non-matching probe rows
|
|
if plan.joinKind == irjkLeft or plan.joinKind == irjkFull or plan.joinKind == irjkRight:
|
|
for i, prow in probeRows:
|
|
if not matchedProbe.getOrDefault(i, false):
|
|
var padded = initTable[string, Value]()
|
|
for k, v in prow:
|
|
if not k.startsWith("$"):
|
|
padded[k] = v
|
|
if buildIsLeft:
|
|
# probe is right side
|
|
for col in leftCols:
|
|
if col notin padded: padded[col] = Value(kind: vkNull)
|
|
if probeAlias.len > 0:
|
|
for k, v in prow:
|
|
if not k.startsWith("$"):
|
|
padded[probeAlias & "." & k] = v
|
|
if leftAlias.len > 0:
|
|
for col in leftCols:
|
|
padded[leftAlias & "." & col] = Value(kind: vkNull)
|
|
else:
|
|
# probe is left side
|
|
for col in rightCols:
|
|
if col notin padded: padded[col] = Value(kind: vkNull)
|
|
if probeAlias.len > 0:
|
|
for k, v in prow:
|
|
if not k.startsWith("$"):
|
|
padded[probeAlias & "." & k] = v
|
|
if rightAlias.len > 0:
|
|
for col in rightCols:
|
|
padded[rightAlias & "." & col] = Value(kind: vkNull)
|
|
result.add(padded)
|
|
|
|
# RIGHT / FULL padding for non-matching build rows
|
|
if plan.joinKind == irjkRight or plan.joinKind == irjkFull:
|
|
for i, brow in buildRows:
|
|
var found = false
|
|
for j, prow in probeRows:
|
|
let pkey = if probeAlias.len > 0 and probeCol in prow: valueToString(prow[probeCol])
|
|
elif probeCol in prow: valueToString(prow[probeCol])
|
|
else: ""
|
|
let bkey = if buildAlias.len > 0 and buildCol in brow: valueToString(brow[buildCol])
|
|
elif buildCol in brow: valueToString(brow[buildCol])
|
|
else: ""
|
|
if pkey == bkey and bkey.len > 0:
|
|
found = true
|
|
break
|
|
if not found:
|
|
var padded = initTable[string, Value]()
|
|
for k, v in brow:
|
|
if not k.startsWith("$"):
|
|
padded[k] = v
|
|
if buildIsLeft:
|
|
for col in rightCols:
|
|
if col notin padded: padded[col] = Value(kind: vkNull)
|
|
if leftAlias.len > 0:
|
|
for k, v in brow:
|
|
if not k.startsWith("$"):
|
|
padded[leftAlias & "." & k] = v
|
|
if rightAlias.len > 0:
|
|
for col in rightCols:
|
|
padded[rightAlias & "." & col] = Value(kind: vkNull)
|
|
else:
|
|
for col in leftCols:
|
|
if col notin padded: padded[col] = Value(kind: vkNull)
|
|
if rightAlias.len > 0:
|
|
for k, v in brow:
|
|
if not k.startsWith("$"):
|
|
padded[rightAlias & "." & k] = v
|
|
if leftAlias.len > 0:
|
|
for col in leftCols:
|
|
padded[leftAlias & "." & col] = Value(kind: vkNull)
|
|
result.add(padded)
|
|
|
|
of irjsIndexNestedLoop:
|
|
let (leftCol, rightCol) = extractJoinEquality(plan.joinCond)
|
|
var outerRows: seq[Row]
|
|
var outerAlias, innerAlias: string
|
|
var outerCol, innerCol: string
|
|
var idxName: string
|
|
var outerIsLeft: bool
|
|
|
|
if plan.joinHashCol == rightCol:
|
|
outerRows = leftRows
|
|
outerAlias = leftAlias
|
|
innerAlias = rightAlias
|
|
outerCol = leftCol
|
|
innerCol = rightCol
|
|
idxName = (if plan.joinRight != nil and plan.joinRight.kind == irpkScan: plan.joinRight.scanTable else: "") & "." & rightCol
|
|
outerIsLeft = true
|
|
else:
|
|
outerRows = rightRows
|
|
outerAlias = rightAlias
|
|
innerAlias = leftAlias
|
|
outerCol = rightCol
|
|
innerCol = leftCol
|
|
idxName = (if plan.joinLeft != nil and plan.joinLeft.kind == irpkScan: plan.joinLeft.scanTable else: "") & "." & leftCol
|
|
outerIsLeft = false
|
|
|
|
if idxName notin ctx.btrees:
|
|
# Fallback to nested loop if index disappeared
|
|
plan.joinStrategy = irjsNestedLoop
|
|
# Fall through to nested loop below
|
|
else:
|
|
let btree = ctx.btrees[idxName]
|
|
var matchedOuter = initTable[int, bool]()
|
|
for i, orow in outerRows:
|
|
let key = if orow.hasKey(outerCol): valueToString(orow[outerCol]) else: ""
|
|
if key.len > 0 :
|
|
let entries = btree.get(key)
|
|
if entries.len > 0:
|
|
matchedOuter[i] = true
|
|
for entry in entries:
|
|
let parsed = parseRowDataToValueRow(entry.rowValue)
|
|
if outerIsLeft:
|
|
result.add(mergeRow(orow, parsed, leftAlias, rightAlias, leftTable, rightTable))
|
|
else:
|
|
result.add(mergeRow(parsed, orow, leftAlias, rightAlias, leftTable, rightTable))
|
|
elif plan.joinKind == irjkLeft or plan.joinKind == irjkRight or plan.joinKind == irjkFull:
|
|
matchedOuter[i] = false
|
|
|
|
# Padding for non-matching outer rows
|
|
if plan.joinKind == irjkLeft or plan.joinKind == irjkRight or plan.joinKind == irjkFull:
|
|
for i, orow in outerRows:
|
|
if not matchedOuter.getOrDefault(i, false):
|
|
var padded = initTable[string, Value]()
|
|
for k, v in orow:
|
|
if not k.startsWith("$"):
|
|
padded[k] = v
|
|
if outerIsLeft:
|
|
for col in rightCols:
|
|
if col notin padded: padded[col] = Value(kind: vkNull)
|
|
if leftAlias.len > 0:
|
|
for k, v in orow:
|
|
if not k.startsWith("$"):
|
|
padded[leftAlias & "." & k] = v
|
|
if rightAlias.len > 0:
|
|
for col in rightCols:
|
|
padded[rightAlias & "." & col] = Value(kind: vkNull)
|
|
else:
|
|
for col in leftCols:
|
|
if col notin padded: padded[col] = Value(kind: vkNull)
|
|
if rightAlias.len > 0:
|
|
for k, v in orow:
|
|
if not k.startsWith("$"):
|
|
padded[rightAlias & "." & k] = v
|
|
if leftAlias.len > 0:
|
|
for col in leftCols:
|
|
padded[leftAlias & "." & col] = Value(kind: vkNull)
|
|
result.add(padded)
|
|
return result
|
|
|
|
of irjsNestedLoop:
|
|
for l in leftRows:
|
|
var matched = false
|
|
for r in rightRows:
|
|
let merged = mergeRow(l, r, leftAlias, rightAlias, leftTable, rightTable)
|
|
if plan.joinCond == nil or valueToString(evalExpr(plan.joinCond, merged, ctx)) == "true":
|
|
result.add(merged)
|
|
matched = true
|
|
if not matched and (plan.joinKind == irjkLeft or plan.joinKind == irjkFull):
|
|
var padded = initTable[string, Value]()
|
|
for k, v in l:
|
|
if not k.startsWith("$"):
|
|
padded[k] = v
|
|
for col in rightCols:
|
|
if col notin padded: padded[col] = Value(kind: vkNull)
|
|
if leftAlias.len > 0:
|
|
for k, v in l:
|
|
if not k.startsWith("$"):
|
|
padded[leftAlias & "." & k] = v
|
|
if rightAlias.len > 0:
|
|
for col in rightCols:
|
|
padded[rightAlias & "." & col] = Value(kind: vkNull)
|
|
result.add(padded)
|
|
|
|
if plan.joinKind == irjkRight or plan.joinKind == irjkFull:
|
|
for r in rightRows:
|
|
var found = false
|
|
for l in leftRows:
|
|
let merged = mergeRow(l, r, leftAlias, rightAlias, leftTable, rightTable)
|
|
if plan.joinCond == nil or valueToString(evalExpr(plan.joinCond, merged, ctx)) == "true":
|
|
found = true
|
|
break
|
|
if not found:
|
|
var padded = initTable[string, Value]()
|
|
for k, v in r:
|
|
if not k.startsWith("$"):
|
|
padded[k] = v
|
|
for col in leftCols:
|
|
if col notin padded: padded[col] = Value(kind: vkNull)
|
|
if rightAlias.len > 0:
|
|
for k, v in r:
|
|
if not k.startsWith("$"):
|
|
padded[rightAlias & "." & k] = v
|
|
if leftAlias.len > 0:
|
|
for col in leftCols:
|
|
padded[leftAlias & "." & col] = Value(kind: vkNull)
|
|
result.add(padded)
|
|
|
|
return result
|
|
|
|
of irpkPivot:
|
|
let sourceRows = executePlan(ctx, plan.pivotSource)
|
|
result = @[]
|
|
# Determine which columns are "group by" (all except pivot column and aggregate target)
|
|
var groupCols: seq[string]
|
|
if sourceRows.len > 0:
|
|
for k, _ in sourceRows[0]:
|
|
if not k.startsWith("$") and k != plan.pivotForCol:
|
|
# Check if this column is the aggregate value column
|
|
let isAggTarget = plan.pivotAgg.kind == irekAggregate and
|
|
plan.pivotAgg.aggArgs.len > 0 and
|
|
plan.pivotAgg.aggArgs[0].kind == irekField and
|
|
plan.pivotAgg.aggArgs[0].fieldPath.len > 0 and
|
|
plan.pivotAgg.aggArgs[0].fieldPath[^1] == k
|
|
if not isAggTarget:
|
|
groupCols.add(k)
|
|
# Group rows by group columns
|
|
var groups = initTable[string, seq[Row]]()
|
|
for row in sourceRows:
|
|
var groupKey = ""
|
|
for col in groupCols:
|
|
groupKey &= (if col in row: valueToString(row[col]) else: "") & "|"
|
|
if groupKey notin groups:
|
|
groups[groupKey] = @[]
|
|
groups[groupKey].add(row)
|
|
# For each group, create a pivoted row
|
|
for gk, groupRows in groups:
|
|
var newRow: Row
|
|
for col in groupCols:
|
|
if col in groupRows[0]:
|
|
newRow[col] = groupRows[0][col]
|
|
# For each pivot value, compute the aggregate
|
|
for pivotVal in plan.pivotInValues:
|
|
var matchingRows: seq[Row]
|
|
for row in groupRows:
|
|
if plan.pivotForCol in row and valueToString(row[plan.pivotForCol]) == pivotVal:
|
|
matchingRows.add(row)
|
|
# Compute aggregate
|
|
var aggResult = ""
|
|
if plan.pivotAgg.kind == irekAggregate:
|
|
case plan.pivotAgg.aggOp
|
|
of irCount:
|
|
if plan.pivotAgg.aggArgs.len == 0:
|
|
aggResult = $matchingRows.len
|
|
else:
|
|
var count = 0
|
|
for row in matchingRows:
|
|
let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx)
|
|
if v.kind != vkNull: count += 1
|
|
aggResult = $count
|
|
of irSum:
|
|
var sum = 0.0
|
|
for row in matchingRows:
|
|
let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx)
|
|
try: sum += parseFloat(valueToString(v)) except CatchableError: discard
|
|
aggResult = $sum
|
|
of irAvg:
|
|
var sum = 0.0
|
|
var count = 0
|
|
for row in matchingRows:
|
|
let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx)
|
|
try: sum += parseFloat(valueToString(v)); count += 1 except CatchableError: discard
|
|
aggResult = if count > 0: $(sum / float(count)) else: "0"
|
|
of irMin:
|
|
var minVal = ""
|
|
for row in matchingRows:
|
|
let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx)
|
|
if v.kind == vkNull: continue
|
|
if minVal == "" or cmpMin(valueToString(v), minVal): minVal = valueToString(v)
|
|
aggResult = minVal
|
|
of irMax:
|
|
var maxVal = ""
|
|
for row in matchingRows:
|
|
let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx)
|
|
if v.kind == vkNull: continue
|
|
if maxVal == "" or cmpMax(valueToString(v), maxVal): maxVal = valueToString(v)
|
|
aggResult = maxVal
|
|
else: discard
|
|
# Clean pivot value (remove quotes)
|
|
let cleanVal = pivotVal.strip(chars = {'\''})
|
|
newRow[cleanVal] = aggResult
|
|
result.add(newRow)
|
|
return result
|
|
|
|
of irpkUnpivot:
|
|
let sourceRows = executePlan(ctx, plan.unpivotSource)
|
|
result = @[]
|
|
# Determine which columns are "identity" (all except the IN columns)
|
|
var identityCols: seq[string]
|
|
if sourceRows.len > 0:
|
|
for k, _ in sourceRows[0]:
|
|
if not k.startsWith("$") and k notin plan.unpivotInCols:
|
|
identityCols.add(k)
|
|
# For each source row, create one row per IN column
|
|
for row in sourceRows:
|
|
for inCol in plan.unpivotInCols:
|
|
var newRow: Row
|
|
for col in identityCols:
|
|
if col in row:
|
|
newRow[col] = row[col]
|
|
newRow[plan.unpivotForCol] = inCol
|
|
newRow[plan.unpivotValueCol] = (if inCol in row: row[inCol] else: Value(kind: vkNull))
|
|
result.add(newRow)
|
|
return result
|
|
|
|
of irpkGraphTraversal:
|
|
# Execute real graph traversal using the graph engine
|
|
result = @[]
|
|
let graphName = plan.graphName
|
|
if graphName notin ctx.graphs:
|
|
return @[]
|
|
|
|
let g = ctx.graphs[graphName]
|
|
if g == nil or g.nodes.len == 0:
|
|
return @[]
|
|
|
|
let algo = plan.graphAlgo.toLowerAscii()
|
|
let returnCols = plan.graphReturnCols
|
|
let firstNodeId = if g.nodes.len > 0: g.nodes.keys.toSeq[0] else: gengine.NodeId(0)
|
|
let explicitStart = try: parseUInt(plan.graphStartNode) except CatchableError: 0'u64
|
|
let explicitEnd = try: parseUInt(plan.graphEndNode) except CatchableError: 0'u64
|
|
|
|
case algo
|
|
of "bfs":
|
|
let startId = if explicitStart > 0: gengine.NodeId(explicitStart) 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, Value]()
|
|
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 explicitStart > 0: gengine.NodeId(explicitStart) 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, Value]()
|
|
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, Value]()
|
|
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 explicitStart > 0 and explicitEnd > 0:
|
|
let startId = gengine.NodeId(explicitStart)
|
|
let endId = gengine.NodeId(explicitEnd)
|
|
let path = gengine.shortestPath(g, startId, endId)
|
|
for nodeId in path:
|
|
var row = initTable[string, Value]()
|
|
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 explicitStart > 0:
|
|
let startId = gengine.NodeId(explicitStart)
|
|
let dists = gengine.dijkstra(g, startId)
|
|
for nodeId, dist in dists:
|
|
var row = initTable[string, Value]()
|
|
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, Value]()
|
|
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, Value]()
|
|
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:
|
|
return @[]
|