diff --git a/maven-projects/io-api/src/main/java/org/apache/graphar/io/ComparisonOperator.java b/maven-projects/io-api/src/main/java/org/apache/graphar/io/ComparisonOperator.java new file mode 100644 index 000000000..1c12c9a83 --- /dev/null +++ b/maven-projects/io-api/src/main/java/org/apache/graphar/io/ComparisonOperator.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.graphar.io; + +/** The simple comparison operations supported by an IO filter hint. */ +public enum ComparisonOperator { + EQUAL, + NOT_EQUAL, + LESS_THAN, + LESS_THAN_OR_EQUAL, + GREATER_THAN, + GREATER_THAN_OR_EQUAL, + IS_NULL, + IS_NOT_NULL +} diff --git a/maven-projects/io-api/src/main/java/org/apache/graphar/io/Filter.java b/maven-projects/io-api/src/main/java/org/apache/graphar/io/Filter.java new file mode 100644 index 000000000..bcf4e5c37 --- /dev/null +++ b/maven-projects/io-api/src/main/java/org/apache/graphar/io/Filter.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.graphar.io; + +import java.util.Objects; + +/** + * A single inspectable table-filter hint; request filters are combined with logical AND. A physical + * reader must validate a comparison literal against the column's {@link ColumnType}: BOOLEAN uses + * Boolean; integer and floating kinds use their matching boxed Java types; STRING uses String; DATE + * uses LocalDate; and TIMESTAMP_MILLIS uses millisecond-precise Instant. Other types cannot be + * compared by this contract. + * + *

Comparison operands must have the same declared type; a type mismatch is invalid rather than a + * coercion. Null values never match a comparison, including NOT_EQUAL; use IS_NULL or IS_NOT_NULL + * for null tests. Ordered STRING comparisons use {@link String#compareTo(String)}, and DATE and + * TIMESTAMP_MILLIS use their natural ordering. Readers must reject an invalid comparison before + * returning a result, whether the filter is pushed down or evaluated as fallback. + */ +public final class Filter { + private final String column; + private final ComparisonOperator operator; + private final Literal value; + + private Filter(String column, ComparisonOperator operator, Literal value) { + if (column == null || column.isBlank()) { + throw new IllegalArgumentException("A filter column cannot be blank."); + } + this.column = column; + this.operator = Objects.requireNonNull(operator, "Filter operator cannot be null."); + if ((operator == ComparisonOperator.IS_NULL || operator == ComparisonOperator.IS_NOT_NULL) + && value != null) { + throw new IllegalArgumentException(operator + " does not accept a comparison value."); + } + if (operator != ComparisonOperator.IS_NULL + && operator != ComparisonOperator.IS_NOT_NULL + && value == null) { + throw new IllegalArgumentException(operator + " requires a non-null comparison value."); + } + this.value = value; + } + + /** Creates a filter with an immutable scalar comparison value. */ + public static Filter comparison(String column, ComparisonOperator operator, Literal value) { + if (operator == ComparisonOperator.IS_NULL || operator == ComparisonOperator.IS_NOT_NULL) { + throw new IllegalArgumentException("Use isNull or isNotNull for null checks."); + } + return new Filter(column, operator, value); + } + + /** Creates a null check for {@code column}. */ + public static Filter isNull(String column) { + return new Filter(column, ComparisonOperator.IS_NULL, null); + } + + /** Creates a non-null check for {@code column}. */ + public static Filter isNotNull(String column) { + return new Filter(column, ComparisonOperator.IS_NOT_NULL, null); + } + + public String column() { + return column; + } + + public ComparisonOperator operator() { + return operator; + } + + /** Returns the scalar comparison value, or {@code null} for null checks. */ + public Literal value() { + return value; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Filter)) { + return false; + } + Filter that = (Filter) other; + return column.equals(that.column) + && operator == that.operator + && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(column, operator, value); + } +} diff --git a/maven-projects/io-api/src/main/java/org/apache/graphar/io/Literal.java b/maven-projects/io-api/src/main/java/org/apache/graphar/io/Literal.java new file mode 100644 index 000000000..9140c32ea --- /dev/null +++ b/maven-projects/io-api/src/main/java/org/apache/graphar/io/Literal.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.graphar.io; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.Objects; + +/** An immutable scalar used by a {@link Filter} comparison. */ +public final class Literal { + private final Object value; + + private Literal(Object value) { + this.value = value; + } + + /** + * Wraps a supported immutable scalar: {@link Boolean}, numeric boxed primitives, {@link + * String}, {@link LocalDate}, or millisecond-precise {@link Instant}. + */ + public static Literal of(Object value) { + Objects.requireNonNull(value, "A literal value cannot be null."); + if (!(value instanceof Boolean) + && !(value instanceof Byte) + && !(value instanceof Short) + && !(value instanceof Integer) + && !(value instanceof Long) + && !(value instanceof Float) + && !(value instanceof Double) + && !(value instanceof String) + && !(value instanceof LocalDate) + && !(value instanceof Instant)) { + throw new IllegalArgumentException( + "A literal must be a Boolean, numeric boxed primitive, String, LocalDate, or Instant."); + } + if (value instanceof Float && !Float.isFinite((Float) value)) { + throw new IllegalArgumentException("A floating point literal must be finite."); + } + if (value instanceof Double && !Double.isFinite((Double) value)) { + throw new IllegalArgumentException("A floating point literal must be finite."); + } + if (value instanceof Instant && ((Instant) value).getNano() % 1_000_000 != 0) { + throw new IllegalArgumentException( + "An Instant literal must have millisecond precision."); + } + return new Literal(value); + } + + /** Returns this literal's immutable scalar value. */ + public Object value() { + return value; + } + + @Override + public boolean equals(Object other) { + return other instanceof Literal && value.equals(((Literal) other).value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/maven-projects/io-api/src/main/java/org/apache/graphar/io/Projection.java b/maven-projects/io-api/src/main/java/org/apache/graphar/io/Projection.java new file mode 100644 index 000000000..d0ab6e7ee --- /dev/null +++ b/maven-projects/io-api/src/main/java/org/apache/graphar/io/Projection.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.graphar.io; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** An ordered set of requested output columns. */ +public final class Projection { + private static final Projection ALL_COLUMNS = new Projection(true, List.of()); + + private final boolean allColumns; + private final List columns; + + private Projection(boolean allColumns, List columns) { + this.allColumns = allColumns; + this.columns = columns; + } + + /** Requests every available column. */ + public static Projection all() { + return ALL_COLUMNS; + } + + /** Requests the supplied columns in order. */ + public static Projection of(List columns) { + if (columns == null || columns.isEmpty()) { + throw new IllegalArgumentException("A projection must contain at least one column."); + } + List copy = new ArrayList<>(columns.size()); + Set names = new HashSet<>(); + for (String column : columns) { + if (column == null || column.isBlank()) { + throw new IllegalArgumentException("Projection column names cannot be blank."); + } + if (!names.add(column)) { + throw new IllegalArgumentException( + "Projection contains duplicate column: " + column); + } + copy.add(column); + } + return new Projection(false, List.copyOf(copy)); + } + + /** Returns whether this projection requests every available column. */ + public boolean isAllColumns() { + return allColumns; + } + + /** Returns the requested columns, or an empty list when all columns are requested. */ + public List columns() { + return columns; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Projection)) { + return false; + } + Projection that = (Projection) other; + return allColumns == that.allColumns && columns.equals(that.columns); + } + + @Override + public int hashCode() { + return Objects.hash(allColumns, columns); + } +} diff --git a/maven-projects/io-api/src/main/java/org/apache/graphar/io/ReadCapability.java b/maven-projects/io-api/src/main/java/org/apache/graphar/io/ReadCapability.java new file mode 100644 index 000000000..581dc86da --- /dev/null +++ b/maven-projects/io-api/src/main/java/org/apache/graphar/io/ReadCapability.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.graphar.io; + +/** A physical optimization that an IO reader may apply to a request. */ +public enum ReadCapability { + PROJECTION, + ROW_RANGE, + FILTER, + LIMIT +} diff --git a/maven-projects/io-api/src/main/java/org/apache/graphar/io/ReadRequest.java b/maven-projects/io-api/src/main/java/org/apache/graphar/io/ReadRequest.java new file mode 100644 index 000000000..fd029c1d7 --- /dev/null +++ b/maven-projects/io-api/src/main/java/org/apache/graphar/io/ReadRequest.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.graphar.io; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.Set; + +/** + * An immutable physical read operation. Filters are combined with logical AND and may require a + * backend to read columns that are not in the requested output projection. + */ +public final class ReadRequest { + private final URI uri; + private final Projection projection; + private final RowRange rowRange; + private final List filters; + private final Long limit; + + private ReadRequest(Builder builder) { + this.uri = Objects.requireNonNull(builder.uri, "A read URI cannot be null."); + this.projection = builder.projection; + this.rowRange = builder.rowRange; + this.filters = List.copyOf(builder.filters); + this.limit = builder.limit; + } + + /** Starts a request for {@code uri} with every available column selected. */ + public static Builder builder(URI uri) { + return new Builder(uri); + } + + /** Returns the logical location of the physical input. */ + public URI uri() { + return uri; + } + + /** Returns the requested output columns. */ + public Projection projection() { + return projection; + } + + /** Returns the optional half-open source-row range. */ + public Optional rowRange() { + return Optional.ofNullable(rowRange); + } + + /** Returns the immutable, ordered conjunction of filter hints. */ + public List filters() { + return filters; + } + + /** Returns the optional maximum number of output rows; zero is a valid limit. */ + public OptionalLong limit() { + return limit == null ? OptionalLong.empty() : OptionalLong.of(limit); + } + + /** Returns every physical optimization requested by this operation. */ + public Set requestedCapabilities() { + EnumSet capabilities = EnumSet.noneOf(ReadCapability.class); + if (!projection.isAllColumns()) { + capabilities.add(ReadCapability.PROJECTION); + } + if (rowRange != null) { + capabilities.add(ReadCapability.ROW_RANGE); + } + if (!filters.isEmpty()) { + capabilities.add(ReadCapability.FILTER); + } + if (limit != null) { + capabilities.add(ReadCapability.LIMIT); + } + return Collections.unmodifiableSet(capabilities); + } + + /** Builder for immutable {@link ReadRequest} values. */ + public static final class Builder { + private final URI uri; + private Projection projection = Projection.all(); + private RowRange rowRange; + private List filters = List.of(); + private Long limit; + + private Builder(URI uri) { + this.uri = Objects.requireNonNull(uri, "A read URI cannot be null."); + } + + /** Replaces the output projection. */ + public Builder projection(Projection projection) { + this.projection = Objects.requireNonNull(projection, "A projection cannot be null."); + return this; + } + + /** Restricts the read to a half-open source-row range. */ + public Builder rowRange(RowRange rowRange) { + this.rowRange = Objects.requireNonNull(rowRange, "A row range cannot be null."); + return this; + } + + /** Replaces the ordered conjunction of filter hints. */ + public Builder filters(List filters) { + Objects.requireNonNull(filters, "Filters cannot be null."); + List copy = new ArrayList<>(filters.size()); + for (Filter filter : filters) { + copy.add(Objects.requireNonNull(filter, "A filter cannot be null.")); + } + this.filters = List.copyOf(copy); + return this; + } + + /** Limits returned rows after any requested filtering. */ + public Builder limit(long limit) { + if (limit < 0) { + throw new IllegalArgumentException("A read limit cannot be negative."); + } + this.limit = limit; + return this; + } + + /** Builds an immutable read operation. */ + public ReadRequest build() { + return new ReadRequest(this); + } + } +} diff --git a/maven-projects/io-api/src/main/java/org/apache/graphar/io/RowRange.java b/maven-projects/io-api/src/main/java/org/apache/graphar/io/RowRange.java new file mode 100644 index 000000000..dea5940ff --- /dev/null +++ b/maven-projects/io-api/src/main/java/org/apache/graphar/io/RowRange.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.graphar.io; + +import java.util.Objects; + +/** A half-open physical row range: {@code [startInclusive, endExclusive)}. */ +public final class RowRange { + private final long startInclusive; + private final long endExclusive; + + public RowRange(long startInclusive, long endExclusive) { + if (startInclusive < 0 || endExclusive < startInclusive) { + throw new IllegalArgumentException( + "A row range must satisfy 0 <= startInclusive <= endExclusive."); + } + this.startInclusive = startInclusive; + this.endExclusive = endExclusive; + } + + public long startInclusive() { + return startInclusive; + } + + public long endExclusive() { + return endExclusive; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof RowRange)) { + return false; + } + RowRange that = (RowRange) other; + return startInclusive == that.startInclusive && endExclusive == that.endExclusive; + } + + @Override + public int hashCode() { + return Objects.hash(startInclusive, endExclusive); + } +} diff --git a/maven-projects/io-api/src/test/java/org/apache/graphar/io/FilterTest.java b/maven-projects/io-api/src/test/java/org/apache/graphar/io/FilterTest.java new file mode 100644 index 000000000..0285bda1e --- /dev/null +++ b/maven-projects/io-api/src/test/java/org/apache/graphar/io/FilterTest.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.graphar.io; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.time.Instant; +import org.junit.Test; + +public class FilterTest { + @Test + public void retainsAnImmutableScalarComparison() { + Filter filter = Filter.comparison("name", ComparisonOperator.EQUAL, Literal.of("alice")); + + assertEquals("name", filter.column()); + assertEquals(ComparisonOperator.EQUAL, filter.operator()); + assertEquals(Literal.of("alice"), filter.value()); + assertEquals(Filter.isNull("name"), Filter.isNull("name")); + } + + @Test + public void rejectsAmbiguousOrLossyComparisonValues() { + assertThrows(IllegalArgumentException.class, () -> Literal.of(Double.NaN)); + assertThrows( + IllegalArgumentException.class, + () -> Literal.of(Instant.parse("2025-01-01T00:00:00.000000001Z"))); + assertThrows( + IllegalArgumentException.class, + () -> Filter.comparison("name", ComparisonOperator.IS_NULL, Literal.of("alice"))); + } +} diff --git a/maven-projects/io-api/src/test/java/org/apache/graphar/io/LiteralTest.java b/maven-projects/io-api/src/test/java/org/apache/graphar/io/LiteralTest.java new file mode 100644 index 000000000..ed34c6ecd --- /dev/null +++ b/maven-projects/io-api/src/test/java/org/apache/graphar/io/LiteralTest.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.graphar.io; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; + +import java.time.Instant; +import java.time.LocalDate; +import org.junit.Test; + +public class LiteralTest { + @Test + public void carriesEverySupportedScalarUnchanged() { + assertEquals(Boolean.TRUE, Literal.of(Boolean.TRUE).value()); + assertEquals((byte) 1, Literal.of((byte) 1).value()); + assertEquals((short) 2, Literal.of((short) 2).value()); + assertEquals(3, Literal.of(3).value()); + assertEquals(4L, Literal.of(4L).value()); + assertEquals(5.5f, Literal.of(5.5f).value()); + assertEquals(6.5d, Literal.of(6.5d).value()); + assertEquals("seven", Literal.of("seven").value()); + assertEquals(LocalDate.of(2024, 1, 31), Literal.of(LocalDate.of(2024, 1, 31)).value()); + assertEquals(Instant.ofEpochMilli(8), Literal.of(Instant.ofEpochMilli(8)).value()); + } + + @Test + public void comparesByValue() { + assertEquals(Literal.of(4L), Literal.of(4L)); + assertEquals(Literal.of(4L).hashCode(), Literal.of(4L).hashCode()); + assertNotEquals(Literal.of(4L), Literal.of(4)); + assertNotEquals(Literal.of(4L), "4"); + } + + @Test + public void refusesAValueAFilterCannotComparePhysically() { + assertThrows(NullPointerException.class, () -> Literal.of(null)); + assertThrows(IllegalArgumentException.class, () -> Literal.of(new byte[] {1})); + assertThrows(IllegalArgumentException.class, () -> Literal.of(Float.NaN)); + assertThrows(IllegalArgumentException.class, () -> Literal.of(Double.POSITIVE_INFINITY)); + } + + @Test + public void refusesATimestampFinerThanTheFormatCanStore() { + assertThrows( + IllegalArgumentException.class, + () -> Literal.of(Instant.ofEpochSecond(1, 1_500_000))); + Literal.of(Instant.ofEpochSecond(1, 2_000_000)); + } +} diff --git a/maven-projects/io-api/src/test/java/org/apache/graphar/io/ProjectionTest.java b/maven-projects/io-api/src/test/java/org/apache/graphar/io/ProjectionTest.java new file mode 100644 index 000000000..2a857570c --- /dev/null +++ b/maven-projects/io-api/src/test/java/org/apache/graphar/io/ProjectionTest.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.graphar.io; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.Test; + +public class ProjectionTest { + @Test + public void requestsEveryColumnWithoutNamingOne() { + Projection projection = Projection.all(); + + assertTrue(projection.isAllColumns()); + assertTrue(projection.columns().isEmpty()); + } + + @Test + public void keepsTheRequestedColumnsInTheGivenOrder() { + Projection projection = Projection.of(Arrays.asList("id", "name")); + + assertFalse(projection.isAllColumns()); + assertEquals(Arrays.asList("id", "name"), projection.columns()); + } + + @Test + public void doesNotFollowLaterEditsToTheSuppliedList() { + List columns = new ArrayList<>(Arrays.asList("id", "name")); + Projection projection = Projection.of(columns); + columns.add("extra"); + + assertEquals(2, projection.columns().size()); + assertThrows( + UnsupportedOperationException.class, () -> projection.columns().add("injected")); + } + + @Test + public void comparesByRequestedColumnsAndOrder() { + assertEquals(Projection.of(List.of("id")), Projection.of(List.of("id"))); + assertEquals( + Projection.of(List.of("id")).hashCode(), Projection.of(List.of("id")).hashCode()); + assertNotEquals(Projection.of(List.of("id", "name")), Projection.of(List.of("name", "id"))); + assertNotEquals(Projection.all(), Projection.of(List.of("id"))); + } + + @Test + public void refusesAProjectionThatSelectsNothingOrRepeatsAColumn() { + assertThrows(IllegalArgumentException.class, () -> Projection.of(null)); + assertThrows(IllegalArgumentException.class, () -> Projection.of(List.of())); + assertThrows( + IllegalArgumentException.class, () -> Projection.of(Arrays.asList("id", "id"))); + assertThrows(IllegalArgumentException.class, () -> Projection.of(Arrays.asList("id", " "))); + assertThrows( + IllegalArgumentException.class, () -> Projection.of(Arrays.asList("id", null))); + } +} diff --git a/maven-projects/io-api/src/test/java/org/apache/graphar/io/ReadRequestTest.java b/maven-projects/io-api/src/test/java/org/apache/graphar/io/ReadRequestTest.java new file mode 100644 index 000000000..1311a842b --- /dev/null +++ b/maven-projects/io-api/src/test/java/org/apache/graphar/io/ReadRequestTest.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.graphar.io; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.net.URI; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import org.junit.Test; + +public class ReadRequestTest { + @Test + public void snapshotsAllReadHints() { + List columns = new ArrayList<>(List.of("dst", "weight")); + List filters = + new ArrayList<>( + List.of( + Filter.comparison( + "weight", + ComparisonOperator.GREATER_THAN_OR_EQUAL, + Literal.of(1.5D)))); + + ReadRequest request = + ReadRequest.builder(URI.create("file:/dataset/part0")) + .projection(Projection.of(columns)) + .rowRange(new RowRange(4, 9)) + .filters(filters) + .limit(0) + .build(); + + columns.clear(); + filters.clear(); + + assertEquals(List.of("dst", "weight"), request.projection().columns()); + assertEquals(1, request.filters().size()); + assertEquals(new RowRange(4, 9), request.rowRange().get()); + assertTrue(request.limit().isPresent()); + assertEquals(0L, request.limit().getAsLong()); + assertEquals(EnumSet.allOf(ReadCapability.class), request.requestedCapabilities()); + assertThrows( + UnsupportedOperationException.class, + () -> request.filters().add(Filter.isNotNull("weight"))); + } + + @Test + public void defaultsDoNotRequestPushdown() { + ReadRequest request = ReadRequest.builder(URI.create("memory:/input")).build(); + + assertTrue(request.projection().isAllColumns()); + assertFalse(request.rowRange().isPresent()); + assertFalse(request.limit().isPresent()); + assertTrue(request.requestedCapabilities().isEmpty()); + } + + @Test + public void rejectsInvalidHints() { + assertThrows(IllegalArgumentException.class, () -> new RowRange(-1, 0)); + assertThrows(IllegalArgumentException.class, () -> new RowRange(2, 1)); + assertThrows( + IllegalArgumentException.class, + () -> ReadRequest.builder(URI.create("file:/input")).limit(-1)); + assertThrows(IllegalArgumentException.class, () -> Projection.of(List.of("id", "id"))); + assertThrows( + IllegalArgumentException.class, + () -> Filter.comparison("id", ComparisonOperator.IS_NULL, Literal.of(1))); + assertThrows( + IllegalArgumentException.class, () -> Literal.of(new StringBuilder("mutable"))); + } +} diff --git a/maven-projects/io-api/src/test/java/org/apache/graphar/io/RowRangeTest.java b/maven-projects/io-api/src/test/java/org/apache/graphar/io/RowRangeTest.java new file mode 100644 index 000000000..3ece89984 --- /dev/null +++ b/maven-projects/io-api/src/test/java/org/apache/graphar/io/RowRangeTest.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.graphar.io; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; + +import org.junit.Test; + +public class RowRangeTest { + @Test + public void keepsTheHalfOpenBoundsItWasGiven() { + RowRange range = new RowRange(10, 25); + + assertEquals(10, range.startInclusive()); + assertEquals(25, range.endExclusive()); + } + + @Test + public void acceptsARangeThatSelectsNoRows() { + RowRange empty = new RowRange(7, 7); + + assertEquals(empty.startInclusive(), empty.endExclusive()); + } + + @Test + public void comparesByBothBounds() { + assertEquals(new RowRange(1, 4), new RowRange(1, 4)); + assertEquals(new RowRange(1, 4).hashCode(), new RowRange(1, 4).hashCode()); + assertNotEquals(new RowRange(1, 4), new RowRange(1, 5)); + } + + @Test + public void refusesANegativeStartOrAnEndBeforeTheStart() { + assertThrows(IllegalArgumentException.class, () -> new RowRange(-1, 5)); + assertThrows(IllegalArgumentException.class, () -> new RowRange(5, 4)); + } +}