mirror of
https://github.com/sasjs/core.git
synced 2025-12-25 20:20:05 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b02a9e3478 | ||
|
|
3d3c76c836 | ||
|
|
e039f1cd83 | ||
|
|
6c8165601d | ||
|
|
596624c1bf | ||
|
|
4aca34d4c2 | ||
|
|
858b378658 | ||
|
|
8af41a8cc3 | ||
|
|
b13c33cbde | ||
|
|
6906f025d6 | ||
|
|
8b355b8056 | ||
|
|
8938553588 | ||
|
|
14852f3647 | ||
|
|
b55e91784d | ||
|
|
fc14aaa37f |
@@ -1,2 +1,11 @@
|
||||
#!/bin/sh
|
||||
sasjs lint
|
||||
sasjs lint
|
||||
|
||||
# Avoid commits to the master branch
|
||||
BRANCH=`git rev-parse --abbrev-ref HEAD`
|
||||
|
||||
if [[ "$BRANCH" =~ ^(master|main|develop)$ ]]; then
|
||||
echo "You are on branch $BRANCH. Are you sure you want to commit to this branch?"
|
||||
echo "If so, commit with -n to bypass the pre-commit hook."
|
||||
exit 1
|
||||
fi
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright 2020 (Allan Bowe)
|
||||
Copyright 2021 (Allan Bowe)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
16
README.md
16
README.md
@@ -1,7 +1,7 @@
|
||||
# Macro Core
|
||||
[![npm package][npm-image]][npm-url]
|
||||
[![Github Workflow][githubworkflow-image]][githubworkflow-url]
|
||||
[]()
|
||||

|
||||

|
||||
[](/LICENSE)
|
||||

