Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/scratch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ ice insert nyc.taxis_p_by_day -p \
ice delete nyc.taxis_p_by_day \
--partition '[{"name": "tpep_pickup_datetime", "values": ["2024-12-31T23:51:20"]}]' --dry-run=false

# delete a range of partitions using a comparison operator (default op is equals)
ice delete nyc.taxis_p_by_day \
--partition '[{"name": "tpep_pickup_datetime", "op": "greater_than_or_equal", "values": ["2024-12-31T23:51:20"]}]' --dry-run=false

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value of "name" should be "tpep_pickup_datetime_day" instead of "tpep_pickup_datetime" for both delete calls. The logic was changed in an old PR (#94) and these docs weren't updated.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated.


# insert data ordered by tpep_pickup_datetime column
ice insert nyc.taxis_s_by_day -p \
https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2025-01.parquet \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ if [[ "$list_after" == *"${TRANSFORMED_PARTITION_COLUMN}=${DELETE_PARTITION_DAY}
fi
echo "OK Validated partition was deleted"

# Exercise a comparison operator (greater_than_or_equal) via a dry run.
# >= 2000-01-01 matches all remaining data, so candidate files must be found.
range_output=$({{ICE_CLI}} --config {{CLI_CONFIG}} delete ${TABLE_NAME} \
--partition "[{\"name\": \"${TRANSFORMED_PARTITION_COLUMN}\", \"op\": \"greater_than_or_equal\", \"values\": [\"2000-01-01\"]}]" 2>&1)
echo "$range_output"
if ! echo "$range_output" | grep -qF "To be deleted"; then
echo "FAIL Expected range (>= 2000-01-01) dry-run to find candidate files"
exit 1
fi
echo "OK Range operator (greater_than_or_equal) matched files"

# Cleanup
{{ICE_CLI}} --config {{CLI_CONFIG}} delete-table ${TABLE_NAME}
echo "OK Deleted table: ${TABLE_NAME}"
Expand Down
21 changes: 21 additions & 0 deletions ice/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,29 @@ ice delete nyc.taxis_p_by_day \
ice delete nyc.taxis_p_by_day \
--partition '[{"name": "tpep_pickup_datetime_day", "values": ["2024-12-31"]}]' \
--dry-run=false

# actually delete and physically purge the data files from storage
ice delete nyc.taxis_p_by_day \
--partition '[{"name": "tpep_pickup_datetime_day", "values": ["2024-12-31"]}]' \
--dry-run=false --purge

# delete a range of partitions using a comparison operator
ice delete nyc.taxis_p_by_day \
--partition '[{"name": "tpep_pickup_datetime_day", "op": "greater_than_or_equal", "values": ["2024-12-31"]}]' \
--dry-run=false
```

Each partition filter accepts an optional `op` field (default `equals`): one of
`equals`, `less_than`, `greater_than`, `less_than_or_equal`, `greater_than_or_equal`.
Values within a filter are OR'd together and filters across fields are AND'd, so a
range delete typically uses a single value.

Without `--purge`, `ice delete` only de-references the data files (a new snapshot is
created); the physical files remain until catalog maintenance reclaims them via
`SNAPSHOT_CLEANUP` / `ORPHAN_CLEANUP`. With `--purge`, the matched data files are
deleted from storage immediately, which breaks time-travel to snapshots that
referenced them.

### Insert Without Copy

Insert an S3 file by reference (no data copy) when it's already in the warehouse:
Expand Down
45 changes: 43 additions & 2 deletions ice/src/main/java/com/altinity/ice/cli/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
import com.altinity.ice.internal.jetty.DebugServer;
import com.altinity.ice.internal.picocli.VersionProvider;
import com.altinity.ice.internal.strings.Strings;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
Expand Down Expand Up @@ -964,7 +966,10 @@ void delete(
names = {"--partition"},
description =
"JSON array of partition filters: [{\"name\": \"vendorId\", \"values\": [5, 6]}]. "
+ "For timestamp columns, use ISO Datetime format YYYY-MM-ddTHH:mm:ss")
+ "For timestamp columns, use ISO Datetime format YYYY-MM-ddTHH:mm:ss. "
+ "Each filter may set an optional \"op\": one of equals (default), less_than, "
+ "greater_than, less_than_or_equal, greater_than_or_equal, e.g. "
+ "[{\"name\": \"vendorId\", \"op\": \"greater_than_or_equal\", \"values\": [5]}]")
String partitionJson,
@CommandLine.Option(
names = "--dry-run",
Expand Down Expand Up @@ -992,7 +997,43 @@ void delete(
}

public record PartitionFilter(
@JsonProperty("name") String name, @JsonProperty("values") List<Object> values) {}
@JsonProperty("name") String name,
@JsonProperty("values") List<Object> values,
@JsonProperty("op") Op op) {

public PartitionFilter {
op = op == null ? Op.EQUALS : op;
}

public enum Op {
EQUALS("equals"),
LESS_THAN("less_than"),
GREATER_THAN("greater_than"),
LESS_THAN_OR_EQUAL("less_than_or_equal"),
GREATER_THAN_OR_EQUAL("greater_than_or_equal");

private final String json;

Op(String json) {
this.json = json;
}

@JsonValue
public String json() {
return json;
}

@JsonCreator
public static Op fromJson(String v) {
for (Op op : values()) {
if (op.json.equalsIgnoreCase(v) || op.name().equalsIgnoreCase(v)) {
return op;
}
}
throw new IllegalArgumentException("unknown partition filter op: " + v);
}
}
}

private RESTCatalog loadCatalog() throws IOException {
return loadCatalog(this.configFile());
Expand Down
12 changes: 11 additions & 1 deletion ice/src/main/java/com/altinity/ice/cli/internal/cmd/Delete.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public static void run(
value = transformed;
}

Expression singleValueExpr = Expressions.equal(fieldName, value);
Expression singleValueExpr = toPredicate(fieldName, value, pf.op());
fieldExpr =
fieldExpr == null ? singleValueExpr : Expressions.or(fieldExpr, singleValueExpr);
}
Expand Down Expand Up @@ -112,4 +112,14 @@ public static void run(
logger.info("No files to delete");
}
}

static Expression toPredicate(String fieldName, Object value, PartitionFilter.Op op) {
return switch (op) {
case EQUALS -> Expressions.equal(fieldName, value);
case LESS_THAN -> Expressions.lessThan(fieldName, value);
case GREATER_THAN -> Expressions.greaterThan(fieldName, value);
case LESS_THAN_OR_EQUAL -> Expressions.lessThanOrEqual(fieldName, value);
case GREATER_THAN_OR_EQUAL -> Expressions.greaterThanOrEqual(fieldName, value);
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (c) 2025 Altinity Inc and/or its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package com.altinity.ice.cli.internal.cmd;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.altinity.ice.cli.Main.PartitionFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import org.apache.iceberg.expressions.Expression;
import org.apache.iceberg.expressions.UnboundPredicate;
import org.testng.annotations.Test;

public class DeleteTest {

private final ObjectMapper mapper = new ObjectMapper();

@Test
public void testToPredicateOperations() {
assertThat(((UnboundPredicate<?>) Delete.toPredicate("f", 5, PartitionFilter.Op.EQUALS)).op())
.isEqualTo(Expression.Operation.EQ);
assertThat(
((UnboundPredicate<?>) Delete.toPredicate("f", 5, PartitionFilter.Op.LESS_THAN)).op())
.isEqualTo(Expression.Operation.LT);
assertThat(
((UnboundPredicate<?>) Delete.toPredicate("f", 5, PartitionFilter.Op.GREATER_THAN))
.op())
.isEqualTo(Expression.Operation.GT);
assertThat(
((UnboundPredicate<?>)
Delete.toPredicate("f", 5, PartitionFilter.Op.LESS_THAN_OR_EQUAL))
.op())
.isEqualTo(Expression.Operation.LT_EQ);
assertThat(
((UnboundPredicate<?>)
Delete.toPredicate("f", 5, PartitionFilter.Op.GREATER_THAN_OR_EQUAL))
.op())
.isEqualTo(Expression.Operation.GT_EQ);
}

@Test
public void testOpDefaultsToEquals() {
PartitionFilter pf = new PartitionFilter("f", List.of(1), null);
assertThat(pf.op()).isEqualTo(PartitionFilter.Op.EQUALS);
}

@Test
public void testDeserializeWithoutOpDefaultsToEquals() throws Exception {
PartitionFilter pf =
mapper.readValue("{\"name\": \"f\", \"values\": [1]}", PartitionFilter.class);
assertThat(pf.name()).isEqualTo("f");
assertThat(pf.op()).isEqualTo(PartitionFilter.Op.EQUALS);
}

@Test
public void testDeserializeWithOp() throws Exception {
PartitionFilter pf =
mapper.readValue(
"{\"name\": \"f\", \"op\": \"greater_than_or_equal\", \"values\": [1]}",
PartitionFilter.class);
assertThat(pf.op()).isEqualTo(PartitionFilter.Op.GREATER_THAN_OR_EQUAL);
}

@Test
public void testDeserializeUnknownOpThrows() {
assertThatThrownBy(
() ->
mapper.readValue(
"{\"name\": \"f\", \"op\": \"between\", \"values\": [1]}",
PartitionFilter.class))
.hasMessageContaining("unknown partition filter op");
}
}
Loading