--- layout: article title: Updating the Filter in a Pre-Edit Hook description: Read the filter a user applied, and hand back a modified one, from a Data Controller pre-edit hook - including changing the rows the editor shows. --- # Updating the Filter in a Pre-Edit Hook A [pre-edit hook](/dcc-tables/#pre_edit_hook) can read the filter a user applied and return a modified one. This is useful when the editor should widen a user's selection behind the scenes - for example, when a user filters for the current quarter and the grid should also show the previous one. The hook runs inside the `editors/getdata` service, so both the applied filter and the filtered rows are in scope. This page describes what is available at that point and works through an example. ## What the editor receives When a filtered editor loads, `editors/getdata` returns the filter to the client in two forms: - `sasparams.FILTER_TEXT` - the resolved WHERE clause, which the editor shows above the grid. - `query` - the filter's clauses as structured rows (`variable_nm`, `operator_nm`, `raw_value`), which the editor renders as the filter chips. The response does not carry the filter key (`FILTER_RK`). That is an *input* to `getdata`, not an output - the editor already holds it in the URL (`/editor//`), so there is nothing to return. ## When the hook runs `getdata` builds the filter, applies it, and sorts the result, all before the pre-edit hook runs. In order: 1. the WHERE clause is written to a temporary fileref, `filtref`; 2. `work.OUT` is populated with `where %inc filtref` - the filter is applied here; 3. the pre-edit hook runs; 4. `FILTER_TEXT` is read back out of `filtref`, and the response is streamed. So the hook sits between "the rows are filtered" and "the filter is returned". Two consequences follow: - It can change what the editor is told the filter was, by rewriting `filtref` and, if the chips must match, `work.query`. - It cannot change which rows came back *that* way, because the data step has already run. To return different rows, the hook must re-read the table itself. ## Take the value from the data A hook that wants to extend a date filter needs the date the user is working with. It does not have to parse the filter text for it - the filtered rows are already in `work.OUT`, so the value can be read from there. ## Example: widen a date filter to the previous quarter The hook below is registered in `MPE_TABLES.PRE_EDIT_HOOK` for a table with a date column `ORDER_DATE`. When a user opens the editor with a filter that includes `ORDER_DATE`, it extends the filter to also cover the previous quarter, re-reads the table so the grid shows both quarters, and returns the widened filter text. ```sas /* PRE_EDIT_HOOK: widen the user's ORDER_DATE filter by one quarter */ %if &filter_rk > 0 %then %do; /* the date the user is looking at, taken from the rows already returned */ %let refdt=; proc sql noprint; select max(ORDER_DATE) into :refdt from work.out; quit; %if &refdt ne %then %do; /* the previous quarter, relative to that date */ %let pq_from=%sysfunc(putn(%sysfunc(intnx(quarter,&refdt,-1,b)),date9.)); %let pq_to =%sysfunc(putn(%sysfunc(intnx(quarter,&refdt,-1,e)),date9.)); /* the user's own clauses, as a standalone WHERE */ %mp_filtergenerate(work.query,outref=userref) /* Data Controller's own clause - current-record validity, plus RLS */ %mpe_filtermaster(EDIT,&orig_libds, dclib=&mpelib, filter_rk=-1, outref=ctlref, outds=work.ctlq ) /* read each clause (which may span several lines) into a macro variable */ %let usertext=; data _null_; infile userref end=eof; input; length t $32767; retain t; t=catx(' ',t,_infile_); if eof then call symputx('usertext',t); run; %let ctltext=; data _null_; infile ctlref end=eof; input; length t $32767; retain t; t=catx(' ',t,_infile_); if eof then call symputx('ctltext',t); run; /* ( user's filter OR previous quarter ) AND ( validity + RLS ) */ filename newfilt temp; data _null_; file newfilt lrecl=32767; length f $32767; f='(( '!!trim(symget('usertext'))!!' ) OR (ORDER_DATE between "&pq_from"d and "&pq_to"d))' !!' AND ('!!trim(symget('ctltext'))!!' )'; put f; run; /* re-read the table, so the grid shows both quarters */ data work.out (drop=&var_txfrom &var_txto &var_processed); _____DELETE__THIS__RECORD_____="No"; set &orig_libds; where %inc newfilt; run; /* hand the widened filter back to the editor */ data _null_; file filtref lrecl=32767; infile newfilt; input; put _infile_; run; %end; %end; ``` ### What the hook does - **The reference date.** `work.OUT` holds the rows the user's filter returned, so the latest `ORDER_DATE` on screen is read straight from it. Nothing has to be parsed out of the filter text. - **The previous quarter.** `INTNX` moves back one quarter from that date and returns its first and last day, formatted as SAS date literals (`01APR2026` and `30JUN2026`, for example). - **The user's clause.** `work.QUERY` is the filter's clauses as structured rows, and `%mp_filtergenerate` turns them back into a WHERE clause. This is the user's filter on its own, with no Row Level Security or validity clause mixed in. - **Data Controller's clause.** Calling `%mpe_filtermaster` with `filter_rk=-1` produces the part of the filter that is not the user's: the current-record validity clause and any Row Level Security, or `1=1` when there is neither. - **The combined filter.** The two clauses are combined as `( user OR previous-quarter ) AND ( validity + RLS )`, so the previous quarter is added to the user's selection without widening anything the user is not entitled to see. - **The rows.** `work.OUT` is rebuilt with the combined filter, mirroring the data step `getdata` uses itself - including dropping the transaction and processing columns it drops. - **The returned text.** `filtref` is overwritten with the combined filter, so the clause the editor shows is the one that was actually applied. ## Row Level Security The user's clause and Data Controller's clause have to be kept apart, because Row Level Security lives in the second one. Combining them as `( user OR previous-quarter ) AND ( validity + RLS )` keeps RLS applied to both branches. Wrapping the whole existing filter instead - `( existing OR previous-quarter )` - would let the previous-quarter rows through without their RLS condition, so do not take that shortcut. ## The row cap `getdata` refuses a result larger than `DC_MAXOBS_WEBEDIT` (500 by default) with *Table is too big*. Widening a filter widens the result, so a hook that adds a quarter can push a table over the cap. Check the count before handing the data back, or raise the option for that table in `MPE_CONFIG`. ## Matching the filter chips The clause above the grid comes from `FILTER_TEXT`, so overwriting `filtref` is what updates it. The filter chips are rendered from the `query` output instead, so if they must reflect the widened filter too, amend `work.QUERY` in the same hook - append the extra clause as a new subgroup with `group_logic='OR'` before the response is built.