Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions base-query/src/main/java/build/base/query/AbstractHeapBasedIndex.java
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ public abstract class AbstractHeapBasedIndex implements Index {
*/
private final Memoizer<Class<?>, Streamable<Field>> uniqueIndexableFunctionFieldsByClass;

/**
* The {@link Indexable} {@link Dynamic} {@code public} {@code static} {@code final} {@link Function}s defined by
* {@link Field}s per known {@link Class} (includes both {@link Unique} and non-{@link Unique} fields).
*/
private final Memoizer<Class<?>, Streamable<Field>> dynamicIndexableFunctionFieldsByClass;

/**
* Constructs an empty {@link AbstractHeapBasedIndex}.
*/
Expand All @@ -111,6 +117,7 @@ protected AbstractHeapBasedIndex() {
this.indexableFunctionFieldsByClass = new Memoizer<>(AbstractHeapBasedIndex::getIndexableFunctionFields);
this.uniqueObjectsByClassFunctionAndKey = new ConcurrentHashMap<>();
this.uniqueIndexableFunctionFieldsByClass = new Memoizer<>(AbstractHeapBasedIndex::getUniqueIndexableFunctionFields);
this.dynamicIndexableFunctionFieldsByClass = new Memoizer<>(AbstractHeapBasedIndex::getDynamicIndexableFunctionFields);
}


Expand Down Expand Up @@ -328,6 +335,118 @@ public void unindex(final Object object) {
}


@Override
public void reindexDynamic(final Object object) {
final var objectClass = object.getClass();

this.dynamicIndexableFunctionFieldsByClass.compute(objectClass).forEach(field -> {
final boolean isUnique = field.getAnnotation(Unique.class) != null;

if (isUnique) {
this.uniqueObjectsByClassFunctionAndKey.compute(objectClass, (_, existingFunctions) -> {
final var functions = existingFunctions == null
? new ConcurrentHashMap<Function<Object, Object>, Pair<ConcurrentHashMap<Object, Object>, ConcurrentHashMap<Object, Object>>>()
: existingFunctions;

try {
field.setAccessible(true);
@SuppressWarnings("unchecked") final var function = (Function<Object, Object>) field.get(null);

functions.compute(function, (_, existingPair) -> {
final var pair = existingPair == null
? Pair.of(new ConcurrentHashMap<>(), new ConcurrentHashMap<Object, Object>())
: existingPair;

// unindex old key using the reverse map — no function invocation needed
final var oldKey = pair.first().remove(object);
if (oldKey != null) {
pair.second().remove(oldKey);
}

// re-index with the current value
try {
final var value = function.apply(object);
final var newKey = value == null ? NULL_OBJECT : value;

final var displaced = pair.second().putIfAbsent(newKey, object);
if (displaced != null && displaced != object) {
throw new IllegalStateException(
"Unique key violation: key [" + value + "] produced by [" + field.getName()
+ "] on [" + objectClass.getName() + "] is already held by [" + displaced + "]");
}

pair.first().put(object, newKey);
} catch (final IllegalStateException e) {
throw e;
} catch (final Throwable e) {
throw new UnsupportedOperationException("Failed to reindex [" + objectClass.getName() + "] as the unique function [" + field.getName() + "] failed to extract a value from the object", e);
}

return pair;
});
} catch (final IllegalAccessException e) {
throw new RuntimeException("Failed to reindex [" + objectClass.getName() + "] as the unique field [" + field.getName() + "] could not be accessed", e);
}

return functions;
});
} else {
this.objectsByClassIndexableFunctionAndValue.compute(objectClass, (_, existingFunctions) -> {
final var functions = existingFunctions == null
? new ConcurrentHashMap<Function<Object, Object>, Pair<ConcurrentHashMap<Object, Object>, ConcurrentHashMap<Object, Set<Object>>>>()
: existingFunctions;

try {
field.setAccessible(true);
@SuppressWarnings("unchecked") final var function = (Function<Object, Object>) field.get(null);

functions.compute(function, (_, existingPair) -> {
final var pair = existingPair == null
? Pair.of(new ConcurrentHashMap<>(), new ConcurrentHashMap<Object, Set<Object>>())
: existingPair;

// unindex old value using the reverse map — no function invocation needed
final var oldValue = pair.first().remove(object);
if (oldValue != null) {
pair.second().compute(oldValue, (_, existingObjects) -> {
if (existingObjects == null) {
return null;
}
existingObjects.remove(object);
return existingObjects.isEmpty() ? null : existingObjects;
});
}

// re-index with the current value
try {
final var value = function.apply(object);
final var newValue = value == null ? NULL_OBJECT : value;

pair.first().put(object, newValue);
pair.second().compute(newValue, (_, existingObjects) -> {
final var objects = existingObjects == null
? ConcurrentHashMap.newKeySet()
: existingObjects;
objects.add(object);
return objects;
});
} catch (final Throwable e) {
throw new UnsupportedOperationException("Failed to reindex [" + objectClass.getName() + "] as the function [" + field.getName() + "] failed to extract a value from the object", e);
}

return pair;
});
} catch (final IllegalAccessException e) {
throw new RuntimeException("Failed to reindex [" + objectClass.getName() + "] as the field [" + field.getName() + "] could not be accessed", e);
}

return functions;
});
}
});
}


@Override
public <T> void add(final Class<T> valueClass, final T value) {
Objects.requireNonNull(valueClass, "The value class must not be null");
Expand Down Expand Up @@ -420,6 +539,23 @@ protected static Streamable<Field> getUniqueIndexableFunctionFields(final Class<
&& Function.class.isAssignableFrom(field.getType())));
}

