Skip to content

feat: add OR operator to QuerySpec constraints#108

Open
javier-godoy wants to merge 2 commits intomasterfrom
querySpec-or
Open

feat: add OR operator to QuerySpec constraints#108
javier-godoy wants to merge 2 commits intomasterfrom
querySpec-or

Conversation

@javier-godoy
Copy link
Member

@javier-godoy javier-godoy commented Mar 16, 2026

Close #61

Summary by CodeRabbit

  • New Features

    • Added OR-based filtering via a new DisjunctionConstraint and a convenience or(...) helper; disjunctions evaluate sub-constraints using left-join semantics for partial-match relations.
  • Tests

    • Added tests validating full-match and partial-match OR scenarios, ensuring expected record counts and flattening/validation behavior.
  • Chores

    • Bumped project version to 1.2.0-SNAPSHOT across modules.

@javier-godoy javier-godoy requested a review from mlopezFC March 16, 2026 18:48
@coderabbitai
Copy link

coderabbitai bot commented Mar 16, 2026

Walkthrough

Adds OR/disjunction support: new DisjunctionConstraint type and factory, Constraint.or(...) convenience, transformer hook and JPA implementation combining sub-predicates with OR using LEFT joins, tests for OR behavior, and Maven version bumps across modules.

Changes

Cohort / File(s) Summary
Constraint Model
backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java, backend-core-model/src/main/java/com/flowingcode/backendcore/model/ConstraintTransformer.java, backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java
Adds DisjunctionConstraint class with of(...) factory, Constraint.or(...) default helper, and a transformDisjunctionConstraint(...) hook in ConstraintTransformer.
JPA Transformer Implementation
backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java
Introduces stateful currentJoinType (default INNER), uses it for join operations, and implements transformDisjunctionConstraint to evaluate sub-constraints with temporary LEFT joins and OR-combine their predicates.
Tests
backend-core-model/src/test/java/.../DisjunctionConstraintTest.java, backend-core-data-impl/src/test/java/com/flowingcode/backendcore/dao/jpa/JpaDaoSupportTest.java
Adds unit tests for DisjunctionConstraint behavior (flattening, null checks) and two JPA tests verifying OR/disjunction and partial-match (LEFT JOIN) behavior.
Maven POMs
pom.xml, backend-core-business/pom.xml, backend-core-business-impl/pom.xml, backend-core-business-spring-impl/pom.xml, backend-core-data/pom.xml, backend-core-data-impl/pom.xml, backend-core-model/pom.xml
Bumps project/parent version from 1.1.1-SNAPSHOT to 1.2.0-SNAPSHOT across modules.
Headers / Javadoc
backend-core-data-impl/src/main/java/.../ConstraintTransformerJpaImpl.java, backend-core-model/src/main/java/.../ConstraintTransformer.java, backend-core-model/src/main/java/.../Constraint.java, backend-core-data-impl/src/test/java/.../JpaDaoSupportTest.java
Updates copyright year ranges to include 2026 and adds Javadoc about non-thread-safety / per-query instantiation in the JPA transformer.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add OR operator to QuerySpec constraints' directly and clearly describes the main feature introduced: adding an OR operator capability to QuerySpec constraints.
Linked Issues check ✅ Passed The PR implements the core requirement from #61: adds an OR operator mechanism for Constraint instances through the new DisjunctionConstraint class and or() method.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the OR operator feature: new DisjunctionConstraint class, or() convenience method, transformer support, tests, and version bumps are all in-scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch querySpec-or
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can generate a title for your PR based on the changes.

