1
0
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:
Tim Voronov
2025-06-25 11:21:18 -04:00
parent 3433e3d4d1
commit df45d13b56
10 changed files with 448 additions and 95 deletions

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)
}

View 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),
},
),
})
}

View File

@@ -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 = [

View 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"),
})
}

View 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"}),
})
}

View File

@@ -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
}))