Skip to content

Commit 3d80665

Browse files
committed
switch ClusterGroupTuples to record class
1 parent de77cf8 commit 3d80665

4 files changed

Lines changed: 80 additions & 123 deletions

File tree

processing/src/main/java/org/apache/druid/timeline/ClusterGroupTuples.java

Lines changed: 47 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -37,54 +37,29 @@
3737

3838
/**
3939
* Typed clustering tuples carried on {@link DataSegment#getClusterGroups()} for clustered base-table segments. Each
40-
* entry in {@link #getTuples()} is one cluster group's clustering-column values, in the order declared by
41-
* {@link #getClusteringColumns()}. Optionally carries the clustering {@link VirtualColumns} when the segment was
40+
* entry in {@link #tuples()} is one cluster group's clustering-column values, in the order declared by
41+
* {@link #clusteringColumns()}. Optionally carries the clustering {@link VirtualColumns} when the segment was
4242
* clustered on a virtual-column expression, so that matching for things like partial load rules and query time
4343
* segment pruning can make use of this information.
44+
* <p>
45+
* The compact constructor validates {@code clusteringColumns}, interns the virtual columns through
46+
* {@link DataSegment#virtualColumnInterner()}, and canonicalizes every tuple value to its declared
47+
* {@link ColumnType} via {@link #coerceValue} so {@link Object#equals} works across the JSON/programmatic boundary.
4448
*/
45-
public class ClusterGroupTuples
49+
public record ClusterGroupTuples(
50+
@JsonProperty("clusteringColumns") RowSignature clusteringColumns,
51+
@JsonProperty("virtualColumns") @JsonInclude(JsonInclude.Include.NON_EMPTY) VirtualColumns virtualColumns,
52+
@JsonProperty("tuples") List<List<Object>> tuples
53+
)
4654
{
47-
private final RowSignature clusteringColumns;
48-
private final List<List<Object>> tuples;
49-
private final VirtualColumns virtualColumns;
50-
5155
@JsonCreator
52-
public ClusterGroupTuples(
53-
@JsonProperty("clusteringColumns") RowSignature clusteringColumns,
54-
@JsonProperty("tuples") @Nullable List<List<Object>> tuples,
55-
@JsonProperty("virtualColumns") @Nullable VirtualColumns virtualColumns
56-
)
56+
public ClusterGroupTuples
5757
{
5858
if (clusteringColumns == null || clusteringColumns.size() == 0) {
5959
throw InvalidInput.exception("clusteringColumns must not be null or empty");
6060
}
61-
this.clusteringColumns = clusteringColumns;
62-
this.virtualColumns = internVirtualColumns(virtualColumns);
63-
64-
final List<List<Object>> source = tuples == null ? Collections.emptyList() : tuples;
65-
final int numCols = clusteringColumns.size();
66-
final List<List<Object>> coerced = new ArrayList<>(source.size());
67-
for (int t = 0; t < source.size(); t++) {
68-
final List<Object> tuple = source.get(t);
69-
if (tuple == null || tuple.size() != numCols) {
70-
throw InvalidInput.exception(
71-
"tuple[%s] has size [%s] but clusteringColumns size is [%s]",
72-
t,
73-
tuple == null ? "null" : tuple.size(),
74-
numCols
75-
);
76-
}
77-
final Object[] out = new Object[numCols];
78-
for (int i = 0; i < numCols; i++) {
79-
final String name = clusteringColumns.getColumnName(i);
80-
final ColumnType type = clusteringColumns.getColumnType(i).orElseThrow(
81-
() -> InvalidInput.exception("clusteringColumn[%s] has no declared type", name)
82-
);
83-
out[i] = coerceValue(name, type, tuple.get(i));
84-
}
85-
coerced.add(Collections.unmodifiableList(Arrays.asList(out)));
86-
}
87-
this.tuples = Collections.unmodifiableList(coerced);
61+
virtualColumns = internVirtualColumns(virtualColumns);
62+
tuples = canonicalizeTuples(clusteringColumns, tuples);
8863
}
8964

9065
/**
@@ -93,56 +68,7 @@ public ClusterGroupTuples(
9368
*/
9469
public ClusterGroupTuples(RowSignature clusteringColumns, @Nullable List<List<Object>> tuples)
9570
{
96-
this(clusteringColumns, tuples, null);
97-
}
98-
99-
@JsonProperty
100-
public RowSignature getClusteringColumns()
101-
{
102-
return clusteringColumns;
103-
}
104-
105-
@JsonProperty
106-
public List<List<Object>> getTuples()
107-
{
108-
return tuples;
109-
}
110-
111-
@JsonProperty
112-
@JsonInclude(JsonInclude.Include.NON_EMPTY)
113-
public VirtualColumns getVirtualColumns()
114-
{
115-
return virtualColumns;
116-
}
117-
118-
@Override
119-
public boolean equals(Object o)
120-
{
121-
if (this == o) {
122-
return true;
123-
}
124-
if (!(o instanceof ClusterGroupTuples)) {
125-
return false;
126-
}
127-
ClusterGroupTuples that = (ClusterGroupTuples) o;
128-
return Objects.equals(clusteringColumns, that.clusteringColumns)
129-
&& Objects.equals(tuples, that.tuples)
130-
&& Objects.equals(virtualColumns, that.virtualColumns);
131-
}
132-
133-
@Override
134-
public int hashCode()
135-
{
136-
return Objects.hash(clusteringColumns, tuples, virtualColumns);
137-
}
138-
139-
@Override
140-
public String toString()
141-
{
142-
return "ClusterGroupTuples{clusteringColumns=" + clusteringColumns
143-
+ ", tuples=" + tuples
144-
+ ", virtualColumns=" + virtualColumns
145-
+ '}';
71+
this(clusteringColumns, null, tuples);
14672
}
14773

14874
/**
@@ -160,7 +86,7 @@ public String toString()
16086
* <p>
16187
* Used by:
16288
* <ul>
163-
* <li>{@link ClusterGroupTuples}'s constructor to canonicalize segment-side tuples (strict).</li>
89+
* <li>{@link ClusterGroupTuples}'s compact constructor to canonicalize segment-side tuples (strict).</li>
16490
* <li>Operator-supplied rule tuples in future cluster-group partial-load matchers, which can catch the
16591
* exception and treat it as "no match for this segment" rather than a hard failure.</li>
16692
* </ul>
@@ -211,6 +137,37 @@ private static VirtualColumns internVirtualColumns(@Nullable VirtualColumns virt
211137
);
212138
}
213139

140+
private static List<List<Object>> canonicalizeTuples(
141+
RowSignature clusteringColumns,
142+
@Nullable List<List<Object>> tuples
143+
)
144+
{
145+
final List<List<Object>> source = tuples == null ? Collections.emptyList() : tuples;
146+
final int numCols = clusteringColumns.size();
147+
final List<List<Object>> coerced = new ArrayList<>(source.size());
148+
for (int t = 0; t < source.size(); t++) {
149+
final List<Object> tuple = source.get(t);
150+
if (tuple == null || tuple.size() != numCols) {
151+
throw InvalidInput.exception(
152+
"tuple[%s] has size [%s] but clusteringColumns size is [%s]",
153+
t,
154+
tuple == null ? "null" : tuple.size(),
155+
numCols
156+
);
157+
}
158+
final Object[] out = new Object[numCols];
159+
for (int i = 0; i < numCols; i++) {
160+
final String name = clusteringColumns.getColumnName(i);
161+
final ColumnType type = clusteringColumns.getColumnType(i).orElseThrow(
162+
() -> InvalidInput.exception("clusteringColumn[%s] has no declared type", name)
163+
);
164+
out[i] = coerceValue(name, type, tuple.get(i));
165+
}
166+
coerced.add(Collections.unmodifiableList(Arrays.asList(out)));
167+
}
168+
return Collections.unmodifiableList(coerced);
169+
}
170+
214171
private static DruidException cannotCoerce(Object raw, String columnName, String targetType)
215172
{
216173
return InvalidInput.exception(

processing/src/test/java/org/apache/druid/timeline/ClusterGroupTuplesTest.java

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,14 @@ void testConstructorRejectsEmptyClusteringColumns()
9393
void testConstructorAllowsEmptyTuples()
9494
{
9595
final ClusterGroupTuples groups = new ClusterGroupTuples(tenantRegion(), List.of());
96-
Assertions.assertTrue(groups.getTuples().isEmpty());
96+
Assertions.assertTrue(groups.tuples().isEmpty());
9797
}
9898

9999
@Test
100100
void testConstructorAllowsNullTuplesList()
101101
{
102102
final ClusterGroupTuples groups = new ClusterGroupTuples(tenantRegion(), null);
103-
Assertions.assertTrue(groups.getTuples().isEmpty());
103+
Assertions.assertTrue(groups.tuples().isEmpty());
104104
}
105105

106106
@Test
@@ -166,11 +166,11 @@ void testNullsAllowedAtAnyTuplePosition()
166166
Arrays.asList(null, null)
167167
)
168168
);
169-
Assertions.assertEquals(3, groups.getTuples().size());
170-
Assertions.assertNull(groups.getTuples().get(0).get(0));
171-
Assertions.assertNull(groups.getTuples().get(1).get(1));
172-
Assertions.assertNull(groups.getTuples().get(2).get(0));
173-
Assertions.assertNull(groups.getTuples().get(2).get(1));
169+
Assertions.assertEquals(3, groups.tuples().size());
170+
Assertions.assertNull(groups.tuples().get(0).get(0));
171+
Assertions.assertNull(groups.tuples().get(1).get(1));
172+
Assertions.assertNull(groups.tuples().get(2).get(0));
173+
Assertions.assertNull(groups.tuples().get(2).get(1));
174174
}
175175

176176
@Test
@@ -195,8 +195,8 @@ void testCoercionIntegerToLong()
195195
tenantPriority(),
196196
List.of(List.of("acme", Integer.valueOf(5)))
197197
);
198-
Assertions.assertEquals(Long.class, groups.getTuples().get(0).get(1).getClass());
199-
Assertions.assertEquals(5L, groups.getTuples().get(0).get(1));
198+
Assertions.assertEquals(Long.class, groups.tuples().get(0).get(1).getClass());
199+
Assertions.assertEquals(5L, groups.tuples().get(0).get(1));
200200
}
201201

202202
@Test
@@ -219,8 +219,8 @@ void testCoercionDoubleToFloat()
219219
{
220220
final RowSignature sig = RowSignature.builder().add("temp", ColumnType.FLOAT).build();
221221
final ClusterGroupTuples groups = new ClusterGroupTuples(sig, List.of(List.of((Object) Double.valueOf(98.6))));
222-
Assertions.assertEquals(Float.class, groups.getTuples().get(0).get(0).getClass());
223-
Assertions.assertEquals(98.6f, (Float) groups.getTuples().get(0).get(0), 0.0001f);
222+
Assertions.assertEquals(Float.class, groups.tuples().get(0).get(0).getClass());
223+
Assertions.assertEquals(98.6f, (Float) groups.tuples().get(0).get(0), 0.0001f);
224224
}
225225

226226
@Test
@@ -255,7 +255,7 @@ void testCoercionAcceptsAnyTypeForString()
255255
{
256256
final RowSignature sig = RowSignature.builder().add("v", ColumnType.STRING).build();
257257
final ClusterGroupTuples groups = new ClusterGroupTuples(sig, List.of(List.of((Object) Long.valueOf(7))));
258-
Assertions.assertEquals("7", groups.getTuples().get(0).get(0));
258+
Assertions.assertEquals("7", groups.tuples().get(0).get(0));
259259
}
260260

261261
@Test
@@ -270,8 +270,8 @@ void testJsonRoundTripPreservesCoercedTypes() throws Exception
270270
final ClusterGroupTuples back = MAPPER.readValue(json, ClusterGroupTuples.class);
271271
Assertions.assertEquals(groups, back);
272272
// Round-tripped tuples must end up with the same canonical types as the in-memory original.
273-
Assertions.assertEquals(Long.class, back.getTuples().get(0).get(1).getClass());
274-
Assertions.assertEquals(Long.class, back.getTuples().get(1).get(1).getClass());
273+
Assertions.assertEquals(Long.class, back.tuples().get(0).get(1).getClass());
274+
Assertions.assertEquals(Long.class, back.tuples().get(1).get(1).getClass());
275275
}
276276

277277
@Test
@@ -283,39 +283,39 @@ void testTuplesAreImmutable()
283283
);
284284
Assertions.assertThrows(
285285
UnsupportedOperationException.class,
286-
() -> groups.getTuples().add(List.of("globex", "us-east-1"))
286+
() -> groups.tuples().add(List.of("globex", "us-east-1"))
287287
);
288288
Assertions.assertThrows(
289289
UnsupportedOperationException.class,
290-
() -> groups.getTuples().get(0).set(0, "hijacked")
290+
() -> groups.tuples().get(0).set(0, "hijacked")
291291
);
292292
}
293293

294294
@Test
295295
void testVirtualColumnsDefaultEmpty()
296296
{
297297
final ClusterGroupTuples groups = new ClusterGroupTuples(tenantRegion(), List.of());
298-
Assertions.assertSame(VirtualColumns.EMPTY, groups.getVirtualColumns());
298+
Assertions.assertSame(VirtualColumns.EMPTY, groups.virtualColumns());
299299
}
300300

301301
@Test
302302
void testVirtualColumnsAreStored()
303303
{
304304
final ClusterGroupTuples groups = new ClusterGroupTuples(
305305
RowSignature.builder().add("tenant_lower", ColumnType.STRING).build(),
306-
List.of(List.of("acme")),
307-
VIRTUAL_COLUMNS
306+
VIRTUAL_COLUMNS,
307+
List.of(List.of("acme"))
308308
);
309-
Assertions.assertNotNull(groups.getVirtualColumns().getVirtualColumn("tenant_lower"));
309+
Assertions.assertNotNull(groups.virtualColumns().getVirtualColumn("tenant_lower"));
310310
}
311311

312312
@Test
313313
void testVirtualColumnsJsonRoundTrip() throws Exception
314314
{
315315
final ClusterGroupTuples original = new ClusterGroupTuples(
316316
RowSignature.builder().add("tenant_lower", ColumnType.STRING).build(),
317-
List.of(List.of("acme")),
318-
VIRTUAL_COLUMNS
317+
VIRTUAL_COLUMNS,
318+
List.of(List.of("acme"))
319319
);
320320
// Round-trip needs an injectable ExprMacroTable for ExpressionVirtualColumn deserialization.
321321
final ObjectMapper mapper = new DefaultObjectMapper();
@@ -344,17 +344,17 @@ void testVirtualColumnInternerSharesAcrossInstances()
344344
// shared interner on DataSegment, so identical clustering VCs dedupe across segments held in memory.
345345
final ClusterGroupTuples a = new ClusterGroupTuples(
346346
RowSignature.builder().add("tenant_lower", ColumnType.STRING).build(),
347-
List.of(List.of("acme")),
348-
VIRTUAL_COLUMNS
347+
VIRTUAL_COLUMNS,
348+
List.of(List.of("acme"))
349349
);
350350
final ClusterGroupTuples b = new ClusterGroupTuples(
351351
RowSignature.builder().add("tenant_lower", ColumnType.STRING).build(),
352-
List.of(List.of("globex")),
353-
VIRTUAL_COLUMNS
352+
VIRTUAL_COLUMNS,
353+
List.of(List.of("globex"))
354354
);
355355
Assertions.assertSame(
356-
a.getVirtualColumns().getVirtualColumns()[0],
357-
b.getVirtualColumns().getVirtualColumns()[0]
356+
a.virtualColumns().getVirtualColumns()[0],
357+
b.virtualColumns().getVirtualColumns()[0]
358358
);
359359
}
360360
}

server/src/main/java/org/apache/druid/server/coordinator/rules/WildcardClusterGroupPartialLoadMatcher.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
* If the operator supplies {@link #getVirtualColumns()}, a pattern key may also reference one of those virtual
5151
* columns. At match time, the matcher resolves such a key to a clustering column on the segment via
5252
* {@link VirtualColumns#findEquivalent(VirtualColumns.Node)} between the matchers VCs and the segment's clustering
53-
* VCs (carried on {@link ClusterGroupTuples#getVirtualColumns()}). This lets operators author portable rules, they
53+
* VCs (carried on {@link ClusterGroupTuples#virtualColumns()}). This lets operators author portable rules, they
5454
* write their preferred VC name and expression, and the matcher resolves to whatever name the segment happens to use
5555
* for the equivalent clustering VC.
5656
* <p>
@@ -132,9 +132,9 @@ protected List<Integer> resolveClusterGroupIndices(DataSegment segment)
132132
if (clusterGroups == null) {
133133
return Collections.emptyList();
134134
}
135-
final RowSignature clusteringColumns = clusterGroups.getClusteringColumns();
136-
final VirtualColumns segmentVcs = clusterGroups.getVirtualColumns();
137-
final List<List<Object>> tuples = clusterGroups.getTuples();
135+
final RowSignature clusteringColumns = clusterGroups.clusteringColumns();
136+
final VirtualColumns segmentVcs = clusterGroups.virtualColumns();
137+
final List<List<Object>> tuples = clusterGroups.tuples();
138138

139139
// Per-pattern resolution: which clustering column does each pattern key map to? Resolution is segment-scoped
140140
// (depends only on the segment's clustering signature + VCs), so compute it once up front and reuse it across

server/src/test/java/org/apache/druid/server/coordinator/rules/WildcardClusterGroupPartialLoadMatcherTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,6 @@ private static DataSegment vcClusteredSegment(String... lowerTenants)
444444
for (String t : lowerTenants) {
445445
tuples.add(Collections.singletonList(t));
446446
}
447-
return segmentWithGroups(new ClusterGroupTuples(clusteringColumns, tuples, lowerTenantVcs("tenant_lower")));
447+
return segmentWithGroups(new ClusterGroupTuples(clusteringColumns, lowerTenantVcs("tenant_lower"), tuples));
448448
}
449449
}

0 commit comments

Comments
 (0)