Macros for SAS Application Developers
https://github.com/sasjs/core
Loading...
Searching...
No Matches
mp_filtercheck.sas
Go to the documentation of this file.
1/**
2 @file
3 @brief Checks an input filter table for validity
4 @details Performs checks on the input table to ensure it arrives in the
5 correct format. This is necessary to prevent code injection. Will update
6 SYSCC to 1008 if bad records are found, and call mp_abort.sas for a
7 graceful service exit (configurable).
8
9 Used for dynamic filtering in [Data Controller for SAS®](
10 https://datacontroller.io).
11
12 Usage:
13
14 %mp_filtercheck(work.filter,targetds=sashelp.class,outds=work.badrecords)
15
16 The input table should have the following format:
17
18 |GROUP_LOGIC:$3|SUBGROUP_LOGIC:$3|SUBGROUP_ID:8.|VARIABLE_NM:$32|OPERATOR_NM:$10|RAW_VALUE:$4000|
19 |---|---|---|---|---|---|
20 |AND|AND|1|AGE|=|12|
21 |AND|AND|1|SEX|<=|'M'|
22 |AND|OR|2|Name|NOT IN|('Jane','Alfred')|
23 |AND|OR|2|Weight|>=|7|
24
25 Rules applied:
26
27 @li GROUP_LOGIC - only AND/OR
28 @li SUBGROUP_LOGIC - only AND/OR
29 @li SUBGROUP_ID - only integers
30 @li VARIABLE_NM - must be in the target table
31 @li OPERATOR_NM - only =/>/</<=/>=/BETWEEN/IN/NOT IN/NE/CONTAINS
32 @li RAW_VALUE - no unquoted values except integers, commas and spaces. For
33 numeric columns, SAS date, datetime and time literals are also accepted, eg
34 '20APR2042'd, '01JAN1960:00:00:42'dt, '00:00:42't.
35
36 @returns The &outds table containing any bad rows, plus a REASON_CD column.
37
38 @param [in] inds The table to be checked, with the format above
39 @param [in] targetds= The target dataset against which to verify VARIABLE_NM.
40 This must be available (ie, the library must be assigned).
41 @param [out] abort= (YES) If YES will call mp_abort.sas on any exceptions
42 @param [out] outds= (work.badrecords) The output table, which is a copy of the
43 &inds. table plus a REASON_CD column, containing only bad records.
44 If bad records are found, the SYSCC value will be set to 1008
45 (a general data problem).
46 Downstream processes should check this table (and return code) before
47 continuing.
48
49 <h4> SAS Macros </h4>
50 @li mp_abort.sas
51 @li mf_getuniquefileref.sas
52 @li mf_getvarlist.sas
53 @li mf_getvartype.sas
54 @li mp_filtergenerate.sas
55 @li mp_filtervalidate.sas
56
57 <h4> Related Macros </h4>
58 @li mp_filtergenerate.sas
59 @li mp_filtervalidate.sas
60
61 @version 9.3
62 @author Allan Bowe
63
64 @todo Support hex / name literals and exponents in RAW_VALUE field
65 @sasjslint {"maxLineLength": 99}
66**/
67
68%macro mp_filtercheck(inds,targetds=,outds=work.badrecords,abort=YES);
69
70%mp_abort(iftrue= (&syscc ne 0)
71 ,mac=&sysmacroname
72 ,msg=%str(syscc=&syscc - on macro entry)
73)
74
75/* Validate input column */
76%local vtype;
77%let vtype=%mf_getvartype(&inds,RAW_VALUE);
78%mp_abort(iftrue=(&abort=YES and &vtype ne C),
79 mac=&sysmacroname,
80 msg=%str(%str(ERR)OR: RAW_VALUE must be character)
81)
82%if &vtype ne C %then %do;
83 %put &sysmacroname: RAW_VALUE must be character;
84 %let syscc=42;
85 %return;
86%end;
87
88
89/**
90 * Sanitise the values based on valid value lists, then strip out
91 * quotes, commas, periods and spaces.
92 */
93%local reason_cd nobs;
94%let nobs=0;
95data &outds;
96 /*length GROUP_LOGIC SUBGROUP_LOGIC $3 SUBGROUP_ID 8 VARIABLE_NM $32
97 OPERATOR_NM $10 RAW_VALUE $4000;*/
98 set &inds end=last;
99 length reason_cd $4032 vtype vtype2 $1 vnum dsid 8 tmp $4000 re_temporal 8
100 vref $64;
101 drop tmp re_temporal vref;
102 retain re_temporal;
103 /* SAS temporal literals - date, datetime and time */
104 if _n_=1 then re_temporal=prxparse("/^'[^']*'(dt|d|t)$/i");
105
106 /* quick check to ensure column exists */
107 if upcase(VARIABLE_NM) not in
108 (%upcase(%mf_getvarlist(&targetds,dlm=%str(,),quote=SINGLE)))
109 then do;
110 REASON_CD="Variable "!!cats(variable_nm)!!" not in &targetds";
111 putlog REASON_CD= VARIABLE_NM=;
112 call symputx('reason_cd',reason_cd,'l');
113 call symputx('nobs',_n_,'l');
114 output;
115 return;
116 end;
117
118 /* need to open the dataset to get the column type */
119 retain dsid;
120 if _n_=1 then dsid=open("&targetds","i");
121 if dsid>0 then do;
122 vnum=varnum(dsid,VARIABLE_NM);
123 if vnum<1 then do;
124 /* should not happen as was also tested for above */
125 REASON_CD=cats("Variable (",VARIABLE_NM,") not found in &targetds");
126 putlog REASON_CD= dsid=;
127 call symputx('reason_cd',reason_cd,'l');
128 call symputx('nobs',_n_,'l');
129 output;
130 goto endstep;
131 end;
132 /* now we can get the type */
133 else vtype=vartype(dsid,vnum);
134 end;
135 else do;
136 REASON_CD=cats("Could not open &targetds");
137 putlog REASON_CD= dsid=;
138 call symputx('reason_cd',reason_cd,'l');
139 call symputx('nobs',_n_,'l');
140 output;
141 stop;
142 end;
143
144 /* closed list checks */
145 if GROUP_LOGIC not in ('AND','OR') then do;
146 REASON_CD='GROUP_LOGIC should be AND/OR, not:'!!cats(GROUP_LOGIC);
147 putlog REASON_CD= GROUP_LOGIC=;
148 call symputx('reason_cd',reason_cd,'l');
149 call symputx('nobs',_n_,'l');
150 output;
151 end;
152 if SUBGROUP_LOGIC not in ('AND','OR') then do;
153 REASON_CD='SUBGROUP_LOGIC should be AND/OR, not:'!!cats(SUBGROUP_LOGIC);
154 putlog REASON_CD= SUBGROUP_LOGIC=;
155 call symputx('reason_cd',reason_cd,'l');
156 call symputx('nobs',_n_,'l');
157 output;
158 end;
159 if mod(SUBGROUP_ID,1) ne 0 then do;
160 REASON_CD='SUBGROUP_ID should be integer, not '!!cats(subgroup_id);
161 putlog REASON_CD= SUBGROUP_ID=;
162 call symputx('reason_cd',reason_cd,'l');
163 call symputx('nobs',_n_,'l');
164 output;
165 end;
166 if OPERATOR_NM not in
167 ('=','>','<','<=','>=','NE','GE','LE','BETWEEN','IN','NOT IN','CONTAINS')
168 then do;
169 REASON_CD='Invalid OPERATOR_NM: '!!cats(OPERATOR_NM);
170 putlog REASON_CD= OPERATOR_NM=;
171 call symputx('reason_cd',reason_cd,'l');
172 call symputx('nobs',_n_,'l');
173 output;
174 end;
175
176 /* CONTAINS is a character-only operator - the engine rejects a numeric
177 operand, so catch the pairing here rather than letting the generated
178 clause fail validation with no reference to the offending variable */
179 if vtype='N' & OPERATOR_NM='CONTAINS' then do;
180 vref=cats("&targetds",'.',VARIABLE_NM);
181 REASON_CD=catx(' ','CONTAINS is only valid for character variables -'
182 ,vref,'is numeric');
183 putlog REASON_CD= OPERATOR_NM=;
184 call symputx('reason_cd',reason_cd,'l');
185 call symputx('nobs',_n_,'l');
186 output;
187 return;
188 end;
189
190 /* the operand must be a string literal or a character variable - the
191 engine rejects an unquoted numeric value even where the column is
192 character, eg 'name contains 12' */
193 if vtype='C' & OPERATOR_NM='CONTAINS'
194 & subpad(strip(raw_value),1,1) not in ('"',"'")
195 then do;
196 vref=cats("&targetds",'.',VARIABLE_NM);
197 /* a bare value can only be a variable reference if it is a valid name */
198 vnum=0;
199 if prxmatch('/^[a-z_][a-z0-9_]*$/i',strip(raw_value))>0 then do;
200 vnum=varnum(dsid,strip(raw_value));
201 end;
202 if vnum>=1 then do;
203 /* the name is a variable of this table - the type must be character */
204 vtype2=vartype(dsid,vnum);
205 if vtype2='C' then return;
206 REASON_CD=catx(' ',"Compared Type ("!!vtype2!!") is not (C) -"
207 ,vref,'CONTAINS',cats(raw_value));
208 end;
209 else REASON_CD=catx(' ','CONTAINS requires a quoted value or a character'
210 ,'variable -',vref,'CONTAINS',cats(raw_value));
211 putlog REASON_CD= OPERATOR_NM=;
212 call symputx('reason_cd',reason_cd,'l');
213 call symputx('nobs',_n_,'l');
214 output;
215 return;
216 end;
217
218 /* special missing logic */
219 if vtype='N' & OPERATOR_NM in ('=','>','<','<=','>=','NE','GE','LE') then do;
220 if cats(upcase(raw_value)) in (
221 '.','.A','.B','.C','.D','.E','.F','.G','.H','.I','.J','.K','.L','.M','.N'
222 '.N','.O','.P','.Q','.R','.S','.T','.U','.V','.W','.X','.Y','.Z','._'
223 )
224 then do;
225 /* valid numeric - exit data step loop */
226 return;
227 end;
228 else if subpad(upcase(raw_value),1,1) in (
229 'A','B','C','D','E','F','G','H','I','J','K','L','M','N'
230 'N','O','P','Q','R','S','T','U','V','W','X','Y','Z','_'
231 )
232 then do;
233 /* check if the raw_value contains a valid variable NAME */
234 vnum=varnum(dsid,subpad(raw_value,1,32));
235 if vnum>0 then do;
236 /* now we can get the type */
237 vtype2=vartype(dsid,vnum);
238 /* check type matches */
239 if vtype2=vtype then do;
240 /* valid target var - exit loop */
241 return;
242 end;
243 else do;
244 REASON_CD=cats("Compared Type (",vtype2,") is not (",vtype,")");
245 putlog REASON_CD= dsid=;
246 call symputx('reason_cd',reason_cd,'l');
247 call symputx('nobs',_n_,'l');
248 output;
249 goto endstep;
250 end;
251 end;
252 end;
253 end;
254
255 /* a SAS temporal literal is a valid value for a numeric column */
256 if vtype='N' and prxmatch(re_temporal,cats(raw_value))>0 then return;
257
258 /* special logic */
259 if OPERATOR_NM in ('IN','NOT IN','BETWEEN') then do;
260 if OPERATOR_NM='BETWEEN' then raw_value1=tranwrd(raw_value,' AND ',',');
261 else do;
262 if substr(raw_value,1,1) ne '('
263 or substr(cats(reverse(raw_value)),1,1) ne ')'
264 then do;
265 REASON_CD='Missing start/end bracket in RAW_VALUE';
266 putlog REASON_CD= OPERATOR_NM= raw_value= raw_value1= ;
267 call symputx('reason_cd',reason_cd,'l');
268 call symputx('nobs',_n_,'l');
269 output;
270 end;
271 else raw_value1=substr(raw_value,2,max(length(raw_value)-2,0));
272 end;
273 /* we now have a comma seperated list of values */
274 if vtype='N' then do i=1 to countc(raw_value1, ',')+1;
275 tmp=scan(raw_value1,i,',');
276 if cats(tmp) ne '.' and input(tmp, ?? 8.) eq . then do;
277 /* a SAS temporal literal is a valid value for a numeric column */
278 if prxmatch(re_temporal,cats(tmp))=0 then do;
279 if OPERATOR_NM ='BETWEEN' and subpad(upcase(tmp),1,1) in (
280 'A','B','C','D','E','F','G','H','I','J','K','L','M','N'
281 'N','O','P','Q','R','S','T','U','V','W','X','Y','Z','_'
282 )
283 then do;
284 /* check if the raw_value contains a valid variable NAME */
285 /* is not valid syntax for IN or NOT IN */
286 vnum=varnum(dsid,subpad(tmp,1,32));
287 if vnum>0 then do;
288 /* now we can get the type */
289 vtype2=vartype(dsid,vnum);
290 /* check type matches */
291 if vtype2 ne vtype then do;
292 REASON_CD=cats("Compared Type (",vtype2,") is not (",vtype,")");
293 putlog REASON_CD= dsid=;
294 call symputx('reason_cd',reason_cd,'l');
295 call symputx('nobs',_n_,'l');
296 output;
297 goto endstep;
298 end;
299 end;
300 else do;
301 REASON_CD='Non Numeric value provided';
302 putlog REASON_CD= OPERATOR_NM= raw_value= raw_value1= ;
303 call symputx('reason_cd',reason_cd,'l');
304 call symputx('nobs',_n_,'l');
305 output;
306 end;
307 end;
308 else do;
309 REASON_CD='Non Numeric value provided';
310 putlog REASON_CD= OPERATOR_NM= raw_value= raw_value1= ;
311 call symputx('reason_cd',reason_cd,'l');
312 call symputx('nobs',_n_,'l');
313 output;
314 end;
315 end;
316 end;
317 end;
318 /* every value of a numeric IN / BETWEEN list is checked one by one above,
319 so a numeric list skips the generic check below - that would only flag
320 the list syntax itself. A character list is unchanged. */
321 if vtype='N' then return;
322 end;
323 else raw_value1=raw_value;
324
325 /* remove nested literals eg '' */
326 raw_value1=tranwrd(raw_value1,"''",'');
327
328 /* now match string literals (always single quotes) */
329 raw_value2=raw_value1;
330 regex = prxparse("s/(\').*?(\')//");
331 call prxchange(regex,-1,raw_value2);
332
333 /* remove commas and periods*/
334 raw_value3=compress(raw_value2,',.');
335
336 /* output records that contain values other than digits and spaces */
337 if notdigit(compress(raw_value3,' '))>0 then do;
338 if vtype='C' and subpad(upcase(raw_value),1,1) in (
339 'A','B','C','D','E','F','G','H','I','J','K','L','M','N'
340 'N','O','P','Q','R','S','T','U','V','W','X','Y','Z','_'
341 )
342 then do;
343 /* check if the raw_value contains a valid variable NAME */
344 vnum=varnum(dsid,subpad(raw_value,1,32));
345 if vnum>0 then do;
346 /* now we can get the type */
347 vtype2=vartype(dsid,vnum);
348 /* check type matches */
349 if vtype2=vtype then do;
350 /* valid target var - exit loop */
351 return;
352 end;
353 else do;
354 REASON_CD=cats("Compared Char Type (",vtype2,") is not (",vtype,")");
355 putlog REASON_CD= dsid=;
356 call symputx('reason_cd',reason_cd,'l');
357 call symputx('nobs',_n_,'l');
358 output;
359 goto endstep;
360 end;
361 end;
362 end;
363
364 putlog raw_value3= $hex32.;
365 REASON_CD=cats('Invalid RAW_VALUE:',raw_value);
366 putlog (_all_)(=);
367 call symputx('reason_cd',reason_cd,'l');
368 call symputx('nobs',_n_,'l');
369 output;
370 end;
371
372 endstep:
373 if last then rc=close(dsid);
374run;
375
376
377data _null_;
378 set &outds end=last;
379 putlog (_all_)(=);
380run;
381
382%mp_abort(iftrue=(&abort=YES and &nobs>0),
383 mac=&sysmacroname,
384 msg=%str(Data issue: %superq(reason_cd))
385)
386
387%if &nobs>0 %then %do;
388 %let syscc=1008;
389 %return;
390%end;
391
392/**
393 * syntax checking passed but it does not mean the filter is valid
394 * for that we can run a proc sql validate query
395 */
396%local fref1;
397%let fref1=%mf_getuniquefileref();
398%mp_filtergenerate(&inds,outref=&fref1)
399
400/* this macro will also set syscc to 1008 if any issues found */
401%mp_filtervalidate(&fref1,&targetds,outds=&outds,abort=&abort)
402
403%mend mp_filtercheck;