Add @coderabbitai placeholder anywhere in the title of your PR and CodeRabbit will replace it with a title based on the changes in the PR. You can change the placeholder by changing the reviews.auto_title_placeholder setting.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java`:
- Around line 173-175: The stream currently maps child constraints with
this::transform which bypasses the public unsupported-constraint guard and can
produce null entries; instead call the transformer’s public apply(...) for each
child so any ConstraintTransformerException is thrown immediately. Replace the
mapping in ConstraintTransformerJpaImpl where predicates are built (the
c.getConstraints() stream that produces predicates) to invoke apply(constraint)
for each child constraint (e.g., map(constraint -> apply(constraint))) so
unsupported constraints are surfaced rather than allowing nulls into the
Predicate[].
- Around line 171-176: transformDisjunctionConstraint currently maps child
constraints with this::transform (which returns null for unsupported
constraints) and uses path navigation that defaults to JoinType.INNER, causing
nullable associations to be filtered out before OR evaluation; change the
implementation to use apply() instead of transform() so unsupported child
constraints throw, and ensure path navigation invoked during disjunctions uses
left-join-safe handling (use LEFT joins for attribute path resolution when
building predicates for transformDisjunctionConstraint) so expressions like
"city.name = X OR id = Y" will include rows with null city; keep combining the
resulting Predicate[] with criteriaBuilder.or(...) as before.

In
`@backend-core-data-impl/src/test/java/com/flowingcode/backendcore/dao/jpa/JpaDaoSupportTest.java`:
- Around line 150-167: The tests testFilterWithOrConstraint and
testFilterWithOrConstraintPartialMatch rely on generated city names being
distinct; make the predicates deterministic by comparing on city.id (e.g.,
ConstraintBuilder.of("city","id").equal(...)) or by using fixed known names
instead of cities.get(...).getName() so the OR branches are unambiguous—update
the ConstraintBuilder calls in both tests (the ones using "city","name") to use
"city","id" (or fixed literals) and adjust expected counts if necessary.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 65232f81-59a7-4593-92e7-142f997b555d

📥 Commits

Reviewing files that changed from the base of the PR and between 39116c3 and 425f07f.

📒 Files selected for processing (5)
  • backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java
  • backend-core-data-impl/src/test/java/com/flowingcode/backendcore/dao/jpa/JpaDaoSupportTest.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/ConstraintTransformer.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java

@javier-godoy javier-godoy marked this pull request as draft March 16, 2026 19:11
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java (1)

87-90: ⚠️ Potential issue | 🟠 Major

LEFT-join disjunction can still reuse an INNER join and drop nullable rows.

At Line 89, existing joins are matched only by attribute name. If an INNER join was created earlier on the same path, the disjunction at Line 174 (which switches to LEFT) can still reuse that INNER join, so rows with null associations may still be filtered before the OR is evaluated.

Suggested fix
 `@SuppressWarnings`("rawtypes")
 private From<?,?> join(From<?,?> source, String attributeName) {
-	Optional<Join> existingJoin = source.getJoins().stream().filter(join->join.getAttribute().getName().equals(attributeName)).map(join->(Join)join).findFirst();
+	Optional<Join> existingJoin = source.getJoins().stream()
+			.map(join -> (Join) join)
+			.filter(join -> join.getAttribute().getName().equals(attributeName))
+			.filter(join -> join.getJoinType() == currentJoinType)
+			.findFirst();
 	return existingJoin.orElseGet(()->source.join(attributeName, currentJoinType));
 }

Also applies to: 174-181

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java`
around lines 87 - 90, The join reuse logic in join(From<?,?> source, String
attributeName) matches only on attribute name and can return an INNER join when
currentJoinType is LEFT; update the matching so you only reuse an existing Join
if both the attribute name and the Join.getJoinType() equal the
requested/currentJoinType, otherwise create a new join with
source.join(attributeName, currentJoinType); apply the same change to the other
join helper used around the disjunction (the overloaded join method at the
174-181 area) so LEFT joins are never replaced by previously-created INNER
joins.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java`:
- Around line 87-90: The join reuse logic in join(From<?,?> source, String
attributeName) matches only on attribute name and can return an INNER join when
currentJoinType is LEFT; update the matching so you only reuse an existing Join
if both the attribute name and the Join.getJoinType() equal the
requested/currentJoinType, otherwise create a new join with
source.join(attributeName, currentJoinType); apply the same change to the other
join helper used around the disjunction (the overloaded join method at the
174-181 area) so LEFT joins are never replaced by previously-created INNER
joins.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 012bb055-a583-4c99-8b5a-7dee1619ed7a

📥 Commits

Reviewing files that changed from the base of the PR and between a7c4315 and 16a2c21.

📒 Files selected for processing (2)
  • backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java
  • backend-core-data-impl/src/test/java/com/flowingcode/backendcore/dao/jpa/JpaDaoSupportTest.java

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java (1)

39-44: Consider null-safety for constraint elements.

The factory accepts varargs that could contain null elements, which would propagate to the list and potentially cause NPEs during transformation. The first parameter also lacks explicit null validation.

💡 Proposed defensive implementation
 public static DisjunctionConstraint of(Constraint first, Constraint... rest) {
+   Objects.requireNonNull(first, "first constraint must not be null");
   List<Constraint> list = new java.util.ArrayList<>();
   list.add(first);
-  list.addAll(Arrays.asList(rest));
+  for (Constraint c : rest) {
+    list.add(Objects.requireNonNull(c, "constraint must not be null"));
+  }
   return new DisjunctionConstraint(list);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java`
