EXPLANATION OF MULTI-VARIABLE SEARCH

OVERVIEW
The Multi-Variable Search is a custom view that allows users to construct advanced multi-condition searches against the Elyse database.  It is registered in custom-views.csv with ViewType SEARCH_BUILDER and is accessed from the navigation bar under the Search submenu or via the command palette.  CustomViewRoutesInitializerService registers the route URL from custom-views.csv as a dynamic route pointing to CustomViewComponent.  When CustomViewComponent loads and reads ViewType=SEARCH_BUILDER it renders BooleanSearchBuilderComponent as a child component.  The component orchestrates its own read route calls internally and does not use a single ApiEndpoint.

Each condition is evaluated by an existing read route from read-routes.csv.  Read routes are identified by TableName; the StoredProcedure column of read-routes.csv is for human reference only and is not used by the code.  No new stored procedures, endpoints or database changes are required.

The search builder operates within the context of an optional Elyse Form.  The form determines which columns appear in the results.  The formId parameter is optional; when it is omitted the database applies its default form.

PAGE STRUCTURE
The page has three sections: a header bar, a clause workspace and an execution bar.

The header bar contains a Documents/Files radio toggle, a Form dropdown populated from the all-form-names read route, a Filter Group dropdown populated from the all-filter-groups read route, and a group mode selector.  The Filter Group dropdown is always visible.  When a filter group is selected the search prefers the filtered route family (usp_SEL_docs_* / usp_SEL_files_*).  When left blank it prefers the unfiltered family (usp_SEL_all_docs_* / usp_SEL_all_files_*) if the user's roles permit.  Both formId and filterGroupId are optional and are omitted from the request when their selector is blank, in which case the database applies its own default.  The group mode selector has two options: "Match ANY group" and "Match ALL groups".  It is always visible.  With a single group the mode has no effect but displaying it from the start avoids a layout shift when the user adds a second group.

The clause workspace is laid out as a table where each row represents one search condition.  Conditions are organised into clause groups separated by visual dividers.  In "Match ANY group" mode the divider label is OR and conditions within a group are AND-connected.  In "Match ALL groups" mode the divider label is AND and conditions within a group are OR-connected.  The first column of the first row in each group displays "Group N".  A remove button appears beside the group label and is hidden when only one group exists.  An "Add condition" button appears below the last row of each group.  An "Add group" button below all groups creates a new empty group with a divider above it.

Each condition row contains five controls from left to right: a NOT checkbox, a Field dropdown, an Operator dropdown, a Value input and a Remove button.

The execution bar contains a Search button, a Clear All button, a progress indicator and a result count display.

CONDITION ROWS
The Field dropdown lists every field defined in the database for each type, grouped under type headings: Date Fields, Integer Fields, Real Number Fields, Text Fields, Radio Button Lists, Multi-Select Lists, Common Objects and Structural fields (Document Group, File Group, Tag, Duty Function, Function List, People List, Transaction Group, User, Document ID, Filename, Full-Text).

When a field is selected the Operator dropdown updates to show only operators valid for that field type and permitted by the user's roles.  Date fields support equals, before, after, between and has any value.  Integer and Real fields support equals, greater than, less than, between and has any value.  Text fields support contains and has any value.  Lookup fields (Radio Button, Tag, Document Group, File Group, Duty Function, Function List, People List, Transaction Group, User) support is.  Multi-Select and Common Object fields support includes.  Document ID, Filename and Full-Text fields support contains.

Some operators are only available to privileged users because only an unfiltered route exists for them.  The has any value operator, Document Group and File Group all fall into this category and are hidden from a user with no roles.

When an operator is selected the Value input renders in the appropriate format.  Single value operators show a date control for dates, a number input for integers and reals, or a dropdown for lookup fields.  The between operator shows two inputs with the word "and" between them.  The contains operator shows a text input.  The has any value operator shows no input.

The controls, the date handling and the styling are the same as those used by the fields on the edit-doc-file-data page, so the two pages look and behave alike.  Dates are displayed and entered in the format configured in the database.

