@sasjs/utils 2.49.0 → 2.49.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fs/internal/helper.js +19 -24
- package/fs/internal/helper.js.map +1 -1
- package/macros/mf_abort.sas +27 -0
- package/macros/mf_dedup.sas +53 -0
- package/macros/mf_deletefile.sas +31 -0
- package/macros/mf_existds.sas +30 -0
- package/macros/mf_existfeature.sas +59 -0
- package/macros/mf_existfileref.sas +33 -0
- package/macros/mf_existfunction.sas +37 -0
- package/macros/mf_existvar.sas +39 -0
- package/macros/mf_existvarlist.sas +56 -0
- package/macros/mf_fmtdttm.sas +42 -0
- package/macros/mf_getapploc.sas +78 -0
- package/macros/mf_getattrc.sas +34 -0
- package/macros/mf_getattrn.sas +34 -0
- package/macros/mf_getengine.sas +54 -0
- package/macros/mf_getfilesize.sas +64 -0
- package/macros/mf_getfmtlist.sas +61 -0
- package/macros/mf_getfmtname.sas +44 -0
- package/macros/mf_getkeyvalue.sas +32 -0
- package/macros/mf_getplatform.sas +75 -0
- package/macros/mf_getquotedstr.sas +57 -0
- package/macros/mf_getschema.sas +43 -0
- package/macros/mf_getuniquefileref.sas +60 -0
- package/macros/mf_getuniquelibref.sas +57 -0
- package/macros/mf_getuniquename.sas +22 -0
- package/macros/mf_getuser.sas +44 -0
- package/macros/mf_getvalue.sas +36 -0
- package/macros/mf_getvarcount.sas +49 -0
- package/macros/mf_getvarformat.sas +72 -0
- package/macros/mf_getvarlen.sas +56 -0
- package/macros/mf_getvarlist.sas +74 -0
- package/macros/mf_getvarnum.sas +58 -0
- package/macros/mf_getvartype.sas +52 -0
- package/macros/mf_getxengine.sas +43 -0
- package/macros/mf_increment.sas +29 -0
- package/macros/mf_isblank.sas +27 -0
- package/macros/mf_isdir.sas +34 -0
- package/macros/mf_isint.sas +36 -0
- package/macros/mf_islibds.sas +40 -0
- package/macros/mf_loc.sas +29 -0
- package/macros/mf_mkdir.sas +67 -0
- package/macros/mf_mval.sas +19 -0
- package/macros/mf_nobs.sas +26 -0
- package/macros/mf_trimstr.sas +50 -0
- package/macros/mf_uid.sas +21 -0
- package/macros/mf_verifymacvars.sas +72 -0
- package/macros/mf_wordsinstr1andstr2.sas +53 -0
- package/macros/mf_wordsinstr1butnotstr2.sas +54 -0
- package/macros/mf_writefile.sas +67 -0
- package/macros/mp_abort.sas +301 -0
- package/macros/mp_appendfile.sas +57 -0
- package/macros/mp_applyformats.sas +181 -0
- package/macros/mp_assert.sas +56 -0
- package/macros/mp_assertcols.sas +145 -0
- package/macros/mp_assertcolvals.sas +168 -0
- package/macros/mp_assertdsobs.sas +121 -0
- package/macros/mp_assertscope.sas +147 -0
- package/macros/mp_base64copy.sas +117 -0
- package/macros/mp_binarycopy.sas +79 -0
- package/macros/mp_chop.sas +194 -0
- package/macros/mp_cleancsv.sas +78 -0
- package/macros/mp_cntlout.sas +85 -0
- package/macros/mp_copyfolder.sas +84 -0
- package/macros/mp_coretable.sas +73 -0
- package/macros/mp_createconstraints.sas +70 -0
- package/macros/mp_createwebservice.sas +32 -0
- package/macros/mp_csv2ds.sas +140 -0
- package/macros/mp_deleteconstraints.sas +52 -0
- package/macros/mp_deletefolder.sas +70 -0
- package/macros/mp_dictionary.sas +52 -0
- package/macros/mp_dirlist.sas +262 -0
- package/macros/mp_distinctfmtvalues.sas +50 -0
- package/macros/mp_dropmembers.sas +46 -0
- package/macros/mp_ds2cards.sas +291 -0
- package/macros/mp_ds2csv.sas +212 -0
- package/macros/mp_ds2ddl.sas +30 -0
- package/macros/mp_ds2fmtds.sas +105 -0
- package/macros/mp_ds2inserts.sas +180 -0
- package/macros/mp_ds2md.sas +103 -0
- package/macros/mp_ds2squeeze.sas +120 -0
- package/macros/mp_dsmeta.sas +110 -0
- package/macros/mp_filtercheck.sas +251 -0
- package/macros/mp_filtergenerate.sas +105 -0
- package/macros/mp_filterstore.sas +254 -0
- package/macros/mp_filtervalidate.sas +105 -0
- package/macros/mp_getcols.sas +67 -0
- package/macros/mp_getconstraints.sas +115 -0
- package/macros/mp_getdbml.sas +335 -0
- package/macros/mp_getddl.sas +400 -0
- package/macros/mp_getformats.sas +124 -0
- package/macros/mp_getmaxvarlengths.sas +130 -0
- package/macros/mp_getpk.sas +281 -0
- package/macros/mp_gitadd.sas +45 -0
- package/macros/mp_gitreleaseinfo.sas +74 -0
- package/macros/mp_gitstatus.sas +67 -0
- package/macros/mp_gsubfile.sas +58 -0
- package/macros/mp_guesspk.sas +332 -0
- package/macros/mp_hashdataset.sas +91 -0
- package/macros/mp_hashdirectory.sas +164 -0
- package/macros/mp_include.sas +106 -0
- package/macros/mp_init.sas +76 -0
- package/macros/mp_jsonout.sas +406 -0
- package/macros/mp_lib2cards.sas +78 -0
- package/macros/mp_lib2inserts.sas +77 -0
- package/macros/mp_loadformat.sas +303 -0
- package/macros/mp_lockanytable.sas +243 -0
- package/macros/mp_lockfilecheck.sas +103 -0
- package/macros/mp_makedata.sas +107 -0
- package/macros/mp_md5.sas +58 -0
- package/macros/mp_perflog.sas +43 -0
- package/macros/mp_prevobs.sas +88 -0
- package/macros/mp_recursivejoin.sas +90 -0
- package/macros/mp_replace.sas +151 -0
- package/macros/mp_reseterror.sas +27 -0
- package/macros/mp_resetoption.sas +39 -0
- package/macros/mp_retainedkey.sas +247 -0
- package/macros/mp_runddl.sas +49 -0
- package/macros/mp_searchcols.sas +103 -0
- package/macros/mp_searchdata.sas +131 -0
- package/macros/mp_setkeyvalue.sas +55 -0
- package/macros/mp_sortinplace.sas +122 -0
- package/macros/mp_stackdiffs.sas +592 -0
- package/macros/mp_storediffs.sas +230 -0
- package/macros/mp_stprequests.sas +74 -0
- package/macros/mp_streamfile.sas +212 -0
- package/macros/mp_testjob.sas +92 -0
- package/macros/mp_testservice.sas +39 -0
- package/macros/mp_testwritespeedlibrary.sas +59 -0
- package/macros/mp_tree.sas +71 -0
- package/macros/mp_unzip.sas +90 -0
- package/macros/mp_updatevarlength.sas +93 -0
- package/macros/mp_validatecol.sas +97 -0
- package/macros/mp_wait4file.sas +35 -0
- package/macros/mp_webin.sas +59 -0
- package/macros/mp_zip.sas +85 -0
- package/package.json +5 -2
- package/scripts/copySASjsCore.ts +36 -0
- package/utils/index.d.ts +1 -2
- package/utils/index.js +2 -3
- package/utils/index.js.map +1 -1
- package/utils/utils.d.ts +1 -0
- package/utils/utils.js +7 -1
- package/utils/utils.js.map +1 -1
- package/utils/getNodeModulePath.d.ts +0 -2
- package/utils/getNodeModulePath.js +0 -83
- package/utils/getNodeModulePath.js.map +0 -1
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@file
|
|
3
|
+
@brief Guess the primary key of a table
|
|
4
|
+
@details Tries to guess the primary key of a table based on the following
|
|
5
|
+
logic:
|
|
6
|
+
|
|
7
|
+
* Columns with nulls are ignored
|
|
8
|
+
* Return only column combinations that provide unique results
|
|
9
|
+
* Start from one column, then move out to composite keys of 2 to 6 columns
|
|
10
|
+
|
|
11
|
+
The library of the target should be assigned before using this macro.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
|
|
15
|
+
filename mc url
|
|
16
|
+
"https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
|
17
|
+
%inc mc;
|
|
18
|
+
%mp_guesspk(sashelp.class,outds=classpks)
|
|
19
|
+
|
|
20
|
+
@param [in] baseds The dataset to analyse
|
|
21
|
+
@param [out] outds= The output dataset to contain the possible PKs
|
|
22
|
+
@param [in] max_guesses= (3) The total number of possible primary keys to
|
|
23
|
+
generate. A table may have multiple (unlikely) PKs, so no need to list them
|
|
24
|
+
all.
|
|
25
|
+
@param [in] min_rows= (5) The minimum number of rows a table should have in
|
|
26
|
+
order to try and guess the PK.
|
|
27
|
+
@param [in] ignore_cols (0) Space seperated list of columns which you are
|
|
28
|
+
sure are not part of the primary key (helps to avoid false positives)
|
|
29
|
+
@param [in] mdebug= Set to 1 to enable DEBUG messages and preserve outputs
|
|
30
|
+
|
|
31
|
+
<h4> SAS Macros </h4>
|
|
32
|
+
@li mf_getvarlist.sas
|
|
33
|
+
@li mf_getuniquename.sas
|
|
34
|
+
@li mf_wordsInstr1butnotstr2.sas
|
|
35
|
+
@li mf_nobs.sas
|
|
36
|
+
|
|
37
|
+
<h4> Related Macros </h4>
|
|
38
|
+
@li mp_getpk.sas
|
|
39
|
+
|
|
40
|
+
@version 9.3
|
|
41
|
+
@author Allan Bowe
|
|
42
|
+
|
|
43
|
+
**/
|
|
44
|
+
|
|
45
|
+
%macro mp_guesspk(baseds
|
|
46
|
+
,outds=mp_guesspk
|
|
47
|
+
,max_guesses=3
|
|
48
|
+
,min_rows=5
|
|
49
|
+
,ignore_cols=0
|
|
50
|
+
,mdebug=0
|
|
51
|
+
)/*/STORE SOURCE*/;
|
|
52
|
+
%local dbg;
|
|
53
|
+
%if &mdebug=1 %then %do;
|
|
54
|
+
%put &sysmacroname entry vars:;
|
|
55
|
+
%put _local_;
|
|
56
|
+
%end;
|
|
57
|
+
%else %let dbg=*;
|
|
58
|
+
|
|
59
|
+
/* declare local vars */
|
|
60
|
+
%local var vars vcnt i j k l tmpvar tmpds rows posspks ppkcnt;
|
|
61
|
+
%let vars=%upcase(%mf_getvarlist(&baseds));
|
|
62
|
+
%let vars=%mf_wordsInStr1ButNotStr2(str1=&vars,str2=%upcase(&ignore_cols));
|
|
63
|
+
%let vcnt=%sysfunc(countw(&vars));
|
|
64
|
+
|
|
65
|
+
%if &vcnt=0 %then %do;
|
|
66
|
+
%put &sysmacroname: &baseds has no variables! Exiting.;
|
|
67
|
+
%return;
|
|
68
|
+
%end;
|
|
69
|
+
|
|
70
|
+
/* get null count and row count */
|
|
71
|
+
%let tmpvar=%mf_getuniquename();
|
|
72
|
+
proc sql noprint;
|
|
73
|
+
create table _data_ as select
|
|
74
|
+
count(*) as &tmpvar
|
|
75
|
+
%do i=1 %to &vcnt;
|
|
76
|
+
%let var=%scan(&vars,&i);
|
|
77
|
+
,sum(case when &var is missing then 1 else 0 end) as &var
|
|
78
|
+
%end;
|
|
79
|
+
from &baseds;
|
|
80
|
+
|
|
81
|
+
/* transpose table and scan for not null cols */
|
|
82
|
+
proc transpose;
|
|
83
|
+
data _null_;
|
|
84
|
+
set &syslast end=last;
|
|
85
|
+
length vars $32767;
|
|
86
|
+
retain vars ;
|
|
87
|
+
if _name_="&tmpvar" then call symputx('rows',col1,'l');
|
|
88
|
+
else if col1=0 then vars=catx(' ',vars,_name_);
|
|
89
|
+
if last then call symputx('posspks',vars,'l');
|
|
90
|
+
run;
|
|
91
|
+
|
|
92
|
+
%let ppkcnt=%sysfunc(countw(&posspks));
|
|
93
|
+
%if &ppkcnt=0 %then %do;
|
|
94
|
+
%put &sysmacroname: &baseds has no non-missing variables! Exiting.;
|
|
95
|
+
%return;
|
|
96
|
+
%end;
|
|
97
|
+
|
|
98
|
+
proc sort data=&baseds(keep=&posspks) out=_data_ noduprec;
|
|
99
|
+
by _all_;
|
|
100
|
+
run;
|
|
101
|
+
%local pkds; %let pkds=&syslast;
|
|
102
|
+
|
|
103
|
+
%if &rows > %mf_nobs(&pkds) %then %do;
|
|
104
|
+
%put &sysmacroname: &baseds has no combination of unique records! Exiting.;
|
|
105
|
+
%return;
|
|
106
|
+
%end;
|
|
107
|
+
|
|
108
|
+
/* now check cardinality */
|
|
109
|
+
proc sql noprint;
|
|
110
|
+
create table _data_ as select
|
|
111
|
+
%do i=1 %to &ppkcnt;
|
|
112
|
+
%let var=%scan(&posspks,&i);
|
|
113
|
+
count(distinct &var) as &var
|
|
114
|
+
%if &i<&ppkcnt %then ,;
|
|
115
|
+
%end;
|
|
116
|
+
from &pkds;
|
|
117
|
+
|
|
118
|
+
/* transpose and sort by cardinality */
|
|
119
|
+
proc transpose;
|
|
120
|
+
proc sort; by descending col1;
|
|
121
|
+
run;
|
|
122
|
+
|
|
123
|
+
/* create initial PK list and re-order posspks list */
|
|
124
|
+
data &outds(keep=pkguesses);
|
|
125
|
+
length pkguesses $5000 vars $5000;
|
|
126
|
+
set &syslast end=last;
|
|
127
|
+
retain vars ;
|
|
128
|
+
vars=catx(' ',vars,_name_);
|
|
129
|
+
if col1=&rows then do;
|
|
130
|
+
pkguesses=_name_;
|
|
131
|
+
output;
|
|
132
|
+
end;
|
|
133
|
+
if last then call symputx('posspks',vars,'l');
|
|
134
|
+
run;
|
|
135
|
+
|
|
136
|
+
%if %mf_nobs(&outds) ge &max_guesses %then %do;
|
|
137
|
+
%put &sysmacroname: %mf_nobs(&outds) possible primary key values found;
|
|
138
|
+
%return;
|
|
139
|
+
%end;
|
|
140
|
+
|
|
141
|
+
%if &ppkcnt=1 %then %do;
|
|
142
|
+
%put &sysmacroname: No more PK guess possible;
|
|
143
|
+
%return;
|
|
144
|
+
%end;
|
|
145
|
+
|
|
146
|
+
/* begin scanning for uniques on pairs of PKs */
|
|
147
|
+
%let tmpds=%mf_getuniquename();
|
|
148
|
+
%local lev1 lev2;
|
|
149
|
+
%do i=1 %to &ppkcnt;
|
|
150
|
+
%let lev1=%scan(&posspks,&i);
|
|
151
|
+
%do j=2 %to &ppkcnt;
|
|
152
|
+
%let lev2=%scan(&posspks,&j);
|
|
153
|
+
%if &lev1 ne &lev2 %then %do;
|
|
154
|
+
/* check for two level uniqueness */
|
|
155
|
+
proc sort data=&pkds(keep=&lev1 &lev2) out=&tmpds noduprec;
|
|
156
|
+
by _all_;
|
|
157
|
+
run;
|
|
158
|
+
%if %mf_nobs(&tmpds)=&rows %then %do;
|
|
159
|
+
proc sql;
|
|
160
|
+
insert into &outds values("&lev1 &lev2");
|
|
161
|
+
%if %mf_nobs(&outds) ge &max_guesses %then %do;
|
|
162
|
+
%put &sysmacroname: Max PKs reached at Level 2 for &baseds;
|
|
163
|
+
%goto exit;
|
|
164
|
+
%end;
|
|
165
|
+
%end;
|
|
166
|
+
%end;
|
|
167
|
+
%end;
|
|
168
|
+
%end;
|
|
169
|
+
|
|
170
|
+
%if &ppkcnt=2 %then %do;
|
|
171
|
+
%put &sysmacroname: No more PK guess possible;
|
|
172
|
+
%goto exit;
|
|
173
|
+
%end;
|
|
174
|
+
|
|
175
|
+
/* begin scanning for uniques on PK triplets */
|
|
176
|
+
%local lev3;
|
|
177
|
+
%do i=1 %to &ppkcnt;
|
|
178
|
+
%let lev1=%scan(&posspks,&i);
|
|
179
|
+
%do j=2 %to &ppkcnt;
|
|
180
|
+
%let lev2=%scan(&posspks,&j);
|
|
181
|
+
%if &lev1 ne &lev2 %then %do k=3 %to &ppkcnt;
|
|
182
|
+
%let lev3=%scan(&posspks,&k);
|
|
183
|
+
%if &lev1 ne &lev3 and &lev2 ne &lev3 %then %do;
|
|
184
|
+
/* check for three level uniqueness */
|
|
185
|
+
proc sort data=&pkds(keep=&lev1 &lev2 &lev3) out=&tmpds noduprec;
|
|
186
|
+
by _all_;
|
|
187
|
+
run;
|
|
188
|
+
%if %mf_nobs(&tmpds)=&rows %then %do;
|
|
189
|
+
proc sql;
|
|
190
|
+
insert into &outds values("&lev1 &lev2 &lev3");
|
|
191
|
+
%if %mf_nobs(&outds) ge &max_guesses %then %do;
|
|
192
|
+
%put &sysmacroname: Max PKs reached at Level 3 for &baseds;
|
|
193
|
+
%goto exit;
|
|
194
|
+
%end;
|
|
195
|
+
%end;
|
|
196
|
+
%end;
|
|
197
|
+
%end;
|
|
198
|
+
%end;
|
|
199
|
+
%end;
|
|
200
|
+
|
|
201
|
+
%if &ppkcnt=3 %then %do;
|
|
202
|
+
%put &sysmacroname: No more PK guess possible;
|
|
203
|
+
%goto exit;
|
|
204
|
+
%end;
|
|
205
|
+
|
|
206
|
+
/* scan for uniques on up to 4 PK fields */
|
|
207
|
+
%local lev4;
|
|
208
|
+
%do i=1 %to &ppkcnt;
|
|
209
|
+
%let lev1=%scan(&posspks,&i);
|
|
210
|
+
%do j=2 %to &ppkcnt;
|
|
211
|
+
%let lev2=%scan(&posspks,&j);
|
|
212
|
+
%if &lev1 ne &lev2 %then %do k=3 %to &ppkcnt;
|
|
213
|
+
%let lev3=%scan(&posspks,&k);
|
|
214
|
+
%if &lev1 ne &lev3 and &lev2 ne &lev3 %then %do l=4 %to &ppkcnt;
|
|
215
|
+
%let lev4=%scan(&posspks,&l);
|
|
216
|
+
%if &lev1 ne &lev4 and &lev2 ne &lev4 and &lev3 ne &lev4 %then %do;
|
|
217
|
+
/* check for four level uniqueness */
|
|
218
|
+
proc sort data=&pkds(keep=&lev1 &lev2 &lev3 &lev4)
|
|
219
|
+
out=&tmpds noduprec;
|
|
220
|
+
by _all_;
|
|
221
|
+
run;
|
|
222
|
+
%if %mf_nobs(&tmpds)=&rows %then %do;
|
|
223
|
+
proc sql;
|
|
224
|
+
insert into &outds values("&lev1 &lev2 &lev3 &lev4");
|
|
225
|
+
%if %mf_nobs(&outds) ge &max_guesses %then %do;
|
|
226
|
+
%put &sysmacroname: Max PKs reached at Level 4 for &baseds;
|
|
227
|
+
%goto exit;
|
|
228
|
+
%end;
|
|
229
|
+
%end;
|
|
230
|
+
%end;
|
|
231
|
+
%end;
|
|
232
|
+
%end;
|
|
233
|
+
%end;
|
|
234
|
+
%end;
|
|
235
|
+
|
|
236
|
+
%if &ppkcnt=4 %then %do;
|
|
237
|
+
%put &sysmacroname: No more PK guess possible;
|
|
238
|
+
%goto exit;
|
|
239
|
+
%end;
|
|
240
|
+
|
|
241
|
+
/* scan for uniques on up to 4 PK fields */
|
|
242
|
+
%local lev5 m;
|
|
243
|
+
%do i=1 %to &ppkcnt;
|
|
244
|
+
%let lev1=%scan(&posspks,&i);
|
|
245
|
+
%do j=2 %to &ppkcnt;
|
|
246
|
+
%let lev2=%scan(&posspks,&j);
|
|
247
|
+
%if &lev1 ne &lev2 %then %do k=3 %to &ppkcnt;
|
|
248
|
+
%let lev3=%scan(&posspks,&k);
|
|
249
|
+
%if &lev1 ne &lev3 and &lev2 ne &lev3 %then %do l=4 %to &ppkcnt;
|
|
250
|
+
%let lev4=%scan(&posspks,&l);
|
|
251
|
+
%if &lev1 ne &lev4 and &lev2 ne &lev4 and &lev3 ne &lev4 %then
|
|
252
|
+
%do m=5 %to &ppkcnt;
|
|
253
|
+
%let lev5=%scan(&posspks,&m);
|
|
254
|
+
%if &lev1 ne &lev5 & &lev2 ne &lev5 & &lev3 ne &lev5 & &lev4 ne &lev5 %then %do;
|
|
255
|
+
/* check for four level uniqueness */
|
|
256
|
+
proc sort data=&pkds(keep=&lev1 &lev2 &lev3 &lev4 &lev5)
|
|
257
|
+
out=&tmpds noduprec;
|
|
258
|
+
by _all_;
|
|
259
|
+
run;
|
|
260
|
+
%if %mf_nobs(&tmpds)=&rows %then %do;
|
|
261
|
+
proc sql;
|
|
262
|
+
insert into &outds values("&lev1 &lev2 &lev3 &lev4 &lev5");
|
|
263
|
+
%if %mf_nobs(&outds) ge &max_guesses %then %do;
|
|
264
|
+
%put &sysmacroname: Max PKs reached at Level 5 for &baseds;
|
|
265
|
+
%goto exit;
|
|
266
|
+
%end;
|
|
267
|
+
%end;
|
|
268
|
+
%end;
|
|
269
|
+
%end;
|
|
270
|
+
%end;
|
|
271
|
+
%end;
|
|
272
|
+
%end;
|
|
273
|
+
%end;
|
|
274
|
+
|
|
275
|
+
%if &ppkcnt=5 %then %do;
|
|
276
|
+
%put &sysmacroname: No more PK guess possible;
|
|
277
|
+
%goto exit;
|
|
278
|
+
%end;
|
|
279
|
+
|
|
280
|
+
/* scan for uniques on up to 4 PK fields */
|
|
281
|
+
%local lev6 n;
|
|
282
|
+
%do i=1 %to &ppkcnt;
|
|
283
|
+
%let lev1=%scan(&posspks,&i);
|
|
284
|
+
%do j=2 %to &ppkcnt;
|
|
285
|
+
%let lev2=%scan(&posspks,&j);
|
|
286
|
+
%if &lev1 ne &lev2 %then %do k=3 %to &ppkcnt;
|
|
287
|
+
%let lev3=%scan(&posspks,&k);
|
|
288
|
+
%if &lev1 ne &lev3 and &lev2 ne &lev3 %then %do l=4 %to &ppkcnt;
|
|
289
|
+
%let lev4=%scan(&posspks,&l);
|
|
290
|
+
%if &lev1 ne &lev4 and &lev2 ne &lev4 and &lev3 ne &lev4 %then
|
|
291
|
+
%do m=5 %to &ppkcnt;
|
|
292
|
+
%let lev5=%scan(&posspks,&m);
|
|
293
|
+
%if &lev1 ne &lev5 & &lev2 ne &lev5 & &lev3 ne &lev5 & &lev4 ne &lev5
|
|
294
|
+
%then %do n=6 %to &ppkcnt;
|
|
295
|
+
%let lev6=%scan(&posspks,&n);
|
|
296
|
+
%if &lev1 ne &lev6 & &lev2 ne &lev6 & &lev3 ne &lev6
|
|
297
|
+
& &lev4 ne &lev6 & &lev5 ne &lev6 %then
|
|
298
|
+
%do;
|
|
299
|
+
/* check for four level uniqueness */
|
|
300
|
+
proc sort data=&pkds(keep=&lev1 &lev2 &lev3 &lev4 &lev5 &lev6)
|
|
301
|
+
out=&tmpds noduprec;
|
|
302
|
+
by _all_;
|
|
303
|
+
run;
|
|
304
|
+
%if %mf_nobs(&tmpds)=&rows %then %do;
|
|
305
|
+
proc sql;
|
|
306
|
+
insert into &outds
|
|
307
|
+
values("&lev1 &lev2 &lev3 &lev4 &lev5 &lev6");
|
|
308
|
+
%if %mf_nobs(&outds) ge &max_guesses %then %do;
|
|
309
|
+
%put &sysmacroname: Max PKs reached at Level 6 for &baseds;
|
|
310
|
+
%goto exit;
|
|
311
|
+
%end;
|
|
312
|
+
%end;
|
|
313
|
+
%end;
|
|
314
|
+
%end;
|
|
315
|
+
%end;
|
|
316
|
+
%end;
|
|
317
|
+
%end;
|
|
318
|
+
%end;
|
|
319
|
+
%end;
|
|
320
|
+
|
|
321
|
+
%if &ppkcnt=6 %then %do;
|
|
322
|
+
%put &sysmacroname: No more PK guess possible;
|
|
323
|
+
%goto exit;
|
|
324
|
+
%end;
|
|
325
|
+
|
|
326
|
+
%exit:
|
|
327
|
+
%if &mdebug=0 %then %do;
|
|
328
|
+
proc sql;
|
|
329
|
+
drop table &tmpds;
|
|
330
|
+
%end;
|
|
331
|
+
|
|
332
|
+
%mend mp_guesspk;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@file
|
|
3
|
+
@brief Returns a unique hash for a dataset
|
|
4
|
+
@details Ignores metadata attributes, used only to hash values. If used to
|
|
5
|
+
compare datasets, they must have their columns and rows in the same order.
|
|
6
|
+
|
|
7
|
+
%mp_hashdataset(sashelp.class,outds=myhash)
|
|
8
|
+
|
|
9
|
+
data _null_;
|
|
10
|
+
set work.myhash;
|
|
11
|
+
put hashkey=;
|
|
12
|
+
run;
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
<h4> SAS Macros </h4>
|
|
17
|
+
@li mf_getattrn.sas
|
|
18
|
+
@li mf_getuniquename.sas
|
|
19
|
+
@li mf_getvarlist.sas
|
|
20
|
+
@li mp_md5.sas
|
|
21
|
+
|
|
22
|
+
<h4> Related Files </h4>
|
|
23
|
+
@li mp_hashdataset.test.sas
|
|
24
|
+
@li mp_hashdirectory.sas
|
|
25
|
+
|
|
26
|
+
@param [in] libds dataset to hash
|
|
27
|
+
@param [in] salt= Provide a salt (could be, for instance, the dataset name)
|
|
28
|
+
@param [in] iftrue= (1=1) A condition under which the macro should be executed
|
|
29
|
+
@param [out] outds= (work._data_) The output dataset to create. This
|
|
30
|
+
will contain one column (hashkey) with one observation (a $hex32.
|
|
31
|
+
representation of the input hash)
|
|
32
|
+
|hashkey:$32.|
|
|
33
|
+
|---|
|
|
34
|
+
|28ABC74ABFC45F50794237BA5566E6CA|
|
|
35
|
+
|
|
36
|
+
@version 9.2
|
|
37
|
+
@author Allan Bowe
|
|
38
|
+
**/
|
|
39
|
+
|
|
40
|
+
%macro mp_hashdataset(
|
|
41
|
+
libds,
|
|
42
|
+
outds=work._data_,
|
|
43
|
+
salt=,
|
|
44
|
+
iftrue=%str(1=1)
|
|
45
|
+
)/*/STORE SOURCE*/;
|
|
46
|
+
|
|
47
|
+
%local keyvar /* roll up the md5 */
|
|
48
|
+
prevkeyvar /* retain prev record md5 */
|
|
49
|
+
lastvar /* last var in input ds */
|
|
50
|
+
cvars nvars;
|
|
51
|
+
|
|
52
|
+
%if not(%eval(%unquote(&iftrue))) %then %return;
|
|
53
|
+
|
|
54
|
+
/* avoid naming conflict for hash key vars */
|
|
55
|
+
%let keyvar=%mf_getuniquename();
|
|
56
|
+
%let prevkeyvar=%mf_getuniquename();
|
|
57
|
+
%let lastvar=%mf_getuniquename();
|
|
58
|
+
|
|
59
|
+
%if %mf_getattrn(&libds,NLOBS)=0 %then %do;
|
|
60
|
+
data &outds;
|
|
61
|
+
length hashkey $32;
|
|
62
|
+
hashkey=put(md5("&salt"),$hex32.);
|
|
63
|
+
output;
|
|
64
|
+
stop;
|
|
65
|
+
run;
|
|
66
|
+
%put &sysmacroname: Dataset &libds is empty, or is not a dataset;
|
|
67
|
+
%put &sysmacroname: hashkey of &outds is based on salt (&salt) only;
|
|
68
|
+
%end;
|
|
69
|
+
%else %if %mf_getattrn(&libds,NLOBS)<0 %then %do;
|
|
70
|
+
%put %str(ERR)OR: Dataset &libds is not a dataset;
|
|
71
|
+
%end;
|
|
72
|
+
%else %do;
|
|
73
|
+
data &outds(rename=(&keyvar=hashkey) keep=&keyvar)
|
|
74
|
+
%if "%substr(&sysver,1,1)" ne "4" and "%substr(&sysver,1,1)" ne "5" %then %do;
|
|
75
|
+
/nonote2err
|
|
76
|
+
%end;
|
|
77
|
+
;
|
|
78
|
+
length &prevkeyvar &keyvar $32;
|
|
79
|
+
retain &prevkeyvar;
|
|
80
|
+
if _n_=1 then &prevkeyvar=put(md5("&salt"),$hex32.);
|
|
81
|
+
set &libds end=&lastvar;
|
|
82
|
+
/* hash should include previous row */
|
|
83
|
+
&keyvar=%mp_md5(
|
|
84
|
+
cvars=%mf_getvarlist(&libds,typefilter=C) &prevkeyvar,
|
|
85
|
+
nvars=%mf_getvarlist(&libds,typefilter=N)
|
|
86
|
+
);
|
|
87
|
+
&prevkeyvar=&keyvar;
|
|
88
|
+
if &lastvar then output;
|
|
89
|
+
run;
|
|
90
|
+
%end;
|
|
91
|
+
%mend mp_hashdataset;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@file
|
|
3
|
+
@brief Returns a unique hash for each file in a directory
|
|
4
|
+
@details Hashes each file in each directory, and then hashes the hashes to
|
|
5
|
+
create a hash for each directory also.
|
|
6
|
+
|
|
7
|
+
This makes use of the new `hashing_file()` and `hashing` functions, available
|
|
8
|
+
since 9.4m6. Interestingly, those functions can be used in pure macro, eg:
|
|
9
|
+
|
|
10
|
+
%put %sysfunc(hashing_file(md5,/path/to/file.blob,0));
|
|
11
|
+
|
|
12
|
+
Actual usage:
|
|
13
|
+
|
|
14
|
+
%let fpath=/some/directory;
|
|
15
|
+
|
|
16
|
+
%mp_hashdirectory(&fpath,outds=myhash,maxdepth=2)
|
|
17
|
+
|
|
18
|
+
data _null_;
|
|
19
|
+
set work.myhash;
|
|
20
|
+
put (_all_)(=);
|
|
21
|
+
run;
|
|
22
|
+
|
|
23
|
+
Whilst files are hashed in their entirety, the logic for creating a folder
|
|
24
|
+
hash is as follows:
|
|
25
|
+
|
|
26
|
+
@li Sort the files by filename (case sensitive, uppercase then lower)
|
|
27
|
+
@li Take the first 100 hashes, concatenate and hash
|
|
28
|
+
@li Concatenate this hash with another 100 hashes and hash again
|
|
29
|
+
@li Continue until the end of the folder. This is the folder hash
|
|
30
|
+
@li If a folder contains other folders, start from the bottom of the tree -
|
|
31
|
+
the folder hashes cascade upwards so you know immediately if there is a
|
|
32
|
+
change in a sub/sub directory
|
|
33
|
+
@li If a subfolder has no content (empty) then it is ignored. No hash created.
|
|
34
|
+
@li If the file is empty, it is also ignored / no hash created.
|
|
35
|
+
@li If the target directory (&inloc) is empty, &outds will also be empty
|
|
36
|
+
|
|
37
|
+
<h4> SAS Macros </h4>
|
|
38
|
+
@li mp_dirlist.sas
|
|
39
|
+
|
|
40
|
+
<h4> Related Files </h4>
|
|
41
|
+
@li mp_hashdataset.sas
|
|
42
|
+
@li mp_hashdirectory.test.sas
|
|
43
|
+
@li mp_md5.sas
|
|
44
|
+
|
|
45
|
+
@param [in] inloc Full filepath of the file to be hashed (unquoted)
|
|
46
|
+
@param [in] iftrue= (1=1) A condition under which the macro should be executed
|
|
47
|
+
@param [in] maxdepth= (0) Set to a positive integer to indicate the level of
|
|
48
|
+
subdirectory scan recursion - eg 3, to go `./3/levels/deep`. For unlimited
|
|
49
|
+
recursion, set to MAX.
|
|
50
|
+
@param [in] method= (MD5) the hashing method to use. Available options:
|
|
51
|
+
@li MD5
|
|
52
|
+
@li SH1
|
|
53
|
+
@li SHA256
|
|
54
|
+
@li SHA384
|
|
55
|
+
@li SHA512
|
|
56
|
+
@li CRC32
|
|
57
|
+
@param [out] outds= (work.mp_hashdirectory) The output dataset. Contains:
|
|
58
|
+
@li directory - the parent folder
|
|
59
|
+
@li file_hash - the hash output
|
|
60
|
+
@li hash_duration - how long the hash took (first hash always takes longer)
|
|
61
|
+
@li file_path - /full/path/to/each/file.ext
|
|
62
|
+
@li file_or_folder - contains either "file" or "folder"
|
|
63
|
+
@li level - the depth of the directory (top level is 0)
|
|
64
|
+
|
|
65
|
+
@version 9.4m6
|
|
66
|
+
@author Allan Bowe
|
|
67
|
+
**/
|
|
68
|
+
|
|
69
|
+
%macro mp_hashdirectory(inloc,
|
|
70
|
+
outds=work.mp_hashdirectory,
|
|
71
|
+
method=MD5,
|
|
72
|
+
maxdepth=0,
|
|
73
|
+
iftrue=%str(1=1)
|
|
74
|
+
)/*/STORE SOURCE*/;
|
|
75
|
+
|
|
76
|
+
%local curlevel tempds maxlevel;
|
|
77
|
+
|
|
78
|
+
%if not(%eval(%unquote(&iftrue))) %then %return;
|
|
79
|
+
|
|
80
|
+
/* get the directory listing */
|
|
81
|
+
%mp_dirlist(path=&inloc, outds=&outds, maxdepth=&maxdepth, showparent=YES)
|
|
82
|
+
|
|
83
|
+
/* create the hashes */
|
|
84
|
+
data &outds;
|
|
85
|
+
set &outds (rename=(filepath=file_path));
|
|
86
|
+
length FILE_HASH $32 HASH_DURATION 8;
|
|
87
|
+
keep directory file_hash hash_duration file_path file_or_folder level;
|
|
88
|
+
|
|
89
|
+
ts=datetime();
|
|
90
|
+
if file_or_folder='file' then do;
|
|
91
|
+
/* if file is empty, hashing_file will break - so ignore / delete */
|
|
92
|
+
length fname val $8;
|
|
93
|
+
drop fname val fid is_empty;
|
|
94
|
+
rc=filename(fname,file_path);
|
|
95
|
+
fid=fopen(fname);
|
|
96
|
+
if fid > 0 then do;
|
|
97
|
+
rc=fread(fid);
|
|
98
|
+
is_empty=fget(fid,val);
|
|
99
|
+
end;
|
|
100
|
+
rc=fclose(fid);
|
|
101
|
+
rc=filename(fname);
|
|
102
|
+
if is_empty ne 0 then delete;
|
|
103
|
+
else file_hash=hashing_file("&method",cats(file_path),0);
|
|
104
|
+
end;
|
|
105
|
+
hash_duration=datetime()-ts;
|
|
106
|
+
run;
|
|
107
|
+
|
|
108
|
+
proc sort data=&outds ;
|
|
109
|
+
by descending level directory file_path;
|
|
110
|
+
run;
|
|
111
|
+
|
|
112
|
+
%let maxlevel=0;
|
|
113
|
+
data _null_;
|
|
114
|
+
set &outds;
|
|
115
|
+
call symputx('maxlevel',level,'l');
|
|
116
|
+
stop;
|
|
117
|
+
run;
|
|
118
|
+
|
|
119
|
+
/* now hash the hashes to populate folder hashes, starting from the bottom */
|
|
120
|
+
%do curlevel=&maxlevel %to 0 %by -1;
|
|
121
|
+
data work._data_ (keep=directory file_hash);
|
|
122
|
+
set &outds;
|
|
123
|
+
where level=&curlevel;
|
|
124
|
+
by descending level directory file_path;
|
|
125
|
+
length str $32767 tmp_hash $32;
|
|
126
|
+
retain str tmp_hash ;
|
|
127
|
+
/* reset vars when starting a new directory */
|
|
128
|
+
if first.directory then do;
|
|
129
|
+
str='';
|
|
130
|
+
tmp_hash='';
|
|
131
|
+
i=0;
|
|
132
|
+
end;
|
|
133
|
+
/* hash each chunk of 100 file paths */
|
|
134
|
+
i+1;
|
|
135
|
+
str=cats(str,file_hash);
|
|
136
|
+
if mod(i,100)=0 or last.directory then do;
|
|
137
|
+
tmp_hash=hashing("&method",cats(tmp_hash,str));
|
|
138
|
+
str='';
|
|
139
|
+
end;
|
|
140
|
+
/* output the hash at directory level */
|
|
141
|
+
if last.directory then do;
|
|
142
|
+
file_hash=tmp_hash;
|
|
143
|
+
output;
|
|
144
|
+
end;
|
|
145
|
+
if last.level then stop;
|
|
146
|
+
run;
|
|
147
|
+
%let tempds=&syslast;
|
|
148
|
+
/* join the hash back into the main table */
|
|
149
|
+
proc sql undo_policy=none;
|
|
150
|
+
create table &outds as
|
|
151
|
+
select a.directory
|
|
152
|
+
,coalesce(b.file_hash,a.file_hash) as file_hash
|
|
153
|
+
,a.hash_duration
|
|
154
|
+
,a.file_path
|
|
155
|
+
,a.file_or_folder
|
|
156
|
+
,a.level
|
|
157
|
+
from &outds a
|
|
158
|
+
left join &tempds b
|
|
159
|
+
on a.file_path=b.directory
|
|
160
|
+
order by level desc, directory, file_path;
|
|
161
|
+
drop table &tempds;
|
|
162
|
+
%end;
|
|
163
|
+
|
|
164
|
+
%mend mp_hashdirectory;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@file
|
|
3
|
+
@brief Performs a wrapped \%include
|
|
4
|
+
@details This macro wrapper is necessary if you need your included code to
|
|
5
|
+
know that it is being \%included.
|
|
6
|
+
|
|
7
|
+
If you are using %include in a regular program, you could make use of the
|
|
8
|
+
following macro variables:
|
|
9
|
+
|
|
10
|
+
@li SYSINCLUDEFILEDEVICE
|
|
11
|
+
@li SYSINCLUDEFILEDIR
|
|
12
|
+
@li SYSINCLUDEFILEFILEREF
|
|
13
|
+
@li SYSINCLUDEFILENAME
|
|
14
|
+
|
|
15
|
+
However these variables are NOT available inside a macro, as documented here:
|
|
16
|
+
https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/mcrolref/n1j5tcc0n2xczyn1kg1o0606gsv9.htm
|
|
17
|
+
|
|
18
|
+
This macro can be used in place of the %include statement, and will insert
|
|
19
|
+
the following (equivalent) global variables:
|
|
20
|
+
|
|
21
|
+
@li _SYSINCLUDEFILEDEVICE
|
|
22
|
+
@li _SYSINCLUDEFILEDIR
|
|
23
|
+
@li _SYSINCLUDEFILEFILEREF
|
|
24
|
+
@li _SYSINCLUDEFILENAME
|
|
25
|
+
|
|
26
|
+
These can be used whenever testing _within a macro_. Outside of the macro,
|
|
27
|
+
the regular automatic variables will still be available (thanks to a
|
|
28
|
+
concatenated file list in the include statement).
|
|
29
|
+
|
|
30
|
+
Example usage:
|
|
31
|
+
|
|
32
|
+
filename example temp;
|
|
33
|
+
data _null_;
|
|
34
|
+
file example;
|
|
35
|
+
put '%macro test();';
|
|
36
|
+
put '%put &=_SYSINCLUDEFILEFILEREF;';
|
|
37
|
+
put '%put &=SYSINCLUDEFILEFILEREF;';
|
|
38
|
+
put '%mend; %test()';
|
|
39
|
+
put '%put &=SYSINCLUDEFILEFILEREF;';
|
|
40
|
+
run;
|
|
41
|
+
%mp_include(example)
|
|
42
|
+
|
|
43
|
+
@param [in] fileref The fileref of the file to be included. Must be provided.
|
|
44
|
+
@param [in] prefix= (_) The prefix to apply to the global variables.
|
|
45
|
+
@param [in] opts= (SOURCE2) The options to apply to the %inc statement
|
|
46
|
+
@param [in] errds= (work.mp_abort_errds) There is no clean way to end a
|
|
47
|
+
process within a %include called within a macro. Furthermore, there is no
|
|
48
|
+
way to test if a macro is called within a %include. To handle this
|
|
49
|
+
particular scenario, the %mp_abort() macro will test for the existence of
|
|
50
|
+
the `_SYSINCLUDEFILEDEVICE` variable and return the outputs (msg,mac) inside
|
|
51
|
+
this dataset.
|
|
52
|
+
It will then run an abort cancel FILE to stop the include running, and pass
|
|
53
|
+
the dataset back.
|
|
54
|
+
|
|
55
|
+
IMPORTANT NOTE - it is NOT possible to read this dataset as part of _this_
|
|
56
|
+
macro! When running abort cancel FILE, ALL macros are closed, so instead it
|
|
57
|
+
is necessary to invoke "%mp_abort(mode=INCLUDE)" OUTSIDE of macro wrappers.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@version 9.4
|
|
61
|
+
@author Allan Bowe
|
|
62
|
+
|
|
63
|
+
<h4> SAS Macros </h4>
|
|
64
|
+
@li mf_getuniquefileref.sas
|
|
65
|
+
@li mp_abort.sas
|
|
66
|
+
|
|
67
|
+
**/
|
|
68
|
+
|
|
69
|
+
%macro mp_include(fileref
|
|
70
|
+
,prefix=_
|
|
71
|
+
,opts=SOURCE2
|
|
72
|
+
,errds=work.mp_abort_errds
|
|
73
|
+
)/*/STORE SOURCE*/;
|
|
74
|
+
|
|
75
|
+
/* prepare precode */
|
|
76
|
+
%local tempref;
|
|
77
|
+
%let tempref=%mf_getuniquefileref();
|
|
78
|
+
data _null_;
|
|
79
|
+
file &tempref;
|
|
80
|
+
set sashelp.vextfl(where=(fileref="%upcase(&fileref)"));
|
|
81
|
+
put '%let _SYSINCLUDEFILEDEVICE=' xengine ';';
|
|
82
|
+
name=scan(xpath,-1,'/\');
|
|
83
|
+
put '%let _SYSINCLUDEFILENAME=' name ';';
|
|
84
|
+
path=subpad(xpath,1,length(xpath)-length(name)-1);
|
|
85
|
+
put '%let _SYSINCLUDEFILEDIR=' path ';';
|
|
86
|
+
put '%let _SYSINCLUDEFILEFILEREF=' "&fileref;";
|
|
87
|
+
run;
|
|
88
|
+
|
|
89
|
+
/* prepare the errds */
|
|
90
|
+
data &errds;
|
|
91
|
+
length msg mac $1000;
|
|
92
|
+
call missing(msg,mac);
|
|
93
|
+
iftrue='1=0';
|
|
94
|
+
run;
|
|
95
|
+
|
|
96
|
+
/* include the include */
|
|
97
|
+
%inc &tempref &fileref/&opts;
|
|
98
|
+
|
|
99
|
+
%mp_abort(iftrue= (&syscc ne 0)
|
|
100
|
+
,mac=%str(&_SYSINCLUDEFILEDIR/&_SYSINCLUDEFILENAME)
|
|
101
|
+
,msg=%str(syscc=&syscc after executing &_SYSINCLUDEFILENAME)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
filename &tempref clear;
|
|
105
|
+
|
|
106
|
+
%mend mp_include;
|