mp_loadformat.sas
Go to the documentation of this file.
1 /**
2  @file
3  @brief Loads a format catalog from a staging dataset
4  @details When loading staged data, it is common to receive only the records
5  that have actually changed. However, when loading a format catalog, if
6  records are missing they are presumed to be no longer required.
7 
8  This macro will augment a staging dataset with other records from the same
9  format, to prevent loss of data - UNLESS the input dataset contains a marker
10  column, specifying that a particular row needs to be deleted (`delete_col=`).
11 
12  Positions of formats are made using the FMTROW variable - this must be present
13  and unique (on TYPE / FMTNAME / FMTROW).
14 
15  This macro can also be used to identify which records would be (or were)
16  considered new, modified or deleted (`loadtarget=`) by creating the following
17  tables:
18 
19  @li work.outds_add
20  @li work.outds_del
21  @li work.outds_mod
22 
23  For example usage, see test (under Related Macros)
24 
25  @param [in] libcat The format catalog to be loaded
26  @param [in] libds The staging table to load
27  @param [in] loadtarget= (NO) Set to YES to actually load the target catalog
28  @param [in] delete_col= (_____DELETE__THIS__RECORD_____) The column used to
29  mark a record for deletion. Values should be "Yes" or "No".
30  @param [out] auditlibds= (0) For change tracking, set to the libds of an audit
31  table as defined in mddl_dc_difftable.sas
32  @param [in] locklibds= (0) For multi-user (parallel) situations, set to the
33  libds of the DC lock table as defined in the mddl_dc_locktable.sas macro.
34  @param [out] outds_add= (0) Set a libds here to see the new records added
35  @param [out] outds_del= (0) Set a libds here to see the records deleted
36  @param [out] outds_mod= (0) Set a libds here to see the modified records
37  @param [in] mdebug= (0) Set to 1 to enable DEBUG messages and preserve outputs
38 
39  <h4> SAS Macros </h4>
40  @li mf_existds.sas
41  @li mf_existvar.sas
42  @li mf_getuniquename.sas
43  @li mf_nobs.sas
44  @li mp_abort.sas
45  @li mp_aligndecimal.sas
46  @li mp_cntlout.sas
47  @li mp_lockanytable.sas
48  @li mp_md5.sas
49  @li mp_storediffs.sas
50 
51  <h4> Related Macros </h4>
52  @li mddl_dc_difftable.sas
53  @li mddl_dc_locktable.sas
54  @li mp_loadformat.test.1.sas
55  @li mp_loadformat.test.2.sas
56  @li mp_lockanytable.sas
57  @li mp_stackdiffs.sas
58 
59 
60  @version 9.2
61  @author Allan Bowe
62 
63 **/
64 
65 %macro mp_loadformat(libcat,libds
66  ,loadtarget=NO
67  ,auditlibds=0
68  ,locklibds=0
69  ,delete_col=_____DELETE__THIS__RECORD_____
70  ,outds_add=0
71  ,outds_del=0
72  ,outds_mod=0
73  ,mdebug=0
74 );
75 /* set up local macro variables and temporary tables (with a prefix) */
76 %local err msg prefix dslist i var fmtlist ibufsize;
77 %let dslist=base_fmts template inlibds ds1 stagedata storediffs del1 del2;
78 %if &outds_add=0 %then %let dslist=&dslist outds_add;
79 %if &outds_del=0 %then %let dslist=&dslist outds_del;
80 %if &outds_mod=0 %then %let dslist=&dslist outds_mod;
81 %let prefix=%substr(%mf_getuniquename(),1,21);
82 %do i=1 %to %sysfunc(countw(&dslist));
83  %let var=%scan(&dslist,&i);
84  %local &var;
85  %let &var=%upcase(&prefix._&var);
86 %end;
87 
88 /* in DC, format catalogs maybe specified in the libds with a -FC extension */
89 %let libcat=%scan(&libcat,1,-);
90 
91 /* perform input validations */
92 %mp_abort(
93  iftrue=(%mf_existds(&libds)=0)
94  ,mac=&sysmacroname
95  ,msg=%str(&libds could not be found)
96 )
97 %mp_abort(
98  iftrue=(%mf_existvar(&libds,FMTROW)=0)
99  ,mac=&sysmacroname
100  ,msg=%str(FMTROW not found in &libds)
101 )
102 %let err=0;
103 %let msg=0;
104 data _null_;
105  if _n_=1 then putlog "&sysmacroname entry vars:";
106  set sashelp.vmacro;
107  where scope="&sysmacroname";
108  value=upcase(value);
109  if &mdebug=0 then put name '=' value;
110  if name=:'LOAD' and value not in ('YES','NO') then do;
111  call symputx('msg',"invalid value for "!!name!!":"!!value);
112  call symputx('err',1);
113  stop;
114  end;
115  else if name='LIBCAT' then do;
116  if exist(value,'CATALOG') le 0 then do;
117  call symputx('msg',"Unable to open catalog: "!!value);
118  call symputx('err',1);
119  stop;
120  end;
121  end;
122  else if (name=:'OUTDS' or name in ('DELETE_COL','LOCKLIBDS','AUDITLIBDS'))
123  and missing(value) then do;
124  call symputx('msg',"missing value in var: "!!name);
125  call symputx('err',1);
126  stop;
127  end;
128 run;
129 data _null_;
130  set &libds;
131  if missing(fmtrow) then do;
132  call symputx('msg',"missing fmtrow in format: "!!FMTNAME);
133  call symputx('err',1);
134  stop;
135  end;
136 run;
137 
138 %mp_abort(
139  iftrue=(&err ne 0)
140  ,mac=&sysmacroname
141  ,msg=%str(&msg)
142 )
143 
144 %local cnt;
145 proc sql noprint;
146 select count(distinct catx('|',type,fmtname,fmtrow)) into: cnt from &libds;
147 %mp_abort(
148  iftrue=(&cnt ne %mf_nobs(&libds))
149  ,mac=&sysmacroname
150  ,msg=%str(Non-unique primary key on &libds)
151 )
152 
153 /**
154  * First, extract only relevant formats from the catalog
155  */
156 proc sql noprint;
157 select distinct
158  case
159  when type='N' then upcase(fmtname)
160  when type='C' then cats('$',upcase(fmtname))
161  when type='I' then cats('@',upcase(fmtname))
162  when type='J' then cats('@$',upcase(fmtname))
163  else "&sysmacroname:UNHANDLED"
164  end
165  into: fmtlist separated by ' '
166  from &libds;
167 
168 %mp_cntlout(libcat=&libcat,fmtlist=&fmtlist,cntlout=&base_fmts)
169 
170 /* get a hash of the row */
171 %local cvars nvars;
172 %let cvars=TYPE FMTNAME START END LABEL PREFIX FILL SEXCL EEXCL HLO DECSEP
173  DIG3SEP DATATYPE LANGUAGE;
174 %let nvars=FMTROW MIN MAX DEFAULT LENGTH FUZZ MULT NOEDIT;
175 data &base_fmts/note2err;
176  set &base_fmts;
177  fmthash=%mp_md5(cvars=&cvars, nvars=&nvars);
178 run;
179 
180 /**
181  * Ensure input table and base_formats have consistent lengths and types
182  */
183 data &inlibds/nonote2err;
184  length &delete_col $3 FMTROW 8 start end label $32767;
185  if 0 then set &base_fmts;
186  set &libds;
187  by type fmtname notsorted;
188  if &delete_col='' then &delete_col='No';
189  fmtname=upcase(fmtname);
190  type=upcase(type);
191  if missing(type) then do;
192  if substr(fmtname,1,1)='@' then do;
193  if substr(fmtname,2,1)='$' then type='J';
194  else type='I';
195  end;
196  else do;
197  if substr(fmtname,1,1)='$' then type='C';
198  else type='N';
199  end;
200  end;
201  if type in ('N','I') then do;
202  %mp_aligndecimal(start,width=16)
203  %mp_aligndecimal(end,width=16)
204  end;
205 
206  fmthash=%mp_md5(cvars=&cvars, nvars=&nvars);
207 run;
208 
209 /**
210  * Identify new records
211  */
212 proc sql;
213 create table &outds_add(drop=&delete_col) as
214  select a.*
215  from &inlibds a
216  left join &base_fmts b
217  on a.type=b.type and a.fmtname=b.fmtname and a.fmtrow=b.fmtrow
218  where b.fmtname is null
219  and upcase(a.&delete_col) ne "YES"
220  order by type, fmtname, fmtrow;
221 
222 /**
223  * Identify modified records
224  */
225 create table &outds_mod (drop=&delete_col) as
226  select a.*
227  from &inlibds a
228  inner join &base_fmts b
229  on a.type=b.type and a.fmtname=b.fmtname and a.fmtrow=b.fmtrow
230  where upcase(a.&delete_col) ne "YES"
231  and a.fmthash ne b.fmthash
232  order by type, fmtname, fmtrow;
233 
234 /**
235  * Identify deleted records
236  */
237 create table &outds_del(drop=&delete_col) as
238  select a.*
239  from &inlibds a
240  inner join &base_fmts b
241  on a.type=b.type and a.fmtname=b.fmtname and a.fmtrow=b.fmtrow
242  where upcase(a.&delete_col)="YES"
243  order by type, fmtname, fmtrow;
244 
245 /**
246  * Identify fully deleted formats (where every record is removed)
247  * These require to be explicitly deleted in proc format
248  * del1 - identify _partial_ deletes
249  * del2 - exclude these, and also formats that come with _additions_
250  */
251 create table &del1 as
252  select a.*
253  from &base_fmts a
254  left join &outds_del b
255  on a.type=b.type and a.fmtname=b.fmtname and a.fmtrow=b.fmtrow
256  where b.fmtrow is null;
257 
258 create table &del2 as
259  select * from &outds_del
260  where cats(type,fmtname) not in (select cats(type,fmtname) from &outds_add)
261  and cats(type,fmtname) not in (select cats(type,fmtname) from &del1);
262 
263 
264 %mp_abort(
265  iftrue=(&syscc ne 0)
266  ,mac=&sysmacroname
267  ,msg=%str(SYSCC=&syscc prior to load prep)
268 )
269 
270 %if &loadtarget=YES %then %do;
271  /* new records plus base records that are not deleted or modified */
272  data &ds1;
273  merge &base_fmts(in=base)
274  &outds_mod(in=mod)
275  &outds_add(in=add)
276  &outds_del(in=del);
277  if not del and not mod;
278  by type fmtname fmtrow;
279  run;
280  /* add back the modified records */
281  data &stagedata;
282  set &ds1 &outds_mod;
283  run;
284  proc sort;
285  by type fmtname fmtrow;
286  run;
287 %end;
288 /* mp abort needs to run outside of conditional blocks */
289 %mp_abort(
290  iftrue=(&syscc ne 0)
291  ,mac=&sysmacroname
292  ,msg=%str(SYSCC=&syscc prior to actual load)
293 )
294 %if &loadtarget=YES %then %do;
295  %if %mf_nobs(&stagedata)=0 and %mf_nobs(&del2)=0 %then %do;
296  %put There are no changes to load in &libcat!;
297  %return;
298  %end;
299  %if &locklibds ne 0 %then %do;
300  /* prevent parallel updates */
301  %mp_lockanytable(LOCK
302  ,lib=%scan(&libcat,1,.)
303  ,ds=%scan(&libcat,2,.)-FC
304  ,ref=MP_LOADFORMAT commencing format load
305  ,ctl_ds=&locklibds
306  )
307  %end;
308  /* do the actual load */
309  proc format lib=&libcat cntlin=&stagedata;
310  run;
311  /* apply any full deletes */
312  %if %mf_nobs(&del2)>0 %then %do;
313  %local delfmtlist;
314  proc sql noprint;
315  select distinct case when type='N' then cats(fmtname,'.FORMAT')
316  when type='C' then cats(fmtname,'.FORMATC')
317  when type='J' then cats(fmtname,'.INFMTC')
318  when type='I' then cats(fmtname,'.INFMT')
319  else cats(fmtname,'.BADENTRY!!!') end
320  into: delfmtlist
321  separated by ' '
322  from &del2;
323  proc catalog catalog=&libcat;
324  delete &delfmtlist;
325  quit;
326  %end;
327  %if &locklibds ne 0 %then %do;
328  /* unlock the table */
329  %mp_lockanytable(UNLOCK
330  ,lib=%scan(&libcat,1,.)
331  ,ds=%scan(&libcat,2,.)-FC
332  ,ref=MP_LOADFORMAT completed format load
333  ,ctl_ds=&locklibds
334  )
335  %end;
336  /* track the changes */
337  %if &auditlibds ne 0 %then %do;
338  %if &locklibds ne 0 %then %do;
339  %mp_lockanytable(LOCK
340  ,lib=%scan(&auditlibds,1,.)
341  ,ds=%scan(&auditlibds,2,.)
342  ,ref=MP_LOADFORMAT commencing audit table load
343  ,ctl_ds=&locklibds
344  )
345  %end;
346 
347  %mp_storediffs(&libcat-FC
348  ,&base_fmts
349  ,TYPE FMTNAME FMTROW
350  ,delds=&outds_del
351  ,modds=&outds_mod
352  ,appds=&outds_add
353  ,outds=&storediffs
354  ,mdebug=&mdebug
355  )
356 
357  proc append base=&auditlibds data=&storediffs;
358  run;
359 
360  %if &locklibds ne 0 %then %do;
361  %mp_lockanytable(UNLOCK
362  ,lib=%scan(&auditlibds,1,.)
363  ,ds=%scan(&auditlibds,2,.)
364  ,ref=MP_LOADFORMAT commencing audit table load
365  ,ctl_ds=&locklibds
366  )
367  %end;
368  %end;
369 %end;
370 %mp_abort(
371  iftrue=(&syscc ne 0)
372  ,mac=&sysmacroname
373  ,msg=%str(SYSCC=&syscc after load)
374 )
375 
376 %if &mdebug=0 %then %do;
377  proc datasets lib=work;
378  delete &prefix:;
379  run;
380  %put &sysmacroname exit vars:;
381  %put _local_;
382 %end;
383 %mend mp_loadformat;