/** @file @brief Auto-generated file @details This file contains all the macros in a single file - which means it can be 'included' in SAS with just 2 lines of code: filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; %inc mc; The `build.py` file in the https://github.com/sasjs/core repo is used to create this file. @author Allan Bowe **/ options noquotelenmax; /** @file @brief Abort, ungracefully @details Will abort with a straightforward %abort if the condition is true.
&sysuserid or SYS_COMPUTE_SESSION_OWNER if it exists.
In a Stored Process session, &sysuserid
resolves to a system account (default=sassrv) and instead there are several
metadata username variables to choose from (_metauser, _metaperson
,_username, _secureusername). The OS account is represented by
_secureusername whilst the metadata account is under
_metaperson.
%let user= %mf_getUser();
%put &user;
@param type - do not use, may be deprecated in a future release
@return SYSUSERID (if workspace server)
@return _METAPERSON (if stored process server)
@return SYS_COMPUTE_SESSION_OWNER (if Viya compute session)
@version 9.2
@author Allan Bowe
**/
%macro mf_getuser(type=META
)/*/STORE SOURCE*/;
%local user metavar;
%if &type=OS %then %let metavar=_secureusername;
%else %let metavar=_metaperson;
%if %symexist(SYS_COMPUTE_SESSION_OWNER) %then %let user=&SYS_COMPUTE_SESSION_OWNER;
%else %if %symexist(&metavar) %then %do;
%if %length(&&&metavar)=0 %then %let user=&sysuserid;
/* sometimes SAS will add @domain extension - remove for consistency */
%else %let user=%scan(&&&metavar,1,@);
%end;
%else %let user=&sysuserid;
%quote(&user)
%mend mf_getuser;
/**
@file
@brief Retrieves a value from a dataset. If no filter supplied, then first
record is used.
@details Be sure to %quote() your where clause. Example usage:
%put %mf_getvalue(sashelp.class,name,filter=%quote(age=15));
%put %mf_getvalue(sashelp.class,name);
%mf_getattrn().
%put Number of observations=%mf_nobs(sashelp.class);
getattrs= is used.
maxobs
observations, enter an integer here.
@param [in] random_sample= (NO) Set to YES to generate a random sample of
data. Can be quite slow.
@param [in] showlog= (YES) Whether to show generated cards file in the SAS
log. Valid values:
@li YES
@li NO
@param [in] outencoding= Provide encoding value for file statement (eg utf-8)
@param [in] append= (NO) If NO then will rebuild the cards file if it already
exists, otherwise will append to it. Used by the mp_lib2cards.sas macro.
%mp_ds2cards() macro.
Usage:
%mp_lib2cards(lib=sashelp
, outloc= C:\temp )
The output will be one cards file in the `outloc` directory per dataset in the
input `lib` library. If the `outloc` directory does not exist, it is created.
To create a single SAS file with the first 1000 records of each table in a
library you could use this syntax:
%mp_lib2cards(lib=sashelp
, outloc= /tmp
, outfile= myfile.sas
, maxobs= 1000
)
maxobs observations
@version 9.2
@author Allan Bowe
**/
%macro mp_lib2cards(lib=
,outloc=%sysfunc(pathname(work)) /* without trailing slash */
,maxobs=max
,random_sample=NO
,outfile=0
)/*/STORE SOURCE*/;
/* Find the tables */
%local x ds memlist;
proc sql noprint;
select distinct lowcase(memname)
into: memlist
separated by ' '
from dictionary.tables
where upcase(libname)="%upcase(&lib)";
/* trim trailing slash, if provided */
%let outloc=%mf_trimstr(&outloc,/);
%let outloc=%mf_trimstr(&outloc,\);
/* create the output directory */
%mf_mkdir(&outloc)
/* create the cards files */
%do x=1 %to %sysfunc(countw(&memlist));
%let ds=%scan(&memlist,&x);
%mp_ds2cards(base_ds=&lib..&ds
,maxobs=&maxobs
,random_sample=&random_sample
%if "&outfile" ne "0" %then %do;
,append=YES
,cards_file="&outloc/&outfile"
%end;
%else %do;
,append=NO
,cards_file="&outloc/&ds..sas"
%end;
)
%end;
%mend mp_lib2cards;/**
@file
@brief Convert all data in a library to SQL insert statements
@details Gets list of members then calls the %mp_ds2inserts()
macro.
Usage:
%mp_getddl(sashelp, schema=work, fref=tempref)
%mp_lib2inserts(sashelp, schema=work, outref=tempref)
%inc tempref;
The output will be one file in the outref fileref.
work.mp_perflog;
@version 9.2
@author Allan Bowe
@source https://github.com/sasjs/core
**/
%macro mp_perflog(label,libds=work.mp_perflog
)/*/STORE SOURCE*/;
%if not (%mf_existds(&libds)) %then %do;
data &libds;
length sysjobid $10 label $256 dttm 8.;
format dttm datetime19.3;
call missing(of _all_);
stop;
run;
%end;
proc sql;
insert into &libds
set sysjobid="&sysjobid"
,label=symget('label')
,dttm=%sysfunc(datetime());
quit;
%mend mp_perflog;/**
@file
@brief Enables previous observations to be re-instated
@details Remembers the last X observations by storing them in a hash table.
Is a convenience over the use of lag() or retain, when an entire observation
needs to be restored.
This macro will also restore automatic variables (such as _n_ and _error_).
Example Usage:
data example;
set sashelp.class;
calc_var=_n_*3;
%* initialise hash and save from PDV ;
%mp_prevobs(INIT,history=2)
if _n_ =10 then do;
%* fetch previous but 1 record;
%mp_prevobs(FETCH,-2)
put _n_= name= age= calc_var=;
%* fetch previous record;
%mp_prevobs(FETCH,-1)
put _n_= name= age= calc_var=;
%* reinstate current record ;
%mp_prevobs(FETCH,0)
put _n_= name= age= calc_var=;
end;
run;
Result:
Credit is made to `data _null_` for authoring this very helpful paper:
https://www.lexjansen.com/pharmasug/2008/cc/CC08.pdf
@param action Either FETCH a current or previous record, or INITialise.
@param record The relative (to current) position of the previous observation
to return.
@param history= The number of records to retain in the hash table. Default=5
@param prefix= the prefix to give to the variables used to store the hash name
and index. Default=mp_prevobs
@version 9.2
@author Allan Bowe
**/
%macro mp_prevobs(action,record,history=5,prefix=mp_prevobs
)/*/STORE SOURCE*/;
%let action=%upcase(&action);
%let prefix=%upcase(&prefix);
%let record=%eval((&record+0) * -1);
%if &action=INIT %then %do;
if _n_ eq 1 then do;
attrib &prefix._VAR length=$64;
dcl hash &prefix._HASH(ordered:'Y');
&prefix._KEY=0;
&prefix._HASH.defineKey("&prefix._KEY");
do while(1);
call vnext(&prefix._VAR);
if &prefix._VAR='' then leave;
if &prefix._VAR eq "&prefix._VAR" then continue;
else if &prefix._VAR eq "&prefix._KEY" then continue;
&prefix._HASH.defineData(&prefix._VAR);
end;
&prefix._HASH.defineDone();
end;
/* this part has to happen before FETCHing */
&prefix._KEY+1;
&prefix._rc=&prefix._HASH.add();
if &prefix._rc then putlog 'adding' &prefix._rc=;
%if &history>0 %then %do;
if &prefix._key>&history+1 then
&prefix._HASH.remove(key: &prefix._KEY - &history - 1);
if &prefix._rc then putlog 'removing' &prefix._rc=;
%end;
%end;
%else %if &action=FETCH %then %do;
if &record>&prefix._key then putlog "Not enough records in &Prefix._hash yet";
else &prefix._rc=&prefix._HASH.find(key: &prefix._KEY - &record);
if &prefix._rc then putlog &prefix._rc= " when fetching " &prefix._KEY=
"with record &record and " _n_=;
%end;
%mend mp_prevobs;/**
@file
@brief Returns all children from a hierarchy table for a specified parent
@details Where data stores hierarchies in a simple parent / child mapping,
it is not always straightforward to extract all the children for a
particular parent. This problem is known as a recursive self join. This
macro will extract all the descendents for a parent.
Usage:
data have;
p=1;c=2;output;
p=2;c=3;output;
p=2;c=4;output;
p=3;c=5;output;
p=6;c=7;output;
p=8;c=9;output;
run;
%mp_recursivejoin(base_ds=have
,outds=want
,matchval=1
,parentvar=p
,childvar=c
)
@param base_ds= base table containing hierarchy (not modified)
@param outds= the output dataset to create with the generated hierarchy
@param matchval= the ultimate parent from which to filter
@param parentvar= name of the parent variable
@param childvar= name of the child variable (should be same type as parent)
@param mdebug= set to 1 to prevent temp tables being dropped
@returns outds contains the following variables:
- level (0 = top level)
- &parentvar
- &childvar (null if none found)
@version 9.2
@author Allan Bowe
**/
%macro mp_recursivejoin(base_ds=
,outds=
,matchval=
,parentvar=
,childvar=
,iter= /* reserved for internal / recursive use by the macro itself */
,maxiter=500 /* avoid infinite loop */
,mDebug=0);
%if &iter= %then %do;
proc sql;
create table &outds as
select 0 as level,&parentvar, &childvar
from &base_ds
where &parentvar=&matchval;
%if &sqlobs.=0 %then %do;
%put NOTE: &sysmacroname: No match for &parentvar=&matchval;
%return;
%end;
%let iter=1;
%end;
%else %if &iter>&maxiter %then %return;
proc sql;
create table _data_ as
select &iter as level
,curr.&childvar as &parentvar
,base_ds.&childvar as &childvar
from &outds curr
left join &base_ds base_ds
on curr.&childvar=base_ds.&parentvar
where curr.level=%eval(&iter.-1)
& curr.&childvar is not null;
%local append_ds; %let append_ds=&syslast;
%local obs; %let obs=&sqlobs;
insert into &outds select distinct * from &append_ds;
%if &mdebug=0 %then drop table &append_ds;;
%if &obs %then %do;
%mp_recursivejoin(iter=%eval(&iter.+1)
,outds=&outds,parentvar=&parentvar
,childvar=&childvar
,base_ds=&base_ds
)
%end;
%mend mp_recursivejoin;
/**
@file
@brief Reset when an err condition occurs
@details When building apps, sometimes an operation must be attempted that
can cause an err condition. There is no try catch in SAS! So the err state
must be caught and reset.
This macro attempts to do that reset.
@version 9.2
@author Allan Bowe
**/
%macro mp_reseterror(
)/*/STORE SOURCE*/;
options obs=max replace nosyntaxcheck;
%let syscc=0;
%if "&sysprocessmode " = "SAS Stored Process Server " %then %do;
data _null_;
rc=stpsrvset('program error', 0);
run;
%end;
%mend mp_reseterror;/**
@file
@brief Reset an option to original value
@details Inspired by the SAS Jedi -
https://blogs.sas.com/content/sastraining/2012/08/14/jedi-sas-tricks-reset-sas-system-options
Called as follows:
options obs=30 ps=max;
%mp_resetoption(OBS)
%mp_resetoption(PS)
@param [in] option the option to reset
@version 9.2
@author Allan Bowe
**/
%macro mp_resetoption(option /* the option to reset */
)/*/STORE SOURCE*/;
data _null_;
length code $1500;
startup=getoption("&option",'startupvalue');
current=getoption("&option");
if startup ne current then do;
code =cat('OPTIONS ',getoption("&option",'keyword','startupvalue'),';');
putlog "NOTE: Resetting system option: " code ;
call execute(code );
end;
run;
%mend mp_resetoption;/**
@file
@brief Generate and apply retained key values to a staging table
@details This macro will populate a staging table with a Retained Key based on
a business key and a base (target) table.
Definition of retained key ([source](
http://bukhantsov.org/2012/04/what-is-data-vault/)):
> The retained key is a key which is mapped to business key one-to-one. In
> comparison, the surrogate key includes time and there can be many surrogate
> keys corresponding to one business key. This explains the name of the key,
> it is retained with insertion of a new version of a row while surrogate key
> is increasing.
This macro is designed to be used as part of a wider load / ETL process (such
as the one in [Data Controller for SAS](https://datacontroller.io)).
Specifically, the macro assumes that the base table has already been 'locked'
(eg with the mp_lockanytable.sas macro) prior to invocation. Also, several
tables are assumed to exist (names are configurable):
@li work.staging_table - the staged data, minus the retained key element
@li permlib.base_table - the target table to be loaded (**not** loaded by this
macro)
@li permlib.maxkeytable - optional, used to store load metaadata.
The definition is available by running mp_coretable.sas as follows:
`mp_coretable(MAXKEYTABLE)`.
@li permlib.locktable - Necessary if maxkeytable is being populated. The
definition is available by running mp_coretable.sas as follows:
`mp_coretable(LOCKTABLE)`.
@param [in] base_lib= (WORK) Libref of the base (target) table.
@param [in] base_dsn= (BASETABLE) Name of the base (target) table.
@param [in] append_lib= (WORK) Libref of the staging table
@param [in] append_dsn= (APPENDTABLE) Name of the staging table
@param [in] retained_key= (DEFAULT_RK) Name of RK to generate (should exist on
base table)
@param [in] business_key= (PK1 PK2) Business key against which to generate
RK values. Should be unique and not null on the staging table.
@param [in] check_uniqueness=(NO) Set to yes to perform a uniqueness check.
Recommended if there is a chance that the staging data is not unique on the
business key.
@param [in] maxkeytable= (0) Provide a maxkeytable libds reference here, to
store load metadata (maxkey val, load time). Set to zero if metadata is not
required, eg, when preparing a 'dummy' load. Structure is described above.
See below for sample data.
|KEYTABLE:$32.|KEYCOLUMN:$32.|MAX_KEY:best.|PROCESSED_DTTM:E8601DT26.6|
|---|---|---|---|
|`DC487173.MPE_SELECTBOX `|`SELECTBOX_RK `|`55 `|`1950427787.8 `|
|`DC487173.MPE_FILTERANYTABLE `|`filter_rk `|`14 `|`1951053886.8 `|
@param [in] locktable= (0) If updating the maxkeytable, provide the libds
reference to the lock table (per mp_lockanytable.sas macro)
@param [in] filter_str= Apply a filter - useful for SCD2 or BITEMPORAL loads.
Example: `filter_str=%str( (where=( &now < &tech_to)) )`
@param [out] outds= (WORK.APPEND) Output table (staging table + retained key)
| Transaction Type | Key Behaviour | Column Behaviour |
|---|---|---|
| Deletes | The row is added to `&outDEL.` UNLESS it no longer exists in the base table, in which case it is added to `&errDS.` instead. | Deletes are unaffected by the addition or removal of non Primary-Key columns. |
| Inserts |
Previously newly added rows are added to the `outADD` table UNLESS they
are present in the Base table. In this case they are added to the `&errDS.` table instead. |
Inserts are unaffected by the addition of columns in the Base Table (they are padded with blanks). Deleted columns are only a problem if they appear on the previous insert - in which case the record is added to `&errDS.`. |
| Updates |
Previously modified rows are merged with base table values such that
only the individual cells that were _previously_ changed are re-applied.
Where the row contains cells that were not marked as having changed in
the prior transaction, the 'blanks' are filled with base table values in
the `outMOD` table. If the row no longer exists on the base table, then the row is added to the `errDS` table instead. |
Updates are unaffected by the addition of columns in the Base Table - the new cells are simply populated with Base Table values. Deleted columns are only a problem if they relate to a modified cell (`is_diff=1`) - in which case the record is added to `&errDS.`. |
tree= parameter.
@param outds= the dataset to create that contains the list of stps.
@param mDebug= set to 1 to show debug messages in the log
@param showDesc= provide a non blank value to return stored process
descriptions
@param showUsageVersion= provide a non blank value to return the UsageVersion.
This is either 1000000 (type 1, 9.2) or 2000000 (type2, 9.3 onwards).
@returns outds dataset containing the following columns
- stpuri
- stpname
- treeuri
- stpdesc (if requested)
- usageversion (if requested)
@version 9.2
@author Allan Bowe
**/
%macro mm_getstps(
tree=
,name=
,outds=work.mm_getstps
,mDebug=0
,showDesc=
,showUsageVersion=
)/*/STORE SOURCE*/;
%local mD;
%if &mDebug=1 %then %let mD=;
%else %let mD=%str(*);
%&mD.put Executing mm_getstps.sas;
%&mD.put _local_;
data &outds;
length stpuri stpname usageversion treeuri stpdesc $256;
call missing (of _all_);
run;
%if %length(&tree)>0 %then %do;
/* get tree info */
%mm_gettree(tree=&tree,inds=&outds, outds=&outds, mDebug=&mDebug)
%if %mf_nobs(&outds)=0 %then %do;
%put NOTE: Tree &tree did not exist!!;
%return;
%end;
%end;
data &outds ;
set &outds(rename=(treeuri=treeuri_compare));
length treeuri query stpuri $256;
i+1;
%if %length(&name)>0 %then %do;
query="omsobj:ClassifierMap?@PublicType='StoredProcess' and @Name='&name'";
putlog query=;
%end;
%else %do;
query="omsobj:ClassifierMap?@PublicType='StoredProcess'";
%end;
%if &mDebug=1 %then %do;
putlog 'start' (_all_)(=);
%end;
do while(0&outds) containing both the path
and uri.
Usage:
%mm_getTree(tree=/User Folders/sasdemo)
@param tree= the BIP Tree folder path or uri
@param outds= the dataset to create that contains the tree path & uri
@param inds= an optional input dataset to augment with treepath & treeuri
@param mDebug= set to 1 to show debug messages in the log
@returns outds dataset containing the following columns:
- treeuri
- treepath
@version 9.2
@author Allan Bowe
**/
%macro mm_getTree(
tree=
,inds=
,outds=work.mm_getTree
,mDebug=0
)/*/STORE SOURCE*/;
%local mD;
%if &mDebug=1 %then %let mD=;
%else %let mD=%str(*);
%&mD.put Executing mm_getTree.sas;
%&mD.put _local_;
data &outds;
length treeuri __parenturi __type __name $256 treepath $512;
%if %length(&inds)>0 %then %do;
set &inds;
%end;
__rc1=metadata_resolve("&tree",__type,treeuri);
if __type='Tree' then do;
__rc2=metadata_getattr(treeuri,"Name",__name);
treepath=cats('/',__name);
/* get parents */
do while (metadata_getnasn(treeuri,"ParentTree",1,__parenturi)>0);
__rc3=metadata_getattr(__parenturi,"Name",__name);
treepath=cats('/',__name,treepath);
treeuri=__parenturi;
end;
treeuri="&tree";
end;
else do;
__rc2=metadata_pathobj(' ',"&tree",'Folder',__type,treeuri);
treepath="&tree";
end;
&mD.put (_all_)(=);
drop __:;
if treeuri ne "" and treepath ne "" then output;
stop;
run;
%mend mm_getTree;/**
@file
@brief Creates a dataset with all metadata types
@details Usage:
%mm_gettypes(outds=types)
@param outds the dataset to create that contains the list of types
@returns outds dataset containing all types
@warning The following filenames are created and then de-assigned:
filename sxlemap clear;
filename response clear;
libname _XML_ clear;
@version 9.2
@author Allan Bowe
**/
%macro mm_gettypes(
outds=work.mm_gettypes
)/*/STORE SOURCE*/;
* use a temporary fileref to hold the response;
filename response temp;
/* get list of libraries */
proc metadata in=
'