From 77f4765cf9fbcd944ca914cb2f4fdbd23aaf3552 Mon Sep 17 00:00:00 2001
From: Leon van Zantvoort
Methods named {@code where} are type-safe and restrict metamodel paths to the root table's entity graph. - * Methods named {@code whereAny} accept metamodel paths from any table, including manually added joins.
+ *The {@code where} methods are type-safe and restrict metamodel paths to the root table's entity graph. + * Predicates for other tables, including manually added joins, can be combined using the + * {@link PredicateBuilder#andAny} and {@link PredicateBuilder#orAny} combinators or expressed as custom string + * template expressions.
* *{@code
diff --git a/storm-kotlin/src/main/kotlin/st/orm/template/PredicateBuilder.kt b/storm-kotlin/src/main/kotlin/st/orm/template/PredicateBuilder.kt
index d87864c86..35c55cff0 100644
--- a/storm-kotlin/src/main/kotlin/st/orm/template/PredicateBuilder.kt
+++ b/storm-kotlin/src/main/kotlin/st/orm/template/PredicateBuilder.kt
@@ -31,13 +31,12 @@ import st.orm.Data
* ```kotlin
* val users = userRepository
* .select()
- * .where { predicate ->
- * predicate
- * .where(User_.active eq true)
- * .and(predicate.where(User_.email.isNotNull()))
- * .or(predicate.where(User_.role eq "admin"))
+ * .whereBuilder {
+ * (User_.active eq true)
+ * .and(User_.email.isNotNull())
+ * .or(User_.role eq "admin")
* }
- * .getResultList()
+ * .resultList
* ```
*
* @param T the type of the table being queried.
diff --git a/storm-kotlin/src/main/kotlin/st/orm/template/QueryBuilder.kt b/storm-kotlin/src/main/kotlin/st/orm/template/QueryBuilder.kt
index 9d3cdb0c2..523f7245e 100644
--- a/storm-kotlin/src/main/kotlin/st/orm/template/QueryBuilder.kt
+++ b/storm-kotlin/src/main/kotlin/st/orm/template/QueryBuilder.kt
@@ -48,7 +48,7 @@ import kotlin.reflect.KClass
* .where(User_.address.city.name eq "Sunnyvale")
* .orderBy(User_.email)
* .limit(10)
- * .getResultList()
+ * .resultList
* ```
*
* ## Example: Join with reified type arguments
@@ -56,7 +56,7 @@ import kotlin.reflect.KClass
* val users = userRepository
* .select()
* .innerJoin().on()
- * .getResultList()
+ * .resultList
* ```
*
* ## Example: Delete with WHERE clause
diff --git a/storm-kotlin/src/main/kotlin/st/orm/template/QueryTemplate.kt b/storm-kotlin/src/main/kotlin/st/orm/template/QueryTemplate.kt
index 7feab3a85..32224aecb 100644
--- a/storm-kotlin/src/main/kotlin/st/orm/template/QueryTemplate.kt
+++ b/storm-kotlin/src/main/kotlin/st/orm/template/QueryTemplate.kt
@@ -38,7 +38,7 @@ import kotlin.reflect.KClass
* ```kotlin
* val users = orm.selectFrom(User::class)
* .where(User_.name eq "Alice")
- * .getResultList()
+ * .resultList
* ```
*
* @see ORMTemplate
diff --git a/storm-kotlin/src/main/kotlin/st/orm/template/TypedJoinBuilder.kt b/storm-kotlin/src/main/kotlin/st/orm/template/TypedJoinBuilder.kt
index 7ec5f3a7c..700978e16 100644
--- a/storm-kotlin/src/main/kotlin/st/orm/template/TypedJoinBuilder.kt
+++ b/storm-kotlin/src/main/kotlin/st/orm/template/TypedJoinBuilder.kt
@@ -31,7 +31,7 @@ import kotlin.reflect.KClass
* val users = userRepository
* .select()
* .innerJoin(Order::class).on(User::class)
- * .getResultList()
+ * .resultList
* ```
*
* The same join can be expressed with reified type arguments:
@@ -39,7 +39,7 @@ import kotlin.reflect.KClass
* val users = userRepository
* .select()
* .innerJoin().on()
- * .getResultList()
+ * .resultList
* ```
*
* @param T the type of the table being queried.
diff --git a/storm-kotlin/src/main/kotlin/st/orm/template/WhereBuilder.kt b/storm-kotlin/src/main/kotlin/st/orm/template/WhereBuilder.kt
index 0ff9972fc..ebba0f75a 100644
--- a/storm-kotlin/src/main/kotlin/st/orm/template/WhereBuilder.kt
+++ b/storm-kotlin/src/main/kotlin/st/orm/template/WhereBuilder.kt
@@ -25,29 +25,28 @@ import st.orm.template.TemplateString.Companion.raw
/**
* A builder for constructing the WHERE clause of a query, providing type-safe predicate construction.
*
- * The `WhereBuilder` is passed to the lambda argument of [QueryBuilder.where] and offers methods for matching by
- * primary key, record, ref, metamodel path, or custom template string expressions. Each method returns a
- * [PredicateBuilder] that can be further composed using `and()` and `or()` combinators.
+ * The `WhereBuilder` is the receiver of the lambda passed to [QueryBuilder.whereBuilder] and offers methods for
+ * matching by primary key, record, ref, metamodel path, or custom template string expressions. Each method returns
+ * a [PredicateBuilder] that can be further composed using `and()` and `or()` combinators.
*
- * Methods named `where` are type-safe and restrict metamodel paths to the root table's entity graph.
- * Methods named `whereAny` accept metamodel paths from any table, including manually added joins.
+ * The `where` methods are type-safe and restrict metamodel paths to the root table's entity graph. Predicates for
+ * other tables, including manually added joins, can be combined using the [PredicateBuilder.andAny] and
+ * [PredicateBuilder.orAny] combinators or expressed as custom template strings.
*
* ## Example
* ```kotlin
* val users = userRepository
* .select()
- * .where { predicate ->
- * predicate
- * .where(User_.active eq true)
- * .and(predicate.where(User_.address.city.name eq "Sunnyvale"))
+ * .whereBuilder {
+ * (User_.active eq true) and (User_.address.city.name eq "Sunnyvale")
* }
- * .getResultList()
+ * .resultList
* ```
*
* @param T the type of the table being queried.
* @param R the type of the result.
* @param ID the type of the primary key.
- * @see QueryBuilder.where
+ * @see QueryBuilder.whereBuilder
* @see PredicateBuilder
*/
@SqlDsl
From 54f6f27e72218b4bce2b082ac6e59f6a8be8d9c9 Mon Sep 17 00:00:00 2001
From: Leon van Zantvoort
Date: Tue, 11 Aug 2026 13:10:42 +0200
Subject: [PATCH 2/4] feat!: predicate combinators inherit the query root,
removing the Any twins
andAny/orAny were the predicate-level Any variants #372 left behind. The
combinators now follow the same model as the clauses: the query root decides
what combines.
- Java: every predicate carries the builder's root, so and/or absorb the Any
twins with a ? extends T parameter; a narrow builder combines within the
root graph and a join widens the root. The twins had no constructible
argument with a foreign root left.
- Kotlin: and/or move into the whereBuilder { } scope as member extensions
bound to the scope's root, so a narrow scope combines within the root graph
and a widened scope combines predicates across joined entities with the
same syntax. Top-level and/or extensions combine same-rooted predicates
outside a scope.
- Core keeps a permissive ? extends Data parameter: it is the engine the
Kotlin bridge feeds mixed-root predicates through, and query-time
validation reports paths on entities outside the query.
---
CHANGELOG.md | 1 +
.../orm/core/template/PredicateBuilder.java | 32 ++--------
.../core/template/impl/QueryBuilderImpl.java | 18 +-----
.../QueryBuilderPredicateIntegrationTest.java | 12 ++--
.../st/orm/template/PredicateBuilder.java | 38 +++---------
.../java/st/orm/template/WhereBuilder.java | 5 +-
.../orm/template/impl/QueryBuilderImpl.java | 18 ++----
.../st/orm/template/QueryBuilderTest.java | 10 ++--
.../st/orm/template/PredicateBuilder.kt | 59 +++----------------
.../kotlin/st/orm/template/QueryBuilder.kt | 24 ++++++++
.../kotlin/st/orm/template/WhereBuilder.kt | 30 +++++++++-
.../template/impl/PredicateBuilderFactory.kt | 24 ++++++++
.../st/orm/template/impl/QueryBuilderImpl.kt | 12 ++--
.../st/orm/template/EntityRepositoryTest.kt | 12 ++--
.../kotlin/st/orm/template/ORMTemplateTest.kt | 10 ++--
.../st/orm/template/QueryBuilderTest.kt | 22 +++++--
16 files changed, 149 insertions(+), 178 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 355684b29..8409dd25d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -31,6 +31,7 @@ A join widens the query: from the join onward, every clause accepts paths from a
```
In Java the lambda escalation goes with it: `.where(it -> it.whereAny(UserRole_.user, EQUALS, user))` becomes the typed overload `.where(UserRole_.user, EQUALS, user)`. A path on an entity that is not part of the query fails when the query is built, with the error naming the entity, the query root, and — when the table appears more than once — the paths that pin it.
+- The predicate combinators follow the same model: `andAny` and `orAny` are removed from every API, and `and`/`or` inherit the query root. In Java every predicate carries the builder's root, so the plain calls absorb the `Any` twins as a rename per call site. In Kotlin, `and`/`or` live in the `whereBuilder { }` scope: a narrow scope combines predicates within the root graph, and a join widens the root so the same expression combines predicates across joined entities — `(Pet_.name eq "Leo") and (Owner_.lastName eq "Davis")` after joining `Owner`. Outside a builder scope, the top-level `and`/`or` extensions combine predicates that share a root.
- Added `widen()` and `narrow(rootType)`, the two directions of the model made explicit: `widen()` widens without a join, admitting short-form references to entities of the query's graph on a query that joins nothing, and `narrow` restores the root after a join for the operations defined relative to it, verified against the query's FROM table. Those are `resultGroupedBy`, which types the map key, and `scroll`, whose key has to identify one row of the root — a unique key on a joined table does not, so scrolling a joined query narrows first.
- Renamed `typed(pkType)` to `typedId(pkType)` — it types the erased primary-key parameter, while `narrow` types the root — and added its missing null check.
- `fetch(...)` comes right after `select()`, before any join, enforced at compile time: resolving references is defined relative to the root, and a join widens the builder past it.
diff --git a/storm-core/src/main/java/st/orm/core/template/PredicateBuilder.java b/storm-core/src/main/java/st/orm/core/template/PredicateBuilder.java
index 05b17d050..c92b88a8a 100644
--- a/storm-core/src/main/java/st/orm/core/template/PredicateBuilder.java
+++ b/storm-core/src/main/java/st/orm/core/template/PredicateBuilder.java
@@ -31,23 +31,13 @@ public interface PredicateBuilder {
* Adds a predicate to the WHERE clause using an AND condition.
*
* This method combines the specified predicate with existing predicates using an AND operation, ensuring
- * that all added conditions must be true.
- *
- * @param predicate the predicate to add.
- * @return the predicate builder.
- */
- PredicateBuilder and(@Nonnull PredicateBuilder predicate);
-
- /**
- * Adds a predicate to the WHERE clause using an AND condition.
- *
- * This method combines the specified predicate with existing predicates using an AND operation, ensuring
- * that all added conditions must be true.
+ * that all added conditions must be true. The predicate may be rooted at any entity in the query; a path on an
+ * entity outside the query fails when the query is built.
*
* @param predicate the predicate to add.
* @return the predicate builder.
*/
- PredicateBuilder andAny(@Nonnull PredicateBuilder predicate);
+ PredicateBuilder and(@Nonnull PredicateBuilder extends Data, ?, ?> predicate);
/**
* Adds a predicate to the WHERE clause using an AND condition.
@@ -64,23 +54,13 @@ public interface PredicateBuilder {
* Adds a predicate to the WHERE clause using an OR condition.
*
* This method combines the specified predicate with existing predicates using an OR operation, allowing any
- * of the added conditions to be true.
- *
- * @param predicate the predicate to add.
- * @return the predicate builder.
- */
- PredicateBuilder or(@Nonnull PredicateBuilder predicate);
-
- /**
- * Adds a predicate to the WHERE clause using an OR condition.
- *
- * This method combines the specified predicate with existing predicates using an OR operation, allowing any
- * of the added conditions to be true.
+ * of the added conditions to be true. The predicate may be rooted at any entity in the query; a path on an
+ * entity outside the query fails when the query is built.
*
* @param predicate the predicate to add.
* @return the predicate builder.
*/
- PredicateBuilder orAny(@Nonnull PredicateBuilder predicate);
+ PredicateBuilder or(@Nonnull PredicateBuilder extends Data, ?, ?> predicate);
/**
* Adds a predicate to the WHERE clause using an OR condition.
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/QueryBuilderImpl.java b/storm-core/src/main/java/st/orm/core/template/impl/QueryBuilderImpl.java
index 60804c933..77b221b0f 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/QueryBuilderImpl.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/QueryBuilderImpl.java
@@ -446,18 +446,11 @@ static class PredicateBuilderImpl implements Predicate
}
@Override
- public PredicateBuilder and(@Nonnull PredicateBuilder predicate) {
+ public PredicateBuilder and(@Nonnull PredicateBuilder extends Data, ?, ?> predicate) {
add(RAW_AND, predicate);
return this;
}
- @Override
- public PredicateBuilder andAny(@Nonnull PredicateBuilder predicate) {
- add(RAW_AND, predicate);
- //noinspection unchecked
- return (PredicateBuilder) this;
- }
-
@Override
public PredicateBuilder and(@Nonnull TemplateString template) {
add(RAW_AND, combine(RAW_OPEN, template, RAW_CLOSE)); // Always wrap a template in parentheses as we don't know if it's a single expression or a complex one.
@@ -465,18 +458,11 @@ public PredicateBuilder and(@Nonnull TemplateString template) {
}
@Override
- public PredicateBuilder or(@Nonnull PredicateBuilder predicate) {
+ public PredicateBuilder or(@Nonnull PredicateBuilder extends Data, ?, ?> predicate) {
add(RAW_OR, predicate);
return this;
}
- @Override
- public PredicateBuilder orAny(@Nonnull PredicateBuilder predicate) {
- add(RAW_OR, predicate);
- //noinspection unchecked
- return (PredicateBuilder) this;
- }
-
@Override
public PredicateBuilder or(@Nonnull TemplateString template) {
add(RAW_OR, combine(RAW_OPEN, template, RAW_CLOSE));
diff --git a/storm-core/src/test/java/st/orm/core/QueryBuilderPredicateIntegrationTest.java b/storm-core/src/test/java/st/orm/core/QueryBuilderPredicateIntegrationTest.java
index e7f1d6bc9..9c24981f4 100644
--- a/storm-core/src/test/java/st/orm/core/QueryBuilderPredicateIntegrationTest.java
+++ b/storm-core/src/test/java/st/orm/core/QueryBuilderPredicateIntegrationTest.java
@@ -238,27 +238,27 @@ public void testPredicateBuilderOrTemplate() {
assertTrue(cities.size() >= 1);
}
- // PredicateBuilder.andAny - AND with cross-type predicate
+ // PredicateBuilder.and - AND with a second predicate
@Test
- public void testPredicateBuilderAndAny() {
+ public void testPredicateBuilderAndPredicate() {
var orm = ORMTemplate.of(dataSource);
List visits = orm.selectFrom(Visit.class)
.where(predicate -> predicate.where(Visit_.id, GREATER_THAN, 0)
- .andAny(predicate.where(Visit_.id, IN, List.of(1, 2, 3))))
+ .and(predicate.where(Visit_.id, IN, List.of(1, 2, 3))))
.getResultList();
assertEquals(3, visits.size());
}
- // PredicateBuilder.orAny - OR with cross-type predicate
+ // PredicateBuilder.or - OR with a second predicate
@Test
- public void testPredicateBuilderOrAny() {
+ public void testPredicateBuilderOrPredicate() {
var orm = ORMTemplate.of(dataSource);
List visits = orm.selectFrom(Visit.class)
.typedId(Integer.class)
.where(predicate -> predicate.whereId(1)
- .orAny(predicate.whereId(2)))
+ .or(predicate.whereId(2)))
.getResultList();
assertEquals(2, visits.size());
}
diff --git a/storm-java21/src/main/java/st/orm/template/PredicateBuilder.java b/storm-java21/src/main/java/st/orm/template/PredicateBuilder.java
index 224f6572e..1777f6953 100644
--- a/storm-java21/src/main/java/st/orm/template/PredicateBuilder.java
+++ b/storm-java21/src/main/java/st/orm/template/PredicateBuilder.java
@@ -25,8 +25,9 @@
* using {@link #and(PredicateBuilder)} and {@link #or(PredicateBuilder)} to build compound conditions. Each
* combinator returns a new {@code PredicateBuilder} that represents the combined expression.
*
- * Methods named {@code and}/{@code or} are type-safe and restrict predicates to the root table's entity graph.
- * Methods named {@code andAny}/{@code orAny} accept predicates from any table, including manually added joins.
+ * Predicates combine within the query root. A join widens the root, after which the {@link WhereBuilder}
+ * produces predicates that may reference any entity in the query; a path on an entity outside the query fails
+ * when the query is built.
*
* Example
* {@code
@@ -51,24 +52,13 @@ public interface PredicateBuilder {
* Adds a predicate to the WHERE clause using an AND condition.
*
* This method combines the specified predicate with existing predicates using an AND operation, ensuring
- * that all added conditions must be true.
+ * that all added conditions must be true. The predicate inherits the query root: a join widens the root,
+ * admitting predicates that reference any entity in the query.
*
* @param predicate the predicate to add.
* @return the predicate builder.
*/
- PredicateBuilder and(@Nonnull PredicateBuilder predicate);
-
- /**
- * Adds a predicate to the WHERE clause using an AND condition.
- *
- * This method combines the specified predicate with existing predicates using an AND operation, ensuring
- * that all added conditions must be true.
- *
- * @param predicate the predicate to add.
- * @return the predicate builder.
- */
- PredicateBuilder andAny(@Nonnull PredicateBuilder predicate);
-
+ PredicateBuilder and(@Nonnull PredicateBuilder extends T, ?, ?> predicate);
/**
* Adds a predicate to the WHERE clause using an AND condition.
@@ -85,23 +75,13 @@ public interface PredicateBuilder {
* Adds a predicate to the WHERE clause using an OR condition.
*
* This method combines the specified predicate with existing predicates using an OR operation, allowing any
- * of the added conditions to be true.
- *
- * @param predicate the predicate to add.
- * @return the predicate builder.
- */
- PredicateBuilder or(@Nonnull PredicateBuilder predicate);
-
- /**
- * Adds a predicate to the WHERE clause using an OR condition.
- *
- * This method combines the specified predicate with existing predicates using an OR operation, allowing any
- * of the added conditions to be true.
+ * of the added conditions to be true. The predicate inherits the query root: a join widens the root,
+ * admitting predicates that reference any entity in the query.
*
* @param predicate the predicate to add.
* @return the predicate builder.
*/
- PredicateBuilder orAny(@Nonnull PredicateBuilder predicate);
+ PredicateBuilder or(@Nonnull PredicateBuilder extends T, ?, ?> predicate);
/**
* Adds a predicate to the WHERE clause using an OR condition.
diff --git a/storm-java21/src/main/java/st/orm/template/WhereBuilder.java b/storm-java21/src/main/java/st/orm/template/WhereBuilder.java
index d232aca82..2d242da44 100644
--- a/storm-java21/src/main/java/st/orm/template/WhereBuilder.java
+++ b/storm-java21/src/main/java/st/orm/template/WhereBuilder.java
@@ -34,9 +34,8 @@
* {@code and()} and {@code or()} combinators.
*
* The {@code where} methods are type-safe and restrict metamodel paths to the root table's entity graph.
- * Predicates for other tables, including manually added joins, can be combined using the
- * {@link PredicateBuilder#andAny} and {@link PredicateBuilder#orAny} combinators or expressed as custom string
- * template expressions.
+ * A join widens the query root, after which the {@code where} methods accept paths from any entity in the query;
+ * a path on an entity outside the query fails when the query is built.
*
* Example
* {@code
diff --git a/storm-java21/src/main/java/st/orm/template/impl/QueryBuilderImpl.java b/storm-java21/src/main/java/st/orm/template/impl/QueryBuilderImpl.java
index 0d8aa90e8..97accdfdb 100644
--- a/storm-java21/src/main/java/st/orm/template/impl/QueryBuilderImpl.java
+++ b/storm-java21/src/main/java/st/orm/template/impl/QueryBuilderImpl.java
@@ -461,13 +461,8 @@ static class PredicateBuilderImpl implements Predicate
}
@Override
- public PredicateBuilder and(@Nonnull PredicateBuilder predicate) {
- return new PredicateBuilderImpl<>(core.and(((PredicateBuilderImpl) predicate).core));
- }
-
- @Override
- public PredicateBuilder andAny(@Nonnull PredicateBuilder predicate) {
- return new PredicateBuilderImpl<>(core.andAny(((PredicateBuilderImpl) predicate).core));
+ public PredicateBuilder and(@Nonnull PredicateBuilder extends TX, ?, ?> predicate) {
+ return new PredicateBuilderImpl<>(core.and(((PredicateBuilderImpl, ?, ?>) predicate).core));
}
@Override
@@ -476,13 +471,8 @@ public PredicateBuilder and(@Nonnull StringTemplate template) {
}
@Override
- public PredicateBuilder or(@Nonnull PredicateBuilder predicate) {
- return new PredicateBuilderImpl<>(core.or(((PredicateBuilderImpl) predicate).core));
- }
-
- @Override
- public PredicateBuilder orAny(@Nonnull PredicateBuilder predicate) {
- return new PredicateBuilderImpl<>(core.orAny(((PredicateBuilderImpl) predicate).core));
+ public PredicateBuilder or(@Nonnull PredicateBuilder extends TX, ?, ?> predicate) {
+ return new PredicateBuilderImpl<>(core.or(((PredicateBuilderImpl, ?, ?>) predicate).core));
}
@Override
diff --git a/storm-java21/src/test/java/st/orm/template/QueryBuilderTest.java b/storm-java21/src/test/java/st/orm/template/QueryBuilderTest.java
index 2e500c4bb..36550d01c 100644
--- a/storm-java21/src/test/java/st/orm/template/QueryBuilderTest.java
+++ b/storm-java21/src/test/java/st/orm/template/QueryBuilderTest.java
@@ -690,22 +690,22 @@ public void testHavingWithOrTemplate() {
assertEquals(3L, result.stream().mapToLong(Long::longValue).sum());
}
- // PredicateBuilder - andAny / orAny
+ // PredicateBuilder - and / or with a second predicate
@Test
- public void testPredicateAndAny() {
+ public void testPredicateAndPredicate() {
List cities = orm.entity(City.class).select()
.where(wb -> wb.where(RAW."\{City.class}.id = \{1}")
- .andAny(wb.where(RAW."\{City.class}.name = \{"Sun Paririe"}")))
+ .and(wb.where(RAW."\{City.class}.name = \{"Sun Paririe"}")))
.getResultList();
assertEquals(1, cities.size());
}
@Test
- public void testPredicateOrAny() {
+ public void testPredicateOrPredicate() {
List cities = orm.entity(City.class).select()
.where(wb -> wb.where(RAW."\{City.class}.id = \{999}")
- .orAny(wb.where(RAW."\{City.class}.name = \{"Madison"}")))
+ .or(wb.where(RAW."\{City.class}.name = \{"Madison"}")))
.getResultList();
assertEquals(1, cities.size());
}
diff --git a/storm-kotlin/src/main/kotlin/st/orm/template/PredicateBuilder.kt b/storm-kotlin/src/main/kotlin/st/orm/template/PredicateBuilder.kt
index 35c55cff0..1da620c22 100644
--- a/storm-kotlin/src/main/kotlin/st/orm/template/PredicateBuilder.kt
+++ b/storm-kotlin/src/main/kotlin/st/orm/template/PredicateBuilder.kt
@@ -20,12 +20,14 @@ import st.orm.Data
/**
* Represents a composable predicate for the WHERE clause of a query, supporting `AND` and `OR` composition.
*
- * `PredicateBuilder` instances are returned by the methods on [WhereBuilder] and can be combined using [and]
- * and [or] to build compound conditions. Each combinator returns a new `PredicateBuilder` that represents the
- * combined expression.
+ * `PredicateBuilder` instances are returned by the methods on [WhereBuilder] and by the infix operators such as
+ * `eq` and `like`. They combine with `and` and `or`; each combinator returns a new `PredicateBuilder` that
+ * represents the combined expression.
*
- * Methods named `and`/`or` are type-safe and restrict predicates to the root table's entity graph.
- * Methods named `andAny`/`orAny` accept predicates from any table, including manually added joins.
+ * Inside a [WhereBuilder] scope, the scope's `and`/`or` combinators inherit the query root: a narrow scope
+ * combines predicates within the root table's entity graph, and a join widens the root so the same syntax
+ * combines predicates across all entities in the query. Outside a scope, the top-level `and`/`or` extensions
+ * combine predicates that share the same root.
*
* ## Example
* ```kotlin
@@ -46,29 +48,6 @@ import st.orm.Data
* @see QueryBuilder
*/
public interface PredicateBuilder {
- /**
- * Adds a predicate to the WHERE clause using an AND condition.
- *
- * This method combines the specified predicate with existing predicates using an AND operation, ensuring
- * that all added conditions must be true.
- *
- * @param predicate the predicate to add.
- * @return the predicate builder.
- */
- public infix fun and(predicate: PredicateBuilder): PredicateBuilder
-
- /**
- * Adds a predicate to the WHERE clause using an AND condition.
- *
- *
- * This method combines the specified predicate with existing predicates using an AND operation, ensuring
- * that all added conditions must be true.
- *
- * @param predicate the predicate to add.
- * @return the predicate builder.
- */
- public infix fun andAny(predicate: PredicateBuilder): PredicateBuilder
-
/**
* Adds a predicate to the WHERE clause using an AND condition.
*
@@ -91,30 +70,6 @@ public interface PredicateBuilder {
*/
public infix fun and(template: TemplateString): PredicateBuilder
- /**
- * Adds a predicate to the WHERE clause using an OR condition.
- *
- *
- * This method combines the specified predicate with existing predicates using an OR operation, allowing any
- * of the added conditions to be true.
- *
- * @param predicate the predicate to add.
- * @return the predicate builder.
- */
- public infix fun or(predicate: PredicateBuilder): PredicateBuilder
-
- /**
- * Adds a predicate to the WHERE clause using an OR condition.
- *
- *
- * This method combines the specified predicate with existing predicates using an OR operation, allowing any
- * of the added conditions to be true.
- *
- * @param predicate the predicate to add.
- * @return the predicate builder.
- */
- public infix fun orAny(predicate: PredicateBuilder): PredicateBuilder
-
/**
* Adds a predicate to the WHERE clause using an OR condition.
*
diff --git a/storm-kotlin/src/main/kotlin/st/orm/template/QueryBuilder.kt b/storm-kotlin/src/main/kotlin/st/orm/template/QueryBuilder.kt
index 523f7245e..21a95489d 100644
--- a/storm-kotlin/src/main/kotlin/st/orm/template/QueryBuilder.kt
+++ b/storm-kotlin/src/main/kotlin/st/orm/template/QueryBuilder.kt
@@ -25,6 +25,8 @@ import st.orm.core.template.impl.Elements.ObjectExpression
import st.orm.template.TemplateString.Companion.combine
import st.orm.template.TemplateString.Companion.raw
import st.orm.template.TemplateString.Companion.wrap
+import st.orm.template.impl.combineAnd
+import st.orm.template.impl.combineOr
import st.orm.template.impl.create
import st.orm.template.impl.createRef
import java.util.stream.Stream
@@ -1369,6 +1371,28 @@ public fun Navigable.isNull(): PredicateBuilder = c
*/
public fun Navigable.isNotNull(): PredicateBuilder = create(this.asMetamodel(), IS_NOT_NULL, emptyList())
+/**
+ * Combines two predicates that share the same root using an AND condition.
+ *
+ * Inside a [WhereBuilder] scope, the scope's own `and` takes precedence and inherits the query root, so a widened
+ * query combines predicates across joined entities with the same syntax.
+ *
+ * @param predicate the predicate to add.
+ * @return the combined predicate builder.
+ */
+public infix fun PredicateBuilder.and(predicate: PredicateBuilder): PredicateBuilder = combineAnd(this, predicate)
+
+/**
+ * Combines two predicates that share the same root using an OR condition.
+ *
+ * Inside a [WhereBuilder] scope, the scope's own `or` takes precedence and inherits the query root, so a widened
+ * query combines predicates across joined entities with the same syntax.
+ *
+ * @param predicate the predicate to add.
+ * @return the combined predicate builder.
+ */
+public infix fun PredicateBuilder.or(predicate: PredicateBuilder): PredicateBuilder = combineOr(this, predicate)
+
// Block-based query DSL
/**
diff --git a/storm-kotlin/src/main/kotlin/st/orm/template/WhereBuilder.kt b/storm-kotlin/src/main/kotlin/st/orm/template/WhereBuilder.kt
index ebba0f75a..e131bccbe 100644
--- a/storm-kotlin/src/main/kotlin/st/orm/template/WhereBuilder.kt
+++ b/storm-kotlin/src/main/kotlin/st/orm/template/WhereBuilder.kt
@@ -29,9 +29,9 @@ import st.orm.template.TemplateString.Companion.raw
* matching by primary key, record, ref, metamodel path, or custom template string expressions. Each method returns
* a [PredicateBuilder] that can be further composed using `and()` and `or()` combinators.
*
- * The `where` methods are type-safe and restrict metamodel paths to the root table's entity graph. Predicates for
- * other tables, including manually added joins, can be combined using the [PredicateBuilder.andAny] and
- * [PredicateBuilder.orAny] combinators or expressed as custom template strings.
+ * The `where` methods are type-safe and restrict metamodel paths to the root table's entity graph. A join widens
+ * the query root, and the scope's `and`/`or` combinators inherit it: a narrow scope combines predicates within
+ * the root graph, a widened scope combines predicates across all entities in the query.
*
* ## Example
* ```kotlin
@@ -62,6 +62,30 @@ public interface WhereBuilder : SubqueryTemplate {
*/
public fun FALSE(): PredicateBuilder = where(raw("FALSE"))
+ /**
+ * Combines this predicate with another using an AND condition.
+ *
+ * The combinator inherits the query root of this `WhereBuilder`: a narrow scope combines predicates within
+ * the root table's entity graph, and a join widens the root so predicates rooted at any entity in the query
+ * combine with the same syntax.
+ *
+ * @param predicate the predicate to add.
+ * @return the combined predicate builder.
+ */
+ public infix fun PredicateBuilder.and(predicate: PredicateBuilder): PredicateBuilder
+
+ /**
+ * Combines this predicate with another using an OR condition.
+ *
+ * The combinator inherits the query root of this `WhereBuilder`: a narrow scope combines predicates within
+ * the root table's entity graph, and a join widens the root so predicates rooted at any entity in the query
+ * combine with the same syntax.
+ *
+ * @param predicate the predicate to add.
+ * @return the combined predicate builder.
+ */
+ public infix fun PredicateBuilder.or(predicate: PredicateBuilder): PredicateBuilder
+
/**
* Adds an `EXISTS` condition to the WHERE clause using the specified subquery.
*
diff --git a/storm-kotlin/src/main/kotlin/st/orm/template/impl/PredicateBuilderFactory.kt b/storm-kotlin/src/main/kotlin/st/orm/template/impl/PredicateBuilderFactory.kt
index cdc11bd8a..f8f56178e 100644
--- a/storm-kotlin/src/main/kotlin/st/orm/template/impl/PredicateBuilderFactory.kt
+++ b/storm-kotlin/src/main/kotlin/st/orm/template/impl/PredicateBuilderFactory.kt
@@ -92,3 +92,27 @@ internal fun createRefWithId(
operator: Operator,
o: Iterable>,
): PredicateBuilder = PredicateBuilderImpl(PredicateBuilderFactory.createRefWithId(path, operator, o))
+
+/**
+ * Combines two predicates using an AND condition, keeping the left-hand predicate's root.
+ *
+ * @param left the predicate to combine into
+ * @param right the predicate to add
+ * @return the combined [PredicateBuilder]
+ */
+internal fun combineAnd(
+ left: PredicateBuilder,
+ right: PredicateBuilder<*, *, *>,
+): PredicateBuilder = PredicateBuilderImpl((left as PredicateBuilderImpl).core.and((right as PredicateBuilderImpl<*, *, *>).core))
+
+/**
+ * Combines two predicates using an OR condition, keeping the left-hand predicate's root.
+ *
+ * @param left the predicate to combine into
+ * @param right the predicate to add
+ * @return the combined [PredicateBuilder]
+ */
+internal fun combineOr(
+ left: PredicateBuilder,
+ right: PredicateBuilder<*, *, *>,
+): PredicateBuilder = PredicateBuilderImpl((left as PredicateBuilderImpl).core.or((right as PredicateBuilderImpl<*, *, *>).core))
diff --git a/storm-kotlin/src/main/kotlin/st/orm/template/impl/QueryBuilderImpl.kt b/storm-kotlin/src/main/kotlin/st/orm/template/impl/QueryBuilderImpl.kt
index b276afe40..73eea250e 100644
--- a/storm-kotlin/src/main/kotlin/st/orm/template/impl/QueryBuilderImpl.kt
+++ b/storm-kotlin/src/main/kotlin/st/orm/template/impl/QueryBuilderImpl.kt
@@ -324,16 +324,8 @@ internal class QueryBuilderImpl(
internal class PredicateBuilderImpl(
val core: st.orm.core.template.PredicateBuilder,
) : PredicateBuilder {
- override infix fun and(predicate: PredicateBuilder): PredicateBuilder = PredicateBuilderImpl(core.and((predicate as PredicateBuilderImpl).core))
-
- override fun andAny(predicate: PredicateBuilder): PredicateBuilder = PredicateBuilderImpl(core.andAny((predicate as PredicateBuilderImpl).core))
-
override fun and(template: TemplateString): PredicateBuilder = PredicateBuilderImpl(core.and(template.unwrap))
- override infix fun or(predicate: PredicateBuilder): PredicateBuilder = PredicateBuilderImpl(core.or((predicate as PredicateBuilderImpl).core))
-
- override fun orAny(predicate: PredicateBuilder): PredicateBuilder = PredicateBuilderImpl(core.orAny((predicate as PredicateBuilderImpl).core))
-
override fun or(template: TemplateString): PredicateBuilder = PredicateBuilderImpl(core.or(template.unwrap))
}
@@ -375,6 +367,10 @@ internal class QueryBuilderImpl(
): PredicateBuilder = PredicateBuilderImpl(core.where(path.asMetamodel(), operator, it))
override fun where(template: TemplateString): PredicateBuilder = PredicateBuilderImpl(core.where((template as TemplateStringHolder).templateString))
+
+ override infix fun PredicateBuilder.and(predicate: PredicateBuilder): PredicateBuilder = PredicateBuilderImpl((this as PredicateBuilderImpl).core.and((predicate as PredicateBuilderImpl<*, *, *>).core))
+
+ override infix fun PredicateBuilder.or(predicate: PredicateBuilder): PredicateBuilder = PredicateBuilderImpl((this as PredicateBuilderImpl).core.or((predicate as PredicateBuilderImpl<*, *, *>).core))
}
/**
diff --git a/storm-kotlin/src/test/kotlin/st/orm/template/EntityRepositoryTest.kt b/storm-kotlin/src/test/kotlin/st/orm/template/EntityRepositoryTest.kt
index 5d8e01bf1..bbd7753f0 100644
--- a/storm-kotlin/src/test/kotlin/st/orm/template/EntityRepositoryTest.kt
+++ b/storm-kotlin/src/test/kotlin/st/orm/template/EntityRepositoryTest.kt
@@ -1033,10 +1033,10 @@ open class EntityRepositoryTest(
preparedQuery.close()
}
- // QueryBuilder: whereAny with multiple predicates
+ // QueryBuilder: whereBuilder with multiple predicates
@Test
- fun `whereAny with combined predicates should match any`() {
+ fun `whereBuilder with combined predicates should match any`() {
val repo = orm.entity(City::class)
val namePath = metamodel(repo.model, "name")
val cities = repo.select().whereBuilder {
@@ -1045,15 +1045,15 @@ open class EntityRepositoryTest(
cities shouldHaveSize 2
}
- // QueryBuilder: andAny / orAny predicate builders
+ // QueryBuilder: and / or predicate combinators
@Test
- fun `andAny should combine predicates with AND-OR logic`() {
+ fun `and should combine predicates with AND-OR logic`() {
val repo = orm.entity(Owner::class)
val firstNamePath = metamodel(repo.model, "first_name")
val lastNamePath = metamodel(repo.model, "last_name")
val result = repo.select().whereBuilder {
- (lastNamePath eq "Davis") andAny (
+ (lastNamePath eq "Davis") and (
(firstNamePath eq "Betty") or (firstNamePath eq "Harold")
)
}.resultList
@@ -1061,7 +1061,7 @@ open class EntityRepositoryTest(
}
@Test
- fun `orAny should combine predicates with OR logic`() {
+ fun `or should combine predicates with OR logic`() {
val repo = orm.entity(City::class)
val namePath = metamodel(repo.model, "name")
val result = repo.select().whereBuilder {
diff --git a/storm-kotlin/src/test/kotlin/st/orm/template/ORMTemplateTest.kt b/storm-kotlin/src/test/kotlin/st/orm/template/ORMTemplateTest.kt
index 0d63463e4..ec9be0096 100644
--- a/storm-kotlin/src/test/kotlin/st/orm/template/ORMTemplateTest.kt
+++ b/storm-kotlin/src/test/kotlin/st/orm/template/ORMTemplateTest.kt
@@ -1540,26 +1540,26 @@ open class ORMTemplateTest(
cities shouldHaveSize 0
}
- // PredicateBuilder: andAny/orAny
+ // PredicateBuilder: and/or
@Test
- fun `predicateBuilder andAny should combine predicates from different entities`() {
+ fun `predicateBuilder and should combine predicates`() {
val repo = orm.entity(City::class)
val namePath = metamodel(repo.model, "name")
val idPath = metamodel(repo.model, "id")
val cities = repo.select().whereBuilder {
- (namePath eq "Madison") andAny (idPath eq 2)
+ (namePath eq "Madison") and (idPath eq 2)
}.resultList
cities shouldHaveSize 1
}
@Test
- fun `predicateBuilder orAny should combine predicates from different entities`() {
+ fun `predicateBuilder or should combine predicates`() {
val repo = orm.entity(City::class)
val namePath = metamodel(repo.model, "name")
val idPath = metamodel(repo.model, "id")
val cities = repo.select().whereBuilder {
- (namePath eq "Madison") orAny (idPath eq 1)
+ (namePath eq "Madison") or (idPath eq 1)
}.resultList
cities shouldHaveSize 2
}
diff --git a/storm-kotlin/src/test/kotlin/st/orm/template/QueryBuilderTest.kt b/storm-kotlin/src/test/kotlin/st/orm/template/QueryBuilderTest.kt
index 7863750f5..5a36a34b5 100644
--- a/storm-kotlin/src/test/kotlin/st/orm/template/QueryBuilderTest.kt
+++ b/storm-kotlin/src/test/kotlin/st/orm/template/QueryBuilderTest.kt
@@ -1243,28 +1243,40 @@ open class QueryBuilderTest(
}
@Test
- fun `andAny should combine predicates across different types`() {
+ fun `and should combine two predicates`() {
val repo = orm.entity(Owner::class)
val firstNamePath = metamodel(repo.model, "first_name")
val lastNamePath = metamodel(repo.model, "last_name")
- // Use andAny to combine a predicate with another typed predicate.
val result = repo.select().whereBuilder {
- (firstNamePath eq "Betty") andAny (lastNamePath eq "Davis")
+ (firstNamePath eq "Betty") and (lastNamePath eq "Davis")
}.resultList
result shouldHaveSize 1
result[0].firstName shouldBe "Betty"
}
@Test
- fun `orAny should combine predicates across different types`() {
+ fun `or should combine two predicates`() {
val repo = orm.entity(City::class)
val namePath = metamodel(repo.model, "name")
val result = repo.select().whereBuilder {
- (namePath eq "Madison") orAny (namePath eq "Windsor")
+ (namePath eq "Madison") or (namePath eq "Windsor")
}.resultList
result shouldHaveSize 2
}
+ @Test
+ fun `and should combine predicates across entities after a join`() {
+ val petNamePath = metamodel(orm.entity(Pet::class).model, "name")
+ val ownerLastNamePath = metamodel(orm.entity(Owner::class).model, "last_name")
+ val pets = orm.entity(Pet::class).select()
+ .innerJoin(Owner::class).on(Pet::class)
+ .whereBuilder {
+ (petNamePath eq "Leo") and (ownerLastNamePath eq "Davis")
+ }.resultList
+ pets shouldHaveSize 1
+ pets[0].name shouldBe "Leo"
+ }
+
@Test
fun `predicate and with TemplateBuilder should combine conditions`() {
val repo = orm.entity(City::class)
From 0836eebd994195a6b2f1dd95e59348902b667ec8 Mon Sep 17 00:00:00 2001
From: Leon van Zantvoort
Date: Tue, 11 Aug 2026 13:29:28 +0200
Subject: [PATCH 3/4] refactor: state the root-relative contract on the core
combinators too
The core and/or parameters tighten from ? extends Data to ? extends T,
the same level where operates at. The bridges' arguments are within the
receiver's root in every sound path: narrow scopes guarantee same-root by
their public signatures, and a widened scope's root is Data itself, so the
permissive parameter stated less than what every caller already proves.
---
.../java/st/orm/core/template/PredicateBuilder.java | 12 ++++++------
.../st/orm/core/template/impl/QueryBuilderImpl.java | 4 ++--
.../java/st/orm/template/impl/QueryBuilderImpl.java | 4 ++--
.../st/orm/template/impl/PredicateBuilderFactory.kt | 8 ++++----
.../kotlin/st/orm/template/impl/QueryBuilderImpl.kt | 4 ++--
5 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/storm-core/src/main/java/st/orm/core/template/PredicateBuilder.java b/storm-core/src/main/java/st/orm/core/template/PredicateBuilder.java
index c92b88a8a..5b3b0e122 100644
--- a/storm-core/src/main/java/st/orm/core/template/PredicateBuilder.java
+++ b/storm-core/src/main/java/st/orm/core/template/PredicateBuilder.java
@@ -31,13 +31,13 @@ public interface PredicateBuilder {
* Adds a predicate to the WHERE clause using an AND condition.
*
* This method combines the specified predicate with existing predicates using an AND operation, ensuring
- * that all added conditions must be true. The predicate may be rooted at any entity in the query; a path on an
- * entity outside the query fails when the query is built.
+ * that all added conditions must be true. The predicate inherits the query root: a join widens the root,
+ * admitting predicates that reference any entity in the query.
*
* @param predicate the predicate to add.
* @return the predicate builder.
*/
- PredicateBuilder and(@Nonnull PredicateBuilder extends Data, ?, ?> predicate);
+ PredicateBuilder and(@Nonnull PredicateBuilder extends T, ?, ?> predicate);
/**
* Adds a predicate to the WHERE clause using an AND condition.
@@ -54,13 +54,13 @@ public interface PredicateBuilder {
* Adds a predicate to the WHERE clause using an OR condition.
*
* This method combines the specified predicate with existing predicates using an OR operation, allowing any
- * of the added conditions to be true. The predicate may be rooted at any entity in the query; a path on an
- * entity outside the query fails when the query is built.
+ * of the added conditions to be true. The predicate inherits the query root: a join widens the root,
+ * admitting predicates that reference any entity in the query.
*
* @param predicate the predicate to add.
* @return the predicate builder.
*/
- PredicateBuilder or(@Nonnull PredicateBuilder extends Data, ?, ?> predicate);
+ PredicateBuilder or(@Nonnull PredicateBuilder extends T, ?, ?> predicate);
/**
* Adds a predicate to the WHERE clause using an OR condition.
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/QueryBuilderImpl.java b/storm-core/src/main/java/st/orm/core/template/impl/QueryBuilderImpl.java
index 77b221b0f..e0e29512a 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/QueryBuilderImpl.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/QueryBuilderImpl.java
@@ -446,7 +446,7 @@ static class PredicateBuilderImpl implements Predicate
}
@Override
- public PredicateBuilder and(@Nonnull PredicateBuilder extends Data, ?, ?> predicate) {
+ public PredicateBuilder and(@Nonnull PredicateBuilder extends TX, ?, ?> predicate) {
add(RAW_AND, predicate);
return this;
}
@@ -458,7 +458,7 @@ public PredicateBuilder and(@Nonnull TemplateString template) {
}
@Override
- public PredicateBuilder or(@Nonnull PredicateBuilder extends Data, ?, ?> predicate) {
+ public PredicateBuilder or(@Nonnull PredicateBuilder extends TX, ?, ?> predicate) {
add(RAW_OR, predicate);
return this;
}
diff --git a/storm-java21/src/main/java/st/orm/template/impl/QueryBuilderImpl.java b/storm-java21/src/main/java/st/orm/template/impl/QueryBuilderImpl.java
index 97accdfdb..f34f9d648 100644
--- a/storm-java21/src/main/java/st/orm/template/impl/QueryBuilderImpl.java
+++ b/storm-java21/src/main/java/st/orm/template/impl/QueryBuilderImpl.java
@@ -462,7 +462,7 @@ static class PredicateBuilderImpl implements Predicate
@Override
public PredicateBuilder and(@Nonnull PredicateBuilder extends TX, ?, ?> predicate) {
- return new PredicateBuilderImpl<>(core.and(((PredicateBuilderImpl, ?, ?>) predicate).core));
+ return new PredicateBuilderImpl<>(core.and(((PredicateBuilderImpl extends TX, ?, ?>) predicate).core));
}
@Override
@@ -472,7 +472,7 @@ public PredicateBuilder and(@Nonnull StringTemplate template) {
@Override
public PredicateBuilder or(@Nonnull PredicateBuilder extends TX, ?, ?> predicate) {
- return new PredicateBuilderImpl<>(core.or(((PredicateBuilderImpl, ?, ?>) predicate).core));
+ return new PredicateBuilderImpl<>(core.or(((PredicateBuilderImpl extends TX, ?, ?>) predicate).core));
}
@Override
diff --git a/storm-kotlin/src/main/kotlin/st/orm/template/impl/PredicateBuilderFactory.kt b/storm-kotlin/src/main/kotlin/st/orm/template/impl/PredicateBuilderFactory.kt
index f8f56178e..7893b251a 100644
--- a/storm-kotlin/src/main/kotlin/st/orm/template/impl/PredicateBuilderFactory.kt
+++ b/storm-kotlin/src/main/kotlin/st/orm/template/impl/PredicateBuilderFactory.kt
@@ -102,8 +102,8 @@ internal fun createRefWithId(
*/
internal fun combineAnd(
left: PredicateBuilder,
- right: PredicateBuilder<*, *, *>,
-): PredicateBuilder = PredicateBuilderImpl((left as PredicateBuilderImpl).core.and((right as PredicateBuilderImpl<*, *, *>).core))
+ right: PredicateBuilder,
+): PredicateBuilder = PredicateBuilderImpl((left as PredicateBuilderImpl).core.and((right as PredicateBuilderImpl).core))
/**
* Combines two predicates using an OR condition, keeping the left-hand predicate's root.
@@ -114,5 +114,5 @@ internal fun combineAnd(
*/
internal fun combineOr(
left: PredicateBuilder,
- right: PredicateBuilder<*, *, *>,
-): PredicateBuilder = PredicateBuilderImpl((left as PredicateBuilderImpl).core.or((right as PredicateBuilderImpl<*, *, *>).core))
+ right: PredicateBuilder,
+): PredicateBuilder = PredicateBuilderImpl((left as PredicateBuilderImpl).core.or((right as PredicateBuilderImpl).core))
diff --git a/storm-kotlin/src/main/kotlin/st/orm/template/impl/QueryBuilderImpl.kt b/storm-kotlin/src/main/kotlin/st/orm/template/impl/QueryBuilderImpl.kt
index 73eea250e..a287bc119 100644
--- a/storm-kotlin/src/main/kotlin/st/orm/template/impl/QueryBuilderImpl.kt
+++ b/storm-kotlin/src/main/kotlin/st/orm/template/impl/QueryBuilderImpl.kt
@@ -368,9 +368,9 @@ internal class QueryBuilderImpl(
override fun where(template: TemplateString): PredicateBuilder = PredicateBuilderImpl(core.where((template as TemplateStringHolder).templateString))
- override infix fun PredicateBuilder.and(predicate: PredicateBuilder): PredicateBuilder = PredicateBuilderImpl((this as PredicateBuilderImpl).core.and((predicate as PredicateBuilderImpl<*, *, *>).core))
+ override infix fun PredicateBuilder.and(predicate: PredicateBuilder): PredicateBuilder = PredicateBuilderImpl((this as PredicateBuilderImpl).core.and((predicate as PredicateBuilderImpl).core))
- override infix fun PredicateBuilder.or(predicate: PredicateBuilder): PredicateBuilder = PredicateBuilderImpl((this as PredicateBuilderImpl).core.or((predicate as PredicateBuilderImpl<*, *, *>).core))
+ override infix fun PredicateBuilder.or(predicate: PredicateBuilder): PredicateBuilder = PredicateBuilderImpl((this as PredicateBuilderImpl).core.or((predicate as PredicateBuilderImpl).core))
}
/**
From 360367831ae0c34358ece329b10962ee9178b918 Mon Sep 17 00:00:00 2001
From: Leon van Zantvoort
Date: Tue, 11 Aug 2026 13:36:55 +0200
Subject: [PATCH 4/4] test: cover and/or across entities on a widened builder
on every surface
The Kotlin surface had the cross-entity and case; core and storm-java21
predicates on a widened builder were untested, and no surface covered or.
Pets joined with owners: name = 'Leo' AND owner 'Davis' finds Betty's Leo;
OR also admits Harold Davis's Iggy.
---
.../QueryBuilderPredicateIntegrationTest.java | 25 +++++++++++++++++++
.../st/orm/template/QueryBuilderTest.java | 23 +++++++++++++++++
.../st/orm/template/QueryBuilderTest.kt | 13 ++++++++++
3 files changed, 61 insertions(+)
diff --git a/storm-core/src/test/java/st/orm/core/QueryBuilderPredicateIntegrationTest.java b/storm-core/src/test/java/st/orm/core/QueryBuilderPredicateIntegrationTest.java
index 9c24981f4..f1d7f1eb0 100644
--- a/storm-core/src/test/java/st/orm/core/QueryBuilderPredicateIntegrationTest.java
+++ b/storm-core/src/test/java/st/orm/core/QueryBuilderPredicateIntegrationTest.java
@@ -263,6 +263,31 @@ public void testPredicateBuilderOrPredicate() {
assertEquals(2, visits.size());
}
+ // PredicateBuilder.and / or - predicates across entities on a widened builder
+
+ @Test
+ public void testPredicateBuilderAndAcrossEntitiesAfterJoin() {
+ var orm = ORMTemplate.of(dataSource);
+ List pets = orm.selectFrom(Pet.class)
+ .innerJoin(Owner.class).on(Pet.class)
+ .where(predicate -> predicate.where(Pet_.name, EQUALS, "Leo")
+ .and(predicate.where(Owner_.lastName, EQUALS, "Davis")))
+ .getResultList();
+ assertEquals(1, pets.size());
+ }
+
+ @Test
+ public void testPredicateBuilderOrAcrossEntitiesAfterJoin() {
+ // Pets named Leo (Betty Davis's pet) or owned by a Davis: Leo and Harold Davis's Iggy.
+ var orm = ORMTemplate.of(dataSource);
+ List pets = orm.selectFrom(Pet.class)
+ .innerJoin(Owner.class).on(Pet.class)
+ .where(predicate -> predicate.where(Pet_.name, EQUALS, "Leo")
+ .or(predicate.where(Owner_.lastName, EQUALS, "Davis")))
+ .getResultList();
+ assertEquals(2, pets.size());
+ }
+
// QueryBuilder.having with raw template
@Test
diff --git a/storm-java21/src/test/java/st/orm/template/QueryBuilderTest.java b/storm-java21/src/test/java/st/orm/template/QueryBuilderTest.java
index 36550d01c..4ad2c82d2 100644
--- a/storm-java21/src/test/java/st/orm/template/QueryBuilderTest.java
+++ b/storm-java21/src/test/java/st/orm/template/QueryBuilderTest.java
@@ -710,6 +710,29 @@ public void testPredicateOrPredicate() {
assertEquals(1, cities.size());
}
+ // PredicateBuilder - and / or across entities on a widened builder
+
+ @Test
+ public void testPredicateAndAcrossEntitiesAfterJoin() {
+ List pets = orm.entity(Pet.class).select()
+ .innerJoin(Owner.class).on(Pet.class)
+ .where(wb -> wb.where(Pet_.name, EQUALS, "Leo")
+ .and(wb.where(Owner_.lastName, EQUALS, "Davis")))
+ .getResultList();
+ assertEquals(1, pets.size());
+ }
+
+ @Test
+ public void testPredicateOrAcrossEntitiesAfterJoin() {
+ // Pets named Leo (Betty Davis's pet) or owned by a Davis: Leo and Harold Davis's Iggy.
+ List pets = orm.entity(Pet.class).select()
+ .innerJoin(Owner.class).on(Pet.class)
+ .where(wb -> wb.where(Pet_.name, EQUALS, "Leo")
+ .or(wb.where(Owner_.lastName, EQUALS, "Davis")))
+ .getResultList();
+ assertEquals(2, pets.size());
+ }
+
@Test
public void testPredicateAndTemplate() {
List cities = orm.entity(City.class).select()
diff --git a/storm-kotlin/src/test/kotlin/st/orm/template/QueryBuilderTest.kt b/storm-kotlin/src/test/kotlin/st/orm/template/QueryBuilderTest.kt
index 5a36a34b5..24121c053 100644
--- a/storm-kotlin/src/test/kotlin/st/orm/template/QueryBuilderTest.kt
+++ b/storm-kotlin/src/test/kotlin/st/orm/template/QueryBuilderTest.kt
@@ -1277,6 +1277,19 @@ open class QueryBuilderTest(
pets[0].name shouldBe "Leo"
}
+ @Test
+ fun `or should combine predicates across entities after a join`() {
+ // Pets named Leo (Betty Davis's pet) or owned by a Davis: Leo and Harold Davis's Iggy.
+ val petNamePath = metamodel(orm.entity(Pet::class).model, "name")
+ val ownerLastNamePath = metamodel(orm.entity(Owner::class).model, "last_name")
+ val pets = orm.entity(Pet::class).select()
+ .innerJoin(Owner::class).on(Pet::class)
+ .whereBuilder {
+ (petNamePath eq "Leo") or (ownerLastNamePath eq "Davis")
+ }.resultList
+ pets shouldHaveSize 2
+ }
+
@Test
fun `predicate and with TemplateBuilder should combine conditions`() {
val repo = orm.entity(City::class)