Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import static ua.com.fielden.platform.eql.retrieval.EntityHibernateRetrievalQueryProducer.produceQueryWithoutPagination;
import static ua.com.fielden.platform.eql.retrieval.EntityResultTreeBuilder.build;
import static ua.com.fielden.platform.eql.retrieval.HibernateScalarsExtractor.getSortedScalars;
import static ua.com.fielden.platform.utils.EntityUtils.isEntityType;
import static ua.com.fielden.platform.utils.EntityUtils.isPersistentEntityType;

@Singleton
Expand Down Expand Up @@ -107,25 +108,25 @@ public <E extends AbstractEntity<?>> Stream<List<EntityContainer<E>>> streamAndE

private <E extends AbstractEntity<?>> QueryModelResult<E> getModelResult(final QueryProcessingModel<E, ?> qpm) {
class $ {
/**
* This predicate identifies cases where only ID is yielded, and a query needs to be extended to a query for retrieving an entity with that ID instead of just an ID value as a number.
*/
static boolean isIdOnlyQuery(final QueryModelResult<?> queryModelResult) {
// A "foreign query" is a query whose single explicit yield is an entity-typed property.
// This predicate identifies whether `queryModelResult` represents a "foreign query".
// An equivalent predicate is present in [EqlQueryTransformer] as method `isForeignIdOnlyQuery`.
static boolean isForeignIdOnlyQuery(final QueryModelResult<?> queryModelResult) {
return isPersistentEntityType(queryModelResult.resultType())
&& queryModelResult.yieldedColumns().size() == 1
&& ID.equals(queryModelResult.yieldedColumns().getFirst().name())
&& !(queryModelResult.fetchModel().getPrimProps().size() == 1 && queryModelResult.fetchModel().getPrimProps().contains(ID) &&
queryModelResult.fetchModel().getRetrievalModels().isEmpty());
// The type of ID can be either an entity or Long.
// If it is an entity type, this is a foreign id-only query (a whole entity is being yielded).
// Otherwise, ID is yielded as a number (a local id-only query).
&& isEntityType(queryModelResult.yieldedColumns().getFirst().propType().javaType());
}
}

final QueryModelResult<E> modelResult = eqlQueryTransformer.getModelResult(qpm, userProvider.getUsername());

// This piece of code is responsible for "re-fetching the whole entity by ID in order to be able to enhance it".
// This is necessary to convert yielded IDs to fully-fledged entities.
// Foreign id-only queries should be wrapped to access the whole graph of the entities being yielded.
// This does not apply to entity aggregates where IDs might be yielded – they are treated as numbers.
// See Issue #1991 (https://github.com/fieldenms/tg/issues/1991).
if ($.isIdOnlyQuery(modelResult)) {
if ($.isForeignIdOnlyQuery(modelResult)) {
final var idOnlyQuery = select(modelResult.resultType())
.where().prop(ID).in().model((SingleResultQueryModel<?>) qpm.queryModel)
.model();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import ua.com.fielden.platform.entity.query.IDbVersionProvider;
import ua.com.fielden.platform.entity.query.IFilter;
import ua.com.fielden.platform.entity.query.QueryProcessingModel;
import ua.com.fielden.platform.entity.query.model.SingleResultQueryModel;
import ua.com.fielden.platform.eql.meta.EqlTables;
import ua.com.fielden.platform.eql.meta.QuerySourceInfoProvider;
import ua.com.fielden.platform.eql.retrieval.records.QueryModelResult;
Expand All @@ -18,44 +19,43 @@
import ua.com.fielden.platform.eql.stage2.queries.ResultQuery2;
import ua.com.fielden.platform.eql.stage2.sources.enhance.PathsToTreeTransformer;
import ua.com.fielden.platform.eql.stage3.queries.ResultQuery3;
import ua.com.fielden.platform.eql.stage3.sundries.Yield3;
import ua.com.fielden.platform.eql.stage3.sundries.Yields3;
import ua.com.fielden.platform.meta.IDomainMetadata;
import ua.com.fielden.platform.utils.IDates;

import java.util.List;
import java.util.Optional;

import static java.util.Collections.unmodifiableList;
import static ua.com.fielden.platform.entity.AbstractEntity.ID;
import static ua.com.fielden.platform.entity.query.fluent.EntityQueryUtils.select;
import static ua.com.fielden.platform.utils.CollectionUtil.first;
import static ua.com.fielden.platform.utils.EntityUtils.isEntityType;
import static ua.com.fielden.platform.utils.EntityUtils.isPersistentEntityType;

/**
* An entry point for transforming an EQL query to SQL.
* <p>
* The transformation of EQL into SQL happens in 4 stages:
* <ol>
* <li> <b>Stage 0: parsing</b>.
* A {@linkplain ua.com.fielden.platform.eql.antlr.tokens.util.ListTokenSource sequence of EQL tokens} is transformed into a {@linkplain ResultQuery1 stage 1 AST}.
* See {@link ua.com.fielden.platform.eql.antlr.EqlCompiler}.
* <li> <b>Stage 1: property resolution</b>.
* Properties are resolved to their respective sources.
* An important part of this stage is the resolution of dot-notated properties.
* <li> <b>Stage 2</b>.
* <ul>
* <li> Processing of dot-expressions: builds up implicit table joins that result from dot-expressions,
* substitutes calculated property names used in dot-expressions with their respective expressions.
* <li> Substitution of literal values with parameters (crucial for strings as to prevent SQL injection).
* <li> Optimisation of union queries with an ordering.
* </ul>
*
* <li> <b>Stage 3: SQL generation</b>.
* This stage also gathers the information, needed to instantiate entities from the SQL query result.
* </ol>
* For stages 1-3 there is a corresponding package, named {@code ua.com.fielden.platform.eql.stage$N}, where {@code $N} is the stage number.
* Each package contains classes that comprise a stage-specific AST.
* These classes are named with a suffix that corresponds to their stage number.
* For example, {@code Prop1} represents a property in stage 1, and is a result of stage 0.
*
* @author TG Team
*/
/// An entry point for transforming an EQL query to SQL.
///
/// The transformation of EQL into SQL happens in 4 stages:
///
/// 1. **Stage 0: parsing**.
/// A [sequence of EQL tokens][ua.com.fielden.platform.eql.antlr.tokens.util.ListTokenSource] is transformed into a [stage 1 AST][ResultQuery1].
/// See [ua.com.fielden.platform.eql.antlr.EqlCompiler].
/// 2. **Stage 1: property resolution**.
/// Properties are resolved to their respective sources.
/// An important part of this stage is the resolution of dot-notated properties.
/// 3. **Stage 2**.
/// - Processing of dot-expressions: builds up implicit table joins that result from dot-expressions,
/// substitutes calculated property names used in dot-expressions with their respective expressions.
/// - Substitution of literal values with parameters (crucial for strings as to prevent SQL injection).
/// - Optimisation of union queries with an ordering.
/// 4. **Stage 3: SQL generation**.
/// This stage also gathers the information, needed to instantiate entities from the SQL query result.
///
/// For stages 1-3 there is a corresponding package, named `ua.com.fielden.platform.eql.stage$N`, where `$N` is the stage number.
/// Each package contains classes that comprise a stage-specific AST.
/// These classes are named with a suffix that corresponds to their stage number.
/// For example, `Prop1` represents a property in stage 1, and is a result of stage 0.
///
@Singleton
public final class EqlQueryTransformer {

Expand Down Expand Up @@ -85,6 +85,24 @@ public EqlQueryTransformer(
}

public <E extends AbstractEntity<?>> TransformationResultFromStage2To3<ResultQuery3> transform(
final QueryProcessingModel<E, ?> qpm,
final Optional<String> username)
{
final var result = transform_(qpm, username);

if (isForeignIdOnlyQuery(result.item)) {
final var idOnlyQuery = select((Class<E>) result.item.resultType)
.where().prop(ID).in().model((SingleResultQueryModel<?>) qpm.queryModel)
.model();
final var idOnlyQpm = new QueryProcessingModel<>(idOnlyQuery, qpm.orderModel, qpm.fetchModel, qpm.getParamValues(), qpm.lightweight);
return transform_(idOnlyQpm, username);
}
else {
return result;
}
}

private <E extends AbstractEntity<?>> TransformationResultFromStage2To3<ResultQuery3> transform_(
final QueryProcessingModel<E, ?> qem,
final Optional<String> username)
{
Expand All @@ -110,7 +128,24 @@ public <E extends AbstractEntity<?>> QueryModelResult<E> getModelResult(
}

private static List<YieldedColumn> getYieldedColumns(final Yields3 model) {
return unmodifiableList(model.getYields().stream().map(yield -> new YieldedColumn(yield.alias(), yield.type(), yield.column())).toList());
return model.getYields().stream().map(yield -> new YieldedColumn(yield.alias(), yield.type(), yield.column())).toList();
}

/// A "foreign query" is a query whose single explicit yield is an entity-typed property.
/// This predicate identifies whether `resultQuery` represents a "foreign query".
///
/// An equivalent predicate is present in [EntityContainerFetcherImpl] as part of method [getModelResult][EntityContainerFetcherImpl#getModelResult(QueryProcessingModel)].
///
private static boolean isForeignIdOnlyQuery(final ResultQuery3 resultQuery) {
final Yield3 yield;
return isPersistentEntityType(resultQuery.resultType)
&& resultQuery.yields.size() == 1
&& (yield = first(resultQuery.yields.getYields()).orElseThrow())
.alias().equals(ID)
// The type of ID can be either an entity or Long.
// If it is an entity type, this is a foreign id-only query (a whole entity is being yielded).
// Otherwise, ID is yielded as a number (a local id-only query).
&& isEntityType(yield.type().javaType());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.Currency;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.*;
import static ua.com.fielden.platform.entity.query.fluent.EntityQueryUtils.*;
import static ua.com.fielden.platform.test_utils.CollectionTestUtils.assertEqualByContents;
Expand All @@ -32,6 +33,31 @@
public class EntityQuery3ExecutionTest extends AbstractDaoTestCase {
private final IEntityAggregatesOperations aggregateDao = getInstance(IEntityAggregatesOperations.class);

@Test
public void local_id_only_query_with_id_only_fetch_returns_entities_with_ID_present() {
final var query = select(TgVehicle.class).model();
final var entities = co(TgVehicle.class).getAllEntities(from(query).with(fetchIdOnly(TgVehicle.class)).model());
assertFalse(entities.isEmpty());
entities.forEach(entity -> assertNotNull(entity.getId()));
}

@Test
public void foreign_id_only_query_with_id_only_fetch_returns_entities_with_ID_present() {
final var query = select(TgVehicle.class).yield().prop("model").modelAsEntity(TgVehicleModel.class);
final var entities = co(TgVehicleModel.class).getAllEntities(from(query).with(fetchIdOnly(TgVehicleModel.class)).model());
assertFalse(entities.isEmpty());
entities.forEach(entity -> assertNotNull(entity.getId()));
}

@Test
public void foreign_id_only_query_returns_entities_using_the_specified_fetch_model() {
final var query = select(TgVehicle.class).yield().prop("model").modelAsEntity(TgVehicleModel.class);
final var entities = co(TgVehicleModel.class).getAllEntities(from(query).with(fetchAllInclCalc(TgVehicleModel.class)).model());
assertThat(entities)
.isNotEmpty()
.allSatisfy(model -> assertThat(model.proxiedPropertyNames()).isEmpty());
}

@Test
public void eql3_query_executes_correctly_for_vehicle_deep_tree() {
final EntityResultQueryModel<TeVehicle> qry = select(TeVehicle.class).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package ua.com.fielden.platform.eql.stage3;

import org.junit.Before;
import org.junit.Test;
import ua.com.fielden.platform.entity.AbstractEntity;
import ua.com.fielden.platform.entity.query.QueryProcessingModel;
import ua.com.fielden.platform.entity.query.fluent.fetch;
import ua.com.fielden.platform.entity.query.model.EntityResultQueryModel;
import ua.com.fielden.platform.eql.meta.QuerySourceInfoProvider;
import ua.com.fielden.platform.eql.retrieval.EqlQueryTransformer;
import ua.com.fielden.platform.eql.stage3.queries.ResultQuery3;
import ua.com.fielden.platform.meta.IDomainMetadata;
import ua.com.fielden.platform.sample.domain.TgVehicle;
import ua.com.fielden.platform.sample.domain.TgVehicleModel;
import ua.com.fielden.platform.test_config.AbstractDaoTestCase;

import static java.util.Collections.emptyMap;
import static java.util.Optional.empty;
import static ua.com.fielden.platform.entity.AbstractEntity.ID;
import static ua.com.fielden.platform.entity.query.IRetrievalModel.createRetrievalModel;
import static ua.com.fielden.platform.entity.query.fluent.EntityQueryUtils.fetchIdOnly;
import static ua.com.fielden.platform.entity.query.fluent.EntityQueryUtils.select;
import static ua.com.fielden.platform.eql.meta.EqlStage3TestCase.*;
import static ua.com.fielden.platform.eql.meta.PropType.LONG_PROP_TYPE;

/// Covers transformations of id-only queries.
///
public class IdOnlyQueryTransformationTest extends AbstractDaoTestCase {

private final EqlQueryTransformer eqlQueryTransformer = getInstance(EqlQueryTransformer.class);

@Before
public void setUp() {
resetSqlId();
}

@Test
public void local_id_only_query_remains_a_top_level_query() {
final var inQuery = select(TgVehicle.class).model();
final var compiledQuery = transform(inQuery, fetchIdOnly(TgVehicle.class));

final ResultQuery3 expectedCompiledQuery;
{
final var vehicleSource = source(TgVehicle.class, 1);
final var yields = yields(yieldProp(ID, vehicleSource, ID, LONG_PROP_TYPE));
expectedCompiledQuery = qry(sources(vehicleSource),
yields,
TgVehicle.class);
}

assertQueryEquals(expectedCompiledQuery, compiledQuery);
}

@Test
public void foreign_id_only_query_becomes_nested() {
final var inQuery = select(TgVehicle.class).yield().prop("model").modelAsEntity(TgVehicleModel.class);
final var compiledQuery = transform(inQuery);

final var expectedInQuery = select(TgVehicleModel.class).where().prop(ID).in().model(inQuery).model();
final var expectedCompiledQuery = transform(expectedInQuery);

assertQueryEquals(expectedCompiledQuery, compiledQuery);
}

@Override
protected void populateDomain() {}

private <T extends AbstractEntity<?>> ResultQuery3 transform(final EntityResultQueryModel<T> qry) {
return eqlQueryTransformer
.transform(new QueryProcessingModel<>(qry, null, null, emptyMap(), true), empty())
.item;
}

private <T extends AbstractEntity<?>> ResultQuery3 transform(final EntityResultQueryModel<T> qry, final fetch<T> fetchModel) {
final var retrievalModel = createRetrievalModel(fetchModel, getInstance(IDomainMetadata.class), getInstance(QuerySourceInfoProvider.class));
return eqlQueryTransformer
.transform(new QueryProcessingModel<>(qry, null, retrievalModel, emptyMap(), true), empty())
.item;
}

}
Loading