EQL Guide - fieldenms/tg GitHub Wiki

Table of Contents

  1. Table of Contents
    1. Resolution of component-typed properties
    2. Yielding component-typed properties
      1. TG version 2.0.0
    3. Source queries
    4. Values and parameters
    5. (Order by | Group by) key
    6. Other topics

Resolution of component-typed properties

When a component-typed property is used as an operand (e.g., comparison condition, yield operand), it has to be resolved against the source of the query. Since a property is a single-valued (scalar) operand, and a component type may have multiple components, a component-typed property implicitly represents one of its components.

As of TG version 2.0.0, the following implicit representations are used:

  • Money - sub-property amount.
  • RichText - sub-property searchText.

For example, the following yields can be used interchangeably:

yield().prop("price") // implicitly resolved to "price.amount"
yield().prop("price.amount")

Yielding component-typed properties

This section describes how to use component-typed properties in yields, both as yield operands and aliases.

The treatment of component-typed properties as yield operands is the same as in other contexts. Refer to the section about resolution of component-typed properties.

TG version 2.0.0

Support for component-typed properties in yield aliases is limited. Such properties can be used standalone as a yield alias only in a source query. In all other kinds of queries, component sub-properties must be yielded explicitly.

Yielding a component-typed property directly is supported for:

  • Money - sub-property amount is implicitly yielded instead.

For the sake of demonstration, assume the following entity:

class Invoice extends AbstractEntity {
  @IsProperty
  @MapTo
  RichText comment;

  @IsProperty
  @MapTo
  Money fee;
}

Properties comment and fee are modelled with component types, which means they cannot be directly yielded in a top-level query. Instead, each component sub-property must be yielded explicitly.

// Invalid query
q1 = select(Invoice.class)
  // equivalent to yield().prop("comment.searchText").as("comment")
  .yield().prop("comment").as("comment")
  // equivalent to yield().prop("fee.amount").as("fee")
  .yield().prop("fee").as("fee")
  .model();

// Valid query
q2 = select(Invoice.class)

  .yield().prop("comment.formattedText").as("comment.formattedText")
  .yield().prop("comment.coreText").as("comment.coreText")
  // Yielding searchText is optional
  .yield().prop("comment.searchText").as("comment.searchText")

  .yield().prop("fee.amount").as("fee.amount")

  .model();

Entities retrieved with query q2 will have properties comment and fee correctly initialised.

As described above, some component types have special support for being used as yield aliases directly. An outer query that uses a source query with such yields can refer to them via prop as is. I.e., property resolution works as expected in such cases.

sourceQ = select().
  yield().X.as("fee"). // (1)
  modelAsEntity(Invoice.class);

q = select(sourceQ).
  yield().beginExpr().prop("fee").mult().val(2).endExpr().as("fee.amount"). // (2)
  modelAsEntity(Invoice.class);

Source query sourceQ doesn't need to include .amount in the alias (1), and its enclosing query q can refer to fee (recall that prop("fee") will expand to prop("fee.amount")). The top-level query q, on the other hand, must specify the full component sub-property path (2) in the yield alias.

This limited form of support is useful for working with synthetic entities. Synthetic entity models are always interpreted as source queries, enabling the use of alias shortcuts described in this section.

Source queries

A source query is a query that is used as a source of another query.

Example:

select(
  // source query 1
  select(InventoryItem.class).yield().prop("price").as("cost").modelAsEntity(ReTransaction.class),
  // source query 2
  select(ReturnReceipt.class).yield().prop("refund").as("cost").modelAsEntity(ReTransaction.class)
)
.where().prop("cost").ge().val(100)
.modelAsEntity(ReTransaction.class)

If two or more source queries are used, they form a union.

A query that contains source queries is subject to the following constraints:

  • If two or more source queries are used, they must have the same number of yields and use the same set of yield aliases.

    The following examples demonstrate invalid queries:

    1. Different numbers of yields.

      select(
        // 2 yields
        select(InventoryItem.class)
          .yield().prop("price").as("cost")
          .yield().prop("id").as("id")
          .modelAsEntity(ReTransaction.class),
        // 1 yield
        select(ReturnReceipt.class)
          .yield().prop("refund").as("cost")
          .modelAsEntity(ReTransaction.class)
      )
      
    2. Different sets of yield aliases.

      select(
        // { "cost", "id" }
        select(InventoryItem.class)
          .yield().prop("price").as("cost")
          .yield().prop("id").as("id")
          .modelAsEntity(ReTransaction.class),
        // { "expense", "id" }
        select(ReturnReceipt.class)
          .yield().prop("price").as("expense")
          .yield().prop("id").as("id")
          .modelAsEntity(ReTransaction.class)
      )
      

Values and parameters

EQL queries can contain values (val()) and parameters (param()).

The set of types that can be used for values and parameters is constrained.

The following rules to apply to component types:

  • Money -- expanded to amount.
  • RichText -- cannot be used. One of its components should be used instead.

(Order by | Group by) key

The orderBy and groupBy clauses have special interpretation of composite keys. Recall that EQL interprets a composite key as a string -- concatenation of key members. In these clauses, however, a composite key is recursively expanded into its key members.

For example, consider the following entity type:

class Request
  @CompositeKeyMember(1)
  User user;
  @CompositeKeyMember(2)
  Date date;

class User
  String key;

The special interpretation of composite keys would result in the following transformation:

select(Request.class).
orderBy().prop("key").asc()
=>
select(Request.class).
orderBy().prop("user.key").asc().prop("date").asc()

The ascending/descending order is inherited by each key member.

A transformation of similar nature applies to groupBy.

This transformation also applies to property paths that end with a composite key.

Order by

This transformation enables preservation of ordering semantics for composite keys. The semantics of ordering by each key member is different from that of ordering by a concatenated representation. For example, consider the following dataset for Request:

id user.key date
106 SU 2025-01-01 00:00:00
107 TEST 2023-12-12 00:00:00
108 TEST 2024-02-02 00:00:00

Column user.key is a shorthand so that all data can be represented by a single table.

  1. The result of ordering by each key member in ascending order:
id user.key date
106 SU 2025-01-01 00:00:00
107 TEST 2023-12-12 00:00:00
108 TEST 2024-02-02 00:00:00
  1. The result of ordering by the concatenated representation in ascending order (for demonstration, column key contains the concatenated representation):
id user.key date key
106 SU 2025-01-01 00:00:00 SU 01/01/2025 00:00:00
108 TEST 2024-02-02 00:00:00 TEST 02/02/2024 00:00:00
107 TEST 2023-12-12 00:00:00 TEST 12/12/2023 00:00:00

Here, the later date 02/02/2024 comes before the earlier date 12/12/2023, despite the ascending order. This is because the order is based on a string representation (02 < 12).

Note that a specific date format was used, but its choice is arbitrary. What is important is that the date format affects the ordering.

Group by

The expanded form of groupBy is equivalent to the original form if and only if the concatenated representation of a composite key preserves its uniqueness property.

For example, when all key members have type String, their concatenation preserves the uniqueness property.

However, if there is a Date key member, and the date format is not precise enough, the uniqueness property may be lost. For example:

user.key date (milliseconds since the Epoch) key
SU 1750318210583 SU 19/06/2025 10:30:10
SU 1750318210612 SU 19/06/2025 10:30:10

Other topics