/**
* Obtains the {@code public static final} {@link Function} {@link Field}s that are annotated as both
* {@link Indexable} and {@link Dynamic} for the specified {@link Class} (includes {@link Unique} fields).
*
* @param indexableClass the {@link Class} of queryable
* @return the {@link Streamable} of {@link Field}s annotated with both {@link Indexable} and {@link Dynamic}
*/
protected static Streamable<Field> getDynamicIndexableFunctionFields(final Class<?> indexableClass) {
return Streamable.of(Introspection.getAllDeclaredFields(indexableClass)
.filter(field -> field.getAnnotation(Indexable.class) != null
&& field.getAnnotation(Dynamic.class) != null
&& Modifier.isPublic(field.getModifiers())
&& Modifier.isStatic(field.getModifiers())
&& Modifier.isFinal(field.getModifiers())
&& Function.class.isAssignableFrom(field.getType())));
}

/**
* An internal {@link Match} implementation.
*
Expand Down
47 changes: 47 additions & 0 deletions base-query/src/main/java/build/base/query/Dynamic.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package build.base.query;

/*-
* #%L
* base.build Query
* %%
* Copyright (C) 2025 Workday Inc
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
* #L%
*/

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.function.Function;

/**
* Marks an {@link Indexable} {@code public} {@code static} {@code final} {@link Function} {@link Field} as
* <i>dynamic</i> — its value for a given object may change after the object is constructed.
*
* <p>When present alongside {@link Indexable} on a {@link Function} field, calls to
* {@link Index#reindexDynamic(Object)} will update only the index entries for {@link Dynamic} fields, leaving
* stable (non-{@link Dynamic}) entries untouched.
*
* @author brian.oliver
* @since Jun-2025
* @see Indexable
* @see Index#reindexDynamic(Object)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Dynamic {

}
18 changes: 18 additions & 0 deletions base-query/src/main/java/build/base/query/Index.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,24 @@ default void reindex(final Object object) {
index(object);
}

/**
* Partially re-indexes the specified {@link Object} by updating only the index entries for
* {@link Indexable} {@link Dynamic} {@link java.util.function.Function} fields.
* <p>
* Use this after mutating state that is reflected by {@link Dynamic}-annotated functions, when stable
* (non-{@link Dynamic}) indexed values are known not to have changed. Stable index entries and
* class-level {@link Indexable} entries are left untouched.
* <p>
* Implementations that do not distinguish dynamic from stable fields fall back to a full {@link #reindex}.
*
* @param object the {@link Object} to partially re-index
* @see #reindex(Object)
* @see Dynamic
*/
default void reindexDynamic(final Object object) {
reindex(object);
}

/**
* Adds the specified value {@link Object} to the index so that is may be returned when the specified {@link Class}
* is queried or matched.
Expand Down
Loading
Loading