Skip to content

Commit b8d09c5

Browse files
committed
Merge branch 'main' into cli-wrapper
# Conflicts: # pom.xml
2 parents 909da8b + 32f785f commit b8d09c5

43 files changed

Lines changed: 3513 additions & 367 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ jobs:
1414
os: [ ubuntu-latest, windows-latest, macos-latest ]
1515
fail-fast: false
1616
runs-on: ${{ matrix.os }}
17+
services:
18+
s3mock:
19+
# This service will only start if the runner is Linux
20+
image: ${{ matrix.os == 'ubuntu-latest' && 'adobe/s3mock:3.11.0' || '' }}
21+
ports:
22+
- 9090:9090
23+
env:
24+
initialBuckets: zarr-test-bucket
1725
defaults:
1826
run:
1927
shell: bash
@@ -49,8 +57,12 @@ jobs:
4957
- name: Test
5058
env:
5159
MAVEN_OPTS: "-Xmx6g"
52-
run: mvn --no-transfer-progress test -DargLine="-Xmx6g"
53-
60+
run: |
61+
if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then
62+
mvn --no-transfer-progress test -DargLine="-Xmx6g" -DrunS3Tests=true
63+
else
64+
mvn --no-transfer-progress test -DargLine="-Xmx6g"
65+
fi
5466
- name: Assemble JAR
5567
run: mvn package -DskipTests
5668

pom.xml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>dev.zarr</groupId>
88
<artifactId>zarr-java</artifactId>
9-
<version>0.0.9</version>
9+
<version>0.0.10</version>
1010

1111
<name>zarr-java</name>
1212

@@ -121,6 +121,11 @@
121121
<version>4.13.1</version>
122122
<scope>test</scope>
123123
</dependency>
124+
<dependency>
125+
<groupId>org.apache.commons</groupId>
126+
<artifactId>commons-compress</artifactId>
127+
<version>1.28.0</version>
128+
</dependency>
124129

125130
<dependency>
126131
<groupId>info.picocli</groupId>
@@ -145,6 +150,10 @@
145150
<version>3.2.5</version>
146151
<configuration>
147152
<useSystemClassLoader>false</useSystemClassLoader>
153+
<environmentVariables>
154+
<!-- Force Testcontainers to use a newer Docker API version supported by your local Daemon -->
155+
<DOCKER_API_VERSION>1.44</DOCKER_API_VERSION>
156+
</environmentVariables>
148157
</configuration>
149158
</plugin>
150159
<plugin>

src/main/java/dev/zarr/zarrjava/core/Array.java

Lines changed: 171 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
public abstract class Array extends AbstractNode {
2222

2323
protected CodecPipeline codecPipeline;
24+
public static final boolean DEFAULT_PARALLELISM = true;
2425

2526
protected Array(StoreHandle storeHandle) throws ZarrException {
2627
super(storeHandle);
@@ -88,7 +89,7 @@ public void write(long[] offset, ucar.ma2.Array array, boolean parallel) {
8889
throw new IllegalArgumentException("'array' needs to have rank '" + metadata.ndim() + "'.");
8990
}
9091

91-
int[] shape = array.getShape();
92+
long[] shape = Utils.toLongArray(array.getShape());
9293

9394
final int[] chunkShape = metadata.chunkShape();
9495
Stream<long[]> chunkStream = Arrays.stream(IndexingUtils.computeChunkCoords(metadata.shape, chunkShape, offset, shape));
@@ -118,8 +119,14 @@ public void write(long[] offset, ucar.ma2.Array array, boolean parallel) {
118119
);
119120
}
120121
writeChunk(chunkCoords, chunkArray);
121-
} catch (ZarrException | InvalidRangeException e) {
122-
throw new RuntimeException(e);
122+
} catch (ZarrException e) {
123+
throw new RuntimeException(
124+
"Failed to write chunk at coordinates " + Arrays.toString(chunkCoords) +
125+
": " + e.getMessage(), e);
126+
} catch (InvalidRangeException e) {
127+
throw new RuntimeException(
128+
"Invalid array range when writing chunk at coordinates " + Arrays.toString(chunkCoords) +
129+
": " + e.getMessage(), e);
123130
}
124131
});
125132

@@ -174,6 +181,110 @@ public ucar.ma2.Array readChunk(long[] chunkCoords) throws ZarrException {
174181
return codecPipeline.decode(chunkBytes);
175182
}
176183

