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));
+ }
+}