|
||||
@@ -31,21 +31,21 @@ Documentation: https://core.sasjs.io
|
||||
|
||||
## Components
|
||||
|
||||
### **base** library (SAS9/Viya)
|
||||
### BASE library (SAS9/Viya)
|
||||
|
||||
- OS independent
|
||||
- Not metadata aware
|
||||
- No X command
|
||||
- Prefixes: _mf_, _mp_
|
||||
|
||||
#### **fcmp** library (SAS9/Viya)
|
||||
#### FCMP library (SAS9/Viya)
|
||||
- Function and macro names are identical, except for special cases
|
||||
- Prefixes: _mcf_
|
||||
|
||||
The fcmp macros are used to generate fcmp functions, and can be used with or
|
||||
without the `proc fcmp` wrapper.
|
||||
|
||||
### **meta** library (SAS9 only)
|
||||
### META library (SAS9 only)
|
||||
|
||||
Macros used in SAS EBI, which connect to the metadata server.
|
||||
|
||||
@@ -54,7 +54,7 @@ Macros used in SAS EBI, which connect to the metadata server.
|
||||
- No X command
|
||||
- Prefixes: _mm_
|
||||
|
||||
### **server** library (@sasjs/server only)
|
||||
### SERVER library (@sasjs/server only)
|
||||
These macros are used for building applications using [@sasjs/server](https://server.sasjs.io) - an open source REST API for Desktop SAS.
|
||||
|
||||
- OS independent
|
||||
@@ -62,7 +62,7 @@ These macros are used for building applications using [@sasjs/server](https://se
|
||||
- No X command
|
||||
- Prefixes: _ms_
|
||||
|
||||
### **viya** library (Viya only)
|
||||
### VIYA library (Viya only)
|
||||
|
||||
Macros used for interfacing with SAS Viya.
|
||||
|
||||
@@ -70,14 +70,14 @@ Macros used for interfacing with SAS Viya.
|
||||
- No X command
|
||||
- Prefixes: _mv_, _mvf_
|
||||
|
||||
### **metax** library (SAS9 only)
|
||||
### METAX library (SAS9 only)
|
||||
|
||||
- OS specific
|
||||
- Metadata aware
|
||||
- X command enabled
|
||||
- Prefixes: _mmw_,_mmu_,_mmx_
|
||||
|
||||
### **lua** library
|
||||
### LUA library
|
||||
|
||||
Wait - this is a macro library - what is LUA doing here? Well, it is a little known fact that you CAN run LUA within a SAS Macro. It has to be written to a text file with a `.lua` extension, from where you can `%include` it. So, without using the `proc lua` wrapper.
|
||||
|
||||
|
||||
232
all.sas
232
all.sas
@@ -626,12 +626,12 @@ https://github.com/yabwon/SAS_PACKAGES/blob/main/packages/baseplus.md#functionex
|
||||
for:
|
||||
> "these","words","are","double","quoted"
|
||||
|
||||
@param in_str the unquoted, spaced delimited string to transform
|
||||
@param dlm= the delimeter to be applied to the output (default comma)
|
||||
@param indlm= the delimeter used for the input (default is space)
|
||||
@param quote= the quote mark to apply (S=Single, D=Double). If any other value
|
||||
than uppercase S or D is supplied, then that value will be used as the
|
||||
quoting character.
|
||||
@param [in] in_str The unquoted, spaced delimited string to transform
|
||||
@param [in] dlm= The delimeter to be applied to the output (default comma)
|
||||
@param [in] indlm= (,) The delimeter used for the input (default is space)
|
||||
@param [in] quote= (S) The quote mark to apply (S=Single, D=Double, N=None).
|
||||
If any other value than uppercase S or D is supplied, then that value will
|
||||
be used as the quoting character.
|
||||
@return output returns a string with the newly quoted / delimited output.
|
||||
|
||||
@version 9.2
|
||||
@@ -641,9 +641,10 @@ https://github.com/yabwon/SAS_PACKAGES/blob/main/packages/baseplus.md#functionex
|
||||
|
||||
%macro mf_getquotedstr(IN_STR,DLM=%str(,),QUOTE=S,indlm=%str( )
|
||||
)/*/STORE SOURCE*/;
|
||||
%if "e=S %then %let quote=%str(%');
|
||||
%else %if "e=D %then %let quote=%str(%");
|
||||
%else %let quote=%str();
|
||||
/* credit Rowland Hale - byte34 is double quote, 39 is single quote */
|
||||
%if "e=S %then %let quote=%qsysfunc(byte(39));
|
||||
%else %if "e=D %then %let quote=%qsysfunc(byte(34));
|
||||
%else %if "e=N %then %let quote=;
|
||||
%local i item buffer;
|
||||
%let i=1;
|
||||
%do %while (%qscan(&IN_STR,&i,%str(&indlm)) ne %str() ) ;
|
||||
@@ -4612,7 +4613,7 @@ data &outds(keep=name type length varnum format label ddtype);
|
||||
else if formatd=0 then format=cats(format2,formatl,'.');
|
||||
else format=cats(format2,formatl,'.',formatd);
|
||||
type='N';
|
||||
if format=:'DATETIME' then ddtype='DATETIME';
|
||||
if format=:'DATETIME' or format=:'E8601DT' then ddtype='DATETIME';
|
||||
else if format=:'DATE' or format=:'DDMMYY' or format=:'MMDDYY'
|
||||
or format=:'YYMMDD' or format=:'E8601DA' or format=:'B8601DA'
|
||||
or format=:'MONYY'
|
||||
@@ -5548,6 +5549,10 @@ create table &outds (rename=(
|
||||
|
||||
Returns:
|
||||
|
||||
|libref:$8.|dsn:$32.|memtype:$8.|dbms_memtype:$32.|typemem:$8.|memlabel:$256.|nvar:best.|compress:$8.|pk_fields:$512.|
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
|WORK|EXAMPLE|DATA| |DATA| |4|NO|TX_FROM DD_TYPE DD_SOURCE|
|
||||
|
||||
|
||||
@param [in] lib The libref to examine
|
||||
@param [in] ds= (0) Select the dataset to examine, else use 0 for all tables
|
||||
@@ -5562,6 +5567,7 @@ create table &outds (rename=(
|
||||
|
||||
<h4> Related Macros </h4>
|
||||
@li mp_getpk.test.sas
|
||||
@li mp_guesspk.sas
|
||||
|
||||
@version 9.3
|
||||
@author Macro People Ltd
|
||||
@@ -5830,34 +5836,39 @@ create table &outds as
|
||||
|
||||
%mend mp_gsubfile;
|
||||
/**
|
||||
@file mp_guesspk.sas
|
||||
@file
|
||||
@brief Guess the primary key of a table
|
||||
@details Tries to guess the primary key of a table based on the following logic:
|
||||
@details Tries to guess the primary key of a table based on the following
|
||||
logic:
|
||||
|
||||
* Columns with nulls are ignored
|
||||
* Return only column combinations that provide unique results
|
||||
* Start from one column, then move out to include composite keys of 2 to 6 columns
|
||||
* Start from one column, then move out to composite keys of 2 to 6 columns
|
||||
|
||||
The library of the target should be assigned before using this macro.
|
||||
|
||||
Usage:
|
||||
|
||||
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||
filename mc url
|
||||
"https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||
%inc mc;
|
||||
%mp_guesspk(sashelp.class,outds=classpks)
|
||||
|
||||
@param baseds The dataset to analyse
|
||||
@param outds= The output dataset to contain the possible PKs
|
||||
@param max_guesses= The total number of possible primary keys to generate. A
|
||||
table is likely to have multiple unlikely PKs, so no need to list them all. Default=3.
|
||||
@param min_rows= The minimum number of rows a table should have in order to try
|
||||
and guess the PK. Default=5.
|
||||
@param max_guesses= (3) The total number of possible primary keys to generate.
|
||||
A table may have multiple unlikely PKs, so no need to list them all.
|
||||
@param min_rows= (5) The minimum number of rows a table should have in order
|
||||
to try and guess the PK.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mf_getvarlist.sas
|
||||
@li mf_getuniquename.sas
|
||||
@li mf_nobs.sas
|
||||
|
||||
<h4> Related Macros </h4>
|
||||
@li mp_getpk.sas
|
||||
|
||||
@version 9.3
|
||||
@author Allan Bowe
|
||||
|
||||
@@ -6027,7 +6038,8 @@ create table &outds as
|
||||
%let lev4=%scan(&posspks,&l);
|
||||
%if &lev1 ne &lev4 and &lev2 ne &lev4 and &lev3 ne &lev4 %then %do;
|
||||
/* check for four level uniqueness */
|
||||
proc sort data=&pkds(keep=&lev1 &lev2 &lev3 &lev4) out=&tmpds noduprec;
|
||||
proc sort data=&pkds(keep=&lev1 &lev2 &lev3 &lev4)
|
||||
out=&tmpds noduprec;
|
||||
by _all_;
|
||||
run;
|
||||
%if %mf_nobs(&tmpds)=&rows %then %do;
|
||||
@@ -6064,7 +6076,8 @@ create table &outds as
|
||||
%let lev5=%scan(&posspks,&m);
|
||||
%if &lev1 ne &lev5 & &lev2 ne &lev5 & &lev3 ne &lev5 & &lev4 ne &lev5 %then %do;
|
||||
/* check for four level uniqueness */
|
||||
proc sort data=&pkds(keep=&lev1 &lev2 &lev3 &lev4 &lev5) out=&tmpds noduprec;
|
||||
proc sort data=&pkds(keep=&lev1 &lev2 &lev3 &lev4 &lev5)
|
||||
out=&tmpds noduprec;
|
||||
by _all_;
|
||||
run;
|
||||
%if %mf_nobs(&tmpds)=&rows %then %do;
|
||||
@@ -6113,7 +6126,8 @@ create table &outds as
|
||||
run;
|
||||
%if %mf_nobs(&tmpds)=&rows %then %do;
|
||||
proc sql;
|
||||
insert into &outds values("&lev1 &lev2 &lev3 &lev4 &lev5 &lev6");
|
||||
insert into &outds
|
||||
values("&lev1 &lev2 &lev3 &lev4 &lev5 &lev6");
|
||||
%if %mf_nobs(&outds) ge &max_guesses %then %do;
|
||||
%put &sysmacroname: Max PKs reached at Level 6 for &baseds;
|
||||
%return;
|
||||
@@ -6155,6 +6169,7 @@ create table &outds as
|
||||
|
||||
@param [in] libds dataset to hash
|
||||
@param [in] salt= Provide a salt (could be, for instance, the dataset name)
|
||||
@param [in] iftrue= A condition under which the macro should be executed.
|
||||
@param [out] outds= (work.mf_hashdataset) The output dataset to create. This
|
||||
will contain one column (hashkey) with one observation (a hex32.
|
||||
representation of the input hash)
|
||||
@@ -6169,10 +6184,14 @@ create table &outds as
|
||||
%macro mp_hashdataset(
|
||||
libds,
|
||||
outds=,
|
||||
salt=
|
||||
salt=,
|
||||
iftrue=%str(1=1)
|
||||
)/*/STORE SOURCE*/;
|
||||
|
||||
%if not(%eval(%unquote(&iftrue))) %then %return;
|
||||
|
||||
%if %mf_getattrn(&libds,NLOBS)=0 %then %do;
|
||||
%put %str(WARN)ING: Dataset &libds is empty;, or is not a dataset;
|
||||
%put %str(WARN)ING: Dataset &libds is empty, or is not a dataset;
|
||||
%end;
|
||||
%else %if %mf_getattrn(&libds,NLOBS)<0 %then %do;
|
||||
%put %str(ERR)OR: Dataset &libds is not a dataset;
|
||||
@@ -6312,6 +6331,59 @@ run;
|
||||
filename &tempref clear;
|
||||
|
||||
%mend mp_include;/**
|
||||
@file
|
||||
@brief Initialise session with useful settings and variables
|
||||
@details Implements a set of recommended options for general SAS use. This
|
||||
macro is NOT used elsewhere within the core library (other than in tests),
|
||||
but it is used by the SASjs team when building web services for
|
||||
SAS-Powered applications elsewhere.
|
||||
|
||||
If you have a good idea for an option, setting, or useful global variable -
|
||||
feel free to [raise an issue](https://github.com/sasjs/core/issues/new)!
|
||||
|
||||
All global variables are prefixed with "SASJS_" (unless modfied with the
|
||||
prefix parameter).
|
||||
|
||||
@param [in] prefix= (SASJS) The prefix to apply to the global macro variables
|
||||
|
||||
|
||||
@version 9.2
|
||||
@author Allan Bowe
|
||||
|
||||
**/
|
||||
|
||||
%macro mp_init(prefix=
|
||||
)/*/STORE SOURCE*/;
|
||||
|
||||
%global
|
||||
&prefix._INIT_NUM /* initialisation time as numeric */
|
||||
&prefix._INIT_DTTM /* initialisation time in E8601DT26.6 format */
|
||||
;
|
||||
%if %eval(&&&prefix._INIT_NUM>0) %then %return; /* only run once */
|
||||
|
||||
data _null_;
|
||||
dttm=datetime();
|
||||
call symputx("&prefix._init_num",dttm);
|
||||
call symputx("&prefix._init_dttm",put(dttm,E8601DT26.6));
|
||||
run;
|
||||
|
||||
options
|
||||
autocorrect /* disallow mis-spelled procedure names */
|
||||
compress=CHAR /* default is none so ensure we have something! */
|
||||
datastmtchk=ALLKEYWORDS /* protection from overwriting input datasets */
|
||||
errorcheck=STRICT /* catch errors in libname/filename statements */
|
||||
fmterr /* ensure err when a format cannot be found */
|
||||
mergenoby=%str(ERR)OR /* Throw err when a merge has no BY variables */
|
||||
missing=. /* some sites change this which causes hard to detect errors */
|
||||
noquotelenmax /* avoid warnings for long strings */
|
||||
noreplace /* avoid overwriting permanent datasets */
|
||||
ps=max /* reduce log size slightly */
|
||||
validmemname=COMPATIBLE /* avoid special characters etc in table names */
|
||||
validvarname=V7 /* avoid special characters etc in variable names */
|
||||
varlenchk=%str(ERR)OR /* fail hard if truncation (data loss) can result */
|
||||
;
|
||||
|
||||
%mend mp_init;/**
|
||||
@file mp_jsonout.sas
|
||||
@brief Writes JSON in SASjs format to a fileref
|
||||
@details PROC JSON is faster but will produce errs like the ones below if
|
||||
@@ -7049,6 +7121,92 @@ lock &libds clear;
|
||||
)
|
||||
|
||||
%mend mp_lockfilecheck;/**
|
||||
@file
|
||||
@brief Create sample data based on the structure of an empty table
|
||||
@details Many SAS projects involve sensitive datasets. One way to _ensure_
|
||||
the data is anonymised, is never to receive it in the first place! Often
|
||||
consultants are provided with empty tables, and expected to create complex
|
||||
ETL flows.
|
||||
|
||||
This macro can help by taking an empty table, and populating it with data
|
||||
according to the variable types and formats.
|
||||
|
||||
TODO:
|
||||
@li Respect PKs
|
||||
@li Respect NOT NULLs
|
||||
@li Consider dates, datetimes, times, integers etc
|
||||
|
||||
Usage:
|
||||
|
||||
proc sql;
|
||||
create table work.example(
|
||||
TX_FROM float format=datetime19.,
|
||||
DD_TYPE char(16),
|
||||
DD_SOURCE char(2048),
|
||||
DD_SHORTDESC char(256),
|
||||
constraint pk primary key(tx_from, dd_type,dd_source),
|
||||
constraint nnn not null(DD_SHORTDESC)
|
||||
);
|
||||
%mp_makedata(work.example)
|
||||
|
||||
@param [in] libds The empty table in which to create data
|
||||
@param [out] obs= (500) The number of records to create.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mf_getuniquename.sas
|
||||
@li mf_getvarlen.sas
|
||||
@li mf_nobs.sas
|
||||
@li mp_getcols.sas
|
||||
@li mp_getpk.sas
|
||||
|
||||
@version 9.2
|
||||
@author Allan Bowe
|
||||
|
||||
**/
|
||||
|
||||
%macro mp_makedata(libds
|
||||
,obs=500
|
||||
)/*/STORE SOURCE*/;
|
||||
|
||||
%local ds1 c1 n1 i col charvars numvars;
|
||||
|
||||
%if %mf_nobs(&libds)>0 %then %do;
|
||||
%put &sysmacroname: &libds has data, it will not be recreated;
|
||||
%return;
|
||||
%end;
|
||||
|
||||
%local ds1 c1 n1;
|
||||
%let ds1=%mf_getuniquename(prefix=mp_makedata);
|
||||
%let c1=%mf_getuniquename(prefix=mp_makedatacol);
|
||||
%let n1=%mf_getuniquename(prefix=mp_makedatacol);
|
||||
data &ds1;
|
||||
if 0 then set &libds;
|
||||
do _n_=1 to &obs;
|
||||
&c1=repeat(uuidgen(),10);
|
||||
&n1=ranuni(1)*5000000;
|
||||
drop &c1 &n1;
|
||||
%let charvars=%mf_getvarlist(&libds,typefilter=C);
|
||||
%do i=1 %to %sysfunc(countw(&charvars));
|
||||
%let col=%scan(&charvars,&i);
|
||||
&col=subpad(&c1,1,%mf_getvarlen(&libds,&col));
|
||||
%end;
|
||||
|
||||
%let numvars=%mf_getvarlist(&libds,typefilter=N);
|
||||
%do i=1 %to %sysfunc(countw(&numvars));
|
||||
%let col=%scan(&numvars,&i);
|
||||
&col=&n1;
|
||||
%end;
|
||||
output;
|
||||
end;
|
||||
run;
|
||||
|
||||
proc append base=&libds data=&ds1;
|
||||
run;
|
||||
|
||||
proc sql;
|
||||
drop table &ds1;
|
||||
|
||||
%mend mp_makedata;/**
|
||||
@file
|
||||
@brief Create a Markdown Table from a dataset
|
||||
@details A markdown table is a simple table representation for use in
|
||||
@@ -7364,6 +7522,32 @@ insert into &outds select distinct * from &append_ds;
|
||||
|
||||
%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 -
|
||||
@@ -7810,7 +7994,7 @@ run;
|
||||
%let tempvw=%mf_getuniquename(prefix=&sysmacroname);
|
||||
proc sql;
|
||||
create view work.&tempvw as select * from &lib..&ds
|
||||
order by %mf_getquotedstr(&sortkey,quote=%str());
|
||||
order by %mf_getquotedstr(&sortkey,quote=N);
|
||||
|
||||
/* append sorted data */
|
||||
proc append base=&lib..&tempds2 data=work.&tempvw;
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
for:
|
||||
> "these","words","are","double","quoted"
|
||||
|
||||
@param in_str the unquoted, spaced delimited string to transform
|
||||
@param dlm= the delimeter to be applied to the output (default comma)
|
||||
@param indlm= the delimeter used for the input (default is space)
|
||||
@param quote= the quote mark to apply (S=Single, D=Double). If any other value
|
||||
than uppercase S or D is supplied, then that value will be used as the
|
||||
quoting character.
|
||||
@param [in] in_str The unquoted, spaced delimited string to transform
|
||||
@param [in] dlm= The delimeter to be applied to the output (default comma)
|
||||
@param [in] indlm= (,) The delimeter used for the input (default is space)
|
||||
@param [in] quote= (S) The quote mark to apply (S=Single, D=Double, N=None).
|
||||
If any other value than uppercase S or D is supplied, then that value will
|
||||
be used as the quoting character.
|
||||
@return output returns a string with the newly quoted / delimited output.
|
||||
|
||||
@version 9.2
|
||||
@@ -30,9 +30,10 @@
|
||||
|
||||
%macro mf_getquotedstr(IN_STR,DLM=%str(,),QUOTE=S,indlm=%str( )
|
||||
)/*/STORE SOURCE*/;
|
||||
%if "e=S %then %let quote=%str(%');
|
||||
%else %if "e=D %then %let quote=%str(%");
|
||||
%else %let quote=%str();
|
||||
/* credit Rowland Hale - byte34 is double quote, 39 is single quote */
|
||||
%if "e=S %then %let quote=%qsysfunc(byte(39));
|
||||
%else %if "e=D %then %let quote=%qsysfunc(byte(34));
|
||||
%else %if "e=N %then %let quote=;
|
||||
%local i item buffer;
|
||||
%let i=1;
|
||||
%do %while (%qscan(&IN_STR,&i,%str(&indlm)) ne %str() ) ;
|
||||
|
||||
@@ -51,7 +51,7 @@ data &outds(keep=name type length varnum format label ddtype);
|
||||
else if formatd=0 then format=cats(format2,formatl,'.');
|
||||
else format=cats(format2,formatl,'.',formatd);
|
||||
type='N';
|
||||
if format=:'DATETIME' then ddtype='DATETIME';
|
||||
if format=:'DATETIME' or format=:'E8601DT' then ddtype='DATETIME';
|
||||
else if format=:'DATE' or format=:'DDMMYY' or format=:'MMDDYY'
|
||||
or format=:'YYMMDD' or format=:'E8601DA' or format=:'B8601DA'
|
||||
or format=:'MONYY'
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
|
||||
Returns:
|
||||
|
||||
|libref:$8.|dsn:$32.|memtype:$8.|dbms_memtype:$32.|typemem:$8.|memlabel:$256.|nvar:best.|compress:$8.|pk_fields:$512.|
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
|WORK|EXAMPLE|DATA| |DATA| |4|NO|TX_FROM DD_TYPE DD_SOURCE|
|
||||
|
||||
|
||||
@param [in] lib The libref to examine
|
||||
@param [in] ds= (0) Select the dataset to examine, else use 0 for all tables
|
||||
@@ -37,6 +41,7 @@
|
||||
|
||||
<h4> Related Macros </h4>
|
||||
@li mp_getpk.test.sas
|
||||
@li mp_guesspk.sas
|
||||
|
||||
@version 9.3
|
||||
@author Macro People Ltd
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
/**
|
||||
@file mp_guesspk.sas
|
||||
@file
|
||||
@brief Guess the primary key of a table
|
||||
@details Tries to guess the primary key of a table based on the following logic:
|
||||
@details Tries to guess the primary key of a table based on the following
|
||||
logic:
|
||||
|
||||
* Columns with nulls are ignored
|
||||
* Return only column combinations that provide unique results
|
||||
* Start from one column, then move out to include composite keys of 2 to 6 columns
|
||||
* Start from one column, then move out to composite keys of 2 to 6 columns
|
||||
|
||||
The library of the target should be assigned before using this macro.
|
||||
|
||||
Usage:
|
||||
|
||||
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||
filename mc url
|
||||
"https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||
%inc mc;
|
||||
%mp_guesspk(sashelp.class,outds=classpks)
|
||||
|
||||
@param baseds The dataset to analyse
|
||||
@param outds= The output dataset to contain the possible PKs
|
||||
@param max_guesses= The total number of possible primary keys to generate. A
|
||||
table is likely to have multiple unlikely PKs, so no need to list them all. Default=3.
|
||||
@param min_rows= The minimum number of rows a table should have in order to try
|
||||
and guess the PK. Default=5.
|
||||
@param max_guesses= (3) The total number of possible primary keys to generate.
|
||||
A table may have multiple unlikely PKs, so no need to list them all.
|
||||
@param min_rows= (5) The minimum number of rows a table should have in order
|
||||
to try and guess the PK.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mf_getvarlist.sas
|
||||
@li mf_getuniquename.sas
|
||||
@li mf_nobs.sas
|
||||
|
||||
<h4> Related Macros </h4>
|
||||
@li mp_getpk.sas
|
||||
|
||||
@version 9.3
|
||||
@author Allan Bowe
|
||||
|
||||
@@ -196,7 +201,8 @@
|
||||
%let lev4=%scan(&posspks,&l);
|
||||
%if &lev1 ne &lev4 and &lev2 ne &lev4 and &lev3 ne &lev4 %then %do;
|
||||
/* check for four level uniqueness */
|
||||
proc sort data=&pkds(keep=&lev1 &lev2 &lev3 &lev4) out=&tmpds noduprec;
|
||||
proc sort data=&pkds(keep=&lev1 &lev2 &lev3 &lev4)
|
||||
out=&tmpds noduprec;
|
||||
by _all_;
|
||||
run;
|
||||
%if %mf_nobs(&tmpds)=&rows %then %do;
|
||||
@@ -233,7 +239,8 @@
|
||||
%let lev5=%scan(&posspks,&m);
|
||||
%if &lev1 ne &lev5 & &lev2 ne &lev5 & &lev3 ne &lev5 & &lev4 ne &lev5 %then %do;
|
||||
/* check for four level uniqueness */
|
||||
proc sort data=&pkds(keep=&lev1 &lev2 &lev3 &lev4 &lev5) out=&tmpds noduprec;
|
||||
proc sort data=&pkds(keep=&lev1 &lev2 &lev3 &lev4 &lev5)
|
||||
out=&tmpds noduprec;
|
||||
by _all_;
|
||||
run;
|
||||
%if %mf_nobs(&tmpds)=&rows %then %do;
|
||||
@@ -282,7 +289,8 @@
|
||||
run;
|
||||
%if %mf_nobs(&tmpds)=&rows %then %do;
|
||||
proc sql;
|
||||
insert into &outds values("&lev1 &lev2 &lev3 &lev4 &lev5 &lev6");
|
||||
insert into &outds
|
||||
values("&lev1 &lev2 &lev3 &lev4 &lev5 &lev6");
|
||||
%if %mf_nobs(&outds) ge &max_guesses %then %do;
|
||||
%put &sysmacroname: Max PKs reached at Level 6 for &baseds;
|
||||
%return;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
@param [in] libds dataset to hash
|
||||
@param [in] salt= Provide a salt (could be, for instance, the dataset name)
|
||||
@param [in] iftrue= A condition under which the macro should be executed.
|
||||
@param [out] outds= (work.mf_hashdataset) The output dataset to create. This
|
||||
will contain one column (hashkey) with one observation (a hex32.
|
||||
representation of the input hash)
|
||||
@@ -35,10 +36,14 @@
|
||||
%macro mp_hashdataset(
|
||||
libds,
|
||||
outds=,
|
||||
salt=
|
||||
salt=,
|
||||
iftrue=%str(1=1)
|
||||
)/*/STORE SOURCE*/;
|
||||
|
||||
%if not(%eval(%unquote(&iftrue))) %then %return;
|
||||
|
||||
%if %mf_getattrn(&libds,NLOBS)=0 %then %do;
|
||||
%put %str(WARN)ING: Dataset &libds is empty;, or is not a dataset;
|
||||
%put %str(WARN)ING: Dataset &libds is empty, or is not a dataset;
|
||||
%end;
|
||||
%else %if %mf_getattrn(&libds,NLOBS)<0 %then %do;
|
||||
%put %str(ERR)OR: Dataset &libds is not a dataset;
|
||||
|
||||
54
base/mp_init.sas
Normal file
54
base/mp_init.sas
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
@file
|
||||
@brief Initialise session with useful settings and variables
|
||||
@details Implements a set of recommended options for general SAS use. This
|
||||
macro is NOT used elsewhere within the core library (other than in tests),
|
||||
but it is used by the SASjs team when building web services for
|
||||
SAS-Powered applications elsewhere.
|
||||
|
||||
If you have a good idea for an option, setting, or useful global variable -
|
||||
feel free to [raise an issue](https://github.com/sasjs/core/issues/new)!
|
||||
|
||||
All global variables are prefixed with "SASJS_" (unless modfied with the
|
||||
prefix parameter).
|
||||
|
||||
@param [in] prefix= (SASJS) The prefix to apply to the global macro variables
|
||||
|
||||
|
||||
@version 9.2
|
||||
@author Allan Bowe
|
||||
|
||||
**/
|
||||
|
||||
%macro mp_init(prefix=
|
||||
)/*/STORE SOURCE*/;
|
||||
|
||||
%global
|
||||
&prefix._INIT_NUM /* initialisation time as numeric */
|
||||
&prefix._INIT_DTTM /* initialisation time in E8601DT26.6 format */
|
||||
;
|
||||
%if %eval(&&&prefix._INIT_NUM>0) %then %return; /* only run once */
|
||||
|
||||
data _null_;
|
||||
dttm=datetime();
|
||||
call symputx("&prefix._init_num",dttm);
|
||||
call symputx("&prefix._init_dttm",put(dttm,E8601DT26.6));
|
||||
run;
|
||||
|
||||
options
|
||||
autocorrect /* disallow mis-spelled procedure names */
|
||||
compress=CHAR /* default is none so ensure we have something! */
|
||||
datastmtchk=ALLKEYWORDS /* protection from overwriting input datasets */
|
||||
errorcheck=STRICT /* catch errors in libname/filename statements */
|
||||
fmterr /* ensure err when a format cannot be found */
|
||||
mergenoby=%str(ERR)OR /* Throw err when a merge has no BY variables */
|
||||
missing=. /* some sites change this which causes hard to detect errors */
|
||||
noquotelenmax /* avoid warnings for long strings */
|
||||
noreplace /* avoid overwriting permanent datasets */
|
||||
ps=max /* reduce log size slightly */
|
||||
validmemname=COMPATIBLE /* avoid special characters etc in table names */
|
||||
validvarname=V7 /* avoid special characters etc in variable names */
|
||||
varlenchk=%str(ERR)OR /* fail hard if truncation (data loss) can result */
|
||||
;
|
||||
|
||||
%mend mp_init;
|
||||
87
base/mp_makedata.sas
Normal file
87
base/mp_makedata.sas
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
@file
|
||||
@brief Create sample data based on the structure of an empty table
|
||||
@details Many SAS projects involve sensitive datasets. One way to _ensure_
|
||||
the data is anonymised, is never to receive it in the first place! Often
|
||||
consultants are provided with empty tables, and expected to create complex
|
||||
ETL flows.
|
||||
|
||||
This macro can help by taking an empty table, and populating it with data
|
||||
according to the variable types and formats.
|
||||
|
||||
TODO:
|
||||
@li Respect PKs
|
||||
@li Respect NOT NULLs
|
||||
@li Consider dates, datetimes, times, integers etc
|
||||
|
||||
Usage:
|
||||
|
||||
proc sql;
|
||||
create table work.example(
|
||||
TX_FROM float format=datetime19.,
|
||||
DD_TYPE char(16),
|
||||
DD_SOURCE char(2048),
|
||||
DD_SHORTDESC char(256),
|
||||
constraint pk primary key(tx_from, dd_type,dd_source),
|
||||
constraint nnn not null(DD_SHORTDESC)
|
||||
);
|
||||
%mp_makedata(work.example)
|
||||
|
||||
@param [in] libds The empty table in which to create data
|
||||
@param [out] obs= (500) The number of records to create.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mf_getuniquename.sas
|
||||
@li mf_getvarlen.sas
|
||||
@li mf_nobs.sas
|
||||
@li mp_getcols.sas
|
||||
@li mp_getpk.sas
|
||||
|
||||
@version 9.2
|
||||
@author Allan Bowe
|
||||
|
||||
**/
|
||||
|
||||
%macro mp_makedata(libds
|
||||
,obs=500
|
||||
)/*/STORE SOURCE*/;
|
||||
|
||||
%local ds1 c1 n1 i col charvars numvars;
|
||||
|
||||
%if %mf_nobs(&libds)>0 %then %do;
|
||||
%put &sysmacroname: &libds has data, it will not be recreated;
|
||||
%return;
|
||||
%end;
|
||||
|
||||
%local ds1 c1 n1;
|
||||
%let ds1=%mf_getuniquename(prefix=mp_makedata);
|
||||
%let c1=%mf_getuniquename(prefix=mp_makedatacol);
|
||||
%let n1=%mf_getuniquename(prefix=mp_makedatacol);
|
||||
data &ds1;
|
||||
if 0 then set &libds;
|
||||
do _n_=1 to &obs;
|
||||
&c1=repeat(uuidgen(),10);
|
||||
&n1=ranuni(1)*5000000;
|
||||
drop &c1 &n1;
|
||||
%let charvars=%mf_getvarlist(&libds,typefilter=C);
|
||||
%do i=1 %to %sysfunc(countw(&charvars));
|
||||
%let col=%scan(&charvars,&i);
|
||||
&col=subpad(&c1,1,%mf_getvarlen(&libds,&col));
|
||||
%end;
|
||||
|
||||
%let numvars=%mf_getvarlist(&libds,typefilter=N);
|
||||
%do i=1 %to %sysfunc(countw(&numvars));
|
||||
%let col=%scan(&numvars,&i);
|
||||
&col=&n1;
|
||||
%end;
|
||||
output;
|
||||
end;
|
||||
run;
|
||||
|
||||
proc append base=&libds data=&ds1;
|
||||
run;
|
||||
|
||||
proc sql;
|
||||
drop table &ds1;
|
||||
|
||||
%mend mp_makedata;
|
||||
27
base/mp_reseterror.sas
Normal file
27
base/mp_reseterror.sas
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
@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;
|
||||
@@ -89,7 +89,7 @@ run;
|
||||
%let tempvw=%mf_getuniquename(prefix=&sysmacroname);
|
||||
proc sql;
|
||||
create view work.&tempvw as select * from &lib..&ds
|
||||
order by %mf_getquotedstr(&sortkey,quote=%str());
|
||||
order by %mf_getquotedstr(&sortkey,quote=N);
|
||||
|
||||
/* append sorted data */
|
||||
proc append base=&lib..&tempds2 data=work.&tempvw;
|
||||
|
||||
24
tests/crossplatform/mp_init.test.sas
Normal file
24
tests/crossplatform/mp_init.test.sas
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
@file
|
||||
@brief Testing mp_gsubfile.sas macro
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_init.sas
|
||||
@li mp_assert.sas
|
||||
|
||||
**/
|
||||
|
||||
/**
|
||||
* Test 1 - mp_init.sas actually already ran as part of testinit
|
||||
* So lets test to make sure it will not run again
|
||||
*/
|
||||
|
||||
%let initial_value=&sasjs_init_num;
|
||||
|
||||
%mp_init();
|
||||
|
||||
%mp_assert(
|
||||
iftrue=("&initial_value"="&sasjs_init_num"),
|
||||
desc=Check that mp_init() did not run twice,
|
||||
outds=work.test_results
|
||||
)
|
||||
@@ -11,9 +11,10 @@
|
||||
**/
|
||||
|
||||
/* grab 20 datasets from SASHELP */
|
||||
%let path=%sysfunc(pathname(work));
|
||||
%let work=%sysfunc(pathname(work));
|
||||
%let path=&work/new;
|
||||
%mf_mkdir(&path)
|
||||
libname sashlp "&path";
|
||||
libname sashlp "&work";
|
||||
proc sql noprint;
|
||||
create table members as
|
||||
select distinct lowcase(memname) as memname
|
||||
@@ -31,6 +32,7 @@ run;
|
||||
%mp_lib2inserts(sashlp, schema=work, outref=tempref,maxobs=50)
|
||||
|
||||
/* check if it actually runs */
|
||||
libname sashlp "&path";
|
||||
options source2;
|
||||
%inc tempref;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_lockfilecheck.sas
|
||||
@li mp_assert.sas
|
||||
@li mp_reseterror.sas
|
||||
|
||||
**/
|
||||
|
||||
@@ -29,6 +30,8 @@ data work.test; a=1;run;
|
||||
|
||||
%mp_lockfilecheck(sashelp.class)
|
||||
|
||||
%mp_reseterror()
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&success=1),
|
||||
desc=Checking sashelp table cannot be locked,
|
||||
|
||||
23
tests/crossplatform/mp_reseterror.test.sas
Normal file
23
tests/crossplatform/mp_reseterror.test.sas
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
@file
|
||||
@brief Testing mp_reseterror macro
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_assert.sas
|
||||
@li mp_reseterror.sas
|
||||
|
||||
**/
|
||||
|
||||
|
||||
/* cause an error */
|
||||
|
||||
lock sashelp.class;
|
||||
|
||||
/* recover ? */
|
||||
%mp_reseterror()
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&syscc=0),
|
||||
desc=Checking error condition was fixed,
|
||||
outds=work.test_results
|
||||
)
|
||||
@@ -2,11 +2,17 @@
|
||||
@file
|
||||
@brief init file for tests
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_init.sas
|
||||
|
||||
**/
|
||||
|
||||
/* location in metadata or SAS Drive for temporary files */
|
||||
%let mcTestAppLoc=/Public/temp/macrocore;
|
||||
|
||||
/* set defaults */
|
||||
%mp_init()
|
||||
|
||||
%macro loglevel();
|
||||
%if &_debug=2477 %then %do;
|
||||
options mprint;
|
||||
|
||||
Reference in New Issue
Block a user