1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-08-15 20:02:56 +02:00

Refactor FOR-WHILE loop compilation to ensure proper LoopKind handling and value/key resolution, update map operations, and add integration tests for COLLECT scenarios.

This commit is contained in:
Tim Voronov
2025-07-02 21:14:33 -04:00
parent b3970fbb43
commit 0b528b62c0
2 changed files with 978 additions and 3 deletions

View File

@@ -53,6 +53,11 @@ func (c *LoopCollectCompiler) compileCollect(ctx fql.ICollectClauseContext, aggr
c.finalizeCollector(loop, collectorType, kv)
// If we are using a projection, we need to ensure the loop is set to ForInLoop
if loop.Kind != core.ForInLoop {
loop.Kind = core.ForInLoop
}
// If the projection is used, we allocate a new register for the variable and put the iterator's value into it
if projectionVarName != "" {
// Now we need to expand group variables from the dataset
@@ -84,7 +89,12 @@ func (c *LoopCollectCompiler) initializeCollector(grouping fql.ICollectGroupingC
// Setup value register and emit value from current loop
kv.Value = c.ctx.Registers.Allocate(core.Temp)
loop.EmitValue(kv.Value, c.ctx.Emitter)
if loop.Kind == core.ForInLoop {
loop.EmitValue(kv.Value, c.ctx.Emitter)
} else {
loop.EmitKey(kv.Value, c.ctx.Emitter)
}
return kv, groupSelectors
}
@@ -222,8 +232,14 @@ func (c *LoopCollectCompiler) compileDefaultGroupProjection(loop *core.Loop, kv
// TODO: Review this. It's quite a questionable ArrangoDB feature of wrapping group items by a nested object
// We will keep it for now for backward compatibility.
loadConstantTo(c.ctx, runtime.String(loop.ValueName), seq[0]) // Map key
c.ctx.Emitter.EmitAB(vm.OpMove, seq[1], kv.Value) // Map value
if loop.Kind == core.ForInLoop {
loadConstantTo(c.ctx, runtime.String(loop.ValueName), seq[0]) // Map key
} else {
loadConstantTo(c.ctx, runtime.String(loop.KeyName), seq[0]) // Map key
}
c.ctx.Emitter.EmitAB(vm.OpMove, seq[1], kv.Value) // Map value
c.ctx.Emitter.EmitAs(vm.OpMap, kv.Value, seq)
c.ctx.Registers.FreeSequence(seq)

View File

@@ -0,0 +1,959 @@
package vm_test
import (
"github.com/MontFerret/ferret/pkg/vm"
"testing"
)
func TestForWhileCollect(t *testing.T) {
RunUseCases(t, []UseCase{
SkipCaseCompilationError(`
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 WHILE UNTIL(LENGTH(users))
COLLECT gender = users[i].gender
RETURN {
user: users[i],
gender: gender
}
`, "Should not have access to initial variables"),
SkipCaseCompilationError(`
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 WHILE UNTIL(LENGTH(users))
LET x = "foo"
COLLECT gender = users[i].gender
RETURN {x, gender}
`, "Should not have access to variables defined before COLLECT"),
CaseArray(`
LET users = []
FOR i WHILE UNTIL(LENGTH(users))
COLLECT gender = users[i].gender
RETURN gender
`,
[]any{},
"Should handle empty arrays gracefully"),
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 WHILE UNTIL(LENGTH(users))
COLLECT gender = users[i].gender
RETURN gender
`, []any{"f", "m"}, "Should group result by a single key"),
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 WHILE UNTIL(LENGTH(users))
COLLECT ageGroup = FLOOR(users[i].age / 5)
RETURN { ageGroup }
`, []any{
map[string]int{"ageGroup": 5},
map[string]int{"ageGroup": 6},
map[string]int{"ageGroup": 7},
map[string]int{"ageGroup": 9},
map[string]int{"ageGroup": 13},
}, "Should group result by a single key expression"),
Case(`
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"
}
]
LET grouped = (FOR i WHILE UNTIL(LENGTH(users))
COLLECT gender = users[i].gender
RETURN gender)
RETURN grouped[0]
`, "f", "Should return correct group key by an index"),
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 WHILE UNTIL(LENGTH(users))
COLLECT gender = users[i].gender, age = users[i].age
RETURN {age, gender}
`, []any{
map[string]any{"age": 25, "gender": "f"},
map[string]any{"age": 45, "gender": "f"},
map[string]any{"age": 31, "gender": "m"},
map[string]any{"age": 36, "gender": "m"},
map[string]any{"age": 69, "gender": "m"},
}, "Should group result by multiple keys"),
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 WHILE UNTIL(LENGTH(users))
COLLECT gender = users[i].gender INTO genders
RETURN {
gender,
values: genders
}
`, []any{
map[string]any{
"gender": "f",
"values": []any{
map[string]any{
"i": 1,
},
map[string]any{
"i": 4,
},
},
},
map[string]any{
"gender": "m",
"values": []any{
map[string]any{
"i": 0,
},
map[string]any{
"i": 2,
},
map[string]any{
"i": 3,
},
},
},
}, "Should create default projection"),
CaseArray(`
LET users = []
FOR i WHILE UNTIL(LENGTH(users))
COLLECT gender = users[i].gender INTO genders
RETURN {
gender,
values: genders
}
`, []any{}, "COLLECT gender = i.gender INTO genders: should return an empty array when source is empty"),
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 WHILE UNTIL(LENGTH(users))
COLLECT gender = users[i].gender INTO genders = { active: users[i].active }
RETURN {
gender,
values: genders
}
`, []any{
map[string]any{
"gender": "f",
"values": []any{
map[string]any{"active": true},
map[string]any{"active": true},
},
},
map[string]any{
"gender": "m",
"values": []any{
map[string]any{"active": true},
map[string]any{"active": true},
map[string]any{"active": false},
},
},
}, "Should create custom projection"),
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 WHILE UNTIL(LENGTH(users))
COLLECT gender = users[i].gender, age = users[i].age INTO genders = { active: users[i].active }
RETURN {
age,
gender,
values: genders
}
`, []any{
map[string]any{
"age": 25,
"gender": "f",
"values": []any{
map[string]any{"active": true},
},
},
map[string]any{
"age": 45,
"gender": "f",
"values": []any{
map[string]any{"active": true},
},
},
map[string]any{
"age": 31,
"gender": "m",
"values": []any{
map[string]any{"active": true},
},
},
map[string]any{
"age": 36,
"gender": "m",
"values": []any{
map[string]any{"active": true},
},
},
map[string]any{
"age": 69,
"gender": "m",
"values": []any{
map[string]any{"active": false},
},
},
}, "Should create custom projection grouped by multiple keys"),
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 WHILE UNTIL(LENGTH(users))
LET married = users[i].married
COLLECT gender = users[i].gender INTO genders KEEP married
RETURN {
gender,
values: genders
}
`, []any{
map[string]any{
"gender": "f",
"values": []any{
map[string]any{"married": false},
map[string]any{"married": true},
},
},
map[string]any{
"gender": "m",
"values": []any{
map[string]any{"married": true},
map[string]any{"married": false},
map[string]any{"married": true},
},
},
}, "Should create default projection with default KEEP"),
CaseArray(`
LET users = []
FOR i WHILE UNTIL(LENGTH(users))
LET married = users[i].married
COLLECT gender = users[i].gender INTO genders KEEP married
RETURN {
gender,
values: genders
}
`, []any{}, "COLLECT gender = i.gender INTO genders KEEP married: Should return an empty array when source is empty"),
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 WHILE UNTIL(LENGTH(users))
LET married = users[i].married
LET age = users[i].age
COLLECT gender = users[i].gender INTO values KEEP married, age
RETURN {
gender,
values
}
`, []any{
map[string]any{
"gender": "f",
"values": []any{
map[string]any{
"married": false,
"age": 25,
},
map[string]any{
"married": true,
"age": 45,
},
},
},
map[string]any{
"gender": "m",
"values": []any{
map[string]any{
"married": true,
"age": 31,
},
map[string]any{
"married": false,
"age": 36,
},
map[string]any{
"married": true,
"age": 69,
},
},
},
}, "Should create default projection with default KEEP using multiple keys"),
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 WHILE UNTIL(LENGTH(users))
LET married = "foo"
COLLECT gender = users[i].gender INTO values KEEP married
RETURN {
gender,
values
}
`, []any{
map[string]any{
"gender": "f",
"values": []any{
map[string]any{"married": "foo"},
map[string]any{"married": "foo"},
},
},
map[string]any{
"gender": "m",
"values": []any{
map[string]any{"married": "foo"},
map[string]any{"married": "foo"},
map[string]any{"married": "foo"},
},
},
}, "Should create default projection with custom KEEP"),
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 WHILE UNTIL(LENGTH(users))
LET married = "foo"
LET age = "bar"
COLLECT gender = users[i].gender INTO values KEEP married, age
RETURN {
gender,
values
}
`, []any{
map[string]any{
"gender": "f",
"values": []any{
map[string]any{
"married": "foo",
"age": "bar",
},
map[string]any{
"married": "foo",
"age": "bar",
},
},
},
map[string]any{
"gender": "m",
"values": []any{
map[string]any{
"married": "foo",
"age": "bar",
},
map[string]any{
"married": "foo",
"age": "bar",
},
map[string]any{
"married": "foo",
"age": "bar",
},
},
},
}, "Should create default projection with custom KEEP using multiple keys"),
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 WHILE UNTIL(LENGTH(users))
LET bar = "foo"
COLLECT gender = users[i].gender INTO values KEEP bar
RETURN {
gender,
values
}
`, []any{
map[string]any{
"gender": "f",
"values": []any{
map[string]any{"bar": "foo"},
map[string]any{"bar": "foo"},
},
},
map[string]any{
"gender": "m",
"values": []any{
map[string]any{"bar": "foo"},
map[string]any{"bar": "foo"},
map[string]any{"bar": "foo"},
},
},
}, "Should create default projection with custom KEEP with custom name"),
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 WHILE UNTIL(LENGTH(users))
LET bar = "foo"
LET baz = "bar"
COLLECT gender = users[i].gender INTO values KEEP bar, baz
RETURN {
gender,
values
}
`, []any{
map[string]any{
"gender": "f",
"values": []any{
map[string]any{"bar": "foo", "baz": "bar"},
map[string]any{"bar": "foo", "baz": "bar"},
},
},
map[string]any{
"gender": "m",
"values": []any{
map[string]any{"bar": "foo", "baz": "bar"},
map[string]any{"bar": "foo", "baz": "bar"},
map[string]any{"bar": "foo", "baz": "bar"},
},
},
}, "Should create default projection with custom KEEP with multiple custom names"),
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 WHILE UNTIL(LENGTH(users))
COLLECT gender = users[i].gender WITH COUNT INTO numberOfUsers
RETURN {
gender,
values: numberOfUsers
}
`, []any{
map[string]any{
"gender": "f",
"values": 2,
},
map[string]any{
"gender": "m",
"values": 3,
},
}, "Should group and count result by a single key"),
CaseArray(
`
LET users = []
FOR i WHILE UNTIL(LENGTH(users))
COLLECT gender = users[i].gender WITH COUNT INTO numberOfUsers
RETURN {
gender,
values: numberOfUsers
}
`, []any{}, "COLLECT gender = i.gender WITH COUNT INTO numberOfUsers: Should return empty array when source is empty"),
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 WHILE UNTIL(LENGTH(users))
COLLECT WITH COUNT INTO numberOfUsers
RETURN numberOfUsers
`, []any{
5,
}, "Should just count the number of items in the source"),
CaseArray(
`
LET users = []
FOR i WHILE UNTIL(LENGTH(users))
COLLECT WITH COUNT INTO numberOfUsers
RETURN numberOfUsers
`, []any{
0,
}, "Should return 0 when there are no items in the source"),
}, vm.WithFunctions(ForWhileHelpers()))
}