Skip to content

Commit 206cc01

Browse files
pditommasoclaude
andauthored
Fix groupTuple operator to handle GString vs String keys consistently (#6400)
The groupTuple operator was not properly grouping items when the same logical key was represented as both GString and String objects. This occurred when channel values contained interpolated strings (GString) that were later mixed with regular String literals having the same content. The root cause was that GString and String objects with identical content have different hash codes and equality behavior, causing them to be treated as separate grouping keys. This commit adds key normalization in the groupTuple operator that converts GString objects to String while preserving all other data types unchanged. Changes: - Add normalizeKey() method to convert GString elements to String - Modify collect() method to normalize keys before grouping - Add comprehensive unit tests covering GString/String scenarios - Preserve existing behavior for all non-GString key types Fixes #6399 🤖 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5bc9162 commit 206cc01

2 files changed

Lines changed: 57 additions & 2 deletions

File tree

modules/nextflow/src/main/groovy/nextflow/extension/GroupTupleOp.groovy

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ class GroupTupleOp {
9191
*/
9292
private void collect(List tuple) {
9393

94-
final key = tuple[indices] // the actual grouping key
94+
final key = normalizeKey(tuple[indices]) // the actual grouping key
9595
final len = tuple.size()
9696

9797
final List items = groups.getOrCreate(key) { // get the group for the specified key
@@ -126,7 +126,9 @@ class GroupTupleOp {
126126
* finalize the grouping binding the remaining values
127127
*/
128128
private void finalise(nop) {
129-
groups.each { keys, items -> bindTuple(items, size ?: sizeBy(keys)) }
129+
for( Map.Entry<List,List> entry : groups.entrySet() ) {
130+
bindTuple(entry.value, size ?: sizeBy(entry.key))
131+
}
130132
target.bind(Channel.STOP)
131133
}
132134

@@ -254,4 +256,19 @@ class GroupTupleOp {
254256
return 0
255257
}
256258

259+
/**
260+
* Normalize key by converting GString objects to String to ensure consistent grouping
261+
*
262+
* @param key The key object or list of key objects to normalize
263+
* @return The normalized key with GString objects converted to String
264+
*/
265+
static protected List normalizeKey(List keyList) {
266+
final result = new ArrayList(keyList.size())
267+
for( int i=0; i<keyList.size(); i++ ) {
268+
final k = keyList[i]
269+
result[i] = k instanceof GString ? k.toString() : k
270+
}
271+
return result
272+
}
273+
257274
}

modules/nextflow/src/test/groovy/nextflow/extension/GroupTupleOpTest.groovy

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,42 @@ class GroupTupleOpTest extends Specification {
140140
result.val == [k1, ['f']]
141141
result.val == Channel.STOP
142142
}
143+
144+
def 'should handle GString vs String keys correctly' () {
145+
given:
146+
def sampleName = "sample1"
147+
def gstringKey = "${sampleName}" // GStringImpl
148+
def stringKey = "sample1" // String
149+
150+
def tuples = [
151+
[gstringKey, 'file1.txt'],
152+
[stringKey, 'file2.txt'],
153+
[gstringKey, 'file3.txt']
154+
]
155+
156+
when:
157+
def result = tuples.channel().groupTuple()
158+
then:
159+
// Without fix, this would create separate groups for GString and String keys
160+
// With fix, they should be grouped together
161+
result.val == [gstringKey, ['file1.txt', 'file2.txt', 'file3.txt']]
162+
result.val == Channel.STOP
163+
}
164+
165+
def 'should preserve non-string key types' () {
166+
given:
167+
def tuples = [
168+
[1, 'file1.txt'], // Integer key
169+
[new File('/tmp'), 'file2.txt'], // File key
170+
[[a: 1], 'file3.txt'] // Map key
171+
]
172+
173+
when:
174+
def result = tuples.channel().groupTuple()
175+
then:
176+
result.val == [1, ['file1.txt']]
177+
result.val == [new File('/tmp'), ['file2.txt']]
178+
result.val == [[a: 1], ['file3.txt']]
179+
result.val == Channel.STOP
180+
}
143181
}

0 commit comments

Comments
 (0)