Class LlmDataLoader

java.lang.Object
io.jmix.reports.libintegration.LlmDataLoader
All Implemented Interfaces:
ReportDataLoader

@NullMarked public class LlmDataLoader extends Object implements ReportDataLoader
Loads band data for the DataSetType.LLM data set type: reads the query stored in the data set, binds what the run holds under the names the query references, and executes it.

A run never generates a query. The query is generated in the report designer of a running application and stored with the report, which is what makes a run reproducible, free of model calls and independent of whether a model is reachable at all.

The rules by which a cell of a cross-tab band finds its place in the matrix live in LlmCrossTabAxes.

  • Field Details

    • NATIVE_ESCAPE_PATTERN

      protected static final Pattern NATIVE_ESCAPE_PATTERN
      The calls with which an EclipseLink JPQL select reaches past the query language: SQL inlines database SQL, FUNCTION, FUNC and OPERATOR call a database function, and COLUMN and TABLE read a column or a table the entity model does not map at all.

      CAST, EXTRACT, TREAT and REGEXP — the rest of what the EclipseLink grammar adds — stay within the model and are left alone.

    • CASE_INSENSITIVE_PARAMETER_PATTERN

      protected static final Pattern CASE_INSENSITIVE_PARAMETER_PATTERN
      The marker with which ordinary Jmix JPQL asks for a parameter to be matched case-insensitively, as in e.name like :(?i)name. A stored query cannot use it: the parameters of a query are read from its text by name, and a name written like this is not one — neither this loader nor the add-on's validator recognises it, so nothing would ever be bound to it.
    • IN_PARAMETER_PATTERN

      protected static final Pattern IN_PARAMETER_PATTERN
      A parameter matched with IN, with or without parentheses around it. Such a parameter cannot be given an empty value: an IN over nothing matches nothing, and a (:name is null or …) guard does not switch it off.
    • dataManager

      @Autowired protected DataManager dataManager
    • llmDataQuerySerializer

      @Autowired protected LlmDataQuerySerializer llmDataQuerySerializer
    • queryTransformerFactory

      @Autowired protected QueryTransformerFactory queryTransformerFactory
    • metadata

      @Autowired protected Metadata metadata
    • accessManager

      @Autowired protected AccessManager accessManager
    • policyStore

      @Autowired protected PolicyStore policyStore
    • runScope

      protected final ThreadLocal<SoftReference<LlmDataLoader.RunScope>> runScope
      Holds what one report run has already generated and already warned about. Bound to the thread the run executes on: the next run reaching that thread replaces its contents, and the reference is soft, so what the last run left there is reclaimed under memory pressure instead of sitting on a pooled thread for the lifetime of the process.
  • Constructor Details

    • LlmDataLoader

      public LlmDataLoader()
  • Method Details

    • loadData

      public List<Map<String,Object>> loadData(ReportQuery reportQuery, @Nullable BandData parentBand, Map<String,Object> params)
      Description copied from interface: ReportDataLoader
      This method MUST return list of mutable maps. We need mutability to put there some values implicitly.
      Specified by:
      loadData in interface ReportDataLoader
    • executeQuery

      protected List<Map<String,@Nullable Object>> executeQuery(LlmDataQuery query, String jpql, Map<String,@Nullable Object> arguments, String storeName)
      Executes the stored query through DataManager, so the attribute permissions and the row-level policies of the current user apply. Entity READ is checked before this, by checkEntityReadPermitted(io.jmix.reports.llm.LlmDataQuery), because the platform does not act on it for a value load.

      Values are bound as named JPQL parameters, never inlined, and the row count the prompt asked for is applied per execution. A row comes out as the report engine needs it: keyed by the query's columns in select-clause order and mutable, with null and an empty string kept apart.

      Parameters:
      query - stored query, which names the columns the rows are keyed by and the count it is limited to
      jpql - text to execute — the stored one with the row-level policies of what it reads woven in
      arguments - value to bind per parameter the query references
      storeName - data store to execute in, the one the query's entity belongs to
      Returns:
      one row per row the query returned
    • checkQueryIsFitToExecute

      protected void checkQueryIsFitToExecute(ReportQuery reportQuery, LlmDataQuery query)
      Refuses a stored query a run will not execute, before it executes anything: one that does more than read the entity model through DataManager, one whose parameters could not be bound, one that names no column to key its rows by, and one that selects an entity rather than its attributes.

      The designer checks a query the add-on generated or the author edited, and refuses some of this. A report also arrives by import, though, bringing whatever text the file holds and no add-on to check it with — so what a run promises about a query, a run has to establish itself.

    • checkQueryOnlyReads

      protected void checkQueryOnlyReads(ReportQuery reportQuery, LlmDataQuery query, String withoutLiterals)
      Refuses a query that does more than read the entity model through DataManager: one that is not a select, and one calling any of the six EclipseLink escapes that reach past JPQL into the database, where the constraints of the current user do not follow.
    • checkParametersCanBeBound

      protected void checkParametersCanBeBound(ReportQuery reportQuery, String withoutLiterals)
      Refuses a query whose parameters could not be bound: the case-insensitive marker :(?i)name, which ordinary Jmix JPQL accepts and neither this loader nor the add-on's validator reads as a parameter name — so nothing would ever be bound to it and the run would fail inside EclipseLink instead.
    • checkQueryReturnsColumns

      protected void checkQueryReturnsColumns(ReportQuery reportQuery, LlmDataQuery query)
      Refuses a query that names no result column, a blank one, or the same one twice. A band row is built from the columns a query declares: none means rows a template cannot print, and a duplicate silently loses one of the values the query selects.
    • resolveQuery

      protected LlmDataQuery resolveQuery(ReportQuery reportQuery, Map<String,Object> additionalParams, LlmDataLoader.RunScope scope)
      Reads the query stored with the data set. A run never generates one: the query is generated in the report designer of a running application and stored with the report, which is what makes a run reproducible and free of model calls. A data set without a stored query therefore has nothing to execute, and says so rather than quietly asking a model for one.

      Read once per document per run: the same data set is loaded once per row of its parent band, and parsing the same JSON for every row would be work done to reach the same result. Reading is all this does — whether the query may run, and whether its text can run at all, is asked afterwards and in that order, so that a user refused the data is told about the permission rather than about the query (checkEntityReadPermitted(io.jmix.reports.llm.LlmDataQuery), then checkQueryIsFitToExecute(io.jmix.reports.yarg.structure.ReportQuery, io.jmix.reports.llm.LlmDataQuery)).

    • resolveStoreName

      protected String resolveStoreName(LlmDataQuery query)
      Tells the data store a query runs in from the query itself: the entity it reads, and that entity's store. Generation is offered the whole entity model, additional stores included, so the data set's own dataStore is not consulted and not shown for this type. Worked out once per query text per run (LlmDataLoader.RunScope.storeName(java.lang.String, java.util.function.Supplier<java.lang.String>)). A text the parser cannot read, or an unknown entity, leaves the main store — such a query fails on its own terms, which is more use than a failure about a store.
    • runScopeOf

      protected LlmDataLoader.RunScope runScopeOf(@Nullable BandData parentBand)
      Returns what the run this call belongs to has already resolved. Runs are told apart by the band data the hierarchy is rooted at, which is created once per run and shared by every band of it.
    • collectAvailableParameters

      protected LlmDataLoader.CollectedParameters collectAvailableParameters(ReportQuery reportQuery, Map<String,Object> params, @Nullable BandData parentBand, @Nullable String bandName, boolean readsAxes, LlmDataLoader.RunScope scope)
      Collects the values a query may bind, keyed by the name it would reference them by: the run parameters first, then the fields of every parent row up the band hierarchy, then the values of the cross-tab axes this band is built from.

      A run parameter the report knows is offered even when this run left it empty, so that a query guarding its condition with (:name is null or …) has a null to switch on. A collection with nothing in it is the exception, and so are a null parent-band field and an axis value: see decisions/0016.

    • checkQuerySelectsValues

      protected void checkQuerySelectsValues(ReportQuery reportQuery, LlmDataQuery query)
      Refuses a query that hands back an entity rather than its attributes — a band row is a tabular value, and an entity carries the very attributes masking would have hidden. The add-on's generation prompt forbids the same thing, so this refuses what generation was told not to produce and an import can still carry in.
    • selectedEntitiesOf

      protected List<String> selectedEntitiesOf(QueryParser parser)
      Names what a query selects whole instead of as a value: a path whose property is its own variable alias (select p), or one the data model says ends in a class (select g.publisher, an embeddable too). Both were measured to hand back the entity itself, whose denied attributes then read fine — masking is applied to the selected column, and the column here is the entity.

      A property the entity does not have is deliberately not treated as one: select p.noSuchAttribute is a query against another data model, which has its own and better failure.

    • entityValuedPathOf

      protected @Nullable String entityValuedPathOf(QueryParser.QueryPath path)
      Returns the description of a selected path that ends in an entity or an embeddable, or null when it ends in a value or cannot be placed in the data model at all.
    • applyRowLevelPolicies

      protected String applyRowLevelPolicies(ReportQuery reportQuery, String jpql)
      Weaves the row-level policies of the entities a query reads into its text, so that a band shows the rows the current user may see. The platform does this for one entity only — the one the query selects from — and a report query joins as a matter of course, so the rest is this loader's to apply: the policy's {E} becomes that entity's own alias and the condition is added to the where through the platform's own QueryTransformer. Only conditions, never joins.

      Three shapes cannot be filtered and are refused instead, each naming the entity and what to do about it: a PREDICATE policy (asked first, and of the query's own entity too, since a value load has no instance to evaluate it against), an entity whose alias is out of scope for a condition (used only in a subquery, or under more than one alias), and a policy carrying a joinClause on anything but the query's own entity — measured, addJoinAndWhere re-bases the join onto the root alias. A text this parser cannot read is left as it is: the platform parses it with the same parser, so such a query does not execute either.

      See docs/features/reports-llm-data-query/decisions/0018-row-level-policies-are-applied-to-the-whole-query-graph.md for what was measured and why.

      Parameters:
      jpql - the stored query text
      Returns:
      the text to execute, unchanged when nothing had to be woven in
    • readEntitiesOf

      protected List<String> readEntitiesOf(QueryParser parser)
      Names the entities of a query graph in a stable order.

      Sorted, so that which of several entities a message blames does not depend on hashing, and without the nulls the parser leaves for a name it could not resolve — it honours no nullness contract, whatever the declared element type says, and sorting would trip over one.

    • aliasOf

      protected @Nullable String aliasOf(QueryParser parser, String entityName)
      Returns the alias a condition on this entity can be written against, or null when there is none to write against: the entity lives in a subquery, or carries more than one alias, of which filtering one would leave the others as they were.
    • withAlias

      protected @Nullable String withAlias(@Nullable String clause, String alias)
    • checkEntityReadPermitted

      protected void checkEntityReadPermitted(LlmDataQuery query)
      Refuses a query that reads an entity the current user may not read — every entity in the graph, not only the one whose attributes it selects. The platform leaves this open twice over: nothing on the value-load path reads isPermitted(), and the context it would ask judges by the selected paths alone. So the question is asked here, of QueryParser#getAllEntityNames, through the platform's own CrudEntityContext. Denied columns are still left to the platform, which masks them.
    • parametersMatchedWithIn

      protected Set<String> parametersMatchedWithIn(String jpql)
      Names the parameters the query matches with IN, which is what makes an empty value unusable for them. Read off the text because nothing else can say it: a collection parameter left empty reaches a run as null, indistinguishable from an empty scalar, and the stored document declares no cardinality. String literals are blanked first, so an in :name spelled inside one is text.
    • isEmptyCollection

      protected boolean isEmptyCollection(@Nullable Object value)
    • addParentBandFields

      protected void addParentBandFields(ReportQuery reportQuery, BandData band, Map<String,Object> availableParameters, LlmDataLoader.RunScope scope)
      Offers the fields of one parent row under names flattened to <band>_<field>, because a JPQL parameter name cannot contain the dot that SQL and JPQL data sets use in ${Band.field}.

      A name already taken is kept as it is: a run parameter outranks a band field, and a nearer parent outranks a more distant one. Skipping a field whose flattened name is not an identifier is deliberate — sanitizing the name would let two different bands collapse onto one parameter.

    • resolveArguments

      protected Map<String,@Nullable Object> resolveArguments(ReportQuery reportQuery, LlmDataQuery query, Map<String,Object> availableParameters, Map<String,Object> params, LlmDataLoader.RunScope scope)
      Collects the value to bind for every parameter the query references. The type the query declares for a parameter is not consulted: it was written to tell a model what the value would be, and a value is bound as the run holds it.
    • describeMissingArgument

      protected String describeMissingArgument(ReportQuery reportQuery, String name, Map<String,Object> params)
      Says why a parameter the query references cannot be bound, telling two cases apart. A name the run has never heard of means a query that does not match its report. A name the run knows reaches here only when its value cannot be bound as it stands — a collection with nothing to match, above all — since an empty value the report knows is otherwise bound as null for a guarded condition to switch off.