Class JoinsContext

java.lang.Object
com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext

public class JoinsContext extends Object
Helper object used throughout query, count, aggregate, and reporting actions to assemble the full set of joins needed for a query, manage table aliases, and enforce record-level security.

Overview: When a backend action (e.g., RDBMSQueryAction, RDBMSCountAction, RDBMSAggregateAction) needs to generate SQL (or an equivalent query for other backends), it constructs a JoinsContext from the main table name, the caller-supplied list of QueryJoin objects, and the QQueryFilter. During construction, this class modifies the join list and the filter to include everything needed for a correct, secure query.

Initialization pipeline (init()):

  1. Process existing QueryJoins: validates that each join's table exists, and populates the aliasToTableNameMap so that later steps can resolve aliases to real table names.
  2. ensureFilterIsRepresented: scans all filter criteria, other-field-names, and order-bys for table.field references. If a referenced table is not yet in the join list, a join is added for it (with metadata resolved from the instance).
  3. ensureRecordSecurityLockIsRepresented (main table): for each read-scope RecordSecurityLock on the main table, ensures that any joins needed to reach the lock's security field are present, and builds security filter criteria.
  4. fillInMissingJoinMetaData: for joins that were added without explicit QJoinMetaData (e.g., only a table name was specified), finds matching metadata from the QInstance's joins. Also resolves indirect/multi-hop joins via ExposedJoin paths on the main table.
  5. ensureAllJoinRecordSecurityLocksAreRepresented: iterates over all joined tables and applies their security locks as well, potentially adding further implicit joins.
  6. addSecurityFiltersToInputFilter: merges the accumulated security filter into the original input filter. If the input filter uses OR, it is wrapped in a new AND alongside the security filter.

Alias management: The internal aliasToTableNameMap maps both aliases and bare table names to actual table names. It is populated as joins are processed. Use resolveTableNameOrAliasToTableName(java.lang.String) to convert any alias/name to the real table name.

Security lock pipeline: When a RecordSecurityLock has a non-empty joinNameChain, the chain is walked (in reverse) to add ImplicitQueryJoinForSecurityLock joins as needed. The resulting security criteria are placed either in the JOIN's ON clause (via QueryJoin.withSecurityCriteria(java.util.List<com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria>)) or in the WHERE clause, depending on whether the lock is part of an OR filter or applies to the main table directly.

Key consumers: AbstractRDBMSAction (uses this context to build FROM, WHERE, ORDER BY, and GROUP BY clauses), MemoryRecordStore, and GenerateReportAction.

  • Constructor Details

    • JoinsContext

      public JoinsContext(String tableName, List<QueryJoin> queryJoins, QQueryFilter filter) throws QException
      Constructor that obtains the QInstance from QContext.
      Parameters:
      tableName - the main (root) table for the query.
      queryJoins - caller-supplied list of joins; may be empty. Additional joins may be added during initialization.
      filter - the query filter; may be mutated to include security criteria.
      Throws:
      QException - if a referenced table or join cannot be resolved.
    • JoinsContext

      public JoinsContext(QInstance instance, String tableName, List<QueryJoin> queryJoins, QQueryFilter filter) throws QException
      Primary constructor.
      Parameters:
      instance - the QInstance containing table and join metadata.
      tableName - the main (root) table for the query.
      queryJoins - caller-supplied list of joins; may be empty. Additional joins may be added during initialization.
      filter - the query filter; may be mutated to include security criteria.
      Throws:
      QException - if a referenced table or join cannot be resolved.
    • JoinsContext

      public JoinsContext(String tableName, QQueryFilter filter, boolean omitSecurity) throws QException
      Constructor that starts with an empty join list and optionally skips security-lock processing. Obtains the QInstance from QContext.
      Parameters:
      tableName - the main (root) table for the query.
      filter - the query filter; may be mutated to include security criteria (unless omitSecurity is true).
      omitSecurity - if true, the initialization pipeline will not process any RecordSecurityLocks or add security filters.
      Throws:
      QException - if a referenced table or join cannot be resolved.
  • Method Details

    • getQueryJoins

      public List<QueryJoin> getQueryJoins()
      Returns the live list of QueryJoin objects in this context. This includes both caller-supplied joins and any that were added automatically (for filters, security locks, or exposed-join paths).
      Returns:
      the mutable list of query joins.
    • resolveTableNameOrAliasToTableName

      public String resolveTableNameOrAliasToTableName(String nameOrAlias)
      Resolves a name (which may be an alias or an actual table name) to the real table name that can be passed to qInstance.getTable(). If the name is not found in the alias map, it is returned as-is.
      Parameters:
      nameOrAlias - an alias or table name present in this context.
      Returns:
      the actual table name.
    • getFieldAndTableNameOrAlias

      public JoinsContext.FieldAndTableNameOrAlias getFieldAndTableNameOrAlias(String fieldName)
      Parses a field reference (optionally prefixed with tableOrAlias.) and returns the resolved QFieldMetaData together with the table name or alias it belongs to.

      If fieldName contains a dot (e.g., "department.name"), the part before the dot is treated as a table name or alias and resolved via the alias map. If there is no dot, the field is looked up on the main table.

      Parameters:
      fieldName - a field name, optionally qualified as table.field.
      Returns:
      a JoinsContext.FieldAndTableNameOrAlias record.
      Throws:
      IllegalArgumentException - if the name has more than one dot, or the table/field cannot be found.
    • hasTable

      public boolean hasTable(String table)
      Check if the given table name exists in the query - but that name may NOT be an alias - it must be an actual table name. e.g., Given: FROM `order` INNER JOIN line_item li hasTable("order") => true hasTable("li") => false hasTable("line_item") => true
    • hasAliasOrTable

      public boolean hasAliasOrTable(String tableOrAlias)
      Check if the given tableOrAlias exists in the query - but note, if a table is in the query, but with an alias, then it would not be found by this method. e.g., Given: FROM `order` INNER JOIN line_item li hasAliasOrTable("order") => false hasAliasOrTable("li") => true hasAliasOrTable("line_item") => false
    • findJoinMetaData

      public QJoinMetaData findJoinMetaData(String baseTableName, String joinTableName, boolean useExposedJoins)
      Searches the QInstance's joins for a QJoinMetaData that connects baseTableName to joinTableName (checking both directions and flipping if needed).

      If multiple matches are found and useExposedJoins is true, the main table's ExposedJoin list is consulted to disambiguate. If disambiguation fails, a RuntimeException is thrown.

      Parameters:
      baseTableName - the "left" / base table (may be null to match any table already in this context).
      joinTableName - the "right" / target table.
      useExposedJoins - whether to use exposed joins for disambiguation when multiple matches are found.
      Returns:
      the matching QJoinMetaData, or null if none found.
      Throws:
      RuntimeException - if more than one join matches and disambiguation fails.