around lines 39 - 44, The factory DisjunctionConstraint.of(Constraint first,
Constraint... rest) should defensively validate inputs: throw a clear exception
(e.g., NullPointerException or IllegalArgumentException) if first is null, and
filter out any null entries from the varargs before constructing the list;
update the method that builds the internal List<Constraint> so it only adds
non-null elements from rest (or throws if an empty/invalid set is produced) and
then passes that sanitized list into the DisjunctionConstraint constructor to
avoid NPEs during later transformation/processing.
backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java (1)

84-89: Mutable state for join type introduced.

The currentJoinType field makes this class not thread-safe. This is acceptable if instances are created per-query (which appears to be the case from constructor usage), but consider adding a class-level Javadoc note about thread-safety expectations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java`
around lines 84 - 89, The class currently holds mutable state in the field
currentJoinType (used by the join(From<?,?>, String) method), making instances
not thread-safe; add a class-level Javadoc on ConstraintTransformerJpaImpl that
clearly states instances are not thread-safe, that currentJoinType is mutable
and that callers must create a new instance per query (referencing the
constructor and the join(...) method), or alternatively remove the mutable field
and pass JoinType as a parameter to join(...) to make instances
thread-safe—include which approach you choose in the Javadoc so callers know
usage expectations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java`:
- Around line 84-89: The class currently holds mutable state in the field
currentJoinType (used by the join(From<?,?>, String) method), making instances
not thread-safe; add a class-level Javadoc on ConstraintTransformerJpaImpl that
clearly states instances are not thread-safe, that currentJoinType is mutable
and that callers must create a new instance per query (referencing the
constructor and the join(...) method), or alternatively remove the mutable field
and pass JoinType as a parameter to join(...) to make instances
thread-safe—include which approach you choose in the Javadoc so callers know
usage expectations.

In
`@backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java`:
- Around line 39-44: The factory DisjunctionConstraint.of(Constraint first,
Constraint... rest) should defensively validate inputs: throw a clear exception
(e.g., NullPointerException or IllegalArgumentException) if first is null, and
filter out any null entries from the varargs before constructing the list;
update the method that builds the internal List<Constraint> so it only adds
non-null elements from rest (or throws if an empty/invalid set is produced) and
then passes that sanitized list into the DisjunctionConstraint constructor to
avoid NPEs during later transformation/processing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 37b60e1c-ddd8-4860-ac1c-ec7009d5140b

📥 Commits

Reviewing files that changed from the base of the PR and between 16a2c21 and 7a5834f.

📒 Files selected for processing (12)
  • backend-core-business-impl/pom.xml
  • backend-core-business-spring-impl/pom.xml
  • backend-core-business/pom.xml
  • backend-core-data-impl/pom.xml
  • backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java
  • backend-core-data-impl/src/test/java/com/flowingcode/backendcore/dao/jpa/JpaDaoSupportTest.java
  • backend-core-data/pom.xml
  • backend-core-model/pom.xml
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/ConstraintTransformer.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java
  • pom.xml
🚧 Files skipped from review as they are similar to previous changes (8)
  • pom.xml
  • backend-core-data/pom.xml
  • backend-core-business/pom.xml
  • backend-core-data-impl/src/test/java/com/flowingcode/backendcore/dao/jpa/JpaDaoSupportTest.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java
  • backend-core-model/pom.xml
  • backend-core-business-spring-impl/pom.xml
  • backend-core-data-impl/pom.xml

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java (1)

33-46: Consider making the constraint list immutable.

The @Getter on constraints exposes the internal mutable ArrayList, allowing callers to modify it after construction. For a value object representing a logical OR, immutability is preferred.

