diff --git a/docs/modules/gigamap/pages/indexing/bitmap/index.adoc b/docs/modules/gigamap/pages/indexing/bitmap/index.adoc
index 8813fae6..f3afa45a 100644
--- a/docs/modules/gigamap/pages/indexing/bitmap/index.adoc
+++ b/docs/modules/gigamap/pages/indexing/bitmap/index.adoc
@@ -52,6 +52,12 @@ Here's a list of all predefined indexers used for low cardinality:
|IndexerLocalDateTime
|java.time.LocalDateTime
+|IndexerInstant
+|java.time.Instant
+
+|IndexerZonedDateTime
+|java.time.ZonedDateTime
+
|IndexerYearMonth
|java.time.YearMonth
diff --git a/gigamap/gigamap/src/main/java/org/eclipse/store/gigamap/types/IndexerInstant.java b/gigamap/gigamap/src/main/java/org/eclipse/store/gigamap/types/IndexerInstant.java
new file mode 100644
index 00000000..57d335f2
--- /dev/null
+++ b/gigamap/gigamap/src/main/java/org/eclipse/store/gigamap/types/IndexerInstant.java
@@ -0,0 +1,479 @@
+package org.eclipse.store.gigamap.types;
+
+/*-
+ * #%L
+ * EclipseStore GigaMap
+ * %%
+ * Copyright (C) 2023 - 2025 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.time.chrono.IsoChronology;
+import java.util.function.IntPredicate;
+
+/**
+ * Indexing logic for {@link Instant} keys.
+ *
+ * The Instant is decomposed into UTC date-time components (year, month, day, hour, minute, second)
+ * for indexing. This enables component-level queries ({@code isYear()}, {@code isMonth()}, etc.)
+ * as well as range queries ({@code before()}, {@code after()}, {@code between()}).
+ *
+ * Nanosecond precision is not included in the index. Two Instants that differ only in their
+ * nanosecond component will be treated as the same key.
+ *
+ * @param the entity type
+ *
+ * @see IndexerDateTime
+ */
+public interface IndexerInstant extends IndexerDateTime
+{
+
+ /**
+ * Abstract base class for an {@link Instant} key {@link Indexer}.
+ *
+ * @param the entity type
+ */
+ public abstract class Abstract extends HashingCompositeIndexer.AbstractSingleValueFixedSize implements IndexerInstant
+ {
+ private final static int YEAR_INDEX = 0;
+ private final static int MONTH_INDEX = 1;
+ private final static int DAY_INDEX = 2;
+ private final static int HOUR_INDEX = 3;
+ private final static int MINUTE_INDEX = 4;
+ private final static int SECOND_INDEX = 5;
+
+ protected Abstract()
+ {
+ super();
+ }
+
+ protected abstract Instant getInstant(E entity);
+
+ @Override
+ protected Instant getValue(final E entity)
+ {
+ return this.getInstant(entity);
+ }
+
+ @Override
+ protected int compositeSize()
+ {
+ return 6;
+ }
+
+ @Override
+ protected void fillCarrier(final Instant value, final Object[] carrier)
+ {
+ final OffsetDateTime utc = value.atOffset(ZoneOffset.UTC);
+ carrier[YEAR_INDEX] = utc.getYear();
+ carrier[MONTH_INDEX] = utc.getMonthValue();
+ carrier[DAY_INDEX] = utc.getDayOfMonth();
+ carrier[HOUR_INDEX] = utc.getHour();
+ carrier[MINUTE_INDEX] = utc.getMinute();
+ carrier[SECOND_INDEX] = utc.getSecond();
+ }
+
+ @Override
+ public final Condition is(final Instant other)
+ {
+ return other == null
+ ? this.isNull()
+ : this.isValue(other)
+ ;
+ }
+
+ @Override
+ public final Condition isDateTime(
+ final int year, final int month, final int day,
+ final int hour, final int minute, final int second
+ )
+ {
+ Validators.validateMonth(month);
+ Validators.validateDay(day);
+ Validators.validateHour(hour);
+ Validators.validateMinute(minute);
+ Validators.validateSecond(second);
+
+ return this.isValue(
+ OffsetDateTime.of(year, month, day, hour, minute, second, 0, ZoneOffset.UTC).toInstant()
+ );
+ }
+
+ @Override
+ public Condition isYear(final IntPredicate yearPredicate)
+ {
+ return this.is(new FieldPredicate(YEAR_INDEX, yearPredicate));
+ }
+
+ @Override
+ public Condition isMonth(final IntPredicate monthPredicate)
+ {
+ return this.is(new FieldPredicate(MONTH_INDEX, monthPredicate));
+ }
+
+ @Override
+ public Condition isDay(final IntPredicate dayPredicate)
+ {
+ return this.is(new FieldPredicate(DAY_INDEX, dayPredicate));
+ }
+
+ @Override
+ public Condition isHour(final IntPredicate hourPredicate)
+ {
+ return this.is(new FieldPredicate(HOUR_INDEX, hourPredicate));
+ }
+
+ @Override
+ public Condition isMinute(final IntPredicate minutePredicate)
+ {
+ return this.is(new FieldPredicate(MINUTE_INDEX, minutePredicate));
+ }
+
+ @Override
+ public Condition isSecond(final IntPredicate secondPredicate)
+ {
+ return this.is(new FieldPredicate(SECOND_INDEX, secondPredicate));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final Condition isDate(final int year, final int month, final int day)
+ {
+ Validators.validateMonth(month);
+ Validators.validateDay(day);
+
+ return (Condition)this.isYear(year).and(this.isMonth(month)).and(this.isDay(day));
+ }
+
+ @Override
+ public final Condition isYear(final int year)
+ {
+ return this.is(new EqualsFieldPredicate(YEAR_INDEX, year));
+ }
+
+ @Override
+ public final Condition isMonth(final int month)
+ {
+ Validators.validateMonth(month);
+
+ return this.is(new EqualsFieldPredicate(MONTH_INDEX, month));
+ }
+
+ @Override
+ public final Condition isDay(final int day)
+ {
+ Validators.validateDay(day);
+
+ return this.is(new EqualsFieldPredicate(DAY_INDEX, day));
+ }
+
+ @Override
+ public final Condition isLeapYear()
+ {
+ return this.is(new FieldPredicate(YEAR_INDEX, IsoChronology.INSTANCE::isLeapYear));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final Condition isTime(final int hour, final int minute, final int second)
+ {
+ Validators.validateHour(hour);
+ Validators.validateMinute(minute);
+ Validators.validateSecond(second);
+
+ return (Condition)this.isHour(hour).and(this.isMinute(minute)).and(this.isSecond(second));
+ }
+
+ @Override
+ public final Condition isHour(final int hour)
+ {
+ Validators.validateHour(hour);
+
+ return this.is(new EqualsFieldPredicate(HOUR_INDEX, hour));
+ }
+
+ @Override
+ public final Condition isMinute(final int minute)
+ {
+ Validators.validateMinute(minute);
+
+ return this.is(new EqualsFieldPredicate(MINUTE_INDEX, minute));
+ }
+
+ @Override
+ public final Condition isSecond(final int second)
+ {
+ Validators.validateSecond(second);
+
+ return this.is(new EqualsFieldPredicate(SECOND_INDEX, second));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final Condition before(final Instant boundExclusive)
+ {
+ if(boundExclusive == null)
+ {
+ throw new IllegalArgumentException("boundExclusive cannot be null");
+ }
+
+ final OffsetDateTime bound = boundExclusive.atOffset(ZoneOffset.UTC);
+
+ return (Condition)this.is(
+ new FieldPredicate(YEAR_INDEX, year -> year < bound.getYear())
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(YEAR_INDEX, boundExclusive)
+ ).and(
+ this.is(new FieldPredicate(MONTH_INDEX, month -> month < bound.getMonthValue()))
+ )
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(MONTH_INDEX, boundExclusive)
+ ).and(
+ this.is(new FieldPredicate(DAY_INDEX, day -> day < bound.getDayOfMonth()))
+ )
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(DAY_INDEX, boundExclusive)
+ ).and(
+ this.is(new FieldPredicate(HOUR_INDEX, hour -> hour < bound.getHour()))
+ )
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(HOUR_INDEX, boundExclusive)
+ ).and(
+ this.is(new FieldPredicate(MINUTE_INDEX, minute -> minute < bound.getMinute()))
+ )
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(MINUTE_INDEX, boundExclusive)
+ ).and(
+ this.is(new FieldPredicate(SECOND_INDEX, second -> second < bound.getSecond()))
+ )
+ );
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final Condition beforeEqual(final Instant boundInclusive)
+ {
+ if(boundInclusive == null)
+ {
+ throw new IllegalArgumentException("boundInclusive cannot be null");
+ }
+
+ return (Condition)this.is(boundInclusive).or(this.before(boundInclusive));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final Condition after(final Instant boundExclusive)
+ {
+ if(boundExclusive == null)
+ {
+ throw new IllegalArgumentException("boundExclusive cannot be null");
+ }
+
+ final OffsetDateTime bound = boundExclusive.atOffset(ZoneOffset.UTC);
+
+ return (Condition)this.is(
+ new FieldPredicate(YEAR_INDEX, year -> year > bound.getYear())
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(YEAR_INDEX, boundExclusive)
+ ).and(
+ this.is(new FieldPredicate(MONTH_INDEX, month -> month > bound.getMonthValue()))
+ )
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(MONTH_INDEX, boundExclusive)
+ ).and(
+ this.is(new FieldPredicate(DAY_INDEX, day -> day > bound.getDayOfMonth()))
+ )
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(DAY_INDEX, boundExclusive)
+ ).and(
+ this.is(new FieldPredicate(HOUR_INDEX, hour -> hour > bound.getHour()))
+ )
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(HOUR_INDEX, boundExclusive)
+ ).and(
+ this.is(new FieldPredicate(MINUTE_INDEX, minute -> minute > bound.getMinute()))
+ )
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(MINUTE_INDEX, boundExclusive)
+ ).and(
+ this.is(new FieldPredicate(SECOND_INDEX, second -> second > bound.getSecond()))
+ )
+ );
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final Condition afterEqual(final Instant boundInclusive)
+ {
+ if(boundInclusive == null)
+ {
+ throw new IllegalArgumentException("boundInclusive cannot be null");
+ }
+
+ return (Condition)this.is(boundInclusive).or(this.after(boundInclusive));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final Condition between(final Instant startInclusive, final Instant endInclusive)
+ {
+ if(startInclusive == null)
+ {
+ throw new IllegalArgumentException("startInclusive cannot be null");
+ }
+ if(endInclusive == null)
+ {
+ throw new IllegalArgumentException("endInclusive cannot be null");
+ }
+
+ return (Condition)this.afterEqual(startInclusive).and(this.beforeEqual(endInclusive));
+ }
+
+
+ static class FieldPredicate implements CompositePredicate
+ {
+ final int subKeyPosition;
+ final IntPredicate predicate;
+
+ FieldPredicate(final int subKeyPosition, final IntPredicate predicate)
+ {
+ this.subKeyPosition = subKeyPosition;
+ this.predicate = predicate;
+ }
+
+ @Override
+ public boolean setSubKeyPosition(final int subKeyPosition)
+ {
+ return subKeyPosition == this.subKeyPosition;
+ }
+
+ @Override
+ public boolean test(final Object[] keys)
+ {
+ return this.test(this.subKeyPosition, keys[this.subKeyPosition]);
+ }
+
+ @Override
+ public boolean test(final int subKeyPosition, final Object subKey)
+ {
+ return subKeyPosition == this.subKeyPosition && subKey instanceof Integer && this.predicate.test((Integer)subKey);
+ }
+
+ }
+
+
+ static class EqualsFieldPredicate implements CompositePredicate
+ {
+ final int subKeyPosition;
+ final int value;
+
+ EqualsFieldPredicate(final int subKeyPosition, final int value)
+ {
+ this.subKeyPosition = subKeyPosition;
+ this.value = value;
+ }
+
+ @Override
+ public boolean setSubKeyPosition(final int subKeyPosition)
+ {
+ return subKeyPosition == this.subKeyPosition;
+ }
+
+ @Override
+ public boolean test(final Object[] keys)
+ {
+ return this.test(this.subKeyPosition, keys[this.subKeyPosition]);
+ }
+
+ @Override
+ public boolean test(final int subKeyPosition, final Object subKey)
+ {
+ return subKeyPosition == this.subKeyPosition && subKey instanceof Integer && this.value == (Integer)subKey;
+ }
+
+ }
+
+
+ static class EqualsUntilPredicate implements CompositePredicate
+ {
+ final int maxSubKeyPosition;
+ final Instant other;
+
+ EqualsUntilPredicate(final int maxSubKeyPosition, final Instant other)
+ {
+ this.maxSubKeyPosition = maxSubKeyPosition;
+ this.other = other;
+ }
+
+ @Override
+ public boolean setSubKeyPosition(final int subKeyPosition)
+ {
+ return subKeyPosition <= this.maxSubKeyPosition;
+ }
+
+ @Override
+ public boolean test(final Object[] keys)
+ {
+ for(int i = 0; i < this.maxSubKeyPosition; i++)
+ {
+ if(!this.test(i, keys[i]))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean test(final int subKeyPosition, final Object subKey)
+ {
+ return subKeyPosition <= this.maxSubKeyPosition && subKey instanceof Integer && get(this.other, subKeyPosition) == (Integer)subKey;
+ }
+
+ static int get(final Instant instant, final int subKeyPosition)
+ {
+ final OffsetDateTime utc = instant.atOffset(ZoneOffset.UTC);
+ switch(subKeyPosition)
+ {
+ case YEAR_INDEX:
+ return utc.getYear();
+ case MONTH_INDEX:
+ return utc.getMonthValue();
+ case DAY_INDEX:
+ return utc.getDayOfMonth();
+ case HOUR_INDEX:
+ return utc.getHour();
+ case MINUTE_INDEX:
+ return utc.getMinute();
+ case SECOND_INDEX:
+ return utc.getSecond();
+ default:
+ throw new IllegalArgumentException("Invalid subKeyPosition: " + subKeyPosition);
+ }
+ }
+
+ }
+
+ }
+
+}
diff --git a/gigamap/gigamap/src/main/java/org/eclipse/store/gigamap/types/IndexerZonedDateTime.java b/gigamap/gigamap/src/main/java/org/eclipse/store/gigamap/types/IndexerZonedDateTime.java
new file mode 100644
index 00000000..c4571337
--- /dev/null
+++ b/gigamap/gigamap/src/main/java/org/eclipse/store/gigamap/types/IndexerZonedDateTime.java
@@ -0,0 +1,487 @@
+package org.eclipse.store.gigamap.types;
+
+/*-
+ * #%L
+ * EclipseStore GigaMap
+ * %%
+ * Copyright (C) 2023 - 2025 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.chrono.IsoChronology;
+import java.util.function.IntPredicate;
+
+/**
+ * Indexing logic for {@link ZonedDateTime} keys.
+ *
+ * The ZonedDateTime is converted to UTC and decomposed into date-time components
+ * (year, month, day, hour, minute, second) for indexing. This enables component-level queries
+ * ({@code isYear()}, {@code isMonth()}, etc.) as well as range queries
+ * ({@code before()}, {@code after()}, {@code between()}).
+ *
+ * All queries operate on the absolute point in time (UTC), not on the local wall-clock time
+ * of the original zone.
+ *
+ * Nanosecond precision is not included in the index. Two ZonedDateTimes that differ only in their
+ * nanosecond component will be treated as the same key.
+ *
+ * @param the entity type
+ *
+ * @see IndexerDateTime
+ * @see IndexerInstant
+ */
+public interface IndexerZonedDateTime extends IndexerDateTime
+{
+
+ /**
+ * Abstract base class for a {@link ZonedDateTime} key {@link Indexer}.
+ *
+ * @param the entity type
+ */
+ public abstract class Abstract extends HashingCompositeIndexer.AbstractSingleValueFixedSize implements IndexerZonedDateTime
+ {
+ private final static int YEAR_INDEX = 0;
+ private final static int MONTH_INDEX = 1;
+ private final static int DAY_INDEX = 2;
+ private final static int HOUR_INDEX = 3;
+ private final static int MINUTE_INDEX = 4;
+ private final static int SECOND_INDEX = 5;
+
+ protected Abstract()
+ {
+ super();
+ }
+
+ protected abstract ZonedDateTime getZonedDateTime(E entity);
+
+ @Override
+ protected ZonedDateTime getValue(final E entity)
+ {
+ return this.getZonedDateTime(entity);
+ }
+
+ @Override
+ protected int compositeSize()
+ {
+ return 6;
+ }
+
+ @Override
+ protected void fillCarrier(final ZonedDateTime value, final Object[] carrier)
+ {
+ final OffsetDateTime utc = value.toInstant().atOffset(ZoneOffset.UTC);
+ carrier[YEAR_INDEX] = utc.getYear();
+ carrier[MONTH_INDEX] = utc.getMonthValue();
+ carrier[DAY_INDEX] = utc.getDayOfMonth();
+ carrier[HOUR_INDEX] = utc.getHour();
+ carrier[MINUTE_INDEX] = utc.getMinute();
+ carrier[SECOND_INDEX] = utc.getSecond();
+ }
+
+ @Override
+ public final Condition is(final ZonedDateTime other)
+ {
+ return other == null
+ ? this.isNull()
+ : this.isValue(other)
+ ;
+ }
+
+ @Override
+ public final Condition isDateTime(
+ final int year, final int month, final int day,
+ final int hour, final int minute, final int second
+ )
+ {
+ Validators.validateMonth(month);
+ Validators.validateDay(day);
+ Validators.validateHour(hour);
+ Validators.validateMinute(minute);
+ Validators.validateSecond(second);
+
+ return this.isValue(
+ OffsetDateTime.of(year, month, day, hour, minute, second, 0, ZoneOffset.UTC).toZonedDateTime()
+ );
+ }
+
+ @Override
+ public Condition isYear(final IntPredicate yearPredicate)
+ {
+ return this.is(new FieldPredicate(YEAR_INDEX, yearPredicate));
+ }
+
+ @Override
+ public Condition isMonth(final IntPredicate monthPredicate)
+ {
+ return this.is(new FieldPredicate(MONTH_INDEX, monthPredicate));
+ }
+
+ @Override
+ public Condition isDay(final IntPredicate dayPredicate)
+ {
+ return this.is(new FieldPredicate(DAY_INDEX, dayPredicate));
+ }
+
+ @Override
+ public Condition isHour(final IntPredicate hourPredicate)
+ {
+ return this.is(new FieldPredicate(HOUR_INDEX, hourPredicate));
+ }
+
+ @Override
+ public Condition isMinute(final IntPredicate minutePredicate)
+ {
+ return this.is(new FieldPredicate(MINUTE_INDEX, minutePredicate));
+ }
+
+ @Override
+ public Condition isSecond(final IntPredicate secondPredicate)
+ {
+ return this.is(new FieldPredicate(SECOND_INDEX, secondPredicate));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final Condition isDate(final int year, final int month, final int day)
+ {
+ Validators.validateMonth(month);
+ Validators.validateDay(day);
+
+ return (Condition)this.isYear(year).and(this.isMonth(month)).and(this.isDay(day));
+ }
+
+ @Override
+ public final Condition isYear(final int year)
+ {
+ return this.is(new EqualsFieldPredicate(YEAR_INDEX, year));
+ }
+
+ @Override
+ public final Condition isMonth(final int month)
+ {
+ Validators.validateMonth(month);
+
+ return this.is(new EqualsFieldPredicate(MONTH_INDEX, month));
+ }
+
+ @Override
+ public final Condition isDay(final int day)
+ {
+ Validators.validateDay(day);
+
+ return this.is(new EqualsFieldPredicate(DAY_INDEX, day));
+ }
+
+ @Override
+ public final Condition isLeapYear()
+ {
+ return this.is(new FieldPredicate(YEAR_INDEX, IsoChronology.INSTANCE::isLeapYear));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final Condition isTime(final int hour, final int minute, final int second)
+ {
+ Validators.validateHour(hour);
+ Validators.validateMinute(minute);
+ Validators.validateSecond(second);
+
+ return (Condition)this.isHour(hour).and(this.isMinute(minute)).and(this.isSecond(second));
+ }
+
+ @Override
+ public final Condition isHour(final int hour)
+ {
+ Validators.validateHour(hour);
+
+ return this.is(new EqualsFieldPredicate(HOUR_INDEX, hour));
+ }
+
+ @Override
+ public final Condition isMinute(final int minute)
+ {
+ Validators.validateMinute(minute);
+
+ return this.is(new EqualsFieldPredicate(MINUTE_INDEX, minute));
+ }
+
+ @Override
+ public final Condition isSecond(final int second)
+ {
+ Validators.validateSecond(second);
+
+ return this.is(new EqualsFieldPredicate(SECOND_INDEX, second));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final Condition before(final ZonedDateTime boundExclusive)
+ {
+ if(boundExclusive == null)
+ {
+ throw new IllegalArgumentException("boundExclusive cannot be null");
+ }
+
+ final Instant boundInstant = boundExclusive.toInstant();
+ final OffsetDateTime bound = boundInstant.atOffset(ZoneOffset.UTC);
+
+ return (Condition)this.is(
+ new FieldPredicate(YEAR_INDEX, year -> year < bound.getYear())
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(YEAR_INDEX, boundInstant)
+ ).and(
+ this.is(new FieldPredicate(MONTH_INDEX, month -> month < bound.getMonthValue()))
+ )
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(MONTH_INDEX, boundInstant)
+ ).and(
+ this.is(new FieldPredicate(DAY_INDEX, day -> day < bound.getDayOfMonth()))
+ )
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(DAY_INDEX, boundInstant)
+ ).and(
+ this.is(new FieldPredicate(HOUR_INDEX, hour -> hour < bound.getHour()))
+ )
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(HOUR_INDEX, boundInstant)
+ ).and(
+ this.is(new FieldPredicate(MINUTE_INDEX, minute -> minute < bound.getMinute()))
+ )
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(MINUTE_INDEX, boundInstant)
+ ).and(
+ this.is(new FieldPredicate(SECOND_INDEX, second -> second < bound.getSecond()))
+ )
+ );
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final Condition beforeEqual(final ZonedDateTime boundInclusive)
+ {
+ if(boundInclusive == null)
+ {
+ throw new IllegalArgumentException("boundInclusive cannot be null");
+ }
+
+ return (Condition)this.is(boundInclusive).or(this.before(boundInclusive));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final Condition after(final ZonedDateTime boundExclusive)
+ {
+ if(boundExclusive == null)
+ {
+ throw new IllegalArgumentException("boundExclusive cannot be null");
+ }
+
+ final Instant boundInstant = boundExclusive.toInstant();
+ final OffsetDateTime bound = boundInstant.atOffset(ZoneOffset.UTC);
+
+ return (Condition)this.is(
+ new FieldPredicate(YEAR_INDEX, year -> year > bound.getYear())
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(YEAR_INDEX, boundInstant)
+ ).and(
+ this.is(new FieldPredicate(MONTH_INDEX, month -> month > bound.getMonthValue()))
+ )
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(MONTH_INDEX, boundInstant)
+ ).and(
+ this.is(new FieldPredicate(DAY_INDEX, day -> day > bound.getDayOfMonth()))
+ )
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(DAY_INDEX, boundInstant)
+ ).and(
+ this.is(new FieldPredicate(HOUR_INDEX, hour -> hour > bound.getHour()))
+ )
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(HOUR_INDEX, boundInstant)
+ ).and(
+ this.is(new FieldPredicate(MINUTE_INDEX, minute -> minute > bound.getMinute()))
+ )
+ ).or(
+ this.is(
+ new EqualsUntilPredicate(MINUTE_INDEX, boundInstant)
+ ).and(
+ this.is(new FieldPredicate(SECOND_INDEX, second -> second > bound.getSecond()))
+ )
+ );
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final Condition afterEqual(final ZonedDateTime boundInclusive)
+ {
+ if(boundInclusive == null)
+ {
+ throw new IllegalArgumentException("boundInclusive cannot be null");
+ }
+
+ return (Condition)this.is(boundInclusive).or(this.after(boundInclusive));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final Condition between(final ZonedDateTime startInclusive, final ZonedDateTime endInclusive)
+ {
+ if(startInclusive == null)
+ {
+ throw new IllegalArgumentException("startInclusive cannot be null");
+ }
+ if(endInclusive == null)
+ {
+ throw new IllegalArgumentException("endInclusive cannot be null");
+ }
+
+ return (Condition)this.afterEqual(startInclusive).and(this.beforeEqual(endInclusive));
+ }
+
+
+ static class FieldPredicate implements CompositePredicate
+ {
+ final int subKeyPosition;
+ final IntPredicate predicate;
+
+ FieldPredicate(final int subKeyPosition, final IntPredicate predicate)
+ {
+ this.subKeyPosition = subKeyPosition;
+ this.predicate = predicate;
+ }
+
+ @Override
+ public boolean setSubKeyPosition(final int subKeyPosition)
+ {
+ return subKeyPosition == this.subKeyPosition;
+ }
+
+ @Override
+ public boolean test(final Object[] keys)
+ {
+ return this.test(this.subKeyPosition, keys[this.subKeyPosition]);
+ }
+
+ @Override
+ public boolean test(final int subKeyPosition, final Object subKey)
+ {
+ return subKeyPosition == this.subKeyPosition && subKey instanceof Integer && this.predicate.test((Integer)subKey);
+ }
+
+ }
+
+
+ static class EqualsFieldPredicate implements CompositePredicate
+ {
+ final int subKeyPosition;
+ final int value;
+
+ EqualsFieldPredicate(final int subKeyPosition, final int value)
+ {
+ this.subKeyPosition = subKeyPosition;
+ this.value = value;
+ }
+
+ @Override
+ public boolean setSubKeyPosition(final int subKeyPosition)
+ {
+ return subKeyPosition == this.subKeyPosition;
+ }
+
+ @Override
+ public boolean test(final Object[] keys)
+ {
+ return this.test(this.subKeyPosition, keys[this.subKeyPosition]);
+ }
+
+ @Override
+ public boolean test(final int subKeyPosition, final Object subKey)
+ {
+ return subKeyPosition == this.subKeyPosition && subKey instanceof Integer && this.value == (Integer)subKey;
+ }
+
+ }
+
+
+ static class EqualsUntilPredicate implements CompositePredicate
+ {
+ final int maxSubKeyPosition;
+ final Instant other;
+
+ EqualsUntilPredicate(final int maxSubKeyPosition, final Instant other)
+ {
+ this.maxSubKeyPosition = maxSubKeyPosition;
+ this.other = other;
+ }
+
+ @Override
+ public boolean setSubKeyPosition(final int subKeyPosition)
+ {
+ return subKeyPosition <= this.maxSubKeyPosition;
+ }
+
+ @Override
+ public boolean test(final Object[] keys)
+ {
+ for(int i = 0; i < this.maxSubKeyPosition; i++)
+ {
+ if(!this.test(i, keys[i]))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean test(final int subKeyPosition, final Object subKey)
+ {
+ return subKeyPosition <= this.maxSubKeyPosition && subKey instanceof Integer && get(this.other, subKeyPosition) == (Integer)subKey;
+ }
+
+ static int get(final Instant instant, final int subKeyPosition)
+ {
+ final OffsetDateTime utc = instant.atOffset(ZoneOffset.UTC);
+ switch(subKeyPosition)
+ {
+ case YEAR_INDEX:
+ return utc.getYear();
+ case MONTH_INDEX:
+ return utc.getMonthValue();
+ case DAY_INDEX:
+ return utc.getDayOfMonth();
+ case HOUR_INDEX:
+ return utc.getHour();
+ case MINUTE_INDEX:
+ return utc.getMinute();
+ case SECOND_INDEX:
+ return utc.getSecond();
+ default:
+ throw new IllegalArgumentException("Invalid subKeyPosition: " + subKeyPosition);
+ }
+ }
+
+ }
+
+ }
+
+}
diff --git a/gigamap/gigamap/src/test/java/org/eclipse/store/gigamap/indexer/InstantIndexTest.java b/gigamap/gigamap/src/test/java/org/eclipse/store/gigamap/indexer/InstantIndexTest.java
new file mode 100644
index 00000000..045ef854
--- /dev/null
+++ b/gigamap/gigamap/src/test/java/org/eclipse/store/gigamap/indexer/InstantIndexTest.java
@@ -0,0 +1,434 @@
+package org.eclipse.store.gigamap.indexer;
+
+/*-
+ * #%L
+ * EclipseStore GigaMap
+ * %%
+ * Copyright (C) 2023 - 2025 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import org.eclipse.store.gigamap.types.BitmapIndices;
+import org.eclipse.store.gigamap.types.GigaMap;
+import org.eclipse.store.gigamap.types.IndexerInstant;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorage;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.file.Path;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class InstantIndexTest
+{
+ @TempDir
+ Path tempDir;
+
+ private InstantPersonIndex instantPersonIndex = new InstantPersonIndex();
+
+ @Test
+ void nullTests()
+ {
+ GigaMap map = prepageGigaMap();
+
+ assertEquals(1, map.query(instantPersonIndex.is((Instant) null)).count());
+
+ assertThrows(IllegalArgumentException.class, () -> map.query(instantPersonIndex.before(null)).count());
+ assertThrows(IllegalArgumentException.class, () -> map.query(instantPersonIndex.beforeEqual(null)).count());
+ assertThrows(IllegalArgumentException.class, () -> map.query(instantPersonIndex.after(null)).count());
+ assertThrows(IllegalArgumentException.class, () -> map.query(instantPersonIndex.afterEqual(null)).count());
+
+ assertThrows(IllegalArgumentException.class, () -> map.query(instantPersonIndex.between(null, null)).count());
+ assertThrows(IllegalArgumentException.class, () -> map.query(instantPersonIndex.between(toInstant(2021, 1, 1, 12, 0, 0), null)).count());
+ assertThrows(IllegalArgumentException.class, () -> map.query(instantPersonIndex.between(null, toInstant(2021, 1, 1, 12, 0, 0))).count());
+
+ }
+
+ @Test
+ void between()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(instantPersonIndex.between(toInstant(2021, 1, 1, 12, 0, 0), toInstant(2021, 1, 1, 14, 0, 0))).count();
+ assertEquals(3, count);
+ long count1 = map.query(instantPersonIndex.between(toInstant(2021, 1, 1, 12, 0, 0), toInstant(2021, 1, 1, 14, 0, 0))).count();
+ assertEquals(3, count1);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(instantPersonIndex.between(toInstant(2021, 1, 1, 12, 0, 0), toInstant(2021, 1, 1, 14, 0, 0))).count();
+ assertEquals(3, count);
+ long count1 = newMap.query(instantPersonIndex.between(toInstant(2021, 1, 1, 12, 0, 0), toInstant(2021, 1, 1, 14, 0, 0))).count();
+ assertEquals(3, count1);
+ long count2 = newMap.query(instantPersonIndex.between(toInstant(2021, 1, 1, 13, 0, 0), toInstant(2021, 1, 1, 14, 0, 0))).count();
+ assertEquals(2, count2);
+ }
+ }
+
+ @Test
+ void after()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(instantPersonIndex.after(toInstant(2021, 1, 1, 12, 0, 0))).count();
+ assertEquals(9, count);
+ long count1 = map.query(instantPersonIndex.after(toInstant(2021, 1, 1, 13, 0, 0))).count();
+ assertEquals(8, count1);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(instantPersonIndex.after(toInstant(2021, 1, 1, 12, 0, 0))).count();
+ assertEquals(9, count);
+ long count1 = newMap.query(instantPersonIndex.after(toInstant(2021, 1, 1, 13, 0, 0))).count();
+ assertEquals(8, count1);
+ }
+ }
+
+ @Test
+ void before()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(instantPersonIndex.before(toInstant(2021, 1, 1, 12, 0, 1))).count();
+ assertEquals(1, count);
+ long count1 = map.query(instantPersonIndex.before(toInstant(2021, 1, 1, 13, 0, 0))).count();
+ assertEquals(1, count1);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(instantPersonIndex.before(toInstant(2021, 1, 1, 12, 0, 1))).count();
+ assertEquals(1, count);
+ long count1 = newMap.query(instantPersonIndex.before(toInstant(2021, 1, 1, 13, 0, 0))).count();
+ assertEquals(1, count1);
+ }
+ }
+
+ @Test
+ void isSecond()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(instantPersonIndex.isSecond(0)).count();
+ assertEquals(10, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(instantPersonIndex.isSecond(0)).count();
+ assertEquals(10, count);
+ }
+ }
+
+ @Test
+ void isMinute()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(instantPersonIndex.isMinute(0)).count();
+ assertEquals(10, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(instantPersonIndex.isMinute(0)).count();
+ assertEquals(10, count);
+ }
+ }
+
+ @Test
+ void isHour()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(instantPersonIndex.isHour(12)).count();
+ assertEquals(1, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(instantPersonIndex.isHour(12)).count();
+ assertEquals(1, count);
+ }
+ }
+
+ @Test
+ void isTime()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(instantPersonIndex.isTime(12, 0, 0)).count();
+ assertEquals(1, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(instantPersonIndex.isTime(12, 0, 0)).count();
+ assertEquals(1, count);
+ }
+ }
+
+ @Test
+ void isDay()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(instantPersonIndex.isDay(1)).count();
+ assertEquals(10, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(instantPersonIndex.isDay(1)).count();
+ assertEquals(10, count);
+ }
+ }
+
+ @Test
+ void isMonth()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(instantPersonIndex.isMonth(1)).count();
+ assertEquals(10, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(instantPersonIndex.isMonth(1)).count();
+ assertEquals(10, count);
+ }
+ }
+
+ @Test
+ void isYear()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(instantPersonIndex.isYear(2021)).count();
+ assertEquals(10, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(instantPersonIndex.isYear(2021)).count();
+ assertEquals(10, count);
+ }
+ }
+
+ @Test
+ void isDate()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(instantPersonIndex.isDate(2021, 1, 1)).count();
+ assertEquals(10, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(instantPersonIndex.isDate(2021, 1, 1)).count();
+ assertEquals(10, count);
+ }
+ }
+
+ @Test
+ void isDateTime()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(instantPersonIndex.isDateTime(2021, 1, 1, 12, 0, 0)).count();
+ assertEquals(1, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(instantPersonIndex.isDateTime(2021, 1, 1, 12, 0, 0)).count();
+ assertEquals(1, count);
+ }
+
+ }
+
+ @Test
+ void instantPersonTest()
+ {
+ GigaMap map = GigaMap.New();
+
+ BitmapIndices bitmap = map.index().bitmap();
+ bitmap.add(instantPersonIndex);
+
+ //generate some data
+ InstantPerson person1 = new InstantPerson("Alice", toInstant(2000, 1, 1, 0, 0, 0));
+ InstantPerson person2 = new InstantPerson("Bob", toInstant(1990, 1, 1, 0, 0, 0));
+ InstantPerson person3 = new InstantPerson("Charlie", toInstant(1980, 1, 1, 0, 0, 0));
+ map.addAll(person1, person2, person3);
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ map.query(instantPersonIndex.is(toInstant(2000, 1, 1, 0, 0, 0))).forEach(person -> assertEquals(toInstant(2000, 1, 1, 0, 0, 0), person.getTimestamp()));
+ map.query(instantPersonIndex.is(toInstant(1990, 1, 1, 0, 0, 0))).forEach(person -> assertEquals(toInstant(1990, 1, 1, 0, 0, 0), person.getTimestamp()));
+
+ map.query(instantPersonIndex.is(toInstant(2000, 1, 1, 0, 0, 0))).and(instantPersonIndex.is(toInstant(1990, 1, 1, 0, 0, 0)))
+ .forEach(person -> fail("Should not be reached"));
+
+ List personList = map.query(instantPersonIndex.is(toInstant(2000, 1, 1, 0, 0, 0))).or(instantPersonIndex.is(toInstant(1990, 1, 1, 0, 0, 0)))
+ .stream().collect(Collectors.toList());
+ assertEquals(2, personList.size());
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ newMap.query(instantPersonIndex.is(toInstant(2000, 1, 1, 0, 0, 0))).forEach(person -> assertEquals(toInstant(2000, 1, 1, 0, 0, 0), person.getTimestamp()));
+ newMap.query(instantPersonIndex.is(toInstant(1990, 1, 1, 0, 0, 0))).forEach(person -> assertEquals(toInstant(1990, 1, 1, 0, 0, 0), person.getTimestamp()));
+
+ newMap.query(instantPersonIndex.is(toInstant(2000, 1, 1, 0, 0, 0))).and(instantPersonIndex.is(toInstant(1990, 1, 1, 0, 0, 0)))
+ .forEach(person -> fail("Should not be reached"));
+
+ List personList = newMap.query(instantPersonIndex.is(toInstant(2000, 1, 1, 0, 0, 0))).or(instantPersonIndex.is(toInstant(1990, 1, 1, 0, 0, 0)))
+ .stream().collect(Collectors.toList());
+ assertEquals(2, personList.size());
+ }
+ }
+
+ @Test
+ void beforeEqualTest()
+ {
+ GigaMap map = GigaMap.New();
+
+ BitmapIndices bitmap = map.index().bitmap();
+ bitmap.add(instantPersonIndex);
+
+ //generate some data
+ InstantPerson person1 = new InstantPerson("Alice", toInstant(2000, 1, 1, 0, 0, 0));
+ InstantPerson person2 = new InstantPerson("Bob", toInstant(1990, 1, 1, 0, 0, 0));
+ InstantPerson person3 = new InstantPerson("Charlie", toInstant(1980, 1, 1, 0, 0, 0));
+ map.addAll(person1, person2, person3);
+
+ List list = map.query(instantPersonIndex.beforeEqual(toInstant(1990, 1, 1, 0, 0, 0))).toList();
+ assertEquals(2, list.size());
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ List list1 = newMap.query(instantPersonIndex.beforeEqual(toInstant(1990, 1, 1, 0, 0, 0))).toList();
+ assertEquals(2, list1.size());
+ list1.forEach(person -> assertNotEquals("Alice", person.name));
+ }
+ }
+
+ @Test
+ void afterEqualTest()
+ {
+ GigaMap map = GigaMap.New();
+
+ BitmapIndices bitmap = map.index().bitmap();
+ bitmap.add(instantPersonIndex);
+
+ //generate some data
+ InstantPerson person1 = new InstantPerson("Alice", toInstant(2000, 1, 1, 0, 0, 0));
+ InstantPerson person2 = new InstantPerson("Bob", toInstant(1990, 1, 1, 0, 0, 0));
+ InstantPerson person3 = new InstantPerson("Charlie", toInstant(1980, 1, 1, 0, 0, 0));
+ map.addAll(person1, person2, person3);
+
+ List list = map.query(instantPersonIndex.afterEqual(toInstant(1990, 1, 1, 0, 0, 0))).toList();
+ assertEquals(2, list.size());
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ List list1 = newMap.query(instantPersonIndex.afterEqual(toInstant(1990, 1, 1, 0, 0, 0))).toList();
+ assertEquals(2, list1.size());
+ list1.forEach(person -> assertNotEquals("Charlie", person.name));
+ }
+ }
+
+ private GigaMap prepageGigaMap()
+ {
+ GigaMap map = GigaMap.New();
+
+ BitmapIndices bitmap = map.index().bitmap();
+ bitmap.add(instantPersonIndex);
+
+ InstantPerson person1 = new InstantPerson("Alice", toInstant(2021, 1, 1, 12, 0, 0));
+ InstantPerson person2 = new InstantPerson("Bob", toInstant(2021, 1, 1, 13, 0, 0));
+ InstantPerson person3 = new InstantPerson("Charlie", toInstant(2021, 1, 1, 14, 0, 0));
+ InstantPerson person4 = new InstantPerson("David", toInstant(2021, 1, 1, 15, 0, 0));
+ InstantPerson person5 = new InstantPerson("Eve", toInstant(2021, 1, 1, 16, 0, 0));
+ InstantPerson person6 = new InstantPerson("Frank", toInstant(2021, 1, 1, 17, 0, 0));
+ InstantPerson person7 = new InstantPerson("Grace", toInstant(2021, 1, 1, 18, 0, 0));
+ InstantPerson person8 = new InstantPerson("Hank", toInstant(2021, 1, 1, 19, 0, 0));
+ InstantPerson person9 = new InstantPerson("Ivy", toInstant(2021, 1, 1, 20, 0, 0));
+ InstantPerson person10 = new InstantPerson("Jack", toInstant(2021, 1, 1, 21, 0, 0));
+ InstantPerson person11 = new InstantPerson("Karl", null);
+
+ map.addAll(person1, person2, person3, person4, person5, person6, person7, person8, person9, person10, person11);
+
+ return map;
+ }
+
+ private static Instant toInstant(int year, int month, int day, int hour, int minute, int second)
+ {
+ return LocalDateTime.of(year, month, day, hour, minute, second).toInstant(ZoneOffset.UTC);
+ }
+
+ private static class InstantPersonIndex extends IndexerInstant.Abstract
+ {
+
+ @Override
+ protected Instant getInstant(InstantPerson entity)
+ {
+ return entity.getTimestamp();
+ }
+ }
+
+ private static class InstantPerson
+ {
+ private final String name;
+ private final Instant timestamp;
+
+ public InstantPerson(String name, Instant timestamp)
+ {
+ this.name = name;
+ this.timestamp = timestamp;
+ }
+
+ public String name()
+ {
+ return this.name;
+ }
+
+ public Instant getTimestamp()
+ {
+ return this.timestamp;
+ }
+ }
+}
diff --git a/gigamap/gigamap/src/test/java/org/eclipse/store/gigamap/indexer/ZonedDateTimeIndexTest.java b/gigamap/gigamap/src/test/java/org/eclipse/store/gigamap/indexer/ZonedDateTimeIndexTest.java
new file mode 100644
index 00000000..e9c16b80
--- /dev/null
+++ b/gigamap/gigamap/src/test/java/org/eclipse/store/gigamap/indexer/ZonedDateTimeIndexTest.java
@@ -0,0 +1,458 @@
+package org.eclipse.store.gigamap.indexer;
+
+/*-
+ * #%L
+ * EclipseStore GigaMap
+ * %%
+ * Copyright (C) 2023 - 2025 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import org.eclipse.store.gigamap.types.BitmapIndices;
+import org.eclipse.store.gigamap.types.GigaMap;
+import org.eclipse.store.gigamap.types.IndexerZonedDateTime;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorage;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ZonedDateTimeIndexTest
+{
+ @TempDir
+ Path tempDir;
+
+ private ZonedDateTimePersonIndex zonedDateTimePersonIndex = new ZonedDateTimePersonIndex();
+
+ @Test
+ void nullTests()
+ {
+ GigaMap map = prepageGigaMap();
+
+ assertEquals(1, map.query(zonedDateTimePersonIndex.is((ZonedDateTime) null)).count());
+
+ assertThrows(IllegalArgumentException.class, () -> map.query(zonedDateTimePersonIndex.before(null)).count());
+ assertThrows(IllegalArgumentException.class, () -> map.query(zonedDateTimePersonIndex.beforeEqual(null)).count());
+ assertThrows(IllegalArgumentException.class, () -> map.query(zonedDateTimePersonIndex.after(null)).count());
+ assertThrows(IllegalArgumentException.class, () -> map.query(zonedDateTimePersonIndex.afterEqual(null)).count());
+
+ assertThrows(IllegalArgumentException.class, () -> map.query(zonedDateTimePersonIndex.between(null, null)).count());
+ assertThrows(IllegalArgumentException.class, () -> map.query(zonedDateTimePersonIndex.between(toUTC(2021, 1, 1, 12, 0, 0), null)).count());
+ assertThrows(IllegalArgumentException.class, () -> map.query(zonedDateTimePersonIndex.between(null, toUTC(2021, 1, 1, 12, 0, 0))).count());
+
+ }
+
+ @Test
+ void between()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(zonedDateTimePersonIndex.between(toUTC(2021, 1, 1, 12, 0, 0), toUTC(2021, 1, 1, 14, 0, 0))).count();
+ assertEquals(3, count);
+ long count1 = map.query(zonedDateTimePersonIndex.between(toUTC(2021, 1, 1, 12, 0, 0), toUTC(2021, 1, 1, 14, 0, 0))).count();
+ assertEquals(3, count1);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(zonedDateTimePersonIndex.between(toUTC(2021, 1, 1, 12, 0, 0), toUTC(2021, 1, 1, 14, 0, 0))).count();
+ assertEquals(3, count);
+ long count1 = newMap.query(zonedDateTimePersonIndex.between(toUTC(2021, 1, 1, 12, 0, 0), toUTC(2021, 1, 1, 14, 0, 0))).count();
+ assertEquals(3, count1);
+ long count2 = newMap.query(zonedDateTimePersonIndex.between(toUTC(2021, 1, 1, 13, 0, 0), toUTC(2021, 1, 1, 14, 0, 0))).count();
+ assertEquals(2, count2);
+ }
+ }
+
+ @Test
+ void after()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(zonedDateTimePersonIndex.after(toUTC(2021, 1, 1, 12, 0, 0))).count();
+ assertEquals(9, count);
+ long count1 = map.query(zonedDateTimePersonIndex.after(toUTC(2021, 1, 1, 13, 0, 0))).count();
+ assertEquals(8, count1);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(zonedDateTimePersonIndex.after(toUTC(2021, 1, 1, 12, 0, 0))).count();
+ assertEquals(9, count);
+ long count1 = newMap.query(zonedDateTimePersonIndex.after(toUTC(2021, 1, 1, 13, 0, 0))).count();
+ assertEquals(8, count1);
+ }
+ }
+
+ @Test
+ void before()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(zonedDateTimePersonIndex.before(toUTC(2021, 1, 1, 12, 0, 1))).count();
+ assertEquals(1, count);
+ long count1 = map.query(zonedDateTimePersonIndex.before(toUTC(2021, 1, 1, 13, 0, 0))).count();
+ assertEquals(1, count1);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(zonedDateTimePersonIndex.before(toUTC(2021, 1, 1, 12, 0, 1))).count();
+ assertEquals(1, count);
+ long count1 = newMap.query(zonedDateTimePersonIndex.before(toUTC(2021, 1, 1, 13, 0, 0))).count();
+ assertEquals(1, count1);
+ }
+ }
+
+ @Test
+ void isSecond()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(zonedDateTimePersonIndex.isSecond(0)).count();
+ assertEquals(10, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(zonedDateTimePersonIndex.isSecond(0)).count();
+ assertEquals(10, count);
+ }
+ }
+
+ @Test
+ void isMinute()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(zonedDateTimePersonIndex.isMinute(0)).count();
+ assertEquals(10, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(zonedDateTimePersonIndex.isMinute(0)).count();
+ assertEquals(10, count);
+ }
+ }
+
+ @Test
+ void isHour()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(zonedDateTimePersonIndex.isHour(12)).count();
+ assertEquals(1, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(zonedDateTimePersonIndex.isHour(12)).count();
+ assertEquals(1, count);
+ }
+ }
+
+ @Test
+ void isTime()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(zonedDateTimePersonIndex.isTime(12, 0, 0)).count();
+ assertEquals(1, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(zonedDateTimePersonIndex.isTime(12, 0, 0)).count();
+ assertEquals(1, count);
+ }
+ }
+
+ @Test
+ void isDay()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(zonedDateTimePersonIndex.isDay(1)).count();
+ assertEquals(10, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(zonedDateTimePersonIndex.isDay(1)).count();
+ assertEquals(10, count);
+ }
+ }
+
+ @Test
+ void isMonth()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(zonedDateTimePersonIndex.isMonth(1)).count();
+ assertEquals(10, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(zonedDateTimePersonIndex.isMonth(1)).count();
+ assertEquals(10, count);
+ }
+ }
+
+ @Test
+ void isYear()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(zonedDateTimePersonIndex.isYear(2021)).count();
+ assertEquals(10, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(zonedDateTimePersonIndex.isYear(2021)).count();
+ assertEquals(10, count);
+ }
+ }
+
+ @Test
+ void isDate()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(zonedDateTimePersonIndex.isDate(2021, 1, 1)).count();
+ assertEquals(10, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(zonedDateTimePersonIndex.isDate(2021, 1, 1)).count();
+ assertEquals(10, count);
+ }
+ }
+
+ @Test
+ void isDateTime()
+ {
+ GigaMap map = prepageGigaMap();
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ long count = map.query(zonedDateTimePersonIndex.isDateTime(2021, 1, 1, 12, 0, 0)).count();
+ assertEquals(1, count);
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ long count = newMap.query(zonedDateTimePersonIndex.isDateTime(2021, 1, 1, 12, 0, 0)).count();
+ assertEquals(1, count);
+ }
+
+ }
+
+ @Test
+ void zonedDateTimePersonTest()
+ {
+ GigaMap map = GigaMap.New();
+
+ BitmapIndices bitmap = map.index().bitmap();
+ bitmap.add(zonedDateTimePersonIndex);
+
+ //generate some data
+ ZonedDateTimePerson person1 = new ZonedDateTimePerson("Alice", toUTC(2000, 1, 1, 0, 0, 0));
+ ZonedDateTimePerson person2 = new ZonedDateTimePerson("Bob", toUTC(1990, 1, 1, 0, 0, 0));
+ ZonedDateTimePerson person3 = new ZonedDateTimePerson("Charlie", toUTC(1980, 1, 1, 0, 0, 0));
+ map.addAll(person1, person2, person3);
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ map.query(zonedDateTimePersonIndex.is(toUTC(2000, 1, 1, 0, 0, 0))).forEach(person -> assertEquals(toUTC(2000, 1, 1, 0, 0, 0), person.getTimestamp()));
+ map.query(zonedDateTimePersonIndex.is(toUTC(1990, 1, 1, 0, 0, 0))).forEach(person -> assertEquals(toUTC(1990, 1, 1, 0, 0, 0), person.getTimestamp()));
+
+ map.query(zonedDateTimePersonIndex.is(toUTC(2000, 1, 1, 0, 0, 0))).and(zonedDateTimePersonIndex.is(toUTC(1990, 1, 1, 0, 0, 0)))
+ .forEach(person -> fail("Should not be reached"));
+
+ List personList = map.query(zonedDateTimePersonIndex.is(toUTC(2000, 1, 1, 0, 0, 0))).or(zonedDateTimePersonIndex.is(toUTC(1990, 1, 1, 0, 0, 0)))
+ .stream().collect(Collectors.toList());
+ assertEquals(2, personList.size());
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ newMap.query(zonedDateTimePersonIndex.is(toUTC(2000, 1, 1, 0, 0, 0))).forEach(person -> assertEquals(toUTC(2000, 1, 1, 0, 0, 0), person.getTimestamp()));
+ newMap.query(zonedDateTimePersonIndex.is(toUTC(1990, 1, 1, 0, 0, 0))).forEach(person -> assertEquals(toUTC(1990, 1, 1, 0, 0, 0), person.getTimestamp()));
+
+ newMap.query(zonedDateTimePersonIndex.is(toUTC(2000, 1, 1, 0, 0, 0))).and(zonedDateTimePersonIndex.is(toUTC(1990, 1, 1, 0, 0, 0)))
+ .forEach(person -> fail("Should not be reached"));
+
+ List personList = newMap.query(zonedDateTimePersonIndex.is(toUTC(2000, 1, 1, 0, 0, 0))).or(zonedDateTimePersonIndex.is(toUTC(1990, 1, 1, 0, 0, 0)))
+ .stream().collect(Collectors.toList());
+ assertEquals(2, personList.size());
+ }
+ }
+
+ @Test
+ void beforeEqualTest()
+ {
+ GigaMap map = GigaMap.New();
+
+ BitmapIndices bitmap = map.index().bitmap();
+ bitmap.add(zonedDateTimePersonIndex);
+
+ //generate some data
+ ZonedDateTimePerson person1 = new ZonedDateTimePerson("Alice", toUTC(2000, 1, 1, 0, 0, 0));
+ ZonedDateTimePerson person2 = new ZonedDateTimePerson("Bob", toUTC(1990, 1, 1, 0, 0, 0));
+ ZonedDateTimePerson person3 = new ZonedDateTimePerson("Charlie", toUTC(1980, 1, 1, 0, 0, 0));
+ map.addAll(person1, person2, person3);
+
+ List list = map.query(zonedDateTimePersonIndex.beforeEqual(toUTC(1990, 1, 1, 0, 0, 0))).toList();
+ assertEquals(2, list.size());
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ List list1 = newMap.query(zonedDateTimePersonIndex.beforeEqual(toUTC(1990, 1, 1, 0, 0, 0))).toList();
+ assertEquals(2, list1.size());
+ list1.forEach(person -> assertNotEquals("Alice", person.name));
+ }
+ }
+
+ @Test
+ void afterEqualTest()
+ {
+ GigaMap map = GigaMap.New();
+
+ BitmapIndices bitmap = map.index().bitmap();
+ bitmap.add(zonedDateTimePersonIndex);
+
+ //generate some data
+ ZonedDateTimePerson person1 = new ZonedDateTimePerson("Alice", toUTC(2000, 1, 1, 0, 0, 0));
+ ZonedDateTimePerson person2 = new ZonedDateTimePerson("Bob", toUTC(1990, 1, 1, 0, 0, 0));
+ ZonedDateTimePerson person3 = new ZonedDateTimePerson("Charlie", toUTC(1980, 1, 1, 0, 0, 0));
+ map.addAll(person1, person2, person3);
+
+ List list = map.query(zonedDateTimePersonIndex.afterEqual(toUTC(1990, 1, 1, 0, 0, 0))).toList();
+ assertEquals(2, list.size());
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(map, tempDir)) {
+ }
+
+ try (EmbeddedStorageManager manager = EmbeddedStorage.start(tempDir)) {
+ GigaMap newMap = (GigaMap) manager.root();
+ List list1 = newMap.query(zonedDateTimePersonIndex.afterEqual(toUTC(1990, 1, 1, 0, 0, 0))).toList();
+ assertEquals(2, list1.size());
+ list1.forEach(person -> assertNotEquals("Charlie", person.name));
+ }
+ }
+
+ @Test
+ void differentTimeZonesTest()
+ {
+ GigaMap map = GigaMap.New();
+
+ BitmapIndices bitmap = map.index().bitmap();
+ bitmap.add(zonedDateTimePersonIndex);
+
+ // Same absolute point in time, different zones: 2021-01-01T12:00 UTC == 2021-01-01T13:00 +01:00
+ ZonedDateTimePerson person1 = new ZonedDateTimePerson("Alice", ZonedDateTime.of(2021, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC));
+ ZonedDateTimePerson person2 = new ZonedDateTimePerson("Bob", ZonedDateTime.of(2021, 1, 1, 13, 0, 0, 0, ZoneId.of("+01:00")));
+ ZonedDateTimePerson person3 = new ZonedDateTimePerson("Charlie", ZonedDateTime.of(2021, 1, 1, 14, 0, 0, 0, ZoneOffset.UTC));
+ map.addAll(person1, person2, person3);
+
+ // Alice and Bob represent the same instant in UTC, so isHour(12) should match both
+ long count = map.query(zonedDateTimePersonIndex.isHour(12)).count();
+ assertEquals(2, count);
+
+ // Only Charlie is at 14:00 UTC
+ long count2 = map.query(zonedDateTimePersonIndex.isHour(14)).count();
+ assertEquals(1, count2);
+ }
+
+ private GigaMap prepageGigaMap()
+ {
+ GigaMap map = GigaMap.New();
+
+ BitmapIndices bitmap = map.index().bitmap();
+ bitmap.add(zonedDateTimePersonIndex);
+
+ ZonedDateTimePerson person1 = new ZonedDateTimePerson("Alice", toUTC(2021, 1, 1, 12, 0, 0));
+ ZonedDateTimePerson person2 = new ZonedDateTimePerson("Bob", toUTC(2021, 1, 1, 13, 0, 0));
+ ZonedDateTimePerson person3 = new ZonedDateTimePerson("Charlie", toUTC(2021, 1, 1, 14, 0, 0));
+ ZonedDateTimePerson person4 = new ZonedDateTimePerson("David", toUTC(2021, 1, 1, 15, 0, 0));
+ ZonedDateTimePerson person5 = new ZonedDateTimePerson("Eve", toUTC(2021, 1, 1, 16, 0, 0));
+ ZonedDateTimePerson person6 = new ZonedDateTimePerson("Frank", toUTC(2021, 1, 1, 17, 0, 0));
+ ZonedDateTimePerson person7 = new ZonedDateTimePerson("Grace", toUTC(2021, 1, 1, 18, 0, 0));
+ ZonedDateTimePerson person8 = new ZonedDateTimePerson("Hank", toUTC(2021, 1, 1, 19, 0, 0));
+ ZonedDateTimePerson person9 = new ZonedDateTimePerson("Ivy", toUTC(2021, 1, 1, 20, 0, 0));
+ ZonedDateTimePerson person10 = new ZonedDateTimePerson("Jack", toUTC(2021, 1, 1, 21, 0, 0));
+ ZonedDateTimePerson person11 = new ZonedDateTimePerson("Karl", null);
+
+ map.addAll(person1, person2, person3, person4, person5, person6, person7, person8, person9, person10, person11);
+
+ return map;
+ }
+
+ private static ZonedDateTime toUTC(int year, int month, int day, int hour, int minute, int second)
+ {
+ return ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneOffset.UTC);
+ }
+
+ private static class ZonedDateTimePersonIndex extends IndexerZonedDateTime.Abstract
+ {
+
+ @Override
+ protected ZonedDateTime getZonedDateTime(ZonedDateTimePerson entity)
+ {
+ return entity.getTimestamp();
+ }
+ }
+
+ private static class ZonedDateTimePerson
+ {
+ private final String name;
+ private final ZonedDateTime timestamp;
+
+ public ZonedDateTimePerson(String name, ZonedDateTime timestamp)
+ {
+ this.name = name;
+ this.timestamp = timestamp;
+ }
+
+ public String name()
+ {
+ return this.name;
+ }
+
+ public ZonedDateTime getTimestamp()
+ {
+ return this.timestamp;
+ }
+ }
+}