From fe18f86e9b46d2efab23b997fa3a88e33dd9fbae Mon Sep 17 00:00:00 2001 From: fl Date: Thu, 25 Jun 2026 09:56:12 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(M6e):=20recursive=20tuple-stream=20dete?= =?UTF-8?q?ction=20=E2=80=94=20focus/index=20under=20sort/predicate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tuple-mode scan and path_is_tuple only checked top-level steps, so a focus (@$v) or index (#$v) step nested inside a sub-path — as a ^(sort) or a [predicate] wraps it — was missed, and the path skipped eval_path_tuple entirely (bindings lost, result null). Make path_is_tuple recurse into nested path steps (mirroring path_keeps_array), use it for the dispatch decision, and route a self-contained nested tuple sub-path through eval_path_tuple(...,true) in the seeding so its bindings survive (the gap M6c left in seeding; the main loop already did this). Fixes joins/employee-map-reduce 7,8 (sort-on-focus) + sorting/case020. Co-Authored-By: Claude Opus 4.8 --- spec/joins_spec.lua | 35 +++++++++++++++++++++++++++++++++++ src/jsonata/evaluator.lua | 22 +++++++++++++--------- 2 files changed, 48 insertions(+), 9 deletions(-) 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/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 From 038f86059067e8c424efde54c927722f6d60e654 Mon Sep 17 00:00:00 2001 From: fl Date: Thu, 25 Jun 2026 09:56:25 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test(M6e):=20regen=20baseline=20=E2=80=94?= =?UTF-8?q?=20sort-on-focus=20joins=20+=20sorting/case020,=20zero=20regres?= =?UTF-8?q?sions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- spec/jsonata-suite/baseline.lua | 3 +++ 1 file changed, 3 insertions(+) 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,