mirror of
https://github.com/MontFerret/ferret.git
synced 2025-08-13 19:52:52 +02:00
Refactor loop and sort compilation; update loop position handling, improve dataset patching logic, and restructure nested sorting and collector workflows. Add integration tests for distinct and nested FOR
loops.
This commit is contained in:
@@ -59,6 +59,37 @@ func (e *Emitter) PatchSwapAs(pos int, op vm.Opcode, dst vm.Operand, seq Registe
|
||||
}
|
||||
}
|
||||
|
||||
// PatchInsertAx inserts a new instruction at a specific position in the instructions slice, shifting elements to the right.
|
||||
// The inserted instruction includes an opcode and operands, where the third operand is set to a no-op by default.
|
||||
func (e *Emitter) PatchInsertAx(pos int, op vm.Opcode, dst vm.Operand, arg int) {
|
||||
// Append a zero value to create space
|
||||
e.instructions = append(e.instructions, vm.Instruction{})
|
||||
|
||||
// Shift elements to the right
|
||||
copy(e.instructions[pos+1:], e.instructions[pos:])
|
||||
|
||||
// Insert the new value
|
||||
e.instructions[pos] = vm.Instruction{
|
||||
Opcode: op,
|
||||
Operands: [3]vm.Operand{dst, vm.Operand(arg), vm.NoopOperand},
|
||||
}
|
||||
}
|
||||
|
||||
// PatchInsertAxy inserts an instruction at the specified position in the instruction list, shifting existing elements to the right.
|
||||
func (e *Emitter) PatchInsertAxy(pos int, op vm.Opcode, dst vm.Operand, arg1, arg2 int) {
|
||||
// Append a zero value to create space
|
||||
e.instructions = append(e.instructions, vm.Instruction{})
|
||||
|
||||
// Shift elements to the right
|
||||
copy(e.instructions[pos+1:], e.instructions[pos:])
|
||||
|
||||
// Insert the new value
|
||||
e.instructions[pos] = vm.Instruction{
|
||||
Opcode: op,
|
||||
Operands: [3]vm.Operand{dst, vm.Operand(arg1), vm.Operand(arg2)},
|
||||
}
|
||||
}
|
||||
|
||||
// PatchJump patches a jump opcode.
|
||||
func (e *Emitter) PatchJump(instr int) {
|
||||
e.instructions[instr].Operands[0] = vm.Operand(len(e.instructions) - 1)
|
||||
@@ -110,6 +141,11 @@ func (e *Emitter) EmitAx(op vm.Opcode, dest vm.Operand, arg int) {
|
||||
e.EmitABC(op, dest, vm.Operand(arg), 0)
|
||||
}
|
||||
|
||||
// EmitAxy emits an instruction with the given opcode, destination operand, and two integer arguments converted to operands.
|
||||
func (e *Emitter) EmitAxy(op vm.Opcode, dest vm.Operand, arg1, agr2 int) {
|
||||
e.EmitABC(op, dest, vm.Operand(arg1), vm.Operand(agr2))
|
||||
}
|
||||
|
||||
// EmitAs emits an opcode with a destination register and a sequence of registers.
|
||||
func (e *Emitter) EmitAs(op vm.Opcode, dest vm.Operand, seq RegisterSequence) {
|
||||
if seq != nil {
|
||||
|
@@ -30,10 +30,12 @@ const (
|
||||
)
|
||||
|
||||
type Loop struct {
|
||||
Type LoopType
|
||||
Kind LoopKind
|
||||
Distinct bool
|
||||
Allocate bool
|
||||
Type LoopType
|
||||
Kind LoopKind
|
||||
Distinct bool
|
||||
Allocate bool
|
||||
|
||||
Pos int
|
||||
Jump int
|
||||
JumpOffset int
|
||||
|
||||
@@ -45,8 +47,7 @@ type Loop struct {
|
||||
KeyName string
|
||||
Key vm.Operand
|
||||
|
||||
Dst vm.Operand
|
||||
DstPos int
|
||||
Dst vm.Operand
|
||||
}
|
||||
|
||||
func (l *Loop) DeclareKeyVar(name string, st *SymbolTable) {
|
||||
@@ -66,9 +67,10 @@ func (l *Loop) DeclareValueVar(name string, st *SymbolTable) {
|
||||
func (l *Loop) EmitInitialization(alloc *RegisterAllocator, emitter *Emitter) {
|
||||
if l.Allocate {
|
||||
emitter.EmitAb(vm.OpDataSet, l.Dst, l.Distinct)
|
||||
l.DstPos = emitter.Position()
|
||||
}
|
||||
|
||||
l.Pos = emitter.Position()
|
||||
|
||||
if l.Iterator == vm.NoopOperand {
|
||||
l.Iterator = alloc.Allocate(Temp)
|
||||
}
|
||||
|
@@ -91,7 +91,7 @@ func (cc *LoopCollectCompiler) initializeCollector(grouping fql.ICollectGrouping
|
||||
|
||||
func (cc *LoopCollectCompiler) finalizeCollector(loop *core.Loop, collectorType core.CollectorType, kv *core.KV) {
|
||||
// We replace DataSet initialization with Collector initialization
|
||||
cc.ctx.Emitter.PatchSwapAx(loop.DstPos, vm.OpDataSetCollector, loop.Dst, int(collectorType))
|
||||
cc.ctx.Emitter.PatchSwapAx(loop.Pos, vm.OpDataSetCollector, loop.Dst, int(collectorType))
|
||||
cc.ctx.Emitter.EmitABC(vm.OpPushKV, loop.Dst, kv.Key, kv.Value)
|
||||
loop.EmitFinalization(cc.ctx.Emitter)
|
||||
|
||||
|
@@ -49,7 +49,7 @@ func (cc *LoopCollectCompiler) compileGroupedAggregation(c fql.ICollectAggregato
|
||||
func (cc *LoopCollectCompiler) compileGlobalAggregation(c fql.ICollectAggregatorContext) {
|
||||
parentLoop := cc.ctx.Loops.Current()
|
||||
// we create a custom collector for aggregators
|
||||
cc.ctx.Emitter.PatchSwapAx(parentLoop.DstPos, vm.OpDataSetCollector, parentLoop.Dst, int(core.CollectorTypeKeyGroup))
|
||||
cc.ctx.Emitter.PatchSwapAx(parentLoop.Pos, vm.OpDataSetCollector, parentLoop.Dst, int(core.CollectorTypeKeyGroup))
|
||||
|
||||
// Nested scope for aggregators
|
||||
cc.ctx.Symbols.EnterScope()
|
||||
|
@@ -23,65 +23,65 @@ func NewLoopSortCompiler(ctx *CompilerContext) *LoopSortCompiler {
|
||||
// 2. Creating KeyValuePairs for sorting
|
||||
// 3. Patching the loop with appropriate sorter operations
|
||||
// 4. Reinitializing the loop with sorted data
|
||||
func (lc *LoopSortCompiler) Compile(ctx fql.ISortClauseContext) {
|
||||
loop := lc.ctx.Loops.Current()
|
||||
func (c *LoopSortCompiler) Compile(ctx fql.ISortClauseContext) {
|
||||
loop := c.ctx.Loops.Current()
|
||||
clauses := ctx.AllSortClauseExpression()
|
||||
|
||||
// Compile sort keys and get sort directions
|
||||
kvKeyReg, directions := lc.compileSortKeys(clauses)
|
||||
kvKeyReg, directions := c.compileSortKeys(clauses)
|
||||
|
||||
// Handle the value part of KeyValuePair
|
||||
kvValReg := lc.resolveValueRegister(loop)
|
||||
kvValReg := c.resolveValueRegister(loop)
|
||||
|
||||
// Apply the appropriate sorter based on number of sort conditions
|
||||
lc.applySorter(loop, clauses, directions)
|
||||
sorterReg := c.compileSorter(loop, clauses, directions)
|
||||
|
||||
// Emit the KeyValuePair and finalize the sorting process
|
||||
lc.finalizeSorting(loop, kvKeyReg, kvValReg)
|
||||
c.finalizeSorting(loop, core.NewKV(kvKeyReg, kvValReg), sorterReg)
|
||||
}
|
||||
|
||||
// compileSortKeys processes all sort expressions and returns the key register and directions.
|
||||
// For multiple expressions, it creates an array of keys; for single expression, uses the key directly.
|
||||
func (lc *LoopSortCompiler) compileSortKeys(clauses []fql.ISortClauseExpressionContext) (vm.Operand, []runtime.SortDirection) {
|
||||
kvKeyReg := lc.ctx.Registers.Allocate(core.Temp)
|
||||
func (c *LoopSortCompiler) compileSortKeys(clauses []fql.ISortClauseExpressionContext) (vm.Operand, []runtime.SortDirection) {
|
||||
kvKeyReg := c.ctx.Registers.Allocate(core.Temp)
|
||||
directions := make([]runtime.SortDirection, len(clauses))
|
||||
isSortMany := len(clauses) > 1
|
||||
|
||||
if isSortMany {
|
||||
return lc.compileMultipleSortKeys(clauses, kvKeyReg, directions)
|
||||
return c.compileMultipleSortKeys(clauses, kvKeyReg, directions)
|
||||
}
|
||||
|
||||
return lc.compileSingleSortKey(clauses[0], kvKeyReg, directions)
|
||||
return c.compileSingleSortKey(clauses[0], kvKeyReg, directions)
|
||||
}
|
||||
|
||||
// compileMultipleSortKeys handles compilation when there are multiple sort expressions.
|
||||
// It creates an array of compiled expressions for multi-key sorting.
|
||||
func (lc *LoopSortCompiler) compileMultipleSortKeys(clauses []fql.ISortClauseExpressionContext, kvKeyReg vm.Operand, directions []runtime.SortDirection) (vm.Operand, []runtime.SortDirection) {
|
||||
func (c *LoopSortCompiler) compileMultipleSortKeys(clauses []fql.ISortClauseExpressionContext, kvKeyReg vm.Operand, directions []runtime.SortDirection) (vm.Operand, []runtime.SortDirection) {
|
||||
clausesRegs := make([]vm.Operand, len(clauses))
|
||||
keyRegs := lc.ctx.Registers.AllocateSequence(len(clauses))
|
||||
keyRegs := c.ctx.Registers.AllocateSequence(len(clauses))
|
||||
|
||||
// Compile each sort expression and store direction
|
||||
for i, clause := range clauses {
|
||||
clauseReg := lc.ctx.ExprCompiler.Compile(clause.Expression())
|
||||
lc.ctx.Emitter.EmitMove(keyRegs[i], clauseReg)
|
||||
clauseReg := c.ctx.ExprCompiler.Compile(clause.Expression())
|
||||
c.ctx.Emitter.EmitMove(keyRegs[i], clauseReg)
|
||||
clausesRegs[i] = keyRegs[i]
|
||||
directions[i] = sortDirection(clause.SortDirection())
|
||||
// TODO: Free registers after use
|
||||
}
|
||||
|
||||
// CreateFor array of sort keys
|
||||
arrReg := lc.ctx.Registers.Allocate(core.Temp)
|
||||
lc.ctx.Emitter.EmitAs(vm.OpList, arrReg, keyRegs)
|
||||
lc.ctx.Emitter.EmitAB(vm.OpMove, kvKeyReg, arrReg)
|
||||
arrReg := c.ctx.Registers.Allocate(core.Temp)
|
||||
c.ctx.Emitter.EmitAs(vm.OpList, arrReg, keyRegs)
|
||||
c.ctx.Emitter.EmitAB(vm.OpMove, kvKeyReg, arrReg)
|
||||
// TODO: Free registers after use
|
||||
|
||||
return kvKeyReg, directions
|
||||
}
|
||||
|
||||
// compileSingleSortKey handles compilation when there is only one sort expression.
|
||||
func (lc *LoopSortCompiler) compileSingleSortKey(clause fql.ISortClauseExpressionContext, kvKeyReg vm.Operand, directions []runtime.SortDirection) (vm.Operand, []runtime.SortDirection) {
|
||||
clauseReg := lc.ctx.ExprCompiler.Compile(clause.Expression())
|
||||
lc.ctx.Emitter.EmitAB(vm.OpMove, kvKeyReg, clauseReg)
|
||||
func (c *LoopSortCompiler) compileSingleSortKey(clause fql.ISortClauseExpressionContext, kvKeyReg vm.Operand, directions []runtime.SortDirection) (vm.Operand, []runtime.SortDirection) {
|
||||
clauseReg := c.ctx.ExprCompiler.Compile(clause.Expression())
|
||||
c.ctx.Emitter.EmitAB(vm.OpMove, kvKeyReg, clauseReg)
|
||||
directions[0] = sortDirection(clause.SortDirection())
|
||||
|
||||
return kvKeyReg, directions
|
||||
@@ -90,33 +90,55 @@ func (lc *LoopSortCompiler) compileSingleSortKey(clause fql.ISortClauseExpressio
|
||||
// resolveValueRegister determines the appropriate register for the value part of KeyValuePair.
|
||||
// If the loop already has a value name, reuse it; otherwise, allocate a new register
|
||||
// and load the value from the iterator.
|
||||
func (lc *LoopSortCompiler) resolveValueRegister(loop *core.Loop) vm.Operand {
|
||||
func (c *LoopSortCompiler) resolveValueRegister(loop *core.Loop) vm.Operand {
|
||||
// If value is already used in the loop body, reuse the existing register
|
||||
if loop.ValueName != "" {
|
||||
return loop.Value
|
||||
}
|
||||
|
||||
// Otherwise, allocate a new register and load the value from iterator
|
||||
kvValReg := lc.ctx.Registers.Allocate(core.Temp)
|
||||
loop.EmitValue(kvValReg, lc.ctx.Emitter)
|
||||
kvValReg := c.ctx.Registers.Allocate(core.Temp)
|
||||
loop.EmitValue(kvValReg, c.ctx.Emitter)
|
||||
return kvValReg
|
||||
}
|
||||
|
||||
// applySorter patches the loop with the appropriate sorter operation based on
|
||||
// whether we have single or multiple sort conditions.
|
||||
func (lc *LoopSortCompiler) applySorter(loop *core.Loop, clauses []fql.ISortClauseExpressionContext, directions []runtime.SortDirection) {
|
||||
// compileSorter configures a sorter for a loop based on provided sort clauses and directions.
|
||||
// It handles both single-key and multi-key sorting by emitting the appropriate VM operations.
|
||||
func (c *LoopSortCompiler) compileSorter(loop *core.Loop, clauses []fql.ISortClauseExpressionContext, directions []runtime.SortDirection) vm.Operand {
|
||||
isSortMany := len(clauses) > 1
|
||||
|
||||
if isSortMany {
|
||||
// Multi-key sorting requires encoded directions and count
|
||||
encoded := runtime.EncodeSortDirections(directions)
|
||||
count := len(clauses)
|
||||
lc.ctx.Emitter.PatchSwapAxy(loop.DstPos, vm.OpDataSetMultiSorter, loop.Dst, encoded, count)
|
||||
} else {
|
||||
// Single-key sorting only needs the direction
|
||||
dir := sortDirection(clauses[0].SortDirection())
|
||||
lc.ctx.Emitter.PatchSwapAx(loop.DstPos, vm.OpDataSetSorter, loop.Dst, int(dir))
|
||||
|
||||
if loop.Allocate {
|
||||
c.ctx.Emitter.PatchSwapAxy(loop.Pos, vm.OpDataSetMultiSorter, loop.Dst, encoded, count)
|
||||
|
||||
return loop.Dst
|
||||
}
|
||||
|
||||
dst := c.ctx.Registers.Allocate(core.Temp)
|
||||
c.ctx.Emitter.PatchInsertAxy(loop.Pos, vm.OpDataSetMultiSorter, loop.Dst, encoded, count)
|
||||
loop.Jump++
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
// Single-key sorting only needs the direction
|
||||
dir := sortDirection(clauses[0].SortDirection())
|
||||
|
||||
if loop.Allocate {
|
||||
c.ctx.Emitter.PatchSwapAx(loop.Pos, vm.OpDataSetSorter, loop.Dst, int(dir))
|
||||
|
||||
return loop.Dst
|
||||
}
|
||||
|
||||
dst := c.ctx.Registers.Allocate(core.Temp)
|
||||
c.ctx.Emitter.PatchInsertAx(loop.Pos, vm.OpDataSetSorter, dst, int(dir))
|
||||
loop.Jump++
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
// finalizeSorting completes the sorting process by:
|
||||
@@ -124,16 +146,20 @@ func (lc *LoopSortCompiler) applySorter(loop *core.Loop, clauses []fql.ISortClau
|
||||
// 2. Finalizing the current loop
|
||||
// 3. Replacing the loop source with sorted results
|
||||
// 4. Reinitializing the loop for iteration over sorted data
|
||||
func (lc *LoopSortCompiler) finalizeSorting(loop *core.Loop, kvKeyReg, kvValReg vm.Operand) {
|
||||
func (c *LoopSortCompiler) finalizeSorting(loop *core.Loop, kv *core.KV, sorter vm.Operand) {
|
||||
// Add the KeyValuePair to the dataset
|
||||
lc.ctx.Emitter.EmitABC(vm.OpPushKV, loop.Dst, kvKeyReg, kvValReg)
|
||||
c.ctx.Emitter.EmitABC(vm.OpPushKV, sorter, kv.Key, kv.Value)
|
||||
|
||||
// Finalize the current loop iteration
|
||||
loop.EmitFinalization(lc.ctx.Emitter)
|
||||
loop.EmitFinalization(c.ctx.Emitter)
|
||||
|
||||
// Replace the loop source with sorted results
|
||||
lc.ctx.Emitter.EmitAB(vm.OpMove, loop.Src, loop.Dst)
|
||||
c.ctx.Emitter.EmitAB(vm.OpMove, loop.Src, sorter)
|
||||
|
||||
if !loop.Allocate {
|
||||
c.ctx.Registers.Free(sorter)
|
||||
}
|
||||
|
||||
// Reinitialize the loop to iterate over sorted data
|
||||
loop.EmitInitialization(lc.ctx.Registers, lc.ctx.Emitter)
|
||||
loop.EmitInitialization(c.ctx.Registers, c.ctx.Emitter)
|
||||
}
|
||||
|
91
test/integration/bytecode/bytecode_for_nested_test.go
Normal file
91
test/integration/bytecode/bytecode_for_nested_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package bytecode_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/MontFerret/ferret/pkg/vm"
|
||||
)
|
||||
|
||||
func TestForNested(t *testing.T) {
|
||||
RunUseCases(t, []UseCase{
|
||||
SkipByteCodeCase(
|
||||
`FOR prop IN ["a"]
|
||||
FOR val IN [1, 2, 3]
|
||||
RETURN {[prop]: val}`,
|
||||
BC{
|
||||
I(vm.OpReturn, 0, 7),
|
||||
},
|
||||
),
|
||||
SkipByteCodeCase(
|
||||
`FOR val IN 1..3
|
||||
FOR prop IN ["a"]
|
||||
RETURN {[prop]: val}`,
|
||||
BC{
|
||||
I(vm.OpReturn, 0, 7),
|
||||
},
|
||||
),
|
||||
SkipByteCodeCase(
|
||||
`FOR prop IN ["a"]
|
||||
FOR val IN 1..3
|
||||
RETURN {[prop]: val}`,
|
||||
BC{
|
||||
I(vm.OpReturn, 0, 7),
|
||||
},
|
||||
),
|
||||
SkipByteCodeCase(
|
||||
`FOR prop IN ["a"]
|
||||
FOR val IN [1, 2, 3]
|
||||
FOR val2 IN [1, 2, 3]
|
||||
RETURN { [prop]: [val, val2] }`,
|
||||
BC{
|
||||
I(vm.OpReturn, 0, 7),
|
||||
},
|
||||
),
|
||||
SkipByteCodeCase(
|
||||
`FOR val IN [1, 2, 3]
|
||||
RETURN (
|
||||
FOR prop IN ["a", "b", "c"]
|
||||
RETURN { [prop]: val }
|
||||
)`,
|
||||
BC{
|
||||
I(vm.OpReturn, 0, 7),
|
||||
},
|
||||
),
|
||||
SkipByteCodeCase(
|
||||
`FOR val IN [1, 2, 3]
|
||||
LET sub = (
|
||||
FOR prop IN ["a", "b", "c"]
|
||||
RETURN { [prop]: val }
|
||||
)
|
||||
|
||||
RETURN sub`,
|
||||
BC{
|
||||
I(vm.OpReturn, 0, 7),
|
||||
},
|
||||
),
|
||||
SkipByteCodeCase(`
|
||||
LET strs = ["foo", "bar", "qaz", "abc"]
|
||||
|
||||
FOR s IN strs
|
||||
SORT s
|
||||
FOR n IN 0..1
|
||||
RETURN CONCAT(s, n)
|
||||
`,
|
||||
BC{
|
||||
I(vm.OpReturn, 0, 7),
|
||||
},
|
||||
),
|
||||
ByteCodeCase(`
|
||||
LET strs = ["foo", "bar", "qaz", "abc"]
|
||||
|
||||
FOR n IN 0..1
|
||||
FOR s IN strs
|
||||
SORT s
|
||||
RETURN CONCAT(s, n)
|
||||
`,
|
||||
BC{
|
||||
I(vm.OpReturn, 0, 7),
|
||||
},
|
||||
),
|
||||
})
|
||||
}
|
@@ -6,7 +6,7 @@ import (
|
||||
. "github.com/MontFerret/ferret/test/integration/base"
|
||||
)
|
||||
|
||||
func TestCollect(t *testing.T) {
|
||||
func TestForCollect(t *testing.T) {
|
||||
RunUseCases(t, []UseCase{
|
||||
SkipCaseCompilationError(`
|
||||
LET users = [
|
||||
|
176
test/integration/vm/vm_for_distinct_test.go
Normal file
176
test/integration/vm/vm_for_distinct_test.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package vm_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/MontFerret/ferret/test/integration/base"
|
||||
)
|
||||
|
||||
func TestForDistinct(t *testing.T) {
|
||||
RunUseCases(t, []UseCase{
|
||||
CaseArray(
|
||||
`FOR i IN [ 1, 2, 3, 4, 1, 3 ]
|
||||
RETURN DISTINCT i
|
||||
`,
|
||||
[]any{1, 2, 3, 4},
|
||||
),
|
||||
CaseArray(
|
||||
`FOR i IN ["foo", "bar", "qaz", "foo", "abc", "bar"]
|
||||
RETURN DISTINCT i
|
||||
`,
|
||||
[]any{"foo", "bar", "qaz", "abc"},
|
||||
),
|
||||
CaseArray(
|
||||
`FOR i IN [["foo"], ["bar"], ["qaz"], ["foo"], ["abc"], ["bar"]]
|
||||
RETURN DISTINCT i
|
||||
`,
|
||||
[]any{[]any{"foo"}, []any{"bar"}, []any{"qaz"}, []any{"abc"}},
|
||||
),
|
||||
CaseArray(`
|
||||
LET strs = ["foo", "bar", "qaz", "foo", "abc", "bar"]
|
||||
|
||||
FOR s IN strs
|
||||
SORT s
|
||||
RETURN DISTINCT s
|
||||
`, []any{"abc", "bar", "foo", "qaz"}, "Should sort and respect DISTINCT keyword"),
|
||||
CaseArray(
|
||||
`
|
||||
FOR i IN [ 1, 1, 2, 3, 4, 1, 3 ]
|
||||
LIMIT 2
|
||||
RETURN DISTINCT i
|
||||
`,
|
||||
[]any{1}),
|
||||
CaseArray(
|
||||
`
|
||||
FOR i IN [ 1, 1, 1, 3, 4, 1, 3 ]
|
||||
LIMIT 1, 2
|
||||
RETURN DISTINCT i
|
||||
`,
|
||||
[]any{1}),
|
||||
CaseArray(
|
||||
`
|
||||
FOR i IN [ 1, 2, 3, 4, 1, 3, 3, 4 ]
|
||||
FILTER i > 2
|
||||
RETURN DISTINCT i
|
||||
`,
|
||||
[]any{3, 4},
|
||||
),
|
||||
CaseArray(
|
||||
`LET users = [
|
||||
{
|
||||
active: true,
|
||||
married: true,
|
||||
age: 31,
|
||||
gender: "m"
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
married: false,
|
||||
age: 25,
|
||||
gender: "f"
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
married: false,
|
||||
age: 36,
|
||||
gender: "m"
|
||||
},
|
||||
{
|
||||
active: false,
|
||||
married: true,
|
||||
age: 69,
|
||||
gender: "m"
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
married: true,
|
||||
age: 45,
|
||||
gender: "f"
|
||||
}
|
||||
]
|
||||
FOR i IN users
|
||||
COLLECT gender = i.gender, age = i.age
|
||||
RETURN DISTINCT {gender}
|
||||
`, []any{
|
||||
map[string]any{"gender": "f"},
|
||||
map[string]any{"gender": "m"},
|
||||
}),
|
||||
CaseArray(
|
||||
`LET users = [
|
||||
{
|
||||
active: true,
|
||||
age: 31,
|
||||
gender: "m",
|
||||
married: true
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
age: 25,
|
||||
gender: "f",
|
||||
married: false
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
age: 36,
|
||||
gender: "m",
|
||||
married: false
|
||||
},
|
||||
{
|
||||
active: false,
|
||||
age: 69,
|
||||
gender: "m",
|
||||
married: true
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
age: 45,
|
||||
gender: "f",
|
||||
married: true
|
||||
}
|
||||
]
|
||||
FOR i IN users
|
||||
COLLECT gender = i.gender INTO genders = { active: i.active }
|
||||
RETURN DISTINCT genders[0]
|
||||
`, []any{
|
||||
map[string]any{"active": true},
|
||||
}),
|
||||
CaseArray(`
|
||||
LET users = [
|
||||
{
|
||||
active: true,
|
||||
age: 39,
|
||||
gender: "f",
|
||||
married: false
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
age: 45,
|
||||
gender: "f",
|
||||
married: true
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
age: 39,
|
||||
gender: "m",
|
||||
married: false
|
||||
},
|
||||
{
|
||||
active: false,
|
||||
age: 45,
|
||||
gender: "m",
|
||||
married: true
|
||||
}
|
||||
]
|
||||
FOR u IN users
|
||||
COLLECT genderGroup = u.gender
|
||||
AGGREGATE minAge = MIN(u.age), maxAge = MAX(u.age)
|
||||
|
||||
RETURN DISTINCT {
|
||||
minAge,
|
||||
maxAge
|
||||
}
|
||||
`, []any{
|
||||
map[string]any{"maxAge": 45, "minAge": 39},
|
||||
}, "Should collect and aggregate values by a single key"),
|
||||
})
|
||||
}
|
71
test/integration/vm/vm_for_nested_test.go
Normal file
71
test/integration/vm/vm_for_nested_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package vm_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/MontFerret/ferret/test/integration/base"
|
||||
)
|
||||
|
||||
func TestForNested(t *testing.T) {
|
||||
RunUseCases(t, []UseCase{
|
||||
CaseArray(
|
||||
`FOR prop IN ["a"]
|
||||
FOR val IN [1, 2, 3]
|
||||
RETURN {[prop]: val}`,
|
||||
[]any{map[string]any{"a": 1}, map[string]any{"a": 2}, map[string]any{"a": 3}},
|
||||
),
|
||||
CaseArray(
|
||||
`FOR val IN 1..3
|
||||
FOR prop IN ["a"]
|
||||
RETURN {[prop]: val}`,
|
||||
[]any{map[string]any{"a": 1}, map[string]any{"a": 2}, map[string]any{"a": 3}},
|
||||
),
|
||||
CaseArray(
|
||||
`FOR prop IN ["a"]
|
||||
FOR val IN 1..3
|
||||
RETURN {[prop]: val}`,
|
||||
[]any{map[string]any{"a": 1}, map[string]any{"a": 2}, map[string]any{"a": 3}},
|
||||
),
|
||||
CaseArray(
|
||||
`FOR prop IN ["a"]
|
||||
FOR val IN [1, 2, 3]
|
||||
FOR val2 IN [1, 2, 3]
|
||||
RETURN { [prop]: [val, val2] }`,
|
||||
[]any{map[string]any{"a": []int{1, 1}}, map[string]any{"a": []int{1, 2}}, map[string]any{"a": []int{1, 3}}, map[string]any{"a": []int{2, 1}}, map[string]any{"a": []int{2, 2}}, map[string]any{"a": []int{2, 3}}, map[string]any{"a": []int{3, 1}}, map[string]any{"a": []int{3, 2}}, map[string]any{"a": []int{3, 3}}},
|
||||
),
|
||||
CaseArray(
|
||||
`FOR val IN [1, 2, 3]
|
||||
RETURN (
|
||||
FOR prop IN ["a", "b", "c"]
|
||||
RETURN { [prop]: val }
|
||||
)`,
|
||||
[]any{[]any{map[string]any{"a": 1}, map[string]any{"b": 1}, map[string]any{"c": 1}}, []any{map[string]any{"a": 2}, map[string]any{"b": 2}, map[string]any{"c": 2}}, []any{map[string]any{"a": 3}, map[string]any{"b": 3}, map[string]any{"c": 3}}},
|
||||
),
|
||||
CaseArray(
|
||||
`FOR val IN [1, 2, 3]
|
||||
LET sub = (
|
||||
FOR prop IN ["a", "b", "c"]
|
||||
RETURN { [prop]: val }
|
||||
)
|
||||
|
||||
RETURN sub`,
|
||||
[]any{[]any{map[string]any{"a": 1}, map[string]any{"b": 1}, map[string]any{"c": 1}}, []any{map[string]any{"a": 2}, map[string]any{"b": 2}, map[string]any{"c": 2}}, []any{map[string]any{"a": 3}, map[string]any{"b": 3}, map[string]any{"c": 3}}},
|
||||
),
|
||||
CaseArray(`
|
||||
LET strs = ["foo", "bar", "qaz", "abc"]
|
||||
|
||||
FOR s IN strs
|
||||
SORT s
|
||||
FOR n IN 0..1
|
||||
RETURN CONCAT(s, n)
|
||||
`, []any{"abc0", "abc1", "bar0", "bar1", "foo0", "foo1", "qaz0", "qaz1"}),
|
||||
CaseArray(`
|
||||
LET strs = ["foo", "bar", "qaz", "abc"]
|
||||
|
||||
FOR n IN 0..1
|
||||
FOR s IN strs
|
||||
SORT s
|
||||
RETURN CONCAT(s, n)
|
||||
`, []any{"abc0", "bar0", "foo0", "qaz0", "abc1", "bar1", "foo1", "qaz1"}),
|
||||
})
|
||||
}
|
@@ -79,55 +79,6 @@ FOR i IN 1..5
|
||||
`FOR i IN { items: [{name: 'foo'}, {name: 'bar'}, {name: 'qaz'}] }.items RETURN i.name`,
|
||||
[]any{"foo", "bar", "qaz"},
|
||||
),
|
||||
CaseArray(
|
||||
`FOR prop IN ["a"]
|
||||
FOR val IN [1, 2, 3]
|
||||
RETURN {[prop]: val}`,
|
||||
[]any{map[string]any{"a": 1}, map[string]any{"a": 2}, map[string]any{"a": 3}},
|
||||
),
|
||||
CaseArray(
|
||||
`FOR val IN 1..3
|
||||
FOR prop IN ["a"]
|
||||
RETURN {[prop]: val}`,
|
||||
[]any{map[string]any{"a": 1}, map[string]any{"a": 2}, map[string]any{"a": 3}},
|
||||
),
|
||||
CaseArray(
|
||||
`FOR prop IN ["a"]
|
||||
FOR val IN 1..3
|
||||
RETURN {[prop]: val}`,
|
||||
[]any{map[string]any{"a": 1}, map[string]any{"a": 2}, map[string]any{"a": 3}},
|
||||
),
|
||||
CaseArray(
|
||||
`FOR prop IN ["a"]
|
||||
FOR val IN [1, 2, 3]
|
||||
FOR val2 IN [1, 2, 3]
|
||||
RETURN { [prop]: [val, val2] }`,
|
||||
[]any{map[string]any{"a": []int{1, 1}}, map[string]any{"a": []int{1, 2}}, map[string]any{"a": []int{1, 3}}, map[string]any{"a": []int{2, 1}}, map[string]any{"a": []int{2, 2}}, map[string]any{"a": []int{2, 3}}, map[string]any{"a": []int{3, 1}}, map[string]any{"a": []int{3, 2}}, map[string]any{"a": []int{3, 3}}},
|
||||
),
|
||||
CaseArray(
|
||||
`FOR val IN [1, 2, 3]
|
||||
RETURN (
|
||||
FOR prop IN ["a", "b", "c"]
|
||||
RETURN { [prop]: val }
|
||||
)`,
|
||||
[]any{[]any{map[string]any{"a": 1}, map[string]any{"b": 1}, map[string]any{"c": 1}}, []any{map[string]any{"a": 2}, map[string]any{"b": 2}, map[string]any{"c": 2}}, []any{map[string]any{"a": 3}, map[string]any{"b": 3}, map[string]any{"c": 3}}},
|
||||
),
|
||||
CaseArray(
|
||||
`FOR val IN [1, 2, 3]
|
||||
LET sub = (
|
||||
FOR prop IN ["a", "b", "c"]
|
||||
RETURN { [prop]: val }
|
||||
)
|
||||
|
||||
RETURN sub`,
|
||||
[]any{[]any{map[string]any{"a": 1}, map[string]any{"b": 1}, map[string]any{"c": 1}}, []any{map[string]any{"a": 2}, map[string]any{"b": 2}, map[string]any{"c": 2}}, []any{map[string]any{"a": 3}, map[string]any{"b": 3}, map[string]any{"c": 3}}},
|
||||
),
|
||||
CaseArray(
|
||||
`FOR i IN [ 1, 2, 3, 4, 1, 3 ]
|
||||
RETURN DISTINCT i
|
||||
`,
|
||||
[]any{1, 2, 3, 4},
|
||||
),
|
||||
}, vm.WithFunction("TEST_FN", func(ctx context.Context, args ...runtime.Value) (runtime.Value, error) {
|
||||
return nil, nil
|
||||
}))
|
||||
|
Reference in New Issue
Block a user