For lookup fields the value dropdown is populated by calling a read route, named here by TableName.  Radio Button members are loaded from doc-radiob-attr-by-list or file-radiob-attr-by-list.  Multi-Select members from doc-ms-attr-by-list or file-ms-attr-by-list.  Common Object items from com-obj-by-list.  Tags from all-tags.  Document Groups from doc-group-names-rstr.  File Groups from file-group-names.  Duty Functions from all-duty-functions.  Function Lists from all-function-lst-names.  People Lists from all-people-list-names.  Transaction Groups from all-transaction-groups.  Users from all-users.  These results are cached for the session after first load.

The field lists come from the per-type read routes: all-doc-date-fields, all-doc-int-fields, all-doc-real-fields, all-doc-text-fields, all-doc-radiob-lists, all-doc-ms-lists and common-object-list-names for documents, and the corresponding all-file-* routes for files.  Structural field types such as Tag, Document Group and Document ID have no field list and contribute a single entry each.

Which columns are read from an option list is declared rather than assumed.  The identifier and display columns come from input-id-parameter-mapping.csv, looked up by the parameter that identifies the list.  To change what a dropdown shows, edit that file.  If a parameter has no entry there the list is refused and an error reported, and if a route does not return the declared display column then the identifier is shown instead and a warning names the columns that were returned.

The field lists are not narrowed by the selected form.  The selected form still governs the search itself, because formId is sent to every route that accepts it and so determines the columns returned in the results.

Note that a lookup route and its corresponding search route use different parameter names for the same list.  The radio button lookup takes the list ID (docRadioButtonListId) whereas the radio button search takes the member ID (docRadioButtonAttributeId).

The NOT checkbox negates the condition.  When checked the search excludes records matching this condition rather than including them.  The NOT checkbox is disabled if checking it would leave the group with no non-negated conditions.  At least one positive condition per group is required.

PREDICATE-TO-READ-ROUTE MAPPING
The file search-predicates.csv maps each combination of field type, operator and search target to the read route that evaluates it.  The columns are FieldType, Operator, SearchTarget, Roles, ReadRoute, Parameters, ValueType, DisplayLabel and SelectivityRank.  There is one row per combination of field type, operator, search target and privilege variant.

ReadRoute holds a TableName from read-routes.csv.  Parameters lists only those of the route's InputParameters that the predicate itself supplies, in slot order: the field name ID first, then the value or values.  ApiEndpoint and DataField are always read from read-routes.csv and are never duplicated in search-predicates.csv, because DataField is not predictable from the route name.  The route all-docs-date-equals returns documentData while docs-date-equals returns resultSets, and the casing of that value varies.

SearchTarget is DOCUMENT or FILE.  The split by target is necessary because the two targets use different parameter names for the same concept.  A document date search takes docDateNameId whereas a file date search takes fileDateNameId, and a document real number search takes docRealNameId whereas a file real number search takes fileRealNumberFieldNameId.  The split also removes empty cells, because many conditions exist for only one target.  Tag and Document Group have no file route, and Filename and Full-Text have no document route.

The Roles column controls which rows are visible to the user based on their active roles, following the same convention as navigation-bar.csv and context-menus.csv.  If blank the row is available to any user including one with no roles.  The SelectivityRank is an integer from 1 to 6 controlling execution order where 1 is most selective and called first.

The authoritative source for parameter names and data types is INFO-ONLY_stored_procedures_MASTER.csv in the backend common folder.  That file records the database parameter names in lower case such as @docdatenameid, whereas search-predicates.csv must use the camel case route names declared by read-routes.csv such as docDateNameId.  The master file does not record which parameters are optional.  The rule is that formId and filterGroupId are optional because the database holds a default for each, tagTreeId and tagDescendants are optional, and every other input parameter is mandatory and forms the predicate's own payload.

PARAMETER ASSEMBLY
For each condition the resolver takes the route's InputParameters from read-routes.csv as the set of names the route accepts.  It binds each name listed in the predicate's Parameters column, in order, to the predicate's slots.  It then injects formId from the Form selector and filterGroupId from the Filter Group selector, omitting either entirely when its selector is blank.  Any remaining route parameter is omitted so that the database applies its default.

