diff --git a/spec/joins_spec.lua b/spec/joins_spec.lua index 1f53dc4..9698148 100644 --- a/spec/joins_spec.lua +++ b/spec/joins_spec.lua @@ -199,3 +199,38 @@ describe("M6d: tuple-stream group-by (the reduce half)", function() assert.are.equal("D1009", err.code) end) end) + +describe("M6e: nested tuple-stream detection (focus under sort/predicate)", function() + local EMP = dataset("employees") + + -- ^(sort) nests the focus step into a sub-path; predicate nests Contact@$c. + -- The path's tuple steps are all nested, so detection must recurse. + it("sort-on-focus-step join: ^($e.Surname) (employee-map-reduce case7)", function() + assert.are.same({ + { name = "Cruse", phone = { "3146458343", "315 782 9279" } }, + { name = "Jones", phone = "0280 564 6543" }, + { name = "Jones", phone = "0280 864 8643" }, + { name = "Jones", phone = "07735 853535" }, + { name = "Smith", phone = { "0203 544 1234", "01962 001234", "077 7700 1234" } }, + }, run("Employee@$e^($e.Surname).Contact@$c[$e.SSN=$c.ssn].{ 'name': $e.Surname, 'phone': $c.Phone.number }", EMP)) + end) + + it("sort-on-focus-step join: ^($e.FirstName) (employee-map-reduce case8)", function() + assert.are.same({ + { name = "Cruse", phone = { "3146458343", "315 782 9279" } }, + { name = "Smith", phone = { "0203 544 1234", "01962 001234", "077 7700 1234" } }, + { name = "Jones", phone = "0280 564 6543" }, + { name = "Jones", phone = "0280 864 8643" }, + { name = "Jones", phone = "07735 853535" }, + }, run("Employee@$e^($e.FirstName).Contact@$c[$e.SSN=$c.ssn].{ 'name': $e.Surname, 'phone': $c.Phone.number }", EMP)) + end) + + it("index then sort then map carries $o (sorting case020)", function() + assert.are.same({ + { Product = "Cloak", ["Order Index"] = 1 }, + { Product = "Trilby hat", ["Order Index"] = 0 }, + { Product = "Bowler Hat", ["Order Index"] = 0 }, + { Product = "Bowler Hat", ["Order Index"] = 1 }, + }, run("Account.Order#$o.Product^(ProductID).{ 'Product': `Product Name`, 'Order Index': $o }", dataset("dataset5"))) + end) +end) diff --git a/spec/jsonata-suite/baseline.lua b/spec/jsonata-suite/baseline.lua index 5c54f76..3a0bb41 100644 --- a/spec/jsonata-suite/baseline.lua +++ b/spec/jsonata-suite/baseline.lua @@ -829,6 +829,8 @@ return { ["joins/employee-map-reduce/4"] = true, ["joins/employee-map-reduce/5"] = true, ["joins/employee-map-reduce/6"] = true, + ["joins/employee-map-reduce/7"] = true, + ["joins/employee-map-reduce/8"] = true, ["joins/employee-map-reduce/9"] = true, ["joins/errors/0"] = true, ["joins/errors/1"] = true, @@ -1089,6 +1091,7 @@ return { ["sorting/case017"] = true, ["sorting/case018"] = true, ["sorting/case019"] = true, + ["sorting/case020"] = true, ["string-concat/case000"] = true, ["string-concat/case001"] = true, ["string-concat/case002"] = true, diff --git a/src/jsonata/evaluator.lua b/src/jsonata/evaluator.lua index a607363..3cfff44 100644 --- a/src/jsonata/evaluator.lua +++ b/src/jsonata/evaluator.lua @@ -604,6 +604,9 @@ local function path_is_tuple(node) if s.tuple then return true end + if s.steps and path_is_tuple(s) then + return true + end end return false end @@ -617,7 +620,15 @@ local function eval_path_tuple(node, input, env, want_tuples) local tuples local start = 1 if step_is_self_contained(steps) then - local var_val = evaluate(steps[1], input, env) + -- a self-contained step-1 that is itself a tuple sub-path (e.g. a focus step + -- the parser nested under a sort) must yield its tuple stream so its bindings + -- survive — mirroring the per-step nested handling in the main loop below. + local var_val + if path_is_tuple(steps[1]) then + var_val = eval_path_tuple(steps[1], input, env, true) + else + var_val = evaluate(steps[1], input, env) + end if V.is_sequence(var_val) and V.get_flag(var_val, "tuple_stream") then tuples = {} for i = 1, #var_val do @@ -865,14 +876,7 @@ local function _evaluate(node, input, env) return V.obj_get(input, node.value) elseif t == "path" then local keep = path_keeps_array(node) - local tuple_mode = false - for _, s in ipairs(node.steps) do - if s.tuple then - tuple_mode = true - break - end - end - if tuple_mode then + if path_is_tuple(node) then local seq = eval_path_tuple(node, input, env) if node.tuple then return seq -- tuple stream for an enclosing tuple step; no unwrap