Skip to content

Commit fab417a

Browse files
authored
Merge pull request #273 from teaql/smart-list-alignment
Align SmartList semantics and typed query boundaries
2 parents 7f5d5ba + 7acc381 commit fab417a

17 files changed

Lines changed: 483 additions & 100 deletions

File tree

‎examples/school-management/src/main/java/com/example/schoolmanagementservice/App.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ public CommandLineRunner teaQLConsoleStartup(
184184
.comment("Facet SchoolType values including zero-count constants")
185185
.purpose("Verify Java native SQLite Facet semantics")
186186
.executeForList(context);
187-
SmartList<SchoolType> allValues = includeAllFacet.getFacet("schoolTypeFacet");
187+
SmartList<SchoolType> allValues = includeAllFacet.getFacet("schoolTypeFacet").map(SchoolType.class::cast);
188188
require(allValues != null && allValues.size() == 2,
189189
"include-all SchoolType facet did not retain both constants");
190190
Number primaryCount = allValues.get(0).getDynamicProperty("schoolCount");
@@ -205,7 +205,7 @@ public CommandLineRunner teaQLConsoleStartup(
205205
.comment("Facet only SchoolType values matched by the outer School filter")
206206
.purpose("Verify Java native SQLite matched-only Facet semantics")
207207
.executeForList(context);
208-
SmartList<SchoolType> matchedValues = matchedOnlyFacet.getFacet("schoolTypeFacet");
208+
SmartList<SchoolType> matchedValues = matchedOnlyFacet.getFacet("schoolTypeFacet").map(SchoolType.class::cast);
209209
require(matchedValues != null && matchedValues.size() == 1,
210210
"matched-only SchoolType facet must contain one value");
211211
require("PRIMARY".equals(matchedValues.get(0).getCode())

‎examples/security-foundations/src/test/java/io/teaql/examples/security/SqlRelationMaskingTest.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,10 @@ private static SQLEntityDescriptor descriptor(String type, Class<? extends Row>
137137
assertNull("Aggregate results must not overwrite the native namespace", rows.get(0).getProperty("childCount"));
138138
}
139139
else if (shape.equals("facet")) {
140-
SmartList<Entity> parents = rows.getFacet("parentFacet");
140+
SmartList<?> parents = rows.getFacet("parentFacet");
141141
assertEquals(1, parents.size());
142-
assertEquals("Riverside", parents.get(0).getProperty("name"));
142+
assertTrue(parents.get(0) instanceof Entity);
143+
assertEquals("Riverside", ((Entity) parents.get(0)).getProperty("name"));
143144
}
144145
else if (forward) {
145146
Entity loadedParent = rows.get(0).getProperty("parent");

‎examples/standard-order/src/test/java/com/example/requesttreeorderservice/StandardOrderTelemetryTest.java‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,18 @@ private static void verifyPage(SmartList<CustomerOrder> rows) {
109109
assertEquals("Box 1", E.customerOrder(row).getOrderItemList().first().getProductName().eval());
110110
assertEquals(5, ((Number) row.getDynamicProperty("itemCount")).intValue());
111111
var facets = rows.getFacet("statusCounts"); assertEquals(3, facets.size());
112-
int[] counts = {1, 2, 0}, sums = {100, 500, 0};
112+
int[] counts = {1, 2, 0};
113+
Integer[] sums = {100, 500, null};
113114
String[] names = {"New", "Paid", "Cancelled"};
114115
for (int index = 0; index < 3; index++) {
115116
var facet = (OrderStatus) facets.get(index);
116117
assertEquals(names[index], E.orderStatus(facet).getName().eval());
117118
assertEquals(counts[index], ((Number) facet.getDynamicProperty("orderCount")).intValue());
118-
assertEquals(0, new BigDecimal(facet.getDynamicProperty("orderTotalAmount").toString()).compareTo(BigDecimal.valueOf(sums[index])));
119+
if (sums[index] == null) {
120+
assertNull(facet.getDynamicProperty("orderTotalAmount"));
121+
} else {
122+
assertEquals(0, new BigDecimal(facet.getDynamicProperty("orderTotalAmount").toString()).compareTo(BigDecimal.valueOf(sums[index])));
123+
}
119124
}
120125
var unloaded = assertThrows(RuntimeException.class, () -> E.customerOrder(row).getTotalAmount().eval());
121126
assertTrue("an omitted amount must be NotLoaded, not null", unloaded.getClass().getSimpleName().contains("NotLoaded"));

‎teaql-core/src/main/java/io/teaql/core/SmartList.java‎

Lines changed: 158 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import java.util.ArrayList;
44
import java.util.HashMap;
5+
import java.util.LinkedHashMap;
6+
import java.util.LinkedHashSet;
57
import java.util.Iterator;
68
import java.util.List;
79
import java.util.Map;
@@ -15,9 +17,10 @@
1517
import io.teaql.core.utils.MapUtil;
1618
import io.teaql.core.utils.ObjectUtil;
1719

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

2225
List<T> data;
2326

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

29-
Map<String, SmartList> facets;
32+
Map<String, SmartList<?>> facets;
33+
private Map<String, Object> summary;
34+
private boolean loaded = true;
3035

3136
private final boolean immutable;
3237

@@ -48,17 +53,24 @@ public SmartList(List<T> data) {
4853
}
4954
}
5055

51-
private SmartList(boolean immutable) {
56+
private SmartList(boolean immutable, boolean loaded) {
5257
this.immutable = immutable;
58+
this.loaded = loaded;
5359
data = List.of();
5460
}
5561

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

68+
/** Unloaded relation marker; successful empty queries use a loaded empty list. */
69+
@SuppressWarnings("unchecked")
70+
public static <T> SmartList<T> unloaded() { return (SmartList<T>) UNLOADED; }
71+
72+
public boolean isLoaded() { return loaded; }
73+
6274
@FrameworkInternal
6375
public static <T extends Entity> SmartList<T> takeOwnership(List<T> data) {
6476
SmartList<T> result = new SmartList<>();
@@ -93,7 +105,20 @@ public <R> Map<R, T> identityMap(Function<T, R> key) {
93105
}
94106

95107
public Map<Long, T> mapById() {
96-
return identityMap(Entity::getId);
108+
Map<Long, T> result = new LinkedHashMap<>();
109+
for (T item : data) {
110+
Long id = entity(item).getId();
111+
if (id != null) result.put(id, item);
112+
}
113+
return result;
114+
}
115+
116+
public List<Long> ids() { return toList(item -> entity(item).getId()); }
117+
public List<Long> versions() { return toList(item -> entity(item).getVersion()); }
118+
119+
private static Entity entity(Object item) {
120+
if (item instanceof Entity entity) return entity;
121+
throw new IllegalStateException("Entity identity operations require entity rows");
97122
}
98123

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

217+
public T getOrNull(int index) { return index >= 0 && index < size() ? data.get(index) : null; }
218+
public T last() { return isEmpty() ? null : data.get(size() - 1); }
219+
192220
public SmartList<T> save(UserContext userContext) {
193221
userContext.saveGraph(this);
194222
return this;
195223
}
196224

197225

198-
public int getTotalCount() {
199-
if (ObjectUtil.isEmpty(aggregationResults)) {
200-
return size();
201-
}
202-
Map<String, Object> numberProps = aggregationNumberProperties();
203-
if (numberProps.isEmpty()) {
204-
return size();
205-
}
206-
Object count = numberProps.get(pageCountAlias);
207-
if (count instanceof Number intCount) {
208-
return intCount.intValue();
226+
/** Exact source-query total, or null when no count was requested. */
227+
public Long getExactTotalCount() {
228+
Map<String, Object> properties = aggregationProperties();
229+
if (!properties.containsKey(pageCountAlias)) return null;
230+
Object count = properties.get(pageCountAlias);
231+
try {
232+
if (!(count instanceof Number)) throw new ArithmeticException();
233+
long exact = new java.math.BigDecimal(count.toString()).longValueExact();
234+
if (exact < 0) throw new ArithmeticException();
235+
return exact;
236+
} catch (ArithmeticException | NumberFormatException invalid) {
237+
throw new IllegalStateException("List count must be an exact non-negative long", invalid);
209238
}
210-
throw new IllegalStateException("Number prop is expected a number, but it is now a " + count.getClass().getSimpleName());
239+
}
211240

212-
//return aggregationResults.get(0).toInt();
241+
public long getTotalCountOrSize() {
242+
Long exact = getExactTotalCount();
243+
return exact == null ? size() : exact;
213244
}
214245

246+
/** @deprecated Use getExactTotalCount, or the explicitly named getTotalCountOrSize fallback. */
247+
@Deprecated
248+
public int getTotalCount() { return Math.toIntExact(getTotalCountOrSize()); }
249+
215250
public Map<String, Object> aggregationProperties(Class<?> clazz) {
216251
if (ObjectUtil.isEmpty(aggregationResults)) {
217252
return MapUtil.empty();
218253
}
219254
Map<String, Object> result = MapUtil.createMap(HashMap.class);
220255
getAggregationResults().forEach(aggregationResult -> {
256+
if (aggregationResult.getData() == null) return;
221257

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

225261
resultList.forEach(map -> {
226262
map.entrySet().forEach(stringObjectEntry -> {
227263

228-
if (clazz.isAssignableFrom(stringObjectEntry.getValue().getClass())) {
264+
if (stringObjectEntry.getValue() == null ? clazz == Object.class
265+
: clazz.isInstance(stringObjectEntry.getValue())) {
229266
result.put(stringObjectEntry.getKey(), stringObjectEntry.getValue());
230267
}
231268

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

239-
public void addFacet(String name, SmartList facet) {
276+
public void addFacet(String name, SmartList<?> facet) {
240277
ensureMutable();
241278
mutableFacets().put(name, facet);
242279
}
243280

244-
public Map<String, SmartList> getFacets() {
281+
public Map<String, SmartList<?>> getFacets() {
245282
return mutableFacets();
246283
}
247284

248-
public void setFacets(Map<String, SmartList> facets) {
285+
public void setFacets(Map<String, ? extends SmartList<?>> facets) {
249286
ensureMutable();
250-
this.facets = facets;
287+
this.facets = facets == null ? null : new LinkedHashMap<>(facets);
251288
}
252289

253-
public SmartList getFacet(String name) {
290+
public SmartList<?> getFacet(String name) {
254291
return facets == null ? null : facets.get(name);
255292
}
256293

257-
public SmartList removeFacet(String name) {
294+
public SmartList<?> removeFacet(String name) {
258295
ensureMutable();
259296
return facets == null ? null : facets.remove(name);
260297
}
@@ -264,15 +301,30 @@ public void clearFacets() {
264301
if (facets != null) facets.clear();
265302
}
266303

304+
public Map<String, SmartList<?>> takeFacets() {
305+
ensureMutable();
306+
Map<String, SmartList<?>> result = facets == null ? new LinkedHashMap<>() : facets;
307+
facets = null;
308+
return result;
309+
}
310+
311+
/** Summary population stays package-private; consumers receive a detached value tree. */
312+
void setSummary(Map<String, Object> values) {
313+
ensureMutable();
314+
summary = copyMap(values);
315+
}
316+
317+
public Map<String, Object> getSummary() { return copyMap(summary); }
318+
267319
private List<AggregationResult> mutableAggregationResults() {
268320
if (immutable) return List.of();
269321
if (aggregationResults == null) aggregationResults = new ArrayList<>();
270322
return aggregationResults;
271323
}
272324

273-
private Map<String, SmartList> mutableFacets() {
325+
private Map<String, SmartList<?>> mutableFacets() {
274326
if (immutable) return Map.of();
275-
if (facets == null) facets = new HashMap<>();
327+
if (facets == null) facets = new LinkedHashMap<>();
276328
return facets;
277329
}
278330

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

357+
/** One output row per source row, with an isolated query metadata snapshot. */
358+
public <R> SmartList<R> map(Function<? super T, ? extends R> mapper) {
359+
SmartList<R> result = new SmartList<>(toList(mapper::apply));
360+
result.loaded = loaded;
361+
result.pageCountAlias = pageCountAlias;
362+
result.summary = copyMap(summary);
363+
if (aggregationResults != null) {
364+
result.aggregationResults = new ArrayList<>();
365+
for (AggregationResult aggregation : aggregationResults) {
366+
AggregationResult copy = new AggregationResult();
367+
copy.setName(aggregation.getName());
368+
List<AggregationItem> items = new ArrayList<>();
369+
if (aggregation.getData() != null) for (AggregationItem item : aggregation.getData()) {
370+
AggregationItem copied = new AggregationItem();
371+
item.getValues().forEach((key, value) -> copied.addValue(key, copyValue(value)));
372+
item.getDimensions().forEach((key, value) -> copied.addDimension(key, copyValue(value)));
373+
items.add(copied);
374+
}
375+
copy.setData(items);
376+
result.aggregationResults.add(copy);
377+
}
378+
}
379+
if (facets != null) {
380+
result.facets = new LinkedHashMap<>();
381+
facets.forEach((name, facet) -> result.facets.put(name, facet.map(SmartList::copyValue)));
382+
}
383+
return result;
384+
}
385+
386+
public SmartList<T> filter(Predicate<? super T> predicate) {
387+
SmartList<T> result = new SmartList<>(data.stream().filter(predicate).toList());
388+
result.loaded = loaded;
389+
return result;
390+
}
391+
392+
public <R> SmartList<R> flatMap(Function<? super T, ? extends Iterable<R>> mapper) {
393+
SmartList<R> result = new SmartList<>();
394+
for (T item : data) for (R value : mapper.apply(item)) result.add(value);
395+
result.loaded = loaded;
396+
return result;
397+
}
398+
399+
public <K> void mergeBy(Iterable<T> incoming, Function<? super T, K> key) {
400+
ensureMutable();
401+
Map<K, Integer> positions = new HashMap<>();
402+
for (int index = 0; index < size(); index++) positions.put(key.apply(data.get(index)), index);
403+
for (T item : incoming) {
404+
K itemKey = key.apply(item);
405+
Integer position = positions.get(itemKey);
406+
if (position != null) set(position, item);
407+
else { positions.put(itemKey, size()); add(item); }
408+
}
409+
}
410+
411+
private static Map<String, Object> copyMap(Map<String, Object> values) {
412+
Map<String, Object> result = new LinkedHashMap<>();
413+
if (values != null) values.forEach((key, value) -> result.put(key, copyValue(value)));
414+
return result;
415+
}
416+
417+
private static Object copyValue(Object value) {
418+
if (value instanceof Map<?, ?> map) {
419+
Map<Object, Object> copy = new LinkedHashMap<>();
420+
map.forEach((key, item) -> copy.put(key, copyValue(item)));
421+
return copy;
422+
}
423+
if (value instanceof List<?> list) {
424+
List<Object> copy = new ArrayList<>();
425+
list.forEach(item -> copy.add(copyValue(item)));
426+
return copy;
427+
}
428+
if (value instanceof Set<?> set) {
429+
Set<Object> copy = new LinkedHashSet<>();
430+
set.forEach(item -> copy.add(copyValue(item)));
431+
return copy;
432+
}
433+
return value;
434+
}
435+
305436
private void ensureMutable() {
306437
if (immutable) {
307438
throw new UnsupportedOperationException("A shared empty SmartList is immutable");

0 commit comments

Comments
 (0)