♻️ Proposed fix to return an unmodifiable list
+import java.util.Collections;
...
 `@Getter`
 `@RequiredArgsConstructor`
 `@FieldDefaults`(level = AccessLevel.PRIVATE, makeFinal = true)
 public class DisjunctionConstraint implements Constraint {

-  `@NonNull` List<Constraint> constraints;
+  `@NonNull` List<Constraint> constraints;
+
+  public List<Constraint> getConstraints() {
+    return Collections.unmodifiableList(constraints);
+  }

   public static DisjunctionConstraint of(Constraint first, Constraint... rest) {
     List<Constraint> list = new java.util.ArrayList<>();
     list.add(Objects.requireNonNull(first, "constraint must not be null"));
     for (Constraint c : rest) {
       list.add(Objects.requireNonNull(c, "constraint must not be null"));
     }
-    return new DisjunctionConstraint(list);
+    return new DisjunctionConstraint(Collections.unmodifiableList(list));
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java`
around lines 33 - 46, The constraints List exposed by DisjunctionConstraint
(field constraints and getter generated by `@Getter`) is currently mutable; make
it immutable by wrapping the list used in the constructor/factory (e.g., in
DisjunctionConstraint.of(Constraint first, Constraint... rest)) with an
unmodifiable copy (for example use Collections.unmodifiableList(new
ArrayList<>(list)) or List.copyOf(list)) before constructing the
DisjunctionConstraint so callers cannot modify the internal list after creation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java`:
- Around line 33-46: The constraints List exposed by DisjunctionConstraint
(field constraints and getter generated by `@Getter`) is currently mutable; make
it immutable by wrapping the list used in the constructor/factory (e.g., in
DisjunctionConstraint.of(Constraint first, Constraint... rest)) with an
unmodifiable copy (for example use Collections.unmodifiableList(new
ArrayList<>(list)) or List.copyOf(list)) before constructing the
DisjunctionConstraint so callers cannot modify the internal list after creation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e6fee4fd-4be0-4f36-aef8-eeeb1e9ed6f2

📥 Commits

Reviewing files that changed from the base of the PR and between 7a5834f and 6d553b4.

📒 Files selected for processing (2)
  • backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java

@javier-godoy
Copy link
Member Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Mar 17, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java (1)

32-37: Consider restricting constructor visibility (optional).

The Lombok-generated public constructor allows bypassing the factory method's per-element null validation. If you want to enforce the factory pattern:

♻️ Optional: Make constructor private
 `@Getter`
-@RequiredArgsConstructor
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
 `@FieldDefaults`(level = AccessLevel.PRIVATE, makeFinal = true)
 public class DisjunctionConstraint implements Constraint {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java`
around lines 32 - 37, The public constructor generated by Lombok
(`@RequiredArgsConstructor`) on DisjunctionConstraint lets callers bypass the
factory null-checks for the constraints field; change the constructor visibility
to private by updating the Lombok annotation to generate a private constructor
(e.g., `@RequiredArgsConstructor`(access = AccessLevel.PRIVATE)) so callers must
use the factory method that enforces per-element null validation for the
List<Constraint> constraints.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java`:
- Around line 39-47: The or(Constraint first, Constraint... rest) path assumes
rest is non-null and will NPE in prepend(...) for calls like or(x,
(Constraint[]) null); modify prepend(Constraint first, Constraint[] rest) (or
add a null-guard in or()) to treat a null rest as an empty array before creating
the result array and calling System.arraycopy — e.g., normalize rest to an empty
Constraint[] when null so the array creation and copy logic work safely and
subsequent constraint-level null validations can run.

---

Nitpick comments:
In
`@backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java`:
- Around line 32-37: The public constructor generated by Lombok
(`@RequiredArgsConstructor`) on DisjunctionConstraint lets callers bypass the
factory null-checks for the constraints field; change the constructor visibility
to private by updating the Lombok annotation to generate a private constructor
(e.g., `@RequiredArgsConstructor`(access = AccessLevel.PRIVATE)) so callers must
use the factory method that enforces per-element null validation for the
List<Constraint> constraints.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 60535b5a-a58f-4190-8082-a5ec1579447d

📥 Commits

Reviewing files that changed from the base of the PR and between 39116c3 and b75b25d.

📒 Files selected for processing (12)
  • backend-core-business-impl/pom.xml
  • backend-core-business-spring-impl/pom.xml
  • backend-core-business/pom.xml
  • backend-core-data-impl/pom.xml
  • backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java
  • backend-core-data-impl/src/test/java/com/flowingcode/backendcore/dao/jpa/JpaDaoSupportTest.java
  • backend-core-data/pom.xml
  • backend-core-model/pom.xml
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/ConstraintTransformer.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java
  • pom.xml

Comment on lines +39 to +47
default Constraint or(Constraint first, Constraint... rest) {
return DisjunctionConstraint.of(this, prepend(first, rest));
}

private static Constraint[] prepend(Constraint first, Constraint[] rest) {
Constraint[] result = new Constraint[1 + rest.length];
result[0] = first;
System.arraycopy(rest, 0, result, 1, rest.length);
return result;
Copy link

@coderabbitai coderabbitai bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle explicit null varargs defensively in or(...).

Line 43 assumes rest is non-null. A call like or(x, (Constraint[]) null) throws an NPE in prepend(...) before constraint-level null validation runs.

Suggested fix
 import com.flowingcode.backendcore.model.constraints.DisjunctionConstraint;
 import com.flowingcode.backendcore.model.constraints.NegatedConstraint;
+import java.util.Objects;
@@
   default Constraint or(Constraint first, Constraint... rest) {
-    return DisjunctionConstraint.of(this, prepend(first, rest));
+    return DisjunctionConstraint.of(this, prepend(first, Objects.requireNonNull(rest, "rest must not be null")));
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
default Constraint or(Constraint first, Constraint... rest) {
return DisjunctionConstraint.of(this, prepend(first, rest));
}
private static Constraint[] prepend(Constraint first, Constraint[] rest) {
Constraint[] result = new Constraint[1 + rest.length];
result[0] = first;
System.arraycopy(rest, 0, result, 1, rest.length);
return result;
default Constraint or(Constraint first, Constraint... rest) {
return DisjunctionConstraint.of(this, prepend(first, Objects.requireNonNull(rest, "rest must not be null")));
}
private static Constraint[] prepend(Constraint first, Constraint[] rest) {
Constraint[] result = new Constraint[1 + rest.length];
result[0] = first;
System.arraycopy(rest, 0, result, 1, rest.length);
return result;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java`
around lines 39 - 47, The or(Constraint first, Constraint... rest) path assumes
rest is non-null and will NPE in prepend(...) for calls like or(x,
(Constraint[]) null); modify prepend(Constraint first, Constraint[] rest) (or
add a null-guard in or()) to treat a null rest as an empty array before creating
the result array and calling System.arraycopy — e.g., normalize rest to an empty
Constraint[] when null so the array creation and copy logic work safely and
subsequent constraint-level null validations can run.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai please check if the comment has been addressed.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@javier-godoy, based on the current file content, the comment has not been addressed yet. The or(Constraint first, Constraint... rest) method still passes rest directly to prepend(...) without any null-guard:

default Constraint or(Constraint first, Constraint... rest) {
    return DisjunctionConstraint.of(this, prepend(first, rest));
}

A call like or(x, (Constraint[]) null) will still throw an NPE inside prepend(...) at rest.length before any constraint-level validation runs. The suggested fix (Objects.requireNonNull(rest, "rest must not be null")) has not been applied.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai Note that DisjunctionConstraint.of already checks whether the varargs is null

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd "DisjunctionConstraint.java" --exec cat {}

Length of output: 1759


@javier-godoy, you're right that DisjunctionConstraint.of does include a null check (Objects.requireNonNull(rest, "constraints must not be null")). However, that check is never reached in this code path when rest is null. The call chain is:

  1. or(first, null) → calls prepend(first, null)
  2. In prepend: new Constraint[1 + rest.length]NPE here (no message), before DisjunctionConstraint.of is ever invoked.

So the null guard in DisjunctionConstraint.of doesn't cover this case — prepend fails first. The original concern remains: a caller passing or(x, (Constraint[]) null) gets an NPE with no helpful message, whereas the guard in DisjunctionConstraint.of would give "constraints must not be null".

That said, explicitly casting null to Constraint[] is a very unusual call pattern, and both paths still fail fast with an NPE. If you consider this acceptable behavior, I can close the comment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai I consider this acceptable behavior.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@javier-godoy, understood — acknowledged and noted. The existing fast-fail NPE behavior for or(x, (Constraint[]) null) is acceptable as-is.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java`:
- Around line 39-44: DisjunctionConstraint.of currently assumes the varargs
array 'rest' is non-null and will throw an implicit NPE in the for-loop; add an
explicit null check for the varargs container before iterating (e.g.,
Objects.requireNonNull(rest, "constraints must not be null")) so callers calling
of(first, (Constraint[]) null) get a predictable error, then continue to
validate each element in 'rest' with Objects.requireNonNull as already done.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3e688aad-15c0-42c2-b24c-b0b3f2181db7

📥 Commits

Reviewing files that changed from the base of the PR and between 7a5834f and 90692ec.

📒 Files selected for processing (5)
  • backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java
  • backend-core-data-impl/src/test/java/com/flowingcode/backendcore/dao/jpa/JpaDaoSupportTest.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/ConstraintTransformer.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/ConstraintTransformer.java
  • backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java (1)

39-47: ⚠️ Potential issue | 🟡 Minor

Guard the varargs container before prepend(...).

or(x, (Constraint[]) null) still blows up on rest.length inside prepend() before DisjunctionConstraint.of(...) can validate anything. Treat a null varargs container as an empty array here so this path behaves predictably.

Suggested fix
   private static Constraint[] prepend(Constraint first, Constraint[] rest) {
+    rest = rest == null ? new Constraint[0] : rest;
     Constraint[] result = new Constraint[1 + rest.length];
     result[0] = first;
     System.arraycopy(rest, 0, result, 1, rest.length);
     return result;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java`
around lines 39 - 47, The prepend(...) helper doesn't guard against a null
varargs array, so calling or(x, (Constraint[]) null) throws on rest.length
before DisjunctionConstraint.of(...) can validate; update the code to treat a
null rest as an empty array (e.g., if rest == null then set rest = new
Constraint[0]) either inside prepend(Constraint first, Constraint[] rest) or by
handling null before calling prepend in or(...), ensuring
DisjunctionConstraint.of(this, prepend(...)) always receives a non-null array.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java`:
- Around line 39-47: The prepend(...) helper doesn't guard against a null
varargs array, so calling or(x, (Constraint[]) null) throws on rest.length
before DisjunctionConstraint.of(...) can validate; update the code to treat a
null rest as an empty array (e.g., if rest == null then set rest = new
Constraint[0]) either inside prepend(Constraint first, Constraint[] rest) or by
handling null before calling prepend in or(...), ensuring
DisjunctionConstraint.of(this, prepend(...)) always receives a non-null array.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cff07aaf-5473-4d1c-9fe5-2e15f414dba8

📥 Commits

Reviewing files that changed from the base of the PR and between 90692ec and 4c30971.

📒 Files selected for processing (5)
  • backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java
  • backend-core-data-impl/src/test/java/com/flowingcode/backendcore/dao/jpa/JpaDaoSupportTest.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/ConstraintTransformer.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/ConstraintTransformer.java

@javier-godoy javier-godoy marked this pull request as ready for review March 17, 2026 12:37
@javier-godoy javier-godoy marked this pull request as draft March 17, 2026 12:45
@sonarqubecloud
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java`:
- Around line 92-95: The join() method currently reuses existing joins based
only on attribute name, which can reuse an INNER join when currentJoinType is
LEFT and break OR semantics in transformDisjunctionConstraint; update
join(From<?,?> source, String attributeName) to check both
join.getAttribute().getName().equals(attributeName) and that the existing Join's
join type equals currentJoinType (compare via join.getJoinType() or equivalent),
and only reuse when both match—otherwise call source.join(attributeName,
currentJoinType) to create a join with the correct type.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 80d29861-6306-4ddc-b1a4-d993da05803d

📥 Commits

Reviewing files that changed from the base of the PR and between 4c30971 and 2060334.

📒 Files selected for processing (6)
  • backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java
  • backend-core-data-impl/src/test/java/com/flowingcode/backendcore/dao/jpa/JpaDaoSupportTest.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/ConstraintTransformer.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java
  • backend-core-model/src/test/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraintTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/ConstraintTransformer.java
  • backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java

Comment on lines 92 to 95
private From<?,?> join(From<?,?> source, String attributeName) {
Optional<Join> existingJoin = source.getJoins().stream().filter(join->join.getAttribute().getName().equals(attributeName)).map(join->(Join)join).findFirst();
return existingJoin.orElseGet(()->source.join(attributeName, JoinType.INNER));
return existingJoin.orElseGet(()->source.join(attributeName, currentJoinType));
}
Copy link

@coderabbitai coderabbitai bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

impl_file="$(fd ConstraintTransformerJpaImpl.java | head -n1)"
test_file="$(fd JpaDaoSupportTest.java | head -n1)"

echo "== join(...) implementation around Line 92 =="
sed -n '88,102p' "$impl_file"

echo
echo "== occurrences of getJoinType() in transformer (expecting join-type-aware reuse) =="
rg -n 'getJoinType\(\)' "$impl_file" || true

echo
echo "== OR-related tests currently present =="
rg -n 'testFilterWithOrConstraint|testFilterWithOrConstraintPartialMatch|\\.or\\(' "$test_file" -A3 -B2

Repository: FlowingCode/backend-core

Length of output: 1805


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the implementation file
impl_file="backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java"

if [ -f "$impl_file" ]; then
    echo "== join(...) implementation around Line 92 =="
    sed -n '88,102p' "$impl_file"
    
    echo
    echo "== All getJoinType() calls in transformer =="
    rg -n 'getJoinType\(\)' "$impl_file" -B2 -A2 || true
    
    echo
    echo "== transformDisjunctionConstraint method context (lines 179-186) =="
    sed -n '175,190p' "$impl_file"
else
    echo "File not found at: $impl_file"
    # Try to locate it
    find . -name "ConstraintTransformerJpaImpl.java" -type f 2>/dev/null || echo "No ConstraintTransformerJpaImpl.java found"
fi

Repository: FlowingCode/backend-core

Length of output: 1388


Make join reuse join-type-aware to preserve OR semantics.

The join() method (lines 92-95) reuses any existing join by attribute name only. When transformDisjunctionConstraint() switches currentJoinType to LEFT (line 183), a previous INNER join with the same attribute name will be incorrectly reused, causing the OR evaluation to apply INNER join semantics (dropping nullable-association rows) before OR logic.

The suggested patch correctly adds a join-type check to ensure reused joins match both the attribute name and the current join type.

Suggested patch
 `@SuppressWarnings`("rawtypes")
 private From<?,?> join(From<?,?> source, String attributeName) {
-    Optional<Join> existingJoin = source.getJoins().stream().filter(join->join.getAttribute().getName().equals(attributeName)).map(join->(Join)join).findFirst();
-    return existingJoin.orElseGet(()->source.join(attributeName, currentJoinType));
+    Optional<Join> existingJoin = source.getJoins().stream()
+            .map(join -> (Join) join)
+            .filter(join -> join.getAttribute().getName().equals(attributeName))
+            .filter(join -> join.getJoinType() == currentJoinType)
+            .findFirst();
+    return existingJoin.orElseGet(() -> source.join(attributeName, currentJoinType));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java`
around lines 92 - 95, The join() method currently reuses existing joins based
only on attribute name, which can reuse an INNER join when currentJoinType is
LEFT and break OR semantics in transformDisjunctionConstraint; update
join(From<?,?> source, String attributeName) to check both
join.getAttribute().getName().equals(attributeName) and that the existing Join's
join type equals currentJoinType (compare via join.getJoinType() or equivalent),
and only reuse when both match—otherwise call source.join(attributeName,
currentJoinType) to create a join with the correct type.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai The finding is technically correct — the filter on line 93 does not check join type — but it has no observable effect on query results. When a prior non-disjunction constraint has already created an INNER JOIN on an association, rows with a null foreign key are eliminated by that join before any WHERE predicate is evaluated, including the OR predicate from the disjunction. Conversely, when the disjunction runs first and creates a LEFT JOIN, any subsequent non-disjunction predicate on that association (e.g. city.population > 0) evaluates to NULL for no-city rows, which SQL treats as UNKNOWN and filters out — the same rows excluded by an INNER JOIN. In both orderings the final result set is identical. The fix would not change which rows are returned.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@javier-godoy, that's a well-reasoned explanation — thank you for the detailed walkthrough. You're right on both orderings:

  • INNER JOIN pre-exists: null-FK rows are already absent from the result set before the OR predicate is even applied, so reusing the INNER join inside transformDisjunctionConstraint produces no different outcome.
  • LEFT JOIN pre-exists (disjunction ran first): any subsequent equality/comparison predicate on the joined table's columns will evaluate to UNKNOWN for the null-padded rows (SQL three-valued logic), causing them to be silently excluded — effectively the same result as an INNER join would produce.

I'll retract the suggestion. The code is functionally correct as written; the join-type mismatch has no observable effect on query results.

ᓚᘏᗢ

@javier-godoy javier-godoy marked this pull request as ready for review March 17, 2026 13:54
@javier-godoy
Copy link
Member Author

javier-godoy commented Mar 17, 2026

#108 (comment): the finding is technically correct — the filter on line 93 does not check join type — but it has no observable effect on query results.

Setup (from the test fixture):

  • persistedPerson — John Doe, no city (city = null)
  • 5 persons assigned to city A (cities[0], name = "Springfield")
  • 5 persons assigned to city B (cities[1], name = "Shelbyville")

Case 1 — INNER JOIN pre-exists, disjunction runs second

// Constraint 1: non-disjunction → creates INNER JOIN on city
pf.addConstraint(ConstraintBuilder.of("city", "name").equal(cities.get(0).getName()));

// Constraint 2: disjunction → join() finds the existing INNER JOIN by attribute name
// and reuses it instead of creating a LEFT JOIN (the reported issue)
pf.addConstraint(
    ConstraintBuilder.of("city", "id").equal(cities.get(0).getId())
        .or(ConstraintBuilder.of("id").equal(persistedPerson.getId())));

Current code (INNER JOIN reused inside the OR):

FROM person
INNER JOIN city ON person.city_id = city.id   -- eliminates persistedPerson here
WHERE city.name = 'Springfield'
  AND (city.id = ? OR person.id = ?)          -- persistedPerson never reaches this

5 rows

"Fixed" code (separate LEFT JOIN created for the OR branch):

FROM person
INNER JOIN city c1 ON person.city_id = c1.id  -- still eliminates persistedPerson
LEFT JOIN  city c2 ON person.city_id = c2.id
WHERE c1.name = 'Springfield'
  AND (c2.id = ? OR person.id = ?)

5 rows — the INNER JOIN on c1 already removed persistedPerson before WHERE runs.


Case 2 — LEFT JOIN pre-exists (disjunction ran first), non-disjunction runs second

// Constraint 1: disjunction → creates LEFT JOIN on city
pf.addConstraint(
    ConstraintBuilder.of("city", "id").equal(cities.get(0).getId())
        .or(ConstraintBuilder.of("id").equal(persistedPerson.getId())));

// Constraint 2: non-disjunction → join() finds the existing LEFT JOIN by attribute name
// and reuses it instead of creating a new INNER JOIN (the reported issue)
pf.addConstraint(ConstraintBuilder.of("city", "name").equal(cities.get(0).getName()));

Current code (LEFT JOIN reused for the equality predicate on city.name):

FROM person
LEFT JOIN city ON person.city_id = city.id
WHERE (city.id = ? OR person.id = ?)   -- persistedPerson matches (person.id = ?)
  AND city.name = 'Springfield'        -- city.name is NULL for persistedPerson
                                       -- NULL = 'Springfield' → UNKNOWN → row excluded

5 rows

"Fixed" code (new INNER JOIN created for city.name):

FROM person
LEFT JOIN  city c1 ON person.city_id = c1.id
INNER JOIN city c2 ON person.city_id = c2.id  -- eliminates persistedPerson
WHERE (c1.id = ? OR person.id = ?)
  AND c2.name = 'Springfield'

5 rowspersistedPerson is excluded by the INNER JOIN rather than by UNKNOWN evaluation, but the count is identical.


In both orderings the result is 5. persistedPerson is never included because the non-disjunction predicate on city always removes null-city rows — either via INNER JOIN elimination (when that join pre-exists) or via SQL three-valued logic treating NULL = 'Springfield' as UNKNOWN (when only a LEFT JOIN exists). The join-type mismatch in join() has no observable effect on the returned rows.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add OR operator to QuerySpec constraints

1 participant