Omitting unlisted parameters is essential rather than cosmetic.  The route all-docs-by-tag declares tagId, formId, tagTreeId and tagDescendants, of which the builder supplies only tagId.  Binding positionally against InputParameters instead of against the declared Parameters list would silently assign a tag ID to tagTreeId.

Which of the condition's values fill those names is determined by the ValueType together with the number of names listed, so no field type is treated as a special case.  A ValueType of none sends the field name ID alone.  A single value type sends the field name ID and one value where two names are listed, or the value alone where one name is listed, the latter covering Document ID, filename and file content searches.  A range type sends the field name ID and both values where three names are listed, or both values alone where two are listed.  A lookup sends the list ID and the chosen member where two names are listed, or the chosen member alone where one is listed.  A row whose Parameters count does not match its ValueType in one of these ways will not run.

Dates are converted using the same conversion the edit-doc-file-data form uses when sending a date to the database.

Nothing is guessed.  A ReadRoute that does not exist, a route with no ApiEndpoint or no DataField, an incomplete condition, or a Parameters name the route does not accept will each stop the search with a message identifying the problem, rather than a call being made.

VARIANT SELECTION
The database provides two families of search route.  The unfiltered family (usp_SEL_all_docs_* and usp_SEL_all_files_*) carries Roles=Reader;Reviewer;Editor;Controller.  The filtered family (usp_SEL_docs_* and usp_SEL_files_*) accepts filterGroupId and carries a blank Roles column.  A privileged user therefore sees both families and chooses between them implicitly: selecting a filter group selects the filtered route and leaving it blank selects the unfiltered route.  An unprivileged user sees only the filtered family.

Where only one family exists for an operator that family is used.  If a filter group was selected but only an unfiltered route exists, the filter group cannot be applied to that condition and the condition is evaluated unfiltered.

Several conditions exist in only one family.  The _any family exists only unfiltered, so has any value is offered only to privileged users.  Document Group and File Group exist only unfiltered and are likewise privileged only.  Transaction Group accepts no filterGroupId and has no unfiltered counterpart, so it ignores the Filter Group selector entirely.

ROUTE PARAMETER RULES
A route's InputParameters is the complete set of names that route accepts.  The resolver never sends a name the route does not declare, even if the stored procedure would accept it, because removing a parameter from a route is how the database default is forced.  A route may declare filterGroupId even though its name places it in the unfiltered family, as all-docs-real-less does; the builder honours what the route declares and never special-cases a route by name.  Parameter order within InputParameters is not significant because binding is by name.  usp_SEL_files_by_filename misspells its filter group parameter as @filtergoupid, so a filter group sent on the files-by-filename route will not bind until this is corrected at source.

PROGRESSIVE EVALUATION
The evaluation engine does not call all conditions simultaneously.  Instead it calls them one at a time in selectivity order, progressively building the result set.

If the user constructed the expression in "Match ALL groups" mode the engine first converts it to "Match ANY group" form internally.  This is a mathematical equivalence.  The evaluation algorithm then operates uniformly on the converted form.

For a single group of AND-connected conditions the engine separates conditions into non-negated and negated groups.  It sorts non-negated conditions by SelectivityRank ascending so the most selective condition is called first.  It calls the first condition's route and extracts document or file IDs from the response.  It then calls the next condition's route, extracts IDs and intersects with the running set.  If the running set becomes empty the engine stops immediately without calling the remaining routes.  This is early termination.  After all non-negated conditions are evaluated the engine processes negated conditions by subtracting their IDs from the running set.

For multiple groups the engine evaluates each group independently and then combines all group results.

NOT is implemented as a relative complement.  It subtracts the negated condition's IDs from the running result set built by the positive conditions.  It never queries the entire database.  This is why at least one non-negated condition per group is required.

Each response is flattened by ReadRoutesAdapterService using the DataField declared by that route in read-routes.csv.  Document searches read the Document ID property from each row and file searches read the File ID property.  The IDs are collected into a JavaScript Set for efficient intersection and subtraction.

