mirror of
https://github.com/sasjs/core.git
synced 2026-01-07 17:40:05 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98118adb9a | ||
|
|
369c4412f3 | ||
|
|
7d7608f06c | ||
|
|
3791cb8a2c | ||
|
|
ff82f7d75c |
500
all.sas
500
all.sas
@@ -1040,14 +1040,17 @@ options noquotelenmax;
|
|||||||
returns:
|
returns:
|
||||||
> List of Variables=Name Sex Age Height Weight
|
> List of Variables=Name Sex Age Height Weight
|
||||||
|
|
||||||
|
For a seperated list of column values:
|
||||||
|
|
||||||
%put %mf_getvarlist(sashelp.class,dlm=%str(,),quote=double);
|
%put %mf_getvarlist(sashelp.class,dlm=%str(,),quote=double);
|
||||||
|
|
||||||
returns:
|
returns:
|
||||||
> "Name","Sex","Age","Height","Weight"
|
> "Name","Sex","Age","Height","Weight"
|
||||||
|
|
||||||
@param libds Two part dataset (or view) reference.
|
@param [in] libds Two part dataset (or view) reference.
|
||||||
@param dlm= provide a delimiter (eg comma or space) to separate the vars
|
@param [in] dlm= ( ) Provide a delimiter (eg comma or space) to separate the
|
||||||
@param quote= use either DOUBLE or SINGLE to quote the results
|
variables
|
||||||
|
@param [in] quote= (none) use either DOUBLE or SINGLE to quote the results
|
||||||
|
|
||||||
@version 9.2
|
@version 9.2
|
||||||
@author Allan Bowe
|
@author Allan Bowe
|
||||||
@@ -1779,6 +1782,240 @@ Usage:
|
|||||||
%mend;
|
%mend;
|
||||||
|
|
||||||
/** @endcond *//**
|
/** @endcond *//**
|
||||||
|
@file
|
||||||
|
@brief Asserts the values in a column
|
||||||
|
@details Useful in the context of writing sasjs tests. The results of the
|
||||||
|
test are _appended_ to the &outds. table.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
data work.checkds;
|
||||||
|
do checkval='Jane','James','Jill';
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
run;
|
||||||
|
%mp_assertcolvals(sashelp.class.name,
|
||||||
|
checkvals=work.checkds.checkval,
|
||||||
|
desc=At least one value has a match,
|
||||||
|
test=ANYVAL
|
||||||
|
)
|
||||||
|
|
||||||
|
data work.check;
|
||||||
|
do val='M','F';
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
run;
|
||||||
|
%mp_assertcolvals(sashelp.class.sex,
|
||||||
|
checkvals=work.check.val,
|
||||||
|
desc=All values have a match,
|
||||||
|
test=ALLVALS
|
||||||
|
)
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mf_existds.sas
|
||||||
|
@li mf_nobs.sas
|
||||||
|
@li mp_abort.sas
|
||||||
|
|
||||||
|
|
||||||
|
@param [in] indscol The input library.dataset.column to test for values
|
||||||
|
@param [in] checkvals= A library.dataset.column value containing a UNIQUE
|
||||||
|
list of values to be compared against the source (indscol).
|
||||||
|
@param [in] desc= (Testing observations) The user provided test description
|
||||||
|
@param [in] test= (ALLVALS) The test to apply. Valid values are:
|
||||||
|
@li ALLVALS - Test is a PASS if ALL values have a match in checkvals
|
||||||
|
@li ANYVAL - Test is a PASS if at least 1 value has a match in checkvals
|
||||||
|
@param [out] outds= (work.test_results) The output dataset to contain the
|
||||||
|
results. If it does not exist, it will be created, with the following format:
|
||||||
|
|TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256|
|
||||||
|
|---|---|---|
|
||||||
|
|User Provided description|PASS|Column &indscol contained ALL target vals|
|
||||||
|
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_assertdsobs.sas
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_assertcolvals(indscol,
|
||||||
|
checkvals=0,
|
||||||
|
test=ALLVALS,
|
||||||
|
desc=mp_assertcolvals - no desc provided,
|
||||||
|
outds=work.test_results
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&syscc ne 0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(syscc=&syscc - on macro entry)
|
||||||
|
)
|
||||||
|
|
||||||
|
%local lib ds col clib cds ccol nobs;
|
||||||
|
%let lib=%scan(&indscol,1,%str(.));
|
||||||
|
%let ds=%scan(&indscol,2,%str(.));
|
||||||
|
%let col=%scan(&indscol,3,%str(.));
|
||||||
|
%mp_abort(iftrue= (%mf_existds(&lib..&ds)=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(&lib..&ds not found!)
|
||||||
|
)
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&checkvals=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(Set CHECKVALS to a library.dataset.column containing check vals)
|
||||||
|
)
|
||||||
|
%let clib=%scan(&checkvals,1,%str(.));
|
||||||
|
%let cds=%scan(&checkvals,2,%str(.));
|
||||||
|
%let ccol=%scan(&checkvals,3,%str(.));
|
||||||
|
%mp_abort(iftrue= (%mf_existds(&clib..&cds)=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(&clib..&cds not found!)
|
||||||
|
)
|
||||||
|
%let nobs=%mf_nobs(&clib..&cds);
|
||||||
|
%mp_abort(iftrue= (&nobs=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(&clib..&cds is empty!)
|
||||||
|
)
|
||||||
|
|
||||||
|
%let test=%upcase(&test);
|
||||||
|
|
||||||
|
%if &test ne ALLVALS and &test ne ANYVAL %then %do;
|
||||||
|
%mp_abort(
|
||||||
|
mac=&sysmacroname,
|
||||||
|
msg=%str(Invalid test - &test)
|
||||||
|
)
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%local result orig;
|
||||||
|
%let result=-1;
|
||||||
|
%let orig=-1;
|
||||||
|
proc sql noprint;
|
||||||
|
select count(*) into: result
|
||||||
|
from &lib..&ds
|
||||||
|
where &col not in (
|
||||||
|
select &ccol from &clib..&cds
|
||||||
|
);
|
||||||
|
select count(*) into: orig from &lib..&ds;
|
||||||
|
quit;
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&syscc ne 0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(syscc=&syscc after macro query)
|
||||||
|
)
|
||||||
|
|
||||||
|
data;
|
||||||
|
length test_description $256 test_result $4 test_comments $256;
|
||||||
|
test_description=symget('desc');
|
||||||
|
test_result='FAIL';
|
||||||
|
test_comments="&sysmacroname: &lib..&ds..&col has &result values "
|
||||||
|
!!"not in &clib..&cds..&ccol ";
|
||||||
|
%if &test=ANYVAL %then %do;
|
||||||
|
if &result < &orig then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %if &test=ALLVALS %then %do;
|
||||||
|
if &result=0 then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
test_comments="&sysmacroname: Unsatisfied test condition - &test";
|
||||||
|
%end;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%local ds;
|
||||||
|
%let ds=&syslast;
|
||||||
|
proc append base=&outds data=&ds;
|
||||||
|
run;
|
||||||
|
proc sql;
|
||||||
|
drop table &ds;
|
||||||
|
|
||||||
|
%mend;/**
|
||||||
|
@file
|
||||||
|
@brief Asserts the number of observations in a dataset
|
||||||
|
@details Useful in the context of writing sasjs tests. The results of the
|
||||||
|
test are _appended_ to the &outds. table.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
%mp_assertdsobs(sashelp.class) %* tests if any observations are present;
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mf_nobs.sas
|
||||||
|
@li mp_abort.sas
|
||||||
|
|
||||||
|
|
||||||
|
@param [in] inds input dataset to test for presence of observations
|
||||||
|
@param [in] desc= (Testing observations) The user provided test description
|
||||||
|
@param [in] test= (HASOBS) The test to apply. Valid values are:
|
||||||
|
@li HASOBS - Test is a PASS if the input dataset has any observations
|
||||||
|
@li EMPTY - Test is a PASS if input dataset is empty
|
||||||
|
@li EQUALS [integer] - Test passes if obs count matches the provided integer
|
||||||
|
@param [out] outds= (work.test_results) The output dataset to contain the
|
||||||
|
results. If it does not exist, it will be created, with the following format:
|
||||||
|
|TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256|
|
||||||
|
|---|---|---|
|
||||||
|
|User Provided description|PASS|Dataset &inds has XX obs|
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_assertcolvals.sas
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_assertdsobs(inds,
|
||||||
|
test=HASOBS,
|
||||||
|
desc=Testing observations,
|
||||||
|
outds=work.test_results
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
%local nobs;
|
||||||
|
%let nobs=%mf_nobs(&inds);
|
||||||
|
%let test=%upcase(&test);
|
||||||
|
|
||||||
|
%if %substr(&test.xxxxx,1,6)=EQUALS %then %do;
|
||||||
|
%let val=%scan(&test,2,%str( ));
|
||||||
|
%mp_abort(iftrue= (%DATATYP(&val)=CHAR)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(Invalid test - &test, expected EQUALS [integer])
|
||||||
|
)
|
||||||
|
%let test=EQUALS;
|
||||||
|
%end;
|
||||||
|
%else %if &test ne HASOBS and &test ne EMPTY %then %do;
|
||||||
|
%mp_abort(
|
||||||
|
mac=&sysmacroname,
|
||||||
|
msg=%str(Invalid test - &test)
|
||||||
|
)
|
||||||
|
%end;
|
||||||
|
|
||||||
|
data;
|
||||||
|
length test_description $256 test_result $4 test_comments $256;
|
||||||
|
test_description=symget('desc');
|
||||||
|
test_result='FAIL';
|
||||||
|
test_comments="&sysmacroname: Dataset &inds has &nobs observations";
|
||||||
|
%if &test=HASOBS %then %do;
|
||||||
|
if &nobs>0 then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %if &test=EMPTY %then %do;
|
||||||
|
if &nobs=0 then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %if &test=EQUALS %then %do;
|
||||||
|
if &nobs=&val then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
test_comments="&sysmacroname: Unsatisfied test condition - &test";
|
||||||
|
%end;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%local ds;
|
||||||
|
%let ds=&syslast;
|
||||||
|
|
||||||
|
proc append base=&outds data=&ds;
|
||||||
|
run;
|
||||||
|
|
||||||
|
proc sql;
|
||||||
|
drop table &ds;
|
||||||
|
|
||||||
|
%mend;/**
|
||||||
@file
|
@file
|
||||||
@brief Copy any file using binary input / output streams
|
@brief Copy any file using binary input / output streams
|
||||||
@details Reads in a file byte by byte and writes it back out. Is an
|
@details Reads in a file byte by byte and writes it back out. Is an
|
||||||
@@ -2812,6 +3049,263 @@ run;
|
|||||||
|
|
||||||
|
|
||||||
%mend;/**
|
%mend;/**
|
||||||
|
@file
|
||||||
|
@brief Checks an input filter table for validity
|
||||||
|
@details Performs checks on the input table to ensure it arrives in the
|
||||||
|
correct format. This is necessary to prevent code injection. Will update
|
||||||
|
SYSCC to 1008 if bad records are found, and call mp_abort.sas for a
|
||||||
|
graceful service exit (configurable).
|
||||||
|
|
||||||
|
Used for dynamic filtering in [Data Controller for SAS®](https://datacontroller.io).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
%mp_filtercheck(work.filter,targetds=sashelp.class,outds=work.badrecords)
|
||||||
|
|
||||||
|
The input table should have the following format:
|
||||||
|
|
||||||
|
|GROUP_LOGIC:$3|SUBGROUP_LOGIC:$3|SUBGROUP_ID:8.|VARIABLE_NM:$32|OPERATOR_NM:$10|RAW_VALUE:$32767|
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
|AND|AND|1|AGE|=|12|
|
||||||
|
|AND|AND|1|SEX|<=|'M'|
|
||||||
|
|AND|OR|2|Name|NOT IN|('Jane','Alfred')|
|
||||||
|
|AND|OR|2|Weight|>=|7|
|
||||||
|
|
||||||
|
Rules applied:
|
||||||
|
|
||||||
|
@li GROUP_LOGIC - only AND/OR
|
||||||
|
@li SUBGROUP_LOGIC - only AND/OR
|
||||||
|
@li SUBGROUP_ID - only integers
|
||||||
|
@li VARIABLE_NM - must be in the target table
|
||||||
|
@li OPERATOR_NM - only =/>/</<=/>=/BETWEEN/IN/NOT IN/NOT EQUAL/CONTAINS
|
||||||
|
@li RAW_VALUE - no unquoted values except integers, commas and spaces.
|
||||||
|
|
||||||
|
@returns The &outds table containing any bad rows, plus a REASON_CD column.
|
||||||
|
|
||||||
|
@param [in] inds The table to be checked, with the format above
|
||||||
|
@param [in] targetds= The target dataset against which to verify VARIABLE_NM
|
||||||
|
@param [out] abort= (YES) If YES will call mp_abort.sas on any exceptions
|
||||||
|
@param [out] outds= The output table, which is a copy of the &inds. table
|
||||||
|
plus a REASON_CD column, containing only bad records. If bad records found,
|
||||||
|
the SYSCC value will be set to 1008 (general data problem). Downstream
|
||||||
|
processes should check this table (and return code) before continuing.
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mp_abort.sas
|
||||||
|
@li mf_getvarlist.sas
|
||||||
|
@li mf_nobs.sas
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_filtergenerate.sas
|
||||||
|
|
||||||
|
@version 9.3
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
@todo Support date / hex / name literals and exponents in RAW_VALUE field
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_filtercheck(inds,targetds=,outds=work.badrecords,abort=YES);
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&syscc ne 0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(syscc=&syscc - on macro entry)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitise the values based on valid value lists, then strip out
|
||||||
|
* quotes, commas, periods and spaces.
|
||||||
|
* Only numeric values should remain
|
||||||
|
*/
|
||||||
|
|
||||||
|
data &outds;
|
||||||
|
set &inds;
|
||||||
|
length reason_cd $32;
|
||||||
|
|
||||||
|
/* closed list checks */
|
||||||
|
if GROUP_LOGIC not in ('AND','OR') then do;
|
||||||
|
REASON_CD='GROUP_LOGIC should be either AND or OR';
|
||||||
|
putlog REASON_CD= GROUP_LOGIC=;
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
if SUBGROUP_LOGIC not in ('AND','OR') then do;
|
||||||
|
REASON_CD='SUBGROUP_LOGIC should be either AND or OR';
|
||||||
|
putlog REASON_CD= SUBGROUP_LOGIC=;
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
if mod(SUBGROUP_ID,1) ne 0 then do;
|
||||||
|
REASON_CD='SUBGROUP_ID should be integer';
|
||||||
|
putlog REASON_CD= SUBGROUP_ID=;
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
if upcase(VARIABLE_NM) not in
|
||||||
|
(%upcase(%mf_getvarlist(&targetds,dlm=%str(,),quote=SINGLE)))
|
||||||
|
then do;
|
||||||
|
REASON_CD="VARIABLE_NM not in &targetds";
|
||||||
|
putlog REASON_CD= VARIABLE_NM=;
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
if OPERATOR_NM not in
|
||||||
|
('=','>','<','<=','>=','BETWEEN','IN','NOT IN','NE','CONTAINS')
|
||||||
|
then do;
|
||||||
|
REASON_CD='Invalid OPERATOR_NM';
|
||||||
|
putlog REASON_CD= OPERATOR_NM=;
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
|
||||||
|
/* special logic */
|
||||||
|
if OPERATOR_NM='BETWEEN' then raw_value1=tranwrd(raw_value,' AND ','');
|
||||||
|
else if OPERATOR_NM in ('IN','NOT IN') then do;
|
||||||
|
if substr(raw_value,1,1) ne '('
|
||||||
|
or substr(cats(reverse(raw_value)),1,1) ne ')'
|
||||||
|
then do;
|
||||||
|
REASON_CD='Missing brackets in RAW_VALUE';
|
||||||
|
putlog REASON_CD= OPERATOR_NM= raw_value= raw_value1= ;
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
else raw_value1=substr(raw_value,2,max(length(raw_value)-2,0));
|
||||||
|
end;
|
||||||
|
else raw_value1=raw_value;
|
||||||
|
|
||||||
|
/* remove nested literals eg '' */
|
||||||
|
raw_value1=tranwrd(raw_value1,"''",'');
|
||||||
|
|
||||||
|
/* now match string literals (always single quotes) */
|
||||||
|
raw_value2=raw_value1;
|
||||||
|
regex = prxparse("s/(\').*?(\')//");
|
||||||
|
call prxchange(regex,-1,raw_value2);
|
||||||
|
|
||||||
|
/* remove commas and periods*/
|
||||||
|
raw_value3=compress(raw_value2,',.');
|
||||||
|
|
||||||
|
/* output records that contain values other than digits and spaces */
|
||||||
|
if notdigit(compress(raw_value3,' '))>0 then do;
|
||||||
|
putlog raw_value3= $hex32.;
|
||||||
|
REASON_CD='Invalid RAW_VALUE';
|
||||||
|
putlog REASON_CD= raw_value= raw_value1= raw_value2= raw_value3=;
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
|
||||||
|
run;
|
||||||
|
|
||||||
|
%if %mf_nobs(&outds)>0 %then %do;
|
||||||
|
%if &abort=YES %then %do;
|
||||||
|
data _null_;
|
||||||
|
set &outds;
|
||||||
|
call symputx('REASON_CD',reason_cd,'l');
|
||||||
|
stop;
|
||||||
|
run;
|
||||||
|
%mp_abort(
|
||||||
|
mac=&sysmacroname,
|
||||||
|
msg=%str(Filter issues in &inds, first was &reason_cd, details in &outds)
|
||||||
|
)
|
||||||
|
%end;
|
||||||
|
%let syscc=1008;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
%mend;
|
||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Generates a filter clause from an input table, to a fileref
|
||||||
|
@details Uses the input table to generate an output filter clause.
|
||||||
|
This feature is used to create dynamic dropdowns in [Data Controller for SAS®](
|
||||||
|
https://datacontroller.io). The input table should be in the format below:
|
||||||
|
|
||||||
|
|GROUP_LOGIC:$3|SUBGROUP_LOGIC:$3|SUBGROUP_ID:8.|VARIABLE_NM:$32|OPERATOR_NM:$10|RAW_VALUE:$32767|
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
|AND|AND|1|AGE|=|12|
|
||||||
|
|AND|AND|1|SEX|<=|'M'|
|
||||||
|
|AND|OR|2|Name|NOT IN|('Jane','Alfred')|
|
||||||
|
|AND|OR|2|Weight|>=|7|
|
||||||
|
|
||||||
|
Note - if the above table is received from an external client, the values
|
||||||
|
should first be validated using the mp_filtercheck.sas macro to avoid risk
|
||||||
|
of SQL injection.
|
||||||
|
|
||||||
|
To generate the filter, run the following code:
|
||||||
|
|
||||||
|
data work.filtertable;
|
||||||
|
infile datalines4 dsd;
|
||||||
|
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
|
||||||
|
OPERATOR_NM:$10. RAW_VALUE:$32767.;
|
||||||
|
datalines4;
|
||||||
|
AND,AND,1,AGE,=,12
|
||||||
|
AND,AND,1,SEX,<=,"'M'"
|
||||||
|
AND,OR,2,Name,NOT IN,"('Jane','Alfred')"
|
||||||
|
AND,OR,2,Weight,>=,7
|
||||||
|
;;;;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mp_filtergenerate(work.filtertable,outref=myfilter)
|
||||||
|
|
||||||
|
data _null_;
|
||||||
|
infile myfilter;
|
||||||
|
input;
|
||||||
|
put _infile_;
|
||||||
|
run;
|
||||||
|
|
||||||
|
Will write the following query to the log:
|
||||||
|
|
||||||
|
> (
|
||||||
|
> AGE = 12
|
||||||
|
> AND
|
||||||
|
> SEX <= 'M'
|
||||||
|
> ) AND (
|
||||||
|
> Name NOT IN ('Jane','Alfred')
|
||||||
|
> OR
|
||||||
|
> Weight >= 7
|
||||||
|
> )
|
||||||
|
|
||||||
|
@param [in] inds The input table with query values
|
||||||
|
@param [out] outref= The output fileref to contain the filter clause. Will
|
||||||
|
be created (or replaced).
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_filtercheck.sas
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mp_abort.sas
|
||||||
|
@li mf_nobs.sas
|
||||||
|
|
||||||
|
@version 9.3
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_filtergenerate(inds,outref=filter);
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&syscc ne 0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(syscc=&syscc - on macro entry)
|
||||||
|
)
|
||||||
|
|
||||||
|
filename &outref temp;
|
||||||
|
|
||||||
|
%if %mf_nobs(&inds)=0 %then %do;
|
||||||
|
/* ensure we have a default filter */
|
||||||
|
data _null_;
|
||||||
|
file &outref;
|
||||||
|
put '1=1';
|
||||||
|
run;
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
data _null_;
|
||||||
|
file &outref lrecl=32800;
|
||||||
|
set &inds end=last;
|
||||||
|
by SUBGROUP_ID;
|
||||||
|
if _n_=1 then put '(';
|
||||||
|
else if first.SUBGROUP_ID then put +1 GROUP_LOGIC '(';
|
||||||
|
else put +2 SUBGROUP_LOGIC;
|
||||||
|
|
||||||
|
put +4 VARIABLE_NM OPERATOR_NM RAW_VALUE;
|
||||||
|
|
||||||
|
if last.SUBGROUP_ID then put ')'@;
|
||||||
|
run;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%mend;
|
||||||
|
/**
|
||||||
@file mp_getconstraints.sas
|
@file mp_getconstraints.sas
|
||||||
@brief Get constraint details at column level
|
@brief Get constraint details at column level
|
||||||
@details Useful for capturing constraints before they are dropped / reapplied
|
@details Useful for capturing constraints before they are dropped / reapplied
|
||||||
|
|||||||
@@ -10,14 +10,17 @@
|
|||||||
returns:
|
returns:
|
||||||
> List of Variables=Name Sex Age Height Weight
|
> List of Variables=Name Sex Age Height Weight
|
||||||
|
|
||||||
|
For a seperated list of column values:
|
||||||
|
|
||||||
%put %mf_getvarlist(sashelp.class,dlm=%str(,),quote=double);
|
%put %mf_getvarlist(sashelp.class,dlm=%str(,),quote=double);
|
||||||
|
|
||||||
returns:
|
returns:
|
||||||
> "Name","Sex","Age","Height","Weight"
|
> "Name","Sex","Age","Height","Weight"
|
||||||
|
|
||||||
@param libds Two part dataset (or view) reference.
|
@param [in] libds Two part dataset (or view) reference.
|
||||||
@param dlm= provide a delimiter (eg comma or space) to separate the vars
|
@param [in] dlm= ( ) Provide a delimiter (eg comma or space) to separate the
|
||||||
@param quote= use either DOUBLE or SINGLE to quote the results
|
variables
|
||||||
|
@param [in] quote= (none) use either DOUBLE or SINGLE to quote the results
|
||||||
|
|
||||||
@version 9.2
|
@version 9.2
|
||||||
@author Allan Bowe
|
@author Allan Bowe
|
||||||
|
|||||||
147
base/mp_assertcolvals.sas
Normal file
147
base/mp_assertcolvals.sas
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Asserts the values in a column
|
||||||
|
@details Useful in the context of writing sasjs tests. The results of the
|
||||||
|
test are _appended_ to the &outds. table.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
data work.checkds;
|
||||||
|
do checkval='Jane','James','Jill';
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
run;
|
||||||
|
%mp_assertcolvals(sashelp.class.name,
|
||||||
|
checkvals=work.checkds.checkval,
|
||||||
|
desc=At least one value has a match,
|
||||||
|
test=ANYVAL
|
||||||
|
)
|
||||||
|
|
||||||
|
data work.check;
|
||||||
|
do val='M','F';
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
run;
|
||||||
|
%mp_assertcolvals(sashelp.class.sex,
|
||||||
|
checkvals=work.check.val,
|
||||||
|
desc=All values have a match,
|
||||||
|
test=ALLVALS
|
||||||
|
)
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mf_existds.sas
|
||||||
|
@li mf_nobs.sas
|
||||||
|
@li mp_abort.sas
|
||||||
|
|
||||||
|
|
||||||
|
@param [in] indscol The input library.dataset.column to test for values
|
||||||
|
@param [in] checkvals= A library.dataset.column value containing a UNIQUE
|
||||||
|
list of values to be compared against the source (indscol).
|
||||||
|
@param [in] desc= (Testing observations) The user provided test description
|
||||||
|
@param [in] test= (ALLVALS) The test to apply. Valid values are:
|
||||||
|
@li ALLVALS - Test is a PASS if ALL values have a match in checkvals
|
||||||
|
@li ANYVAL - Test is a PASS if at least 1 value has a match in checkvals
|
||||||
|
@param [out] outds= (work.test_results) The output dataset to contain the
|
||||||
|
results. If it does not exist, it will be created, with the following format:
|
||||||
|
|TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256|
|
||||||
|
|---|---|---|
|
||||||
|
|User Provided description|PASS|Column &indscol contained ALL target vals|
|
||||||
|
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_assertdsobs.sas
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_assertcolvals(indscol,
|
||||||
|
checkvals=0,
|
||||||
|
test=ALLVALS,
|
||||||
|
desc=mp_assertcolvals - no desc provided,
|
||||||
|
outds=work.test_results
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&syscc ne 0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(syscc=&syscc - on macro entry)
|
||||||
|
)
|
||||||
|
|
||||||
|
%local lib ds col clib cds ccol nobs;
|
||||||
|
%let lib=%scan(&indscol,1,%str(.));
|
||||||
|
%let ds=%scan(&indscol,2,%str(.));
|
||||||
|
%let col=%scan(&indscol,3,%str(.));
|
||||||
|
%mp_abort(iftrue= (%mf_existds(&lib..&ds)=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(&lib..&ds not found!)
|
||||||
|
)
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&checkvals=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(Set CHECKVALS to a library.dataset.column containing check vals)
|
||||||
|
)
|
||||||
|
%let clib=%scan(&checkvals,1,%str(.));
|
||||||
|
%let cds=%scan(&checkvals,2,%str(.));
|
||||||
|
%let ccol=%scan(&checkvals,3,%str(.));
|
||||||
|
%mp_abort(iftrue= (%mf_existds(&clib..&cds)=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(&clib..&cds not found!)
|
||||||
|
)
|
||||||
|
%let nobs=%mf_nobs(&clib..&cds);
|
||||||
|
%mp_abort(iftrue= (&nobs=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(&clib..&cds is empty!)
|
||||||
|
)
|
||||||
|
|
||||||
|
%let test=%upcase(&test);
|
||||||
|
|
||||||
|
%if &test ne ALLVALS and &test ne ANYVAL %then %do;
|
||||||
|
%mp_abort(
|
||||||
|
mac=&sysmacroname,
|
||||||
|
msg=%str(Invalid test - &test)
|
||||||
|
)
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%local result orig;
|
||||||
|
%let result=-1;
|
||||||
|
%let orig=-1;
|
||||||
|
proc sql noprint;
|
||||||
|
select count(*) into: result
|
||||||
|
from &lib..&ds
|
||||||
|
where &col not in (
|
||||||
|
select &ccol from &clib..&cds
|
||||||
|
);
|
||||||
|
select count(*) into: orig from &lib..&ds;
|
||||||
|
quit;
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&syscc ne 0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(syscc=&syscc after macro query)
|
||||||
|
)
|
||||||
|
|
||||||
|
data;
|
||||||
|
length test_description $256 test_result $4 test_comments $256;
|
||||||
|
test_description=symget('desc');
|
||||||
|
test_result='FAIL';
|
||||||
|
test_comments="&sysmacroname: &lib..&ds..&col has &result values "
|
||||||
|
!!"not in &clib..&cds..&ccol ";
|
||||||
|
%if &test=ANYVAL %then %do;
|
||||||
|
if &result < &orig then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %if &test=ALLVALS %then %do;
|
||||||
|
if &result=0 then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
test_comments="&sysmacroname: Unsatisfied test condition - &test";
|
||||||
|
%end;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%local ds;
|
||||||
|
%let ds=&syslast;
|
||||||
|
proc append base=&outds data=&ds;
|
||||||
|
run;
|
||||||
|
proc sql;
|
||||||
|
drop table &ds;
|
||||||
|
|
||||||
|
%mend;
|
||||||
89
base/mp_assertdsobs.sas
Normal file
89
base/mp_assertdsobs.sas
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Asserts the number of observations in a dataset
|
||||||
|
@details Useful in the context of writing sasjs tests. The results of the
|
||||||
|
test are _appended_ to the &outds. table.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
%mp_assertdsobs(sashelp.class) %* tests if any observations are present;
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mf_nobs.sas
|
||||||
|
@li mp_abort.sas
|
||||||
|
|
||||||
|
|
||||||
|
@param [in] inds input dataset to test for presence of observations
|
||||||
|
@param [in] desc= (Testing observations) The user provided test description
|
||||||
|
@param [in] test= (HASOBS) The test to apply. Valid values are:
|
||||||
|
@li HASOBS - Test is a PASS if the input dataset has any observations
|
||||||
|
@li EMPTY - Test is a PASS if input dataset is empty
|
||||||
|
@li EQUALS [integer] - Test passes if obs count matches the provided integer
|
||||||
|
@param [out] outds= (work.test_results) The output dataset to contain the
|
||||||
|
results. If it does not exist, it will be created, with the following format:
|
||||||
|
|TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256|
|
||||||
|
|---|---|---|
|
||||||
|
|User Provided description|PASS|Dataset &inds has XX obs|
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_assertcolvals.sas
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_assertdsobs(inds,
|
||||||
|
test=HASOBS,
|
||||||
|
desc=Testing observations,
|
||||||
|
outds=work.test_results
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
%local nobs;
|
||||||
|
%let nobs=%mf_nobs(&inds);
|
||||||
|
%let test=%upcase(&test);
|
||||||
|
|
||||||
|
%if %substr(&test.xxxxx,1,6)=EQUALS %then %do;
|
||||||
|
%let val=%scan(&test,2,%str( ));
|
||||||
|
%mp_abort(iftrue= (%DATATYP(&val)=CHAR)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(Invalid test - &test, expected EQUALS [integer])
|
||||||
|
)
|
||||||
|
%let test=EQUALS;
|
||||||
|
%end;
|
||||||
|
%else %if &test ne HASOBS and &test ne EMPTY %then %do;
|
||||||
|
%mp_abort(
|
||||||
|
mac=&sysmacroname,
|
||||||
|
msg=%str(Invalid test - &test)
|
||||||
|
)
|
||||||
|
%end;
|
||||||
|
|
||||||
|
data;
|
||||||
|
length test_description $256 test_result $4 test_comments $256;
|
||||||
|
test_description=symget('desc');
|
||||||
|
test_result='FAIL';
|
||||||
|
test_comments="&sysmacroname: Dataset &inds has &nobs observations";
|
||||||
|
%if &test=HASOBS %then %do;
|
||||||
|
if &nobs>0 then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %if &test=EMPTY %then %do;
|
||||||
|
if &nobs=0 then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %if &test=EQUALS %then %do;
|
||||||
|
if &nobs=&val then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
test_comments="&sysmacroname: Unsatisfied test condition - &test";
|
||||||
|
%end;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%local ds;
|
||||||
|
%let ds=&syslast;
|
||||||
|
|
||||||
|
proc append base=&outds data=&ds;
|
||||||
|
run;
|
||||||
|
|
||||||
|
proc sql;
|
||||||
|
drop table &ds;
|
||||||
|
|
||||||
|
%mend;
|
||||||
157
base/mp_filtercheck.sas
Normal file
157
base/mp_filtercheck.sas
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Checks an input filter table for validity
|
||||||
|
@details Performs checks on the input table to ensure it arrives in the
|
||||||
|
correct format. This is necessary to prevent code injection. Will update
|
||||||
|
SYSCC to 1008 if bad records are found, and call mp_abort.sas for a
|
||||||
|
graceful service exit (configurable).
|
||||||
|
|
||||||
|
Used for dynamic filtering in [Data Controller for SAS®](https://datacontroller.io).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
%mp_filtercheck(work.filter,targetds=sashelp.class,outds=work.badrecords)
|
||||||
|
|
||||||
|
The input table should have the following format:
|
||||||
|
|
||||||
|
|GROUP_LOGIC:$3|SUBGROUP_LOGIC:$3|SUBGROUP_ID:8.|VARIABLE_NM:$32|OPERATOR_NM:$10|RAW_VALUE:$32767|
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
|AND|AND|1|AGE|=|12|
|
||||||
|
|AND|AND|1|SEX|<=|'M'|
|
||||||
|
|AND|OR|2|Name|NOT IN|('Jane','Alfred')|
|
||||||
|
|AND|OR|2|Weight|>=|7|
|
||||||
|
|
||||||
|
Rules applied:
|
||||||
|
|
||||||
|
@li GROUP_LOGIC - only AND/OR
|
||||||
|
@li SUBGROUP_LOGIC - only AND/OR
|
||||||
|
@li SUBGROUP_ID - only integers
|
||||||
|
@li VARIABLE_NM - must be in the target table
|
||||||
|
@li OPERATOR_NM - only =/>/</<=/>=/BETWEEN/IN/NOT IN/NOT EQUAL/CONTAINS
|
||||||
|
@li RAW_VALUE - no unquoted values except integers, commas and spaces.
|
||||||
|
|
||||||
|
@returns The &outds table containing any bad rows, plus a REASON_CD column.
|
||||||
|
|
||||||
|
@param [in] inds The table to be checked, with the format above
|
||||||
|
@param [in] targetds= The target dataset against which to verify VARIABLE_NM
|
||||||
|
@param [out] abort= (YES) If YES will call mp_abort.sas on any exceptions
|
||||||
|
@param [out] outds= The output table, which is a copy of the &inds. table
|
||||||
|
plus a REASON_CD column, containing only bad records. If bad records found,
|
||||||
|
the SYSCC value will be set to 1008 (general data problem). Downstream
|
||||||
|
processes should check this table (and return code) before continuing.
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mp_abort.sas
|
||||||
|
@li mf_getvarlist.sas
|
||||||
|
@li mf_nobs.sas
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_filtergenerate.sas
|
||||||
|
|
||||||
|
@version 9.3
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
@todo Support date / hex / name literals and exponents in RAW_VALUE field
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_filtercheck(inds,targetds=,outds=work.badrecords,abort=YES);
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&syscc ne 0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(syscc=&syscc - on macro entry)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitise the values based on valid value lists, then strip out
|
||||||
|
* quotes, commas, periods and spaces.
|
||||||
|
* Only numeric values should remain
|
||||||
|
*/
|
||||||
|
|
||||||
|
data &outds;
|
||||||
|
set &inds;
|
||||||
|
length reason_cd $32;
|
||||||
|
|
||||||
|
/* closed list checks */
|
||||||
|
if GROUP_LOGIC not in ('AND','OR') then do;
|
||||||
|
REASON_CD='GROUP_LOGIC should be either AND or OR';
|
||||||
|
putlog REASON_CD= GROUP_LOGIC=;
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
if SUBGROUP_LOGIC not in ('AND','OR') then do;
|
||||||
|
REASON_CD='SUBGROUP_LOGIC should be either AND or OR';
|
||||||
|
putlog REASON_CD= SUBGROUP_LOGIC=;
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
if mod(SUBGROUP_ID,1) ne 0 then do;
|
||||||
|
REASON_CD='SUBGROUP_ID should be integer';
|
||||||
|
putlog REASON_CD= SUBGROUP_ID=;
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
if upcase(VARIABLE_NM) not in
|
||||||
|
(%upcase(%mf_getvarlist(&targetds,dlm=%str(,),quote=SINGLE)))
|
||||||
|
then do;
|
||||||
|
REASON_CD="VARIABLE_NM not in &targetds";
|
||||||
|
putlog REASON_CD= VARIABLE_NM=;
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
if OPERATOR_NM not in
|
||||||
|
('=','>','<','<=','>=','BETWEEN','IN','NOT IN','NE','CONTAINS')
|
||||||
|
then do;
|
||||||
|
REASON_CD='Invalid OPERATOR_NM';
|
||||||
|
putlog REASON_CD= OPERATOR_NM=;
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
|
||||||
|
/* special logic */
|
||||||
|
if OPERATOR_NM='BETWEEN' then raw_value1=tranwrd(raw_value,' AND ','');
|
||||||
|
else if OPERATOR_NM in ('IN','NOT IN') then do;
|
||||||
|
if substr(raw_value,1,1) ne '('
|
||||||
|
or substr(cats(reverse(raw_value)),1,1) ne ')'
|
||||||
|
then do;
|
||||||
|
REASON_CD='Missing brackets in RAW_VALUE';
|
||||||
|
putlog REASON_CD= OPERATOR_NM= raw_value= raw_value1= ;
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
else raw_value1=substr(raw_value,2,max(length(raw_value)-2,0));
|
||||||
|
end;
|
||||||
|
else raw_value1=raw_value;
|
||||||
|
|
||||||
|
/* remove nested literals eg '' */
|
||||||
|
raw_value1=tranwrd(raw_value1,"''",'');
|
||||||
|
|
||||||
|
/* now match string literals (always single quotes) */
|
||||||
|
raw_value2=raw_value1;
|
||||||
|
regex = prxparse("s/(\').*?(\')//");
|
||||||
|
call prxchange(regex,-1,raw_value2);
|
||||||
|
|
||||||
|
/* remove commas and periods*/
|
||||||
|
raw_value3=compress(raw_value2,',.');
|
||||||
|
|
||||||
|
/* output records that contain values other than digits and spaces */
|
||||||
|
if notdigit(compress(raw_value3,' '))>0 then do;
|
||||||
|
putlog raw_value3= $hex32.;
|
||||||
|
REASON_CD='Invalid RAW_VALUE';
|
||||||
|
putlog REASON_CD= raw_value= raw_value1= raw_value2= raw_value3=;
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
|
||||||
|
run;
|
||||||
|
|
||||||
|
%if %mf_nobs(&outds)>0 %then %do;
|
||||||
|
%if &abort=YES %then %do;
|
||||||
|
data _null_;
|
||||||
|
set &outds;
|
||||||
|
call symputx('REASON_CD',reason_cd,'l');
|
||||||
|
stop;
|
||||||
|
run;
|
||||||
|
%mp_abort(
|
||||||
|
mac=&sysmacroname,
|
||||||
|
msg=%str(Filter issues in &inds, first was &reason_cd, details in &outds)
|
||||||
|
)
|
||||||
|
%end;
|
||||||
|
%let syscc=1008;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
%mend;
|
||||||
100
base/mp_filtergenerate.sas
Normal file
100
base/mp_filtergenerate.sas
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Generates a filter clause from an input table, to a fileref
|
||||||
|
@details Uses the input table to generate an output filter clause.
|
||||||
|
This feature is used to create dynamic dropdowns in [Data Controller for SAS®](
|
||||||
|
https://datacontroller.io). The input table should be in the format below:
|
||||||
|
|
||||||
|
|GROUP_LOGIC:$3|SUBGROUP_LOGIC:$3|SUBGROUP_ID:8.|VARIABLE_NM:$32|OPERATOR_NM:$10|RAW_VALUE:$32767|
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
|AND|AND|1|AGE|=|12|
|
||||||
|
|AND|AND|1|SEX|<=|'M'|
|
||||||
|
|AND|OR|2|Name|NOT IN|('Jane','Alfred')|
|
||||||
|
|AND|OR|2|Weight|>=|7|
|
||||||
|
|
||||||
|
Note - if the above table is received from an external client, the values
|
||||||
|
should first be validated using the mp_filtercheck.sas macro to avoid risk
|
||||||
|
of SQL injection.
|
||||||
|
|
||||||
|
To generate the filter, run the following code:
|
||||||
|
|
||||||
|
data work.filtertable;
|
||||||
|
infile datalines4 dsd;
|
||||||
|
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
|
||||||
|
OPERATOR_NM:$10. RAW_VALUE:$32767.;
|
||||||
|
datalines4;
|
||||||
|
AND,AND,1,AGE,=,12
|
||||||
|
AND,AND,1,SEX,<=,"'M'"
|
||||||
|
AND,OR,2,Name,NOT IN,"('Jane','Alfred')"
|
||||||
|
AND,OR,2,Weight,>=,7
|
||||||
|
;;;;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mp_filtergenerate(work.filtertable,outref=myfilter)
|
||||||
|
|
||||||
|
data _null_;
|
||||||
|
infile myfilter;
|
||||||
|
input;
|
||||||
|
put _infile_;
|
||||||
|
run;
|
||||||
|
|
||||||
|
Will write the following query to the log:
|
||||||
|
|
||||||
|
> (
|
||||||
|
> AGE = 12
|
||||||
|
> AND
|
||||||
|
> SEX <= 'M'
|
||||||
|
> ) AND (
|
||||||
|
> Name NOT IN ('Jane','Alfred')
|
||||||
|
> OR
|
||||||
|
> Weight >= 7
|
||||||
|
> )
|
||||||
|
|
||||||
|
@param [in] inds The input table with query values
|
||||||
|
@param [out] outref= The output fileref to contain the filter clause. Will
|
||||||
|
be created (or replaced).
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_filtercheck.sas
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mp_abort.sas
|
||||||
|
@li mf_nobs.sas
|
||||||
|
|
||||||
|
@version 9.3
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_filtergenerate(inds,outref=filter);
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&syscc ne 0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(syscc=&syscc - on macro entry)
|
||||||
|
)
|
||||||
|
|
||||||
|
filename &outref temp;
|
||||||
|
|
||||||
|
%if %mf_nobs(&inds)=0 %then %do;
|
||||||
|
/* ensure we have a default filter */
|
||||||
|
data _null_;
|
||||||
|
file &outref;
|
||||||
|
put '1=1';
|
||||||
|
run;
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
data _null_;
|
||||||
|
file &outref lrecl=32800;
|
||||||
|
set &inds end=last;
|
||||||
|
by SUBGROUP_ID;
|
||||||
|
if _n_=1 then put '(';
|
||||||
|
else if first.SUBGROUP_ID then put +1 GROUP_LOGIC '(';
|
||||||
|
else put +2 SUBGROUP_LOGIC;
|
||||||
|
|
||||||
|
put +4 VARIABLE_NM OPERATOR_NM RAW_VALUE;
|
||||||
|
|
||||||
|
if last.SUBGROUP_ID then put ')'@;
|
||||||
|
run;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%mend;
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://cli.sasjs.io/sasjsconfig-schema.json",
|
"$schema": "https://cli.sasjs.io/sasjsconfig-schema.json",
|
||||||
"macroFolders": ["base", "meta", "metax", "viya", "lua"],
|
"macroFolders": [
|
||||||
|
"base",
|
||||||
|
"meta",
|
||||||
|
"metax",
|
||||||
|
"viya",
|
||||||
|
"lua"
|
||||||
|
],
|
||||||
"docConfig": {
|
"docConfig": {
|
||||||
"displayMacroCore": false,
|
"displayMacroCore": false,
|
||||||
"enableLineage": false,
|
"enableLineage": false,
|
||||||
@@ -21,14 +27,18 @@
|
|||||||
"serverType": "SASVIYA",
|
"serverType": "SASVIYA",
|
||||||
"appLoc": "/Public/temp/macrocore",
|
"appLoc": "/Public/temp/macrocore",
|
||||||
"serviceConfig": {
|
"serviceConfig": {
|
||||||
"serviceFolders": ["tests/viya"],
|
"serviceFolders": [
|
||||||
|
"tests/base",
|
||||||
|
"tests/viya"
|
||||||
|
],
|
||||||
"macroVars": {
|
"macroVars": {
|
||||||
"mcTestAppLoc": "/Public/temp/macrocore"
|
"mcTestAppLoc": "/Public/temp/macrocore"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deployConfig": {
|
"deployConfig": {
|
||||||
"deployServicePack": true
|
"deployServicePack": true
|
||||||
}
|
},
|
||||||
|
"contextName": "SAS Job Execution compute context"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
36
tests/base/mp_assertcolvals.test.sas
Normal file
36
tests/base/mp_assertcolvals.test.sas
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Testing mp_assertcolvals macro
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mp_assertcolvals.sas
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
|
||||||
|
data work.checkds;
|
||||||
|
do checkval='Jane','James','Jill';
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
run;
|
||||||
|
%mp_assertcolvals(sashelp.class.name,
|
||||||
|
checkvals=work.checkds.checkval,
|
||||||
|
desc=At least one value has a match,
|
||||||
|
test=ANYVAL
|
||||||
|
)
|
||||||
|
|
||||||
|
data work.check;
|
||||||
|
do val='M','F';
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
run;
|
||||||
|
%mp_assertcolvals(sashelp.class.sex,
|
||||||
|
checkvals=work.check.val,
|
||||||
|
desc=All values have a match,
|
||||||
|
test=ALLVALS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
%webout(OPEN)
|
||||||
|
%webout(OBJ, TEST_RESULTS)
|
||||||
|
%webout(CLOSE)
|
||||||
132
tests/base/mp_filtercheck.test.sas
Normal file
132
tests/base/mp_filtercheck.test.sas
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Testing mp_filtercheck macro
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mp_filtercheck.sas
|
||||||
|
@li mp_assertdsobs.sas
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
|
||||||
|
/* valid filter */
|
||||||
|
data work.inds;
|
||||||
|
infile datalines4 dsd;
|
||||||
|
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
|
||||||
|
OPERATOR_NM:$10. RAW_VALUE:$32767.;
|
||||||
|
datalines4;
|
||||||
|
AND,AND,1,AGE,=,12
|
||||||
|
AND,AND,1,SEX,<=,"'M'"
|
||||||
|
AND,OR,2,Name,NOT IN,"('Jane','Alfred')"
|
||||||
|
AND,OR,2,Weight,>=,77.7
|
||||||
|
AND,OR,2,Weight,NE,77.7
|
||||||
|
;;;;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mp_filtercheck(work.inds,
|
||||||
|
targetds=sashelp.class,
|
||||||
|
outds=work.badrecords,
|
||||||
|
abort=NO
|
||||||
|
)
|
||||||
|
%let syscc=0;
|
||||||
|
%mp_assertdsobs(work.badrecords,
|
||||||
|
desc=Valid filter query,
|
||||||
|
test=EMPTY,
|
||||||
|
outds=work.test_results
|
||||||
|
)
|
||||||
|
|
||||||
|
/* invalid column */
|
||||||
|
data work.inds;
|
||||||
|
infile datalines4 dsd;
|
||||||
|
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
|
||||||
|
OPERATOR_NM:$10. RAW_VALUE:$32767.;
|
||||||
|
datalines4;
|
||||||
|
AND,AND,1,invalid,=,12
|
||||||
|
AND,AND,1,SEX,<=,"'M'"
|
||||||
|
AND,OR,2,Name,NOT IN,"('Jane','Alfred')"
|
||||||
|
AND,OR,2,Weight,>=,7
|
||||||
|
;;;;
|
||||||
|
run;
|
||||||
|
%mp_filtercheck(work.inds,
|
||||||
|
targetds=sashelp.class,
|
||||||
|
outds=work.badrecords,
|
||||||
|
abort=NO
|
||||||
|
)
|
||||||
|
%let syscc=0;
|
||||||
|
%mp_assertdsobs(work.badrecords,
|
||||||
|
desc=Invalid column name,
|
||||||
|
test=HASOBS,
|
||||||
|
outds=work.test_results
|
||||||
|
)
|
||||||
|
|
||||||
|
/* invalid raw value */
|
||||||
|
data work.inds;
|
||||||
|
infile datalines4 dsd;
|
||||||
|
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
|
||||||
|
OPERATOR_NM:$10. RAW_VALUE:$32767.;
|
||||||
|
datalines4;
|
||||||
|
AND,OR,2,Name,NOT IN,"(''''Jane','Alfred')"
|
||||||
|
;;;;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mp_filtercheck(work.inds,
|
||||||
|
targetds=sashelp.class,
|
||||||
|
outds=work.badrecords,
|
||||||
|
abort=NO
|
||||||
|
)
|
||||||
|
%let syscc=0;
|
||||||
|
%mp_assertdsobs(work.badrecords,
|
||||||
|
desc=Invalid raw value,
|
||||||
|
test=HASOBS,
|
||||||
|
outds=work.test_results
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Code injection - column name */
|
||||||
|
data work.inds;
|
||||||
|
infile datalines4 dsd;
|
||||||
|
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
|
||||||
|
OPERATOR_NM:$10. RAW_VALUE:$32767.;
|
||||||
|
datalines4;
|
||||||
|
AND,AND,1,%abort,=,12
|
||||||
|
AND,OR,2,Weight,>=,7
|
||||||
|
;;;;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mp_filtercheck(work.inds,
|
||||||
|
targetds=sashelp.class,
|
||||||
|
outds=work.badrecords,
|
||||||
|
abort=NO
|
||||||
|
)
|
||||||
|
%let syscc=0;
|
||||||
|
%mp_assertdsobs(work.badrecords,
|
||||||
|
desc=Code injection - column name,
|
||||||
|
test=HASOBS,
|
||||||
|
outds=work.test_results
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Code injection - raw values*/
|
||||||
|
data work.inds;
|
||||||
|
infile datalines4 dsd;
|
||||||
|
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
|
||||||
|
OPERATOR_NM:$10. RAW_VALUE:$32767.;
|
||||||
|
datalines4;
|
||||||
|
AND,AND,1,age,=,;;%abort
|
||||||
|
;;;;
|
||||||
|
run;
|
||||||
|
%mp_filtercheck(work.inds,
|
||||||
|
targetds=sashelp.class,
|
||||||
|
outds=work.badrecords,
|
||||||
|
abort=NO
|
||||||
|
)
|
||||||
|
%let syscc=0;
|
||||||
|
%mp_assertdsobs(work.badrecords,
|
||||||
|
desc=Code injection - raw value abort,
|
||||||
|
test=HASOBS,
|
||||||
|
outds=work.test_results
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
%webout(OPEN)
|
||||||
|
%webout(OBJ, TEST_RESULTS)
|
||||||
|
%webout(CLOSE)
|
||||||
126
tests/base/mp_filtergenerate.test.sas
Normal file
126
tests/base/mp_filtergenerate.test.sas
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Testing mp_filtergenerate macro
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mp_filtergenerate.sas
|
||||||
|
@li mp_filtercheck.sas
|
||||||
|
@li mp_assertdsobs.sas
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
options source2;
|
||||||
|
|
||||||
|
/* valid filter */
|
||||||
|
data work.inds;
|
||||||
|
infile datalines4 dsd;
|
||||||
|
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
|
||||||
|
OPERATOR_NM:$10. RAW_VALUE:$32767.;
|
||||||
|
datalines4;
|
||||||
|
AND,AND,1,AGE,>,5
|
||||||
|
AND,AND,1,SEX,NE,"'M'"
|
||||||
|
AND,OR,2,Name,NOT IN,"('Jane','Janet')"
|
||||||
|
AND,OR,2,Weight,>=,84.6
|
||||||
|
;;;;
|
||||||
|
run;
|
||||||
|
%mp_filtercheck(work.inds,targetds=sashelp.class)
|
||||||
|
%mp_filtergenerate(work.inds,outref=myfilter)
|
||||||
|
data work.test;
|
||||||
|
set sashelp.class;
|
||||||
|
where %inc myfilter;;
|
||||||
|
run;
|
||||||
|
%mp_assertdsobs(work.test,
|
||||||
|
desc=Valid filter,
|
||||||
|
test=EQUALS 8,
|
||||||
|
outds=work.test_results
|
||||||
|
)
|
||||||
|
|
||||||
|
/* empty filter (return all records) */
|
||||||
|
data work.inds;
|
||||||
|
infile datalines4 dsd;
|
||||||
|
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
|
||||||
|
OPERATOR_NM:$10. RAW_VALUE:$32767.;
|
||||||
|
datalines4;
|
||||||
|
;;;;
|
||||||
|
run;
|
||||||
|
%mp_filtercheck(work.inds,targetds=sashelp.class)
|
||||||
|
%mp_filtergenerate(work.inds,outref=myfilter)
|
||||||
|
data work.test;
|
||||||
|
set sashelp.class;
|
||||||
|
where %inc myfilter;;
|
||||||
|
run;
|
||||||
|
%mp_assertdsobs(work.test,
|
||||||
|
desc=Empty filter (return all records) ,
|
||||||
|
test=EQUALS 19,
|
||||||
|
outds=work.test_results
|
||||||
|
)
|
||||||
|
|
||||||
|
/* single line filter */
|
||||||
|
data work.inds;
|
||||||
|
infile datalines4 dsd;
|
||||||
|
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
|
||||||
|
OPERATOR_NM:$10. RAW_VALUE:$32767.;
|
||||||
|
datalines4;
|
||||||
|
AND,OR,2,Name,IN,"('Jane','Janet')"
|
||||||
|
;;;;
|
||||||
|
run;
|
||||||
|
%mp_filtercheck(work.inds,targetds=sashelp.class)
|
||||||
|
%mp_filtergenerate(work.inds,outref=myfilter)
|
||||||
|
data work.test;
|
||||||
|
set sashelp.class;
|
||||||
|
where %inc myfilter;;
|
||||||
|
run;
|
||||||
|
%mp_assertdsobs(work.test,
|
||||||
|
desc=Single line filter ,
|
||||||
|
test=EQUALS 2,
|
||||||
|
outds=work.test_results
|
||||||
|
)
|
||||||
|
|
||||||
|
/* single line 2 group filter */
|
||||||
|
data work.inds;
|
||||||
|
infile datalines4 dsd;
|
||||||
|
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
|
||||||
|
OPERATOR_NM:$10. RAW_VALUE:$32767.;
|
||||||
|
datalines4;
|
||||||
|
OR,OR,2,Name,IN,"('Jane','Janet')"
|
||||||
|
OR,OR,3,Name,IN,"('James')"
|
||||||
|
;;;;
|
||||||
|
run;
|
||||||
|
%mp_filtercheck(work.inds,targetds=sashelp.class)
|
||||||
|
%mp_filtergenerate(work.inds,outref=myfilter)
|
||||||
|
data work.test;
|
||||||
|
set sashelp.class;
|
||||||
|
where %inc myfilter;;
|
||||||
|
run;
|
||||||
|
%mp_assertdsobs(work.test,
|
||||||
|
desc=Single line 2 group filter ,
|
||||||
|
test=EQUALS 3,
|
||||||
|
outds=work.test_results
|
||||||
|
)
|
||||||
|
|
||||||
|
/* filter with nothing returned */
|
||||||
|
data work.inds;
|
||||||
|
infile datalines4 dsd;
|
||||||
|
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
|
||||||
|
OPERATOR_NM:$10. RAW_VALUE:$32767.;
|
||||||
|
datalines4;
|
||||||
|
AND,OR,2,Name,IN,"('Jane','Janet')"
|
||||||
|
AND,OR,3,Name,IN,"('James')"
|
||||||
|
;;;;
|
||||||
|
run;
|
||||||
|
%mp_filtercheck(work.inds,targetds=sashelp.class)
|
||||||
|
%mp_filtergenerate(work.inds,outref=myfilter)
|
||||||
|
data work.test;
|
||||||
|
set sashelp.class;
|
||||||
|
where %inc myfilter;;
|
||||||
|
run;
|
||||||
|
%mp_assertdsobs(work.test,
|
||||||
|
desc=Filter with nothing returned,
|
||||||
|
test=EQUALS 0,
|
||||||
|
outds=work.test_results
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
%webout(OPEN)
|
||||||
|
%webout(OBJ, TEST_RESULTS)
|
||||||
|
%webout(CLOSE)
|
||||||
Reference in New Issue
Block a user