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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,17 @@ public static Context getThreadLocalContext(Context lctx, Context cloneFrom) {
return null;
}

/**
* Execute a set operation (UNION, INTERSECT, EXCEPT). The operands are
* compiled into a single compound SQL statement: operand 0 becomes the
* main {@link Select}, every later operand is rendered on its own and
* concatenated onto it via
* {@link org.apache.openjpa.jdbc.sql.SelectImpl#addSetOperatorSQL}.
* <p>
* A {@link Range} is therefore applied in memory over the compound
* result and is never pushed into the SQL: the range of the main select
* would bind to the first operand only, not to the compound result.
*/
private ResultObjectProvider executeSetOperatorQuery(Executor ex,
ClassMetaData base, ClassMetaData[] metas, boolean subclasses,
ExpressionFactory[] facts, QueryExpressions[] exps,
Expand Down Expand Up @@ -1235,13 +1246,28 @@ private ResultObjectProvider executeSetOperatorQuery(Executor ex,

localContext.remove();

ResultObjectProvider rop;
if (mainExps.projections.length > 0) {
return new ProjectionResultObjectProvider(
rop = new ProjectionResultObjectProvider(
mainSel, mainExps, mainState,
new ExpContext(_store, params, fetch));
} else {
rop = new InstanceResultObjectProvider(
mainSel, mapping, _store, fetch);
}
return new InstanceResultObjectProvider(
mainSel, mapping, _store, fetch);

// OPENJPA-2964: the range cannot be pushed into the SQL of a set
// operation. DBDictionary.toSelect() renders the first operand in
// full - including any LIMIT/OFFSET derived from the select's start
// and end index - and only then appends the set operator buffer, so
// a range set on the main select would bind to the first operand
// instead of to the compound result. Apply it in memory over the
// compound result instead; this is the same fallback executeQuery()
// uses when the dictionary cannot express the range in SQL.
if (range.start != 0 || range.end != Long.MAX_VALUE) {
rop = new RangeResultObjectProvider(rop, range.start, range.end);
}
return rop;
}

private void flattenSetOperator(QueryExpressions exps,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,138 @@ public void testIntersectProjection() {
endEm(em);
}

/**
* A UNION ALL of "age &gt; 25" (Seetha, Shannon, Famzy) and "age &gt; 30"
* (Seetha, Shannon), i.e. 5 rows, using the given identification
* variable. Every execution below needs its own JPQL string, see
* {@link #testSetOperatorRange()}.
*/
private String unionAllQuery(String var) {
return "SELECT " + var + ".name FROM CompUser " + var
+ " WHERE " + var + ".age > 25"
+ " UNION ALL SELECT " + var + ".name FROM CompUser " + var
+ " WHERE " + var + ".age > 30";
}

/**
* A UNION of "age &gt; 30" (Seetha, Shannon) and "name = 'Ugo'", i.e. 3
* rows after duplicate elimination, using the given identification
* variable.
*/
private String unionQuery(String var) {
return "SELECT " + var + ".name FROM CompUser " + var
+ " WHERE " + var + ".age > 30"
+ " UNION SELECT " + var + ".name FROM CompUser " + var
+ " WHERE " + var + ".name = 'Ugo'";
}

/**
* An INTERSECT of "age &gt; 20" and "age &gt; 30", i.e. 2 rows (Seetha,
* Shannon), using the given identification variable.
*/
private String intersectQuery(String var) {
return "SELECT " + var + ".name FROM CompUser " + var
+ " WHERE " + var + ".age > 20"
+ " INTERSECT SELECT " + var + ".name FROM CompUser " + var
+ " WHERE " + var + ".age > 30";
}

/**
* An EXCEPT of "age &gt; 20" and "age &gt; 30", i.e. 2 rows (Famzy, Shade),
* using the given identification variable.
*/
private String exceptQuery(String var) {
return "SELECT " + var + ".name FROM CompUser " + var
+ " WHERE " + var + ".age > 20"
+ " EXCEPT SELECT " + var + ".name FROM CompUser " + var
+ " WHERE " + var + ".age > 30";
}

/**
* The same UNION as {@link #unionQuery(String)}, but selecting the entity
* rather than a projection, so that the range is applied over an
* InstanceResultObjectProvider instead of a ProjectionResultObjectProvider.
*/
private String entityUnionQuery(String var) {
return "SELECT " + var + " FROM CompUser " + var
+ " WHERE " + var + ".age > 30"
+ " UNION SELECT " + var + " FROM CompUser " + var
+ " WHERE " + var + ".name = 'Ugo'";
}

/**
* OPENJPA-2964: setFirstResult()/setMaxResults() must be honoured for a
* set operation. The range cannot be pushed into the SQL of a compound
* statement, so it is applied in memory over the compound result.
* <p>
* Only the cardinality of the result is asserted: JPQL attaches an
* ORDER BY to an individual select, so the row order of a compound set
* operation is not defined and the identity of the rows on a given page
* is unspecified.
* <p>
* Every execution uses its own JPQL string (the identification variable
* differs) because re-executing the very same set operation string is
* broken by the prepared query SQL cache - an unrelated, pre-existing
* defect that has nothing to do with the range.
*/
public void testSetOperatorRange() {
EntityManager em = currentEntityManager();

// UNION ALL, 3 rows + 2 rows = 5. The max-4 case is load-bearing:
// it is unsatisfiable if the bound had been pushed into operand 1,
// which yields only 3 rows.
assertEquals(5, em.createQuery(unionAllQuery("a1"))
.getResultList().size());
assertEquals(4, em.createQuery(unionAllQuery("a2")).setMaxResults(4)
.getResultList().size());
assertEquals(2, em.createQuery(unionAllQuery("a3")).setFirstResult(3)
.getResultList().size());
assertEquals(2, em.createQuery(unionAllQuery("a4")).setFirstResult(1)
.setMaxResults(2).getResultList().size());
assertEquals(0, em.createQuery(unionAllQuery("a5")).setFirstResult(5)
.getResultList().size());

// UNION (duplicate elimination), 3 distinct rows - same query as
// testUnionProjection. setFirstResult(2) must yield 1, proving the
// window is applied after the database's dedup (a window over the
// 5-row pre-dedup stream would yield more).
assertEquals(3, em.createQuery(unionQuery("b1"))
.getResultList().size());
assertEquals(2, em.createQuery(unionQuery("b2")).setMaxResults(2)
.getResultList().size());
assertEquals(1, em.createQuery(unionQuery("b3")).setFirstResult(2)
.getResultList().size());
// a range wider than the result set is a no-op
assertEquals(3, em.createQuery(unionQuery("b4")).setMaxResults(10)
.getResultList().size());

// INTERSECT, 2 rows - same query as testIntersectProjection.
assertEquals(1, em.createQuery(intersectQuery("c1")).setMaxResults(1)
.getResultList().size());
assertEquals(1, em.createQuery(intersectQuery("c2")).setFirstResult(1)
.getResultList().size());
assertEquals(0, em.createQuery(intersectQuery("c3")).setFirstResult(2)
.getResultList().size());

// EXCEPT, 2 rows - same query as testExceptProjection.
assertEquals(1, em.createQuery(exceptQuery("d1")).setMaxResults(1)
.getResultList().size());
assertEquals(1, em.createQuery(exceptQuery("d2")).setFirstResult(1)
.getResultList().size());
assertEquals(0, em.createQuery(exceptQuery("d3")).setFirstResult(2)
.getResultList().size());

// the entity, not a projection: the other provider branch.
assertEquals(3, em.createQuery(entityUnionQuery("e1"))
.getResultList().size());
assertEquals(2, em.createQuery(entityUnionQuery("e2")).setMaxResults(2)
.getResultList().size());
assertEquals(1, em.createQuery(entityUnionQuery("e3")).setFirstResult(2)
.getResultList().size());

endEm(em);
}

public void testScalarOrderBy() {
EntityManager em = currentEntityManager();

Expand Down
Loading