Every condition is checked against its route before any call is made, so a misconfigured row in search-predicates.csv is reported without touching the database.

Conjunctive expressions are normalised by taking the cartesian product of the groups.  The expansion is capped at 64 groups, beyond which the search is refused with a message asking the user to reduce the number of conditions.

After evaluation the engine retains the full row data from the last called positive condition and filters it against the final ID set.  The column tooltips and the column name mapping from that same response are retained with it, so the results table shows the same headings and hover text as any other table displaying those rows.

CACHING
Field lists and lookup dropdown options are cached for the session, keyed by route and parameters.  Result caching and saved search templates are not implemented.

STATE MANAGEMENT
BooleanSearchStateService holds the search expression and tracks execution state, progress and results.  On completion the resulting rows are registered with the standard table state under the results table name for the current target, and the matching IDs are published for downstream operations in the same way that selecting rows in a table publishes them.

Switching the Documents/Files target clears the workspace because the available routes and field types differ between the two.  Changing the form does not clear anything.

FORM SELECTION
The Form dropdown is optional.  If the user does not select a form the database automatically uses a default form ID.  The user may change the form at any time including after adding conditions.

RESULT DISPLAY
The results are displayed by the same component that displays every read route table, so the results table is a standard Elyse table.  Sorting by clicking a column heading, column resizing with widths remembered, hidden columns, the column tooltips, the File ID download link, the Document ID drill-down link, row selection including Ctrl and Shift multi-select, publishing of the selected key columns and the context menu all behave exactly as they do on any other table.  The difference is only in where the rows come from: the search engine supplies them instead of a single endpoint call.

The table is configured by a read-routes.csv row like any other table.  Two rows exist, mv-search-docs for document results and mv-search-files for file results, chosen by the Documents/Files target.  Each declares the DataField, KeyColumns, HiddenColumns, DisplayName and DefaultSort for its results, so what the table shows and which IDs a selected row publishes are both edited in read-routes.csv and nowhere else.  Neither row declares an ApiEndpoint, because there is no single endpoint to call.  A row without an ApiEndpoint is not registered as a frontend route, so the RouteUrl of these two rows is never navigated to and exists only to give the row a unique address.

The context menu for the results is defined by the context-menus.csv rows carrying TableName mv-search-docs or mv-search-files.  These are presently copies of the rows for the all-documents and list-all-files tables, so the same actions are offered on search results as on those tables.  To offer a different set of actions on search results, edit only these rows.

The current table, which is what determines whose context menu is shown, is normally resolved from the page URL through the RouteUrl column of read-routes.csv.  That cannot work here because the search page is a custom view and its URL is in custom-views.csv, so the page registers its results table name with the context menu service directly and releases it when the page is left.

ERROR HANDLING
During progressive evaluation each route call returns transactionMessage and transactionStatus output parameters alongside the data.  The evaluation engine collects these responses from every call.  If all calls return transactionStatus=Good the results are displayed normally.  If any call returns a transactionStatus other than Good the engine halts evaluation and opens a TransactionResultsComponent dialog.  This dialog displays a table with one row per call showing the condition description, transactionMessage and transactionStatus.  This follows the same pattern used by the edit-doc-file-data form for displaying outcomes of multiple operations.  The user closes the dialog to return to the search builder with their expression intact so they can modify or remove the failing condition.

Two kinds of misconfiguration surface through the same dialog.  A row naming a route that the user's application role cannot execute produces a database error in transactionStatus.  A row naming a ReadRoute that does not exist in read-routes.csv cannot be resolved at all, and is reported with an application-level message instead of a database message so that both faults are diagnosed the same way.

A transactionStatus is treated as success when it equals Good, ignoring case, or when the route returns none at all, because not every route returns one.

If the Field dropdown is empty, the cause is normally that search-predicates.csv failed validation, so no field type is permitted, or that the per-type field list routes returned no rows.  Both are reported in the browser console, the first through the CSV validation summary.