184+
/**
185+
* Deletes chunks that are completely outside the new shape and trims boundary chunks.
186+
*
187+
* @param newShape the new shape of the array
188+
* @param parallel utilizes parallelism if true
189+
*/
190+
protected void cleanupChunksForResize(long[] newShape, boolean parallel) {
191+
ArrayMetadata metadata = metadata();
192+
final int[] chunkShape = metadata.chunkShape();
193+
final int ndim = metadata.ndim();
194+
final dev.zarr.zarrjava.core.chunkkeyencoding.ChunkKeyEncoding chunkKeyEncoding = metadata.chunkKeyEncoding();
195+
196+
// Calculate max valid chunk coordinates for the new shape
197+
long[] newMaxChunkCoords = new long[ndim];
198+
for (int i = 0; i < ndim; i++) {
199+
newMaxChunkCoords[i] = (newShape[i] + chunkShape[i] - 1) / chunkShape[i];
200+
}
201+
202+
// Iterate over all possible chunk coordinates in the old shape
203+
long[][] allOldChunkCoords = IndexingUtils.computeChunkCoords(metadata.shape, chunkShape);
204+
205+
Stream<long[]> chunkStream = Arrays.stream(allOldChunkCoords);
206+
if (parallel) {
207+
chunkStream = chunkStream.parallel();
208+
}
209+
210+
chunkStream.forEach(chunkCoords -> {
211+
boolean isOutsideBounds = false;
212+
boolean isOnBoundary = false;
213+
214+
for (int dimIdx = 0; dimIdx < ndim; dimIdx++) {
215+
if (chunkCoords[dimIdx] >= newMaxChunkCoords[dimIdx]) {
216+
isOutsideBounds = true;
217+
break;
218+
}
219+
// Check if this chunk is on the boundary (partially outside new shape)
220+
long chunkEnd = (chunkCoords[dimIdx] + 1) * chunkShape[dimIdx];
221+
if (chunkEnd > newShape[dimIdx]) {
222+
isOnBoundary = true;
223+
}
224+
}
225+
226+
String[] chunkKeys = chunkKeyEncoding.encodeChunkKey(chunkCoords);
227+
StoreHandle chunkHandle = storeHandle.resolve(chunkKeys);
228+
229+
if (isOutsideBounds) {
230+
// Delete chunk that is completely outside
231+
chunkHandle.delete();
232+
} else if (isOnBoundary) {
233+
// Trim boundary chunk - read, clear out-of-bounds data, write back
234+
try {
235+
trimBoundaryChunk(chunkCoords, newShape, chunkShape);
236+
} catch (ZarrException e) {
237+
throw new RuntimeException(e);
238+
}
239+
}
240+
});
241+
}
242+
243+
/**
244+
* Trims a boundary chunk by reading it, clearing the out-of-bounds portion, and writing it back.
245+
*
246+
* @param chunkCoords the coordinates of the chunk to trim
247+
* @param newShape the new shape of the array
248+
* @param chunkShape the shape of the chunks
249+
* @throws ZarrException if reading or writing the chunk fails
250+
*/
251+
protected void trimBoundaryChunk(long[] chunkCoords, long[] newShape, int[] chunkShape) throws ZarrException {
252+
ArrayMetadata metadata = metadata();
253+
final int ndim = metadata.ndim();
254+
255+
// Calculate the valid region within this chunk
256+
int[] validShape = new int[ndim];
257+
boolean needsTrimming = false;
258+
for (int dimIdx = 0; dimIdx < ndim; dimIdx++) {
259+
long chunkStart = chunkCoords[dimIdx] * chunkShape[dimIdx];
260+
long chunkEnd = chunkStart + chunkShape[dimIdx];
261+
if (chunkEnd > newShape[dimIdx]) {
262+
validShape[dimIdx] = (int) (newShape[dimIdx] - chunkStart);
263+
needsTrimming = true;
264+
} else {
265+
validShape[dimIdx] = chunkShape[dimIdx];
266+
}
267+
}
268+
269+
if (!needsTrimming) {
270+
return;
271+
}
272+
273+
// Read the existing chunk
274+
ucar.ma2.Array chunkData = readChunk(chunkCoords);
275+
276+
// Create a new chunk filled with fill value
277+
ucar.ma2.Array newChunkData = metadata.allocateFillValueChunk();
278+
279+
// Copy only the valid region
280+
MultiArrayUtils.copyRegion(
281+
chunkData, new int[ndim], newChunkData, new int[ndim], validShape
282+
);
283+
284+
// Write the trimmed chunk back
285+
writeChunk(chunkCoords, newChunkData);
286+
}
287+
177288

