Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ public CommandLineRunner teaQLConsoleStartup(
.comment("Facet SchoolType values including zero-count constants")
.purpose("Verify Java native SQLite Facet semantics")
.executeForList(context);
SmartList<SchoolType> allValues = includeAllFacet.getFacet("schoolTypeFacet");
SmartList<SchoolType> allValues = includeAllFacet.getFacet("schoolTypeFacet").map(SchoolType.class::cast);
require(allValues != null && allValues.size() == 2,
"include-all SchoolType facet did not retain both constants");
Number primaryCount = allValues.get(0).getDynamicProperty("schoolCount");
Expand All @@ -205,7 +205,7 @@ public CommandLineRunner teaQLConsoleStartup(
.comment("Facet only SchoolType values matched by the outer School filter")
.purpose("Verify Java native SQLite matched-only Facet semantics")
.executeForList(context);
SmartList<SchoolType> matchedValues = matchedOnlyFacet.getFacet("schoolTypeFacet");
SmartList<SchoolType> matchedValues = matchedOnlyFacet.getFacet("schoolTypeFacet").map(SchoolType.class::cast);
require(matchedValues != null && matchedValues.size() == 1,
"matched-only SchoolType facet must contain one value");
require("PRIMARY".equals(matchedValues.get(0).getCode())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,10 @@ private static SQLEntityDescriptor descriptor(String type, Class<? extends Row>
assertNull("Aggregate results must not overwrite the native namespace", rows.get(0).getProperty("childCount"));
}
else if (shape.equals("facet")) {
SmartList<Entity> parents = rows.getFacet("parentFacet");
SmartList<?> parents = rows.getFacet("parentFacet");
assertEquals(1, parents.size());
assertEquals("Riverside", parents.get(0).getProperty("name"));
assertTrue(parents.get(0) instanceof Entity);
assertEquals("Riverside", ((Entity) parents.get(0)).getProperty("name"));
}
else if (forward) {
Entity loadedParent = rows.get(0).getProperty("parent");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,18 @@ private static void verifyPage(SmartList<CustomerOrder> rows) {
assertEquals("Box 1", E.customerOrder(row).getOrderItemList().first().getProductName().eval());
assertEquals(5, ((Number) row.getDynamicProperty("itemCount")).intValue());
var facets = rows.getFacet("statusCounts"); assertEquals(3, facets.size());
int[] counts = {1, 2, 0}, sums = {100, 500, 0};
int[] counts = {1, 2, 0};
Integer[] sums = {100, 500, null};
String[] names = {"New", "Paid", "Cancelled"};
for (int index = 0; index < 3; index++) {
var facet = (OrderStatus) facets.get(index);
assertEquals(names[index], E.orderStatus(facet).getName().eval());
assertEquals(counts[index], ((Number) facet.getDynamicProperty("orderCount")).intValue());
assertEquals(0, new BigDecimal(facet.getDynamicProperty("orderTotalAmount").toString()).compareTo(BigDecimal.valueOf(sums[index])));
if (sums[index] == null) {
assertNull(facet.getDynamicProperty("orderTotalAmount"));
} else {
assertEquals(0, new BigDecimal(facet.getDynamicProperty("orderTotalAmount").toString()).compareTo(BigDecimal.valueOf(sums[index])));
}
}
var unloaded = assertThrows(RuntimeException.class, () -> E.customerOrder(row).getTotalAmount().eval());
assertTrue("an omitted amount must be NotLoaded, not null", unloaded.getClass().getSimpleName().contains("NotLoaded"));
Expand Down
185 changes: 158 additions & 27 deletions teaql-core/src/main/java/io/teaql/core/SmartList.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
Expand All @@ -15,9 +17,10 @@
import io.teaql.core.utils.MapUtil;
import io.teaql.core.utils.ObjectUtil;

public class SmartList<T extends Entity> implements Iterable<T> {
public class SmartList<T> implements Iterable<T> {
private static final Map<Class<?>, SmartList<?>> TYPED_EMPTY =
new java.util.concurrent.ConcurrentHashMap<>();
private static final SmartList<?> UNLOADED = new SmartList<>(true, false);

List<T> data;

Expand All @@ -26,7 +29,9 @@ public class SmartList<T extends Entity> implements Iterable<T> {
@FrameworkInternal("Named page total metadata, independent of physical count SQL")
public void __internalSetPageCountAlias(String alias) { ensureMutable(); pageCountAlias = alias; }

Map<String, SmartList> facets;
Map<String, SmartList<?>> facets;
private Map<String, Object> summary;
private boolean loaded = true;

private final boolean immutable;

Expand All @@ -48,17 +53,24 @@ public SmartList(List<T> data) {
}
}

private SmartList(boolean immutable) {
private SmartList(boolean immutable, boolean loaded) {
this.immutable = immutable;
this.loaded = loaded;
data = List.of();
}

@SuppressWarnings("unchecked")
public static <T extends Entity> SmartList<T> empty(Class<? extends T> entityType) {
if (entityType == null) throw new IllegalArgumentException("entityType must not be null");
return (SmartList<T>) TYPED_EMPTY.computeIfAbsent(entityType, ignored -> new SmartList<>(true));
return (SmartList<T>) TYPED_EMPTY.computeIfAbsent(entityType, ignored -> new SmartList<>(true, true));
}

/** Unloaded relation marker; successful empty queries use a loaded empty list. */
@SuppressWarnings("unchecked")
public static <T> SmartList<T> unloaded() { return (SmartList<T>) UNLOADED; }

public boolean isLoaded() { return loaded; }

@FrameworkInternal
public static <T extends Entity> SmartList<T> takeOwnership(List<T> data) {
SmartList<T> result = new SmartList<>();
Expand Down Expand Up @@ -93,7 +105,20 @@ public <R> Map<R, T> identityMap(Function<T, R> key) {
}

public Map<Long, T> mapById() {
return identityMap(Entity::getId);
Map<Long, T> result = new LinkedHashMap<>();
for (T item : data) {
Long id = entity(item).getId();
if (id != null) result.put(id, item);
}
return result;
}

public List<Long> ids() { return toList(item -> entity(item).getId()); }
public List<Long> versions() { return toList(item -> entity(item).getVersion()); }

private static Entity entity(Object item) {
if (item instanceof Entity entity) return entity;
throw new IllegalStateException("Entity identity operations require entity rows");
}

public <R> Map<R, List<T>> groupBy(Function<T, R> key) {
Expand Down Expand Up @@ -189,43 +214,55 @@ public T get(int index) {
return data.get(index);
}

public T getOrNull(int index) { return index >= 0 && index < size() ? data.get(index) : null; }
public T last() { return isEmpty() ? null : data.get(size() - 1); }

public SmartList<T> save(UserContext userContext) {
userContext.saveGraph(this);
return this;
}


public int getTotalCount() {
if (ObjectUtil.isEmpty(aggregationResults)) {
return size();
}
Map<String, Object> numberProps = aggregationNumberProperties();
if (numberProps.isEmpty()) {
return size();
}
Object count = numberProps.get(pageCountAlias);
if (count instanceof Number intCount) {
return intCount.intValue();
/** Exact source-query total, or null when no count was requested. */
public Long getExactTotalCount() {
Map<String, Object> properties = aggregationProperties();
if (!properties.containsKey(pageCountAlias)) return null;
Object count = properties.get(pageCountAlias);
try {
if (!(count instanceof Number)) throw new ArithmeticException();
long exact = new java.math.BigDecimal(count.toString()).longValueExact();
if (exact < 0) throw new ArithmeticException();
return exact;
} catch (ArithmeticException | NumberFormatException invalid) {
throw new IllegalStateException("List count must be an exact non-negative long", invalid);
}
throw new IllegalStateException("Number prop is expected a number, but it is now a " + count.getClass().getSimpleName());
}

//return aggregationResults.get(0).toInt();
public long getTotalCountOrSize() {
Long exact = getExactTotalCount();
return exact == null ? size() : exact;
}

/** @deprecated Use getExactTotalCount, or the explicitly named getTotalCountOrSize fallback. */
@Deprecated
public int getTotalCount() { return Math.toIntExact(getTotalCountOrSize()); }

public Map<String, Object> aggregationProperties(Class<?> clazz) {
if (ObjectUtil.isEmpty(aggregationResults)) {
return MapUtil.empty();
}
Map<String, Object> result = MapUtil.createMap(HashMap.class);
getAggregationResults().forEach(aggregationResult -> {
if (aggregationResult.getData() == null) return;

//Map s=aggregationResult.toSimpleMap();
List<Map<String, Object>> resultList = aggregationResult.valueList();

resultList.forEach(map -> {
map.entrySet().forEach(stringObjectEntry -> {

if (clazz.isAssignableFrom(stringObjectEntry.getValue().getClass())) {
if (stringObjectEntry.getValue() == null ? clazz == Object.class
: clazz.isInstance(stringObjectEntry.getValue())) {
result.put(stringObjectEntry.getKey(), stringObjectEntry.getValue());
}

Expand All @@ -236,25 +273,25 @@ public Map<String, Object> aggregationProperties(Class<?> clazz) {
return result;
}

public void addFacet(String name, SmartList facet) {
public void addFacet(String name, SmartList<?> facet) {
ensureMutable();
mutableFacets().put(name, facet);
}

public Map<String, SmartList> getFacets() {
public Map<String, SmartList<?>> getFacets() {
return mutableFacets();
}

public void setFacets(Map<String, SmartList> facets) {
public void setFacets(Map<String, ? extends SmartList<?>> facets) {
ensureMutable();
this.facets = facets;
this.facets = facets == null ? null : new LinkedHashMap<>(facets);
}

public SmartList getFacet(String name) {
public SmartList<?> getFacet(String name) {
return facets == null ? null : facets.get(name);
}

public SmartList removeFacet(String name) {
public SmartList<?> removeFacet(String name) {
ensureMutable();
return facets == null ? null : facets.remove(name);
}
Expand All @@ -264,15 +301,30 @@ public void clearFacets() {
if (facets != null) facets.clear();
}

public Map<String, SmartList<?>> takeFacets() {
ensureMutable();
Map<String, SmartList<?>> result = facets == null ? new LinkedHashMap<>() : facets;
facets = null;
return result;
}

/** Summary population stays package-private; consumers receive a detached value tree. */
void setSummary(Map<String, Object> values) {
ensureMutable();
summary = copyMap(values);
}

public Map<String, Object> getSummary() { return copyMap(summary); }

private List<AggregationResult> mutableAggregationResults() {
if (immutable) return List.of();
if (aggregationResults == null) aggregationResults = new ArrayList<>();
return aggregationResults;
}

private Map<String, SmartList> mutableFacets() {
private Map<String, SmartList<?>> mutableFacets() {
if (immutable) return Map.of();
if (facets == null) facets = new HashMap<>();
if (facets == null) facets = new LinkedHashMap<>();
return facets;
}

Expand Down Expand Up @@ -302,6 +354,85 @@ public boolean removeIf(Predicate<T> filter) {
return data.removeIf(filter);
}

/** One output row per source row, with an isolated query metadata snapshot. */
public <R> SmartList<R> map(Function<? super T, ? extends R> mapper) {
SmartList<R> result = new SmartList<>(toList(mapper::apply));
result.loaded = loaded;
result.pageCountAlias = pageCountAlias;
result.summary = copyMap(summary);
if (aggregationResults != null) {
result.aggregationResults = new ArrayList<>();
for (AggregationResult aggregation : aggregationResults) {
AggregationResult copy = new AggregationResult();
copy.setName(aggregation.getName());
List<AggregationItem> items = new ArrayList<>();
if (aggregation.getData() != null) for (AggregationItem item : aggregation.getData()) {
AggregationItem copied = new AggregationItem();
item.getValues().forEach((key, value) -> copied.addValue(key, copyValue(value)));
item.getDimensions().forEach((key, value) -> copied.addDimension(key, copyValue(value)));
items.add(copied);
}
copy.setData(items);
result.aggregationResults.add(copy);
}
}
if (facets != null) {
result.facets = new LinkedHashMap<>();
facets.forEach((name, facet) -> result.facets.put(name, facet.map(SmartList::copyValue)));
}
return result;
}

public SmartList<T> filter(Predicate<? super T> predicate) {
SmartList<T> result = new SmartList<>(data.stream().filter(predicate).toList());
result.loaded = loaded;
return result;
}

public <R> SmartList<R> flatMap(Function<? super T, ? extends Iterable<R>> mapper) {
SmartList<R> result = new SmartList<>();
for (T item : data) for (R value : mapper.apply(item)) result.add(value);
result.loaded = loaded;
return result;
}

public <K> void mergeBy(Iterable<T> incoming, Function<? super T, K> key) {
ensureMutable();
Map<K, Integer> positions = new HashMap<>();
for (int index = 0; index < size(); index++) positions.put(key.apply(data.get(index)), index);
for (T item : incoming) {
K itemKey = key.apply(item);
Integer position = positions.get(itemKey);
if (position != null) set(position, item);
else { positions.put(itemKey, size()); add(item); }
}
}

private static Map<String, Object> copyMap(Map<String, Object> values) {
Map<String, Object> result = new LinkedHashMap<>();
if (values != null) values.forEach((key, value) -> result.put(key, copyValue(value)));
return result;
}

private static Object copyValue(Object value) {
if (value instanceof Map<?, ?> map) {
Map<Object, Object> copy = new LinkedHashMap<>();
map.forEach((key, item) -> copy.put(key, copyValue(item)));
return copy;
}
if (value instanceof List<?> list) {
List<Object> copy = new ArrayList<>();
list.forEach(item -> copy.add(copyValue(item)));
return copy;
}
if (value instanceof Set<?> set) {
Set<Object> copy = new LinkedHashSet<>();
set.forEach(item -> copy.add(copyValue(item)));
return copy;
}
return value;
}

private void ensureMutable() {
if (immutable) {
throw new UnsupportedOperationException("A shared empty SmartList is immutable");
Expand Down
Loading
Loading