Macros for SAS Application Developers
https://github.com/sasjs/core
mp_storediffs.sas
Go to the documentation of this file.
1 /**
2  @file
3  @brief Converts deletes/changes/appends into a single audit table.
4  @details When tracking changes to data over time, it can be helpful to have
5  a single base table to track ALL modifications - enabling audit trail,
6  data recovery, and change re-application. This macro is one of many
7  data management utilities used in [Data Controller for SAS](
8  https:datacontroller.io) - a comprehensive data ingestion solution, which
9  works on any SAS platform (Viya, SAS 9, Foundation).
10 
11  NOTE - this macro does not validate the inputs. It is assumed that the
12  datasets containing the new / changed / deleted rows are CORRECT, contain
13  no additional (or missing columns), and that the originals dataset contains
14  all relevant base records (and no additionals).
15 
16  Usage:
17 
18  data work.orig work.deleted work.changed work.appended;
19  set sashelp.class;
20  if _n_=1 then do;
21  output work.orig work.deleted;
22  end;
23  else if _n_=2 then do;
24  output work.orig;
25  age=99;
26  output work.changed;
27  end;
28  else do;
29  name='Newbie';
30  output work.appended;
31  stop;
32  end;
33  run;
34 
35  %mp_storediffs(sashelp.class,work.orig,NAME
36  ,delds=work.deleted
37  ,modds=work.changed
38  ,appds=work.appended
39  ,outds=work.final
40  ,mdebug=1
41  )
42 
43  @param [in] libds Target table against which the changes were applied
44  @param [in] origds Dataset with original (unchanged) records. Can be empty if
45  only appending.
46  @param [in] key Space seperated list of key variables
47  @param [in] delds= (0) Dataset with deleted records
48  @param [in] appds= (0) Dataset with appended records
49  @param [in] modds= (0) Dataset with modified records
50  @param [out] outds= (work.mp_storediffs) Output table containing stored data.
51  DDL as follows: %mp_coretable(DIFFTABLE)
52 
53  @param [in] processed_dttm= (0) Provide a datetime constant in relation to
54  the actual load time. If not provided, current timestamp is used.
55  @param [in] mdebug= set to 1 to enable DEBUG messages and preserve outputs
56  @param [out] loadref= (0) Provide a unique key to reference the load,
57  otherwise a UUID will be generated.
58 
59  <h4> SAS Macros </h4>
60  @li mf_getquotedstr.sas
61  @li mf_getuniquename.sas
62  @li mf_getvarlist.sas
63 
64  <h4> Related Macros </h4>
65  @li mp_stackdiffs.sas
66  @li mp_storediffs.test.sas
67 
68  @version 9.2
69  @author Allan Bowe
70 **/
71 /** @cond */
72 
73 %macro mp_storediffs(libds
74  ,origds
75  ,key
76  ,delds=0
77  ,appds=0
78  ,modds=0
79  ,outds=work.mp_storediffs
80  ,loadref=0
81  ,processed_dttm=0
82  ,mdebug=0
83 )/*/STORE SOURCE*/;
84 %local dbg;
85 %if &mdebug=1 %then %do;
86  %put &sysmacroname entry vars:;
87  %put _local_;
88 %end;
89 %else %let dbg=*;
90 
91 /* set up unique and temporary vars */
92 %local ds1 ds2 ds3 ds4 hashkey inds_auto inds_keep dslist vlist;
93 %let ds1=%upcase(work.%mf_getuniquename(prefix=mpsd_ds1));
94 %let ds2=%upcase(work.%mf_getuniquename(prefix=mpsd_ds2));
95 %let ds3=%upcase(work.%mf_getuniquename(prefix=mpsd_ds3));
96 %let ds4=%upcase(work.%mf_getuniquename(prefix=mpsd_ds4));
97 %let hashkey=%upcase(%mf_getuniquename(prefix=mpsd_hashkey));
98 %let inds_auto=%upcase(%mf_getuniquename(prefix=mpsd_inds_auto));
99 %let inds_keep=%upcase(%mf_getuniquename(prefix=mpsd_inds_keep));
100 
101 %let dslist=&origds;
102 %if &delds ne 0 %then %do;
103  %let delds=%upcase(&delds);
104  %if %scan(&delds,-1,.)=&delds %then %let delds=WORK.&delds;
105  %let dslist=&dslist &delds;
106 %end;
107 %if &appds ne 0 %then %do;
108  %let appds=%upcase(&appds);
109  %if %scan(&appds,-1,.)=&appds %then %let appds=WORK.&appds;
110  %let dslist=&dslist &appds;
111 %end;
112 %if &modds ne 0 %then %do;
113  %let modds=%upcase(&modds);
114  %if %scan(&modds,-1,.)=&modds %then %let modds=WORK.&modds;
115  %let dslist=&dslist &modds;
116 %end;
117 
118 %let origds=%upcase(&origds);
119 %if %scan(&origds,-1,.)=&origds %then %let origds=WORK.&origds;
120 
121 %let key=%upcase(&key);
122 
123 /* hash the key and append all the tables (marking the source) */
124 data &ds1;
125  set &dslist indsname=&inds_auto;
126  &hashkey=put(md5(catx('|',%mf_getquotedstr(&key,quote=N))),$hex32.);
127  &inds_keep=upcase(&inds_auto);
128 proc sort;
129  by &inds_keep &hashkey;
130 run;
131 
132 /* transpose numeric & char vars */
133 proc transpose data=&ds1
134  out=&ds2(rename=(&hashkey=key_hash _name_=tgtvar_nm col1=newval_num));
135  by &inds_keep &hashkey;
136  var _numeric_;
137 run;
138 proc transpose data=&ds1
139  out=&ds3(
140  rename=(&hashkey=key_hash _name_=tgtvar_nm col1=newval_char)
141  where=(tgtvar_nm not in ("&hashkey","&inds_keep"))
142  );
143  by &inds_keep &hashkey;
144  var _character_;
145 run;
146 
147 %if %index(&libds,-)>0 and %scan(&libds,2,-)=FC %then %do;
148  /* this is a format catalog - cannot query cols directly */
149  %let vlist="TYPE","FMTNAME","FMTROW","START","END","LABEL","MIN","MAX"
150  ,"DEFAULT","LENGTH","FUZZ","PREFIX","MULT","FILL","NOEDIT","SEXCL"
151  ,"EEXCL","HLO","DECSEP","DIG3SEP","DATATYPE","LANGUAGE";
152 %end;
153 %else %let vlist=%mf_getvarlist(&libds,dlm=%str(,),quote=DOUBLE);
154 
155 data &ds4;
156  length &inds_keep $41 tgtvar_nm $32 _label_ $256;
157  if _n_=1 then call missing(_label_);
158  drop _label_;
159  set &ds2 &ds3 indsname=&inds_auto;
160 
161  tgtvar_nm=upcase(tgtvar_nm);
162  if tgtvar_nm in (%upcase(&vlist));
163 
164  if upcase(&inds_auto)="&ds2" then tgtvar_type='N';
165  else if upcase(&inds_auto)="&ds3" then tgtvar_type='C';
166  else do;
167  putlog 'ERR' +(-1) "OR: unidentified vartype input!" &inds_auto;
168  call symputx('syscc',98);
169  end;
170 
171  if &inds_keep="&appds" then move_type='A';
172  else if &inds_keep="&delds" then move_type='D';
173  else if &inds_keep="&modds" then move_type='M';
174  else if &inds_keep="&origds" then move_type='O';
175  else do;
176  putlog 'ERR' +(-1) "OR: unidentified movetype input!" &inds_keep;
177  call symputx('syscc',99);
178  end;
179  tgtvar_nm=upcase(tgtvar_nm);
180  if tgtvar_nm in (%mf_getquotedstr(&key)) then is_pk=1;
181  else is_pk=0;
182  drop &inds_keep;
183 run;
184 
185 %if "&loadref"="0" %then %let loadref=%sysfunc(uuidgen());
186 %if &processed_dttm=0 %then %let processed_dttm=%sysfunc(datetime());
187 %let libds=%upcase(&libds);
188 
189 /* join orig vals for modified & deleted */
190 proc sql;
191 create table &outds as
192  select "&loadref" as load_ref length=36
193  ,&processed_dttm as processed_dttm format=E8601DT26.6
194  ,"%scan(&libds,1,.)" as libref length=8
195  ,"%scan(&libds,2,.)" as dsn length=32
196  ,b.key_hash length=32
197  ,b.move_type length=1
198  ,b.tgtvar_nm length=32
199  ,b.is_pk
200  ,case when b.move_type ne 'M' then -1
201  when a.newval_num=b.newval_num and a.newval_char=b.newval_char then 0
202  else 1
203  end as is_diff
204  ,b.tgtvar_type length=1
205  ,case when b.move_type='D' then b.newval_num
206  else a.newval_num
207  end as oldval_num format=best32.
208  ,case when b.move_type='D' then .
209  else b.newval_num
210  end as newval_num format=best32.
211  ,case when b.move_type='D' then b.newval_char
212  else a.newval_char
213  end as oldval_char length=32765
214  ,case when b.move_type='D' then ''
215  else b.newval_char
216  end as newval_char length=32765
217  from &ds4(where=(move_type='O')) as a
218  right join &ds4(where=(move_type ne 'O')) as b
219  on a.tgtvar_nm=b.tgtvar_nm
220  and a.key_hash=b.key_hash
221  order by move_type, key_hash,is_pk desc, tgtvar_nm;
222 
223 %if &mdebug=0 %then %do;
224  proc sql;
225  drop table &ds1, &ds2, &ds3, &ds4;
226 %end;
227 
228 %mend mp_storediffs;
229 /** @endcond */