diff --git a/pkg/compiler/internal/core/emitter.go b/pkg/compiler/internal/core/emitter.go index 3720f066..05b273ed 100644 --- a/pkg/compiler/internal/core/emitter.go +++ b/pkg/compiler/internal/core/emitter.go @@ -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 { diff --git a/pkg/compiler/internal/core/loop.go b/pkg/compiler/internal/core/loop.go index 3bb1fc2d..8d803bc2 100644 --- a/pkg/compiler/internal/core/loop.go +++ b/pkg/compiler/internal/core/loop.go @@ -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) } diff --git a/pkg/compiler/internal/loop_collect.go b/pkg/compiler/internal/loop_collect.go index 067dea93..b4e1deaf 100644 --- a/pkg/compiler/internal/loop_collect.go +++ b/pkg/compiler/internal/loop_collect.go @@ -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) diff --git a/pkg/compiler/internal/loop_collect_agg.go b/pkg/compiler/internal/loop_collect_agg.go index 4028af96..5526f58e 100644 --- a/pkg/compiler/internal/loop_collect_agg.go +++ b/pkg/compiler/internal/loop_collect_agg.go @@ -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() diff --git a/pkg/compiler/internal/loop_sort.go b/pkg/compiler/internal/loop_sort.go index 8efefea3..8d038725 100644 --- a/pkg/compiler/internal/loop_sort.go +++ b/pkg/compiler/internal/loop_sort.go @@ -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) } diff --git a/test/integration/bytecode/bytecode_for_nested_test.go b/test/integration/bytecode/bytecode_for_nested_test.go new file mode 100644 index 00000000..914a9753 --- /dev/null +++ b/test/integration/bytecode/bytecode_for_nested_test.go @@ -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), + }, + ), + }) +} diff --git a/test/integration/vm/vm_for_collect_test.go b/test/integration/vm/vm_for_collect_test.go index d4eda812..74cf4239 100644 --- a/test/integration/vm/vm_for_collect_test.go +++ b/test/integration/vm/vm_for_collect_test.go @@ -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 = [ diff --git a/test/integration/vm/vm_for_distinct_test.go b/test/integration/vm/vm_for_distinct_test.go new file mode 100644 index 00000000..746b9ae9 --- /dev/null +++ b/test/integration/vm/vm_for_distinct_test.go @@ -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"), + }) +} diff --git a/test/integration/vm/vm_for_nested_test.go b/test/integration/vm/vm_for_nested_test.go new file mode 100644 index 00000000..92de0e9a --- /dev/null +++ b/test/integration/vm/vm_for_nested_test.go @@ -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"}), + }) +} diff --git a/test/integration/vm/vm_for_test.go b/test/integration/vm/vm_for_test.go index 53333d6b..40f4d8d8 100644 --- a/test/integration/vm/vm_for_test.go +++ b/test/integration/vm/vm_for_test.go @@ -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 }))