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