178289
/**
179290
* Writes a ucar.ma2.Array into the Zarr array at the beginning of the Zarr array. The shape of
@@ -195,7 +306,7 @@ public void write(ucar.ma2.Array array) {
195306
* @param array the data to write
196307
*/
197308
public void write(long[] offset, ucar.ma2.Array array) {
198-
write(offset, array, false);
309+
write(offset, array, DEFAULT_PARALLELISM);
199310
}
200311

201312
/**
@@ -217,7 +328,7 @@ public void write(ucar.ma2.Array array, boolean parallel) {
217328
*/
218329
@Nonnull
219330
public ucar.ma2.Array read() throws ZarrException {
220-
return read(new long[metadata().ndim()], Utils.toIntArray(metadata().shape));
331+
return read(new long[metadata().ndim()], metadata().shape);
221332
}
222333

223334
/**
@@ -229,8 +340,8 @@ public ucar.ma2.Array read() throws ZarrException {
229340
* @throws ZarrException throws ZarrException if the requested data is outside the array's domain or if the read fails
230341
*/
231342
@Nonnull
232-
public ucar.ma2.Array read(final long[] offset, final int[] shape) throws ZarrException {
233-
return read(offset, shape, false);
343+
public ucar.ma2.Array read(final long[] offset, final long[] shape) throws ZarrException {
344+
return read(offset, shape, DEFAULT_PARALLELISM);
234345
}
235346

236347
/**
@@ -241,7 +352,7 @@ public ucar.ma2.Array read(final long[] offset, final int[] shape) throws ZarrEx
241352
*/
242353
@Nonnull
243354
public ucar.ma2.Array read(final boolean parallel) throws ZarrException {
244-
return read(new long[metadata().ndim()], Utils.toIntArray(metadata().shape), parallel);
355+
return read(new long[metadata().ndim()], metadata().shape, parallel);
245356
}
246357

247358
boolean chunkIsInArray(long[] chunkCoords) {
@@ -264,7 +375,7 @@ boolean chunkIsInArray(long[] chunkCoords) {
264375
* @throws ZarrException throws ZarrException if the requested data is outside the array's domain or if the read fails
265376
*/
266377
@Nonnull
267-
public ucar.ma2.Array read(final long[] offset, final int[] shape, final boolean parallel) throws ZarrException {
378+
public ucar.ma2.Array read(final long[] offset, final long[] shape, final boolean parallel) throws ZarrException {
268379
ArrayMetadata metadata = metadata();
269380
if (offset.length != metadata.ndim()) {
270381
throw new IllegalArgumentException("'offset' needs to have rank '" + metadata.ndim() + "'.");
@@ -284,7 +395,7 @@ public ucar.ma2.Array read(final long[] offset, final int[] shape, final boolean
284395
}
285396

286397
final ucar.ma2.Array outputArray = ucar.ma2.Array.factory(metadata.dataType().getMA2DataType(),
287-
shape);
398+
Utils.toIntArray(shape));
288399
Stream<long[]> chunkStream = Arrays.stream(IndexingUtils.computeChunkCoords(metadata.shape, chunkShape, offset, shape));
289400
if (parallel) {
290401
chunkStream = chunkStream.parallel();
@@ -306,19 +417,20 @@ public ucar.ma2.Array read(final long[] offset, final int[] shape, final boolean
306417

307418
final String[] chunkKeys = metadata.chunkKeyEncoding().encodeChunkKey(chunkCoords);
308419
final StoreHandle chunkHandle = storeHandle.resolve(chunkKeys);
309-
if (!chunkHandle.exists()) {
310-
return;
311-
}
420+
312421
if (codecPipeline.supportsPartialDecode()) {
313422
final ucar.ma2.Array chunkArray = codecPipeline.decodePartial(chunkHandle,
314423
Utils.toLongArray(chunkProjection.chunkOffset), chunkProjection.shape);
315424
MultiArrayUtils.copyRegion(chunkArray, new int[metadata.ndim()], outputArray,
316425
chunkProjection.outOffset, chunkProjection.shape
317426
);
318427
} else {
319-
MultiArrayUtils.copyRegion(readChunk(chunkCoords), chunkProjection.chunkOffset,
320-
outputArray, chunkProjection.outOffset, chunkProjection.shape
321-
);
428+
ByteBuffer chunkBytes = chunkHandle.read();
429+
if (chunkBytes != null) {
430+
MultiArrayUtils.copyRegion(codecPipeline.decode(chunkBytes), chunkProjection.chunkOffset,
431+
outputArray, chunkProjection.outOffset, chunkProjection.shape
432+
);
433+
}
322434
}
323435

324436
} catch (ZarrException e) {
@@ -328,6 +440,46 @@ public ucar.ma2.Array read(final long[] offset, final int[] shape, final boolean
328440
return outputArray;
329441
}
330442

443+
/**
444+
* Sets a new shape for the Zarr array. Only the metadata is updated by default.
445+
* This method returns a new instance of the Zarr array class and the old instance
446+
* becomes invalid.
447+
*
448+
* @param newShape the new shape of the Zarr array
449+
* @throws ZarrException if the new metadata is invalid
450+
* @throws IOException throws IOException if the new metadata cannot be serialized
451+
*/
452+
public Array resize(long[] newShape) throws ZarrException, IOException {
453+
return resize(newShape, true);
454+
}
455+
456+
/**
457+
* Sets a new shape for the Zarr array. This method returns a new instance of the Zarr array class
458+
* and the old instance becomes invalid.
459+
*
460+
* @param newShape the new shape of the Zarr array
461+
* @param resizeMetadataOnly if true, only the metadata is updated; if false, chunks outside the new
462+
* bounds are deleted and boundary chunks are trimmed
463+
* @throws ZarrException if the new metadata is invalid
464+
* @throws IOException throws IOException if the new metadata cannot be serialized
465+
*/
466+
public Array resize(long[] newShape, boolean resizeMetadataOnly) throws ZarrException, IOException {
467+
return resize(newShape, resizeMetadataOnly, DEFAULT_PARALLELISM);
468+
}
469+
470+
/**
471+
* Sets a new shape for the Zarr array. This method returns a new instance of the Zarr array class
472+
* and the old instance becomes invalid.
473+
*
474+
* @param newShape the new shape of the Zarr array
475+
* @param resizeMetadataOnly if true, only the metadata is updated; if false, chunks outside the new
476+
* bounds are deleted and boundary chunks are trimmed
477+
* @param parallel utilizes parallelism if true when cleaning up chunks
478+
* @throws ZarrException if the new metadata is invalid
479+
* @throws IOException throws IOException if the new metadata cannot be serialized
480+
*/
481+
public abstract Array resize(long[] newShape, boolean resizeMetadataOnly, boolean parallel) throws ZarrException, IOException;
482+
331483
public ArrayAccessor access() {
332484
return new ArrayAccessor(this);
333485
}
@@ -336,7 +488,7 @@ public static final class ArrayAccessor {
336488
@Nullable
337489
long[] offset;
338490
@Nullable
339-
int[] shape;
491+
long[] shape;
340492
@Nonnull
341493
Array array;
342494

@@ -353,13 +505,13 @@ public ArrayAccessor withOffset(@Nonnull long... offset) {
353505

354506
@Nonnull
355507
public ArrayAccessor withShape(@Nonnull int... shape) {
356-
this.shape = shape;
508+
this.shape = Utils.toLongArray(shape);
357509
return this;
358510
}
359511

360512
@Nonnull
361513
public ArrayAccessor withShape(@Nonnull long... shape) {
362-
this.shape = Utils.toIntArray(shape);
514+
this.shape = shape;
363515
return this;
364516
}
365517

src/main/java/dev/zarr/zarrjava/core/Group.java

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import java.io.IOException;
1010
import java.nio.file.Path;
1111
import java.nio.file.Paths;
12-
import java.util.Objects;
1312
import java.util.stream.Stream;
1413

1514
public abstract class Group extends AbstractNode {
@@ -64,20 +63,15 @@ public static Group open(String path) throws IOException, ZarrException {
6463
}
6564

6665
@Nullable
67-
public abstract Node get(String key) throws ZarrException, IOException;
66+
public abstract Node get(String[] key) throws ZarrException, IOException;
6867

69-
public Stream<Node> list() {
70-
return storeHandle.list()
71-
.map(key -> {
72-
try {
73-
return get(key);
74-
} catch (Exception e) {
75-
throw new RuntimeException(e);
76-
}
77-
})
78-
.filter(Objects::nonNull);
68+
@Nullable
69+
public Node get(String key) throws ZarrException, IOException {
70+
return get(new String[]{key});
7971
}
8072

73+
public abstract Stream<Node> list();
74+
8175
public Node[] listAsArray() {
8276
try (Stream<Node> nodeStream = list()) {
8377
return nodeStream.toArray(Node[]::new);

0 commit comments

Comments
 (0)