From 87acfc26d13f0fbd5523803fcc38b3f77804b7e4 Mon Sep 17 00:00:00 2001 From: bart Date: Thu, 16 May 2024 19:27:11 +0200 Subject: [PATCH] FEAT: Releasing 4.52.3 --- .github/workflows/release.yml | 4 +- {005_macros => 001_macros}/mf_abort.sas | 0 {005_macros => 001_macros}/mf_dedup.sas | 0 {005_macros => 001_macros}/mf_deletefile.sas | 0 {005_macros => 001_macros}/mf_existds.sas | 0 .../mf_existfeature.sas | 0 .../mf_existfileref.sas | 0 .../mf_existfunction.sas | 0 {005_macros => 001_macros}/mf_existvar.sas | 0 .../mf_existvarlist.sas | 0 {005_macros => 001_macros}/mf_fmtdttm.sas | 0 {005_macros => 001_macros}/mf_getapploc.sas | 0 {005_macros => 001_macros}/mf_getattrc.sas | 0 {005_macros => 001_macros}/mf_getattrn.sas | 0 {005_macros => 001_macros}/mf_getengine.sas | 0 {005_macros => 001_macros}/mf_getfilesize.sas | 0 {005_macros => 001_macros}/mf_getfmtlist.sas | 0 {005_macros => 001_macros}/mf_getfmtname.sas | 0 .../mf_getgitbranch.sas | 0 {005_macros => 001_macros}/mf_getkeyvalue.sas | 0 {005_macros => 001_macros}/mf_getplatform.sas | 0 .../mf_getquotedstr.sas | 0 {005_macros => 001_macros}/mf_getschema.sas | 0 .../mf_getuniquefileref.sas | 0 .../mf_getuniquelibref.sas | 0 .../mf_getuniquename.sas | 0 {005_macros => 001_macros}/mf_getuser.sas | 0 {005_macros => 001_macros}/mf_getvalue.sas | 0 {005_macros => 001_macros}/mf_getvarcount.sas | 0 .../mf_getvarformat.sas | 0 {005_macros => 001_macros}/mf_getvarlen.sas | 0 {005_macros => 001_macros}/mf_getvarlist.sas | 0 {005_macros => 001_macros}/mf_getvarnum.sas | 0 {005_macros => 001_macros}/mf_getvartype.sas | 0 {005_macros => 001_macros}/mf_getxengine.sas | 0 {005_macros => 001_macros}/mf_increment.sas | 0 {009_macros => 001_macros}/mf_isblank.sas | 0 {005_macros => 001_macros}/mf_isdir.sas | 0 {005_macros => 001_macros}/mf_isint.sas | 0 {005_macros => 001_macros}/mf_islibds.sas | 0 {005_macros => 001_macros}/mf_loc.sas | 0 {005_macros => 001_macros}/mf_mkdir.sas | 0 {005_macros => 001_macros}/mf_mval.sas | 0 {005_macros => 001_macros}/mf_nobs.sas | 0 {005_macros => 001_macros}/mf_readfile.sas | 0 {005_macros => 001_macros}/mf_trimstr.sas | 0 {005_macros => 001_macros}/mf_uid.sas | 0 .../mf_verifymacvars.sas | 0 .../mf_wordsinstr1andstr2.sas | 0 .../mf_wordsinstr1butnotstr2.sas | 0 {005_macros => 001_macros}/mf_writefile.sas | 0 {005_macros => 001_macros}/mp_abort.sas | 0 .../mp_aligndecimal.sas | 0 {005_macros => 001_macros}/mp_appendfile.sas | 0 .../mp_applyformats.sas | 0 {005_macros => 001_macros}/mp_assert.sas | 0 {005_macros => 001_macros}/mp_assertcols.sas | 0 .../mp_assertcolvals.sas | 0 {005_macros => 001_macros}/mp_assertdsobs.sas | 0 {005_macros => 001_macros}/mp_assertscope.sas | 0 {005_macros => 001_macros}/mp_base64copy.sas | 0 {005_macros => 001_macros}/mp_binarycopy.sas | 0 {005_macros => 001_macros}/mp_chop.sas | 0 {005_macros => 001_macros}/mp_cleancsv.sas | 0 {009_macros => 001_macros}/mp_cntlout.sas | 0 {005_macros => 001_macros}/mp_copyfolder.sas | 0 {005_macros => 001_macros}/mp_coretable.sas | 0 .../mp_createconstraints.sas | 0 .../mp_createwebservice.sas | 0 {005_macros => 001_macros}/mp_csv2ds.sas | 0 .../mp_deleteconstraints.sas | 0 .../mp_deletefolder.sas | 0 {005_macros => 001_macros}/mp_dictionary.sas | 0 {009_macros => 001_macros}/mp_dirlist.sas | 0 .../mp_distinctfmtvalues.sas | 0 {005_macros => 001_macros}/mp_dropmembers.sas | 0 {005_macros => 001_macros}/mp_ds2cards.sas | 0 {009_macros => 001_macros}/mp_ds2csv.sas | 0 {005_macros => 001_macros}/mp_ds2ddl.sas | 0 {005_macros => 001_macros}/mp_ds2fmtds.sas | 0 {005_macros => 001_macros}/mp_ds2inserts.sas | 0 {005_macros => 001_macros}/mp_ds2md.sas | 0 {005_macros => 001_macros}/mp_ds2squeeze.sas | 0 {005_macros => 001_macros}/mp_dsmeta.sas | 0 {009_macros => 001_macros}/mp_filtercheck.sas | 0 .../mp_filtergenerate.sas | 0 {005_macros => 001_macros}/mp_filterstore.sas | 0 .../mp_filtervalidate.sas | 0 {005_macros => 001_macros}/mp_getcols.sas | 0 .../mp_getconstraints.sas | 0 {005_macros => 001_macros}/mp_getdbml.sas | 0 {005_macros => 001_macros}/mp_getddl.sas | 0 {005_macros => 001_macros}/mp_getformats.sas | 0 .../mp_getmaxvarlengths.sas | 0 {005_macros => 001_macros}/mp_getpk.sas | 0 {005_macros => 001_macros}/mp_gitadd.sas | 0 {005_macros => 001_macros}/mp_gitlog.sas | 0 .../mp_gitreleaseinfo.sas | 0 {005_macros => 001_macros}/mp_gitstatus.sas | 0 {005_macros => 001_macros}/mp_gsubfile.sas | 0 {005_macros => 001_macros}/mp_guesspk.sas | 0 {005_macros => 001_macros}/mp_hashdataset.sas | 0 .../mp_hashdirectory.sas | 0 {005_macros => 001_macros}/mp_include.sas | 0 {005_macros => 001_macros}/mp_init.sas | 0 {005_macros => 001_macros}/mp_jsonout.sas | 0 {005_macros => 001_macros}/mp_lib2cards.sas | 0 {005_macros => 001_macros}/mp_lib2inserts.sas | 0 {009_macros => 001_macros}/mp_loadformat.sas | 0 .../mp_lockanytable.sas | 0 .../mp_lockfilecheck.sas | 0 {005_macros => 001_macros}/mp_makedata.sas | 0 {005_macros => 001_macros}/mp_md5.sas | 0 {005_macros => 001_macros}/mp_perflog.sas | 0 {005_macros => 001_macros}/mp_prevobs.sas | 0 .../mp_recursivejoin.sas | 0 {005_macros => 001_macros}/mp_replace.sas | 0 {005_macros => 001_macros}/mp_reseterror.sas | 0 {005_macros => 001_macros}/mp_resetoption.sas | 0 {005_macros => 001_macros}/mp_retainedkey.sas | 0 {005_macros => 001_macros}/mp_runddl.sas | 0 {005_macros => 001_macros}/mp_searchcols.sas | 0 {005_macros => 001_macros}/mp_searchdata.sas | 0 {005_macros => 001_macros}/mp_setkeyvalue.sas | 0 {005_macros => 001_macros}/mp_sortinplace.sas | 0 {009_macros => 001_macros}/mp_stackdiffs.sas | 0 {009_macros => 001_macros}/mp_storediffs.sas | 0 {005_macros => 001_macros}/mp_stprequests.sas | 0 {005_macros => 001_macros}/mp_streamfile.sas | 0 {009_macros => 001_macros}/mp_stripdiffs.sas | 0 {005_macros => 001_macros}/mp_testjob.sas | 0 {005_macros => 001_macros}/mp_testservice.sas | 0 .../mp_testwritespeedlibrary.sas | 0 {005_macros => 001_macros}/mp_tree.sas | 0 {005_macros => 001_macros}/mp_unzip.sas | 0 .../mp_updatevarlength.sas | 0 {009_macros => 001_macros}/mp_validatecol.sas | 0 {005_macros => 001_macros}/mp_wait4file.sas | 0 {005_macros => 001_macros}/mp_webin.sas | 0 {005_macros => 001_macros}/mp_zip.sas | 0 .../mddl_dc_difftable.sas | 0 .../mddl_dc_filterdetail.sas | 0 .../mddl_dc_filtersummary.sas | 0 .../mddl_dc_locktable.sas | 0 .../mddl_dc_maxkeytable.sas | 0 .../mddl_sas_cntlout.sas | 0 002_macros/mm_assigndirectlib.sas | 461 --------- 002_macros/mm_createstp.sas | 403 -------- 002_macros/mm_getgroups.sas | 95 -- {001_macros => 003_macros}/mcf_getfmttype.sas | 0 {001_macros => 003_macros}/mcf_init.sas | 0 {001_macros => 003_macros}/mcf_length.sas | 0 .../mcf_string2file.sas | 0 003_macros/mfv_existfile.sas | 52 - 003_macros/mfv_existfolder.sas | 48 - 003_macros/mfv_existsashdat.sas | 60 -- 003_macros/mm_adduser2group.sas | 102 -- 003_macros/mm_assignlib.sas | 97 -- 003_macros/mm_createapplication.sas | 161 --- 003_macros/mm_createdataset.sas | 91 -- 003_macros/mm_createdocument.sas | 128 --- 003_macros/mm_createfolder.sas | 161 --- 003_macros/mm_createlibrary.sas | 326 ------ 003_macros/mm_createwebservice.sas | 711 ------------- 003_macros/mm_deletedocument.sas | 71 -- 003_macros/mm_deletelibrary.sas | 93 -- 003_macros/mm_deletestp.sas | 70 -- 003_macros/mm_getauthinfo.sas | 119 --- 003_macros/mm_getcols.sas | 53 - 003_macros/mm_getdetails.sas | 68 -- 003_macros/mm_getdirectories.sas | 58 -- 003_macros/mm_getdocument.sas | 146 --- 003_macros/mm_getfoldermembers.sas | 96 -- 003_macros/mm_getfoldertree.sas | 89 -- 003_macros/mm_getgroupmembers.sas | 79 -- 003_macros/mm_getlibmetadiffs.sas | 128 --- 003_macros/mm_getlibs.sas | 97 -- 003_macros/mm_getobjects.sas | 70 -- 003_macros/mm_getpublictypes.sas | 88 -- 003_macros/mm_getrepos.sas | 133 --- 003_macros/mm_getroles.sas | 74 -- 003_macros/mm_getservercontexts.sas | 92 -- 003_macros/mm_getstpcode.sas | 170 --- 003_macros/mm_getstpinfo.sas | 67 -- 003_macros/mm_getstps.sas | 110 -- 003_macros/mm_gettableid.sas | 67 -- 003_macros/mm_gettables.sas | 120 --- 003_macros/mm_gettree.sas | 68 -- 003_macros/mm_gettypes.sas | 76 -- 003_macros/mm_getusers.sas | 96 -- 003_macros/mm_getwebappsrvprops.sas | 135 --- 003_macros/mm_spkexport.sas | 146 --- 003_macros/mm_tree.sas | 173 ---- 003_macros/mm_updateappextension.sas | 134 --- 003_macros/mm_updatedocument.sas | 116 --- 003_macros/mm_updatestpservertype.sas | 67 -- 003_macros/mm_updatestpsourcecode.sas | 133 --- 003_macros/mm_webout.sas | 251 ----- 003_macros/mv_createfile.sas | 129 --- 003_macros/mv_createfolder.sas | 184 ---- 003_macros/mv_createjob.sas | 355 ------- 003_macros/mv_createwebservice.sas | 966 ------------------ 003_macros/mv_deletefoldermember.sas | 154 --- 003_macros/mv_deletejes.sas | 151 --- 003_macros/mv_deleteviyafolder.sas | 154 --- 003_macros/mv_getclients.sas | 99 -- 003_macros/mv_getfoldermembers.sas | 140 --- 003_macros/mv_getgroupmembers.sas | 109 -- 003_macros/mv_getgroups.sas | 86 -- 003_macros/mv_getjobcode.sas | 230 ----- 003_macros/mv_getjoblog.sas | 290 ------ 003_macros/mv_getjobresult.sas | 232 ----- 003_macros/mv_getjobstate.sas | 171 ---- 003_macros/mv_getusergroups.sas | 95 -- 003_macros/mv_getusers.sas | 118 --- 003_macros/mv_jobexecute.sas | 188 ---- 003_macros/mv_jobflow.sas | 395 ------- 003_macros/mv_jobwaitfor.sas | 263 ----- 003_macros/mv_registerclient.sas | 287 ------ 003_macros/mv_tokenauth.sas | 149 --- 003_macros/mv_tokenrefresh.sas | 134 --- 003_macros/mv_webout.sas | 271 ----- {002_macros => 004_macros}/ml_gsubfile.sas | 0 {002_macros => 004_macros}/ml_json.sas | 0 005_macros/mf_isblank.sas | 27 - 005_macros/mfs_httpheader.sas | 53 - .../mm_adduser2group.sas | 0 .../mm_assigndirectlib.sas | 0 {002_macros => 005_macros}/mm_assignlib.sas | 0 .../mm_createapplication.sas | 0 .../mm_createdataset.sas | 0 .../mm_createdocument.sas | 0 .../mm_createfolder.sas | 0 .../mm_createlibrary.sas | 0 {003_macros => 005_macros}/mm_createstp.sas | 0 .../mm_createwebservice.sas | 0 .../mm_deletedocument.sas | 0 .../mm_deletelibrary.sas | 0 {002_macros => 005_macros}/mm_deletestp.sas | 0 {002_macros => 005_macros}/mm_getauthinfo.sas | 0 {002_macros => 005_macros}/mm_getcols.sas | 0 {002_macros => 005_macros}/mm_getdetails.sas | 0 .../mm_getdirectories.sas | 0 {002_macros => 005_macros}/mm_getdocument.sas | 0 .../mm_getfoldermembers.sas | 0 .../mm_getfoldertree.sas | 0 .../mm_getgroupmembers.sas | 0 {003_macros => 005_macros}/mm_getgroups.sas | 0 .../mm_getlibmetadiffs.sas | 0 {002_macros => 005_macros}/mm_getlibs.sas | 0 {002_macros => 005_macros}/mm_getobjects.sas | 0 .../mm_getpublictypes.sas | 0 {002_macros => 005_macros}/mm_getrepos.sas | 0 {002_macros => 005_macros}/mm_getroles.sas | 0 .../mm_getservercontexts.sas | 0 {002_macros => 005_macros}/mm_getstpcode.sas | 0 {002_macros => 005_macros}/mm_getstpinfo.sas | 0 {002_macros => 005_macros}/mm_getstps.sas | 0 {002_macros => 005_macros}/mm_gettableid.sas | 0 {002_macros => 005_macros}/mm_gettables.sas | 0 {002_macros => 005_macros}/mm_gettree.sas | 0 {002_macros => 005_macros}/mm_gettypes.sas | 0 {002_macros => 005_macros}/mm_getusers.sas | 0 .../mm_getwebappsrvprops.sas | 0 {002_macros => 005_macros}/mm_spkexport.sas | 0 {002_macros => 005_macros}/mm_tree.sas | 0 .../mm_updateappextension.sas | 0 .../mm_updatedocument.sas | 0 .../mm_updatestpservertype.sas | 0 .../mm_updatestpsourcecode.sas | 0 {002_macros => 005_macros}/mm_webout.sas | 0 005_macros/mp_cntlout.sas | 94 -- 005_macros/mp_dirlist.sas | 262 ----- 005_macros/mp_ds2csv.sas | 212 ---- 005_macros/mp_filtercheck.sas | 253 ----- 005_macros/mp_loadformat.sas | 363 ------- 005_macros/mp_stackdiffs.sas | 592 ----------- 005_macros/mp_storediffs.sas | 229 ----- 005_macros/mp_validatecol.sas | 97 -- 005_macros/ms_adduser2group.sas | 122 --- 005_macros/ms_createfile.sas | 112 -- 005_macros/ms_creategroup.sas | 149 --- 005_macros/ms_createuser.sas | 146 --- 005_macros/ms_createwebservice.sas | 689 ------------- 005_macros/ms_deletefile.sas | 48 - 005_macros/ms_getfile.sas | 50 - 005_macros/ms_getgroups.sas | 147 --- 005_macros/ms_getusers.sas | 148 --- 005_macros/ms_runstp.sas | 215 ---- 005_macros/ms_testservice.sas | 141 --- 005_macros/ms_webout.sas | 228 ----- 006_macros/mcf_getfmttype.sas | 118 --- 006_macros/mcf_init.sas | 44 - 006_macros/mcf_length.sas | 90 -- 006_macros/mcf_string2file.sas | 95 -- 006_macros/mddl_dc_difftable.sas | 44 - 006_macros/mddl_dc_filterdetail.sas | 40 - 006_macros/mddl_dc_filtersummary.sas | 35 - 006_macros/mddl_dc_locktable.sas | 41 - 006_macros/mddl_dc_maxkeytable.sas | 33 - 006_macros/mddl_sas_cntlout.sas | 57 -- .../mmx_createmetafolder.sas | 0 .../mmx_deletemetafolder.sas | 0 {004_macros => 006_macros}/mmx_spkexport.sas | 0 007_macros/mx_createwebservice.sas | 98 -- 007_macros/mx_getcode.sas | 57 -- 007_macros/mx_testservice.sas | 299 ------ {001_macros => 008_macros}/mfv_existfile.sas | 0 .../mfv_existfolder.sas | 0 .../mfv_existsashdat.sas | 0 008_macros/ml_gsubfile.sas | 52 - 008_macros/ml_json.sas | 402 -------- 008_macros/mmx_createmetafolder.sas | 50 - 008_macros/mmx_deletemetafolder.sas | 40 - 008_macros/mmx_spkexport.sas | 94 -- {001_macros => 008_macros}/mv_createfile.sas | 0 .../mv_createfolder.sas | 0 {001_macros => 008_macros}/mv_createjob.sas | 0 .../mv_createwebservice.sas | 0 .../mv_deletefoldermember.sas | 0 {001_macros => 008_macros}/mv_deletejes.sas | 0 .../mv_deleteviyafolder.sas | 0 {001_macros => 008_macros}/mv_getclients.sas | 0 .../mv_getfoldermembers.sas | 0 .../mv_getgroupmembers.sas | 0 {001_macros => 008_macros}/mv_getgroups.sas | 0 {001_macros => 008_macros}/mv_getjobcode.sas | 0 {001_macros => 008_macros}/mv_getjoblog.sas | 0 .../mv_getjobresult.sas | 0 {001_macros => 008_macros}/mv_getjobstate.sas | 0 .../mv_getusergroups.sas | 0 {001_macros => 008_macros}/mv_getusers.sas | 0 {001_macros => 008_macros}/mv_jobexecute.sas | 0 {001_macros => 008_macros}/mv_jobflow.sas | 0 {001_macros => 008_macros}/mv_jobwaitfor.sas | 0 .../mv_registerclient.sas | 0 {001_macros => 008_macros}/mv_tokenauth.sas | 0 .../mv_tokenrefresh.sas | 0 {001_macros => 008_macros}/mv_webout.sas | 0 009_macros/mf_abort.sas | 32 - 009_macros/mf_dedup.sas | 53 - 009_macros/mf_deletefile.sas | 31 - 009_macros/mf_existds.sas | 30 - 009_macros/mf_existfeature.sas | 59 -- 009_macros/mf_existfileref.sas | 33 - 009_macros/mf_existfunction.sas | 37 - 009_macros/mf_existvar.sas | 43 - 009_macros/mf_existvarlist.sas | 56 - 009_macros/mf_fmtdttm.sas | 42 - 009_macros/mf_getapploc.sas | 79 -- 009_macros/mf_getattrc.sas | 34 - 009_macros/mf_getattrn.sas | 34 - 009_macros/mf_getengine.sas | 54 - 009_macros/mf_getfilesize.sas | 64 -- 009_macros/mf_getfmtlist.sas | 60 -- 009_macros/mf_getfmtname.sas | 44 - 009_macros/mf_getgitbranch.sas | 37 - 009_macros/mf_getkeyvalue.sas | 33 - 009_macros/mf_getplatform.sas | 75 -- 009_macros/mf_getquotedstr.sas | 57 -- 009_macros/mf_getschema.sas | 43 - 009_macros/mf_getuniquefileref.sas | 60 -- 009_macros/mf_getuniquelibref.sas | 57 -- 009_macros/mf_getuniquename.sas | 22 - 009_macros/mf_getuser.sas | 42 - 009_macros/mf_getvalue.sas | 36 - 009_macros/mf_getvarcount.sas | 49 - 009_macros/mf_getvarformat.sas | 73 -- 009_macros/mf_getvarlen.sas | 56 - 009_macros/mf_getvarlist.sas | 74 -- 009_macros/mf_getvarnum.sas | 58 -- 009_macros/mf_getvartype.sas | 52 - 009_macros/mf_getxengine.sas | 43 - 009_macros/mf_increment.sas | 29 - 009_macros/mf_isdir.sas | 34 - 009_macros/mf_isint.sas | 36 - 009_macros/mf_islibds.sas | 40 - 009_macros/mf_loc.sas | 34 - 009_macros/mf_mkdir.sas | 67 -- 009_macros/mf_mval.sas | 21 - 009_macros/mf_nobs.sas | 26 - 009_macros/mf_readfile.sas | 63 -- 009_macros/mf_trimstr.sas | 50 - 009_macros/mf_uid.sas | 21 - 009_macros/mf_verifymacvars.sas | 72 -- 009_macros/mf_wordsinstr1andstr2.sas | 53 - 009_macros/mf_wordsinstr1butnotstr2.sas | 54 - 009_macros/mf_writefile.sas | 67 -- 009_macros/mp_abort.sas | 338 ------ 009_macros/mp_aligndecimal.sas | 95 -- 009_macros/mp_appendfile.sas | 57 -- 009_macros/mp_applyformats.sas | 181 ---- 009_macros/mp_assert.sas | 56 - 009_macros/mp_assertcols.sas | 145 --- 009_macros/mp_assertcolvals.sas | 172 ---- 009_macros/mp_assertdsobs.sas | 121 --- 009_macros/mp_assertscope.sas | 147 --- 009_macros/mp_base64copy.sas | 117 --- 009_macros/mp_binarycopy.sas | 80 -- 009_macros/mp_chop.sas | 195 ---- 009_macros/mp_cleancsv.sas | 79 -- 009_macros/mp_copyfolder.sas | 84 -- 009_macros/mp_coretable.sas | 73 -- 009_macros/mp_createconstraints.sas | 70 -- 009_macros/mp_createwebservice.sas | 32 - 009_macros/mp_csv2ds.sas | 141 --- 009_macros/mp_deleteconstraints.sas | 54 - 009_macros/mp_deletefolder.sas | 70 -- 009_macros/mp_dictionary.sas | 52 - 009_macros/mp_distinctfmtvalues.sas | 50 - 009_macros/mp_dropmembers.sas | 46 - 009_macros/mp_ds2cards.sas | 291 ------ 009_macros/mp_ds2ddl.sas | 42 - 009_macros/mp_ds2fmtds.sas | 105 -- 009_macros/mp_ds2inserts.sas | 180 ---- 009_macros/mp_ds2md.sas | 104 -- 009_macros/mp_ds2squeeze.sas | 120 --- 009_macros/mp_dsmeta.sas | 110 -- 009_macros/mp_filtergenerate.sas | 105 -- 009_macros/mp_filterstore.sas | 255 ----- 009_macros/mp_filtervalidate.sas | 105 -- 009_macros/mp_getcols.sas | 68 -- 009_macros/mp_getconstraints.sas | 115 --- 009_macros/mp_getdbml.sas | 335 ------ 009_macros/mp_getddl.sas | 405 -------- 009_macros/mp_getformats.sas | 134 --- 009_macros/mp_getmaxvarlengths.sas | 130 --- 009_macros/mp_getpk.sas | 281 ----- 009_macros/mp_gitadd.sas | 45 - 009_macros/mp_gitlog.sas | 104 -- 009_macros/mp_gitreleaseinfo.sas | 74 -- 009_macros/mp_gitstatus.sas | 67 -- 009_macros/mp_gsubfile.sas | 58 -- 009_macros/mp_guesspk.sas | 332 ------ 009_macros/mp_hashdataset.sas | 91 -- 009_macros/mp_hashdirectory.sas | 164 --- 009_macros/mp_include.sas | 106 -- 009_macros/mp_init.sas | 76 -- 009_macros/mp_jsonout.sas | 406 -------- 009_macros/mp_lib2cards.sas | 78 -- 009_macros/mp_lib2inserts.sas | 77 -- 009_macros/mp_lockanytable.sas | 250 ----- 009_macros/mp_lockfilecheck.sas | 103 -- 009_macros/mp_makedata.sas | 107 -- 009_macros/mp_md5.sas | 57 -- 009_macros/mp_perflog.sas | 43 - 009_macros/mp_prevobs.sas | 88 -- 009_macros/mp_recursivejoin.sas | 91 -- 009_macros/mp_replace.sas | 152 --- 009_macros/mp_reseterror.sas | 27 - 009_macros/mp_resetoption.sas | 39 - 009_macros/mp_retainedkey.sas | 247 ----- 009_macros/mp_runddl.sas | 49 - 009_macros/mp_searchcols.sas | 104 -- 009_macros/mp_searchdata.sas | 131 --- 009_macros/mp_setkeyvalue.sas | 55 - 009_macros/mp_sortinplace.sas | 122 --- 009_macros/mp_stprequests.sas | 75 -- 009_macros/mp_streamfile.sas | 212 ---- 009_macros/mp_testjob.sas | 92 -- 009_macros/mp_testservice.sas | 39 - 009_macros/mp_testwritespeedlibrary.sas | 59 -- 009_macros/mp_tree.sas | 71 -- 009_macros/mp_unzip.sas | 91 -- 009_macros/mp_updatevarlength.sas | 93 -- 009_macros/mp_wait4file.sas | 35 - 009_macros/mp_webin.sas | 59 -- 009_macros/mp_zip.sas | 85 -- {007_macros => 009_macros}/mx_getgroups.sas | 0 description.sas | 2 +- sasjscore.zip | Bin 780675 -> 401209 bytes 471 files changed, 3 insertions(+), 29781 deletions(-) rename {005_macros => 001_macros}/mf_abort.sas (100%) rename {005_macros => 001_macros}/mf_dedup.sas (100%) rename {005_macros => 001_macros}/mf_deletefile.sas (100%) rename {005_macros => 001_macros}/mf_existds.sas (100%) rename {005_macros => 001_macros}/mf_existfeature.sas (100%) rename {005_macros => 001_macros}/mf_existfileref.sas (100%) rename {005_macros => 001_macros}/mf_existfunction.sas (100%) rename {005_macros => 001_macros}/mf_existvar.sas (100%) rename {005_macros => 001_macros}/mf_existvarlist.sas (100%) rename {005_macros => 001_macros}/mf_fmtdttm.sas (100%) rename {005_macros => 001_macros}/mf_getapploc.sas (100%) rename {005_macros => 001_macros}/mf_getattrc.sas (100%) rename {005_macros => 001_macros}/mf_getattrn.sas (100%) rename {005_macros => 001_macros}/mf_getengine.sas (100%) rename {005_macros => 001_macros}/mf_getfilesize.sas (100%) rename {005_macros => 001_macros}/mf_getfmtlist.sas (100%) rename {005_macros => 001_macros}/mf_getfmtname.sas (100%) rename {005_macros => 001_macros}/mf_getgitbranch.sas (100%) rename {005_macros => 001_macros}/mf_getkeyvalue.sas (100%) rename {005_macros => 001_macros}/mf_getplatform.sas (100%) rename {005_macros => 001_macros}/mf_getquotedstr.sas (100%) rename {005_macros => 001_macros}/mf_getschema.sas (100%) rename {005_macros => 001_macros}/mf_getuniquefileref.sas (100%) rename {005_macros => 001_macros}/mf_getuniquelibref.sas (100%) rename {005_macros => 001_macros}/mf_getuniquename.sas (100%) rename {005_macros => 001_macros}/mf_getuser.sas (100%) rename {005_macros => 001_macros}/mf_getvalue.sas (100%) rename {005_macros => 001_macros}/mf_getvarcount.sas (100%) rename {005_macros => 001_macros}/mf_getvarformat.sas (100%) rename {005_macros => 001_macros}/mf_getvarlen.sas (100%) rename {005_macros => 001_macros}/mf_getvarlist.sas (100%) rename {005_macros => 001_macros}/mf_getvarnum.sas (100%) rename {005_macros => 001_macros}/mf_getvartype.sas (100%) rename {005_macros => 001_macros}/mf_getxengine.sas (100%) rename {005_macros => 001_macros}/mf_increment.sas (100%) rename {009_macros => 001_macros}/mf_isblank.sas (100%) rename {005_macros => 001_macros}/mf_isdir.sas (100%) rename {005_macros => 001_macros}/mf_isint.sas (100%) rename {005_macros => 001_macros}/mf_islibds.sas (100%) rename {005_macros => 001_macros}/mf_loc.sas (100%) rename {005_macros => 001_macros}/mf_mkdir.sas (100%) rename {005_macros => 001_macros}/mf_mval.sas (100%) rename {005_macros => 001_macros}/mf_nobs.sas (100%) rename {005_macros => 001_macros}/mf_readfile.sas (100%) rename {005_macros => 001_macros}/mf_trimstr.sas (100%) rename {005_macros => 001_macros}/mf_uid.sas (100%) rename {005_macros => 001_macros}/mf_verifymacvars.sas (100%) rename {005_macros => 001_macros}/mf_wordsinstr1andstr2.sas (100%) rename {005_macros => 001_macros}/mf_wordsinstr1butnotstr2.sas (100%) rename {005_macros => 001_macros}/mf_writefile.sas (100%) rename {005_macros => 001_macros}/mp_abort.sas (100%) rename {005_macros => 001_macros}/mp_aligndecimal.sas (100%) rename {005_macros => 001_macros}/mp_appendfile.sas (100%) rename {005_macros => 001_macros}/mp_applyformats.sas (100%) rename {005_macros => 001_macros}/mp_assert.sas (100%) rename {005_macros => 001_macros}/mp_assertcols.sas (100%) rename {005_macros => 001_macros}/mp_assertcolvals.sas (100%) rename {005_macros => 001_macros}/mp_assertdsobs.sas (100%) rename {005_macros => 001_macros}/mp_assertscope.sas (100%) rename {005_macros => 001_macros}/mp_base64copy.sas (100%) rename {005_macros => 001_macros}/mp_binarycopy.sas (100%) rename {005_macros => 001_macros}/mp_chop.sas (100%) rename {005_macros => 001_macros}/mp_cleancsv.sas (100%) rename {009_macros => 001_macros}/mp_cntlout.sas (100%) rename {005_macros => 001_macros}/mp_copyfolder.sas (100%) rename {005_macros => 001_macros}/mp_coretable.sas (100%) rename {005_macros => 001_macros}/mp_createconstraints.sas (100%) rename {005_macros => 001_macros}/mp_createwebservice.sas (100%) rename {005_macros => 001_macros}/mp_csv2ds.sas (100%) rename {005_macros => 001_macros}/mp_deleteconstraints.sas (100%) rename {005_macros => 001_macros}/mp_deletefolder.sas (100%) rename {005_macros => 001_macros}/mp_dictionary.sas (100%) rename {009_macros => 001_macros}/mp_dirlist.sas (100%) rename {005_macros => 001_macros}/mp_distinctfmtvalues.sas (100%) rename {005_macros => 001_macros}/mp_dropmembers.sas (100%) rename {005_macros => 001_macros}/mp_ds2cards.sas (100%) rename {009_macros => 001_macros}/mp_ds2csv.sas (100%) rename {005_macros => 001_macros}/mp_ds2ddl.sas (100%) rename {005_macros => 001_macros}/mp_ds2fmtds.sas (100%) rename {005_macros => 001_macros}/mp_ds2inserts.sas (100%) rename {005_macros => 001_macros}/mp_ds2md.sas (100%) rename {005_macros => 001_macros}/mp_ds2squeeze.sas (100%) rename {005_macros => 001_macros}/mp_dsmeta.sas (100%) rename {009_macros => 001_macros}/mp_filtercheck.sas (100%) rename {005_macros => 001_macros}/mp_filtergenerate.sas (100%) rename {005_macros => 001_macros}/mp_filterstore.sas (100%) rename {005_macros => 001_macros}/mp_filtervalidate.sas (100%) rename {005_macros => 001_macros}/mp_getcols.sas (100%) rename {005_macros => 001_macros}/mp_getconstraints.sas (100%) rename {005_macros => 001_macros}/mp_getdbml.sas (100%) rename {005_macros => 001_macros}/mp_getddl.sas (100%) rename {005_macros => 001_macros}/mp_getformats.sas (100%) rename {005_macros => 001_macros}/mp_getmaxvarlengths.sas (100%) rename {005_macros => 001_macros}/mp_getpk.sas (100%) rename {005_macros => 001_macros}/mp_gitadd.sas (100%) rename {005_macros => 001_macros}/mp_gitlog.sas (100%) rename {005_macros => 001_macros}/mp_gitreleaseinfo.sas (100%) rename {005_macros => 001_macros}/mp_gitstatus.sas (100%) rename {005_macros => 001_macros}/mp_gsubfile.sas (100%) rename {005_macros => 001_macros}/mp_guesspk.sas (100%) rename {005_macros => 001_macros}/mp_hashdataset.sas (100%) rename {005_macros => 001_macros}/mp_hashdirectory.sas (100%) rename {005_macros => 001_macros}/mp_include.sas (100%) rename {005_macros => 001_macros}/mp_init.sas (100%) rename {005_macros => 001_macros}/mp_jsonout.sas (100%) rename {005_macros => 001_macros}/mp_lib2cards.sas (100%) rename {005_macros => 001_macros}/mp_lib2inserts.sas (100%) rename {009_macros => 001_macros}/mp_loadformat.sas (100%) rename {005_macros => 001_macros}/mp_lockanytable.sas (100%) rename {005_macros => 001_macros}/mp_lockfilecheck.sas (100%) rename {005_macros => 001_macros}/mp_makedata.sas (100%) rename {005_macros => 001_macros}/mp_md5.sas (100%) rename {005_macros => 001_macros}/mp_perflog.sas (100%) rename {005_macros => 001_macros}/mp_prevobs.sas (100%) rename {005_macros => 001_macros}/mp_recursivejoin.sas (100%) rename {005_macros => 001_macros}/mp_replace.sas (100%) rename {005_macros => 001_macros}/mp_reseterror.sas (100%) rename {005_macros => 001_macros}/mp_resetoption.sas (100%) rename {005_macros => 001_macros}/mp_retainedkey.sas (100%) rename {005_macros => 001_macros}/mp_runddl.sas (100%) rename {005_macros => 001_macros}/mp_searchcols.sas (100%) rename {005_macros => 001_macros}/mp_searchdata.sas (100%) rename {005_macros => 001_macros}/mp_setkeyvalue.sas (100%) rename {005_macros => 001_macros}/mp_sortinplace.sas (100%) rename {009_macros => 001_macros}/mp_stackdiffs.sas (100%) rename {009_macros => 001_macros}/mp_storediffs.sas (100%) rename {005_macros => 001_macros}/mp_stprequests.sas (100%) rename {005_macros => 001_macros}/mp_streamfile.sas (100%) rename {009_macros => 001_macros}/mp_stripdiffs.sas (100%) rename {005_macros => 001_macros}/mp_testjob.sas (100%) rename {005_macros => 001_macros}/mp_testservice.sas (100%) rename {005_macros => 001_macros}/mp_testwritespeedlibrary.sas (100%) rename {005_macros => 001_macros}/mp_tree.sas (100%) rename {005_macros => 001_macros}/mp_unzip.sas (100%) rename {005_macros => 001_macros}/mp_updatevarlength.sas (100%) rename {009_macros => 001_macros}/mp_validatecol.sas (100%) rename {005_macros => 001_macros}/mp_wait4file.sas (100%) rename {005_macros => 001_macros}/mp_webin.sas (100%) rename {005_macros => 001_macros}/mp_zip.sas (100%) rename {004_macros => 002_macros}/mddl_dc_difftable.sas (100%) rename {004_macros => 002_macros}/mddl_dc_filterdetail.sas (100%) rename {004_macros => 002_macros}/mddl_dc_filtersummary.sas (100%) rename {004_macros => 002_macros}/mddl_dc_locktable.sas (100%) rename {004_macros => 002_macros}/mddl_dc_maxkeytable.sas (100%) rename {004_macros => 002_macros}/mddl_sas_cntlout.sas (100%) delete mode 100644 002_macros/mm_assigndirectlib.sas delete mode 100644 002_macros/mm_createstp.sas delete mode 100644 002_macros/mm_getgroups.sas rename {001_macros => 003_macros}/mcf_getfmttype.sas (100%) rename {001_macros => 003_macros}/mcf_init.sas (100%) rename {001_macros => 003_macros}/mcf_length.sas (100%) rename {001_macros => 003_macros}/mcf_string2file.sas (100%) delete mode 100644 003_macros/mfv_existfile.sas delete mode 100644 003_macros/mfv_existfolder.sas delete mode 100644 003_macros/mfv_existsashdat.sas delete mode 100644 003_macros/mm_adduser2group.sas delete mode 100644 003_macros/mm_assignlib.sas delete mode 100644 003_macros/mm_createapplication.sas delete mode 100644 003_macros/mm_createdataset.sas delete mode 100644 003_macros/mm_createdocument.sas delete mode 100644 003_macros/mm_createfolder.sas delete mode 100644 003_macros/mm_createlibrary.sas delete mode 100644 003_macros/mm_createwebservice.sas delete mode 100644 003_macros/mm_deletedocument.sas delete mode 100644 003_macros/mm_deletelibrary.sas delete mode 100644 003_macros/mm_deletestp.sas delete mode 100644 003_macros/mm_getauthinfo.sas delete mode 100644 003_macros/mm_getcols.sas delete mode 100644 003_macros/mm_getdetails.sas delete mode 100644 003_macros/mm_getdirectories.sas delete mode 100644 003_macros/mm_getdocument.sas delete mode 100644 003_macros/mm_getfoldermembers.sas delete mode 100644 003_macros/mm_getfoldertree.sas delete mode 100644 003_macros/mm_getgroupmembers.sas delete mode 100644 003_macros/mm_getlibmetadiffs.sas delete mode 100644 003_macros/mm_getlibs.sas delete mode 100644 003_macros/mm_getobjects.sas delete mode 100644 003_macros/mm_getpublictypes.sas delete mode 100644 003_macros/mm_getrepos.sas delete mode 100644 003_macros/mm_getroles.sas delete mode 100644 003_macros/mm_getservercontexts.sas delete mode 100644 003_macros/mm_getstpcode.sas delete mode 100644 003_macros/mm_getstpinfo.sas delete mode 100644 003_macros/mm_getstps.sas delete mode 100644 003_macros/mm_gettableid.sas delete mode 100644 003_macros/mm_gettables.sas delete mode 100644 003_macros/mm_gettree.sas delete mode 100644 003_macros/mm_gettypes.sas delete mode 100644 003_macros/mm_getusers.sas delete mode 100644 003_macros/mm_getwebappsrvprops.sas delete mode 100644 003_macros/mm_spkexport.sas delete mode 100644 003_macros/mm_tree.sas delete mode 100644 003_macros/mm_updateappextension.sas delete mode 100644 003_macros/mm_updatedocument.sas delete mode 100644 003_macros/mm_updatestpservertype.sas delete mode 100644 003_macros/mm_updatestpsourcecode.sas delete mode 100644 003_macros/mm_webout.sas delete mode 100644 003_macros/mv_createfile.sas delete mode 100644 003_macros/mv_createfolder.sas delete mode 100644 003_macros/mv_createjob.sas delete mode 100644 003_macros/mv_createwebservice.sas delete mode 100644 003_macros/mv_deletefoldermember.sas delete mode 100644 003_macros/mv_deletejes.sas delete mode 100644 003_macros/mv_deleteviyafolder.sas delete mode 100644 003_macros/mv_getclients.sas delete mode 100644 003_macros/mv_getfoldermembers.sas delete mode 100644 003_macros/mv_getgroupmembers.sas delete mode 100644 003_macros/mv_getgroups.sas delete mode 100644 003_macros/mv_getjobcode.sas delete mode 100644 003_macros/mv_getjoblog.sas delete mode 100644 003_macros/mv_getjobresult.sas delete mode 100644 003_macros/mv_getjobstate.sas delete mode 100644 003_macros/mv_getusergroups.sas delete mode 100644 003_macros/mv_getusers.sas delete mode 100644 003_macros/mv_jobexecute.sas delete mode 100644 003_macros/mv_jobflow.sas delete mode 100644 003_macros/mv_jobwaitfor.sas delete mode 100644 003_macros/mv_registerclient.sas delete mode 100644 003_macros/mv_tokenauth.sas delete mode 100644 003_macros/mv_tokenrefresh.sas delete mode 100644 003_macros/mv_webout.sas rename {002_macros => 004_macros}/ml_gsubfile.sas (100%) rename {002_macros => 004_macros}/ml_json.sas (100%) delete mode 100644 005_macros/mf_isblank.sas delete mode 100644 005_macros/mfs_httpheader.sas rename {002_macros => 005_macros}/mm_adduser2group.sas (100%) rename {003_macros => 005_macros}/mm_assigndirectlib.sas (100%) rename {002_macros => 005_macros}/mm_assignlib.sas (100%) rename {002_macros => 005_macros}/mm_createapplication.sas (100%) rename {002_macros => 005_macros}/mm_createdataset.sas (100%) rename {002_macros => 005_macros}/mm_createdocument.sas (100%) rename {002_macros => 005_macros}/mm_createfolder.sas (100%) rename {002_macros => 005_macros}/mm_createlibrary.sas (100%) rename {003_macros => 005_macros}/mm_createstp.sas (100%) rename {002_macros => 005_macros}/mm_createwebservice.sas (100%) rename {002_macros => 005_macros}/mm_deletedocument.sas (100%) rename {002_macros => 005_macros}/mm_deletelibrary.sas (100%) rename {002_macros => 005_macros}/mm_deletestp.sas (100%) rename {002_macros => 005_macros}/mm_getauthinfo.sas (100%) rename {002_macros => 005_macros}/mm_getcols.sas (100%) rename {002_macros => 005_macros}/mm_getdetails.sas (100%) rename {002_macros => 005_macros}/mm_getdirectories.sas (100%) rename {002_macros => 005_macros}/mm_getdocument.sas (100%) rename {002_macros => 005_macros}/mm_getfoldermembers.sas (100%) rename {002_macros => 005_macros}/mm_getfoldertree.sas (100%) rename {002_macros => 005_macros}/mm_getgroupmembers.sas (100%) rename {003_macros => 005_macros}/mm_getgroups.sas (100%) rename {002_macros => 005_macros}/mm_getlibmetadiffs.sas (100%) rename {002_macros => 005_macros}/mm_getlibs.sas (100%) rename {002_macros => 005_macros}/mm_getobjects.sas (100%) rename {002_macros => 005_macros}/mm_getpublictypes.sas (100%) rename {002_macros => 005_macros}/mm_getrepos.sas (100%) rename {002_macros => 005_macros}/mm_getroles.sas (100%) rename {002_macros => 005_macros}/mm_getservercontexts.sas (100%) rename {002_macros => 005_macros}/mm_getstpcode.sas (100%) rename {002_macros => 005_macros}/mm_getstpinfo.sas (100%) rename {002_macros => 005_macros}/mm_getstps.sas (100%) rename {002_macros => 005_macros}/mm_gettableid.sas (100%) rename {002_macros => 005_macros}/mm_gettables.sas (100%) rename {002_macros => 005_macros}/mm_gettree.sas (100%) rename {002_macros => 005_macros}/mm_gettypes.sas (100%) rename {002_macros => 005_macros}/mm_getusers.sas (100%) rename {002_macros => 005_macros}/mm_getwebappsrvprops.sas (100%) rename {002_macros => 005_macros}/mm_spkexport.sas (100%) rename {002_macros => 005_macros}/mm_tree.sas (100%) rename {002_macros => 005_macros}/mm_updateappextension.sas (100%) rename {002_macros => 005_macros}/mm_updatedocument.sas (100%) rename {002_macros => 005_macros}/mm_updatestpservertype.sas (100%) rename {002_macros => 005_macros}/mm_updatestpsourcecode.sas (100%) rename {002_macros => 005_macros}/mm_webout.sas (100%) delete mode 100644 005_macros/mp_cntlout.sas delete mode 100644 005_macros/mp_dirlist.sas delete mode 100644 005_macros/mp_ds2csv.sas delete mode 100644 005_macros/mp_filtercheck.sas delete mode 100644 005_macros/mp_loadformat.sas delete mode 100644 005_macros/mp_stackdiffs.sas delete mode 100644 005_macros/mp_storediffs.sas delete mode 100644 005_macros/mp_validatecol.sas delete mode 100644 005_macros/ms_adduser2group.sas delete mode 100644 005_macros/ms_createfile.sas delete mode 100644 005_macros/ms_creategroup.sas delete mode 100644 005_macros/ms_createuser.sas delete mode 100644 005_macros/ms_createwebservice.sas delete mode 100644 005_macros/ms_deletefile.sas delete mode 100644 005_macros/ms_getfile.sas delete mode 100644 005_macros/ms_getgroups.sas delete mode 100644 005_macros/ms_getusers.sas delete mode 100644 005_macros/ms_runstp.sas delete mode 100644 005_macros/ms_testservice.sas delete mode 100644 005_macros/ms_webout.sas delete mode 100644 006_macros/mcf_getfmttype.sas delete mode 100644 006_macros/mcf_init.sas delete mode 100644 006_macros/mcf_length.sas delete mode 100644 006_macros/mcf_string2file.sas delete mode 100644 006_macros/mddl_dc_difftable.sas delete mode 100644 006_macros/mddl_dc_filterdetail.sas delete mode 100644 006_macros/mddl_dc_filtersummary.sas delete mode 100644 006_macros/mddl_dc_locktable.sas delete mode 100644 006_macros/mddl_dc_maxkeytable.sas delete mode 100644 006_macros/mddl_sas_cntlout.sas rename {004_macros => 006_macros}/mmx_createmetafolder.sas (100%) rename {004_macros => 006_macros}/mmx_deletemetafolder.sas (100%) rename {004_macros => 006_macros}/mmx_spkexport.sas (100%) delete mode 100644 007_macros/mx_createwebservice.sas delete mode 100644 007_macros/mx_getcode.sas delete mode 100644 007_macros/mx_testservice.sas rename {001_macros => 008_macros}/mfv_existfile.sas (100%) rename {001_macros => 008_macros}/mfv_existfolder.sas (100%) rename {001_macros => 008_macros}/mfv_existsashdat.sas (100%) delete mode 100644 008_macros/ml_gsubfile.sas delete mode 100644 008_macros/ml_json.sas delete mode 100644 008_macros/mmx_createmetafolder.sas delete mode 100644 008_macros/mmx_deletemetafolder.sas delete mode 100644 008_macros/mmx_spkexport.sas rename {001_macros => 008_macros}/mv_createfile.sas (100%) rename {001_macros => 008_macros}/mv_createfolder.sas (100%) rename {001_macros => 008_macros}/mv_createjob.sas (100%) rename {001_macros => 008_macros}/mv_createwebservice.sas (100%) rename {001_macros => 008_macros}/mv_deletefoldermember.sas (100%) rename {001_macros => 008_macros}/mv_deletejes.sas (100%) rename {001_macros => 008_macros}/mv_deleteviyafolder.sas (100%) rename {001_macros => 008_macros}/mv_getclients.sas (100%) rename {001_macros => 008_macros}/mv_getfoldermembers.sas (100%) rename {001_macros => 008_macros}/mv_getgroupmembers.sas (100%) rename {001_macros => 008_macros}/mv_getgroups.sas (100%) rename {001_macros => 008_macros}/mv_getjobcode.sas (100%) rename {001_macros => 008_macros}/mv_getjoblog.sas (100%) rename {001_macros => 008_macros}/mv_getjobresult.sas (100%) rename {001_macros => 008_macros}/mv_getjobstate.sas (100%) rename {001_macros => 008_macros}/mv_getusergroups.sas (100%) rename {001_macros => 008_macros}/mv_getusers.sas (100%) rename {001_macros => 008_macros}/mv_jobexecute.sas (100%) rename {001_macros => 008_macros}/mv_jobflow.sas (100%) rename {001_macros => 008_macros}/mv_jobwaitfor.sas (100%) rename {001_macros => 008_macros}/mv_registerclient.sas (100%) rename {001_macros => 008_macros}/mv_tokenauth.sas (100%) rename {001_macros => 008_macros}/mv_tokenrefresh.sas (100%) rename {001_macros => 008_macros}/mv_webout.sas (100%) delete mode 100644 009_macros/mf_abort.sas delete mode 100644 009_macros/mf_dedup.sas delete mode 100644 009_macros/mf_deletefile.sas delete mode 100644 009_macros/mf_existds.sas delete mode 100644 009_macros/mf_existfeature.sas delete mode 100644 009_macros/mf_existfileref.sas delete mode 100644 009_macros/mf_existfunction.sas delete mode 100644 009_macros/mf_existvar.sas delete mode 100644 009_macros/mf_existvarlist.sas delete mode 100644 009_macros/mf_fmtdttm.sas delete mode 100644 009_macros/mf_getapploc.sas delete mode 100644 009_macros/mf_getattrc.sas delete mode 100644 009_macros/mf_getattrn.sas delete mode 100644 009_macros/mf_getengine.sas delete mode 100644 009_macros/mf_getfilesize.sas delete mode 100644 009_macros/mf_getfmtlist.sas delete mode 100644 009_macros/mf_getfmtname.sas delete mode 100644 009_macros/mf_getgitbranch.sas delete mode 100644 009_macros/mf_getkeyvalue.sas delete mode 100644 009_macros/mf_getplatform.sas delete mode 100644 009_macros/mf_getquotedstr.sas delete mode 100644 009_macros/mf_getschema.sas delete mode 100644 009_macros/mf_getuniquefileref.sas delete mode 100644 009_macros/mf_getuniquelibref.sas delete mode 100644 009_macros/mf_getuniquename.sas delete mode 100644 009_macros/mf_getuser.sas delete mode 100644 009_macros/mf_getvalue.sas delete mode 100644 009_macros/mf_getvarcount.sas delete mode 100644 009_macros/mf_getvarformat.sas delete mode 100644 009_macros/mf_getvarlen.sas delete mode 100644 009_macros/mf_getvarlist.sas delete mode 100644 009_macros/mf_getvarnum.sas delete mode 100644 009_macros/mf_getvartype.sas delete mode 100644 009_macros/mf_getxengine.sas delete mode 100644 009_macros/mf_increment.sas delete mode 100644 009_macros/mf_isdir.sas delete mode 100644 009_macros/mf_isint.sas delete mode 100644 009_macros/mf_islibds.sas delete mode 100644 009_macros/mf_loc.sas delete mode 100644 009_macros/mf_mkdir.sas delete mode 100644 009_macros/mf_mval.sas delete mode 100644 009_macros/mf_nobs.sas delete mode 100644 009_macros/mf_readfile.sas delete mode 100644 009_macros/mf_trimstr.sas delete mode 100644 009_macros/mf_uid.sas delete mode 100644 009_macros/mf_verifymacvars.sas delete mode 100644 009_macros/mf_wordsinstr1andstr2.sas delete mode 100644 009_macros/mf_wordsinstr1butnotstr2.sas delete mode 100644 009_macros/mf_writefile.sas delete mode 100644 009_macros/mp_abort.sas delete mode 100644 009_macros/mp_aligndecimal.sas delete mode 100644 009_macros/mp_appendfile.sas delete mode 100644 009_macros/mp_applyformats.sas delete mode 100644 009_macros/mp_assert.sas delete mode 100644 009_macros/mp_assertcols.sas delete mode 100644 009_macros/mp_assertcolvals.sas delete mode 100644 009_macros/mp_assertdsobs.sas delete mode 100644 009_macros/mp_assertscope.sas delete mode 100644 009_macros/mp_base64copy.sas delete mode 100644 009_macros/mp_binarycopy.sas delete mode 100644 009_macros/mp_chop.sas delete mode 100644 009_macros/mp_cleancsv.sas delete mode 100644 009_macros/mp_copyfolder.sas delete mode 100644 009_macros/mp_coretable.sas delete mode 100644 009_macros/mp_createconstraints.sas delete mode 100644 009_macros/mp_createwebservice.sas delete mode 100644 009_macros/mp_csv2ds.sas delete mode 100644 009_macros/mp_deleteconstraints.sas delete mode 100644 009_macros/mp_deletefolder.sas delete mode 100644 009_macros/mp_dictionary.sas delete mode 100644 009_macros/mp_distinctfmtvalues.sas delete mode 100644 009_macros/mp_dropmembers.sas delete mode 100644 009_macros/mp_ds2cards.sas delete mode 100644 009_macros/mp_ds2ddl.sas delete mode 100644 009_macros/mp_ds2fmtds.sas delete mode 100644 009_macros/mp_ds2inserts.sas delete mode 100644 009_macros/mp_ds2md.sas delete mode 100644 009_macros/mp_ds2squeeze.sas delete mode 100644 009_macros/mp_dsmeta.sas delete mode 100644 009_macros/mp_filtergenerate.sas delete mode 100644 009_macros/mp_filterstore.sas delete mode 100644 009_macros/mp_filtervalidate.sas delete mode 100644 009_macros/mp_getcols.sas delete mode 100644 009_macros/mp_getconstraints.sas delete mode 100644 009_macros/mp_getdbml.sas delete mode 100644 009_macros/mp_getddl.sas delete mode 100644 009_macros/mp_getformats.sas delete mode 100644 009_macros/mp_getmaxvarlengths.sas delete mode 100644 009_macros/mp_getpk.sas delete mode 100644 009_macros/mp_gitadd.sas delete mode 100644 009_macros/mp_gitlog.sas delete mode 100644 009_macros/mp_gitreleaseinfo.sas delete mode 100644 009_macros/mp_gitstatus.sas delete mode 100644 009_macros/mp_gsubfile.sas delete mode 100644 009_macros/mp_guesspk.sas delete mode 100644 009_macros/mp_hashdataset.sas delete mode 100644 009_macros/mp_hashdirectory.sas delete mode 100644 009_macros/mp_include.sas delete mode 100644 009_macros/mp_init.sas delete mode 100644 009_macros/mp_jsonout.sas delete mode 100644 009_macros/mp_lib2cards.sas delete mode 100644 009_macros/mp_lib2inserts.sas delete mode 100644 009_macros/mp_lockanytable.sas delete mode 100644 009_macros/mp_lockfilecheck.sas delete mode 100644 009_macros/mp_makedata.sas delete mode 100644 009_macros/mp_md5.sas delete mode 100644 009_macros/mp_perflog.sas delete mode 100644 009_macros/mp_prevobs.sas delete mode 100644 009_macros/mp_recursivejoin.sas delete mode 100644 009_macros/mp_replace.sas delete mode 100644 009_macros/mp_reseterror.sas delete mode 100644 009_macros/mp_resetoption.sas delete mode 100644 009_macros/mp_retainedkey.sas delete mode 100644 009_macros/mp_runddl.sas delete mode 100644 009_macros/mp_searchcols.sas delete mode 100644 009_macros/mp_searchdata.sas delete mode 100644 009_macros/mp_setkeyvalue.sas delete mode 100644 009_macros/mp_sortinplace.sas delete mode 100644 009_macros/mp_stprequests.sas delete mode 100644 009_macros/mp_streamfile.sas delete mode 100644 009_macros/mp_testjob.sas delete mode 100644 009_macros/mp_testservice.sas delete mode 100644 009_macros/mp_testwritespeedlibrary.sas delete mode 100644 009_macros/mp_tree.sas delete mode 100644 009_macros/mp_unzip.sas delete mode 100644 009_macros/mp_updatevarlength.sas delete mode 100644 009_macros/mp_wait4file.sas delete mode 100644 009_macros/mp_webin.sas delete mode 100644 009_macros/mp_zip.sas rename {007_macros => 009_macros}/mx_getgroups.sas (100%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b8c4d7c..e93f607 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,5 +14,5 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} branch: main draft: false - version: 4.52.2 - description: 'Version 4.52.2 of sasjs/core is now on SAS PACKAGES :ok_hand:' + version: 4.52.3 + description: 'Version 4.52.3 of sasjs/core is now on SAS PACKAGES :ok_hand:' diff --git a/005_macros/mf_abort.sas b/001_macros/mf_abort.sas similarity index 100% rename from 005_macros/mf_abort.sas rename to 001_macros/mf_abort.sas diff --git a/005_macros/mf_dedup.sas b/001_macros/mf_dedup.sas similarity index 100% rename from 005_macros/mf_dedup.sas rename to 001_macros/mf_dedup.sas diff --git a/005_macros/mf_deletefile.sas b/001_macros/mf_deletefile.sas similarity index 100% rename from 005_macros/mf_deletefile.sas rename to 001_macros/mf_deletefile.sas diff --git a/005_macros/mf_existds.sas b/001_macros/mf_existds.sas similarity index 100% rename from 005_macros/mf_existds.sas rename to 001_macros/mf_existds.sas diff --git a/005_macros/mf_existfeature.sas b/001_macros/mf_existfeature.sas similarity index 100% rename from 005_macros/mf_existfeature.sas rename to 001_macros/mf_existfeature.sas diff --git a/005_macros/mf_existfileref.sas b/001_macros/mf_existfileref.sas similarity index 100% rename from 005_macros/mf_existfileref.sas rename to 001_macros/mf_existfileref.sas diff --git a/005_macros/mf_existfunction.sas b/001_macros/mf_existfunction.sas similarity index 100% rename from 005_macros/mf_existfunction.sas rename to 001_macros/mf_existfunction.sas diff --git a/005_macros/mf_existvar.sas b/001_macros/mf_existvar.sas similarity index 100% rename from 005_macros/mf_existvar.sas rename to 001_macros/mf_existvar.sas diff --git a/005_macros/mf_existvarlist.sas b/001_macros/mf_existvarlist.sas similarity index 100% rename from 005_macros/mf_existvarlist.sas rename to 001_macros/mf_existvarlist.sas diff --git a/005_macros/mf_fmtdttm.sas b/001_macros/mf_fmtdttm.sas similarity index 100% rename from 005_macros/mf_fmtdttm.sas rename to 001_macros/mf_fmtdttm.sas diff --git a/005_macros/mf_getapploc.sas b/001_macros/mf_getapploc.sas similarity index 100% rename from 005_macros/mf_getapploc.sas rename to 001_macros/mf_getapploc.sas diff --git a/005_macros/mf_getattrc.sas b/001_macros/mf_getattrc.sas similarity index 100% rename from 005_macros/mf_getattrc.sas rename to 001_macros/mf_getattrc.sas diff --git a/005_macros/mf_getattrn.sas b/001_macros/mf_getattrn.sas similarity index 100% rename from 005_macros/mf_getattrn.sas rename to 001_macros/mf_getattrn.sas diff --git a/005_macros/mf_getengine.sas b/001_macros/mf_getengine.sas similarity index 100% rename from 005_macros/mf_getengine.sas rename to 001_macros/mf_getengine.sas diff --git a/005_macros/mf_getfilesize.sas b/001_macros/mf_getfilesize.sas similarity index 100% rename from 005_macros/mf_getfilesize.sas rename to 001_macros/mf_getfilesize.sas diff --git a/005_macros/mf_getfmtlist.sas b/001_macros/mf_getfmtlist.sas similarity index 100% rename from 005_macros/mf_getfmtlist.sas rename to 001_macros/mf_getfmtlist.sas diff --git a/005_macros/mf_getfmtname.sas b/001_macros/mf_getfmtname.sas similarity index 100% rename from 005_macros/mf_getfmtname.sas rename to 001_macros/mf_getfmtname.sas diff --git a/005_macros/mf_getgitbranch.sas b/001_macros/mf_getgitbranch.sas similarity index 100% rename from 005_macros/mf_getgitbranch.sas rename to 001_macros/mf_getgitbranch.sas diff --git a/005_macros/mf_getkeyvalue.sas b/001_macros/mf_getkeyvalue.sas similarity index 100% rename from 005_macros/mf_getkeyvalue.sas rename to 001_macros/mf_getkeyvalue.sas diff --git a/005_macros/mf_getplatform.sas b/001_macros/mf_getplatform.sas similarity index 100% rename from 005_macros/mf_getplatform.sas rename to 001_macros/mf_getplatform.sas diff --git a/005_macros/mf_getquotedstr.sas b/001_macros/mf_getquotedstr.sas similarity index 100% rename from 005_macros/mf_getquotedstr.sas rename to 001_macros/mf_getquotedstr.sas diff --git a/005_macros/mf_getschema.sas b/001_macros/mf_getschema.sas similarity index 100% rename from 005_macros/mf_getschema.sas rename to 001_macros/mf_getschema.sas diff --git a/005_macros/mf_getuniquefileref.sas b/001_macros/mf_getuniquefileref.sas similarity index 100% rename from 005_macros/mf_getuniquefileref.sas rename to 001_macros/mf_getuniquefileref.sas diff --git a/005_macros/mf_getuniquelibref.sas b/001_macros/mf_getuniquelibref.sas similarity index 100% rename from 005_macros/mf_getuniquelibref.sas rename to 001_macros/mf_getuniquelibref.sas diff --git a/005_macros/mf_getuniquename.sas b/001_macros/mf_getuniquename.sas similarity index 100% rename from 005_macros/mf_getuniquename.sas rename to 001_macros/mf_getuniquename.sas diff --git a/005_macros/mf_getuser.sas b/001_macros/mf_getuser.sas similarity index 100% rename from 005_macros/mf_getuser.sas rename to 001_macros/mf_getuser.sas diff --git a/005_macros/mf_getvalue.sas b/001_macros/mf_getvalue.sas similarity index 100% rename from 005_macros/mf_getvalue.sas rename to 001_macros/mf_getvalue.sas diff --git a/005_macros/mf_getvarcount.sas b/001_macros/mf_getvarcount.sas similarity index 100% rename from 005_macros/mf_getvarcount.sas rename to 001_macros/mf_getvarcount.sas diff --git a/005_macros/mf_getvarformat.sas b/001_macros/mf_getvarformat.sas similarity index 100% rename from 005_macros/mf_getvarformat.sas rename to 001_macros/mf_getvarformat.sas diff --git a/005_macros/mf_getvarlen.sas b/001_macros/mf_getvarlen.sas similarity index 100% rename from 005_macros/mf_getvarlen.sas rename to 001_macros/mf_getvarlen.sas diff --git a/005_macros/mf_getvarlist.sas b/001_macros/mf_getvarlist.sas similarity index 100% rename from 005_macros/mf_getvarlist.sas rename to 001_macros/mf_getvarlist.sas diff --git a/005_macros/mf_getvarnum.sas b/001_macros/mf_getvarnum.sas similarity index 100% rename from 005_macros/mf_getvarnum.sas rename to 001_macros/mf_getvarnum.sas diff --git a/005_macros/mf_getvartype.sas b/001_macros/mf_getvartype.sas similarity index 100% rename from 005_macros/mf_getvartype.sas rename to 001_macros/mf_getvartype.sas diff --git a/005_macros/mf_getxengine.sas b/001_macros/mf_getxengine.sas similarity index 100% rename from 005_macros/mf_getxengine.sas rename to 001_macros/mf_getxengine.sas diff --git a/005_macros/mf_increment.sas b/001_macros/mf_increment.sas similarity index 100% rename from 005_macros/mf_increment.sas rename to 001_macros/mf_increment.sas diff --git a/009_macros/mf_isblank.sas b/001_macros/mf_isblank.sas similarity index 100% rename from 009_macros/mf_isblank.sas rename to 001_macros/mf_isblank.sas diff --git a/005_macros/mf_isdir.sas b/001_macros/mf_isdir.sas similarity index 100% rename from 005_macros/mf_isdir.sas rename to 001_macros/mf_isdir.sas diff --git a/005_macros/mf_isint.sas b/001_macros/mf_isint.sas similarity index 100% rename from 005_macros/mf_isint.sas rename to 001_macros/mf_isint.sas diff --git a/005_macros/mf_islibds.sas b/001_macros/mf_islibds.sas similarity index 100% rename from 005_macros/mf_islibds.sas rename to 001_macros/mf_islibds.sas diff --git a/005_macros/mf_loc.sas b/001_macros/mf_loc.sas similarity index 100% rename from 005_macros/mf_loc.sas rename to 001_macros/mf_loc.sas diff --git a/005_macros/mf_mkdir.sas b/001_macros/mf_mkdir.sas similarity index 100% rename from 005_macros/mf_mkdir.sas rename to 001_macros/mf_mkdir.sas diff --git a/005_macros/mf_mval.sas b/001_macros/mf_mval.sas similarity index 100% rename from 005_macros/mf_mval.sas rename to 001_macros/mf_mval.sas diff --git a/005_macros/mf_nobs.sas b/001_macros/mf_nobs.sas similarity index 100% rename from 005_macros/mf_nobs.sas rename to 001_macros/mf_nobs.sas diff --git a/005_macros/mf_readfile.sas b/001_macros/mf_readfile.sas similarity index 100% rename from 005_macros/mf_readfile.sas rename to 001_macros/mf_readfile.sas diff --git a/005_macros/mf_trimstr.sas b/001_macros/mf_trimstr.sas similarity index 100% rename from 005_macros/mf_trimstr.sas rename to 001_macros/mf_trimstr.sas diff --git a/005_macros/mf_uid.sas b/001_macros/mf_uid.sas similarity index 100% rename from 005_macros/mf_uid.sas rename to 001_macros/mf_uid.sas diff --git a/005_macros/mf_verifymacvars.sas b/001_macros/mf_verifymacvars.sas similarity index 100% rename from 005_macros/mf_verifymacvars.sas rename to 001_macros/mf_verifymacvars.sas diff --git a/005_macros/mf_wordsinstr1andstr2.sas b/001_macros/mf_wordsinstr1andstr2.sas similarity index 100% rename from 005_macros/mf_wordsinstr1andstr2.sas rename to 001_macros/mf_wordsinstr1andstr2.sas diff --git a/005_macros/mf_wordsinstr1butnotstr2.sas b/001_macros/mf_wordsinstr1butnotstr2.sas similarity index 100% rename from 005_macros/mf_wordsinstr1butnotstr2.sas rename to 001_macros/mf_wordsinstr1butnotstr2.sas diff --git a/005_macros/mf_writefile.sas b/001_macros/mf_writefile.sas similarity index 100% rename from 005_macros/mf_writefile.sas rename to 001_macros/mf_writefile.sas diff --git a/005_macros/mp_abort.sas b/001_macros/mp_abort.sas similarity index 100% rename from 005_macros/mp_abort.sas rename to 001_macros/mp_abort.sas diff --git a/005_macros/mp_aligndecimal.sas b/001_macros/mp_aligndecimal.sas similarity index 100% rename from 005_macros/mp_aligndecimal.sas rename to 001_macros/mp_aligndecimal.sas diff --git a/005_macros/mp_appendfile.sas b/001_macros/mp_appendfile.sas similarity index 100% rename from 005_macros/mp_appendfile.sas rename to 001_macros/mp_appendfile.sas diff --git a/005_macros/mp_applyformats.sas b/001_macros/mp_applyformats.sas similarity index 100% rename from 005_macros/mp_applyformats.sas rename to 001_macros/mp_applyformats.sas diff --git a/005_macros/mp_assert.sas b/001_macros/mp_assert.sas similarity index 100% rename from 005_macros/mp_assert.sas rename to 001_macros/mp_assert.sas diff --git a/005_macros/mp_assertcols.sas b/001_macros/mp_assertcols.sas similarity index 100% rename from 005_macros/mp_assertcols.sas rename to 001_macros/mp_assertcols.sas diff --git a/005_macros/mp_assertcolvals.sas b/001_macros/mp_assertcolvals.sas similarity index 100% rename from 005_macros/mp_assertcolvals.sas rename to 001_macros/mp_assertcolvals.sas diff --git a/005_macros/mp_assertdsobs.sas b/001_macros/mp_assertdsobs.sas similarity index 100% rename from 005_macros/mp_assertdsobs.sas rename to 001_macros/mp_assertdsobs.sas diff --git a/005_macros/mp_assertscope.sas b/001_macros/mp_assertscope.sas similarity index 100% rename from 005_macros/mp_assertscope.sas rename to 001_macros/mp_assertscope.sas diff --git a/005_macros/mp_base64copy.sas b/001_macros/mp_base64copy.sas similarity index 100% rename from 005_macros/mp_base64copy.sas rename to 001_macros/mp_base64copy.sas diff --git a/005_macros/mp_binarycopy.sas b/001_macros/mp_binarycopy.sas similarity index 100% rename from 005_macros/mp_binarycopy.sas rename to 001_macros/mp_binarycopy.sas diff --git a/005_macros/mp_chop.sas b/001_macros/mp_chop.sas similarity index 100% rename from 005_macros/mp_chop.sas rename to 001_macros/mp_chop.sas diff --git a/005_macros/mp_cleancsv.sas b/001_macros/mp_cleancsv.sas similarity index 100% rename from 005_macros/mp_cleancsv.sas rename to 001_macros/mp_cleancsv.sas diff --git a/009_macros/mp_cntlout.sas b/001_macros/mp_cntlout.sas similarity index 100% rename from 009_macros/mp_cntlout.sas rename to 001_macros/mp_cntlout.sas diff --git a/005_macros/mp_copyfolder.sas b/001_macros/mp_copyfolder.sas similarity index 100% rename from 005_macros/mp_copyfolder.sas rename to 001_macros/mp_copyfolder.sas diff --git a/005_macros/mp_coretable.sas b/001_macros/mp_coretable.sas similarity index 100% rename from 005_macros/mp_coretable.sas rename to 001_macros/mp_coretable.sas diff --git a/005_macros/mp_createconstraints.sas b/001_macros/mp_createconstraints.sas similarity index 100% rename from 005_macros/mp_createconstraints.sas rename to 001_macros/mp_createconstraints.sas diff --git a/005_macros/mp_createwebservice.sas b/001_macros/mp_createwebservice.sas similarity index 100% rename from 005_macros/mp_createwebservice.sas rename to 001_macros/mp_createwebservice.sas diff --git a/005_macros/mp_csv2ds.sas b/001_macros/mp_csv2ds.sas similarity index 100% rename from 005_macros/mp_csv2ds.sas rename to 001_macros/mp_csv2ds.sas diff --git a/005_macros/mp_deleteconstraints.sas b/001_macros/mp_deleteconstraints.sas similarity index 100% rename from 005_macros/mp_deleteconstraints.sas rename to 001_macros/mp_deleteconstraints.sas diff --git a/005_macros/mp_deletefolder.sas b/001_macros/mp_deletefolder.sas similarity index 100% rename from 005_macros/mp_deletefolder.sas rename to 001_macros/mp_deletefolder.sas diff --git a/005_macros/mp_dictionary.sas b/001_macros/mp_dictionary.sas similarity index 100% rename from 005_macros/mp_dictionary.sas rename to 001_macros/mp_dictionary.sas diff --git a/009_macros/mp_dirlist.sas b/001_macros/mp_dirlist.sas similarity index 100% rename from 009_macros/mp_dirlist.sas rename to 001_macros/mp_dirlist.sas diff --git a/005_macros/mp_distinctfmtvalues.sas b/001_macros/mp_distinctfmtvalues.sas similarity index 100% rename from 005_macros/mp_distinctfmtvalues.sas rename to 001_macros/mp_distinctfmtvalues.sas diff --git a/005_macros/mp_dropmembers.sas b/001_macros/mp_dropmembers.sas similarity index 100% rename from 005_macros/mp_dropmembers.sas rename to 001_macros/mp_dropmembers.sas diff --git a/005_macros/mp_ds2cards.sas b/001_macros/mp_ds2cards.sas similarity index 100% rename from 005_macros/mp_ds2cards.sas rename to 001_macros/mp_ds2cards.sas diff --git a/009_macros/mp_ds2csv.sas b/001_macros/mp_ds2csv.sas similarity index 100% rename from 009_macros/mp_ds2csv.sas rename to 001_macros/mp_ds2csv.sas diff --git a/005_macros/mp_ds2ddl.sas b/001_macros/mp_ds2ddl.sas similarity index 100% rename from 005_macros/mp_ds2ddl.sas rename to 001_macros/mp_ds2ddl.sas diff --git a/005_macros/mp_ds2fmtds.sas b/001_macros/mp_ds2fmtds.sas similarity index 100% rename from 005_macros/mp_ds2fmtds.sas rename to 001_macros/mp_ds2fmtds.sas diff --git a/005_macros/mp_ds2inserts.sas b/001_macros/mp_ds2inserts.sas similarity index 100% rename from 005_macros/mp_ds2inserts.sas rename to 001_macros/mp_ds2inserts.sas diff --git a/005_macros/mp_ds2md.sas b/001_macros/mp_ds2md.sas similarity index 100% rename from 005_macros/mp_ds2md.sas rename to 001_macros/mp_ds2md.sas diff --git a/005_macros/mp_ds2squeeze.sas b/001_macros/mp_ds2squeeze.sas similarity index 100% rename from 005_macros/mp_ds2squeeze.sas rename to 001_macros/mp_ds2squeeze.sas diff --git a/005_macros/mp_dsmeta.sas b/001_macros/mp_dsmeta.sas similarity index 100% rename from 005_macros/mp_dsmeta.sas rename to 001_macros/mp_dsmeta.sas diff --git a/009_macros/mp_filtercheck.sas b/001_macros/mp_filtercheck.sas similarity index 100% rename from 009_macros/mp_filtercheck.sas rename to 001_macros/mp_filtercheck.sas diff --git a/005_macros/mp_filtergenerate.sas b/001_macros/mp_filtergenerate.sas similarity index 100% rename from 005_macros/mp_filtergenerate.sas rename to 001_macros/mp_filtergenerate.sas diff --git a/005_macros/mp_filterstore.sas b/001_macros/mp_filterstore.sas similarity index 100% rename from 005_macros/mp_filterstore.sas rename to 001_macros/mp_filterstore.sas diff --git a/005_macros/mp_filtervalidate.sas b/001_macros/mp_filtervalidate.sas similarity index 100% rename from 005_macros/mp_filtervalidate.sas rename to 001_macros/mp_filtervalidate.sas diff --git a/005_macros/mp_getcols.sas b/001_macros/mp_getcols.sas similarity index 100% rename from 005_macros/mp_getcols.sas rename to 001_macros/mp_getcols.sas diff --git a/005_macros/mp_getconstraints.sas b/001_macros/mp_getconstraints.sas similarity index 100% rename from 005_macros/mp_getconstraints.sas rename to 001_macros/mp_getconstraints.sas diff --git a/005_macros/mp_getdbml.sas b/001_macros/mp_getdbml.sas similarity index 100% rename from 005_macros/mp_getdbml.sas rename to 001_macros/mp_getdbml.sas diff --git a/005_macros/mp_getddl.sas b/001_macros/mp_getddl.sas similarity index 100% rename from 005_macros/mp_getddl.sas rename to 001_macros/mp_getddl.sas diff --git a/005_macros/mp_getformats.sas b/001_macros/mp_getformats.sas similarity index 100% rename from 005_macros/mp_getformats.sas rename to 001_macros/mp_getformats.sas diff --git a/005_macros/mp_getmaxvarlengths.sas b/001_macros/mp_getmaxvarlengths.sas similarity index 100% rename from 005_macros/mp_getmaxvarlengths.sas rename to 001_macros/mp_getmaxvarlengths.sas diff --git a/005_macros/mp_getpk.sas b/001_macros/mp_getpk.sas similarity index 100% rename from 005_macros/mp_getpk.sas rename to 001_macros/mp_getpk.sas diff --git a/005_macros/mp_gitadd.sas b/001_macros/mp_gitadd.sas similarity index 100% rename from 005_macros/mp_gitadd.sas rename to 001_macros/mp_gitadd.sas diff --git a/005_macros/mp_gitlog.sas b/001_macros/mp_gitlog.sas similarity index 100% rename from 005_macros/mp_gitlog.sas rename to 001_macros/mp_gitlog.sas diff --git a/005_macros/mp_gitreleaseinfo.sas b/001_macros/mp_gitreleaseinfo.sas similarity index 100% rename from 005_macros/mp_gitreleaseinfo.sas rename to 001_macros/mp_gitreleaseinfo.sas diff --git a/005_macros/mp_gitstatus.sas b/001_macros/mp_gitstatus.sas similarity index 100% rename from 005_macros/mp_gitstatus.sas rename to 001_macros/mp_gitstatus.sas diff --git a/005_macros/mp_gsubfile.sas b/001_macros/mp_gsubfile.sas similarity index 100% rename from 005_macros/mp_gsubfile.sas rename to 001_macros/mp_gsubfile.sas diff --git a/005_macros/mp_guesspk.sas b/001_macros/mp_guesspk.sas similarity index 100% rename from 005_macros/mp_guesspk.sas rename to 001_macros/mp_guesspk.sas diff --git a/005_macros/mp_hashdataset.sas b/001_macros/mp_hashdataset.sas similarity index 100% rename from 005_macros/mp_hashdataset.sas rename to 001_macros/mp_hashdataset.sas diff --git a/005_macros/mp_hashdirectory.sas b/001_macros/mp_hashdirectory.sas similarity index 100% rename from 005_macros/mp_hashdirectory.sas rename to 001_macros/mp_hashdirectory.sas diff --git a/005_macros/mp_include.sas b/001_macros/mp_include.sas similarity index 100% rename from 005_macros/mp_include.sas rename to 001_macros/mp_include.sas diff --git a/005_macros/mp_init.sas b/001_macros/mp_init.sas similarity index 100% rename from 005_macros/mp_init.sas rename to 001_macros/mp_init.sas diff --git a/005_macros/mp_jsonout.sas b/001_macros/mp_jsonout.sas similarity index 100% rename from 005_macros/mp_jsonout.sas rename to 001_macros/mp_jsonout.sas diff --git a/005_macros/mp_lib2cards.sas b/001_macros/mp_lib2cards.sas similarity index 100% rename from 005_macros/mp_lib2cards.sas rename to 001_macros/mp_lib2cards.sas diff --git a/005_macros/mp_lib2inserts.sas b/001_macros/mp_lib2inserts.sas similarity index 100% rename from 005_macros/mp_lib2inserts.sas rename to 001_macros/mp_lib2inserts.sas diff --git a/009_macros/mp_loadformat.sas b/001_macros/mp_loadformat.sas similarity index 100% rename from 009_macros/mp_loadformat.sas rename to 001_macros/mp_loadformat.sas diff --git a/005_macros/mp_lockanytable.sas b/001_macros/mp_lockanytable.sas similarity index 100% rename from 005_macros/mp_lockanytable.sas rename to 001_macros/mp_lockanytable.sas diff --git a/005_macros/mp_lockfilecheck.sas b/001_macros/mp_lockfilecheck.sas similarity index 100% rename from 005_macros/mp_lockfilecheck.sas rename to 001_macros/mp_lockfilecheck.sas diff --git a/005_macros/mp_makedata.sas b/001_macros/mp_makedata.sas similarity index 100% rename from 005_macros/mp_makedata.sas rename to 001_macros/mp_makedata.sas diff --git a/005_macros/mp_md5.sas b/001_macros/mp_md5.sas similarity index 100% rename from 005_macros/mp_md5.sas rename to 001_macros/mp_md5.sas diff --git a/005_macros/mp_perflog.sas b/001_macros/mp_perflog.sas similarity index 100% rename from 005_macros/mp_perflog.sas rename to 001_macros/mp_perflog.sas diff --git a/005_macros/mp_prevobs.sas b/001_macros/mp_prevobs.sas similarity index 100% rename from 005_macros/mp_prevobs.sas rename to 001_macros/mp_prevobs.sas diff --git a/005_macros/mp_recursivejoin.sas b/001_macros/mp_recursivejoin.sas similarity index 100% rename from 005_macros/mp_recursivejoin.sas rename to 001_macros/mp_recursivejoin.sas diff --git a/005_macros/mp_replace.sas b/001_macros/mp_replace.sas similarity index 100% rename from 005_macros/mp_replace.sas rename to 001_macros/mp_replace.sas diff --git a/005_macros/mp_reseterror.sas b/001_macros/mp_reseterror.sas similarity index 100% rename from 005_macros/mp_reseterror.sas rename to 001_macros/mp_reseterror.sas diff --git a/005_macros/mp_resetoption.sas b/001_macros/mp_resetoption.sas similarity index 100% rename from 005_macros/mp_resetoption.sas rename to 001_macros/mp_resetoption.sas diff --git a/005_macros/mp_retainedkey.sas b/001_macros/mp_retainedkey.sas similarity index 100% rename from 005_macros/mp_retainedkey.sas rename to 001_macros/mp_retainedkey.sas diff --git a/005_macros/mp_runddl.sas b/001_macros/mp_runddl.sas similarity index 100% rename from 005_macros/mp_runddl.sas rename to 001_macros/mp_runddl.sas diff --git a/005_macros/mp_searchcols.sas b/001_macros/mp_searchcols.sas similarity index 100% rename from 005_macros/mp_searchcols.sas rename to 001_macros/mp_searchcols.sas diff --git a/005_macros/mp_searchdata.sas b/001_macros/mp_searchdata.sas similarity index 100% rename from 005_macros/mp_searchdata.sas rename to 001_macros/mp_searchdata.sas diff --git a/005_macros/mp_setkeyvalue.sas b/001_macros/mp_setkeyvalue.sas similarity index 100% rename from 005_macros/mp_setkeyvalue.sas rename to 001_macros/mp_setkeyvalue.sas diff --git a/005_macros/mp_sortinplace.sas b/001_macros/mp_sortinplace.sas similarity index 100% rename from 005_macros/mp_sortinplace.sas rename to 001_macros/mp_sortinplace.sas diff --git a/009_macros/mp_stackdiffs.sas b/001_macros/mp_stackdiffs.sas similarity index 100% rename from 009_macros/mp_stackdiffs.sas rename to 001_macros/mp_stackdiffs.sas diff --git a/009_macros/mp_storediffs.sas b/001_macros/mp_storediffs.sas similarity index 100% rename from 009_macros/mp_storediffs.sas rename to 001_macros/mp_storediffs.sas diff --git a/005_macros/mp_stprequests.sas b/001_macros/mp_stprequests.sas similarity index 100% rename from 005_macros/mp_stprequests.sas rename to 001_macros/mp_stprequests.sas diff --git a/005_macros/mp_streamfile.sas b/001_macros/mp_streamfile.sas similarity index 100% rename from 005_macros/mp_streamfile.sas rename to 001_macros/mp_streamfile.sas diff --git a/009_macros/mp_stripdiffs.sas b/001_macros/mp_stripdiffs.sas similarity index 100% rename from 009_macros/mp_stripdiffs.sas rename to 001_macros/mp_stripdiffs.sas diff --git a/005_macros/mp_testjob.sas b/001_macros/mp_testjob.sas similarity index 100% rename from 005_macros/mp_testjob.sas rename to 001_macros/mp_testjob.sas diff --git a/005_macros/mp_testservice.sas b/001_macros/mp_testservice.sas similarity index 100% rename from 005_macros/mp_testservice.sas rename to 001_macros/mp_testservice.sas diff --git a/005_macros/mp_testwritespeedlibrary.sas b/001_macros/mp_testwritespeedlibrary.sas similarity index 100% rename from 005_macros/mp_testwritespeedlibrary.sas rename to 001_macros/mp_testwritespeedlibrary.sas diff --git a/005_macros/mp_tree.sas b/001_macros/mp_tree.sas similarity index 100% rename from 005_macros/mp_tree.sas rename to 001_macros/mp_tree.sas diff --git a/005_macros/mp_unzip.sas b/001_macros/mp_unzip.sas similarity index 100% rename from 005_macros/mp_unzip.sas rename to 001_macros/mp_unzip.sas diff --git a/005_macros/mp_updatevarlength.sas b/001_macros/mp_updatevarlength.sas similarity index 100% rename from 005_macros/mp_updatevarlength.sas rename to 001_macros/mp_updatevarlength.sas diff --git a/009_macros/mp_validatecol.sas b/001_macros/mp_validatecol.sas similarity index 100% rename from 009_macros/mp_validatecol.sas rename to 001_macros/mp_validatecol.sas diff --git a/005_macros/mp_wait4file.sas b/001_macros/mp_wait4file.sas similarity index 100% rename from 005_macros/mp_wait4file.sas rename to 001_macros/mp_wait4file.sas diff --git a/005_macros/mp_webin.sas b/001_macros/mp_webin.sas similarity index 100% rename from 005_macros/mp_webin.sas rename to 001_macros/mp_webin.sas diff --git a/005_macros/mp_zip.sas b/001_macros/mp_zip.sas similarity index 100% rename from 005_macros/mp_zip.sas rename to 001_macros/mp_zip.sas diff --git a/004_macros/mddl_dc_difftable.sas b/002_macros/mddl_dc_difftable.sas similarity index 100% rename from 004_macros/mddl_dc_difftable.sas rename to 002_macros/mddl_dc_difftable.sas diff --git a/004_macros/mddl_dc_filterdetail.sas b/002_macros/mddl_dc_filterdetail.sas similarity index 100% rename from 004_macros/mddl_dc_filterdetail.sas rename to 002_macros/mddl_dc_filterdetail.sas diff --git a/004_macros/mddl_dc_filtersummary.sas b/002_macros/mddl_dc_filtersummary.sas similarity index 100% rename from 004_macros/mddl_dc_filtersummary.sas rename to 002_macros/mddl_dc_filtersummary.sas diff --git a/004_macros/mddl_dc_locktable.sas b/002_macros/mddl_dc_locktable.sas similarity index 100% rename from 004_macros/mddl_dc_locktable.sas rename to 002_macros/mddl_dc_locktable.sas diff --git a/004_macros/mddl_dc_maxkeytable.sas b/002_macros/mddl_dc_maxkeytable.sas similarity index 100% rename from 004_macros/mddl_dc_maxkeytable.sas rename to 002_macros/mddl_dc_maxkeytable.sas diff --git a/004_macros/mddl_sas_cntlout.sas b/002_macros/mddl_sas_cntlout.sas similarity index 100% rename from 004_macros/mddl_sas_cntlout.sas rename to 002_macros/mddl_sas_cntlout.sas diff --git a/002_macros/mm_assigndirectlib.sas b/002_macros/mm_assigndirectlib.sas deleted file mode 100644 index 8d118db..0000000 --- a/002_macros/mm_assigndirectlib.sas +++ /dev/null @@ -1,461 +0,0 @@ -/*** HELP START ***//** - @file - @brief Assigns library directly using details from metadata - @details Queries metadata to get the libname definition then allocates the - library directly (ie, not using the META engine). - usage: - - %mm_assignDirectLib(MyLib); - data x; set mylib.sometable; run; - - %mm_assignDirectLib(MyDB,open_passthrough=MyAlias); - create table MyTable as - select * from connection to MyAlias( select * from DBTable); - disconnect from MyAlias; - quit; - -

SAS Macros

- @li mf_getengine.sas - @li mp_abort.sas - - @param [in] libref the libref (not name) of the metadata library - @param [in] open_passthrough= () Provide an alias to produce the CONNECT TO - statement for the relevant external database - @param [in] sql_options= () Add any options to add to proc sql statement, - eg outobs= (only valid for pass through) - @param [in] mDebug= (0) set to 1 to show debug messages in the log - @param [in] mAbort= (0) set to 1 to call %mp_abort(). - - @returns libname statement - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_assigndirectlib( - libref - ,open_passthrough= - ,sql_options= - ,mDebug=0 - ,mAbort=0 -)/*/STORE SOURCE*/; - -%local mD; -%if &mDebug=1 %then %let mD=; -%else %let mD=%str(*); -%&mD.put Executing mm_assigndirectlib.sas; -%&mD.put _local_; - -%if &mAbort=1 %then %let mAbort=; -%else %let mAbort=%str(*); - -%&mD.put NOTE: Creating direct (non META) connection to &libref library; - -%local cur_engine; -%let cur_engine=%mf_getengine(&libref); -%if &cur_engine ne META and &cur_engine ne %then %do; - %put NOTE: &libref already has a direct (&cur_engine) libname connection; - %return; -%end; -%else %if %upcase(&libref)=WORK %then %do; - %put NOTE: We already have a direct connection to WORK :-) ; - %return; -%end; - -/* need to determine the library ENGINE first */ -%local engine; -data _null_; - length lib_uri engine $256; - call missing (of _all_); - /* get URI for the particular library */ - rc1=metadata_getnobj("omsobj:SASLibrary?@Libref ='&libref'",1,lib_uri); - /* get the Engine attribute of the previous object */ - rc2=metadata_getattr(lib_uri,'Engine',engine); - putlog "mm_assigndirectlib for &libref:" rc1= lib_uri= rc2= engine=; - call symputx("liburi",lib_uri,'l'); - call symputx("engine",engine,'l'); -run; - -/* now obtain engine specific connection details */ -%if &engine=BASE %then %do; - %&mD.put NOTE: Retrieving BASE library path; - data _null_; - length up_uri $256 path cat_path $1024; - retain cat_path; - call missing (of _all_); - /* get all the filepaths of the UsingPackages association */ - i=1; - rc3=metadata_getnasn("&liburi",'UsingPackages',i,up_uri); - do while (rc3>0); - /* get the DirectoryName attribute of the previous object */ - rc4=metadata_getattr(up_uri,'DirectoryName',path); - if i=1 then path = '("'!!trim(path)!!'" '; - else path =' "'!!trim(path)!!'" '; - cat_path = trim(cat_path) !! " " !! trim(path) ; - i+1; - rc3=metadata_getnasn("&liburi",'UsingPackages',i,up_uri); - end; - cat_path = trim(cat_path) !! ")"; - &mD.putlog "NOTE: Getting physical path for &libref library"; - &mD.putlog rc3= up_uri= rc4= cat_path= path=; - &mD.putlog "NOTE: Libname cmd will be:"; - &mD.putlog "libname &libref" cat_path; - call symputx("filepath",cat_path,'l'); - run; - - %if %sysevalf(&sysver<9.4) %then %do; - libname &libref &filepath; - %end; - %else %do; - /* apply the new filelocks option to cater for temporary locks */ - libname &libref &filepath filelockwait=5; - %end; - -%end; -%else %if &engine=REMOTE %then %do; - data x; - length rcCon rcProp rc k 3 uriCon uriProp PropertyValue PropertyName - Delimiter $256 properties $2048; - retain properties; - rcCon = metadata_getnasn("&liburi", "LibraryConnection", 1, uriCon); - - rcProp = metadata_getnasn(uriCon, "Properties", 1, uriProp); - - k = 1; - rcProp = metadata_getnasn(uriCon, "Properties", k, uriProp); - do while (rcProp > 0); - rc = metadata_getattr(uriProp , "DefaultValue",PropertyValue); - rc = metadata_getattr(uriProp , "PropertyName",PropertyName); - rc = metadata_getattr(uriProp , "Delimiter",Delimiter); - properties = trim(properties) !! " " !! trim(PropertyName) - !! trim(Delimiter) !! trim(PropertyValue); - output; - k+1; - rcProp = metadata_getnasn(uriCon, "Properties", k, uriProp); - end; - %&mD.put NOTE: Getting properties for REMOTE SHARE &libref library; - &mD.put _all_; - %&mD.put NOTE: Libname cmd will be:; - %&mD.put libname &libref &engine &properties slibref=&libref; - call symputx ("properties",trim(properties),'l'); - run; - - libname &libref &engine &properties slibref=&libref; - -%end; - -%else %if &engine=OLEDB %then %do; - %&mD.put NOTE: Retrieving OLEDB connection details; - data _null_; - length domain datasource provider properties schema - connx_uri domain_uri conprop_uri lib_uri schema_uri value $256.; - call missing (of _all_); - /* get source connection ID */ - rc=metadata_getnasn("&liburi",'LibraryConnection',1,connx_uri); - /* get connection domain */ - rc1=metadata_getnasn(connx_uri,'Domain',1,domain_uri); - rc2=metadata_getattr(domain_uri,'Name',domain); - &mD.putlog / 'NOTE: ' // 'NOTE- connection id: ' connx_uri ; - &mD.putlog 'NOTE- domain: ' domain; - /* get DSN and PROVIDER from connection properties */ - i=0; - do until (rc<0); - i+1; - rc=metadata_getnasn(connx_uri,'Properties',i,conprop_uri); - rc2=metadata_getattr(conprop_uri,'Name',value); - if value='Connection.OLE.Property.DATASOURCE.Name.xmlKey.txt' then do; - rc3=metadata_getattr(conprop_uri,'DefaultValue',datasource); - end; - else if value='Connection.OLE.Property.PROVIDER.Name.xmlKey.txt' then do; - rc4=metadata_getattr(conprop_uri,'DefaultValue',provider); - end; - else if value='Connection.OLE.Property.PROPERTIES.Name.xmlKey.txt' then - do; - rc5=metadata_getattr(conprop_uri,'DefaultValue',properties); - end; - end; - &mD.putlog 'NOTE- dsn/provider/properties: ' / - datasource provider properties; - &mD.putlog 'NOTE- schema: ' schema // 'NOTE-'; - - /* get SCHEMA */ - rc6=metadata_getnasn("&liburi",'UsingPackages',1,lib_uri); - rc7=metadata_getattr(lib_uri,'SchemaName',schema); - call symputx('SQL_domain',domain,'l'); - call symputx('SQL_dsn',datasource,'l'); - call symputx('SQL_provider',provider,'l'); - call symputx('SQL_properties',properties,'l'); - call symputx('SQL_schema',schema,'l'); - run; - - %if %length(&open_passthrough)>0 %then %do; - proc sql &sql_options; - connect to OLEDB as &open_passthrough(INSERT_SQL=YES - /* need additional properties to make this work */ - properties=('Integrated Security'=SSPI - 'Persist Security Info'=True - %sysfunc(compress(%str(&SQL_properties),%str(()))) - ) - DATASOURCE=&sql_dsn PROMPT=NO - PROVIDER=&sql_provider SCHEMA=&sql_schema CONNECTION = GLOBAL); - %end; - %else %do; - LIBNAME &libref OLEDB PROPERTIES=&sql_properties - DATASOURCE=&sql_dsn PROVIDER=&sql_provider SCHEMA=&sql_schema - %if %length(&sql_domain)>0 %then %do; - authdomain="&sql_domain" - %end; - connection=shared; - %end; -%end; -%else %if &engine=ODBC %then %do; - &mD.%put NOTE: Retrieving ODBC connection details; - data _null_; - length connx_uri conprop_uri value datasource up_uri schema $256.; - call missing (of _all_); - /* get source connection ID */ - rc=metadata_getnasn("&liburi",'LibraryConnection',1,connx_uri); - /* get connection properties */ - i=0; - do until (rc2<0); - i+1; - rc2=metadata_getnasn(connx_uri,'Properties',i,conprop_uri); - rc3=metadata_getattr(conprop_uri,'Name',value); - if value='Connection.ODBC.Property.DATASRC.Name.xmlKey.txt' then do; - rc4=metadata_getattr(conprop_uri,'DefaultValue',datasource); - rc2=-1; - end; - end; - /* get SCHEMA */ - rc6=metadata_getnasn("&liburi",'UsingPackages',1,up_uri); - rc7=metadata_getattr(up_uri,'SchemaName',schema); - &mD.put rc= connx_uri= rc2= conprop_uri= rc3= value= rc4= datasource= - rc6= up_uri= rc7= schema=; - - call symputx('SQL_schema',schema,'l'); - call symputx('SQL_dsn',datasource,'l'); - run; - - %if %length(&open_passthrough)>0 %then %do; - proc sql &sql_options; - connect to ODBC as &open_passthrough - (INSERT_SQL=YES DATASRC=&sql_dsn. CONNECTION=global); - %end; - %else %do; - libname &libref ODBC DATASRC=&sql_dsn SCHEMA=&sql_schema; - %end; -%end; -%else %if &engine=POSTGRES %then %do; - %put NOTE: Obtaining POSTGRES library details; - data _null_; - length database ignore_read_only_columns direct_exe preserve_col_names - preserve_tab_names server schema authdomain user password - prop name value uri urisrc $256.; - call missing (of _all_); - /* get database value */ - prop='Connection.DBMS.Property.DB.Name.xmlKey.txt'; - rc=metadata_getprop("&liburi",prop,database,""); - if database^='' then database='database='!!quote(trim(database)); - call symputx('database',database,'l'); - - /* get IGNORE_READ_ONLY_COLUMNS value */ - prop='Library.DBMS.Property.DBIROC.Name.xmlKey.txt'; - rc=metadata_getprop("&liburi",prop,ignore_read_only_columns,""); - if ignore_read_only_columns^='' then ignore_read_only_columns= - 'ignore_read_only_columns='!!ignore_read_only_columns; - call symputx('ignore_read_only_columns',ignore_read_only_columns,'l'); - - /* get DIRECT_EXE value */ - prop='Library.DBMS.Property.DirectExe.Name.xmlKey.txt'; - rc=metadata_getprop("&liburi",prop,direct_exe,""); - if direct_exe^='' then direct_exe='direct_exe='!!direct_exe; - call symputx('direct_exe',direct_exe,'l'); - - /* get PRESERVE_COL_NAMES value */ - prop='Library.DBMS.Property.PreserveColNames.Name.xmlKey.txt'; - rc=metadata_getprop("&liburi",prop,preserve_col_names,""); - if preserve_col_names^='' then preserve_col_names= - 'preserve_col_names='!!preserve_col_names; - call symputx('preserve_col_names',preserve_col_names,'l'); - - /* get PRESERVE_TAB_NAMES value */ - /* be careful with PRESERVE_TAB_NAMES=YES - it will mean your table will - become case sensitive!! */ - prop='Library.DBMS.Property.PreserveTabNames.Name.xmlKey.txt'; - rc=metadata_getprop("&liburi",prop,preserve_tab_names,""); - if preserve_tab_names^='' then preserve_tab_names= - 'preserve_tab_names='!!preserve_tab_names; - call symputx('preserve_tab_names',preserve_tab_names,'l'); - - /* get SERVER value */ - if metadata_getnasn("&liburi","LibraryConnection",1,uri)>0 then do; - prop='Connection.DBMS.Property.SERVER.Name.xmlKey.txt'; - rc=metadata_getprop(uri,prop,server,""); - end; - if server^='' then server='server='!!quote(cats(server)); - call symputx('server',server,'l'); - - /* get SCHEMA value */ - if metadata_getnasn("&liburi","UsingPackages",1,uri)>0 then do; - rc=metadata_getattr(uri,"SchemaName",schema); - end; - if schema^='' then schema='schema='!!schema; - call symputx('schema',schema,'l'); - - /* get AUTHDOMAIN value */ - /* this is only useful if the user account contains that auth domain - if metadata_getnasn("&liburi","DefaultLogin",1,uri)>0 then do; - rc=metadata_getnasn(uri,"Domain",1,urisrc); - rc=metadata_getattr(urisrc,"Name",authdomain); - end; - if authdomain^='' then authdomain='authdomain='!!quote(trim(authdomain)); - */ - call symputx('authdomain',authdomain,'l'); - - /* get user & pass */ - if authdomain='' & metadata_getnasn("&liburi","DefaultLogin",1,uri)>0 then - do; - rc=metadata_getattr(uri,"UserID",user); - rc=metadata_getattr(uri,"Password",password); - end; - if user^='' then do; - user='user='!!quote(trim(user)); - password='password='!!quote(trim(password)); - end; - call symputx('user',user,'l'); - call symputx('password',password,'l'); - - &md.put _all_; - run; - - %if %length(&open_passthrough)>0 %then %do; - %put %str(WARN)ING: Passthrough option for postgres not yet supported; - %return; - %end; - %else %do; - %if &mdebug=1 %then %do; - %put NOTE: Executing the following:/; - %put NOTE- libname &libref POSTGRES &database &ignore_read_only_columns; - %put NOTE- &direct_exe &preserve_col_names &preserve_tab_names; - %put NOTE- &server &schema &authdomain &user &password //; - %end; - libname &libref POSTGRES &database &ignore_read_only_columns &direct_exe - &preserve_col_names &preserve_tab_names &server &schema &authdomain - &user &password; - %end; -%end; -%else %if &engine=ORACLE %then %do; - %put NOTE: Obtaining &engine library details; - data _null_; - length assocuri1 assocuri2 assocuri3 authdomain path schema $256; - call missing (of _all_); - - /* get auth domain */ - rc=metadata_getnasn("&liburi",'LibraryConnection',1,assocuri1); - rc=metadata_getnasn(assocuri1,'Domain',1,assocuri2); - rc=metadata_getattr(assocuri2,"Name",authdomain); - call symputx('authdomain',authdomain,'l'); - - /* path */ - rc=metadata_getprop(assocuri1, - 'Connection.Oracle.Property.PATH.Name.xmlKey.txt',path); - call symputx('path',path,'l'); - - /* schema */ - rc=metadata_getnasn("&liburi",'UsingPackages',1,assocuri3); - rc=metadata_getattr(assocuri3,'SchemaName',schema); - call symputx('schema',schema,'l'); - run; - %put NOTE: Executing the following:/; %put NOTE-; - %put NOTE- libname &libref ORACLE path=&path schema=&schema; - %put NOTE- authdomain=&authdomain; - %put NOTE-; - libname &libref ORACLE path=&path schema=&schema authdomain=&authdomain; -%end; -%else %if &engine=SQLSVR %then %do; - %put NOTE: Obtaining &engine library details; - data _null; - length assocuri1 assocuri2 assocuri3 authdomain path schema userid - passwd $256; - call missing (of _all_); - - rc=metadata_getnasn("&liburi",'DefaultLogin',1,assocuri1); - rc=metadata_getattr(assocuri1,"UserID",userid); - rc=metadata_getattr(assocuri1,"Password",passwd); - call symputx('user',userid,'l'); - call symputx('pass',passwd,'l'); - - /* path */ - rc=metadata_getnasn("&liburi",'LibraryConnection',1,assocuri2); - rc=metadata_getprop(assocuri2, - 'Connection.SQL.Property.Datasrc.Name.xmlKey.txt',path); - call symputx('path',path,'l'); - - /* schema */ - rc=metadata_getnasn("&liburi",'UsingPackages',1,assocuri3); - rc=metadata_getattr(assocuri3,'SchemaName',schema); - call symputx('schema',schema,'l'); - run; - - %put NOTE: Executing the following:/; %put NOTE-; - %put NOTE- libname &libref SQLSVR datasrc=&path schema=&schema ; - %put NOTE- user="&user" pass="XXX"; - %put NOTE-; - - libname &libref SQLSVR datasrc=&path schema=&schema user="&user" pass="&pass"; -%end; -%else %if &engine=TERADATA %then %do; - %put NOTE: Obtaining &engine library details; - data _null; - length assocuri1 assocuri2 assocuri3 authdomain path schema userid - passwd $256; - call missing (of _all_); - - /* get auth domain */ - rc=metadata_getnasn("&liburi",'LibraryConnection',1,assocuri1); - rc=metadata_getnasn(assocuri1,'Domain',1,assocuri2); - rc=metadata_getattr(assocuri2,"Name",authdomain); - call symputx('authdomain',authdomain,'l'); - - /* - rc=metadata_getnasn("&liburi",'DefaultLogin',1,assocuri1); - rc=metadata_getattr(assocuri1,"UserID",userid); - rc=metadata_getattr(assocuri1,"Password",passwd); - call symputx('user',userid,'l'); - call symputx('pass',passwd,'l'); - */ - - /* path */ - rc=metadata_getnasn("&liburi",'LibraryConnection',1,assocuri2); - rc=metadata_getprop(assocuri2, - 'Connection.Teradata.Property.SERVER.Name.xmlKey.txt',path); - call symputx('path',path,'l'); - - /* schema */ - rc=metadata_getnasn("&liburi",'UsingPackages',1,assocuri3); - rc=metadata_getattr(assocuri3,'SchemaName',schema); - call symputx('schema',schema,'l'); - run; - - %put NOTE: Executing the following:/; %put NOTE-; - %put NOTE- libname &libref TERADATA server="&path" schema=&schema ; - %put NOTe- authdomain=&authdomain; - %put NOTE-; - - libname &libref TERADATA server="&path" schema=&schema authdomain=&authdomain; -%end; -%else %if &engine= %then %do; - %put NOTE: Libref &libref is not registered in metadata; - %&mAbort.mp_abort( - msg=%str(ERR)OR: Libref &libref is not registered in metadata - ,mac=mm_assigndirectlib.sas); - %return; -%end; -%else %do; - %put %str(WARN)ING: Engine &engine is currently unsupported; - %put %str(WARN)ING- Please contact your support team.; - %return; -%end; - -%mend mm_assigndirectlib; diff --git a/002_macros/mm_createstp.sas b/002_macros/mm_createstp.sas deleted file mode 100644 index d5a883d..0000000 --- a/002_macros/mm_createstp.sas +++ /dev/null @@ -1,403 +0,0 @@ -/*** HELP START ***//** - @file - @brief Create a type 1 Stored Process (9.2 compatible) - @details This macro creates a Type 1 stored process, and also the necessary - PromptGroup / File / TextStore objects. It requires the location (or uri) - for the App Server / Directory / Folder (Tree) objects. - To upgrade this macro to work with type 2 (which can embed SAS code - and is compabitible with SAS from 9.3 onwards) then the UsageVersion should - change to 2000000 and the TextStore object updated. The ComputeServer - reference will also be to ServerContext rather than LogicalServer. - - This macro is idempotent - if you run it twice, it will only create an STP - once. - - Usage (type 1 STP): - - %mm_createstp(stpname=MyNewSTP - ,filename=mySpecialProgram.sas - ,directory=SASEnvironment/SASCode/STPs - ,tree=/User Folders/sasdemo - ,outds=work.uris) - - If you wish to remove the new STP you can do so by running: - - data _null_; - set work.uris; - rc1 = METADATA_DELOBJ(texturi); - rc2 = METADATA_DELOBJ(prompturi); - rc3 = METADATA_DELOBJ(fileuri); - rc4 = METADATA_DELOBJ(stpuri); - putlog (_all_)(=); - run; - - Usage (type 2 STP): - - %mm_createstp(stpname=MyNewType2STP - ,filename=mySpecialProgram.sas - ,directory=SASEnvironment/SASCode/STPs - ,tree=/User Folders/sasdemo - ,Server=SASApp - ,stptype=2) - - @param [in] stpname= (SASjs Default STP) Stored Process name. - Avoid spaces - testing has shown that - the check to avoid creating multiple STPs in the same folder with the same - name does not work when the name contains spaces. - @param [in] stpdesc= Stored Process description (optional) - @param [in] filename= the name of the .sas program to run - @param [in] directory= (SASEnvironment/sascode) - The directory uri or the actual path to the sas program (no trailing slash). - If more than uri is found with that path, then the first one will be used. - @param [in] tree= The metadata folder uri, or the metadata path, in which to - create the STP. - @param [in] server= (SASApp) The server which will run the STP. - Server name or uri is fine. - @param [out] outds= (work.mm_createstp) - The two level name of the output dataset. Will contain all the meta uris. - @param [in] mDebug= set to 1 to show debug messages in the log - @param [in] stptype= Default is 1 (STP code saved on filesystem). Set to 2 if - source code is to be saved in metadata (9.3 and above feature). - @param [in] minify= set to YES to strip comments / blank lines etc - @param [in] frefin= (mm_in) fileref to use (enables change if there is - a conflict). - The filerefs are left open, to enable inspection after running the - macro (or importing into an xmlmap if needed). - @param [out] frefout= (mm_out) fileref to use (enables change if there is - a conflict) - @param [in] repo= ServerContext is tied to a repo, if you are not using the - foundation repo then select a different one here - - @returns outds dataset containing the following columns: - - stpuri - - prompturi - - fileuri - - texturi - -

SAS Macros

- @li mf_nobs.sas - @li mf_verifymacvars.sas - @li mm_getdirectories.sas - @li mm_updatestpsourcecode.sas - @li mm_getservercontexts.sas - @li mp_abort.sas - @li mp_dropmembers.sas - -

Related Macros

- @li mm_createwebservice.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_createstp( - stpname=SASjs Default STP - ,stpdesc=This stp was created automatically by the mm_createstp macro - ,filename=mm_createstp.sas - ,directory=SASEnvironment/SASCode - ,tree=/User Folders/sasdemo - ,package=false - ,streaming=true - ,outds=work.mm_createstp - ,mDebug=0 - ,server=SASApp - ,stptype=1 - ,minify=NO - ,frefin=mm_in - ,frefout=mm_out -)/*/STORE SOURCE*/; - -%local mD; -%if &mDebug=1 %then %let mD=; -%else %let mD=%str(*); -%&mD.put Executing mm_CreateSTP.sas; -%&mD.put _local_; - -%mp_abort( - iftrue=(%mf_verifymacvars(stpname filename directory tree)=0) - ,mac=&sysmacroname - ,msg=%str(Empty inputs: stpname filename directory tree) -) - -%mp_dropmembers(%scan(&outds,2,.)) - -/** - * check tree exists - */ -data _null_; - length type uri $256; - rc=metadata_pathobj("","&tree","Folder",type,uri); - call symputx('foldertype',type,'l'); - call symputx('treeuri',uri,'l'); -run; -%if &foldertype ne Tree %then %do; - %put %str(WARN)ING: Tree &tree does not exist!; - %return; -%end; - -/** - * Check STP does not exist already - */ -%local cmtype; -data _null_; - length type uri $256; - rc=metadata_pathobj("","&tree/&stpname",'StoredProcess',type,uri); - call symputx('cmtype',type,'l'); - call symputx('stpuri',uri,'l'); -run; -%if &cmtype = ClassifierMap %then %do; - %put %str(WARN)ING: Stored Process &stpname already exists in &tree!; - %return; -%end; - -/** - * Check that the physical file exists - */ -%if %sysfunc(fileexist(&directory/&filename)) ne 1 %then %do; - %put %str(WARN)ING: FILE *&directory/&filename* NOT FOUND!; - %return; -%end; - -%if &stptype=1 %then %do; - /* type 1 STP - where code is stored on filesystem */ - %if %sysevalf(&sysver lt 9.2) %then %do; - %put %str(WARN)ING: Version 9.2 or later required; - %return; - %end; - - /* check directory object (where 9.2 source code reference is stored) */ - data _null_; - length id $20 dirtype $256; - rc=metadata_resolve("&directory",dirtype,id); - call symputx('checkdirtype',dirtype,'l'); - run; - - %if &checkdirtype ne Directory %then %do; - %mm_getdirectories(path=&directory,outds=&outds ,mDebug=&mDebug) - %if %mf_nobs(&outds)=0 or %sysfunc(exist(&outds))=0 %then %do; - %put %str(WARN)ING: The directory object does not exist for &directory; - %return; - %end; - %end; - %else %do; - data &outds; - directoryuri="&directory"; - run; - %end; - - data &outds (keep=stpuri prompturi fileuri texturi); - length stpuri prompturi fileuri texturi serveruri $256 ; - if _n_=1 then call missing (of _all_); - set &outds; - - /* final checks on uris */ - length id $20 type $256; - __rc=metadata_resolve("&treeuri",type,id); - if type ne 'Tree' then do; - putlog "%str(WARN)ING: Invalid tree URI: &treeuri"; - stopme=1; - end; - __rc=metadata_resolve(directoryuri,type,id); - if type ne 'Directory' then do; - putlog "%str(WARN)ING: Invalid directory URI: " directoryuri; - stopme=1; - end; - - /* get server info */ - __rc=metadata_resolve("&server",type,serveruri); - if type ne 'LogicalServer' then do; - __rc=metadata_getnobj("omsobj:LogicalServer?@Name='&server'",1,serveruri); - if serveruri='' then do; - putlog "%str(WARN)ING: Invalid server: &server"; - stopme=1; - end; - end; - - if stopme=1 then do; - putlog (_all_)(=); - stop; - end; - - /* create empty prompt */ - rc1=METADATA_NEWOBJ('PromptGroup',prompturi,'Parameters'); - rc2=METADATA_SETATTR(prompturi, 'UsageVersion', '1000000'); - rc3=METADATA_SETATTR(prompturi, 'GroupType','2'); - rc4=METADATA_SETATTR(prompturi, 'Name','Parameters'); - rc5=METADATA_SETATTR(prompturi, 'PublicType','Embedded:PromptGroup'); - GroupInfo= - ""; - rc6 = METADATA_SETATTR(prompturi, 'GroupInfo',groupinfo); - - if sum(of rc1-rc6) ne 0 then do; - putlog "%str(WARN)ING: Issue creating prompt."; - if prompturi ne . then do; - putlog ' Removing orphan: ' prompturi; - rc = METADATA_DELOBJ(prompturi); - put rc=; - end; - stop; - end; - - /* create a file uri */ - rc7=METADATA_NEWOBJ('File',fileuri,'SP Source File'); - rc8=METADATA_SETATTR(fileuri, 'FileName',"&filename"); - rc9=METADATA_SETATTR(fileuri, 'IsARelativeName','1'); - rc10=METADATA_SETASSN(fileuri, 'Directories','MODIFY',directoryuri); - if sum(of rc7-rc10) ne 0 then do; - putlog "%str(WARN)ING: Issue creating file."; - if fileuri ne . then do; - putlog ' Removing orphans:' prompturi fileuri; - rc = METADATA_DELOBJ(prompturi); - rc = METADATA_DELOBJ(fileuri); - put (_all_)(=); - end; - stop; - end; - - /* create a TextStore object */ - rc11= METADATA_NEWOBJ('TextStore',texturi,'Stored Process'); - rc12= METADATA_SETATTR(texturi, 'TextRole','StoredProcessConfiguration'); - rc13= METADATA_SETATTR(texturi, 'TextType','XML'); - storedtext='' - !!"" - !!""; - rc14= METADATA_SETATTR(texturi, 'StoredText',storedtext); - if sum(of rc11-rc14) ne 0 then do; - putlog "%str(WARN)ING: Issue creating TextStore."; - if texturi ne . then do; - putlog ' Removing orphans: ' prompturi fileuri texturi; - rc = METADATA_DELOBJ(prompturi); - rc = METADATA_DELOBJ(fileuri); - rc = METADATA_DELOBJ(texturi); - put (_all_)(=); - end; - stop; - end; - - /* create meta obj */ - rc15= METADATA_NEWOBJ('ClassifierMap',stpuri,"&stpname"); - rc16= METADATA_SETASSN(stpuri, 'Trees','MODIFY',treeuri); - rc17= METADATA_SETASSN(stpuri, 'ComputeLocations','MODIFY',serveruri); - rc18= METADATA_SETASSN(stpuri, 'SourceCode','MODIFY',fileuri); - rc19= METADATA_SETASSN(stpuri, 'Prompts','MODIFY',prompturi); - rc20= METADATA_SETASSN(stpuri, 'Notes','MODIFY',texturi); - rc21= METADATA_SETATTR(stpuri, 'PublicType', 'StoredProcess'); - rc22= METADATA_SETATTR(stpuri, 'TransformRole', 'StoredProcess'); - rc23= METADATA_SETATTR(stpuri, 'UsageVersion', '1000000'); - rc24= METADATA_SETATTR(stpuri, 'Desc', "&stpdesc"); - - /* tidy up if err */ - if sum(of rc15-rc24) ne 0 then do; - putlog "%str(WARN)ING: Issue creating STP."; - if stpuri ne . then do; - putlog ' Removing orphans: ' prompturi fileuri texturi stpuri; - rc = METADATA_DELOBJ(prompturi); - rc = METADATA_DELOBJ(fileuri); - rc = METADATA_DELOBJ(texturi); - rc = METADATA_DELOBJ(stpuri); - put (_all_)(=); - end; - end; - else do; - fullpath=cats('_program=',treepath,"/&stpname"); - putlog "NOTE: Stored Process Created!"; - putlog "NOTE- "; putlog "NOTE-"; putlog "NOTE-" fullpath; - putlog "NOTE- "; putlog "NOTE-"; - end; - output; - stop; - run; -%end; -%else %if &stptype=2 %then %do; - /* type 2 stp - code is stored in metadata */ - %if %sysevalf(&sysver lt 9.3) %then %do; - %put %str(WARN)ING: SAS version 9.3 or later required to create type2 STPs; - %return; - %end; - /* check we have the correct ServerContext */ - %mm_getservercontexts(outds=contexts) - %local serveruri; %let serveruri=NOTFOUND; - data _null_; - set contexts; - where upcase(servername)="%upcase(&server)"; - call symputx('serveruri',serveruri); - run; - %if &serveruri=NOTFOUND %then %do; - %put %str(WARN)ING: ServerContext *&server* not found!; - %return; - %end; - - /** - * First, create a Hello World type 2 stored process - */ - filename &frefin temp; - data _null_; - file &frefin; - treeuri=quote(symget('treeuri')); - serveruri=quote(symget('serveruri')); - stpdesc=quote(symget('stpdesc')); - stpname=quote(symget('stpname')); - - put "$METAREPOSITORY "/ - ''/ - " "/ - " "/ - " "/ - " "/ - ' '/ - ' ' / - " "/ - " "/ - ' '/ - " "/ - ""/ - "SAS"/ - "268435456"; - run; - - filename &frefout temp; - - proc metadata in= &frefin out=&frefout ; - run; - - %if &mdebug=1 %then %do; - /* write the response to the log for debugging */ - data _null_; - infile &frefout lrecl=1048576; - input; - put _infile_; - run; - %end; - - /** - * Next, add the source code - */ - %mm_updatestpsourcecode(stp=&tree/&stpname - ,stpcode="&directory/&filename" - ,mdebug=&mdebug - ,minify=&minify) - - -%end; -%else %do; - %put %str(WARN)ING: STPTYPE=*&stptype* not recognised!; -%end; - -%mend mm_createstp; diff --git a/002_macros/mm_getgroups.sas b/002_macros/mm_getgroups.sas deleted file mode 100644 index 42eef58..0000000 --- a/002_macros/mm_getgroups.sas +++ /dev/null @@ -1,95 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates dataset with all groups or just those for a particular user - @details Provide a metadata user to get groups for just that user, or leave - blank to return all groups. - Usage: - - - all groups: `%mm_getGroups()` - - - all groups for a particular user: `%mm_getgroups(user=&sysuserid)` - - @param [in] user= the metadata user to return groups for. Leave blank for all - groups. - @param [in] repo= the metadata repository that contains the user/group - information - @param [in] mDebug= set to 1 to show debug messages in the log - @param [out] outds= the dataset to create that contains the list of groups - - @returns outds dataset containing all groups in a column named "metagroup" - - groupuri - - groupname - - groupdesc - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getGroups( - user= - ,outds=work.mm_getGroups - ,repo=foundation - ,mDebug=0 -)/*/STORE SOURCE*/; - -%local mD oldrepo; -%let oldrepo=%sysfunc(getoption(metarepository)); -%if &mDebug=1 %then %let mD=; -%else %let mD=%str(*); -%&mD.put Executing mm_getGroups.sas; -%&mD.put _local_; - -/* on some sites, user / group info is in a different metadata repo to the - default */ -%if &oldrepo ne &repo %then %do; - options metarepository=&repo; -%end; - -%if %length(&user)=0 %then %do; - data &outds (keep=groupuri groupname groupdesc); - length groupuri groupname groupdesc group_or_role $256; - call missing(of _all_); - i+1; - do while - (metadata_getnobj("omsobj:IdentityGroup?@Id contains '.'",i,groupuri)>0); - rc=metadata_getattr(groupuri, "Name", groupname); - rc=metadata_getattr(groupuri, "Desc", groupdesc); - rc=metadata_getattr(groupuri,"PublicType",group_or_role); - if Group_or_Role = 'UserGroup' then output; - i+1; - end; - run; -%end; -%else %do; - data &outds (keep=groupuri groupname groupdesc); - length uri groupuri groupname groupdesc group_or_role $256; - call missing(of _all_); - rc=metadata_getnobj("omsobj:Person?@Name='&user'",1,uri); - if rc<=0 then do; - putlog "%str(WARN)ING: rc=" rc "&user not found " - ", or there was an issue reading the repository."; - stop; - end; - a=1; - grpassn=metadata_getnasn(uri,"IdentityGroups",a,groupuri); - if grpassn in (-3,-4) then do; - putlog "%str(WARN)ING: No metadata groups found for &user"; - output; - end; - else do while (grpassn > 0); - rc=metadata_getattr(groupuri, "Name", groupname); - rc=metadata_getattr(groupuri, "Desc", groupdesc); - a+1; - rc=metadata_getattr(groupuri,"PublicType",group_or_role); - if Group_or_Role = 'UserGroup' then output; - grpassn=metadata_getnasn(uri,"IdentityGroups",a,groupuri); - end; - run; -%end; - -%if &oldrepo ne &repo %then %do; - options metarepository=&oldrepo; -%end; - -%mend mm_getGroups; diff --git a/001_macros/mcf_getfmttype.sas b/003_macros/mcf_getfmttype.sas similarity index 100% rename from 001_macros/mcf_getfmttype.sas rename to 003_macros/mcf_getfmttype.sas diff --git a/001_macros/mcf_init.sas b/003_macros/mcf_init.sas similarity index 100% rename from 001_macros/mcf_init.sas rename to 003_macros/mcf_init.sas diff --git a/001_macros/mcf_length.sas b/003_macros/mcf_length.sas similarity index 100% rename from 001_macros/mcf_length.sas rename to 003_macros/mcf_length.sas diff --git a/001_macros/mcf_string2file.sas b/003_macros/mcf_string2file.sas similarity index 100% rename from 001_macros/mcf_string2file.sas rename to 003_macros/mcf_string2file.sas diff --git a/003_macros/mfv_existfile.sas b/003_macros/mfv_existfile.sas deleted file mode 100644 index 8b25cb0..0000000 --- a/003_macros/mfv_existfile.sas +++ /dev/null @@ -1,52 +0,0 @@ -/*** HELP START ***//** - @file - @brief Checks whether a file exists in SAS Drive - @details Returns 1 if the file exists, and 0 if it doesn't. Works by - attempting to assign a fileref with the filesrvc engine. If not found, the - syscc is automatically set to a non zero value - so in this case it is reset. - To avoid hiding issues, there is therefore a test at the start to ensure the - syscc is zero. - - Usage: - - %put %mfv_existfile(/does/exist.txt); - %put %mfv_existfile(/does/not/exist.txt); - - @param [in] filepath The full path to the file on SAS drive - (eg /Public/myfile.txt) - -

SAS Macros

- @li mf_abort.sas - @li mf_getuniquefileref.sas - -

Related Macros

- @li mfv_existfolder.sas - - @version 3.5 - @author [Allan Bowe](https://www.linkedin.com/in/allanbowe/) -**//*** HELP END ***/ - -%macro mfv_existfile(filepath -)/*/STORE SOURCE*/; - - %mf_abort( - iftrue=(&syscc ne 0), - msg=Cannot enter mfv_existfile.sas with syscc=&syscc - ) - - %local fref rc path name; - %let fref=%mf_getuniquefileref(); - %let name=%scan(&filepath,-1,/); - %let path=%substr(&filepath,1,%length(&filepath)-%length(&name)-1); - - %if %sysfunc(filename(fref,,filesrvc,folderPath="&path" filename="&name"))=0 - %then %do; - %sysfunc(fexist(&fref)) - %let rc=%sysfunc(filename(fref)); - %end; - %else %do; - 0 - %let syscc=0; - %end; - -%mend mfv_existfile; diff --git a/003_macros/mfv_existfolder.sas b/003_macros/mfv_existfolder.sas deleted file mode 100644 index 8fb73fb..0000000 --- a/003_macros/mfv_existfolder.sas +++ /dev/null @@ -1,48 +0,0 @@ -/*** HELP START ***//** - @file - @brief Checks whether a folder exists in SAS Drive - @details Returns 1 if the folder exists, and 0 if it doesn't. Works by - attempting to assign a fileref with the filesrvc engine. If not found, the - syscc is automatically set to a non zero value - so in this case it is reset. - To avoid hiding issues, there is therefore a test at the start to ensure the - syscc is zero. - - Usage: - - %put %mfv_existfolder(/does/exist); - %put %mfv_existfolder(/does/not/exist); - - @param [in] path The path to the folder on SAS drive - -

SAS Macros

- @li mf_abort.sas - @li mf_getuniquefileref.sas - -

Related Macros

- @li mfv_existfile.sas - - @version 3.5 - @author [Allan Bowe](https://www.linkedin.com/in/allanbowe/) -**//*** HELP END ***/ - -%macro mfv_existfolder(path -)/*/STORE SOURCE*/; - - %mf_abort( - iftrue=(&syscc ne 0), - msg=Cannot enter mfv_existfolder.sas with syscc=&syscc - ) - - %local fref rc; - %let fref=%mf_getuniquefileref(); - - %if %sysfunc(filename(fref,,filesrvc,folderPath="&path"))=0 %then %do; - 1 - %let rc=%sysfunc(filename(fref)); - %end; - %else %do; - 0 - %let syscc=0; - %end; - -%mend mfv_existfolder; diff --git a/003_macros/mfv_existsashdat.sas b/003_macros/mfv_existsashdat.sas deleted file mode 100644 index 332fec6..0000000 --- a/003_macros/mfv_existsashdat.sas +++ /dev/null @@ -1,60 +0,0 @@ -/*** HELP START ***//** - @file mfv_existsashdat.sas - @brief Checks whether a CAS sashdat dataset exists in persistent storage. - @details Can be used in open code, eg as follows: - - %if %mfv_existsashdat(libds=casuser.sometable) %then %put yes it does!; - - The function uses `dosubl()` to run the `table.fileinfo` action, for the - specified library, filtering for `*.sashdat` tables. The results are stored - in a WORK table (&outprefix._&lib). If that table already exists, it is - queried instead, to avoid the dosubl() performance hit. - - To force a rescan, just use a new `&outprefix` value, or delete the table(s) - before running the function. - - @param [in] libds library.dataset - @param [out] outprefix= (work.mfv_existsashdat) - Used to store current HDATA tables to improve subsequent query performance. - This reference is a prefix and is converted to `&prefix._{libref}` - - @return output returns 1 or 0 - - @version 0.2 - @author Mathieu Blauw -**//*** HELP END ***/ - -%macro mfv_existsashdat(libds,outprefix=work.mfv_existsashdat -); -%local rc dsid name lib ds; -%let lib=%upcase(%scan(&libds,1,'.')); -%let ds=%upcase(%scan(&libds,-1,'.')); - -/* if table does not exist, create it */ -%if %sysfunc(exist(&outprefix._&lib)) ne 1 %then %do; - %let rc=%sysfunc(dosubl(%nrstr( - /* Read in table list (once per &lib per session) */ - proc cas; - table.fileinfo result=source_list /caslib="&lib"; - val=findtable(source_list); - saveresult val dataout=&outprefix._&lib; - quit; - /* Only keep name, without file extension */ - data &outprefix._&lib; - set &outprefix._&lib(where=(Name like '%.sashdat') keep=Name); - Name=upcase(scan(Name,1,'.')); - run; - ))); -%end; - -/* Scan table for hdat existence */ -%let dsid=%sysfunc(open(&outprefix._&lib(where=(name="&ds")))); -%syscall set(dsid); -%let rc = %sysfunc(fetch(&dsid)); -%let rc = %sysfunc(close(&dsid)); - -/* Return result */ -%if "%trim(&name)"="%trim(&ds)" %then 1; -%else 0; - -%mend mfv_existsashdat; diff --git a/003_macros/mm_adduser2group.sas b/003_macros/mm_adduser2group.sas deleted file mode 100644 index 9f28ac3..0000000 --- a/003_macros/mm_adduser2group.sas +++ /dev/null @@ -1,102 +0,0 @@ -/*** HELP START ***//** - @file mm_adduser2group.sas - @brief Adds a user to a group - @details Adds a user to a metadata group. The macro first checks whether the - user is in that group, and if not, the user is added. - - Note that the macro does not check inherited group memberships - it looks at - direct members only. - - Usage: - - %mm_adduser2group(user=sasdemo - ,group=someGroup) - - - @param [in] user= the user name (not displayname) - @param [in] group= the group to which to add the user - @param [in] mdebug= (0) set to 1 to show debug info in log - -

Related Files

- @li ms_adduser2group.sas - - @version 9.3 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_adduser2group(user= - ,group= - ,mdebug=0 -); -/* first, check if user is in group already exists */ -%local check uuri guri; -%let check=ok; - -data _null_; - length uri type msg $256; - call missing(of _all_); - rc=metadata_getnobj("omsobj:Person?@Name='&user'",1,uri); - if rc<=0 then do; - msg="%str(WARN)ING: rc="!!cats(rc)!!" &user not found "!! - ", or there was an err reading the repository."; - call symputx('check',msg); - putlog msg; - stop; - end; - call symputx('uuri',scan(uri,2,'\')); - - rc=metadata_getnobj("omsobj:IdentityGroup?@Name='&group'",1,uri); - if rc<=0 then do; - msg="%str(WARN)ING: rc="!!cats(rc)!!" &group not found "!! - ", or there was an err reading the repository."; - call symputx('check',msg); - putlog msg; - stop; - end; - call symputx('guri',scan(uri,2,'\')); - - rc=metadata_getnobj("omsobj:Person?Person[@Name='&user'][IdentityGroups/*[@Name='&group']]",1,uri); - if rc=0 then do; - msg="%str(WARN)ING: rc="!!cats(rc)!!" &user already in &group"; - call symputx('check',msg); - stop; - end; - - if &mdebug ne 0 then put (_all_)(=); -run; - -/* stop if issues */ -%if %quote(&check) ne %quote(ok) %then %do; - %put ✓ - %return; -%end; - -%if &syscc ge 4 %then %do; - %put %str(WARN)ING: SYSCC=&syscc, exiting &sysmacroname; - %return; -%end; - - -filename __us2grp temp; - -proc metadata in= "$METAREPOSITORY - - - - SAS268435456" - out=__us2grp verbose; -run; - -%if &mdebug ne 0 %then %do; - /* write the response to the log for debugging */ - data _null_; - infile __us2grp lrecl=32767; - input; - put _infile_; - run; -%end; - -filename __us2grp clear; - -%mend mm_adduser2group; diff --git a/003_macros/mm_assignlib.sas b/003_macros/mm_assignlib.sas deleted file mode 100644 index c33a252..0000000 --- a/003_macros/mm_assignlib.sas +++ /dev/null @@ -1,97 +0,0 @@ -/*** HELP START ***//** - @file - @brief Assigns a meta engine library using LIBREF - @details Queries metadata to get the library NAME which can then be used in - a libname statement with the meta engine. - - usage: - - %macro mp_abort(iftrue,mac,msg);%put &=msg;%mend; - - %mm_assignlib(SOMEREF) - -

SAS Macros

- @li mp_abort.sas - - @param [in] libref The libref (not name) of the metadata library - @param [in] mAbort= (HARD) If not assigned, HARD will call %mp_abort(), SOFT - will silently return - - @returns libname statement - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_assignlib( - libref - ,mAbort=HARD -)/*/STORE SOURCE*/; -%local mp_abort msg; -%let mp_abort=0; -%if %sysfunc(libref(&libref)) %then %do; - data _null_; - length liburi LibName msg $200; - call missing(of _all_); - nobj=metadata_getnobj("omsobj:SASLibrary?@Libref='&libref'",1,liburi); - if nobj=1 then do; - rc=metadata_getattr(liburi,"Name",LibName); - /* now try and assign it */ - if libname("&libref",,'meta',cats('liburi="',liburi,'";')) ne 0 then do; - putlog "&libref could not be assigned"; - putlog liburi=; - /** - * Fetch the system message for display in the abort modal. This is - * not always helpful though. One example, previously received: - * NOTE: Libref XX refers to the same library metadata as libref XX. - */ - msg=sysmsg(); - if msg=:'ERROR: Libref SAVE is not assigned.' then do; - msg=catx(" ", - "Could not assign %upcase(&libref).", - "Please check metadata permissions! Libname:",libname, - "Liburi:",liburi - ); - end; - else if msg="ERROR: User does not have appropriate authorization "!! - "level for library SAVE." - then do; - msg=catx(" ", - "ERROR: User does not have appropriate authorization level", - "for library %upcase(&libref), libname:",libname, - "Liburi:",liburi - ); - end; - call symputx('msg',msg,'l'); - if "&mabort"='HARD' then call symputx('mp_abort',1,'l'); - end; - else do; - put (_all_)(=); - call symputx('libname',libname,'L'); - call symputx('liburi',liburi,'L'); - end; - end; - else if nobj>1 then do; - if "&mabort"='HARD' then call symputx('mp_abort',1); - call symputx('msg',"More than one library with libref=&libref"); - end; - else do; - if "&mabort"='HARD' then call symputx('mp_abort',1); - call symputx('msg',"Library &libref not found in metadata"); - end; - run; - - %put NOTE: &msg; - -%end; -%else %do; - %put NOTE: Library &libref is already assigned; -%end; - -%mp_abort(iftrue= (&mp_abort=1) - ,mac=mm_assignlib.sas - ,msg=%superq(msg) -) - -%mend mm_assignlib; diff --git a/003_macros/mm_createapplication.sas b/003_macros/mm_createapplication.sas deleted file mode 100644 index 25cab62..0000000 --- a/003_macros/mm_createapplication.sas +++ /dev/null @@ -1,161 +0,0 @@ -/*** HELP START ***//** - @file - @brief Create an Application object in a metadata folder - @details Application objects are useful for storing properties in metadata. - This macro is idempotent - it will not create an object with the same name - in the same location, twice. - - usage: - - %mm_createapplication(tree=/User Folders/sasdemo - ,name=MyApp - ,classidentifier=myAppSeries - ,params= name1=value1 name2=value2 emptyvalue= - ) - - @warning application components do not get deleted when removing the container - folder! be sure you have the administrative priviliges to remove this kind of - metadata from the SMC plugin (or be ready to do to so programmatically). - -

SAS Macros

- @li mp_abort.sas - @li mf_verifymacvars.sas - - @param [in] tree= The metadata folder uri, or the metadata path, in which to - create the object. This must exist. - @param [in] name= Application object name. Avoid spaces. - @param [in] ClassIdentifier= - The class of applications to which this app belongs - @param [in] params= - name=value pairs which will become public properties of the - application object. These are delimited using (newline character) - - @param [in] desc= Application description (optional). - Avoid ampersands as these - are illegal characters (unless they are escapted- eg &) - @param [in] version= version number of application - @param [in] frefin= fileref to use (enables change if there is a conflict). - The filerefs are left open, to enable inspection after running the - macro (or importing into an xmlmap if needed). - @param [out] frefout= fileref to use (enables change if there is a conflict) - @param [in] mDebug= set to 1 to show debug messages in the log - - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_createapplication( - tree=/User Folders/sasdemo - ,name=myApp - ,ClassIdentifier=mcore - ,desc=Created by mm_createapplication - ,params= param1=1 param2=blah - ,version= - ,frefin=mm_in - ,frefout=mm_out - ,mDebug=1 - ); - -%local mD; -%if &mDebug=1 %then %let mD=; -%else %let mD=%str(*); -%&mD.put Executing &sysmacroname..sas; -%&mD.put _local_; - -%mp_abort(iftrue= (%mf_verifymacvars(tree name)=0) - ,mac=&sysmacroname - ,msg=%str(Empty inputs: tree name) -) - -/** - * check tree exists - */ - -data _null_; - length type uri $256; - rc=metadata_pathobj("","&tree","Folder",type,uri); - call symputx('type',type,'l'); - call symputx('treeuri',uri,'l'); -run; - -%mp_abort( - iftrue= (&type ne Tree) - ,mac=mm_createapplication.sas - ,msg=Tree &tree does not exist! -) - -/** - * Check object does not exist already - */ -data _null_; - length type uri $256; - rc=metadata_pathobj("","&tree/&name","Application",type,uri); - call symputx('type',type,'l'); - putlog (_all_)(=); -run; - -%mp_abort( - iftrue= (&type = SoftwareComponent) - ,mac=mm_createapplication.sas - ,msg=Application &name already exists in &tree! -) - - -/** - * Now we can create the application - */ -filename &frefin temp; - -/* write header XML */ -data _null_; - file &frefin; - name=quote(symget('name')); - desc=quote(symget('desc')); - ClassIdentifier=quote(symget('ClassIdentifier')); - version=quote(symget('version')); - params=quote(symget('params')); - treeuri=quote(symget('treeuri')); - - put "$METAREPOSITORY "/ - '' / - ' ' / - ' ' / - ' ' / - ""/ - "SAS"/ - "268435456"; -run; - -filename &frefout temp; - -proc metadata in= &frefin out=&frefout verbose; -run; - -%if &mdebug=1 %then %do; - /* write the response to the log for debugging */ - data _null_; - infile &frefout lrecl=1048576; - input; - put _infile_; - run; -%end; - -%put NOTE: Checking to ensure application (&name) was created; -data _null_; - length type uri $256; - rc=metadata_pathobj("","&tree/&name","Application",type,uri); - call symputx('apptype',type,'l'); - %if &mdebug=1 %then putlog (_all_)(=);; -run; -%if &apptype ne SoftwareComponent %then %do; - %put %str(ERR)OR: Could not find (&name) at (&tree)!!; - %return; -%end; -%else %put NOTE: Application (&name) successfully created in (&tree)!; - - -%mend mm_createapplication; diff --git a/003_macros/mm_createdataset.sas b/003_macros/mm_createdataset.sas deleted file mode 100644 index 695e01a..0000000 --- a/003_macros/mm_createdataset.sas +++ /dev/null @@ -1,91 +0,0 @@ -/*** HELP START ***//** - @file - @brief Create an empty dataset from a metadata definition - @details This macro was built to support viewing empty tables in - https://datacontroller.io - - The table can be retrieved using LIBRARY.DATASET reference, or directly - using the metadata URI. - - The dataset is written to the WORK library. - - Usage: - - %mm_createdataset(libds=metlib.some_dataset) - - or - - %mm_createdataset(tableuri=G5X8AFW1.BE00015Y) - -

SAS Macros

- @li mm_getlibs.sas - @li mm_gettables.sas - @li mm_getcols.sas - - @param [in] libds= library.dataset metadata source. Note - table names in metadata - can be longer than 32 chars (just fyi, not an issue here) - @param [in] tableuri= Metadata URI of the table to be created - @param [out] outds= (work.mm_createdataset) The dataset to create. The table - name needs to be 32 chars or less as per SAS naming rules. - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_createdataset(libds=,tableuri=,outds=work.mm_createdataset,mDebug=0); -%local dbg errorcheck tempds1 tempds2 tempds3; -%if &mDebug=0 %then %let dbg=*; -%let errorcheck=1; - -%if %index(&libds,.)>0 %then %do; - /* get lib uri */ - data;run;%let tempds1=&syslast; - %mm_getlibs(outds=&tempds1) - data _null_; - set &tempds1; - if upcase(libraryref)="%upcase(%scan(&libds,1,.))"; - call symputx('liburi',LibraryId,'l'); - run; - /* get ds uri */ - data;run;%let tempds2=&syslast; - %mm_gettables(uri=&liburi,outds=&tempds2) - data _null_; - set &tempds2; - where upcase(tablename)="%upcase(%scan(&libds,2,.))"; - &dbg putlog tableuri=; - call symputx('tableuri',tableuri); - run; -%end; - -data;run; -%let tempds3=&syslast; -%mm_getcols(tableuri=&tableuri,outds=&tempds3) - -%if %mf_nobs(&tempds3)=0 %then %do; - %put &libds (&tableuri) has no columns defined!!; - data &outds; - run; - %return; -%end; - -data _null_; - set &tempds3 end=last; - if _n_=1 then call execute('data &outds;'); - length attrib $32767; - - if SAScolumntype='C' then type='$'; - attrib='attrib '!!cats(colname)!!' length='!!cats(type,SASColumnLength,'.'); - - if not missing(sasformat) then fmt=' format='!!cats(sasformat); - if not missing(sasinformat) then infmt=' informat='!!cats(sasinformat); - if not missing(coldesc) then desc=' label='!!quote(cats(coldesc)); - - attrib=trim(attrib)!!fmt!!infmt!!desc!!';'; - - call execute(attrib); - if last then call execute('call missing(of _all_);stop;run;'); -run; - -%mend mm_createdataset; diff --git a/003_macros/mm_createdocument.sas b/003_macros/mm_createdocument.sas deleted file mode 100644 index 9237a73..0000000 --- a/003_macros/mm_createdocument.sas +++ /dev/null @@ -1,128 +0,0 @@ -/*** HELP START ***//** - @file - @brief Create a Document object in a metadata folder - @details Document objects are useful for storing properties in metadata. - This macro is idempotent - it will not create an object with the same name - in the same location, twice. - Note - the filerefs are left open, to enable inspection after running the - macro (or importing into an xmlmap if needed). - - usage: - - %mm_createdocument(tree=/User Folders/sasdemo - ,name=MyNote) - -

SAS Macros

- @li mp_abort.sas - @li mf_verifymacvars.sas - - - @param [in] tree= The metadata folder uri, or the metadata path, in which to - create the document. This must exist. - @param [in] name= Document object name. Avoid spaces. - - @param [in] desc= Document description (optional) - @param [in] textrole= TextRole property (optional) - @param [in] frefin= fileref to use (enables change if there is a conflict) - @param [out] frefout= fileref to use (enables change if there is a conflict) - @param [in] mDebug= set to 1 to show debug messages in the log - - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_createdocument( - tree=/User Folders/sasdemo - ,name=myNote - ,desc=Created by &sysmacroname - ,textrole= - ,frefin=mm_in - ,frefout=mm_out - ,mDebug=1 - ); - -%local mD; -%if &mDebug=1 %then %let mD=; -%else %let mD=%str(*); -%&mD.put Executing &sysmacroname..sas; -%&mD.put _local_; - -%mp_abort(iftrue= (%mf_verifymacvars(tree name)=0) - ,mac=&sysmacroname - ,msg=%str(Empty inputs: tree name) -) - -/** - * check tree exists - */ - -data _null_; - length type uri $256; - rc=metadata_pathobj("","&tree","Folder",type,uri); - call symputx('type',type,'l'); - call symputx('treeuri',uri,'l'); -run; - -%mp_abort( - iftrue= (&type ne Tree) - ,mac=mm_createdocument.sas - ,msg=Tree &tree does not exist! -) - -/** - * Check object does not exist already - */ -data _null_; - length type uri $256; - rc=metadata_pathobj("","&tree/&name","Note",type,uri); - call symputx('type',type,'l'); - call symputx('docuri',uri,'l'); - putlog (_all_)(=); -run; - -%if &type = Document %then %do; - %put Document &name already exists in &tree!; - %return; -%end; - -/** - * Now we can create the document - */ -filename &frefin temp; - -/* write header XML */ -data _null_; - file &frefin; - name=quote("&name"); - desc=quote("&desc"); - textrole=quote("&textrole"); - treeuri=quote("&treeuri"); - - put "$METAREPOSITORY"/ - '"/ - " "/ - ' ' / - ''/ - /*URI="Document for public note" */ - ""/ - "SAS"/ - "268435456"; -run; - -filename &frefout temp; - -proc metadata in= &frefin out=&frefout verbose; -run; - -%if &mdebug=1 %then %do; - /* write the response to the log for debugging */ - data _null_; - infile &frefout lrecl=1048576; - input; - put _infile_; - run; -%end; - -%mend mm_createdocument; diff --git a/003_macros/mm_createfolder.sas b/003_macros/mm_createfolder.sas deleted file mode 100644 index 161704f..0000000 --- a/003_macros/mm_createfolder.sas +++ /dev/null @@ -1,161 +0,0 @@ -/*** HELP START ***//** - @file - @brief Recursively create a metadata folder - @details This macro was inspired by Paul Homes who wrote an early - version (mkdirmd.sas) in 2010. The original is described here: - https://platformadmin.com/blogs/paul/2010/07/mkdirmd/ - - The macro will NOT create a new ROOT folder - not - because it can't, but more because that is generally not something - your administrator would like you to do! - - The macro is idempotent - if you run it twice, it will only create a folder - once. - - Usage: - - %mm_createfolder(path=/some/meta/folder) - - @param [in] path= Name of the folder to create. - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_createfolder(path=,mDebug=0); -%put &sysmacroname: execution started for &path; -%local dbg errorcheck; -%if &mDebug=0 %then %let dbg=*; - -%local parentFolderObjId child errorcheck paths; -%let paths=0; -%let errorcheck=1; - -%if &syscc ge 4 %then %do; - %put SYSCC=&syscc - this macro requires a clean session; - %return; -%end; - -data _null_; - length objId parentId objType parent child $200 - folderPath $1000; - call missing (of _all_); - folderPath = "%trim(&path)"; - - * remove any trailing slash ; - if ( substr(folderPath,length(folderPath),1) = '/' ) then - folderPath=substr(folderPath,1,length(folderPath)-1); - - * name must not be blank; - if ( folderPath = '' ) then do; - put 'ERR' +(-1) "OR: &sysmacroname PATH parameter value must be non-blank"; - end; - - * must have a starting slash ; - if ( substr(folderPath,1,1) ne '/' ) then do; - put 'ERR' +(-1) "OR: &sysmacroname PATH param value must have starting slash"; - stop; - end; - - * check if folder already exists ; - rc=metadata_pathobj('',cats(folderPath,"(Folder)"),"",objType,objId); - if rc ge 1 then do; - put "NOTE: Folder " folderPath " already exists!"; - stop; - end; - - * do not create a root (one level) folder ; - if countc(folderPath,'/')=1 then do; - put 'ERR' +(-1) "OR: &sysmacroname will not create a new ROOT folder"; - stop; - end; - - * check that root folder exists ; - root=cats('/',scan(folderpath,1,'/'),"(Folder)"); - if metadata_pathobj('',root,"",objType,parentId)<1 then do; - put 'ERR' +(-1) "OR: " root " does not exist!"; - stop; - end; - - * check that parent folder exists ; - child=scan(folderPath,-1,'/'); - parent=substr(folderpath,1,length(folderpath)-length(child)-1); - rc=metadata_pathobj('',cats(parent,"(Folder)"),"",objType,parentId); - if rc<1 then do; - putlog 'The following folders will be created:'; - /* folder does not exist - so start from top and work down */ - length newpath $1000; - paths=0; - do x=2 to countw(folderpath,'/'); - newpath=''; - do i=1 to x; - newpath=cats(newpath,'/',scan(folderpath,i,'/')); - end; - rc=metadata_pathobj('',cats(newpath,"(Folder)"),"",objType,parentId); - if rc<1 then do; - paths+1; - call symputx(cats('path',paths),newpath); - putlog newpath; - end; - call symputx('paths',paths); - end; - end; - else putlog "parent " parent " exists"; - - call symputx('parentFolderObjId',parentId,'l'); - call symputx('child',child,'l'); - call symputx('errorcheck',0,'l'); - - &dbg put (_all_)(=); -run; - -%if &errorcheck=1 or &syscc ge 4 %then %return; - -%if &paths>0 %then %do x=1 %to &paths; - %put executing recursive call for &&path&x; - %mm_createfolder(path=&&path&x) -%end; -%else %do; - filename __newdir temp; - options noquotelenmax; - %local inmeta; - %put creating: &path; - %let inmeta=$METAREPOSITORY - - SAS268435456 - ; - - proc metadata in="&inmeta" out=__newdir verbose; - run ; - - /* check it was successful */ - data _null_; - length objId parentId objType parent child $200 ; - call missing (of _all_); - rc=metadata_pathobj('',cats("&path","(Folder)"),"",objType,objId); - if rc ge 1 then do; - putlog "SUCCCESS! &path created."; - end; - else do; - putlog 'ERR' +(-1) "OR: unsuccessful attempt to create &path"; - call symputx('syscc',8); - end; - run; - - /* write the response to the log for debugging */ - %if &mDebug ne 0 %then %do; - data _null_; - infile __newdir lrecl=32767; - input; - put _infile_; - run; - %end; - filename __newdir clear; -%end; - -%put &sysmacroname: execution finished for &path; -%mend mm_createfolder; diff --git a/003_macros/mm_createlibrary.sas b/003_macros/mm_createlibrary.sas deleted file mode 100644 index 84a9232..0000000 --- a/003_macros/mm_createlibrary.sas +++ /dev/null @@ -1,326 +0,0 @@ -/*** HELP START ***//** - @file - @brief Create a SAS Library - @details Currently only supports BASE engine - - This macro is idempotent - if you run it twice (for the same libref or - libname), it will only create one library. There is a dependency on other - macros in this library - they should be installed as a suite (see README). - - Usage: - - %mm_createlibrary( - libname=My New Library - ,libref=mynewlib - ,libdesc=Super & - ,engine=BASE - ,tree=/User Folders/sasdemo - ,servercontext=SASApp - ,directory=/tmp/tests - ,mDebug=1) - -

SAS Macros

- @li mf_verifymacvars.sas - @li mm_createfolder.sas - - - @param [in] libname= (My New Library) Library name (as displayed to user, - 256 chars). Duplicates are not created (case sensitive). - @param [in] libref= (mynewlib) Library libref (8 chars). Duplicate - librefs are not created, HOWEVER- the check is not case sensitive - if - *libref* exists, *LIBREF* will still be created. - Librefs created will always be uppercased. - @param [in] engine= Library engine (currently only BASE supported) - @param [in] tree= The metadata folder uri, or the metadata path, in which to - create the library. - @param [in] servercontext= (SASApp) The SAS server against which - the library is registered. - @param [in] IsPreassigned= set to 1 if the library should be pre-assigned. - - @param [in] libdesc= Library description (optional) - @param [in] directory= (/tmp/somelib) Required for the BASE engine. - The metadata directory objects are searched to find an existing - one with a matching physical path. - If more than one uri found with that path, then the first one will be used. - If no URI is found, a new directory object will be created. The physical - path will also be created, if it doesn't exist. - - - @param [in] mDebug= set to 1 to show debug messages in the log - @param [in] frefin= fileref to use (enables change if there is a conflict). - The filerefs are left open, to enable inspection after running the - macro (or importing into an xmlmap if needed). - @param [out] frefout= fileref to use (enables change if there is a conflict) - - - @version 9.3 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_createlibrary( - libname=My New Library - ,libref=mynewlib - ,libdesc=Created automatically using the mm_createlibrary macro - ,engine=BASE - ,tree=/User Folders/sasdemo - ,servercontext=SASApp - ,directory=/tmp/somelib - ,IsPreassigned=0 - ,mDebug=0 - ,frefin=mm_in - ,frefout=mm_out -)/*/STORE SOURCE*/; - -%local mD; -%if &mDebug=1 %then %let mD=; -%else %let mD=%str(*); -%&mD.put Executing &sysmacroname..sas; -%&mD.put _local_; - -%let libref=%upcase(&libref); - -/** - * Check Library does not exist already with this libname - */ -data _null_; - length type uri $256; - rc=metadata_resolve("omsobj:SASLibrary?@Name='&libname'",type,uri); - call symputx('checktype',type,'l'); - call symputx('liburi',uri,'l'); - putlog (_all_)(=); -run; -%if &checktype = SASLibrary %then %do; - %put %str(WARN)ING: Library (&liburi) already exists with libname (&libname); - %return; -%end; - -/** - * Check Library does not exist already with this libref - */ -data _null_; - length type uri $256; - rc=metadata_resolve("omsobj:SASLibrary?@Libref='&libref'",type,uri); - call symputx('checktype',type,'l'); - call symputx('liburi',uri,'l'); - putlog (_all_)(=); -run; -%if &checktype = SASLibrary %then %do; - %put %str(WARN)ING: Library (&liburi) already exists with libref (&libref) ; - %return; -%end; - - -/** - * Attempt to create tree - */ -%mm_createfolder(path=&tree) - -/** - * check tree exists - */ -data _null_; - length type uri $256; - rc=metadata_pathobj("","&tree","Folder",type,uri); - call symputx('foldertype',type,'l'); - call symputx('treeuri',uri,'l'); -run; -%if &foldertype ne Tree %then %do; - %put %str(WARN)ING: Tree &tree does not exist!; - %return; -%end; - -/** - * Create filerefs for proc metadata call - */ -filename &frefin temp; -filename &frefout temp; - -%mp_abort(iftrue= ( - &engine=BASE & %mf_verifymacvars(libname libref engine servercontext tree)=0 - ) - ,mac=&sysmacroname - ,msg=%str(Empty inputs: libname libref engine servercontext tree) -) - -%if &engine=BASE %then %do; - /** - * Check that the ServerContext exists - */ - data _null_; - length type uri $256; - rc=metadata_resolve("omsobj:ServerContext?@Name='&ServerContext'",type,uri); - call symputx('checktype',type,'l'); - call symputx('serveruri',uri,'l'); - putlog (_all_)(=); - run; - %if &checktype ne ServerContext %then %do; - %put %str(ERR)OR: ServerContext (&ServerContext) does not exist!; - %return; - %end; - - /** - * Get prototype info - */ - data _null_; - length type uri str $256; - str="omsobj:Prototype?@Name='Library.SAS.Prototype.Name.xmlKey.txt'"; - rc=metadata_resolve(str,type,uri); - call symputx('checktype',type,'l'); - call symputx('prototypeuri',uri,'l'); - putlog (_all_)(=); - run; - %if &checktype ne Prototype %then %do; - %put %str(ERR)OR: Prototype Library.SAS.Prototype.Name.xmlKey.txt not found; - %return; - %end; - - /** - * Check that Physical location exists - */ - %if %sysfunc(fileexist(&directory))=0 %then %do; - %put %str(ERR)OR: Physical directory (&directory) does not appear to exist!; - %return; - %end; - - /** - * Check that Directory Object exists in metadata - */ - data _null_; - length type uri $256; - rc=metadata_resolve("omsobj:Directory?@DirectoryRole='LibraryPath'" - !!" and @DirectoryName='&directory'",type,uri); - call symputx('checktype',type,'l'); - call symputx('directoryuri',uri,'l'); - putlog (_all_)(=); - run; - %if &checktype ne Directory %then %do; - %put NOTE: Directory object does not exist for (&directory) location; - %put NOTE: It will now be created; - - data _null_; - file &frefin; - directory=quote(symget('directory')); - put "$METAREPOSITORY "/ - ''/ - "SAS"/ - "268435456"; - run; - - proc metadata in= &frefin out=&frefout %if &mdebug=1 %then verbose;; - run; - %if &mdebug=1 %then %do; - data _null_; - infile &frefout lrecl=1048576; - input; put _infile_; - run; - %end; - %put NOTE: Checking to ensure directory (&directory) object was created; - data _null_; - length type uri $256; - rc=metadata_resolve("omsobj:Directory?@DirectoryRole='LibraryPath'" - !!" and @DirectoryName='&directory'",type,uri); - call symputx('checktype2',type,'l'); - call symputx('directoryuri',uri,'l'); - %if &mdebug=1 %then putlog (_all_)(=);; - run; - %if &checktype2 ne Directory %then %do; - %put %str(ERR)OR: Directory (&directory) object was NOT created!; - %return; - %end; - %else %put NOTE: Directory (&directoryuri) successfully created!; - %end; - - /** - * check SAS version - */ - %if %sysevalf(&sysver lt 9.3) %then %do; - %put %str(WARN)ING: Version 9.3 or later required; - %return; - %end; - - /** - * Prepare the XML and create the library - */ - data _null_; - file &frefin; - treeuri=quote(symget('treeuri')); - serveruri=quote(symget('serveruri')); - directoryuri=quote(symget('directoryuri')); - libname=quote(symget('libname')); - libref=quote(symget('libref')); - IsPreassigned=quote(symget('IsPreassigned')); - prototypeuri=quote(symget('prototypeuri')); - - /* escape description so it can be stored as XML */ - libdesc=tranwrd(symget('libdesc'),'&','&'); - libdesc=tranwrd(libdesc,'<','<'); - libdesc=tranwrd(libdesc,'>','>'); - libdesc=tranwrd(libdesc,"'",'''); - libdesc=tranwrd(libdesc,'"','"'); - libdesc=tranwrd(libdesc,'0A'x,' '); - libdesc=tranwrd(libdesc,'0D'x,' '); - libdesc=tranwrd(libdesc,'$','$'); - libdesc=quote(trim(libdesc)); - - put "$METAREPOSITORY "/ - ''/ - ' '/ - ' '/ - ' '/ - ' '/ - ' '/ - " "/ - ' '/ - ' '/ - ' '/ - ' '/ - ' '/ - ' '/ - 'SAS'/ - '268435456'; - run; - - - proc metadata in= &frefin out=&frefout %if &mdebug=1 %then verbose ;; - run; - - %if &mdebug=1 %then %do; - data _null_; - infile &frefout lrecl=1048576; - input;put _infile_; - run; - %end; - %put NOTE: Checking to ensure library (&libname) was created; - data _null_; - length type uri $256; - rc=metadata_pathobj("","&tree/&libname","Library",type,uri); - call symputx('libtype',type,'l'); - call symputx('liburi',uri,'l'); - %if &mdebug=1 %then putlog (_all_)(=);; - run; - %if &libtype ne SASLibrary %then %do; - %put %str(ERR)OR: Could not find (&libname) at (&tree)!!; - %return; - %end; - %else %put NOTE: Library (&libname) successfully created in (&tree)!; -%end; -%else %do; - %put %str(ERR)OR: Other library engine types are not yet supported!!; -%end; - - -/** - * Wrap up - */ -%if &mdebug ne 1 %then %do; - filename &frefin clear; - filename &frefout clear; -%end; - -%mend mm_createlibrary; diff --git a/003_macros/mm_createwebservice.sas b/003_macros/mm_createwebservice.sas deleted file mode 100644 index 4fd2ac0..0000000 --- a/003_macros/mm_createwebservice.sas +++ /dev/null @@ -1,711 +0,0 @@ -/*** HELP START ***//** - @file mm_createwebservice.sas - @brief Create a Web Ready Stored Process - @details This macro creates a Type 2 Stored Process with the mm_webout macro - (and dependencies) included as pre-code. - -Usage: - - %* compile macros ; - filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - %* parmcards lets us write to a text file from open code ; - filename ft15f001 temp; - parmcards4; - %webout(FETCH) - %* do some sas, any inputs are now already WORK tables; - data example1 example2; - set sashelp.class; - run; - %* send data back; - %webout(OPEN) - %webout(ARR,example1) * Array format, fast, suitable for large tables ; - %webout(OBJ,example2) * Object format, easier to work with ; - %webout(CLOSE) - ;;;; - %mm_createwebservice(path=/Public/app/common,name=appInit,code=ft15f001) - - For more examples of using these web services with the SASjs Adapter, see: - https://github.com/sasjs/adapter#readme - - @param [in] path= () The full path (in SAS Metadata) where the service - will be created - @param [in] name= Stored Process name. Avoid spaces - testing has shown that - the check to avoid creating multiple STPs in the same folder with the same - name does not work when the name contains spaces. - @param [in] desc= The description of the service (optional) - @param [in] precode= () Space separated list of filerefs, pointing to the - code that needs to be attached to the beginning of the service (optional) - @param [in] code= (ft15f001) Space seperated fileref(s) of the actual code - to be added - @param [in] server= (SASApp) The server which will run the STP. Server - name or uri is fine. - @param [in] mDebug= (0) set to 1 to show debug messages in the log - @param [in] replace= (YES) select NO to avoid replacing an existing service - in that location - @param [in] adapter= (sasjs) the macro uses the sasjs adapter by default. - To use another adapter, add a (different) fileref here. - -

SAS Macros

- @li mm_createstp.sas - @li mf_getuser.sas - @li mm_createfolder.sas - @li mm_deletestp.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_createwebservice(path= - ,name=initService - ,precode= - ,code=ft15f001 - ,desc=This stp was created automagically by the mm_createwebservice macro - ,mDebug=0 - ,server=SASApp - ,replace=YES - ,adapter=sasjs -)/*/STORE SOURCE*/; - -%if &syscc ge 4 %then %do; - %put &=syscc - &sysmacroname will not execute in this state; - %return; -%end; - -%local mD; -%if &mDebug=1 %then %let mD=; -%else %let mD=%str(*); -%&mD.put Executing mm_createwebservice.sas; -%&mD.put _local_; - -* remove any trailing slash ; -%if "%substr(&path,%length(&path),1)" = "/" %then - %let path=%substr(&path,1,%length(&path)-1); - -/** - * Add webout macro - * These put statements are auto generated - to change the macro, change the - * source (mm_webout) and run `build.py` - */ -filename sasjs temp; -data _null_; - file sasjs lrecl=3000 ; - put "/* Created on %sysfunc(datetime(),datetime19.) by %mf_getuser() */"; -/* WEBOUT BEGIN */ - put '%macro mp_jsonout(action,ds,jref=_webout,dslabel=,fmt=Y '; - put ' ,engine=DATASTEP '; - put ' ,missing=NULL '; - put ' ,showmeta=N '; - put ' ,maxobs=MAX '; - put ')/*/STORE SOURCE*/; '; - put '%local tempds colinfo fmtds i numcols numobs stmt_obs lastobs optval '; - put ' tmpds1 tmpds2 tmpds3 tmpds4; '; - put '%let numcols=0; '; - put '%if &maxobs ne MAX %then %let stmt_obs=%str(if _n_>&maxobs then stop;); '; - put ' '; - put '%if &action=OPEN %then %do; '; - put ' options nobomfile; '; - put ' data _null_;file &jref encoding=''utf-8'' lrecl=200; '; - put ' put ''{"PROCESSED_DTTM" : "'' "%sysfunc(datetime(),E8601DT26.6)" ''"''; '; - put ' run; '; - put '%end; '; - put '%else %if (&action=ARR or &action=OBJ) %then %do; '; - put ' /* force variable names to always be uppercase in the JSON */ '; - put ' options validvarname=upcase; '; - put ' /* To avoid issues with _webout on EBI - such as encoding diffs and truncation '; - put ' (https://support.sas.com/kb/49/325.html) we use temporary files */ '; - put ' filename _sjs1 temp lrecl=200 ; '; - put ' data _null_; file _sjs1 encoding=''utf-8''; '; - put ' put ", ""%lowcase(%sysfunc(coalescec(&dslabel,&ds)))"":"; '; - put ' run; '; - put ' /* now write to _webout 1 char at a time */ '; - put ' data _null_; '; - put ' infile _sjs1 lrecl=1 recfm=n; '; - put ' file &jref mod lrecl=1 recfm=n; '; - put ' input sourcechar $char1. @@; '; - put ' format sourcechar hex2.; '; - put ' put sourcechar char1. @@; '; - put ' run; '; - put ' filename _sjs1 clear; '; - put ' '; - put ' /* grab col defs */ '; - put ' proc contents noprint data=&ds '; - put ' out=_data_(keep=name type length format formatl formatd varnum label); '; - put ' run; '; - put ' %let colinfo=%scan(&syslast,2,.); '; - put ' proc sort data=&colinfo; '; - put ' by varnum; '; - put ' run; '; - put ' /* move meta to mac vars */ '; - put ' data &colinfo; '; - put ' if _n_=1 then call symputx(''numcols'',nobs,''l''); '; - put ' set &colinfo end=last nobs=nobs; '; - put ' name=upcase(name); '; - put ' /* fix formats */ '; - put ' if type=2 or type=6 then do; '; - put ' typelong=''char''; '; - put ' length fmt $49.; '; - put ' if format='''' then fmt=cats(''$'',length,''.''); '; - put ' else if formatl=0 then fmt=cats(format,''.''); '; - put ' else fmt=cats(format,formatl,''.''); '; - put ' end; '; - put ' else do; '; - put ' typelong=''num''; '; - put ' if format='''' then fmt=''best.''; '; - put ' else if formatl=0 then fmt=cats(format,''.''); '; - put ' else if formatd=0 then fmt=cats(format,formatl,''.''); '; - put ' else fmt=cats(format,formatl,''.'',formatd); '; - put ' end; '; - put ' /* 32 char unique name */ '; - put ' newname=''sasjs''!!substr(cats(put(md5(name),$hex32.)),1,27); '; - put ' '; - put ' call symputx(cats(''name'',_n_),name,''l''); '; - put ' call symputx(cats(''newname'',_n_),newname,''l''); '; - put ' call symputx(cats(''length'',_n_),length,''l''); '; - put ' call symputx(cats(''fmt'',_n_),fmt,''l''); '; - put ' call symputx(cats(''type'',_n_),type,''l''); '; - put ' call symputx(cats(''typelong'',_n_),typelong,''l''); '; - put ' call symputx(cats(''label'',_n_),coalescec(label,name),''l''); '; - put ' /* overwritten when fmt=Y and a custom format exists in catalog */ '; - put ' if typelong=''num'' then call symputx(cats(''fmtlen'',_n_),200,''l''); '; - put ' else call symputx(cats(''fmtlen'',_n_),min(32767,ceil((length+10)*1.5)),''l''); '; - put ' run; '; - put ' '; - put ' %let tempds=%substr(_%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); '; - put ' proc sql; '; - put ' select count(*) into: lastobs from &ds; '; - put ' %if &maxobs ne MAX %then %let lastobs=%sysfunc(min(&lastobs,&maxobs)); '; - put ' '; - put ' %if &engine=PROCJSON %then %do; '; - put ' %if &missing=STRING %then %do; '; - put ' %put &sysmacroname: Special Missings not supported in proc json.; '; - put ' %put &sysmacroname: Switching to DATASTEP engine; '; - put ' %goto datastep; '; - put ' %end; '; - put ' data &tempds; '; - put ' set &ds; '; - put ' &stmt_obs; '; - put ' %if &fmt=N %then format _numeric_ best32.;; '; - put ' /* PRETTY is necessary to avoid line truncation in large files */ '; - put ' filename _sjs2 temp lrecl=131068 encoding=''utf-8''; '; - put ' proc json out=_sjs2 pretty '; - put ' %if &action=ARR %then nokeys ; '; - put ' ;export &tempds / nosastags fmtnumeric; '; - put ' run; '; - put ' /* send back to webout */ '; - put ' data _null_; '; - put ' infile _sjs2 lrecl=1 recfm=n; '; - put ' file &jref mod lrecl=1 recfm=n; '; - put ' input sourcechar $char1. @@; '; - put ' format sourcechar hex2.; '; - put ' put sourcechar char1. @@; '; - put ' run; '; - put ' filename _sjs2 clear; '; - put ' %end; '; - put ' %else %if &engine=DATASTEP %then %do; '; - put ' %datastep: '; - put ' %if %sysfunc(exist(&ds)) ne 1 & %sysfunc(exist(&ds,VIEW)) ne 1 '; - put ' %then %do; '; - put ' %put &sysmacroname: &ds NOT FOUND!!!; '; - put ' %return; '; - put ' %end; '; - put ' '; - put ' %if &fmt=Y %then %do; '; - put ' /** '; - put ' * Extract format definitions '; - put ' * First, by getting library locations from dictionary.formats '; - put ' * Then, by exporting the width using proc format '; - put ' * Cannot use maxw from sashelp.vformat as not always populated '; - put ' * Cannot use fmtinfo() as not supported in all flavours '; - put ' */ '; - put ' %let tmpds1=%substr(fmtsum%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); '; - put ' %let tmpds2=%substr(cntl%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); '; - put ' %let tmpds3=%substr(cntl%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); '; - put ' %let tmpds4=%substr(col%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); '; - put ' proc sql noprint; '; - put ' create table &tmpds1 as '; - put ' select cats(libname,''.'',memname) as FMTCAT, '; - put ' FMTNAME '; - put ' from dictionary.formats '; - put ' where fmttype=''F'' and libname is not null '; - put ' and fmtname in (select format from &colinfo where format is not null) '; - put ' order by 1; '; - put ' create table &tmpds2( '; - put ' FMTNAME char(32), '; - put ' LENGTH num '; - put ' ); '; - put ' %local catlist cat fmtlist i; '; - put ' select distinct fmtcat into: catlist separated by '' '' from &tmpds1; '; - put ' %do i=1 %to %sysfunc(countw(&catlist,%str( ))); '; - put ' %let cat=%scan(&catlist,&i,%str( )); '; - put ' proc sql; '; - put ' select distinct fmtname into: fmtlist separated by '' '' '; - put ' from &tmpds1 where fmtcat="&cat"; '; - put ' proc format lib=&cat cntlout=&tmpds3(keep=fmtname length); '; - put ' select &fmtlist; '; - put ' run; '; - put ' proc sql; '; - put ' insert into &tmpds2 select distinct fmtname,length from &tmpds3; '; - put ' %end; '; - put ' '; - put ' proc sql; '; - put ' create table &tmpds4 as '; - put ' select a.*, b.length as MAXW '; - put ' from &colinfo a '; - put ' left join &tmpds2 b '; - put ' on cats(a.format)=cats(upcase(b.fmtname)) '; - put ' order by a.varnum; '; - put ' data _null_; '; - put ' set &tmpds4; '; - put ' if not missing(maxw); '; - put ' call symputx( '; - put ' cats(''fmtlen'',_n_), '; - put ' /* vars need extra padding due to JSON escaping of special chars */ '; - put ' min(32767,ceil((max(length,maxw)+10)*1.5)) '; - put ' ,''l'' '; - put ' ); '; - put ' run; '; - put ' '; - put ' /* configure varlenchk - as we are explicitly shortening the variables */ '; - put ' %let optval=%sysfunc(getoption(varlenchk)); '; - put ' options varlenchk=NOWARN; '; - put ' data _data_(compress=char); '; - put ' /* shorten the new vars */ '; - put ' length '; - put ' %do i=1 %to &numcols; '; - put ' &&name&i $&&fmtlen&i '; - put ' %end; '; - put ' ; '; - put ' /* rename on entry */ '; - put ' set &ds(rename=( '; - put ' %do i=1 %to &numcols; '; - put ' &&name&i=&&newname&i '; - put ' %end; '; - put ' )); '; - put ' &stmt_obs; '; - put ' '; - put ' drop '; - put ' %do i=1 %to &numcols; '; - put ' &&newname&i '; - put ' %end; '; - put ' ; '; - put ' %do i=1 %to &numcols; '; - put ' %if &&typelong&i=num %then %do; '; - put ' &&name&i=cats(put(&&newname&i,&&fmt&i)); '; - put ' %end; '; - put ' %else %do; '; - put ' &&name&i=put(&&newname&i,&&fmt&i); '; - put ' %end; '; - put ' %end; '; - put ' if _error_ then do; '; - put ' call symputx(''syscc'',1012); '; - put ' stop; '; - put ' end; '; - put ' run; '; - put ' %let fmtds=&syslast; '; - put ' options varlenchk=&optval; '; - put ' %end; '; - put ' '; - put ' proc format; /* credit yabwon for special null removal */ '; - put ' value bart (default=40) '; - put ' %if &missing=NULL %then %do; '; - put ' ._ - .z = null '; - put ' %end; '; - put ' %else %do; '; - put ' ._ = [quote()] '; - put ' . = null '; - put ' .a - .z = [quote()] '; - put ' %end; '; - put ' other = [best.]; '; - put ' '; - put ' data &tempds; '; - put ' attrib _all_ label=''''; '; - put ' %do i=1 %to &numcols; '; - put ' %if &&typelong&i=char or &fmt=Y %then %do; '; - put ' length &&name&i $&&fmtlen&i...; '; - put ' format &&name&i $&&fmtlen&i...; '; - put ' %end; '; - put ' %end; '; - put ' %if &fmt=Y %then %do; '; - put ' set &fmtds; '; - put ' %end; '; - put ' %else %do; '; - put ' set &ds; '; - put ' %end; '; - put ' &stmt_obs; '; - put ' format _numeric_ bart.; '; - put ' %do i=1 %to &numcols; '; - put ' %if &&typelong&i=char or &fmt=Y %then %do; '; - put ' if findc(&&name&i,''"\''!!''0A0D09000E0F010210111A''x) then do; '; - put ' &&name&i=''"''!!trim( '; - put ' prxchange(''s/"/\\"/'',-1, /* double quote */ '; - put ' prxchange(''s/\x0A/\n/'',-1, /* new line */ '; - put ' prxchange(''s/\x0D/\r/'',-1, /* carriage return */ '; - put ' prxchange(''s/\x09/\\t/'',-1, /* tab */ '; - put ' prxchange(''s/\x00/\\u0000/'',-1, /* NUL */ '; - put ' prxchange(''s/\x0E/\\u000E/'',-1, /* SS */ '; - put ' prxchange(''s/\x0F/\\u000F/'',-1, /* SF */ '; - put ' prxchange(''s/\x01/\\u0001/'',-1, /* SOH */ '; - put ' prxchange(''s/\x02/\\u0002/'',-1, /* STX */ '; - put ' prxchange(''s/\x10/\\u0010/'',-1, /* DLE */ '; - put ' prxchange(''s/\x11/\\u0011/'',-1, /* DC1 */ '; - put ' prxchange(''s/\x1A/\\u001A/'',-1, /* SUB */ '; - put ' prxchange(''s/\\/\\\\/'',-1,&&name&i) '; - put ' )))))))))))))!!''"''; '; - put ' end; '; - put ' else &&name&i=quote(cats(&&name&i)); '; - put ' %end; '; - put ' %end; '; - put ' run; '; - put ' '; - put ' filename _sjs3 temp lrecl=131068 ; '; - put ' data _null_; '; - put ' file _sjs3 encoding=''utf-8''; '; - put ' if _n_=1 then put "["; '; - put ' set &tempds; '; - put ' if _n_>1 then put "," @; put '; - put ' %if &action=ARR %then "[" ; %else "{" ; '; - put ' %do i=1 %to &numcols; '; - put ' %if &i>1 %then "," ; '; - put ' %if &action=OBJ %then """&&name&i"":" ; '; - put ' "&&name&i"n /* name literal for reserved variable names */ '; - put ' %end; '; - put ' %if &action=ARR %then "]" ; %else "}" ; ; '; - put ' '; - put ' /* close out the table */ '; - put ' data _null_; '; - put ' file _sjs3 mod encoding=''utf-8''; '; - put ' put '']''; '; - put ' run; '; - put ' data _null_; '; - put ' infile _sjs3 lrecl=1 recfm=n; '; - put ' file &jref mod lrecl=1 recfm=n; '; - put ' input sourcechar $char1. @@; '; - put ' format sourcechar hex2.; '; - put ' put sourcechar char1. @@; '; - put ' run; '; - put ' filename _sjs3 clear; '; - put ' %end; '; - put ' '; - put ' proc sql; '; - put ' drop table &colinfo, &tempds; '; - put ' '; - put ' %if %substr(&showmeta,1,1)=Y %then %do; '; - put ' filename _sjs4 temp lrecl=131068 encoding=''utf-8''; '; - put ' data _null_; '; - put ' file _sjs4; '; - put ' length label $350; '; - put ' put ", ""$%lowcase(%sysfunc(coalescec(&dslabel,&ds)))"":{""vars"":{"; '; - put ' do i=1 to &numcols; '; - put ' name=quote(trim(symget(cats(''name'',i)))); '; - put ' format=quote(trim(symget(cats(''fmt'',i)))); '; - put ' label=quote(prxchange(''s/\\/\\\\/'',-1,trim(symget(cats(''label'',i))))); '; - put ' length=quote(trim(symget(cats(''length'',i)))); '; - put ' type=quote(trim(symget(cats(''typelong'',i)))); '; - put ' if i>1 then put "," @@; '; - put ' put name '':{"format":'' format '',"label":'' label '; - put ' '',"length":'' length '',"type":'' type ''}''; '; - put ' end; '; - put ' put ''}}''; '; - put ' run; '; - put ' /* send back to webout */ '; - put ' data _null_; '; - put ' infile _sjs4 lrecl=1 recfm=n; '; - put ' file &jref mod lrecl=1 recfm=n; '; - put ' input sourcechar $char1. @@; '; - put ' format sourcechar hex2.; '; - put ' put sourcechar char1. @@; '; - put ' run; '; - put ' filename _sjs4 clear; '; - put ' %end; '; - put '%end; '; - put ' '; - put '%else %if &action=CLOSE %then %do; '; - put ' data _null_; file &jref encoding=''utf-8'' mod ; '; - put ' put "}"; '; - put ' run; '; - put '%end; '; - put '%mend mp_jsonout; '; - put ' '; - put '%macro mf_getuser( '; - put ')/*/STORE SOURCE*/; '; - put ' %local user; '; - put ' '; - put ' %if %symexist(_sasjs_username) %then %let user=&_sasjs_username; '; - put ' %else %if %symexist(SYS_COMPUTE_SESSION_OWNER) %then %do; '; - put ' %let user=&SYS_COMPUTE_SESSION_OWNER; '; - put ' %end; '; - put ' %else %if %symexist(_metaperson) %then %do; '; - put ' %if %length(&_metaperson)=0 %then %let user=&sysuserid; '; - put ' /* sometimes SAS will add @domain extension - remove for consistency */ '; - put ' /* but be sure to quote in case of usernames with commas */ '; - put ' %else %let user=%unquote(%scan(%quote(&_metaperson),1,@)); '; - put ' %end; '; - put ' %else %let user=&sysuserid; '; - put ' '; - put ' %quote(&user) '; - put ' '; - put '%mend mf_getuser; '; - put '%macro mm_webout(action,ds,dslabel=,fref=_webout,fmt=N,missing=NULL '; - put ' ,showmeta=N,maxobs=MAX,workobs=0 '; - put '); '; - put '%global _webin_file_count _webin_fileref1 _webin_name1 _program _debug '; - put ' sasjs_tables; '; - put '%local i tempds jsonengine; '; - put ' '; - put '/* see https://github.com/sasjs/core/issues/41 */ '; - put '%if "%upcase(&SYSENCODING)" ne "UTF-8" %then %let jsonengine=PROCJSON; '; - put '%else %let jsonengine=DATASTEP; '; - put ' '; - put ' '; - put '%if &action=FETCH %then %do; '; - put ' %if %str(&_debug) ge 131 %then %do; '; - put ' options mprint notes mprintnest; '; - put ' %end; '; - put ' %let _webin_file_count=%eval(&_webin_file_count+0); '; - put ' /* now read in the data */ '; - put ' %do i=1 %to &_webin_file_count; '; - put ' %if &_webin_file_count=1 %then %do; '; - put ' %let _webin_fileref1=&_webin_fileref; '; - put ' %let _webin_name1=&_webin_name; '; - put ' %end; '; - put ' data _null_; '; - put ' infile &&_webin_fileref&i termstr=crlf; '; - put ' input; '; - put ' call symputx(''input_statement'',_infile_); '; - put ' putlog "&&_webin_name&i input statement: " _infile_; '; - put ' stop; '; - put ' data &&_webin_name&i; '; - put ' infile &&_webin_fileref&i firstobs=2 dsd termstr=crlf encoding=''utf-8''; '; - put ' input &input_statement; '; - put ' %if %str(&_debug) ge 131 %then %do; '; - put ' if _n_<20 then putlog _infile_; '; - put ' %end; '; - put ' run; '; - put ' %let sasjs_tables=&sasjs_tables &&_webin_name&i; '; - put ' %end; '; - put '%end; '; - put ' '; - put '%else %if &action=OPEN %then %do; '; - put ' /* fix encoding */ '; - put ' OPTIONS NOBOMFILE; '; - put ' '; - put ' /** '; - put ' * check xengine type to avoid the below err message: '; - put ' * > Function is only valid for filerefs using the CACHE access method. '; - put ' */ '; - put ' data _null_; '; - put ' set sashelp.vextfl(where=(fileref="_WEBOUT")); '; - put ' if xengine=''STREAM'' then do; '; - put ' rc=stpsrv_header(''Content-type'',"text/html; encoding=utf-8"); '; - put ' end; '; - put ' run; '; - put ' '; - put ' /* setup json */ '; - put ' data _null_;file &fref encoding=''utf-8''; '; - put ' %if %str(&_debug) ge 131 %then %do; '; - put ' put ''>>weboutBEGIN<<''; '; - put ' %end; '; - put ' put ''{"SYSDATE" : "'' "&SYSDATE" ''"''; '; - put ' put '',"SYSTIME" : "'' "&SYSTIME" ''"''; '; - put ' run; '; - put ' '; - put '%end; '; - put ' '; - put '%else %if &action=ARR or &action=OBJ %then %do; '; - put ' %mp_jsonout(&action,&ds,dslabel=&dslabel,fmt=&fmt,jref=&fref '; - put ' ,engine=&jsonengine,missing=&missing,showmeta=&showmeta,maxobs=&maxobs '; - put ' ) '; - put '%end; '; - put '%else %if &action=CLOSE %then %do; '; - put ' /* To avoid issues with _webout on EBI we use a temporary file */ '; - put ' filename _sjsref temp lrecl=131068; '; - put ' %if %str(&workobs) > 0 %then %do; '; - put ' /* if debug mode, send back first XX records of each work table also */ '; - put ' data;run;%let tempds=%scan(&syslast,2,.); '; - put ' ods output Members=&tempds; '; - put ' proc datasets library=WORK memtype=data; '; - put ' %local wtcnt;%let wtcnt=0; '; - put ' data _null_; '; - put ' set &tempds; '; - put ' if not (upcase(name) =:"DATA"); /* ignore temp datasets */ '; - put ' i+1; '; - put ' call symputx(cats(''wt'',i),name,''l''); '; - put ' call symputx(''wtcnt'',i,''l''); '; - put ' data _null_; file _sjsref mod encoding=''utf-8''; '; - put ' put ",""WORK"":{"; '; - put ' %do i=1 %to &wtcnt; '; - put ' %let wt=&&wt&i; '; - put ' data _null_; file _sjsref mod encoding=''utf-8''; '; - put ' dsid=open("WORK.&wt",''is''); '; - put ' nlobs=attrn(dsid,''NLOBS''); '; - put ' nvars=attrn(dsid,''NVARS''); '; - put ' rc=close(dsid); '; - put ' if &i>1 then put '',''@; '; - put ' put " ""&wt"" : {"; '; - put ' put ''"nlobs":'' nlobs; '; - put ' put '',"nvars":'' nvars; '; - put ' %mp_jsonout(OBJ,&wt,jref=_sjsref,dslabel=first10rows,showmeta=Y '; - put ' ,maxobs=&workobs '; - put ' ) '; - put ' data _null_; file _sjsref mod encoding=''utf-8''; '; - put ' put "}"; '; - put ' %end; '; - put ' data _null_; file _sjsref mod encoding=''utf-8''; '; - put ' put "}"; '; - put ' run; '; - put ' %end; '; - put ' /* close off json */ '; - put ' data _null_;file _sjsref mod encoding=''utf-8''; '; - put ' length SYSPROCESSNAME syserrortext syswarningtext autoexec $512; '; - put ' put ",""_DEBUG"" : ""&_debug"" "; '; - put ' _METAUSER=quote(trim(symget(''_METAUSER''))); '; - put ' put ",""_METAUSER"": " _METAUSER; '; - put ' _METAPERSON=quote(trim(symget(''_METAPERSON''))); '; - put ' put '',"_METAPERSON": '' _METAPERSON; '; - put ' _PROGRAM=quote(trim(resolve(symget(''_PROGRAM'')))); '; - put ' put '',"_PROGRAM" : '' _PROGRAM ; '; - put ' autoexec=quote(urlencode(trim(getoption(''autoexec'')))); '; - put ' put '',"AUTOEXEC" : '' autoexec; '; - put ' put ",""MF_GETUSER"" : ""%mf_getuser()"" "; '; - put ' put ",""SYSCC"" : ""&syscc"" "; '; - put ' put ",""SYSENCODING"" : ""&sysencoding"" "; '; - put ' syserrortext=cats(symget(''syserrortext'')); '; - put ' if findc(syserrortext,''"\''!!''0A0D09000E0F010210111A''x) then do; '; - put ' syserrortext=''"''!!trim( '; - put ' prxchange(''s/"/\\"/'',-1, /* double quote */ '; - put ' prxchange(''s/\x0A/\n/'',-1, /* new line */ '; - put ' prxchange(''s/\x0D/\r/'',-1, /* carriage return */ '; - put ' prxchange(''s/\x09/\\t/'',-1, /* tab */ '; - put ' prxchange(''s/\x00/\\u0000/'',-1, /* NUL */ '; - put ' prxchange(''s/\x0E/\\u000E/'',-1, /* SS */ '; - put ' prxchange(''s/\x0F/\\u000F/'',-1, /* SF */ '; - put ' prxchange(''s/\x01/\\u0001/'',-1, /* SOH */ '; - put ' prxchange(''s/\x02/\\u0002/'',-1, /* STX */ '; - put ' prxchange(''s/\x10/\\u0010/'',-1, /* DLE */ '; - put ' prxchange(''s/\x11/\\u0011/'',-1, /* DC1 */ '; - put ' prxchange(''s/\x1A/\\u001A/'',-1, /* SUB */ '; - put ' prxchange(''s/\\/\\\\/'',-1,syserrortext) '; - put ' )))))))))))))!!''"''; '; - put ' end; '; - put ' else syserrortext=cats(''"'',syserrortext,''"''); '; - put ' put '',"SYSERRORTEXT" : '' syserrortext; '; - put ' put ",""SYSHOSTNAME"" : ""&syshostname"" "; '; - put ' put ",""SYSPROCESSID"" : ""&SYSPROCESSID"" "; '; - put ' put ",""SYSPROCESSMODE"" : ""&SYSPROCESSMODE"" "; '; - put ' SYSPROCESSNAME=quote(urlencode(cats(SYSPROCESSNAME))); '; - put ' put ",""SYSPROCESSNAME"" : " SYSPROCESSNAME; '; - put ' put ",""SYSJOBID"" : ""&sysjobid"" "; '; - put ' put ",""SYSSCPL"" : ""&sysscpl"" "; '; - put ' put ",""SYSSITE"" : ""&syssite"" "; '; - put ' put ",""SYSUSERID"" : ""&sysuserid"" "; '; - put ' sysvlong=quote(trim(symget(''sysvlong''))); '; - put ' put '',"SYSVLONG" : '' sysvlong; '; - put ' syswarningtext=cats(symget(''syswarningtext'')); '; - put ' if findc(syswarningtext,''"\''!!''0A0D09000E0F010210111A''x) then do; '; - put ' syswarningtext=''"''!!trim( '; - put ' prxchange(''s/"/\\"/'',-1, /* double quote */ '; - put ' prxchange(''s/\x0A/\n/'',-1, /* new line */ '; - put ' prxchange(''s/\x0D/\r/'',-1, /* carriage return */ '; - put ' prxchange(''s/\x09/\\t/'',-1, /* tab */ '; - put ' prxchange(''s/\x00/\\u0000/'',-1, /* NUL */ '; - put ' prxchange(''s/\x0E/\\u000E/'',-1, /* SS */ '; - put ' prxchange(''s/\x0F/\\u000F/'',-1, /* SF */ '; - put ' prxchange(''s/\x01/\\u0001/'',-1, /* SOH */ '; - put ' prxchange(''s/\x02/\\u0002/'',-1, /* STX */ '; - put ' prxchange(''s/\x10/\\u0010/'',-1, /* DLE */ '; - put ' prxchange(''s/\x11/\\u0011/'',-1, /* DC1 */ '; - put ' prxchange(''s/\x1A/\\u001A/'',-1, /* SUB */ '; - put ' prxchange(''s/\\/\\\\/'',-1,syswarningtext) '; - put ' )))))))))))))!!''"''; '; - put ' end; '; - put ' else syswarningtext=cats(''"'',syswarningtext,''"''); '; - put ' put '',"SYSWARNINGTEXT" : '' syswarningtext; '; - put ' put '',"END_DTTM" : "'' "%sysfunc(datetime(),E8601DT26.6)" ''" ''; '; - put ' length memsize $32; '; - put ' memsize="%sysfunc(INPUTN(%sysfunc(getoption(memsize)), best.),sizekmg.)"; '; - put ' memsize=quote(cats(memsize)); '; - put ' put '',"MEMSIZE" : '' memsize; '; - put ' put "}" @; '; - put ' %if %str(&_debug) ge 131 %then %do; '; - put ' put ''>>weboutEND<<''; '; - put ' %end; '; - put ' run; '; - put ' /* now write to _webout 1 char at a time */ '; - put ' data _null_; '; - put ' infile _sjsref lrecl=1 recfm=n; '; - put ' file &fref mod lrecl=1 recfm=n; '; - put ' input sourcechar $char1. @@; '; - put ' format sourcechar hex2.; '; - put ' put sourcechar char1. @@; '; - put ' run; '; - put ' filename _sjsref clear; '; - put ' '; - put '%end; '; - put ' '; - put '%mend mm_webout; '; -/* WEBOUT END */ - put '%macro webout(action,ds,dslabel=,fmt=,missing=NULL,showmeta=NO'; - put ' ,maxobs=MAX'; - put ');'; - put ' %mm_webout(&action,ds=&ds,dslabel=&dslabel,fmt=&fmt,missing=&missing'; - put ' ,showmeta=&showmeta,maxobs=&maxobs'; - put ' )'; - put '%mend;'; -run; - -/* add precode and code */ -%local work tmpfile; -%let work=%sysfunc(pathname(work)); -%let tmpfile=__mm_createwebservice.temp; -%local x fref freflist mod; -%let freflist= &adapter &precode &code ; -%do x=1 %to %sysfunc(countw(&freflist)); - %if &x>1 %then %let mod=mod; - - %let fref=%scan(&freflist,&x); - %&mD.put &sysmacroname: adding &fref; - data _null_; - file "&work/&tmpfile" lrecl=3000 &mod; - infile &fref; - input; - put _infile_; - run; -%end; - -/* create the metadata folder if not already there */ -%mm_createfolder(path=&path) -%if &syscc ge 4 %then %return; - -%if %upcase(&replace)=YES %then %do; - %mm_deletestp(target=&path/&name) -%end; - -/* create the web service */ -%mm_createstp(stpname=&name - ,filename=&tmpfile - ,directory=&work - ,tree=&path - ,stpdesc=&desc - ,mDebug=&mdebug - ,server=&server - ,stptype=2) - -/* find the web app url */ -%local url; -%let url=localhost/SASStoredProcess; -data _null_; - length url $128; - rc=METADATA_GETURI("Stored Process Web App",url); - if rc=0 then call symputx('url',url,'l'); -run; - -%put &sysmacroname: STP &name successfully created in &path; -%put Check it out here:; -%put ;%put ;%put ; -%put &url?_PROGRAM=&path/&name; -%put ;%put ;%put ; - -%mend mm_createwebservice; diff --git a/003_macros/mm_deletedocument.sas b/003_macros/mm_deletedocument.sas deleted file mode 100644 index 7a3b72c..0000000 --- a/003_macros/mm_deletedocument.sas +++ /dev/null @@ -1,71 +0,0 @@ -/*** HELP START ***//** - @file mm_deletedocument.sas - @brief Deletes a Document using path as reference - @details - - Usage: - - %mm_createdocument(tree=/User Folders/&sysuserid,name=MyNote) - %mm_deletedocument(target=/User Folders/&sysuserid/MyNote) - -

SAS Macros

- - @param [in] target= full path to the document being deleted - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_deletedocument( - target= -)/*/STORE SOURCE*/; - -/** - * Check document exist - */ -%local type; -data _null_; - length type uri $256; - rc=metadata_pathobj("","&target",'Note',type,uri); - call symputx('type',type,'l'); - call symputx('stpuri',uri,'l'); -run; -%if &type ne Document %then %do; - %put %str(WARN)ING: No Document found at ⌖ - %return; -%end; - -filename __in temp lrecl=10000; -filename __out temp lrecl=10000; -data _null_ ; - file __in ; - put ""; - put "SAS268436480"; - put ""; -run ; -proc metadata in=__in out=__out verbose;run; - -/* list the result */ -data _null_;infile __out; input; list; run; - -filename __in clear; -filename __out clear; - -/** - * Check deletion - */ -%local isgone; -data _null_; - length type uri $256; - call missing (of _all_); - rc=metadata_pathobj("","&target",'Note',type,uri); - call symputx('isgone',type,'l'); -run; -%if &isgone = Document %then %do; - %put %str(ERR)OR: Document not deleted from ⌖ - %let syscc=4; - %return; -%end; - -%mend mm_deletedocument; diff --git a/003_macros/mm_deletelibrary.sas b/003_macros/mm_deletelibrary.sas deleted file mode 100644 index 892fc9e..0000000 --- a/003_macros/mm_deletelibrary.sas +++ /dev/null @@ -1,93 +0,0 @@ -/*** HELP START ***//** - @file - @brief Deletes a library by Name - - @details Used to delete a library. - Usage: - - %* create a library in the home directory ; - %mm_createlibrary( - libname=My Temp Library, - libref=XXTEMPXX, - tree=/User Folders/&sysuserid, - directory=%sysfunc(pathname(work)) - ) - - %* delete the library ; - %mm_deletelibrary(name=My Temp Library) - - After running the above, the following will be shown in the log: - - ![](https://i.imgur.com/Y4Tog24.png) - - @param [in] name= () the name (not libref) of the library to be deleted - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mp_abort.sas - - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_deletelibrary( - name= -)/*/STORE SOURCE*/; - - -/** - * Check if library exists and get uri - */ -data _null_; - length type uri $256; - rc=metadata_resolve("omsobj:SASLibrary?@Name='&name'",type,uri); - call symputx('checktype',type,'l'); - call symputx('liburi',uri,'l'); - putlog (_all_)(=); -run; -%if &checktype ne SASLibrary %then %do; - %put &sysmacroname: Library (&name) was not found, and so will not be deleted; - %return; -%end; - -%local fname1 fname2; -%let fname1=%mf_getuniquefileref(); -%let fname2=%mf_getuniquefileref(); - -filename &fname1 temp lrecl=10000; -filename &fname2 temp lrecl=10000; -data _null_ ; - file &fname1 ; - put ""; - put "SAS268436480"; - put ""; -run ; -proc metadata in=&fname1 out=&fname2 verbose;run; - -/* list the result */ -data _null_;infile &fname2; input; list; run; - -filename &fname1 clear; -filename &fname2 clear; - -/** - * Check deletion - */ -%local isgone; -data _null_; - length type uri $256; - call missing (of _all_); - rc=metadata_resolve("omsobj:SASLibrary?@Id='&liburi'",type,uri); - call symputx('isgone',type,'l'); -run; - -%mp_abort(iftrue=(&isgone = SASLibrary) - ,mac=&sysmacroname - ,msg=%str(Library (&name) NOT deleted) -) - -%put &sysmacroname: Library &name (&liburi) was successfully deleted; - -%mend mm_deletelibrary; diff --git a/003_macros/mm_deletestp.sas b/003_macros/mm_deletestp.sas deleted file mode 100644 index fc3d2a4..0000000 --- a/003_macros/mm_deletestp.sas +++ /dev/null @@ -1,70 +0,0 @@ -/*** HELP START ***//** - @file mm_deletestp.sas - @brief Deletes a Stored Process using path as reference - @details Will only delete the metadata, not any physical files associated. - - Usage: - - %mm_deletestp(target=/some/meta/path/myStoredProcess) - -

SAS Macros

- - @param [in] target= full path to the STP being deleted - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_deletestp( - target= -)/*/STORE SOURCE*/; - -/** - * Check STP does exist - */ -%local cmtype; -data _null_; - length type uri $256; - rc=metadata_pathobj("","&target",'StoredProcess',type,uri); - call symputx('cmtype',type,'l'); - call symputx('stpuri',uri,'l'); -run; -%if &cmtype ne ClassifierMap %then %do; - %put NOTE: No Stored Process found at ⌖ - %return; -%end; - -filename __in temp lrecl=10000; -filename __out temp lrecl=10000; -data _null_ ; - file __in ; - put ""; - put "SAS268436480"; - put ""; -run ; -proc metadata in=__in out=__out verbose;run; - -/* list the result */ -data _null_;infile __out; input; list; run; - -filename __in clear; -filename __out clear; - -/** - * Check deletion - */ -%local isgone; -data _null_; - length type uri $256; - call missing (of _all_); - rc=metadata_pathobj("","&target",'Note',type,uri); - call symputx('isgone',type,'l'); -run; -%if &isgone = ClassifierMap %then %do; - %put %str(ERR)OR: STP not deleted from ⌖ - %let syscc=4; - %return; -%end; - -%mend mm_deletestp; diff --git a/003_macros/mm_getauthinfo.sas b/003_macros/mm_getauthinfo.sas deleted file mode 100644 index f0013d0..0000000 --- a/003_macros/mm_getauthinfo.sas +++ /dev/null @@ -1,119 +0,0 @@ -/*** HELP START ***//** - @file mm_getauthinfo.sas - @brief Extracts authentication info for each user in metadata - @details - Usage: - - %mm_getauthinfo(outds=auths) - - - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages and preserve outputs - @param [out] outds= (mm_getauthinfo) The output dataset to create - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mf_getuniquename.sas - @li mm_getdetails.sas - @li mm_getobjects.sas - - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getauthinfo(outds=mm_getauthinfo - ,mdebug=0 -)/*/STORE SOURCE*/; -%local prefix fileref; -%let prefix=%substr(%mf_getuniquename(),1,25); - -%mm_getobjects(type=Login,outds=&prefix.0) - -%local fileref; -%let fileref=%mf_getuniquefileref(); - -data _null_; - file &fileref; - set &prefix.0 end=last; - /* run macro */ - str=cats('%mm_getdetails(uri=',id,",outattrs=&prefix.d",_n_ - ,",outassocs=&prefix.a",_n_,")"); - put str; - /* transpose attributes */ - str=cats("proc transpose data=&prefix.d",_n_,"(drop=type) out=&prefix.da" - ,_n_,"(drop=_name_);var value;id name;run;"); - put str; - /* add extra info to attributes */ - str=cats("data &prefix.da",_n_,";length login_id login_name $256; login_id=" - ,quote(trim(id)),";set &prefix.da",_n_ - ,";login_name=trim(subpad(name,1,256));drop name;run;"); - put str; - /* add extra info to associations */ - str=cats("data &prefix.a",_n_,";length login_id login_name $256; login_id=" - ,quote(trim(id)),";login_name=",quote(trim(name)) - ,";set &prefix.a",_n_,";run;"); - put str; - if last then do; - /* collate attributes */ - str=cats("data &prefix._logat; set &prefix.da1-&prefix.da",_n_,";run;"); - put str; - /* collate associations */ - str=cats("data &prefix._logas; set &prefix.a1-&prefix.a",_n_,";run;"); - put str; - /* tidy up */ - str=cats("proc delete data=&prefix.da1-&prefix.da",_n_,";run;"); - put str; - str=cats("proc delete data=&prefix.d1-&prefix.d",_n_,";run;"); - put str; - str=cats("proc delete data=&prefix.a1-&prefix.a",_n_,";run;"); - put str; - end; -run; - -%if &mdebug=1 %then %do; - data _null_; - infile &fileref; - if _n_=1 then putlog // "Now executing the following code:" //; - input; putlog _infile_; - run; -%end; -%inc &fileref; -filename &fileref clear; - -/* get libraries */ -proc sort data=&prefix._logas(where=(assoc='Libraries')) out=&prefix._temp; - by login_id; -data &prefix._temp; - set &prefix._temp; - by login_id; - length library_list $32767; - retain library_list; - if first.login_id then library_list=name; - else library_list=catx(' !! ',library_list,name); -proc sql; -/* get auth domain */ -create table &prefix._dom as - select login_id,name as domain - from &prefix._logas - where assoc='Domain'; -create unique index login_id on &prefix._dom(login_id); -/* join it all together */ -create table &outds as - select a.* - ,c.domain - ,b.library_list - from &prefix._logat (drop=ishidden lockedby usageversion publictype) a - left join &prefix._temp b - on a.login_id=b.login_id - left join &prefix._dom c - on a.login_id=c.login_id; - -%if &mdebug=0 %then %do; - proc datasets lib=work; - delete &prefix:; - run; -%end; - - -%mend mm_getauthinfo; diff --git a/003_macros/mm_getcols.sas b/003_macros/mm_getcols.sas deleted file mode 100644 index 3a1886b..0000000 --- a/003_macros/mm_getcols.sas +++ /dev/null @@ -1,53 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a dataset with all metadata columns for a particular table - @details - - usage: - - %mm_getcols(tableuri=A5X8AHW1.B40001S5) - - @param [out] outds the dataset to create that contains the list of columns - @param [in] uri the uri of the table for which to return columns - - @returns outds dataset containing all columns, specifically: - - colname - - coluri - - coldesc - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getcols( - tableuri= - ,outds=work.mm_getcols -)/*/STORE SOURCE*/; - -data &outds; - keep col: SAS:; - length assoc uri coluri colname coldesc SASColumnType SASFormat SASInformat - SASPrecision SASColumnLength $256; - call missing (of _all_); - uri=symget('tableuri'); - n=1; - do while (metadata_getnasn(uri,'Columns',n,coluri)>0); - rc3=metadata_getattr(coluri,"Name",colname); - rc3=metadata_getattr(coluri,"Desc",coldesc); - rc4=metadata_getattr(coluri,"SASColumnType",SASColumnType); - rc5=metadata_getattr(coluri,"SASFormat",SASFormat); - rc6=metadata_getattr(coluri,"SASInformat",SASInformat); - rc7=metadata_getattr(coluri,"SASPrecision",SASPrecision); - rc8=metadata_getattr(coluri,"SASColumnLength",SASColumnLength); - output; - call missing(colname,coldesc,SASColumnType,SASFormat,SASInformat - ,SASPrecision,SASColumnLength); - n+1; - end; -run; -proc sort; - by colname; -run; - -%mend mm_getcols; diff --git a/003_macros/mm_getdetails.sas b/003_macros/mm_getdetails.sas deleted file mode 100644 index 02b3937..0000000 --- a/003_macros/mm_getdetails.sas +++ /dev/null @@ -1,68 +0,0 @@ -/*** HELP START ***//** - @file mm_getdetails.sas - @brief extracts metadata attributes and associations for a particular uri - - @param [in] uri the metadata object for which to return - attributes / associations - @param [out] outattrs= (work.attributes) - The dataset to create that contains the list of attributes - @param [out] outassocs= (work.associations) - The dataset to contain the list of associations - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getdetails(uri - ,outattrs=work.attributes - ,outassocs=work.associations -)/*/STORE SOURCE*/; - -data &outassocs; - keep assoc assocuri name; - length assoc assocuri name $256; - call missing(of _all_); - rc1=1;n1=1; - do while(rc1>0); - /* Walk through all possible associations of this object. */ - rc1=metadata_getnasl("&uri",n1,assoc); - rc2=1;n2=1; - do while(rc2>0); - /* Walk through all the associations on this machine object. */ - rc2=metadata_getnasn("&uri",trim(assoc),n2,assocuri); - if (rc2>0) then do; - rc3=metadata_getattr(assocuri,"Name",name); - output; - end; - call missing(name,assocuri); - n2+1; - end; - n1+1; - end; -run; -proc sort; - by assoc name; -run; - -data &outattrs; - keep type name value; - length type $4 name $256 value $32767; - rc1=1;n1=1;type='Prop';name='';value=''; - do while(rc1>0); - rc1=metadata_getnprp("&uri",n1,name,value); - if rc1>0 then output; - n1+1; - end; - rc1=1;n1=1;type='Attr'; - do while(rc1>0); - rc1=metadata_getnatr("&uri",n1,name,value); - if rc1>0 then output; - n1+1; - end; -run; -proc sort; - by type name; -run; - -%mend mm_getdetails; diff --git a/003_macros/mm_getdirectories.sas b/003_macros/mm_getdirectories.sas deleted file mode 100644 index 947a6b4..0000000 --- a/003_macros/mm_getdirectories.sas +++ /dev/null @@ -1,58 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns a dataset with the meta directory object for a physical path - @details Provide a file path to get matching directory objects, or leave - blank to return all directories. The Directory object is used to reference - a physical filepath (eg when registering a .sas program in a Stored process) - - @param [in] path= Physical path for which to return a meta Directory object - @param [out] outds= (work.mm_getdirectories) - the dataset to create that contains the list of directories - @param [in] mDebug= (0) set to 1 to show debug messages in the log - - @returns outds dataset containing the following columns: - - directoryuri - - groupname - - groupdesc - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getDirectories( - path= - ,outds=work.mm_getDirectories - ,mDebug=0 -)/*/STORE SOURCE*/; - -%local mD; -%if &mDebug=1 %then %let mD=; -%else %let mD=%str(*); -%&mD.put Executing mm_getDirectories.sas; -%&mD.put _local_; - -data &outds (keep=directoryuri name directoryname directorydesc ); - length directoryuri name directoryname directorydesc $256; - call missing(of _all_); - __i+1; -%if %length(&path)=0 %then %do; - do while - (metadata_getnobj("omsobj:Directory?@Id contains '.'",__i,directoryuri)>0); -%end; %else %do; - do while( - metadata_getnobj("omsobj:Directory?@DirectoryName='&path'",__i,directoryuri) - >0 - ); -%end; - __rc1=metadata_getattr(directoryuri, "Name", name); - __rc2=metadata_getattr(directoryuri, "DirectoryName", directoryname); - __rc3=metadata_getattr(directoryuri, "Desc", directorydesc); - &mD.putlog (_all_) (=); - drop __:; - __i+1; - if sum(of __rc1-__rc3)=0 then output; - end; -run; - -%mend mm_getDirectories; diff --git a/003_macros/mm_getdocument.sas b/003_macros/mm_getdocument.sas deleted file mode 100644 index 3766f96..0000000 --- a/003_macros/mm_getdocument.sas +++ /dev/null @@ -1,146 +0,0 @@ -/*** HELP START ***//** - @file - @brief Writes the TextStore of a Document Object to an external file - @details If the document exists, and has a textstore object, the contents - of that textstore are written to an external file. - - usage: - - %mm_getdocument(tree=/some/meta/path - ,name=someDocument - ,outref=/some/unquoted/filename.ext - ) - -

SAS Macros

- @li mp_abort.sas - - @param [in] tree= The metadata path of the document - @param [in] name= Document object name. - @param [out] outref= full and unquoted path to the desired text file. - This will be overwritten if it already exists. - - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getdocument( - tree=/User Folders/sasdemo - ,name=myNote - ,outref=%sysfunc(pathname(work))/mm_getdocument.txt - ,mDebug=1 - ); - -%local mD; -%if &mDebug=1 %then %let mD=; -%else %let mD=%str(*); -%&mD.put Executing &sysmacroname..sas; -%&mD.put _local_; - -/** - * check tree exists - */ - -data _null_; - length type uri $256; - rc=metadata_pathobj("","&tree","Folder",type,uri); - call symputx('type',type,'l'); - call symputx('treeuri',uri,'l'); -run; - -%mp_abort( - iftrue= (&type ne Tree) - ,mac=mm_getdocument.sas - ,msg=Tree &tree does not exist! -) - -/** - * Check object exists - */ -data _null_; - length type docuri tsuri tsid $256 ; - rc1=metadata_pathobj("","&tree/&name","Note",type,docuri); - rc2=metadata_getnasn(docuri,"Notes",1,tsuri); - rc3=metadata_getattr(tsuri,"Id",tsid); - call symputx('type',type,'l'); - call symputx("tsid",tsid,'l'); - putlog (_all_)(=); -run; - -%mp_abort( - iftrue= (&type ne Document) - ,mac=mm_getdocument.sas - ,msg=Document &name could not be found in &tree! -) - -/** - * Now we can extract the textstore - */ -filename __getdoc temp lrecl=10000000; -proc metadata - in="$METAREPOSITORY - - SAS1" - out=__getdoc ; -run; - -/* find the beginning of the text */ -data _null_; - infile __getdoc lrecl=10000; - input; - start=index(_infile_,'StoredText="'); - if start then do; - call symputx("start",start+11); - put start= "type=&type"; - putlog '"' _infile_ '"'; - end; - stop; - -/* read the content, byte by byte, resolving escaped chars */ -filename __outdoc "&outref" lrecl=100000; -data _null_; - length filein 8 fileid 8; - filein = fopen("__getdoc","I",1,"B"); - fileid = fopen("__outdoc","O",1,"B"); - rec = "20"x; - length entity $6; - do while(fread(filein)=0); - x+1; - if x>&start then do; - rc = fget(filein,rec,1); - if rec='"' then leave; - else if rec="&" then do; - entity=rec; - do until (rec=";"); - if fread(filein) ne 0 then goto getout; - rc = fget(filein,rec,1); - entity=cats(entity,rec); - end; - select (entity); - when ('&' ) rec='&' ; - when ('<' ) rec='<' ; - when ('>' ) rec='>' ; - when (''') rec="'" ; - when ('"') rec='"' ; - when (' ') rec='0A'x; - when (' ') rec='0D'x; - when ('$' ) rec='$' ; - when (' ') rec='09'x; - otherwise putlog "%str(WARN)ING: missing value for " entity=; - end; - rc =fput(fileid, substr(rec,1,1)); - rc =fwrite(fileid); - end; - else do; - rc =fput(fileid,rec); - rc =fwrite(fileid); - end; - end; - end; - getout: - rc=fclose(filein); - rc=fclose(fileid); -run; -filename __getdoc clear; -filename __outdoc clear; - -%mend mm_getdocument; diff --git a/003_macros/mm_getfoldermembers.sas b/003_macros/mm_getfoldermembers.sas deleted file mode 100644 index 4c6a1b6..0000000 --- a/003_macros/mm_getfoldermembers.sas +++ /dev/null @@ -1,96 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns all direct child members of a particular folder - @details Displays the children for a particular folder, in a similar fashion - to the viya counterpart (mv_getfoldermembers.sas) - - Usage: - - %mm_getfoldermembers(root=/, outds=rootfolders) - - %mm_getfoldermembers(root=/User Folders/&sysuserid, outds=usercontent) - - @param [in] root= the parent folder under which to return all contents - @param [out] outds= the dataset to create that contains the list of - directories - @param [in] mDebug= set to 1 to show debug messages in the log - -

Data Outputs

- - Example for `root=/`: - - |metauri $17|metaname $256|metatype $32| - |---|---|---| - |A5XLSNXI.AA000001|Products |Folder| - |A5XLSNXI.AA000002|Shared Data |Folder| - |A5XLSNXI.AA000003|User Folders |Folder| - |A5XLSNXI.AA000004|System |Folder| - |A5XLSNXI.AA00003K|30.SASApps |Folder| - |A5XLSNXI.AA00006A|Public|Folder| - -

SAS Macros

- @li mm_getfoldertree.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ -%macro mm_getfoldermembers( - root= - ,outds=work.mm_getfoldertree -)/*/STORE SOURCE*/; - -%if "&root" = "/" %then %do; - %local fname1 fname2 fname3; - %let fname1=%mf_getuniquefileref(); - %let fname2=%mf_getuniquefileref(); - %let fname3=%mf_getuniquefileref(); - data _null_ ; - file &fname1 ; - put '' ; - put '$METAREPOSITORY' ; - put 'Tree' ; - put 'SAS' ; - put '388' ; - put '' ; - put ''; - put '' ; - put '' ; - run ; - proc metadata in=&fname1 out=&fname2 verbose;run; - - /* create an XML map to read the response */ - data _null_; - file &fname3; - put ''; - put ''; - put '//Objects/Tree'; - put '>17'; - put '//Objects/Tree/@Id'; - put '256>'; - put '//Objects/Tree/@Name'; - put '
'; - run; - %local libref1; - %let libref1=%mf_getuniquelibref(); - libname &libref1 xml xmlfileref=&fname2 xmlmap=&fname3; - - data &outds; - length metatype $32; - retain metatype 'Folder'; - set &libref1..sasfolders; - run; - -%end; -%else %do; - %mm_getfoldertree(root=&root, outds=&outds,depth=1) - data &outds; - set &outds(rename=(name=metaname publictype=metatype)); - keep metaname metauri metatype; - run; -%end; - -%mend mm_getfoldermembers; diff --git a/003_macros/mm_getfoldertree.sas b/003_macros/mm_getfoldertree.sas deleted file mode 100644 index c9c3a3c..0000000 --- a/003_macros/mm_getfoldertree.sas +++ /dev/null @@ -1,89 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns all folders / subfolder content for a particular root - @details Shows all members and SubTrees recursively for a particular root. - Note - for big sites, this returns a lot of data! So you may wish to reduce - the logging to speed up the process (see example below), OR - use mm_tree.sas - which uses proc metadata and is far more efficient. - - Usage: - - options ps=max nonotes nosource; - %mm_getfoldertree(root=/My/Meta/Path, outds=iwantthisdataset) - options notes source; - - @param [in] root= the parent folder under which to return all contents - @param [out] outds= the dataset to create that contains the list of - directories - @param [in] mDebug= set to 1 to show debug messages in the log - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ -%macro mm_getfoldertree( - root= - ,outds=work.mm_getfoldertree - ,mDebug=0 - ,depth=50 /* how many nested folders to query */ - ,level=1 /* system var - to track current level depth */ - ,append=NO /* system var - when YES means appending within nested loop */ -)/*/STORE SOURCE*/; - -%if &level>&depth %then %return; - -%local mD; -%if &mDebug=1 %then %let mD=; -%else %let mD=%str(*); -%&mD.put Executing &sysmacroname; -%&mD.put _local_; - -%if &append=NO %then %do; - /* ensure table doesn't exist already */ - data &outds; run; - proc sql; drop table &outds; -%end; - -/* get folder contents */ -data &outds.TMP/view=&outds.TMP; - length metauri pathuri $64 name $256 path $1024 - assoctype publictype MetadataUpdated MetadataCreated $32; - keep metauri assoctype name publictype MetadataUpdated MetadataCreated path; - call missing(of _all_); - path="&root"; - rc=metadata_pathobj("",path,"Folder",publictype,pathuri); - if publictype ne 'Tree' then do; - putlog "%str(WAR)NING: Tree " path 'does not exist!' publictype=; - stop; - end; - __n1=1; - do while(metadata_getnasl(pathuri,__n1,assoctype)>0); - __n1+1; - /* Walk through all possible associations of this object. */ - __n2=1; - if assoctype in ('Members','SubTrees') then - do while(metadata_getnasn(pathuri,assoctype,__n2,metauri)>0); - __n2+1; - call missing(name,publictype,MetadataUpdated,MetadataCreated); - __rc1=metadata_getattr(metauri,"Name", name); - __rc2=metadata_getattr(metauri,"MetadataUpdated", MetadataUpdated); - __rc3=metadata_getattr(metauri,"MetadataCreated", MetadataCreated); - __rc4=metadata_getattr(metauri,"PublicType", PublicType); - output; - end; - n1+1; - end; - drop __:; -run; - -proc append base=&outds data=&outds.TMP; -run; - -data _null_; - set &outds.TMP(where=(assoctype='SubTrees')); - call execute('%mm_getfoldertree(root=' - !!cats(path,"/",name)!!",outds=&outds,mDebug=&mdebug,depth=&depth" - !!",level=%eval(&level+1),append=YES)"); -run; - -%mend mm_getfoldertree; diff --git a/003_macros/mm_getgroupmembers.sas b/003_macros/mm_getgroupmembers.sas deleted file mode 100644 index 821df50..0000000 --- a/003_macros/mm_getgroupmembers.sas +++ /dev/null @@ -1,79 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates dataset with all members of a metadata group - @details This macro will query SAS metadata and return all the members - of a particular group. - - Usage: - - %mm_getgroupmembers(someGroupName - ,outds=work.mm_getgroupmembers - ,emails=YES - ) - - @param [in] group metadata group for which to bring back members - @param [out] outds= (work.mm_getgroupmembers) - The dataset to create that contains the list of members - @param [in] emails= (NO) Set to YES to bring back email addresses - @param [in] id= (NO) Set to yes if passing an ID rather than a group name - - @returns outds dataset containing all members of the metadata group - -

Related Macros

- @li mm_getgorups.sas - @li mm_adduser2group.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getgroupmembers( - group /* metadata group for which to bring back members */ - ,outds=work.mm_getgroupmembers /* output dataset to contain the results */ - ,emails=NO /* set to yes to bring back emails also */ - ,id=NO /* set to yes if passing an ID rather than group name */ -)/*/STORE SOURCE*/; - - data &outds ; - attrib uriGrp uriMem GroupId GroupName Group_or_Role MemberName MemberType - euri email length=$64 - GroupDesc length=$256 - rcGrp rcMem rc i j length=3; - call missing (of _all_); - drop uriGrp uriMem rcGrp rcMem rc i j arc ; - - i=1; - * Grab the URI for the first Group ; - %if &id=NO %then %do; - rcGrp=metadata_getnobj("omsobj:IdentityGroup?@Name='&group'",i,uriGrp); - %end; - %else %do; - rcGrp=metadata_getnobj("omsobj:IdentityGroup?@Id='&group'",i,uriGrp); - %end; - * If Group found, enter do loop ; - if rcGrp>0 then do; - call missing (rcMem,uriMem,GroupId,GroupName,Group_or_Role - ,MemberName,MemberType); - * get group info ; - rc = metadata_getattr(uriGrp,"Id",GroupId); - rc = metadata_getattr(uriGrp,"Name",GroupName); - rc = metadata_getattr(uriGrp,"PublicType",Group_or_Role); - rc = metadata_getattr(uriGrp,"Desc",GroupDesc); - j=1; - do while (metadata_getnasn(uriGrp,"MemberIdentities",j,uriMem) > 0); - call missing (MemberName, MemberType, email); - rc = metadata_getattr(uriMem,"Name",MemberName); - rc = metadata_getattr(uriMem,"PublicType",MemberType); - if membertype='User' and "&emails"='YES' then do; - if metadata_getnasn(uriMem,"EmailAddresses",1,euri)>0 then do; - arc=metadata_getattr(euri,"Address",email); - end; - end; - output; - j+1; - end; - end; - run; - -%mend mm_getgroupmembers; diff --git a/003_macros/mm_getlibmetadiffs.sas b/003_macros/mm_getlibmetadiffs.sas deleted file mode 100644 index 33ce5af..0000000 --- a/003_macros/mm_getlibmetadiffs.sas +++ /dev/null @@ -1,128 +0,0 @@ -/*** HELP START ***//** - @file - @brief Compares the metadata of a library with the physical tables - @details Creates a series of output tables that show the differences between - metadata and physical tables. - Each output can be created with an optional prefix. - - Credit - Paul Homes - https://platformadmin.com/blogs/paul/2012/11/sas-proc-metalib-ods-output - - Usage: - - %* create (and assign) a library for testing purposes ; - %mm_createlibrary( - libname=My Temp Library, - libref=XXTEMPXX, - tree=/User Folders/&sysuserid, - directory=%sysfunc(pathname(work)) - ) - - %* create some tables; - data work.table1 table2 table3; - a=1;b='two';c=3; - run; - - %* register the tables; - proc metalib; - omr=(library="My Temp Library"); - report(type=detail); - update_rule (delete); - run; - - %* modify the tables; - proc sql; - drop table table3; - alter table table2 drop c; - alter table table2 add d num; - - %* run the macro; - %mm_getlibmetadiffs(libname=My Temp Library) - - %* delete the library ; - %mm_deletelibrary(name=My Temp Library) - - The program will create four output tables, with the following structure (and - example data): - - #### &prefix.added - |name:$32.|metaID:$17.|SAStabName:$32.| - |---|---|---| - | | |DATA1| - - #### &prefix.deleted - |name:$32.|metaID:$17.|SAStabName:$32.| - |---|---|---| - |TABLE3|A5XLSNXI.BK0001HO|TABLE3| - - #### &prefix.updated - |tabName:$32.|tabMetaID:$17.|SAStabName:$32.|metaName:$32.|metaID:$17.|sasname:$32.|metaType:$16.|change:$64.| - |---|---|---|---|---|---|---|---| - |TABLE2|A5XLSNXI.BK0001HN|TABLE2|c|A5XLSNXI.BM000MA9|c|Column|Deleted| - | | | |d| |d|Column|Added| - - #### &prefix.meta - |Label1:$28.|cValue1:$1.|nValue1:D12.3| - |---|---|---| - |Total tables analyzed|4|4| - |Tables to be Updated|1|1| - |Tables to be Deleted|1|1| - |Tables to be Added|1|1| - |Tables matching data source|1|1| - |Tables not processed|0|0| - - If you are interested in more functionality like this (checking the health of - SAS metadata and your SAS 9 environment) then do contact [Allan Bowe]( - https://www.linkedin.com/in/allanbowe) for details of our SAS 9 Health Check - service. - - Our system scan will perform hundreds of checks to identify common issues, - such as dangling metadata, embedded passwords, security issues and more. - - @param [in] libname= the metadata name of the library to be compared - @param [out] outlib=(work) The library in which to store the output tables. - @param [out] prefix=(metadiff) The prefix for the four tables created. - - @version 9.3 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getlibmetadiffs( - libname= , - prefix=metadiff, - outlib=work -)/*/STORE SOURCE*/; - - /* create tempds */ - data;run; - %local tempds; - %let tempds=&syslast; - - /* save options */ - proc optsave out=&tempds; - run; - - options VALIDVARNAME=ANY VALIDMEMNAME=EXTEND; - - ods output - factoid1=&outlib..&prefix.meta - updtab=&outlib..&prefix.updated - addtab=&outlib..&prefix.added - deltab=&outlib..&prefix.deleted - ; - - proc metalib; - omr=(library="&libname"); - noexec; - report(type=detail); - update_rule (delete); - run; - - ods output close; - - /* restore options */ - proc optload data=&tempds; - run; - -%mend mm_getlibmetadiffs; diff --git a/003_macros/mm_getlibs.sas b/003_macros/mm_getlibs.sas deleted file mode 100644 index a6baa84..0000000 --- a/003_macros/mm_getlibs.sas +++ /dev/null @@ -1,97 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a dataset with all metadata libraries - @details Will only show the libraries to which a user has the requisite - metadata access. - - @param [out] outds= (work.mm_getlibs) - The library.dataset to create that contains the list of libraries - - @returns outds dataset containing all groups in a column named "metagroup" - (defaults to work.mm_getlibs). The following columns are provided: - - LibraryId - - LibraryName - - LibraryRef - - Engine - - @warning The following filenames are created and then de-assigned: - - filename sxlemap clear; - filename response clear; - libname _XML_ clear; - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getlibs( - outds=work.mm_getLibs -)/*/STORE SOURCE*/; - -/* - flags: - - OMI_SUCCINCT (2048) Do not return attributes with null values. - OMI_GET_METADATA (256) Executes a GetMetadata call for each object that - is returned by the GetMetadataObjects method. - OMI_ALL_SIMPLE (8) Gets all of the attributes of the requested object. -*/ -data _null_; - flags=2048+256+8; - call symputx('flags',flags,'l'); -run; - -* use a temporary fileref to hold the response; -filename response temp; -/* get list of libraries */ -proc metadata in= - ' - $METAREPOSITORY - SASLibrary - - SAS - &flags - - ' - out=response; -run; - -/* write the response to the log for debugging */ -data _null_; - infile response lrecl=32767; - input; - put _infile_; -run; - -/* create an XML map to read the response */ -filename sxlemap temp; -data _null_; - file sxlemap; - put ''; - put ''; - put '//Objects/SASLibrary'; - put '>17'; - put '//Objects/SASLibrary/@Id'; - put '256>'; - put '//Objects/SASLibrary/@Name'; - put '8'; - put '//Objects/SASLibrary/@Libref'; - put '>12'; - put '//Objects/SASLibrary/@Engine'; - put '
'; -run; -libname _XML_ xml xmlfileref=response xmlmap=sxlemap; - -/* sort the response by library name */ -proc sort data=_XML_.saslibrary out=&outds; - by libraryname; -run; - - -/* clear references */ -filename sxlemap clear; -filename response clear; -libname _XML_ clear; - -%mend mm_getlibs; diff --git a/003_macros/mm_getobjects.sas b/003_macros/mm_getobjects.sas deleted file mode 100644 index 3da3393..0000000 --- a/003_macros/mm_getobjects.sas +++ /dev/null @@ -1,70 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a dataset with all metadata objects for a particular type - - @param [in] type= the metadata type for which to return all objects - @param [out] outds= the dataset to create that contains the list of types - - @returns outds dataset containing all objects - - @warning The following filenames are created and then de-assigned: - - filename sxlemap clear; - filename response clear; - libname _XML_ clear; - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getobjects( - type=SASLibrary - ,outds=work.mm_getobjects -)/*/STORE SOURCE*/; - - -* use a temporary fileref to hold the response; -filename response temp; -/* get list of libraries */ -proc metadata in= - "$METAREPOSITORY - &typeSAS - 0" - out=response; -run; - -/* write the response to the log for debugging */ -data _null_; - infile response lrecl=1048576; - input; - put _infile_; -run; - -/* create an XML map to read the response */ -filename sxlemap temp; -data _null_; - file sxlemap; - put ''; - put "/GetMetadataObjects/Objects/&type"; - put ""; - put ''; - put "/GetMetadataObjects/Objects/&type/@Id"; - put "characterstring200"; - put ''; - put "/GetMetadataObjects/Objects/&type/@Name"; - put "characterstring200"; - put '
'; -run; -libname _XML_ xml xmlfileref=response xmlmap=sxlemap; - -proc sort data= _XML_.SASObjects out=&outds; - by name; -run; - -/* clear references */ -filename sxlemap clear; -filename response clear; -libname _XML_ clear; - -%mend mm_getobjects; diff --git a/003_macros/mm_getpublictypes.sas b/003_macros/mm_getpublictypes.sas deleted file mode 100644 index a06d256..0000000 --- a/003_macros/mm_getpublictypes.sas +++ /dev/null @@ -1,88 +0,0 @@ -/*** HELP START ***//** - @file mm_getpublictypes.sas - @brief Creates a dataset with all deployable public types - @details More info: - https://support.sas.com/documentation/cdl/en/bisag/65422/HTML/default/viewer.htm#n1nkrdzsq5iunln18bk2236istkb.htm - - Usage: - - * dataset will contain one column - publictype ($64); - %mm_getpublictypes(outds=types) - - @param [out] outds= (work.mm_getpublictypes) The library.dataset to create - - @returns outds= dataset containing all types - - @version 9.3 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getpublictypes( - outds=work.mm_getpublictypes -)/*/STORE SOURCE*/; - -proc sql; -create table &outds (publictype char(64)); /* longest is currently 52 */ -insert into &outds values ('ACT'); -insert into &outds values ('Action'); -insert into &outds values ('Application'); -insert into &outds values ('ApplicationServer'); -insert into &outds values ('BurstDefinition'); -insert into &outds values ('Channel'); -insert into &outds values ('Condition'); -insert into &outds values ('ConditionActionSet'); -insert into &outds values ('ContentSubscriber'); -insert into &outds values ('Cube'); -insert into &outds values ('DataExploration'); -insert into &outds values ('DeployedFlow'); -insert into &outds values ('DeployedJob'); -insert into &outds values ('Document'); -insert into &outds values ('EventSubscriber'); -insert into &outds values ('ExternalFile'); -insert into &outds values ('FavoritesFolder'); -insert into &outds values ('Folder'); -insert into &outds values ('Folder.SecuredData'); -insert into &outds values ('GeneratedTransform'); -insert into &outds values ('InformationMap'); -insert into &outds values ('InformationMap.OLAP'); -insert into &outds values ('InformationMap.Relational'); -insert into &outds values ('JMSDestination (Java Messaging System message queue)'); -insert into &outds values ('Job'); -insert into &outds values ('Job.Cube'); -insert into &outds values ('Library'); -insert into &outds values ('MessageQueue'); -insert into &outds values ('MiningResults'); -insert into &outds values ('MQM.JMS (queue manager for Java Messaging Service)'); -insert into &outds values ('MQM.MSMQ (queue manager for MSMQ)'); -insert into &outds values ('MQM.Websphere (queue manager for WebSphere MQ)'); -insert into &outds values ('Note'); -insert into &outds values ('OLAPSchema'); -insert into &outds values ('Project'); -insert into &outds values ('Project.EG'); -insert into &outds values ('Project.AMOExcel'); -insert into &outds values ('Project.AMOPowerPoint'); -insert into &outds values ('Project.AMOWord'); -insert into &outds values ('Prompt'); -insert into &outds values ('PromptGroup'); -insert into &outds values ('Report'); -insert into &outds values ('Report.Component'); -insert into &outds values ('Report.Image'); -insert into &outds values ('Report.StoredProcess'); -insert into &outds values ('Role'); -insert into &outds values ('SearchFolder'); -insert into &outds values ('SecuredLibrary'); -insert into &outds values ('Server'); -insert into &outds values ('Service.SoapGenerated'); -insert into &outds values ('SharedDimension'); -insert into &outds values ('Spawner.Connect'); -insert into &outds values ('Spawner.IOM (object spawner)'); -insert into &outds values ('StoredProcess'); -insert into &outds values ('SubscriberGroup.Content'); -insert into &outds values ('SubscriberGroup.Event'); -insert into &outds values ('Table'); -insert into &outds values ('User'); -insert into &outds values ('UserGroup'); -quit; - -%mend mm_getpublictypes; diff --git a/003_macros/mm_getrepos.sas b/003_macros/mm_getrepos.sas deleted file mode 100644 index 470bb83..0000000 --- a/003_macros/mm_getrepos.sas +++ /dev/null @@ -1,133 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a dataset with all available repositories - - @param [out] outds= (work.mm_getrepos) - The dataset to create that contains the list of repos - - @returns outds dataset containing all repositories - - @warning The following filenames are created and then de-assigned: - - filename sxlemap clear; - filename response clear; - libname _XML_ clear; - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getrepos( - outds=work.mm_getrepos -)/*/STORE SOURCE*/; - - -* use a temporary fileref to hold the response; -filename response temp; -/* get list of libraries */ -proc metadata in= - "1" - out=response; -run; - -/* write the response to the log for debugging */ -/* -data _null_; - infile response lrecl=1048576; - input; - put _infile_; -run; -*/ - -/* create an XML map to read the response */ -filename sxlemap temp; -data _null_; - file sxlemap; - put ''; - put "/GetRepositories/Repositories/Repository"; - put ""; - put ''; - put "/GetRepositories/Repositories/Repository/@Id"; - put ""; - put "characterstring200"; - put ''; - put ''; - put "/GetRepositories/Repositories/Repository/@Name"; - put ""; - put "characterstring200"; - put ''; - put ''; - put "/GetRepositories/Repositories/Repository/@Desc"; - put ""; - put "characterstring200"; - put ''; - put ''; - put ""; - put "/GetRepositories/Repositories/Repository/@DefaultNS"; - put "characterstring200"; - put ''; - put ''; - put ""; - put "/GetRepositories/Repositories/Repository/@RepositoryType"; - put "characterstring20"; - put ''; - put ''; - put ""; - put "/GetRepositories/Repositories/Repository/@RepositoryFormat"; - put "characterstring10"; - put ''; - put ''; - put ""; - put "/GetRepositories/Repositories/Repository/@Access"; - put "characterstring16"; - put ''; - put ''; - put ""; - put "/GetRepositories/Repositories/Repository/@CurrentAccess"; - put "characterstring16"; - put ''; - put ''; - put ""; - put "/GetRepositories/Repositories/Repository/@PauseState"; - put "characterstring16"; - put ''; - put ''; - put "/GetRepositories/Repositories/Repository/@Path"; - put ""; - put "characterstring256"; - put ''; - put ''; - put "/GetRepositories/Repositories/Repository/@Engine"; - put ""; - put "characterstring8"; - put ''; - put ''; - put "/GetRepositories/Repositories/Repository/@Options"; - put ""; - put "characterstring32"; - put ''; - put ''; - put ""; - put "/GetRepositories/Repositories/Repository/@MetadataCreated"; - put "characterstring24"; - put ''; - put ''; - put ""; - put "/GetRepositories/Repositories/Repository/@MetadataUpdated"; - put "characterstring24"; - put ''; - put '
'; -run; -libname _XML_ xml xmlfileref=response xmlmap=sxlemap; - -proc sort data= _XML_.SASRepos out=&outds; - by name; -run; - -/* clear references */ -filename sxlemap clear; -filename response clear; -libname _XML_ clear; - -%mend mm_getrepos; diff --git a/003_macros/mm_getroles.sas b/003_macros/mm_getroles.sas deleted file mode 100644 index 99f0362..0000000 --- a/003_macros/mm_getroles.sas +++ /dev/null @@ -1,74 +0,0 @@ -/*** HELP START ***//** - @file mm_getroles.sas - @brief Creates a table containing a list of roles - @details - - Usage: - - %mm_getroles() - - @param [out] outds= (work.mm_getroles) - The dataset to create that contains the list of roles - - @returns outds dataset containing all roles, with the following columns: - - uri - - name - - @warning The following filenames are created and then de-assigned: - - filename sxlemap clear; - filename response clear; - libname _XML_ clear; - - @version 9.3 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getroles( - outds=work.mm_getroles -)/*/STORE SOURCE*/; - -filename response temp; -options noquotelenmax; -proc metadata in= '$METAREPOSITORY - IdentityGroupSAS388 - - - - - ' - out=response; -run; - -filename sxlemap temp; -data _null_; - file sxlemap; - put ''; - put "/GetMetadataObjects/Objects/IdentityGroup"; - put ""; - put ''; - put "/GetMetadataObjects/Objects/IdentityGroup/@Id"; - put ""; - put "characterstring32"; - put ''; - put "/GetMetadataObjects/Objects/IdentityGroup/@Name"; - put ""; - put "characterstring256"; - put ''; - put "/GetMetadataObjects/Objects/IdentityGroup/@Desc"; - put ""; - put "characterstring500"; - put '
'; -run; -libname _XML_ xml xmlfileref=response xmlmap=sxlemap; - -proc sort data= _XML_.roles out=&outds; - by rolename; -run; - -filename sxlemap clear; -filename response clear; -libname _XML_ clear; - -%mend mm_getroles; diff --git a/003_macros/mm_getservercontexts.sas b/003_macros/mm_getservercontexts.sas deleted file mode 100644 index c47dfe2..0000000 --- a/003_macros/mm_getservercontexts.sas +++ /dev/null @@ -1,92 +0,0 @@ -/*** HELP START ***//** - @file mm_getservercontexts.sas - @brief Creates a dataset with all server contexts in all repos - @details - Usage: - - %mm_getservercontexts(outds=mm_getservercontexts) - - @param [out] outds= (work.mm_getrepos) - the dataset to create that contains the list - - @warning The following filenames are created and then de-assigned: - - filename __mc1 clear; - filename __mc2 clear; - libname __mc3 clear; - -

SAS Macros

- @li mm_getrepos.sas - - @version 9.3 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getservercontexts( - outds=work.mm_getrepos -)/*/STORE SOURCE*/; -%local repo repocnt x; -%let repo=%sysfunc(getoption(metarepository)); - -/* first get list of available repos */ -%mm_getrepos(outds=work.repos) -%let repocnt=0; -data _null_; - set repos; - where repositorytype in('CUSTOM','FOUNDATION'); - keep id name ; - call symputx(cats('repo',_n_),name,'l'); - call symputx('repocnt',_n_,'l'); -run; - -filename __mc1 temp; -filename __mc2 temp; -data &outds; - length serveruri servername $200; - call missing (of _all_); - stop; -run; -%do x=1 %to &repocnt; - options metarepository=&&repo&x; - proc metadata in= - "$METAREPOSITORY - ServerContextSAS - 0" - out=__mc1; - run; - /* - data _null_; - infile __mc1 lrecl=1048576; - input; - put _infile_; - run; - */ - data _null_; - file __mc2; - put ''; - put "/GetMetadataObjects/Objects/ServerContext"; - put ""; - put ''; - put "/GetMetadataObjects/Objects/ServerContext/@Id"; - put ""; - put "characterstring200"; - put ''; - put ''; - put "/GetMetadataObjects/Objects/ServerContext/@Name"; - put ""; - put "characterstring200"; - put ''; - put '
'; - run; - libname __mc3 xml xmlfileref=__mc1 xmlmap=__mc2; - proc append base=&outds data=__mc3.SASContexts;run; - libname __mc3 clear; -%end; - -options metarepository=&repo; - -filename __mc1 clear; -filename __mc2 clear; - -%mend mm_getservercontexts; diff --git a/003_macros/mm_getstpcode.sas b/003_macros/mm_getstpcode.sas deleted file mode 100644 index 8101fba..0000000 --- a/003_macros/mm_getstpcode.sas +++ /dev/null @@ -1,170 +0,0 @@ -/*** HELP START ***//** - @file - @brief Writes the code of an STP to an external file - @details Fetches the SAS code from a Stored Process where the code is stored - in metadata. - - Usage: - - %mm_getstpcode(tree=/some/meta/path - ,name=someSTP - ,outloc=/some/unquoted/filename.ext - ) - - @param [in] tree= The metadata path of the Stored Process (can also contain - name) - @param [in] name= Stored Process name. Leave blank if included above. - @param [out] outloc= (0) full and unquoted path to the desired text file. - This will be overwritten if it already exists. - @param [out] outref= (0) Fileref to which to write the code. - @param [out] showlog=(NO) Set to YES to print log to the window - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mp_abort.sas - - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getstpcode( - tree=/User Folders/sasdemo/somestp - ,name= - ,outloc=0 - ,outref=0 - ,mDebug=1 - ,showlog=NO - ); - -%local mD; -%if &mDebug=1 %then %let mD=; -%else %let mD=%str(*); -%&mD.put Executing &sysmacroname..sas; -%&mD.put _local_; - -%if %length(&name)>0 %then %let name=/&name; - -/* first, check if STP exists */ -%local tsuri; -%let tsuri=stopifempty ; - -data _null_; - format type uri tsuri value $200.; - call missing (of _all_); - path="&tree&name(StoredProcess)"; - /* first, find the STP ID */ - if metadata_pathobj("",path,"StoredProcess",type,uri)>0 then do; - /* get sourcecode */ - cnt=1; - do while (metadata_getnasn(uri,"Notes",cnt,tsuri)>0); - rc=metadata_getattr(tsuri,"Name",value); - &mD.put tsuri= value=; - if value="SourceCode" then do; - /* found it! */ - rc=metadata_getattr(tsuri,"Id",value); - call symputx('tsuri',value,'l'); - stop; - end; - cnt+1; - end; - end; - else put (_all_)(=); -run; - -%mp_abort(iftrue= (&tsuri=stopifempty) - ,mac=mm_getstpcode - ,msg=%str(&tree&name.(StoredProcess) not found!) -) - -/** - * Now we can extract the textstore - */ -filename __getdoc temp lrecl=10000000; -proc metadata - in="$METAREPOSITORY - - SAS1" - out=__getdoc ; -run; - -/* find the beginning of the text */ -%local start; -data _null_; - infile __getdoc lrecl=10000; - input; - start=index(_infile_,'StoredText="'); - if start then do; - call symputx("start",start+11); - *putlog '"' _infile_ '"'; - end; - stop; - -%local outeng; -%if "&outloc"="0" %then %let outeng=TEMP; -%else %let outeng="&outloc"; -%local fref; -%if &outref=0 %then %let fref=%mf_getuniquefileref(); -%else %let fref=&outref; - -/* read the content, byte by byte, resolving escaped chars */ -filename &fref &outeng lrecl=100000; -data _null_; - length filein 8 fileid 8; - filein = fopen("__getdoc","I",1,"B"); - fileid = fopen("&fref","O",1,"B"); - rec = "20"x; - length entity $6; - do while(fread(filein)=0); - x+1; - if x>&start then do; - rc = fget(filein,rec,1); - if rec='"' then leave; - else if rec="&" then do; - entity=rec; - do until (rec=";"); - if fread(filein) ne 0 then goto getout; - rc = fget(filein,rec,1); - entity=cats(entity,rec); - end; - select (entity); - when ('&' ) rec='&' ; - when ('<' ) rec='<' ; - when ('>' ) rec='>' ; - when (''') rec="'" ; - when ('"') rec='"' ; - when (' ') rec='0A'x; - when (' ') rec='0D'x; - when ('$' ) rec='$' ; - when (' ') rec='09'x; - otherwise putlog "%str(WARN)ING: missing value for " entity=; - end; - rc =fput(fileid, substr(rec,1,1)); - rc =fwrite(fileid); - end; - else do; - rc =fput(fileid,rec); - rc =fwrite(fileid); - end; - end; - end; - getout: - rc=fclose(filein); - rc=fclose(fileid); -run; - -%if &showlog=YES %then %do; - data _null_; - infile &fref lrecl=32767 end=last; - input; - if _n_=1 then putlog '>>stpcodeBEGIN<<'; - putlog _infile_; - if last then putlog '>>stpcodeEND<<'; - run; -%end; - -filename __getdoc clear; -%if &outref=0 %then %do; - filename &fref clear; -%end; - -%mend mm_getstpcode; diff --git a/003_macros/mm_getstpinfo.sas b/003_macros/mm_getstpinfo.sas deleted file mode 100644 index 23a44f2..0000000 --- a/003_macros/mm_getstpinfo.sas +++ /dev/null @@ -1,67 +0,0 @@ -/*** HELP START ***//** - @file - @brief Get the properties of a Stored Process - @details Extracts various properties and creates an output table in the - structure below: - -|STP_URI:$200.|SERVERCONTEXT:$200.|STOREDPROCESSCONFIGURATION:$1000.|SOURCECODE_FIRST32K:$32767.|PATH:$76.| -|---|---|---|---|---| -|`A5DN9TDQ.BH0000C8 `|`SASApp `|` `|`%put first 32767 bytes of code; `|`/path/to/my/stp`| - - @param [in] pgm The metadata path of the Stored Process - @param [out] outds= (work.mm_getstpinfo) The output table to create - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - -

Related Files

- @li mm_getstpcode.sas - @li mm_getstps.sas - @li mm_createstp.sas - @li mm_deletestp.sas - -**//*** HELP END ***/ - -%macro mm_getstpinfo(pgm - ,outds=work.mm_getstpinfo - ,mDebug=0 -); - -%local mD; -%if &mDebug=1 %then %let mD=; -%else %let mD=%str(*); -%&mD.put Executing &sysmacroname..sas; -%&mD.put _local_; - -data &outds; - length type stp_uri tsuri servercontext value $200 - StoredProcessConfiguration $1000 sourcecode_first32k $32767; - keep path stp_uri sourcecode_first32k StoredProcessConfiguration - servercontext; - call missing (of _all_); - path="&pgm(StoredProcess)"; - /* first, find the STP ID */ - if metadata_pathobj("",path,"StoredProcess",type,stp_uri)>0 then do; - /* get attributes */ - cnt=1; - do while (metadata_getnasn(stp_uri,"Notes",cnt,tsuri)>0); - rc1=metadata_getattr(tsuri,"Name",value); - &mD.put tsuri= value=; - if value="SourceCode" then do; - rc2=metadata_getattr(tsuri,"StoredText",sourcecode_first32k); - end; - else if value="Stored Process" then do; - rc3=metadata_getattr(tsuri,"StoredText",StoredProcessConfiguration); - end; - cnt+1; - end; - /* get context (should only be one) */ - rc4=metadata_getnasn(stp_uri,"ComputeLocations",1,tsuri); - rc5=metadata_getattr(tsuri,"Name",servercontext); - end; - else do; - put 'ERR' +(-1) "OR: could not find " path; - put (_all_)(=); - end; - &md.put (_all_)(=); -run; - -%mend mm_getstpinfo ; diff --git a/003_macros/mm_getstps.sas b/003_macros/mm_getstps.sas deleted file mode 100644 index b8d0f7c..0000000 --- a/003_macros/mm_getstps.sas +++ /dev/null @@ -1,110 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns a dataset with all Stored Processes, or just those in a - particular folder / with a particular name. - @details Leave blank to get all stps. Provide a Tree (path or uri) or a - name (not case sensitive) to filter that way also. - usage: - - %mm_getstps() - - %mm_getstps(name=My STP) - - %mm_getstps(tree=/My Folder/My STPs) - - %mm_getstps(tree=/My Folder/My STPs, name=My STP) - -

SAS Macros

- @li mm_gettree.sas - - @param [in] tree= () the metadata folder location in which to search. - Leave blank for all folders. Does not search subdirectories. - @param [in] name= Provide the name of an STP to search for just that one. Can - combine with the tree= parameter. - @param [out] outds= the dataset to create that contains the list of stps. - @param [in] mDebug= set to 1 to show debug messages in the log - @param [in] showDesc= provide a non blank value to return stored process - descriptions - @param [in] showUsageVersion= () - Provide a non blank value to return the UsageVersion. - This is either 1000000 (type 1, 9.2) or 2000000 (type2, 9.3 onwards). - - @returns outds dataset containing the following columns - - stpuri - - stpname - - treeuri - - stpdesc (if requested) - - usageversion (if requested) - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getstps( - tree= - ,name= - ,outds=work.mm_getstps - ,mDebug=0 - ,showDesc= - ,showUsageVersion= -)/*/STORE SOURCE*/; - -%local mD; -%if &mDebug=1 %then %let mD=; -%else %let mD=%str(*); -%&mD.put Executing mm_getstps.sas; -%&mD.put _local_; - -data &outds; - length stpuri stpname usageversion treeuri stpdesc $256; - call missing (of _all_); -run; - -%if %length(&tree)>0 %then %do; - /* get tree info */ - %mm_gettree(tree=&tree,inds=&outds, outds=&outds, mDebug=&mDebug) - %if %mf_nobs(&outds)=0 %then %do; - %put NOTE: Tree &tree did not exist!!; - %return; - %end; -%end; - - -data &outds ; - set &outds(rename=(treeuri=treeuri_compare)); - length treeuri query stpuri $256; - i+1; -%if %length(&name)>0 %then %do; - query="omsobj:ClassifierMap?@PublicType='StoredProcess' and @Name='&name'"; - putlog query=; -%end; -%else %do; - query="omsobj:ClassifierMap?@PublicType='StoredProcess'"; -%end; -%if &mDebug=1 %then %do; - putlog 'start' (_all_)(=); -%end; - do while(00 %then %do; - if treeuri ne treeuri_compare then goto exitloop; - %end; - %if %length(&showDesc)>0 %then %do; - rc3=metadata_getattr(stpuri,"Desc", stpdesc); - keep stpdesc; - %end; - %if %length(&showUsageVersion)>0 %then %do; - rc4=metadata_getattr(stpuri,"UsageVersion",UsageVersion); - keep usageversion; - %end; - output; - &mD.put (_all_)(=); - exitloop: - end; - keep stpuri stpname treeuri; -run; - -%mend mm_getstps; diff --git a/003_macros/mm_gettableid.sas b/003_macros/mm_gettableid.sas deleted file mode 100644 index a1450ff..0000000 --- a/003_macros/mm_gettableid.sas +++ /dev/null @@ -1,67 +0,0 @@ -/*** HELP START ***//** - @file mm_gettableid.sas - @brief Get the metadata id for a particular table - @details Provide a libref and table name to return the corresponding metadata - in an output datataset. - - Usage: - - - get a table id - %mm_gettableid(libref=METALIB,ds=SOMETABLE,outds=iwant) - - @param [in] libref= The libref to search - @param [in] ds= The input dataset to check - @param [out] outds= the dataset to create that contains the `tableuri` - @param [in] mDebug= set to 1 to show debug messages in the log - - @returns outds dataset containing `tableuri` and `tablename` - - @version 9.3 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_gettableid( - libref= - ,ds= - ,outds=work.mm_gettableid - ,mDebug=0 -)/*/STORE SOURCE*/; - -%local mD; -%if &mDebug=1 %then %let mD=; -%else %let mD=%str(*); -%&mD.put Executing &sysmacroname..sas; -%&mD.put _local_; - -data &outds; - length uri usingpkguri id type tableuri tablename tmpuri $256; - call missing(of _all_); - keep tableuri tablename; - n=1; - rc=0; - if metadata_getnobj("omsobj:SASLibrary?@Libref='&libref'",n,uri)<1 then do; - put "Library &libref not found"; - stop; - end; - &mD.putlog "uri is " uri; - if metadata_getnasn(uri, "UsingPackages", 1, usingpkguri)>0 then do; - rc=metadata_resolve(usingpkguri,type,id); - &mD.putlog "Type is " type; - end; - - if type='DatabaseSchema' then tmpuri=usingpkguri; - else tmpuri=uri; - - t=1; - do while(metadata_getnasn(tmpuri, "Tables", t, tableuri)>0); - t+1; - rc= metadata_getattr(tableuri, "Name", tablename); - &mD.putlog "Table is " tablename; - if upcase(tablename)="%upcase(&ds)" then do; - output; - end; - end; -run; - -%mend mm_gettableid; diff --git a/003_macros/mm_gettables.sas b/003_macros/mm_gettables.sas deleted file mode 100644 index bd22c6b..0000000 --- a/003_macros/mm_gettables.sas +++ /dev/null @@ -1,120 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a dataset with all metadata tables for a particular library - @details Will only show the tables for which the executing user has the - requisite metadata access. - - usage: - - %mm_gettables(uri=A5X8AHW1.B40001S5) - - @param [in] uri= the uri of the library for which to return tables - @param [out] outds= (work.mm_gettables) the dataset to contain the list of - tables - @param [in] getauth= (YES) Fetch the authdomain used in database connections. - Set to NO to improve runtimes in larger environments, as there can be a - performance hit on the `metadata_getattr(domainuri, "Name", AuthDomain)` - call. - - @returns outds dataset containing all groups in a column named "metagroup" - (defaults to work.mm_getlibs). The following columns are provided: - - tablename - - tableuri - - libref - - libname - - libdesc - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_gettables( - uri= - ,outds=work.mm_gettables - ,getauth=YES -)/*/STORE SOURCE*/; - - -data &outds; - length uri serveruri conn_uri domainuri libname ServerContext AuthDomain - path_schema usingpkguri type tableuri $256 id $17 - libdesc $200 libref engine $8 IsDBMSLibname IsPreassigned $1 - tablename $50 /* metadata table names can be longer than $32 */ - ; - keep libname libdesc libref engine ServerContext path_schema AuthDomain - tableuri tablename IsPreassigned IsDBMSLibname id; - call missing (of _all_); - - uri=symget('uri'); - rc= metadata_getattr(uri, "Name", libname); - if rc <0 then do; - put 'The library is not defined in this metadata repository.'; - stop; - end; - rc= metadata_getattr(uri, "Desc", libdesc); - rc= metadata_getattr(uri, "Libref", libref); - rc= metadata_getattr(uri, "Engine", engine); - rc= metadata_getattr(uri, "IsDBMSLibname", IsDBMSLibname); - rc= metadata_getattr(uri, "IsPreassigned", IsPreassigned); - rc= metadata_getattr(uri, "Id", Id); - - /*** Get associated ServerContext ***/ - rc= metadata_getnasn(uri, "DeployedComponents", 1, serveruri); - if rc > 0 then rc2= metadata_getattr(serveruri, "Name", ServerContext); - else ServerContext=''; - - /*** If the library is a DBMS library, get the Authentication Domain - associated with the DBMS connection credentials ***/ - if IsDBMSLibname="1" and "&getauth"='YES' then do; - rc= metadata_getnasn(uri, "LibraryConnection", 1, conn_uri); - if rc>0 then do; - rc2= metadata_getnasn(conn_uri, "Domain", 1, domainuri); - if rc2>0 then rc3= metadata_getattr(domainuri, "Name", AuthDomain); - end; - end; - - /*** Get the path/database schema for this library ***/ - rc=metadata_getnasn(uri, "UsingPackages", 1, usingpkguri); - if rc>0 then do; - rc=metadata_resolve(usingpkguri,type,id); - if type='Directory' then - rc=metadata_getattr(usingpkguri, "DirectoryName", path_schema); - else if type='DatabaseSchema' then - rc=metadata_getattr(usingpkguri, "Name", path_schema); - else path_schema="unknown"; - end; - - /*** Get the tables associated with this library ***/ - /*** If DBMS, tables are associated with DatabaseSchema ***/ - if type='DatabaseSchema' then do; - t=1; - ntab=metadata_getnasn(usingpkguri, "Tables", t, tableuri); - if ntab>0 then do t=1 to ntab; - tableuri=''; - tablename=''; - ntab=metadata_getnasn(usingpkguri, "Tables", t, tableuri); - tabrc= metadata_getattr(tableuri, "Name", tablename); - output; - end; - else put 'Library ' libname ' has no tables registered'; - end; - else if type in ('Directory','SASLibrary') then do; - t=1; - ntab=metadata_getnasn(uri, "Tables", t, tableuri); - if ntab>0 then do t=1 to ntab; - tableuri=''; - tablename=''; - ntab=metadata_getnasn(uri, "Tables", t, tableuri); - tabrc= metadata_getattr(tableuri, "Name", tablename); - output; - end; - else put 'Library ' libname ' has no tables registered'; - end; -run; - -proc sort; -by tablename tableuri; -run; - -%mend mm_gettables; diff --git a/003_macros/mm_gettree.sas b/003_macros/mm_gettree.sas deleted file mode 100644 index 31984a6..0000000 --- a/003_macros/mm_gettree.sas +++ /dev/null @@ -1,68 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns the metadata path and object from either the path or object - @details Provide a metadata BIP tree path, or the uri for the bottom level - folder, to obtain a dataset (&outds) containing both the path - and uri. - - Usage: - - %mm_getTree(tree=/User Folders/sasdemo) - - - @param [in] tree= the BIP Tree folder path or uri - @param [out] outds= the dataset to create that contains the tree path & uri - @param [in] inds= an optional input dataset to augment with treepath & treeuri - @param [in] mDebug= set to 1 to show debug messages in the log - - @returns outds dataset containing the following columns: - - treeuri - - treepath - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getTree( - tree= - ,inds= - ,outds=work.mm_getTree - ,mDebug=0 -)/*/STORE SOURCE*/; - -%local mD; -%if &mDebug=1 %then %let mD=; -%else %let mD=%str(*); -%&mD.put Executing mm_getTree.sas; -%&mD.put _local_; - -data &outds; - length treeuri __parenturi __type __name $256 treepath $512; -%if %length(&inds)>0 %then %do; - set &inds; -%end; - __rc1=metadata_resolve("&tree",__type,treeuri); - - if __type='Tree' then do; - __rc2=metadata_getattr(treeuri,"Name",__name); - treepath=cats('/',__name); - /* get parents */ - do while (metadata_getnasn(treeuri,"ParentTree",1,__parenturi)>0); - __rc3=metadata_getattr(__parenturi,"Name",__name); - treepath=cats('/',__name,treepath); - treeuri=__parenturi; - end; - treeuri="&tree"; - end; - else do; - __rc2=metadata_pathobj(' ',"&tree",'Folder',__type,treeuri); - treepath="&tree"; - end; - - &mD.put (_all_)(=); - drop __:; - if treeuri ne "" and treepath ne "" then output; - stop; -run; -%mend mm_getTree; diff --git a/003_macros/mm_gettypes.sas b/003_macros/mm_gettypes.sas deleted file mode 100644 index 43cd8d1..0000000 --- a/003_macros/mm_gettypes.sas +++ /dev/null @@ -1,76 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a dataset with all metadata types - @details Usage: - - %mm_gettypes(outds=types) - - @param [in] outds= (work.mm_gettypes) - The dataset to create that contains the list of types - @returns outds dataset containing all types - @warning The following filenames are created and then de-assigned: - - filename sxlemap clear; - filename response clear; - libname _XML_ clear; - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_gettypes( - outds=work.mm_gettypes -)/*/STORE SOURCE*/; - -* use a temporary fileref to hold the response; -filename response temp; -/* get list of libraries */ -proc metadata in= - ' - - SAS - - 2048 - - - $METAREPOSITORY - - ' - out=response; -run; - -/* write the response to the log for debugging */ -data _null_; - infile response lrecl=1048576; - input; - put _infile_; -run; - -/* create an XML map to read the response */ -filename sxlemap temp; -data _null_; - file sxlemap; - put ''; - put '//GetTypes/Types/Type'; - put '64'; - put '//GetTypes/Types/Type/@Id'; - put '256'; - put '//GetTypes/Types/Type/@Desc'; - put ''; - put '//GetTypes/Types/Type/@HasSubtypes'; - put '
'; -run; -libname _XML_ xml xmlfileref=response xmlmap=sxlemap; -/* sort the response by library name */ -proc sort data=_XML_.sastypes out=&outds; - by id; -run; - - -/* clear references */ -filename sxlemap clear; -filename response clear; -libname _XML_ clear; - -%mend mm_gettypes; diff --git a/003_macros/mm_getusers.sas b/003_macros/mm_getusers.sas deleted file mode 100644 index 682b979..0000000 --- a/003_macros/mm_getusers.sas +++ /dev/null @@ -1,96 +0,0 @@ -/*** HELP START ***//** - @file mm_getusers.sas - @brief Creates a table containing a list of all users - @details Only shows a limited number of attributes as some sites will have a - LOT of users. - - Usage: - - %mm_getusers() - - Optionally, filter for a user (useful to get the uri): - - %mm_getusers(user=&_metaperson) - - @returns outds dataset containing all users, with the following columns: - - uri - - name - - @param [in] user= (0) Set to a metadata user to filter on that user - @param [out] outds= (work.mm_getusers) The output table to create - - @version 9.3 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_getusers( - outds=work.mm_getusers, - user=0 -)/*/STORE SOURCE*/; - -filename response temp; -%if %superq(user)=0 %then %do; - proc metadata in= ' - $METAREPOSITORY - Person - SAS - 0 - - - - - - ' - out=response; - run; -%end; -%else %do; - filename inref temp; - data _null_; - file inref; - put ""; - put "$METAREPOSITORY"; - put "Person"; - put "SAS"; - put ""; - put "128"; - put ""; - put ""; - put ''; - put ""; - length string $10000; - string=cats(''); - put string; - put ""; - put ""; - run; - proc metadata in=inref out=response; - run; -%end; - -filename sxlemap temp; -data _null_; - file sxlemap; - put ''; - put "/GetMetadataObjects/Objects/Person"; - put ""; - put ''; - put "/GetMetadataObjects/Objects/Person/@Id"; - put "characterstring32"; - put ''; - put "/GetMetadataObjects/Objects/Person/@Name"; - put "characterstring256"; - put '
'; -run; -libname _XML_ xml xmlfileref=response xmlmap=sxlemap; - -proc sort data= _XML_.SASObjects out=&outds; - by name; -run; - -filename sxlemap clear; -filename response clear; -libname _XML_ clear; - -%mend mm_getusers; diff --git a/003_macros/mm_getwebappsrvprops.sas b/003_macros/mm_getwebappsrvprops.sas deleted file mode 100644 index ab1e584..0000000 --- a/003_macros/mm_getwebappsrvprops.sas +++ /dev/null @@ -1,135 +0,0 @@ -/*** HELP START ***//** - @file - @brief Retrieves properties of the SAS web app server - @details - Usage: - - %mm_getwebappsrvprops(outds= some_ds) - data _null_; - set some_ds(where=(name='webappsrv.server.url')); - put value=; - run; - - @param [out] outds= the dataset to create that contains the list of properties - - @returns outds dataset containing all properties - - @warning The following filenames are created and then de-assigned: - - filename __in clear; - filename __out clear; - libname __shake clear; - - @version 9.4 - @author Allan Bowe https://github.com/sasjs/core - -**//*** HELP END ***/ - -%macro mm_getwebappsrvprops( - outds= mm_getwebappsrvprops -)/*/STORE SOURCE*/; - -filename __in temp lrecl=10000; -filename __out temp lrecl=10000; -filename __shake temp lrecl=10000; -data _null_ ; - file __in ; - put '' ; - put '$METAREPOSITORY' ; - put 'TextStore' ; - put 'SAS' ; - put '388' ; - put '' ; - put ''; - put '' ; - put '' ; - put '' ; - put '' ; - put '' ; - put '' ; -run ; -proc metadata in=__in out=__out verbose;run; - -/* find the beginning of the text */ -%local start; -%let start=0; -data _null_; - infile __out lrecl=10000; - input; - length cleartemplate $32000; - cleartemplate=tranwrd(_infile_,'StoredText=""',''); - start=index(cleartemplate,'StoredText="'); - if start then do; - call symputx("start",start+11+length('StoredText=""')-1); - putlog cleartemplate ; - end; - stop; -run; -%put &=start; -%if &start>0 %then %do; - /* read the content, byte by byte, resolving escaped chars */ - data _null_; - length filein 8 fileid 8; - filein = fopen("__out","I",1,"B"); - fileid = fopen("__shake","O",1,"B"); - rec = "20"x; - length entity $6; - do while(fread(filein)=0); - x+1; - if x>&start then do; - rc = fget(filein,rec,1); - if rec='"' then leave; - else if rec="&" then do; - entity=rec; - do until (rec=";"); - if fread(filein) ne 0 then goto getout; - rc = fget(filein,rec,1); - entity=cats(entity,rec); - end; - select (entity); - when ('&' ) rec='&' ; - when ('<' ) rec='<' ; - when ('>' ) rec='>' ; - when (''') rec="'" ; - when ('"') rec='"' ; - when (' ') rec='0A'x; - when (' ') rec='0D'x; - when ('$' ) rec='$' ; - when (' ') rec='09'x; - otherwise putlog "%str(WARN)ING: missing value for " entity=; - end; - rc =fput(fileid, substr(rec,1,1)); - rc =fwrite(fileid); - end; - else do; - rc =fput(fileid,rec); - rc =fwrite(fileid); - end; - end; - end; - getout: - rc=fclose(filein); - rc=fclose(fileid); - run; - data &outds ; - infile __shake dlm='=' missover; - length name $50 value $500; - input name $ value $; - run; -%end; -%else %do; - %put NOTE: Unable to retrieve Web App Server Properties; - data &outds; - length name $50 value $500; - run; -%end; - -/* clear references */ -filename __in clear; -filename __out clear; -filename __shake clear; - -%mend mm_getwebappsrvprops; diff --git a/003_macros/mm_spkexport.sas b/003_macros/mm_spkexport.sas deleted file mode 100644 index 2be9bf8..0000000 --- a/003_macros/mm_spkexport.sas +++ /dev/null @@ -1,146 +0,0 @@ -/*** HELP START ***//** - @file mm_spkexport.sas - @brief Creates an batch spk export command - @details Creates a script that will export everything in a metadata folder to - a specified location. - If you have XCMD enabled, then you can use mmx_spkexport (which performs - the actual export) - - Note - the batch tools require a username and password. For security, - these are expected to have been provided in a protected directory. - - Usage: - - %* import the macros (or make them available some other way); - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - %* create sample text file as input to the macro; - filename tmp temp; - data _null_; - file tmp; - put '%let mmxuser="sasdemo";'; - put '%let mmxpass="Mars321";'; - run; - - filename myref "%sysfunc(pathname(work))/mmxexport.sh" - permission='A::u::rwx,A::g::r-x,A::o::---'; - %mm_spkexport(metaloc=%str(/my/meta/loc) - ,outref=myref - ,secureref=tmp - ,cmdoutloc=%str(/tmp) - ) - - Alternatively, call without inputs to create a function style output - - filename myref "/tmp/mmscript.sh" - permission='A::u::rwx,A::g::r-x,A::o::---'; - %mm_spkexport(metaloc=%str(/my/meta/loc) - outref=myref - ,cmdoutloc=%str(/tmp) - ,cmdoutname=mmx - ) - - You can then navigate and execute as follows: - - cd /tmp - ./mmscript.sh "myuser" "mypass" - - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mf_getuniquename.sas - @li mf_isblank.sas - @li mf_loc.sas - @li mm_tree.sas - @li mp_abort.sas - - - @param [in] metaloc= the metadata folder to export - @param [in] secureref= fileref containing the username / password (should - point to a file in a secure location). Leave blank to substitute $bash vars. - @param [in] excludevars= (0) A space seperated list of macro variable names, - each of which contains a value that should be used to filter the output - objects. - @param [out] outref= fileref to which to write the command - @param [out] cmdoutloc= (%sysfunc(pathname(work))) The directory to which the - command will write the SPK - @param [out] cmdoutname= (mmxport) The name of the spk / log files to create - (will be identical just with .spk or .log extension) - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_spkexport(metaloc= - ,secureref= - ,excludevars=0 - ,outref= - ,cmdoutloc=%sysfunc(pathname(work)) - ,cmdoutname=mmxport -); - -%if &sysscp=WIN %then %do; - %put %str(WARN)ING: the script has been written assuming a unix system; - %put %str(WARN)ING- it will run anyway as should be easy to modify; -%end; - -/* set creds */ -%local mmxuser mmxpath i var; -%let mmxuser=$1; -%let mmxpass=$2; -%if %mf_isblank(&secureref)=0 %then %do; - %inc &secureref/nosource; -%end; - -/* setup metadata connection options */ -%local host port platform_object_path ds; -%let host=%sysfunc(getoption(metaserver)); -%let port=%sysfunc(getoption(metaport)); -%let platform_object_path=%mf_loc(POF); -%let ds=%mf_getuniquename(prefix=spkexportable); - -%mm_tree(root=%str(&metaloc),types=EXPORTABLE ,outds=&ds) - -%if %mf_isblank(&outref)=1 %then %let outref=%mf_getuniquefileref(); - -data _null_; - set &ds end=last; - file &outref lrecl=32767; - length str $32767; - if _n_=1 then do; - put "# Script generated by &sysuserid on %sysfunc(datetime(),datetime19.)"; - put "cd ""&platform_object_path"" \"; - put "; ./ExportPackage -host &host -port &port -user &mmxuser \"; - put " -disableX11 -password &mmxpass \"; - put " -package ""&cmdoutloc/&cmdoutname..spk"" \"; - end; -/* exclude particular patterns from the exported SPK */ -%if "&excludevars" ne "0" %then %do; - %do i=1 %to %sysfunc(countw(&excludevars)); - %let var=%scan(&excludevars,&i); - if _n_=1 then do; - length excludestr&i $1000; - retain excludestr&i; - excludestr&i=symget("&var"); - putlog excludestr&i=; - putlog path=; - end; - if index(path,cats(excludestr&i))=0 and index(name,cats(excludestr&i))=0; - %end; - /* ignore top level folder else all subcontent will be exported regardless */ - if _n_>1; -%end; - str=' -objects '!!cats('"',path,'/',name,"(",publictype,')" \'); - put str; - if last then put " -log ""&cmdoutloc/&cmdoutname..log"" 2>&1 "; -run; - -%mp_abort(iftrue= (&syscc ne 0) - ,mac=mm_spkexport - ,msg=%str(syscc=&syscc) -) - -%mend mm_spkexport; diff --git a/003_macros/mm_tree.sas b/003_macros/mm_tree.sas deleted file mode 100644 index ca7efe9..0000000 --- a/003_macros/mm_tree.sas +++ /dev/null @@ -1,173 +0,0 @@ -/*** HELP START ***//** - @file mm_tree.sas - @brief Returns all folders / subfolder content for a particular root - @details Shows all members and SubTrees for a particular root. - - Model: - - metauri char(64), - name char(256) format=$256. informat=$256. label='name', - path char(1024), - publictype char(32), - MetadataUpdated char(32), - MetadataCreated char(32) - - Usage: - - %* load macros; - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - %* export everything; - %mm_tree(root= ,outds=iwantthisdataset) - - %* export everything in a specific folder; - %mm_tree(root=%str(/my/folder) ,outds=stuff) - - %* export only folders; - %mm_tree(root=%str(/my/folder) ,types=Folder ,outds=stuf) - - %* show only exportable content; - %mm_tree(root=%str(/) ,types=EXPORTABLE ,outds=exportable) - - %* with specific types; - %mm_tree(root=%str(/my/folder) - ,types= - DeployedJob - ExternalFile - Folder - Folder.SecuredData - GeneratedTransform - InformationMap.Relational - Job - Library - Prompt - StoredProcess - Table - ,outds=morestuff) - -

SAS Macros

- @li mf_getquotedstr.sas - @li mm_getpublictypes.sas - @li mf_isblank.sas - - @param [in] root= the parent folder under which to return all contents - @param [in] outds= the dataset to create that contains the list of directories - @param [in] types= Space-seperated, unquoted list of types for filtering the - output. Special types: - - * ALl - return all types (the default) - * EXPORTABLE - return only the content types that can be exported in an SPK - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ -%macro mm_tree( - root= - ,types=ALL - ,outds=work.mm_tree -)/*/STORE SOURCE*/; -options noquotelenmax; - -%if %mf_isblank(&root) %then %let root=/; - -%if %str(&types)=EXPORTABLE %then %do; - data;run;%local tempds; %let tempds=&syslast; - %mm_getpublictypes(outds=&tempds) - proc sql noprint; - select publictype into: types separated by ' ' from &tempds; - drop table &tempds; -%end; - -* use a temporary fileref to hold the response; -filename response temp; -/* get list of libraries */ -proc metadata in= - '$METAREPOSITORY - TreeSAS - 384 - - ' - out=response; -run; -/* -data _null_; - infile response; - input; - put _infile_; - run; -*/ - -/* create an XML map to read the response */ -filename sxlemap temp; -data _null_; - file sxlemap; - put ''; - put "/GetMetadataObjects/Objects/Tree"; - put ""; - put ''; - put "/GetMetadataObjects/Objects/Tree/@Id"; - put "characterstring64"; - put ''; - put "/GetMetadataObjects/Objects/Tree/@Name"; - put "characterstring256"; - put '
'; -run; -libname _XML_ xml xmlfileref=response xmlmap=sxlemap; - -data &outds; - length metauri pathuri $64 name $256 path $1024 - publictype MetadataUpdated MetadataCreated $32; - set _XML_.SASObjects; - keep metauri name publictype MetadataUpdated MetadataCreated path; - length parenturi pname $128 ; - call missing(parenturi,pname); - path=cats('/',name); - /* get parents */ - tmpuri=pathuri; - do while (metadata_getnasn(tmpuri,"ParentTree",1,parenturi)>0); - rc=metadata_getattr(parenturi,"Name",pname); - path=cats('/',pname,path); - tmpuri=parenturi; - end; - - if path=:"&root"; - - %if "&types"="ALL" or ("&types" ne "ALL" and "&types" ne "Folder") %then %do; - n=1; - do while (metadata_getnasn(pathuri,"Members",n,metauri)>0); - n+1; - call missing(name,publictype,MetadataUpdated,MetadataCreated); - rc=metadata_getattr(metauri,"Name", name); - rc=metadata_getattr(metauri,"MetadataUpdated", MetadataUpdated); - rc=metadata_getattr(metauri,"MetadataCreated", MetadataCreated); - rc=metadata_getattr(metauri,"PublicType", PublicType); - %if "&types" ne "ALL" %then %do; - if publictype in (%mf_getquotedstr(&types)) then output; - %end; - %else output; ; - end; - %end; - - rc=metadata_resolve(pathuri,pname,tmpuri); - metauri=cats('OMSOBJ:',pname,'\',pathuri); - rc=metadata_getattr(metauri,"Name", name); - rc=metadata_getattr(pathuri,"MetadataUpdated", MetadataUpdated); - rc=metadata_getattr(pathuri,"MetadataCreated", MetadataCreated); - rc=metadata_getattr(pathuri,"PublicType", PublicType); - path=substr(path,1,length(path)-length(name)-1); - if publictype ne '' then output; -run; - -proc sort; - by path; -run; - -/* clear references */ -filename sxlemap clear; -filename response clear; -libname _XML_ clear; - -%mend mm_tree; diff --git a/003_macros/mm_updateappextension.sas b/003_macros/mm_updateappextension.sas deleted file mode 100644 index 93a2392..0000000 --- a/003_macros/mm_updateappextension.sas +++ /dev/null @@ -1,134 +0,0 @@ -/*** HELP START ***//** - @file - @brief Add or update an extension to an application component - @details A SAS Application (SoftwareComponent) is a great place to store app - specific parameters. There are two main places those params can be stored: - 1) Configuration, and 2) Extensions. The second location will enable end - users to modify parameters even if they don't have the Configuration Manager - plugin in SMC. This macro can be used after creating an application with - the mm_createapplication.sas macro. If a parameter with the same name - exists, it is updated. If it does not, it is created. - - Usage: - - %mm_updateappextension(app=/my/metadata/path/myappname - ,paramname=My Param - ,paramvalue=My value - ,paramdesc=some description) - - - @param [in] app= the BIP Tree folder path plus Application Name - @param [in] paramname= Parameter name - @param [in] paramvalue= Parameter value - @param [in] paramdesc= Parameter description - - @param [in] frefin= change default inref if it clashes with an existing one - @param [out] frefout= change default outref if it clashes with an existing one - @param [in] mDebug= set to 1 to show debug messages in the log - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_updateappextension(app= - ,paramname= - ,paramvalue= - ,paramdesc=Created by mm_updateappextension - ,frefin=inmeta,frefout=outmeta - , mdebug=0); - - -/* first, check if app (and param) exists */ -%local appuri exturi; -%let appuri=stopifempty; -%let exturi=createifempty; - -data _null_; - format type uri tsuri value $200.; - call missing (of _all_); - paramname=symget('paramname'); - path="&app(Application)"; - /* first, find the STP ID */ - if metadata_pathobj("",path,"Application",type,uri)>0 then do; - /* we have an app in this location! */ - call symputx('appuri',uri,'l'); - cnt=1; - do while (metadata_getnasn(uri,"Extensions",cnt,tsuri)>0); - rc=metadata_getattr(tsuri,"Name",value); - put tsuri= value=; - if value=paramname then do; - putlog "&sysmacroname: found existing param - " tsuri; - rc=metadata_getattr(tsuri,"Id",value); - call symputx('exturi',value,'l'); - stop; - end; - cnt+1; - end; - end; - else put (_all_)(=); -run; - -%if &appuri=stopifempty %then %do; - %put %str(WARN)ING: &app.(Application) not found!; - %return; -%end; - -/* escape the description so it can be stored as XML */ -data _null_; - length outstr $32767; - outstr=symget('paramdesc'); - outstr=tranwrd(outstr,'&','&'); - outstr=tranwrd(outstr,'<','<'); - outstr=tranwrd(outstr,'>','>'); - outstr=tranwrd(outstr,"'",'''); - outstr=tranwrd(outstr,'"','"'); - outstr=tranwrd(outstr,'0A'x,' '); - outstr=tranwrd(outstr,'0D'x,' '); - outstr=tranwrd(outstr,'$','$'); - call symputx('paramdesc',outstr,'l'); -run; - -filename &frefin temp; - -%if &exturi=createifempty %then %do; - /* write header XML */ - data _null_; - file &frefin; - pname=quote(trim(symget('paramname'))); - pdesc=quote(trim(symget('paramdesc'))); - pvalue=quote(trim(symget('paramvalue'))); - put "$METAREPOSITORY"/ - " " / - ' ' / - ' '/ - 'SAS268435456'; - run; - -%end; -%else %do; - data _null_; - file &frefin; - pdesc=quote(trim(symget('paramdesc'))); - pvalue=quote(trim(symget('paramvalue'))); - put "$METAREPOSITORY"/ - " 0 then do; - /* get sourcetext */ - cnt=1; - do while (metadata_getnasn(uri,"Notes",cnt,tsuri)>0); - rc=metadata_getattr(tsuri,"Name",value); - put tsuri= value=; - if value="&name" then do; - /* found it! */ - rc=metadata_getattr(tsuri,"Id",value); - call symputx('tsuri',value,'l'); - stop; - end; - cnt+1; - end; - end; - else put (_all_)(=); -run; - -%if &tsuri=stopifempty %then %do; - %put %str(WARN)ING: &path/&name.(Document) not found!; - %return; -%end; - -%if %length(&text)<2 %then %do; - %put %str(WARN)ING: No text supplied!!; - %return; -%end; - -filename &frefin temp recfm=n; - -/* escape code so it can be stored as XML */ -/* input file may be over 32k wide, so deal with one char at a time */ -data _null_; - file &frefin recfm=n; - infile &text recfm=n; - input instr $CHAR1. ; - if _n_=1 then put "$METAREPOSITORY - ') put '>'; - when ("'") put '''; - when ('"') put '"'; - when ('0A'x) put ' '; - when ('0D'x) put ' '; - when ('$') put '$'; - otherwise put instr $CHAR1.; - end; -run; - -data _null_; - file &frefin mod; - put "'>SAS268435456 - "; -run; - - -filename &frefout temp; - -proc metadata in= &frefin - %if &mdebug=1 %then out=&frefout verbose; -; -run; - -%if &mdebug=1 %then %do; - /* write the response to the log for debugging */ - data _null_; - infile &frefout lrecl=1048576; - input; - put _infile_; - run; -%end; - -%mend mm_updatedocument; diff --git a/003_macros/mm_updatestpservertype.sas b/003_macros/mm_updatestpservertype.sas deleted file mode 100644 index 6d37399..0000000 --- a/003_macros/mm_updatestpservertype.sas +++ /dev/null @@ -1,67 +0,0 @@ -/*** HELP START ***//** - @file - @brief Updates a type 2 stored process to run on STP or WKS context - @details Only works on Type 2 (9.3 compatible) STPs - - Usage: - - %mm_updatestpservertype(target=/some/meta/path/myStoredProcess - ,type=WKS) - - - @param [in] target= full path to the STP being deleted - @param [in] type= Either WKS or STP depending on whether Workspace or - Stored Process type required - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mm_updatestpservertype( - target= - ,type= -)/*/STORE SOURCE*/; - -/** - * Check STP does exist - */ -%local cmtype; -data _null_; - length type uri $256; - rc=metadata_pathobj("","&target",'StoredProcess',type,uri); - call symputx('cmtype',type,'l'); - call symputx('stpuri',uri,'l'); -run; -%if &cmtype ne ClassifierMap %then %do; - %put %str(WARN)ING: No Stored Process found at ⌖ - %return; -%end; - -%local newtype; -%if &type=WKS %then %let newtype=Wks; -%else %let newtype=Sps; - -%local result; -%let result=NOT FOUND; -data _null_; - length uri name value $256; - n=1; - do while(metadata_getnasn("&stpuri","Notes",n,uri)>0); - n+1; - rc=metadata_getattr(uri,"Name",name); - if name='Stored Process' then do; - rc = METADATA_SETATTR(uri,'StoredText' - ,'' - !!''); - if rc=0 then call symputx('result','SUCCESS'); - stop; - end; - end; -run; -%if &result=SUCCESS %then %put NOTE: SUCCESS: STP &target changed to &type type; -%else %put %str(ERR)OR: Issue with &sysmacroname; - -%mend mm_updatestpservertype; diff --git a/003_macros/mm_updatestpsourcecode.sas b/003_macros/mm_updatestpsourcecode.sas deleted file mode 100644 index e6ec26c..0000000 --- a/003_macros/mm_updatestpsourcecode.sas +++ /dev/null @@ -1,133 +0,0 @@ -/*** HELP START ***//** - @file - @brief Update the source code of a type 2 STP - @details Uploads the contents of a text file or fileref to an existing type 2 - STP. A type 2 STP has its source code saved in metadata. - - Usage: - - %mm_updatestpsourcecode(stp=/my/metadata/path/mystpname - ,stpcode="/file/system/source.sas") - - @param [in] stp= the BIP Tree folder path plus Stored Process Name - @param [in] stpcode= () The source file (or fileref) containing the SAS - code to load into the stp. - For multiple files, they should simply be concatenated first. - @param [in] minify= (NO) Set to YES in order to strip comments, blank lines, - and CRLFs. - @param [in] mDebug= set to 1 to show debug messages in the log - - @version 9.3 - @author Allan Bowe - -

SAS Macros

- @li mf_getuniquefileref.sas - -**//*** HELP END ***/ - -%macro mm_updatestpsourcecode(stp= - ,stpcode= - ,minify=NO - ,mdebug=0 -); - -/* first, check if STP exists */ -%local tsuri; -%let tsuri=stopifempty ; - -data _null_; - format type uri tsuri value $200.; - call missing (of _all_); - path="&stp.(StoredProcess)"; - /* first, find the STP ID */ - if metadata_pathobj("",path,"StoredProcess",type,uri)>0 then do; - /* get sourcecode */ - cnt=1; - do while (metadata_getnasn(uri,"Notes",cnt,tsuri)>0); - rc=metadata_getattr(tsuri,"Name",value); - %if &mdebug=1 %then %do; - put tsuri= value=; - %end; - if value="SourceCode" then do; - /* found it! */ - rc=metadata_getattr(tsuri,"Id",value); - call symputx('tsuri',value,'l'); - stop; - end; - cnt+1; - end; - end; - else put (_all_)(=); -run; - -%if &tsuri=stopifempty %then %do; - %put %str(WARN)ING: &stp.(StoredProcess) not found!; - %return; -%end; - -%if %length(&stpcode)<2 %then %do; - %put %str(WARN)ING: No SAS code supplied!!; - %return; -%end; - -%local frefin frefout; -%let frefin=%mf_getuniquefileref(); -%let frefout=%mf_getuniquefileref(); - -/* write header XML */ -data _null_; - file &frefin; - put "$METAREPOSITORY - ','>'); - outstr=tranwrd(outstr,"'",'''); - outstr=tranwrd(outstr,'"','"'); - outstr=tranwrd(outstr,'0A'x,' '); - outstr=tranwrd(outstr,'0D'x,' '); - outstr=tranwrd(outstr,'$','$'); - %if &minify=YES %then %do; - outstr=cats(outstr); - if outstr ne ''; - if not (outstr=:'/*' and subpad(left(reverse(outstr)),1,2)='/*'); - %end; - outstr=trim(outstr); - put outstr ' '; - run; -%end; - -data _null_; - file &frefin mod; - put "'>SAS268435456 - "; -run; - -proc metadata in= &frefin out=&frefout; -run; - -%if &mdebug=1 %then %do; - /* write the response to the log for debugging */ - data _null_; - infile &frefout lrecl=32767; - input; - put _infile_; - run; -%end; -%else %do; - filename &frefin clear; - filename &frefout clear; -%end; - -%mend mm_updatestpsourcecode; diff --git a/003_macros/mm_webout.sas b/003_macros/mm_webout.sas deleted file mode 100644 index cb00533..0000000 --- a/003_macros/mm_webout.sas +++ /dev/null @@ -1,251 +0,0 @@ -/*** HELP START ***//** - @file mm_webout.sas - @brief Send data to/from SAS Stored Processes - @details This macro should be added to the start of each Stored Process, - **immediately** followed by a call to: - - %mm_webout(FETCH) - - This will read all the input data and create same-named SAS datasets in the - WORK library. You can then insert your code, and send data back using the - following syntax: - - data some datasets; * make some data ; - retain some columns; - run; - - %mm_webout(OPEN) - %mm_webout(ARR,some) * Array format, fast, suitable for large tables ; - %mm_webout(OBJ,datasets) * Object format, easier to work with ; - - Finally, wrap everything up send some helpful system variables too - - %mm_webout(CLOSE) - - - @param [in] action Either FETCH, OPEN, ARR, OBJ or CLOSE - @param [in] ds The dataset to send back to the frontend - @param [out] dslabel= Value to use instead of table name for sending to JSON - @param [in] fmt= (N) Setting Y converts all vars to their formatted values - @param [out] fref= (_webout) The fileref to which to write the JSON - @param [in] missing= (NULL) Special numeric missing values can be sent as NULL - (eg `null`) or as STRING values (eg `".a"` or `".b"`) - @param [in] showmeta= (N) Set to Y to output metadata alongside each table, - such as the column formats and types. The metadata is contained inside an - object with the same name as the table but prefixed with a dollar sign - ie, - `,"$tablename":{"formats":{"col1":"$CHAR1"},"types":{"COL1":"C"}}` - @param [in] workobs= (0) When set to a positive integer, will create a new - output object (WORK) which contains this number of observations from all - tables in the WORK library. - @param [in] maxobs= (MAX) Provide an integer to limit the number of input rows - that should be converted to output JSON - -

SAS Macros

- @li mp_jsonout.sas - -

Related Macros

- @li ms_webout.sas - @li mv_webout.sas - - @version 9.3 - @author Allan Bowe - -**//*** HELP END ***/ -%macro mm_webout(action,ds,dslabel=,fref=_webout,fmt=N,missing=NULL - ,showmeta=N,maxobs=MAX,workobs=0 -); -%global _webin_file_count _webin_fileref1 _webin_name1 _program _debug - sasjs_tables; -%local i tempds jsonengine; - -/* see https://github.com/sasjs/core/issues/41 */ -%if "%upcase(&SYSENCODING)" ne "UTF-8" %then %let jsonengine=PROCJSON; -%else %let jsonengine=DATASTEP; - - -%if &action=FETCH %then %do; - %if %str(&_debug) ge 131 %then %do; - options mprint notes mprintnest; - %end; - %let _webin_file_count=%eval(&_webin_file_count+0); - /* now read in the data */ - %do i=1 %to &_webin_file_count; - %if &_webin_file_count=1 %then %do; - %let _webin_fileref1=&_webin_fileref; - %let _webin_name1=&_webin_name; - %end; - data _null_; - infile &&_webin_fileref&i termstr=crlf; - input; - call symputx('input_statement',_infile_); - putlog "&&_webin_name&i input statement: " _infile_; - stop; - data &&_webin_name&i; - infile &&_webin_fileref&i firstobs=2 dsd termstr=crlf encoding='utf-8'; - input &input_statement; - %if %str(&_debug) ge 131 %then %do; - if _n_<20 then putlog _infile_; - %end; - run; - %let sasjs_tables=&sasjs_tables &&_webin_name&i; - %end; -%end; - -%else %if &action=OPEN %then %do; - /* fix encoding */ - OPTIONS NOBOMFILE; - - /** - * check xengine type to avoid the below err message: - * > Function is only valid for filerefs using the CACHE access method. - */ - data _null_; - set sashelp.vextfl(where=(fileref="_WEBOUT")); - if xengine='STREAM' then do; - rc=stpsrv_header('Content-type',"text/html; encoding=utf-8"); - end; - run; - - /* setup json */ - data _null_;file &fref encoding='utf-8'; - %if %str(&_debug) ge 131 %then %do; - put '>>weboutBEGIN<<'; - %end; - put '{"SYSDATE" : "' "&SYSDATE" '"'; - put ',"SYSTIME" : "' "&SYSTIME" '"'; - run; - -%end; - -%else %if &action=ARR or &action=OBJ %then %do; - %mp_jsonout(&action,&ds,dslabel=&dslabel,fmt=&fmt,jref=&fref - ,engine=&jsonengine,missing=&missing,showmeta=&showmeta,maxobs=&maxobs - ) -%end; -%else %if &action=CLOSE %then %do; - /* To avoid issues with _webout on EBI we use a temporary file */ - filename _sjsref temp lrecl=131068; - %if %str(&workobs) > 0 %then %do; - /* if debug mode, send back first XX records of each work table also */ - data;run;%let tempds=%scan(&syslast,2,.); - ods output Members=&tempds; - proc datasets library=WORK memtype=data; - %local wtcnt;%let wtcnt=0; - data _null_; - set &tempds; - if not (upcase(name) =:"DATA"); /* ignore temp datasets */ - i+1; - call symputx(cats('wt',i),name,'l'); - call symputx('wtcnt',i,'l'); - data _null_; file _sjsref mod encoding='utf-8'; - put ",""WORK"":{"; - %do i=1 %to &wtcnt; - %let wt=&&wt&i; - data _null_; file _sjsref mod encoding='utf-8'; - dsid=open("WORK.&wt",'is'); - nlobs=attrn(dsid,'NLOBS'); - nvars=attrn(dsid,'NVARS'); - rc=close(dsid); - if &i>1 then put ','@; - put " ""&wt"" : {"; - put '"nlobs":' nlobs; - put ',"nvars":' nvars; - %mp_jsonout(OBJ,&wt,jref=_sjsref,dslabel=first10rows,showmeta=Y - ,maxobs=&workobs - ) - data _null_; file _sjsref mod encoding='utf-8'; - put "}"; - %end; - data _null_; file _sjsref mod encoding='utf-8'; - put "}"; - run; - %end; - /* close off json */ - data _null_;file _sjsref mod encoding='utf-8'; - length SYSPROCESSNAME syserrortext syswarningtext autoexec $512; - put ",""_DEBUG"" : ""&_debug"" "; - _METAUSER=quote(trim(symget('_METAUSER'))); - put ",""_METAUSER"": " _METAUSER; - _METAPERSON=quote(trim(symget('_METAPERSON'))); - put ',"_METAPERSON": ' _METAPERSON; - _PROGRAM=quote(trim(resolve(symget('_PROGRAM')))); - put ',"_PROGRAM" : ' _PROGRAM ; - autoexec=quote(urlencode(trim(getoption('autoexec')))); - put ',"AUTOEXEC" : ' autoexec; - put ",""MF_GETUSER"" : ""%mf_getuser()"" "; - put ",""SYSCC"" : ""&syscc"" "; - put ",""SYSENCODING"" : ""&sysencoding"" "; - syserrortext=cats(symget('syserrortext')); - if findc(syserrortext,'"\'!!'0A0D09000E0F010210111A'x) then do; - syserrortext='"'!!trim( - prxchange('s/"/\\"/',-1, /* double quote */ - prxchange('s/\x0A/\n/',-1, /* new line */ - prxchange('s/\x0D/\r/',-1, /* carriage return */ - prxchange('s/\x09/\\t/',-1, /* tab */ - prxchange('s/\x00/\\u0000/',-1, /* NUL */ - prxchange('s/\x0E/\\u000E/',-1, /* SS */ - prxchange('s/\x0F/\\u000F/',-1, /* SF */ - prxchange('s/\x01/\\u0001/',-1, /* SOH */ - prxchange('s/\x02/\\u0002/',-1, /* STX */ - prxchange('s/\x10/\\u0010/',-1, /* DLE */ - prxchange('s/\x11/\\u0011/',-1, /* DC1 */ - prxchange('s/\x1A/\\u001A/',-1, /* SUB */ - prxchange('s/\\/\\\\/',-1,syserrortext) - )))))))))))))!!'"'; - end; - else syserrortext=cats('"',syserrortext,'"'); - put ',"SYSERRORTEXT" : ' syserrortext; - put ",""SYSHOSTNAME"" : ""&syshostname"" "; - put ",""SYSPROCESSID"" : ""&SYSPROCESSID"" "; - put ",""SYSPROCESSMODE"" : ""&SYSPROCESSMODE"" "; - SYSPROCESSNAME=quote(urlencode(cats(SYSPROCESSNAME))); - put ",""SYSPROCESSNAME"" : " SYSPROCESSNAME; - put ",""SYSJOBID"" : ""&sysjobid"" "; - put ",""SYSSCPL"" : ""&sysscpl"" "; - put ",""SYSSITE"" : ""&syssite"" "; - put ",""SYSUSERID"" : ""&sysuserid"" "; - sysvlong=quote(trim(symget('sysvlong'))); - put ',"SYSVLONG" : ' sysvlong; - syswarningtext=cats(symget('syswarningtext')); - if findc(syswarningtext,'"\'!!'0A0D09000E0F010210111A'x) then do; - syswarningtext='"'!!trim( - prxchange('s/"/\\"/',-1, /* double quote */ - prxchange('s/\x0A/\n/',-1, /* new line */ - prxchange('s/\x0D/\r/',-1, /* carriage return */ - prxchange('s/\x09/\\t/',-1, /* tab */ - prxchange('s/\x00/\\u0000/',-1, /* NUL */ - prxchange('s/\x0E/\\u000E/',-1, /* SS */ - prxchange('s/\x0F/\\u000F/',-1, /* SF */ - prxchange('s/\x01/\\u0001/',-1, /* SOH */ - prxchange('s/\x02/\\u0002/',-1, /* STX */ - prxchange('s/\x10/\\u0010/',-1, /* DLE */ - prxchange('s/\x11/\\u0011/',-1, /* DC1 */ - prxchange('s/\x1A/\\u001A/',-1, /* SUB */ - prxchange('s/\\/\\\\/',-1,syswarningtext) - )))))))))))))!!'"'; - end; - else syswarningtext=cats('"',syswarningtext,'"'); - put ',"SYSWARNINGTEXT" : ' syswarningtext; - put ',"END_DTTM" : "' "%sysfunc(datetime(),E8601DT26.6)" '" '; - length memsize $32; - memsize="%sysfunc(INPUTN(%sysfunc(getoption(memsize)), best.),sizekmg.)"; - memsize=quote(cats(memsize)); - put ',"MEMSIZE" : ' memsize; - put "}" @; - %if %str(&_debug) ge 131 %then %do; - put '>>weboutEND<<'; - %end; - run; - /* now write to _webout 1 char at a time */ - data _null_; - infile _sjsref lrecl=1 recfm=n; - file &fref mod lrecl=1 recfm=n; - input sourcechar $char1. @@; - format sourcechar hex2.; - put sourcechar char1. @@; - run; - filename _sjsref clear; - -%end; - -%mend mm_webout; diff --git a/003_macros/mv_createfile.sas b/003_macros/mv_createfile.sas deleted file mode 100644 index 5d52eb8..0000000 --- a/003_macros/mv_createfile.sas +++ /dev/null @@ -1,129 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a file in SAS Drive - @details Creates a file in SAS Drive and adds the appropriate content type. - If the parent folder does not exist, it is created. - - Usage: - - filename myfile temp; - data _null_; - file myfile; - put 'something'; - run; - %mv_createfile(path=/Public/temp,name=newfile.txt,inref=myfile) - - - @param [in] path= The parent folder in which to create the file - @param [in] name= The name of the file to be created - @param [in] inref= The fileref pointing to the file to be uploaded - @param [in] intype= (BINARY) The type of the input data. Valid values: - @li BINARY File is copied byte for byte using the mp_binarycopy.sas macro. - @li BASE64 File will be first decoded using the mp_base64.sas macro, then - loaded byte by byte to SAS Drive. - @param [in] contentdisp= (inline) Content Disposition. Example values: - @li inline - @li attachment - @param [in] ctype= (0) Set a default HTTP Content-Type header to be returned - with the file when the content is retrieved from the Files service. - @param [in] access_token_var= The global macro variable to contain the access - token, if using authorization_code grant type. - @param [in] grant_type= (sas_services) Valid values are: - @li password - @li authorization_code - @li sas_services - - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - - @version VIYA V.03.05 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mf_isblank.sas - @li mp_abort.sas - @li mp_base64copy.sas - @li mp_binarycopy.sas - @li mv_createfolder.sas - -**//*** HELP END ***/ - -%macro mv_createfile(path= - ,name= - ,inref= - ,intype=BINARY - ,contentdisp=inline - ,ctype=0 - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ,mdebug=0 - ); -%local dbg; -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; -%end; -%else %let dbg=*; - -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; - -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) - -%mp_abort(iftrue=(%mf_isblank(&path)=1 or %length(&path)=1) - ,mac=&sysmacroname - ,msg=%str(path value must be provided) -) -%mp_abort(iftrue=(%mf_isblank(&name)=1 or %length(&name)=1) - ,mac=&sysmacroname - ,msg=%str(name value with length >1 must be provided) -) - -/* create folder if it does not already exist */ -%mv_createfolder(path=&path - ,access_token_var=&access_token_var - ,grant_type=&grant_type - ,mdebug=&mdebug -) - -/* create file with relevant options */ -%local fref; -%let fref=%mf_getuniquefileref(); -filename &fref filesrvc - folderPath="&path" - filename="&name" - cdisp="&contentdisp" -%if "&ctype" ne "0" %then %do; - ctype="&ctype" -%end; - lrecl=1048544; -%if &intype=BINARY %then %do; - %mp_binarycopy(inref=&inref, outref=&fref) -%end; -%else %if &intype=BASE64 %then %do; - %mp_base64copy(inref=&inref, outref=&fref, action=DECODE) -%end; - -filename &fref clear; - -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -%put &sysmacroname: File &name successfully created in &path; -%put &sysmacroname:;%put; -%put &base_uri/SASJobExecution?_file=&path/&name;%put; -%put &sysmacroname:; - -%mend mv_createfile; diff --git a/003_macros/mv_createfolder.sas b/003_macros/mv_createfolder.sas deleted file mode 100644 index 7244534..0000000 --- a/003_macros/mv_createfolder.sas +++ /dev/null @@ -1,184 +0,0 @@ -/*** HELP START ***//** - @file mv_createfolder.sas - @brief Creates a viya folder if that folder does not already exist - @details Creates a viya folder by checking if each parent folder exists, and - recursively creating children if needed. - Usage: - - %mv_createfolder(path=/Public) - - - @param [in] path= The full path of the folder to be created - @param [in] access_token_var= The global macro variable to contain the access - token, if using authorization_code grant type. - @param [in] grant_type= (sas_services) Valid values are: - @li password - @li authorization_code - @li sas_services - - @param [in] mdebug=(0) set to 1 to enable DEBUG messages - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - @li mf_isblank.sas - @li mf_getplatform.sas - @li mfv_existfolder.sas - - -**//*** HELP END ***/ - -%macro mv_createfolder(path= - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ,mdebug=0 - ); -%local dbg; -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; -%end; -%else %let dbg=*; - -%if %mfv_existfolder(&path)=1 %then %do; - %put &sysmacroname: &path already exists; - %return; -%end; - -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; - -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) - -%mp_abort(iftrue=(%mf_isblank(&path)=1) - ,mac=&sysmacroname - ,msg=%str(path value must be provided) -) -%mp_abort(iftrue=(%length(&path)=1) - ,mac=&sysmacroname - ,msg=%str(path value must be provided) -) - -options noquotelenmax; - -%local subfolder_cnt; /* determine the number of subfolders */ -%let subfolder_cnt=%sysfunc(countw(&path,/)); - -%local href; /* resource address (none for root) */ -%let href="/folders/folders?parentFolderUri=/folders/folders/none"; - -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -%local x newpath subfolder; -%do x=1 %to &subfolder_cnt; - %let subfolder=%scan(&path,&x,%str(/)); - %let newpath=&newpath/&subfolder; - - %local fname1; - %let fname1=%mf_getuniquefileref(); - - %put &sysmacroname checking to see if &newpath exists; - proc http method='GET' out=&fname1 &oauth_bearer - url="&base_uri/folders/folders/@item?path=&newpath"; - %if &grant_type=authorization_code %then %do; - headers "Authorization"="Bearer &&&access_token_var"; - %end; - run; - %local libref1; - %let libref1=%mf_getuniquelibref(); - libname &libref1 JSON fileref=&fname1; - %mp_abort( - iftrue=( - &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 404 - ) - ,mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) - ) - %if &mdebug=1 %then %do; - %put &sysmacroname following check to see if &newpath exists:; - %put _local_; - data _null_; - set &fname1; - input; - putlog _infile_; - run; - %end; - %if &SYS_PROCHTTP_STATUS_CODE=200 %then %do; - %*put &sysmacroname &newpath exists so grab the follow on link ; - data _null_; - set &libref1..links; - if rel='createChild' then - call symputx('href',quote(cats("&base_uri",href)),'l'); - run; - %end; - %else %if &SYS_PROCHTTP_STATUS_CODE=404 %then %do; - %put &sysmacroname &newpath not found - creating it now; - %local fname2; - %let fname2=%mf_getuniquefileref(); - data _null_; - length json $1000; - json=cats("'" - ,'{"name":' - ,quote(trim(symget('subfolder'))) - ,',"description":' - ,quote("&subfolder, created by &sysmacroname") - ,',"type":"folder"}' - ,"'" - ); - call symputx('json',json,'l'); - run; - - proc http method='POST' - in=&json - out=&fname2 - &oauth_bearer - url=%unquote(%superq(href)); - headers - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; - 'Content-Type'='application/vnd.sas.content.folder+json' - 'Accept'='application/vnd.sas.content.folder+json'; - run; - %put &=SYS_PROCHTTP_STATUS_CODE; - %put &=SYS_PROCHTTP_STATUS_PHRASE; - %mp_abort(iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 201) - ,mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) - ) - %local libref2; - %let libref2=%mf_getuniquelibref(); - libname &libref2 JSON fileref=&fname2; - %put &sysmacroname &newpath now created. Grabbing the follow on link ; - data _null_; - set &libref2..links; - if rel='createChild' then do; - call symputx('href',quote(cats("&base_uri",href)),'l'); - &dbg put (_all_)(=); - end; - run; - - libname &libref2 clear; - filename &fname2 clear; - %end; - filename &fname1 clear; - libname &libref1 clear; -%end; -%mend mv_createfolder; diff --git a/003_macros/mv_createjob.sas b/003_macros/mv_createjob.sas deleted file mode 100644 index 503acde..0000000 --- a/003_macros/mv_createjob.sas +++ /dev/null @@ -1,355 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a Viya Job - @details - Code is passed in as one or more filerefs. - - %* Step 1 - compile macros ; - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - %* Step 2 - Create some SAS code and add it to a job; - filename ft15f001 temp; - parmcards4; - data some_code; - set sashelp.class; - run; - ;;;; - %mv_createjob(path=/Public/app/sasjstemp/jobs/myjobs,name=myjob) - - The path to the job will then be shown in the log, eg as follows: - - ![viya job location](https://i.imgur.com/XRUDHgA.png) - - -

SAS Macros

- @li mp_abort.sas - @li mv_createfolder.sas - @li mf_getuniquelibref.sas - @li mf_getuniquefileref.sas - @li mf_getplatform.sas - @li mf_isblank.sas - @li mv_deletejes.sas - - @param [in] path= The full path (on SAS Drive) where the job will be created - @param [in] name= The name of the job - @param [in] desc= (Created by the mv_createjob.sas macro) The job description - @param [in] precode= () - Space separated list of filerefs, pointing to the code that - needs to be attached to the beginning of the job - @param [in] code= (ft15f001) Fileref(s) of the actual code to be added - @param [in] access_token_var= (ACCESS_TOKEN) - Global macro variable containing the access token - @param [in] grant_type= (sas_services) Valid values: - @li sas_services - @li detect - @li authorization_code - @li password - @param [in] replace= (YES) select NO to avoid replacing any existing job - @param [in] addjesbeginendmacros= (false) - Relates to the `_addjesbeginendmacros` setting. - Normally this would always be false however due to a Viya bug - (https://github.com/sasjs/cli/issues/1229) this is now configurable. Valid - values: - @li true - @li false - @li 0 - this will prevent the flag from being set (job will default to true) - @param [in] contextname= () Choose a specific context on which to run the Job. - Leave blank to use the default context. - From Viya 3.5 it is possible to configure a shared context - see -https://go.documentation.sas.com/?docsetId=calcontexts&docsetTarget=n1hjn8eobk5pyhn1wg3ja0drdl6h.htm&docsetVersion=3.5&locale=en - - @version VIYA V.03.04 - @author [Allan Bowe](https://www.linkedin.com/in/allanbowe) - -**//*** HELP END ***/ - -%macro mv_createjob(path= - ,name= - ,desc=Created by the mv_createjob.sas macro - ,precode= - ,code=ft15f001 - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ,replace=YES - ,debug=0 - ,contextname= - ,addjesbeginendmacros=false - ); -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; - -/* initial validation checking */ -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) -%mp_abort(iftrue=(%mf_isblank(&path)=1) - ,mac=&sysmacroname - ,msg=%str(path value must be provided) -) -%mp_abort(iftrue=(%length(&path)=1) - ,mac=&sysmacroname - ,msg=%str(path value must be provided) -) -%mp_abort(iftrue=(%mf_isblank(&name)=1) - ,mac=&sysmacroname - ,msg=%str(name value must be provided) -) - -options noquotelenmax; - -* remove any trailing slash ; -%if "%substr(&path,%length(&path),1)" = "/" %then - %let path=%substr(&path,1,%length(&path)-1); - -/* ensure folder exists */ -%put &sysmacroname: Path &path being checked / created; -%mv_createfolder(path=&path) - -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -/* fetching folder details for provided path */ -%local fname1; -%let fname1=%mf_getuniquefileref(); -proc http method='GET' out=&fname1 &oauth_bearer - url="&base_uri/folders/folders/@item?path=&path"; -%if &grant_type=authorization_code %then %do; - headers "Authorization"="Bearer &&&access_token_var"; -%end; -run; -%if &debug %then %do; - data _null_; - infile &fname1; - input; - putlog _infile_; - run; -%end; -%mp_abort(iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200) - ,mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) -) - -/* path exists. Grab follow on link to check members */ -%local libref1; -%let libref1=%mf_getuniquelibref(); -libname &libref1 JSON fileref=&fname1; - -data _null_; - set &libref1..links; - if rel='members' then call symputx('membercheck',quote("&base_uri"!!trim(href)),'l'); - else if rel='self' then call symputx('parentFolderUri',href,'l'); -run; -data _null_; - set &libref1..root; - call symputx('folderid',id,'l'); -run; -%local fname2; -%let fname2=%mf_getuniquefileref(); -proc http method='GET' - out=&fname2 - &oauth_bearer - url=%unquote(%superq(membercheck)); - headers - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; - 'Accept'='application/vnd.sas.collection+json' - 'Accept-Language'='string'; -%if &debug=1 %then %do; - debug level = 3; -%end; -run; -/*data _null_;infile &fname2;input;putlog _infile_;run;*/ -%mp_abort(iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200) - ,mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) -) - -%if %upcase(&replace)=YES %then %do; - %mv_deletejes(path=&path, name=&name) -%end; -%else %do; - /* check that job does not already exist in that folder */ - %local libref2; - %let libref2=%mf_getuniquelibref(); - libname &libref2 JSON fileref=&fname2; - %local exists; %let exists=0; - data _null_; - set &libref2..items; - if contenttype='jobDefinition' and upcase(name)="%upcase(&name)" then - call symputx('exists',1,'l'); - run; - %mp_abort(iftrue=(&exists=1) - ,mac=&sysmacroname - ,msg=%str(Job &name already exists in &path) - ) - libname &libref2 clear; -%end; - -/* set up the body of the request to create the service */ -%local fname3 comma; -%let fname3=%mf_getuniquefileref(); -data _null_; - file &fname3 TERMSTR=' '; - length string $32767; - string=cats('{"version": 0,"name":"' - ,"&name" - ,'","type":"Compute","parameters":[' -%if &addjesbeginendmacros ne 0 %then %do; - ,'{"name":"_addjesbeginendmacros"' - ,',"type":"CHARACTER","defaultValue":"' - ,"&addjesbeginendmacros" - ,'"}' - %let comma=%str(,); -%end; - ); - context=quote(cats(symget('contextname'))); - if context ne '""' then do; - string=cats(string - ,"&comma" - ,'{"version": 1,"name": "_contextName","defaultValue":' - ,context,',"type":"CHARACTER","label":"Context Name","required": false}' - ); - end; - string=cats(string,'],"code":"'); - put string; -run; - - -/* insert the code, escaping double quotes and carriage returns */ -%local x fref freflist; -%let freflist= &precode &code ; -%do x=1 %to %sysfunc(countw(&freflist)); - %let fref=%scan(&freflist,&x); - %put &sysmacroname: adding &fref; - data _null_; - length filein 8 fileid 8; - filein = fopen("&fref","I",1,"B"); - fileid = fopen("&fname3","A",1,"B"); - rec = "20"x; - do while(fread(filein)=0); - rc = fget(filein,rec,1); - if rec='"' then do; /* DOUBLE QUOTE */ - rc =fput(fileid,'\');rc =fwrite(fileid); - rc =fput(fileid,'"');rc =fwrite(fileid); - end; - else if rec='0A'x then do; /* LF */ - rc =fput(fileid,'\');rc =fwrite(fileid); - rc =fput(fileid,'n');rc =fwrite(fileid); - end; - else if rec='0D'x then do; /* CR */ - rc =fput(fileid,'\');rc =fwrite(fileid); - rc =fput(fileid,'r');rc =fwrite(fileid); - end; - else if rec='09'x then do; /* TAB */ - rc =fput(fileid,'\');rc =fwrite(fileid); - rc =fput(fileid,'t');rc =fwrite(fileid); - end; - else if rec='5C'x then do; /* BACKSLASH */ - rc =fput(fileid,'\');rc =fwrite(fileid); - rc =fput(fileid,'\');rc =fwrite(fileid); - end; - else if rec='01'x then do; /* Unprintable */ - rc =fput(fileid,'\');rc =fwrite(fileid); - rc =fput(fileid,'u');rc =fwrite(fileid); - rc =fput(fileid,'0');rc =fwrite(fileid); - rc =fput(fileid,'0');rc =fwrite(fileid); - rc =fput(fileid,'0');rc =fwrite(fileid); - rc =fput(fileid,'1');rc =fwrite(fileid); - end; - else if rec='07'x then do; /* Bell Char */ - rc =fput(fileid,'\');rc =fwrite(fileid); - rc =fput(fileid,'u');rc =fwrite(fileid); - rc =fput(fileid,'0');rc =fwrite(fileid); - rc =fput(fileid,'0');rc =fwrite(fileid); - rc =fput(fileid,'0');rc =fwrite(fileid); - rc =fput(fileid,'7');rc =fwrite(fileid); - end; - else if rec='1B'x then do; /* escape char */ - rc =fput(fileid,'\');rc =fwrite(fileid); - rc =fput(fileid,'u');rc =fwrite(fileid); - rc =fput(fileid,'0');rc =fwrite(fileid); - rc =fput(fileid,'0');rc =fwrite(fileid); - rc =fput(fileid,'1');rc =fwrite(fileid); - rc =fput(fileid,'B');rc =fwrite(fileid); - end; - else do; - rc =fput(fileid,rec); - rc =fwrite(fileid); - end; - end; - rc=fclose(filein); - rc=fclose(fileid); - run; -%end; - -/* finish off the body of the code file loaded to JES */ -data _null_; - file &fname3 mod TERMSTR=' '; - put '"}'; -run; - -/* now we can create the job!! */ -%local fname4; -%let fname4=%mf_getuniquefileref(); -proc http method='POST' - in=&fname3 - out=&fname4 - &oauth_bearer - url="&base_uri/jobDefinitions/definitions?parentFolderUri=&parentFolderUri"; - headers 'Content-Type'='application/vnd.sas.job.definition+json' - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; - "Accept"="application/vnd.sas.job.definition+json"; -%if &debug=1 %then %do; - debug level = 3; -%end; -run; -/*data _null_;infile &fname4;input;putlog _infile_;run;*/ -%mp_abort(iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 201) - ,mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) -) -/* clear refs */ -filename &fname1 clear; -filename &fname2 clear; -filename &fname3 clear; -filename &fname4 clear; -libname &libref1 clear; - -/* get the url so we can give a helpful log message */ -%local url; -data _null_; - if symexist('_baseurl') then do; - url=symget('_baseurl'); - if subpad(url,length(url)-9,9)='SASStudio' - then url=substr(url,1,length(url)-11); - else url="&systcpiphostname"; - end; - else url="&systcpiphostname"; - call symputx('url',url); -run; - - -%put &sysmacroname: Job &name successfully created in &path; -%put &sysmacroname:; -%put &sysmacroname: Check it out here:; -%put &sysmacroname:;%put; -%put &url/SASJobExecution?_PROGRAM=&path/&name;%put; -%put &sysmacroname:; -%put &sysmacroname:; - -%mend mv_createjob; diff --git a/003_macros/mv_createwebservice.sas b/003_macros/mv_createwebservice.sas deleted file mode 100644 index 1eaf54b..0000000 --- a/003_macros/mv_createwebservice.sas +++ /dev/null @@ -1,966 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a JobExecution web service if it doesn't already exist - @details - Code is passed in as one or more filerefs. - - %* Step 1 - compile macros ; - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - %* Step 2 - Create some code and add it to a web service; - filename ft15f001 temp; - parmcards4; - %webout(FETCH) %* fetch any tables sent from frontend; - %* do some sas, any inputs are now already WORK tables; - data example1 example2; - set sashelp.class; - run; - %* send data back; - %webout(OPEN) - %webout(ARR,example1) * Array format, fast, suitable for large tables; - %webout(OBJ,example2) * Object format, easier to work with ; - %webout(CLOSE) - ;;;; - %mv_createwebservice(path=/Public/app/common,name=appinit) - - - Notes: - To minimise postgres requests, output json is stored in a temporary file - and then sent to _webout in one go at the end. - -

SAS Macros

- @li mp_abort.sas - @li mv_createfolder.sas - @li mf_getuniquelibref.sas - @li mf_getuniquefileref.sas - @li mf_getplatform.sas - @li mf_isblank.sas - @li mv_deletejes.sas - - @param [in] path= The full path (on SAS Drive) where the service will be - created - @param [in] name= The name of the service - @param [in] desc= The description of the service - @param [in] precode= Space separated list of filerefs, pointing to the code - that needs to be attached to the beginning of the service - @param [in] code= Fileref(s) of the actual code to be added - @param [in] access_token_var= The global macro variable to contain the access - token - @param [in] grant_type= valid values are "password" or "authorization_code" - (unquoted). The default is authorization_code. - @param [in] replace=(YES) Select NO to avoid replacing any existing service in - that location - @param [in] adapter= the macro uses the sasjs adapter by default. To use - another adapter, add a (different) fileref here. - @param [in] contextname= Choose a specific context on which to run the Job. - Leave blank to use the default context. From Viya 3.5 it is possible to - configure a shared context - see -https://go.documentation.sas.com/?docsetId=calcontexts&docsetTarget=n1hjn8eobk5pyhn1wg3ja0drdl6h.htm&docsetVersion=3.5&locale=en - @param [in] mdebug=(0) set to 1 to enable DEBUG messages - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -**//*** HELP END ***/ - -%macro mv_createwebservice(path= - ,name= - ,desc=Created by the mv_createwebservice.sas macro - ,precode= - ,code=ft15f001 - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ,replace=YES - ,adapter=sasjs - ,mdebug=0 - ,contextname= - ,debug=0 /* @TODO - Deprecate */ - ); -%local dbg; -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; -%end; -%else %let dbg=*; - -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; - -/* initial validation checking */ -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) -%mp_abort(iftrue=(%mf_isblank(&path)=1) - ,mac=&sysmacroname - ,msg=%str(path value must be provided) -) -%mp_abort(iftrue=(%length(&path)=1) - ,mac=&sysmacroname - ,msg=%str(path value must be provided) -) -%mp_abort(iftrue=(%mf_isblank(&name)=1) - ,mac=&sysmacroname - ,msg=%str(name value must be provided) -) - -options noquotelenmax; - -* remove any trailing slash ; -%if "%substr(&path,%length(&path),1)" = "/" %then - %let path=%substr(&path,1,%length(&path)-1); - -/* ensure folder exists */ -%put &sysmacroname: Path &path being checked / created; -%mv_createfolder(path=&path) - -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -/* fetching folder details for provided path */ -%local fname1; -%let fname1=%mf_getuniquefileref(); -proc http method='GET' out=&fname1 &oauth_bearer - url="&base_uri/folders/folders/@item?path=&path"; -%if &grant_type=authorization_code %then %do; - headers "Authorization"="Bearer &&&access_token_var"; -%end; -run; -%if &mdebug=1 %then %do; - data _null_; - infile &fname1; - input; - putlog _infile_; - run; -%end; -%mp_abort(iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200) - ,mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) -) - -/* path exists. Grab follow on link to check members */ -%local libref1; -%let libref1=%mf_getuniquelibref(); -libname &libref1 JSON fileref=&fname1; - -data _null_; - set &libref1..links; - if rel='members' then - call symputx('membercheck',quote("&base_uri"!!trim(href)),'l'); - else if rel='self' then call symputx('parentFolderUri',href,'l'); -run; -data _null_; - set &libref1..root; - call symputx('folderid',id,'l'); -run; -%local fname2; -%let fname2=%mf_getuniquefileref(); -proc http method='GET' - out=&fname2 - &oauth_bearer - url=%unquote(%superq(membercheck)); - headers - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; - 'Accept'='application/vnd.sas.collection+json' - 'Accept-Language'='string'; -%if &mdebug=1 %then %do; - debug level = 3; -%end; -run; -/*data _null_;infile &fname2;input;putlog _infile_;run;*/ -%mp_abort(iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200) - ,mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) -) - -%if %upcase(&replace)=YES %then %do; - %mv_deletejes(path=&path, name=&name) -%end; -%else %do; - /* check that job does not already exist in that folder */ - %local libref2; - %let libref2=%mf_getuniquelibref(); - libname &libref2 JSON fileref=&fname2; - %local exists; %let exists=0; - data _null_; - set &libref2..items; - if contenttype='jobDefinition' and upcase(name)="%upcase(&name)" then - call symputx('exists',1,'l'); - run; - %mp_abort(iftrue=(&exists=1) - ,mac=&sysmacroname - ,msg=%str(Job &name already exists in &path) - ) - libname &libref2 clear; -%end; - -/* set up the body of the request to create the service */ -%local fname3; -%let fname3=%mf_getuniquefileref(); -data _null_; - file &fname3 TERMSTR=' '; - length string $32767; - string=cats('{"version": 0,"name":"' - ,"&name" - ,'","type":"Compute","parameters":[{"name":"_addjesbeginendmacros"' - ,',"type":"CHARACTER","defaultValue":"false"}'); - context=quote(cats(symget('contextname'))); - if context ne '""' then do; - string=cats(string,',{"version": 1,"name": "_contextName","defaultValue":' - ,context,',"type":"CHARACTER","label":"Context Name","required": false}'); - end; - string=cats(string,'],"code":"'); - put string; -run; - -/** - * Add webout macro - * These put statements are auto generated - to change the macro, change the - * source (mv_webout) and run `build.py` - */ -filename &adapter temp lrecl=3000; -data _null_; - file &adapter; - put "/* Created on %sysfunc(datetime(),datetime19.) by &sysuserid */"; -/* WEBOUT BEGIN */ - put '%macro mp_jsonout(action,ds,jref=_webout,dslabel=,fmt=Y '; - put ' ,engine=DATASTEP '; - put ' ,missing=NULL '; - put ' ,showmeta=N '; - put ' ,maxobs=MAX '; - put ')/*/STORE SOURCE*/; '; - put '%local tempds colinfo fmtds i numcols numobs stmt_obs lastobs optval '; - put ' tmpds1 tmpds2 tmpds3 tmpds4; '; - put '%let numcols=0; '; - put '%if &maxobs ne MAX %then %let stmt_obs=%str(if _n_>&maxobs then stop;); '; - put ' '; - put '%if &action=OPEN %then %do; '; - put ' options nobomfile; '; - put ' data _null_;file &jref encoding=''utf-8'' lrecl=200; '; - put ' put ''{"PROCESSED_DTTM" : "'' "%sysfunc(datetime(),E8601DT26.6)" ''"''; '; - put ' run; '; - put '%end; '; - put '%else %if (&action=ARR or &action=OBJ) %then %do; '; - put ' /* force variable names to always be uppercase in the JSON */ '; - put ' options validvarname=upcase; '; - put ' /* To avoid issues with _webout on EBI - such as encoding diffs and truncation '; - put ' (https://support.sas.com/kb/49/325.html) we use temporary files */ '; - put ' filename _sjs1 temp lrecl=200 ; '; - put ' data _null_; file _sjs1 encoding=''utf-8''; '; - put ' put ", ""%lowcase(%sysfunc(coalescec(&dslabel,&ds)))"":"; '; - put ' run; '; - put ' /* now write to _webout 1 char at a time */ '; - put ' data _null_; '; - put ' infile _sjs1 lrecl=1 recfm=n; '; - put ' file &jref mod lrecl=1 recfm=n; '; - put ' input sourcechar $char1. @@; '; - put ' format sourcechar hex2.; '; - put ' put sourcechar char1. @@; '; - put ' run; '; - put ' filename _sjs1 clear; '; - put ' '; - put ' /* grab col defs */ '; - put ' proc contents noprint data=&ds '; - put ' out=_data_(keep=name type length format formatl formatd varnum label); '; - put ' run; '; - put ' %let colinfo=%scan(&syslast,2,.); '; - put ' proc sort data=&colinfo; '; - put ' by varnum; '; - put ' run; '; - put ' /* move meta to mac vars */ '; - put ' data &colinfo; '; - put ' if _n_=1 then call symputx(''numcols'',nobs,''l''); '; - put ' set &colinfo end=last nobs=nobs; '; - put ' name=upcase(name); '; - put ' /* fix formats */ '; - put ' if type=2 or type=6 then do; '; - put ' typelong=''char''; '; - put ' length fmt $49.; '; - put ' if format='''' then fmt=cats(''$'',length,''.''); '; - put ' else if formatl=0 then fmt=cats(format,''.''); '; - put ' else fmt=cats(format,formatl,''.''); '; - put ' end; '; - put ' else do; '; - put ' typelong=''num''; '; - put ' if format='''' then fmt=''best.''; '; - put ' else if formatl=0 then fmt=cats(format,''.''); '; - put ' else if formatd=0 then fmt=cats(format,formatl,''.''); '; - put ' else fmt=cats(format,formatl,''.'',formatd); '; - put ' end; '; - put ' /* 32 char unique name */ '; - put ' newname=''sasjs''!!substr(cats(put(md5(name),$hex32.)),1,27); '; - put ' '; - put ' call symputx(cats(''name'',_n_),name,''l''); '; - put ' call symputx(cats(''newname'',_n_),newname,''l''); '; - put ' call symputx(cats(''length'',_n_),length,''l''); '; - put ' call symputx(cats(''fmt'',_n_),fmt,''l''); '; - put ' call symputx(cats(''type'',_n_),type,''l''); '; - put ' call symputx(cats(''typelong'',_n_),typelong,''l''); '; - put ' call symputx(cats(''label'',_n_),coalescec(label,name),''l''); '; - put ' /* overwritten when fmt=Y and a custom format exists in catalog */ '; - put ' if typelong=''num'' then call symputx(cats(''fmtlen'',_n_),200,''l''); '; - put ' else call symputx(cats(''fmtlen'',_n_),min(32767,ceil((length+10)*1.5)),''l''); '; - put ' run; '; - put ' '; - put ' %let tempds=%substr(_%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); '; - put ' proc sql; '; - put ' select count(*) into: lastobs from &ds; '; - put ' %if &maxobs ne MAX %then %let lastobs=%sysfunc(min(&lastobs,&maxobs)); '; - put ' '; - put ' %if &engine=PROCJSON %then %do; '; - put ' %if &missing=STRING %then %do; '; - put ' %put &sysmacroname: Special Missings not supported in proc json.; '; - put ' %put &sysmacroname: Switching to DATASTEP engine; '; - put ' %goto datastep; '; - put ' %end; '; - put ' data &tempds; '; - put ' set &ds; '; - put ' &stmt_obs; '; - put ' %if &fmt=N %then format _numeric_ best32.;; '; - put ' /* PRETTY is necessary to avoid line truncation in large files */ '; - put ' filename _sjs2 temp lrecl=131068 encoding=''utf-8''; '; - put ' proc json out=_sjs2 pretty '; - put ' %if &action=ARR %then nokeys ; '; - put ' ;export &tempds / nosastags fmtnumeric; '; - put ' run; '; - put ' /* send back to webout */ '; - put ' data _null_; '; - put ' infile _sjs2 lrecl=1 recfm=n; '; - put ' file &jref mod lrecl=1 recfm=n; '; - put ' input sourcechar $char1. @@; '; - put ' format sourcechar hex2.; '; - put ' put sourcechar char1. @@; '; - put ' run; '; - put ' filename _sjs2 clear; '; - put ' %end; '; - put ' %else %if &engine=DATASTEP %then %do; '; - put ' %datastep: '; - put ' %if %sysfunc(exist(&ds)) ne 1 & %sysfunc(exist(&ds,VIEW)) ne 1 '; - put ' %then %do; '; - put ' %put &sysmacroname: &ds NOT FOUND!!!; '; - put ' %return; '; - put ' %end; '; - put ' '; - put ' %if &fmt=Y %then %do; '; - put ' /** '; - put ' * Extract format definitions '; - put ' * First, by getting library locations from dictionary.formats '; - put ' * Then, by exporting the width using proc format '; - put ' * Cannot use maxw from sashelp.vformat as not always populated '; - put ' * Cannot use fmtinfo() as not supported in all flavours '; - put ' */ '; - put ' %let tmpds1=%substr(fmtsum%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); '; - put ' %let tmpds2=%substr(cntl%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); '; - put ' %let tmpds3=%substr(cntl%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); '; - put ' %let tmpds4=%substr(col%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); '; - put ' proc sql noprint; '; - put ' create table &tmpds1 as '; - put ' select cats(libname,''.'',memname) as FMTCAT, '; - put ' FMTNAME '; - put ' from dictionary.formats '; - put ' where fmttype=''F'' and libname is not null '; - put ' and fmtname in (select format from &colinfo where format is not null) '; - put ' order by 1; '; - put ' create table &tmpds2( '; - put ' FMTNAME char(32), '; - put ' LENGTH num '; - put ' ); '; - put ' %local catlist cat fmtlist i; '; - put ' select distinct fmtcat into: catlist separated by '' '' from &tmpds1; '; - put ' %do i=1 %to %sysfunc(countw(&catlist,%str( ))); '; - put ' %let cat=%scan(&catlist,&i,%str( )); '; - put ' proc sql; '; - put ' select distinct fmtname into: fmtlist separated by '' '' '; - put ' from &tmpds1 where fmtcat="&cat"; '; - put ' proc format lib=&cat cntlout=&tmpds3(keep=fmtname length); '; - put ' select &fmtlist; '; - put ' run; '; - put ' proc sql; '; - put ' insert into &tmpds2 select distinct fmtname,length from &tmpds3; '; - put ' %end; '; - put ' '; - put ' proc sql; '; - put ' create table &tmpds4 as '; - put ' select a.*, b.length as MAXW '; - put ' from &colinfo a '; - put ' left join &tmpds2 b '; - put ' on cats(a.format)=cats(upcase(b.fmtname)) '; - put ' order by a.varnum; '; - put ' data _null_; '; - put ' set &tmpds4; '; - put ' if not missing(maxw); '; - put ' call symputx( '; - put ' cats(''fmtlen'',_n_), '; - put ' /* vars need extra padding due to JSON escaping of special chars */ '; - put ' min(32767,ceil((max(length,maxw)+10)*1.5)) '; - put ' ,''l'' '; - put ' ); '; - put ' run; '; - put ' '; - put ' /* configure varlenchk - as we are explicitly shortening the variables */ '; - put ' %let optval=%sysfunc(getoption(varlenchk)); '; - put ' options varlenchk=NOWARN; '; - put ' data _data_(compress=char); '; - put ' /* shorten the new vars */ '; - put ' length '; - put ' %do i=1 %to &numcols; '; - put ' &&name&i $&&fmtlen&i '; - put ' %end; '; - put ' ; '; - put ' /* rename on entry */ '; - put ' set &ds(rename=( '; - put ' %do i=1 %to &numcols; '; - put ' &&name&i=&&newname&i '; - put ' %end; '; - put ' )); '; - put ' &stmt_obs; '; - put ' '; - put ' drop '; - put ' %do i=1 %to &numcols; '; - put ' &&newname&i '; - put ' %end; '; - put ' ; '; - put ' %do i=1 %to &numcols; '; - put ' %if &&typelong&i=num %then %do; '; - put ' &&name&i=cats(put(&&newname&i,&&fmt&i)); '; - put ' %end; '; - put ' %else %do; '; - put ' &&name&i=put(&&newname&i,&&fmt&i); '; - put ' %end; '; - put ' %end; '; - put ' if _error_ then do; '; - put ' call symputx(''syscc'',1012); '; - put ' stop; '; - put ' end; '; - put ' run; '; - put ' %let fmtds=&syslast; '; - put ' options varlenchk=&optval; '; - put ' %end; '; - put ' '; - put ' proc format; /* credit yabwon for special null removal */ '; - put ' value bart (default=40) '; - put ' %if &missing=NULL %then %do; '; - put ' ._ - .z = null '; - put ' %end; '; - put ' %else %do; '; - put ' ._ = [quote()] '; - put ' . = null '; - put ' .a - .z = [quote()] '; - put ' %end; '; - put ' other = [best.]; '; - put ' '; - put ' data &tempds; '; - put ' attrib _all_ label=''''; '; - put ' %do i=1 %to &numcols; '; - put ' %if &&typelong&i=char or &fmt=Y %then %do; '; - put ' length &&name&i $&&fmtlen&i...; '; - put ' format &&name&i $&&fmtlen&i...; '; - put ' %end; '; - put ' %end; '; - put ' %if &fmt=Y %then %do; '; - put ' set &fmtds; '; - put ' %end; '; - put ' %else %do; '; - put ' set &ds; '; - put ' %end; '; - put ' &stmt_obs; '; - put ' format _numeric_ bart.; '; - put ' %do i=1 %to &numcols; '; - put ' %if &&typelong&i=char or &fmt=Y %then %do; '; - put ' if findc(&&name&i,''"\''!!''0A0D09000E0F010210111A''x) then do; '; - put ' &&name&i=''"''!!trim( '; - put ' prxchange(''s/"/\\"/'',-1, /* double quote */ '; - put ' prxchange(''s/\x0A/\n/'',-1, /* new line */ '; - put ' prxchange(''s/\x0D/\r/'',-1, /* carriage return */ '; - put ' prxchange(''s/\x09/\\t/'',-1, /* tab */ '; - put ' prxchange(''s/\x00/\\u0000/'',-1, /* NUL */ '; - put ' prxchange(''s/\x0E/\\u000E/'',-1, /* SS */ '; - put ' prxchange(''s/\x0F/\\u000F/'',-1, /* SF */ '; - put ' prxchange(''s/\x01/\\u0001/'',-1, /* SOH */ '; - put ' prxchange(''s/\x02/\\u0002/'',-1, /* STX */ '; - put ' prxchange(''s/\x10/\\u0010/'',-1, /* DLE */ '; - put ' prxchange(''s/\x11/\\u0011/'',-1, /* DC1 */ '; - put ' prxchange(''s/\x1A/\\u001A/'',-1, /* SUB */ '; - put ' prxchange(''s/\\/\\\\/'',-1,&&name&i) '; - put ' )))))))))))))!!''"''; '; - put ' end; '; - put ' else &&name&i=quote(cats(&&name&i)); '; - put ' %end; '; - put ' %end; '; - put ' run; '; - put ' '; - put ' filename _sjs3 temp lrecl=131068 ; '; - put ' data _null_; '; - put ' file _sjs3 encoding=''utf-8''; '; - put ' if _n_=1 then put "["; '; - put ' set &tempds; '; - put ' if _n_>1 then put "," @; put '; - put ' %if &action=ARR %then "[" ; %else "{" ; '; - put ' %do i=1 %to &numcols; '; - put ' %if &i>1 %then "," ; '; - put ' %if &action=OBJ %then """&&name&i"":" ; '; - put ' "&&name&i"n /* name literal for reserved variable names */ '; - put ' %end; '; - put ' %if &action=ARR %then "]" ; %else "}" ; ; '; - put ' '; - put ' /* close out the table */ '; - put ' data _null_; '; - put ' file _sjs3 mod encoding=''utf-8''; '; - put ' put '']''; '; - put ' run; '; - put ' data _null_; '; - put ' infile _sjs3 lrecl=1 recfm=n; '; - put ' file &jref mod lrecl=1 recfm=n; '; - put ' input sourcechar $char1. @@; '; - put ' format sourcechar hex2.; '; - put ' put sourcechar char1. @@; '; - put ' run; '; - put ' filename _sjs3 clear; '; - put ' %end; '; - put ' '; - put ' proc sql; '; - put ' drop table &colinfo, &tempds; '; - put ' '; - put ' %if %substr(&showmeta,1,1)=Y %then %do; '; - put ' filename _sjs4 temp lrecl=131068 encoding=''utf-8''; '; - put ' data _null_; '; - put ' file _sjs4; '; - put ' length label $350; '; - put ' put ", ""$%lowcase(%sysfunc(coalescec(&dslabel,&ds)))"":{""vars"":{"; '; - put ' do i=1 to &numcols; '; - put ' name=quote(trim(symget(cats(''name'',i)))); '; - put ' format=quote(trim(symget(cats(''fmt'',i)))); '; - put ' label=quote(prxchange(''s/\\/\\\\/'',-1,trim(symget(cats(''label'',i))))); '; - put ' length=quote(trim(symget(cats(''length'',i)))); '; - put ' type=quote(trim(symget(cats(''typelong'',i)))); '; - put ' if i>1 then put "," @@; '; - put ' put name '':{"format":'' format '',"label":'' label '; - put ' '',"length":'' length '',"type":'' type ''}''; '; - put ' end; '; - put ' put ''}}''; '; - put ' run; '; - put ' /* send back to webout */ '; - put ' data _null_; '; - put ' infile _sjs4 lrecl=1 recfm=n; '; - put ' file &jref mod lrecl=1 recfm=n; '; - put ' input sourcechar $char1. @@; '; - put ' format sourcechar hex2.; '; - put ' put sourcechar char1. @@; '; - put ' run; '; - put ' filename _sjs4 clear; '; - put ' %end; '; - put '%end; '; - put ' '; - put '%else %if &action=CLOSE %then %do; '; - put ' data _null_; file &jref encoding=''utf-8'' mod ; '; - put ' put "}"; '; - put ' run; '; - put '%end; '; - put '%mend mp_jsonout; '; - put ' '; - put '%macro mf_getuser( '; - put ')/*/STORE SOURCE*/; '; - put ' %local user; '; - put ' '; - put ' %if %symexist(_sasjs_username) %then %let user=&_sasjs_username; '; - put ' %else %if %symexist(SYS_COMPUTE_SESSION_OWNER) %then %do; '; - put ' %let user=&SYS_COMPUTE_SESSION_OWNER; '; - put ' %end; '; - put ' %else %if %symexist(_metaperson) %then %do; '; - put ' %if %length(&_metaperson)=0 %then %let user=&sysuserid; '; - put ' /* sometimes SAS will add @domain extension - remove for consistency */ '; - put ' /* but be sure to quote in case of usernames with commas */ '; - put ' %else %let user=%unquote(%scan(%quote(&_metaperson),1,@)); '; - put ' %end; '; - put ' %else %let user=&sysuserid; '; - put ' '; - put ' %quote(&user) '; - put ' '; - put '%mend mf_getuser; '; - put '%macro mv_webout(action,ds,fref=_mvwtemp,dslabel=,fmt=N,stream=Y,missing=NULL '; - put ' ,showmeta=N,maxobs=MAX,workobs=0 '; - put '); '; - put '%global _webin_file_count _webin_fileuri _debug _omittextlog _webin_name '; - put ' sasjs_tables SYS_JES_JOB_URI; '; - put '%if %index("&_debug",log) %then %let _debug=131; '; - put ' '; - put '%local i tempds table; '; - put '%let action=%upcase(&action); '; - put ' '; - put '%if &action=FETCH %then %do; '; - put ' %if %upcase(&_omittextlog)=FALSE or %str(&_debug) ge 131 %then %do; '; - put ' options mprint notes mprintnest; '; - put ' %end; '; - put ' '; - put ' %if not %symexist(_webin_fileuri1) %then %do; '; - put ' %let _webin_file_count=%eval(&_webin_file_count+0); '; - put ' %let _webin_fileuri1=&_webin_fileuri; '; - put ' %let _webin_name1=&_webin_name; '; - put ' %end; '; - put ' '; - put ' /* if the sasjs_tables param is passed, we expect param based upload */ '; - put ' %if %length(&sasjs_tables.X)>1 %then %do; '; - put ' '; - put ' /* convert data from macro variables to datasets */ '; - put ' %do i=1 %to %sysfunc(countw(&sasjs_tables)); '; - put ' %let table=%scan(&sasjs_tables,&i,%str( )); '; - put ' %if %symexist(sasjs&i.data0)=0 %then %let sasjs&i.data0=1; '; - put ' data _null_; '; - put ' file "%sysfunc(pathname(work))/&table..csv" recfm=n; '; - put ' retain nrflg 0; '; - put ' length line $32767; '; - put ' do i=1 to &&sasjs&i.data0; '; - put ' if &&sasjs&i.data0=1 then line=symget("sasjs&i.data"); '; - put ' else line=symget(cats("sasjs&i.data",i)); '; - put ' if i=1 and substr(line,1,7)=''%nrstr('' then do; '; - put ' nrflg=1; '; - put ' line=substr(line,8); '; - put ' end; '; - put ' if i=&&sasjs&i.data0 and nrflg=1 then do; '; - put ' line=substr(line,1,length(line)-1); '; - put ' end; '; - put ' put line +(-1) @; '; - put ' end; '; - put ' run; '; - put ' data _null_; '; - put ' infile "%sysfunc(pathname(work))/&table..csv" termstr=crlf ; '; - put ' input; '; - put ' if _n_=1 then call symputx(''input_statement'',_infile_); '; - put ' list; '; - put ' data work.&table; '; - put ' infile "%sysfunc(pathname(work))/&table..csv" firstobs=2 dsd '; - put ' termstr=crlf; '; - put ' input &input_statement; '; - put ' run; '; - put ' %end; '; - put ' %end; '; - put ' %else %do i=1 %to &_webin_file_count; '; - put ' /* read in any files that are sent */ '; - put ' /* this part needs refactoring for wide files */ '; - put ' filename indata filesrvc "&&_webin_fileuri&i" lrecl=999999; '; - put ' data _null_; '; - put ' infile indata termstr=crlf lrecl=32767; '; - put ' input; '; - put ' if _n_=1 then call symputx(''input_statement'',_infile_); '; - put ' %if %str(&_debug) ge 131 %then %do; '; - put ' if _n_<20 then putlog _infile_; '; - put ' else stop; '; - put ' %end; '; - put ' %else %do; '; - put ' stop; '; - put ' %end; '; - put ' run; '; - put ' data &&_webin_name&i; '; - put ' infile indata firstobs=2 dsd termstr=crlf ; '; - put ' input &input_statement; '; - put ' run; '; - put ' %let sasjs_tables=&sasjs_tables &&_webin_name&i; '; - put ' %end; '; - put '%end; '; - put '%else %if &action=OPEN %then %do; '; - put ' /* setup webout */ '; - put ' OPTIONS NOBOMFILE; '; - put ' %if "X&SYS_JES_JOB_URI.X"="XX" %then %do; '; - put ' filename _webout temp lrecl=999999 mod; '; - put ' %end; '; - put ' %else %do; '; - put ' filename _webout filesrvc parenturi="&SYS_JES_JOB_URI" '; - put ' name="_webout.json" lrecl=999999 mod; '; - put ' %end; '; - put ' '; - put ' /* setup temp ref */ '; - put ' %if %upcase(&fref) ne _WEBOUT %then %do; '; - put ' filename &fref temp lrecl=999999 permission=''A::u::rwx,A::g::rw-,A::o::---''; '; - put ' %end; '; - put ' '; - put ' /* setup json */ '; - put ' data _null_;file &fref; '; - put ' put ''{"SYSDATE" : "'' "&SYSDATE" ''"''; '; - put ' put '',"SYSTIME" : "'' "&SYSTIME" ''"''; '; - put ' run; '; - put '%end; '; - put '%else %if &action=ARR or &action=OBJ %then %do; '; - put ' %mp_jsonout(&action,&ds,dslabel=&dslabel,fmt=&fmt,jref=&fref '; - put ' ,engine=DATASTEP,missing=&missing,showmeta=&showmeta,maxobs=&maxobs '; - put ' ) '; - put '%end; '; - put '%else %if &action=CLOSE %then %do; '; - put ' %if %str(&workobs) > 0 %then %do; '; - put ' /* send back first XX records of each work table for debugging */ '; - put ' data;run;%let tempds=%scan(&syslast,2,.); '; - put ' ods output Members=&tempds; '; - put ' proc datasets library=WORK memtype=data; '; - put ' %local wtcnt;%let wtcnt=0; '; - put ' data _null_; '; - put ' set &tempds; '; - put ' if not (upcase(name) =:"DATA"); /* ignore temp datasets */ '; - put ' i+1; '; - put ' call symputx(cats(''wt'',i),name,''l''); '; - put ' call symputx(''wtcnt'',i,''l''); '; - put ' data _null_; file &fref mod; put ",""WORK"":{"; '; - put ' %do i=1 %to &wtcnt; '; - put ' %let wt=&&wt&i; '; - put ' data _null_; file &fref mod; '; - put ' dsid=open("WORK.&wt",''is''); '; - put ' nlobs=attrn(dsid,''NLOBS''); '; - put ' nvars=attrn(dsid,''NVARS''); '; - put ' rc=close(dsid); '; - put ' if &i>1 then put '',''@; '; - put ' put " ""&wt"" : {"; '; - put ' put ''"nlobs":'' nlobs; '; - put ' put '',"nvars":'' nvars; '; - put ' %mp_jsonout(OBJ,&wt,jref=&fref,dslabel=first10rows,showmeta=Y '; - put ' ,maxobs=&workobs '; - put ' ) '; - put ' data _null_; file &fref mod;put "}"; '; - put ' %end; '; - put ' data _null_; file &fref mod;put "}";run; '; - put ' %end; '; - put ' '; - put ' /* close off json */ '; - put ' data _null_;file &fref mod; '; - put ' length SYSPROCESSNAME syserrortext syswarningtext autoexec $512; '; - put ' put ",""_DEBUG"" : ""&_debug"" "; '; - put ' _PROGRAM=quote(trim(resolve(symget(''_PROGRAM'')))); '; - put ' put '',"_PROGRAM" : '' _PROGRAM ; '; - put ' autoexec=quote(urlencode(trim(getoption(''autoexec'')))); '; - put ' put '',"AUTOEXEC" : '' autoexec; '; - put ' put ",""MF_GETUSER"" : ""%mf_getuser()"" "; '; - put ' SYS_JES_JOB_URI=quote(trim(resolve(symget(''SYS_JES_JOB_URI'')))); '; - put ' put '',"SYS_JES_JOB_URI" : '' SYS_JES_JOB_URI ; '; - put ' put ",""SYSJOBID"" : ""&sysjobid"" "; '; - put ' put ",""SYSCC"" : ""&syscc"" "; '; - put ' syserrortext=cats(symget(''syserrortext'')); '; - put ' if findc(syserrortext,''"\''!!''0A0D09000E0F010210111A''x) then do; '; - put ' syserrortext=''"''!!trim( '; - put ' prxchange(''s/"/\\"/'',-1, /* double quote */ '; - put ' prxchange(''s/\x0A/\n/'',-1, /* new line */ '; - put ' prxchange(''s/\x0D/\r/'',-1, /* carriage return */ '; - put ' prxchange(''s/\x09/\\t/'',-1, /* tab */ '; - put ' prxchange(''s/\x00/\\u0000/'',-1, /* NUL */ '; - put ' prxchange(''s/\x0E/\\u000E/'',-1, /* SS */ '; - put ' prxchange(''s/\x0F/\\u000F/'',-1, /* SF */ '; - put ' prxchange(''s/\x01/\\u0001/'',-1, /* SOH */ '; - put ' prxchange(''s/\x02/\\u0002/'',-1, /* STX */ '; - put ' prxchange(''s/\x10/\\u0010/'',-1, /* DLE */ '; - put ' prxchange(''s/\x11/\\u0011/'',-1, /* DC1 */ '; - put ' prxchange(''s/\x1A/\\u001A/'',-1, /* SUB */ '; - put ' prxchange(''s/\\/\\\\/'',-1,syserrortext) '; - put ' )))))))))))))!!''"''; '; - put ' end; '; - put ' else syserrortext=cats(''"'',syserrortext,''"''); '; - put ' put '',"SYSERRORTEXT" : '' syserrortext; '; - put ' put ",""SYSHOSTNAME"" : ""&syshostname"" "; '; - put ' put ",""SYSPROCESSID"" : ""&SYSPROCESSID"" "; '; - put ' put ",""SYSPROCESSMODE"" : ""&SYSPROCESSMODE"" "; '; - put ' SYSPROCESSNAME=quote(urlencode(cats(SYSPROCESSNAME))); '; - put ' put ",""SYSPROCESSNAME"" : " SYSPROCESSNAME; '; - put ' put ",""SYSJOBID"" : ""&sysjobid"" "; '; - put ' put ",""SYSSCPL"" : ""&sysscpl"" "; '; - put ' put ",""SYSSITE"" : ""&syssite"" "; '; - put ' put ",""SYSUSERID"" : ""&sysuserid"" "; '; - put ' sysvlong=quote(trim(symget(''sysvlong''))); '; - put ' put '',"SYSVLONG" : '' sysvlong; '; - put ' syswarningtext=cats(symget(''syswarningtext'')); '; - put ' if findc(syswarningtext,''"\''!!''0A0D09000E0F010210111A''x) then do; '; - put ' syswarningtext=''"''!!trim( '; - put ' prxchange(''s/"/\\"/'',-1, /* double quote */ '; - put ' prxchange(''s/\x0A/\n/'',-1, /* new line */ '; - put ' prxchange(''s/\x0D/\r/'',-1, /* carriage return */ '; - put ' prxchange(''s/\x09/\\t/'',-1, /* tab */ '; - put ' prxchange(''s/\x00/\\u0000/'',-1, /* NUL */ '; - put ' prxchange(''s/\x0E/\\u000E/'',-1, /* SS */ '; - put ' prxchange(''s/\x0F/\\u000F/'',-1, /* SF */ '; - put ' prxchange(''s/\x01/\\u0001/'',-1, /* SOH */ '; - put ' prxchange(''s/\x02/\\u0002/'',-1, /* STX */ '; - put ' prxchange(''s/\x10/\\u0010/'',-1, /* DLE */ '; - put ' prxchange(''s/\x11/\\u0011/'',-1, /* DC1 */ '; - put ' prxchange(''s/\x1A/\\u001A/'',-1, /* SUB */ '; - put ' prxchange(''s/\\/\\\\/'',-1,syswarningtext) '; - put ' )))))))))))))!!''"''; '; - put ' end; '; - put ' else syswarningtext=cats(''"'',syswarningtext,''"''); '; - put ' put '',"SYSWARNINGTEXT" : '' syswarningtext; '; - put ' put '',"END_DTTM" : "'' "%sysfunc(datetime(),E8601DT26.6)" ''" ''; '; - put ' length memsize $32; '; - put ' memsize="%sysfunc(INPUTN(%sysfunc(getoption(memsize)), best.),sizekmg.)"; '; - put ' memsize=quote(cats(memsize)); '; - put ' put '',"MEMSIZE" : '' memsize; '; - put ' put "}"; '; - put ' '; - put ' %if %upcase(&fref) ne _WEBOUT and &stream=Y %then %do; '; - put ' data _null_; rc=fcopy("&fref","_webout");run; '; - put ' %end; '; - put ' '; - put '%end; '; - put ' '; - put '%mend mv_webout; '; -/* WEBOUT END */ - put '/* if calling viya service with _job param, _program will conflict */'; - put '/* so it is provided by SASjs instead as __program */'; - put '%global __program _program;'; - put '%let _program=%sysfunc(coalescec(&__program,&_program));'; - put ' '; - put '%macro webout(action,ds,dslabel=,fmt=,missing=NULL,showmeta=NO'; - put ' ,maxobs=MAX'; - put ');'; - put ' %mv_webout(&action,ds=&ds,dslabel=&dslabel,fmt=&fmt,missing=&missing'; - put ' ,showmeta=&showmeta,maxobs=&maxobs'; - put ' )'; - put '%mend;'; -run; - -/* insert the code, escaping double quotes and carriage returns */ -%&dbg.put &sysmacroname: Creating final input file; -%local x fref freflist; -%let freflist= &adapter &precode &code ; -%do x=1 %to %sysfunc(countw(&freflist)); - %let fref=%scan(&freflist,&x); - %&dbg.put &sysmacroname: adding &fref fileref; - data _null_; - length filein 8 fileid 8; - filein = fopen("&fref","I",1,"B"); - fileid = fopen("&fname3","A",1,"B"); - rec = "20"x; - do while(fread(filein)=0); - rc = fget(filein,rec,1); - if rec='"' then do; /* DOUBLE QUOTE */ - rc =fput(fileid,'\');rc =fwrite(fileid); - rc =fput(fileid,'"');rc =fwrite(fileid); - end; - else if rec='0A'x then do; /* LF */ - rc =fput(fileid,'\');rc =fwrite(fileid); - rc =fput(fileid,'n');rc =fwrite(fileid); - end; - else if rec='0D'x then do; /* CR */ - rc =fput(fileid,'\');rc =fwrite(fileid); - rc =fput(fileid,'r');rc =fwrite(fileid); - end; - else if rec='09'x then do; /* TAB */ - rc =fput(fileid,'\');rc =fwrite(fileid); - rc =fput(fileid,'t');rc =fwrite(fileid); - end; - else if rec='5C'x then do; /* BACKSLASH */ - rc =fput(fileid,'\');rc =fwrite(fileid); - rc =fput(fileid,'\');rc =fwrite(fileid); - end; - else if rec='01'x then do; /* Unprintable */ - rc =fput(fileid,'\');rc =fwrite(fileid); - rc =fput(fileid,'u');rc =fwrite(fileid); - rc =fput(fileid,'0');rc =fwrite(fileid); - rc =fput(fileid,'0');rc =fwrite(fileid); - rc =fput(fileid,'0');rc =fwrite(fileid); - rc =fput(fileid,'1');rc =fwrite(fileid); - end; - else if rec='07'x then do; /* Bell Char */ - rc =fput(fileid,'\');rc =fwrite(fileid); - rc =fput(fileid,'u');rc =fwrite(fileid); - rc =fput(fileid,'0');rc =fwrite(fileid); - rc =fput(fileid,'0');rc =fwrite(fileid); - rc =fput(fileid,'0');rc =fwrite(fileid); - rc =fput(fileid,'7');rc =fwrite(fileid); - end; - else if rec='1B'x then do; /* escape char */ - rc =fput(fileid,'\');rc =fwrite(fileid); - rc =fput(fileid,'u');rc =fwrite(fileid); - rc =fput(fileid,'0');rc =fwrite(fileid); - rc =fput(fileid,'0');rc =fwrite(fileid); - rc =fput(fileid,'1');rc =fwrite(fileid); - rc =fput(fileid,'B');rc =fwrite(fileid); - end; - else do; - rc =fput(fileid,rec); - rc =fwrite(fileid); - end; - end; - rc=fclose(filein); - rc=fclose(fileid); - run; -%end; - -/* finish off the body of the code file loaded to JES */ -data _null_; - file &fname3 mod TERMSTR=' '; - put '"}'; -run; - -%if &mdebug=1 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then %do; - %put &sysmacroname: input about to be POSTed; - data _null_;infile &fname3;input;putlog _infile_;run; -%end; - -%&dbg.put &sysmacroname: Creating the actual service!; -%local fname4; -%let fname4=%mf_getuniquefileref(); -proc http method='POST' - in=&fname3 - out=&fname4 - &oauth_bearer - url="&base_uri/jobDefinitions/definitions?parentFolderUri=&parentFolderUri"; - headers 'Content-Type'='application/vnd.sas.job.definition+json' - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; - "Accept"="application/vnd.sas.job.definition+json"; -%if &mdebug=1 %then %do; - debug level = 3; -%end; -run; -%if &mdebug=1 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then %do; - %put &sysmacroname: output from POSTing job definition; - data _null_;infile &fname4;input;putlog _infile_;run; -%end; -%mp_abort(iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 201) - ,mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) -) - -/* get the url so we can give a helpful log message */ -%local url; -data _null_; - if symexist('_baseurl') then do; - url=symget('_baseurl'); - if subpad(url,length(url)-9,9)='SASStudio' - then url=substr(url,1,length(url)-11); - else url="&systcpiphostname"; - end; - else url="&systcpiphostname"; - call symputx('url',url); -run; - -%if &mdebug=1 %then %do; - %put &sysmacroname exit vars:; - %put _local_; -%end; -%else %do; - /* clear refs */ - filename &fname1 clear; - filename &fname2 clear; - filename &fname3 clear; - filename &fname4 clear; - filename &adapter clear; - libname &libref1 clear; -%end; - -%put &sysmacroname: Job &name successfully created in &path; -%put &sysmacroname:; -%put &sysmacroname: Check it out here:; -%put &sysmacroname:;%put; -%put &url/SASJobExecution?_PROGRAM=&path/&name;%put; -%put &sysmacroname:; -%put &sysmacroname:; - -%mend mv_createwebservice; diff --git a/003_macros/mv_deletefoldermember.sas b/003_macros/mv_deletefoldermember.sas deleted file mode 100644 index e01feb6..0000000 --- a/003_macros/mv_deletefoldermember.sas +++ /dev/null @@ -1,154 +0,0 @@ -/*** HELP START ***//** - @file mv_deletefoldermember.sas - @brief Deletes an item in a Viya folder - @details If not executed in Studio 5+ will expect oauth token in a global - macro variable (default ACCESS_TOKEN). - - filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - %mv_createwebservice(path=/Public/test, name=blah) - %mv_deletejes(path=/Public/test, name=blah) - - - @param [in] path= () - The full path of the folder containing the item to be deleted - @param [in] name= The name of the item to be deleted - @param [in] contenttype= The contenttype of the item, eg: file, jobDefinition - @param [in] access_token_var= (ACCESS_TOKEN) - The global macro variable to contain the access token - @param [in] grant_type= (sas_services) - valid values are "password" or "authorization_code" (unquoted). - - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - @li mf_isblank.sas - -**//*** HELP END ***/ - -%macro mv_deletefoldermember(path= - ,name= - ,contenttype= - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ); -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; - -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) -%mp_abort(iftrue=(%mf_isblank(&path)=1) - ,mac=&sysmacroname - ,msg=%str(path value must be provided) -) -%mp_abort(iftrue=(%mf_isblank(&name)=1) - ,mac=&sysmacroname - ,msg=%str(name value must be provided) -) -%mp_abort(iftrue=(%length(&path)=1) - ,mac=&sysmacroname - ,msg=%str(path value must be provided) -) - -options noquotelenmax; - -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -%put &sysmacroname: fetching details for &path ; -%local fname1; -%let fname1=%mf_getuniquefileref(); -proc http method='GET' out=&fname1 &oauth_bearer - url="&base_uri/folders/folders/@item?path=&path"; -%if &grant_type=authorization_code %then %do; - headers "Authorization"="Bearer &&&access_token_var"; -%end; -run; -%if &SYS_PROCHTTP_STATUS_CODE=404 %then %do; - %put &sysmacroname: Folder &path NOT FOUND - nothing to delete!; - %return; -%end; -%else %if &SYS_PROCHTTP_STATUS_CODE ne 200 %then %do; - /*data _null_;infile &fname1;input;putlog _infile_;run;*/ - %mp_abort(mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) - ) -%end; - -%put &sysmacroname: grab the follow on link ; -%local libref1; -%let libref1=%mf_getuniquelibref(); -libname &libref1 JSON fileref=&fname1; -data _null_; - set &libref1..links; - if rel='members' then call symputx('mref',quote("&base_uri"!!trim(href)),'l'); -run; - -/* get the children */ -%local fname1a; -%let fname1a=%mf_getuniquefileref(); -proc http method='GET' out=&fname1a &oauth_bearer - url=%unquote(%superq(mref)); -%if &grant_type=authorization_code %then %do; - headers "Authorization"="Bearer &&&access_token_var"; -%end; -run; -%put &=SYS_PROCHTTP_STATUS_CODE; -%local libref1a; -%let libref1a=%mf_getuniquelibref(); -libname &libref1a JSON fileref=&fname1a; -%local uri found; -%let found=0; -%put Getting object uri from &libref1a..items; -data _null_; - length contenttype name $1000; - set &libref1a..items; - if contenttype="&contenttype" and upcase(name)="%upcase(&name)" then do; - call symputx('uri',uri,'l'); - call symputx('found',1,'l'); - end; -run; -%if &found=0 %then %do; - %put NOTE:;%put NOTE- &sysmacroname: &path/&name NOT FOUND;%put NOTE- ; - %return; -%end; -proc http method="DELETE" url="&base_uri&uri" &oauth_bearer; - headers -%if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" -%end; - "Accept"="*/*";/**/ -run; -%if &SYS_PROCHTTP_STATUS_CODE ne 204 %then %do; - data _null_; infile &fname2; input; putlog _infile_;run; - %mp_abort(mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) - ) -%end; -%else %put &sysmacroname: &path/&name(&contenttype) successfully deleted; - -/* clear refs */ -filename &fname1 clear; -libname &libref1 clear; -filename &fname1a clear; -libname &libref1a clear; - -%mend mv_deletefoldermember; diff --git a/003_macros/mv_deletejes.sas b/003_macros/mv_deletejes.sas deleted file mode 100644 index e21a917..0000000 --- a/003_macros/mv_deletejes.sas +++ /dev/null @@ -1,151 +0,0 @@ -/*** HELP START ***//** - @file - @brief Deletes a Viya Job, if it exists - @details If not executed in Studio 5+ will expect oauth token in a global - macro variable (default ACCESS_TOKEN). - - filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - %mv_createwebservice(path=/Public/test, name=blah) - %mv_deletejes(path=/Public/test, name=blah) - - - @param [in] path= () - The full path of the folder containing the Job Execution Service - @param [in] name= The name of the Job Execution Service to be deleted - @param [in] access_token_var= (ACCESS_TOKEN) - The global macro variable to contain the access token - @param [in] grant_type= (sas_services) - Valid values are "password" or "authorization_code" (unquoted). - - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - @li mf_isblank.sas - -**//*** HELP END ***/ - -%macro mv_deletejes(path= - ,name= - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ); -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; - -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) -%mp_abort(iftrue=(%mf_isblank(&path)=1) - ,mac=&sysmacroname - ,msg=%str(path value must be provided) -) -%mp_abort(iftrue=(%mf_isblank(&name)=1) - ,mac=&sysmacroname - ,msg=%str(name value must be provided) -) -%mp_abort(iftrue=(%length(&path)=1) - ,mac=&sysmacroname - ,msg=%str(path value must be provided) -) - -options noquotelenmax; -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -%put &sysmacroname: fetching details for &path ; -%local fname1; -%let fname1=%mf_getuniquefileref(); -proc http method='GET' out=&fname1 &oauth_bearer - url="&base_uri/folders/folders/@item?path=&path"; -%if &grant_type=authorization_code %then %do; - headers "Authorization"="Bearer &&&access_token_var"; -%end; -run; -%if &SYS_PROCHTTP_STATUS_CODE=404 %then %do; - %put &sysmacroname: Folder &path NOT FOUND - nothing to delete!; - %return; -%end; -%else %if &SYS_PROCHTTP_STATUS_CODE ne 200 %then %do; - /*data _null_;infile &fname1;input;putlog _infile_;run;*/ - %mp_abort(mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) - ) -%end; - -%put &sysmacroname: grab the follow on link ; -%local libref1; -%let libref1=%mf_getuniquelibref(); -libname &libref1 JSON fileref=&fname1; -data _null_; - set &libref1..links; - if rel='members' then call symputx('mref',quote("&base_uri"!!trim(href)),'l'); -run; - -/* get the children */ -%local fname1a; -%let fname1a=%mf_getuniquefileref(); -proc http method='GET' out=&fname1a &oauth_bearer - url=%unquote(%superq(mref)); -%if &grant_type=authorization_code %then %do; - headers "Authorization"="Bearer &&&access_token_var"; -%end; -run; -%put &=SYS_PROCHTTP_STATUS_CODE; -%local libref1a; -%let libref1a=%mf_getuniquelibref(); -libname &libref1a JSON fileref=&fname1a; -%local uri found; -%let found=0; -%put Getting object uri from &libref1a..items; -data _null_; - length contenttype name $1000; - set &libref1a..items; - if contenttype='jobDefinition' and upcase(name)="%upcase(&name)" then do; - call symputx('uri',cats("&base_uri",uri),'l'); - call symputx('found',1,'l'); - end; -run; -%if &found=0 %then %do; - %put NOTE:;%put NOTE- &sysmacroname: &path/&name NOT FOUND;%put NOTE- ; - %return; -%end; -proc http method="DELETE" url="&uri" &oauth_bearer; - headers -%if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" -%end; - "Accept"="*/*";/**/ -run; -%if &SYS_PROCHTTP_STATUS_CODE ne 204 %then %do; - data _null_; infile &fname2; input; putlog _infile_;run; - %mp_abort(mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) - ) -%end; -%else %put &sysmacroname: &path/&name successfully deleted; - -/* clear refs */ -filename &fname1 clear; -libname &libref1 clear; -filename &fname1a clear; -libname &libref1a clear; - -%mend mv_deletejes; diff --git a/003_macros/mv_deleteviyafolder.sas b/003_macros/mv_deleteviyafolder.sas deleted file mode 100644 index a8e18a9..0000000 --- a/003_macros/mv_deleteviyafolder.sas +++ /dev/null @@ -1,154 +0,0 @@ -/*** HELP START ***//** - @file mv_deleteviyafolder.sas - @brief Creates a viya folder if that folder does not already exist - @details If not running in Studo 5 +, will expect an oauth token in a global - macro variable (default ACCESS_TOKEN). - - %mv_createfolder(path=/Public/test/blah) - %mv_deleteviyafolder(path=/Public/test) - - - @param [in] path= The full path of the folder to be deleted - @param [in] access_token_var= (ACCESS_TOKEN) The global macro variable to - contain the access token - @param [in] grant_type= (sas_services) Valid values are: - @li password - @li authorization_code - @li detect - will check if access_token exists, if not will use sas_services - if a SASStudioV session else authorization_code. Default option. - @li sas_services - will use oauth_bearer=sas_services. - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mf_existds.sas - @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - @li mf_isblank.sas - -**//*** HELP END ***/ - -%macro mv_deleteviyafolder(path= - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ,mdebug=0 - ); -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; - -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) -%mp_abort(iftrue=(%mf_isblank(&path)=1) - ,mac=&sysmacroname - ,msg=%str(path value must be provided) -) -%mp_abort(iftrue=(%length(&path)=1) - ,mac=&sysmacroname - ,msg=%str(path value must be provided) -) - -options noquotelenmax; -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -%put &sysmacroname: fetching details for &path ; -%local fname1; -%let fname1=%mf_getuniquefileref(); -proc http method='GET' out=&fname1 &oauth_bearer - url="&base_uri/folders/folders/@item?path=&path"; - %if &grant_type=authorization_code %then %do; - headers "Authorization"="Bearer &&&access_token_var"; - %end; -run; -%if &SYS_PROCHTTP_STATUS_CODE=404 %then %do; - %put &sysmacroname: Folder &path NOT FOUND - nothing to delete!; - %return; -%end; -%else %if &SYS_PROCHTTP_STATUS_CODE ne 200 %then %do; - /*data _null_;infile &fname1;input;putlog _infile_;run;*/ - %mp_abort(mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) - ) -%end; - -%put &sysmacroname: grab the follow on link ; -%local libref1; -%let libref1=%mf_getuniquelibref(); -libname &libref1 JSON fileref=&fname1; -data _null_; - set &libref1..links; - if rel='deleteRecursively' then - call symputx('href',quote("&base_uri"!!trim(href)),'l'); - else if rel='members' then - call symputx('mref',quote(cats("&base_uri",href,'?recursive=true')),'l'); -run; - -/* before we can delete the folder, we need to delete the children */ -%local fname1a; -%let fname1a=%mf_getuniquefileref(); -proc http method='GET' out=&fname1a &oauth_bearer - url=%unquote(%superq(mref)); -%if &grant_type=authorization_code %then %do; - headers "Authorization"="Bearer &&&access_token_var"; -%end; -run; -%put &=SYS_PROCHTTP_STATUS_CODE; -%local libref1a; -%let libref1a=%mf_getuniquelibref(); -libname &libref1a JSON fileref=&fname1a; - -%if %mf_existds(&libref1a..items_links) %then %do; - data _null_; - set &libref1a..items_links; - if href=:'/folders/folders' then return; - if rel='deleteResource' then - call execute('proc http method="DELETE" url=' - !!quote("&base_uri"!!trim(href)) - !!'; headers "Authorization"="Bearer &&&access_token_var" ' - !!' "Accept"="*/*";run; /**/'); - run; -%end; - -%put &sysmacroname: perform the delete operation ; -%local fname2; -%let fname2=%mf_getuniquefileref(); -proc http method='DELETE' out=&fname2 &oauth_bearer - url=%unquote(%superq(href)); - headers - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; - 'Accept'='*/*'; /**/ -run; -%if &SYS_PROCHTTP_STATUS_CODE ne 204 %then %do; - data _null_; infile &fname2; input; putlog _infile_;run; - %mp_abort(mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) - ) -%end; -%else %put &sysmacroname: &path successfully deleted; - -%if &mdebug=0 %then %do; - /* clear refs */ - filename &fname1 clear; - filename &fname2 clear; - libname &libref1 clear; -%end; - -%mend mv_deleteviyafolder; diff --git a/003_macros/mv_getclients.sas b/003_macros/mv_getclients.sas deleted file mode 100644 index be55a84..0000000 --- a/003_macros/mv_getclients.sas +++ /dev/null @@ -1,99 +0,0 @@ -/*** HELP START ***//** - @file mv_getclients.sas - @brief Get a list of Viya Clients - @details First, be sure you have an access token (which requires an app token). - - Using the macros here: - - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - An administrator needs to set you up with an access code: - - %mv_registerclient(outds=client) - - Navigate to the url from the log (opting in to the groups) and paste the - access code below: - - %mv_tokenauth(inds=client,code=wKDZYTEPK6) - - Now we can run the macro! - - %mv_getclients() - - @param [out] outds= (work.mv_getclients) - The library.dataset to be created that contains the list of clients - - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - @li mf_loc.sas - -**//*** HELP END ***/ - -%macro mv_getclients(outds=work.mv_getclients -)/*/STORE SOURCE*/; - -options noquotelenmax; -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -/* first, get consul token needed to get client id / secret */ -data _null_; - infile "%mf_loc(VIYACONFIG)/etc/SASSecurityCertificateFramework/tokens/consul/default/client.token"; - input token:$64.; - call symputx('consul_token',token); -run; - -/* request the client details */ -%local fname1; -%let fname1=%mf_getuniquefileref(); -proc http method='POST' out=&fname1 - url="&base_uri/SASLogon/oauth/clients/consul?callback=false%str(&)serviceId=app"; - headers "X-Consul-Token"="&consul_token"; -run; - -%local libref1; -%let libref1=%mf_getuniquelibref(); -libname &libref1 JSON fileref=&fname1; - -/* extract the token */ -data _null_; - set &libref1..root; - call symputx('access_token',access_token,'l'); -run; - -/* fetching folder details for provided path */ -%local fname2; -%let fname2=%mf_getuniquefileref(); -%let libref2=%mf_getuniquelibref(); - -proc http method='GET' out=&fname2 oauth_bearer=sas_services - url="&base_uri/SASLogon/oauth/clients"; - headers "Accept"="application/json"; -run; -/*data _null_;infile &fname1;input;putlog _infile_;run;*/ -%mp_abort(iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200) - ,mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) -) -libname &libref2 JSON fileref=&fname1; - -data &outds; - set &libref2..items; -run; - - - -/* clear refs -filename &fname1 clear; -libname &libref1 clear; -*/ -%mend mv_getclients; diff --git a/003_macros/mv_getfoldermembers.sas b/003_macros/mv_getfoldermembers.sas deleted file mode 100644 index 9bc91e2..0000000 --- a/003_macros/mv_getfoldermembers.sas +++ /dev/null @@ -1,140 +0,0 @@ -/*** HELP START ***//** - @file - @brief Gets a list of folder members (and ids) for a given root - @details Returns all members for a particular Viya folder. Works at both root - level and below, and results are created in an output dataset. - - %mv_getfoldermembers(root=/Public, outds=work.mymembers) - - - @param [in] root= (/) The path for which to return the list of folders - @param [out] outds= (work.mv_getfolders) The output dataset to create. Format: - |ordinal_root|ordinal_items|creationTimeStamp| modifiedTimeStamp|createdBy|modifiedBy|id| uri|added| type|name|description| - |---|---|---|---|---|---|---|---|---|---|---|---| - |1|1|2021-05-25T11:15:04.204Z|2021-05-25T11:15:04.204Z|allbow|allbow|4f1e3945-9655-462b-90f2-c31534b3ca47|/folders/folders/ed701ff3-77e8-468d-a4f5-8c43dec0fd9e|2021-05-25T11:15:04.212Z|child|my_folder_name|My folder Description| - - @param [in] access_token_var= (ACCESS_TOKEN) The global macro variable to - contain the access token - @param [in] grant_type= (sas_services) Valid values are: - @li password - @li authorization_code - @li detect - @li sas_services - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - @li mf_isblank.sas - -

Related Macros

- @li mv_createfolder.sas - @li mv_deletefoldermember.sas - @li mv_deleteviyafolder.sas - @li mv_getfoldermembers.test.sas - -**//*** HELP END ***/ - -%macro mv_getfoldermembers(root=/ - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ,outds=mv_getfolders - ); -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; - -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) - -%if %mf_isblank(&root)=1 %then %let root=/; - -options noquotelenmax; - -/* request the client details */ -%local fname1 libref1; -%let fname1=%mf_getuniquefileref(); -%let libref1=%mf_getuniquelibref(); - -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -%if "&root"="/" %then %do; - /* if root just list root folders */ - proc http method='GET' out=&fname1 &oauth_bearer - url="&base_uri/folders/rootFolders?limit=1000"; - %if &grant_type=authorization_code %then %do; - headers "Authorization"="Bearer &&&access_token_var"; - %end; - run; - libname &libref1 JSON fileref=&fname1; - data &outds; - set &libref1..items; - run; -%end; -%else %do; - /* first get parent folder id */ - proc http method='GET' out=&fname1 &oauth_bearer - url="&base_uri/folders/folders/@item?path=&root"; - %if &grant_type=authorization_code %then %do; - headers "Authorization"="Bearer &&&access_token_var"; - %end; - run; - /*data _null_;infile &fname1;input;putlog _infile_;run;*/ - libname &libref1 JSON fileref=&fname1; - /* now get the followon link to list members */ - %local href cnt; - %let cnt=0; - data _null_; - length rel href $512; - call missing(rel,href); - set &libref1..links; - if rel='members' then do; - url=cats("'","&base_uri",href,"?limit=10000'"); - call symputx('href',url,'l'); - call symputx('cnt',1,'l'); - end; - run; - %if &cnt=0 %then %do; - %put NOTE:;%put NOTE- No members found in &root!!;%put NOTE-; - %return; - %end; - %local fname2 libref2; - %let fname2=%mf_getuniquefileref(); - %let libref2=%mf_getuniquelibref(); - proc http method='GET' out=&fname2 &oauth_bearer - url=%unquote(%superq(href)); - %if &grant_type=authorization_code %then %do; - headers "Authorization"="Bearer &&&access_token_var"; - %end; - run; - libname &libref2 JSON fileref=&fname2; - data &outds; - length id $36 name $128 uri $64 type $32 description $256; - if _n_=1 then call missing (of _all_); - set &libref2..items; - run; - filename &fname2 clear; - libname &libref2 clear; -%end; - - -/* clear refs */ -filename &fname1 clear; -libname &libref1 clear; - -%mend mv_getfoldermembers; diff --git a/003_macros/mv_getgroupmembers.sas b/003_macros/mv_getgroupmembers.sas deleted file mode 100644 index f0e7888..0000000 --- a/003_macros/mv_getgroupmembers.sas +++ /dev/null @@ -1,109 +0,0 @@ -/*** HELP START ***//** - @file mv_getgroupmembers.sas - @brief Creates a dataset with a list of group members - @details First, be sure you have an access token (which requires an app token) - - Using the macros here: - - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - Now we can run the macro! - - %mv_getgroupmembers(All Users) - - outputs: - - ordinal_root num, - ordinal_items num, - version num, - id char(43), - name char(43), - providerId char(5), - implicit num - - @param [in] group Group id for which to return group members - @param [in] access_token_var= (ACCESS_TOKEN) - The global macro variable to contain the access token - @param [in] grant_type= (sas_services) - valid values are "password" or "authorization_code" (unquoted). - @param [out] outds= (work.viyagroupmembers) - The library.dataset to be created that contains the list of group members - - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - -**//*** HELP END ***/ - -%macro mv_getgroupmembers(group - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ,outds=work.viyagroupmembers - ); -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; - -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) - -options noquotelenmax; - -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -/* fetching folder details for provided path */ -%local fname1; -%let fname1=%mf_getuniquefileref(); -proc http method='GET' out=&fname1 &oauth_bearer - url="&base_uri/identities/groups/&group/members?limit=10000"; - headers - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; - "Accept"="application/json"; -run; -/*data _null_;infile &fname1;input;putlog _infile_;run;*/ -%if &SYS_PROCHTTP_STATUS_CODE=404 %then %do; - %put NOTE: Group &group not found!!; - data &outds; - length id name $43; - call missing(of _all_); - run; -%end; -%else %do; - %mp_abort(iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200) - ,mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) - ) - %let libref1=%mf_getuniquelibref(); - libname &libref1 JSON fileref=&fname1; - data &outds; - length id name $43; - set &libref1..items; - run; - libname &libref1 clear; -%end; - -/* clear refs */ -filename &fname1 clear; - -%mend mv_getgroupmembers; diff --git a/003_macros/mv_getgroups.sas b/003_macros/mv_getgroups.sas deleted file mode 100644 index ad7ff12..0000000 --- a/003_macros/mv_getgroups.sas +++ /dev/null @@ -1,86 +0,0 @@ -/*** HELP START ***//** - @file mv_getgroups.sas - @brief Creates a dataset with a list of viya groups - @details First, load the macros: - - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - Next, execute: - - %mv_getgroups(outds=work.groups) - - @param [in] access_token_var= (ACCESS_TOKEN) - The global macro variable to contain the access token - @param [in] grant_type= (sas_services) - valid values are "password" or "authorization_code" (unquoted). - @param [out] outds= (work.viyagroups) - The library.dataset to be created that contains the list of groups - - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - -**//*** HELP END ***/ - -%macro mv_getgroups(access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ,outds=work.viyagroups - ); -%local oauth_bearer base_uri fname1 libref1; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; - -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) - -options noquotelenmax; -/* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -/* fetching folder details for provided path */ -%let fname1=%mf_getuniquefileref(); -%let libref1=%mf_getuniquelibref(); - -proc http method='GET' out=&fname1 &oauth_bearer - url="&base_uri/identities/groups?limit=10000"; - headers - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; - "Accept"="application/json"; -run; -/*data _null_;infile &fname1;input;putlog _infile_;run;*/ -%mp_abort(iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200) - ,mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) -) -libname &libref1 JSON fileref=&fname1; - -data &outds; - set &libref1..items; -run; - - -/* clear refs */ -filename &fname1 clear; -libname &libref1 clear; - -%mend mv_getgroups; diff --git a/003_macros/mv_getjobcode.sas b/003_macros/mv_getjobcode.sas deleted file mode 100644 index 9f21414..0000000 --- a/003_macros/mv_getjobcode.sas +++ /dev/null @@ -1,230 +0,0 @@ -/*** HELP START ***//** - @file - @brief Extract the source code from a SAS Viya Job - @details Extracts the SAS code from a Job into a fileref or physical file. - Example: - - %mv_getjobcode( - path=/Public/jobs - ,name=some_job - ,outfile=/tmp/some_job.sas - ) - - @param [in] access_token_var= The global macro variable to contain the access - token - @param [in] grant_type= valid values: - @li password - @liauthorization_code - @li detect - will check if access_token exists, if not will use sas_services - if a SASStudioV session else authorization_code. Default option. - @li sas_services - will use oauth_bearer=sas_services - @param [in] path= The SAS Drive path of the job - @param [in] name= The name of the job - @param [in] mdebug=(0) set to 1 to enable DEBUG messages - @param [out] outref=(0) A fileref to which to write the source code (will be - created with a TEMP engine) - @param [out] outfile=(0) A file to which to write the source code - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mv_getfoldermembers.sas - -**//*** HELP END ***/ - -%macro mv_getjobcode(outref=0,outfile=0 - ,name=0,path=0 - ,contextName=SAS Job Execution compute context - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ,mdebug=0 - ); -%local dbg bufsize varcnt fname1 fname2 errmsg; -%if &mdebug=1 %then %do; - %put &sysmacroname local entry vars:; - %put _local_; -%end; -%else %let dbg=*; - -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) -%mp_abort(iftrue=("&path"="0") - ,mac=&sysmacroname - ,msg=%str(Job Path not provided) -) -%mp_abort(iftrue=("&name"="0") - ,mac=&sysmacroname - ,msg=%str(Job Name not provided) -) -%mp_abort(iftrue=("&outfile"="0" and "&outref"="0") - ,mac=&sysmacroname - ,msg=%str(Output destination (file or fileref) must be provided) -) -options noquotelenmax; -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); -data;run; -%local foldermembers; -%let foldermembers=&syslast; -%mv_getfoldermembers(root=&path - ,access_token_var=&access_token_var - ,grant_type=&grant_type - ,outds=&foldermembers -) -%local joburi; -%let joburi=0; -data _null_; - length name uri $512; - call missing(name,uri); - set &foldermembers; - if name="&name" and uri=:'/jobDefinitions/definitions' - then call symputx('joburi',uri); -run; -%mp_abort(iftrue=("&joburi"="0") - ,mac=&sysmacroname - ,msg=%str(Job &path/&name not found) -) - -/* prepare request*/ -%let fname1=%mf_getuniquefileref(); -proc http method='GET' out=&fname1 &oauth_bearer - url="&base_uri&joburi"; - headers "Accept"="application/vnd.sas.job.definition+json" - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; - ; -run; - -%if &mdebug=1 %then %do; - data _null_; - infile &fname1; - input; - putlog _infile_; - run; -%end; - -%mp_abort( - iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201) - ,mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) -) - -%let fname2=%mf_getuniquefileref(); -filename &fname2 temp ; - -/* cannot use lua IO package as not available in Viya 4 */ -/* so use data step to read the JSON until the string `"code":"` is found */ -data _null_; - file &fname2 recfm=n; - infile &fname1 lrecl=1 recfm=n; - input sourcechar $char1. @@; - format sourcechar hex2.; - retain startwrite 0; - if startwrite=0 and sourcechar='"' then do; - reentry: - input sourcechar $ 1. @@; - if sourcechar='c' then do; - reentry2: - input sourcechar $ 1. @@; - if sourcechar='o' then do; - input sourcechar $ 1. @@; - if sourcechar='d' then do; - input sourcechar $ 1. @@; - if sourcechar='e' then do; - input sourcechar $ 1. @@; - if sourcechar='"' then do; - input sourcechar $ 1. @@; - if sourcechar=':' then do; - input sourcechar $ 1. @@; - if sourcechar='"' then do; - putlog 'code found'; - startwrite=1; - input sourcechar $ 1. @@; - end; - end; - else if sourcechar='c' then goto reentry2; - end; - end; - else if sourcechar='"' then goto reentry; - end; - else if sourcechar='"' then goto reentry; - end; - else if sourcechar='"' then goto reentry; - end; - else if sourcechar='"' then goto reentry; - end; - /* once the `"code":"` string is found, write until unescaped `"` is found */ - if startwrite=1 then do; - if sourcechar='\' then do; - input sourcechar $ 1. @@; - if sourcechar in ('"','\') then put sourcechar char1.; - else if sourcechar='n' then put '0A'x; - else if sourcechar='r' then put '0D'x; - else if sourcechar='t' then put '09'x; - else if sourcechar='u' then do; - length uni $4; - input uni $ 4. @@; - sourcechar=unicode('\u'!!uni); - put sourcechar char1.; - end; - else do; - call symputx('errmsg',"Uncaught escape char: "!!sourcechar,'l'); - call symputx('syscc',99); - stop; - end; - end; - else if sourcechar='"' then stop; - else put sourcechar char1.; - end; -run; - -%mp_abort(iftrue=("&syscc"="99") - ,mac=mv_getjobcode - ,msg=%str(&errmsg) -) - -/* export to desired destination */ -%if "&outref"="0" %then %do; - data _null_; - file "&outfile" lrecl=32767; -%end; -%else %do; - filename &outref temp; - data _null_; - file &outref; -%end; - infile &fname2; - input; - put _infile_; - &dbg. putlog _infile_; -run; - -%if &mdebug=1 %then %do; - %put &sysmacroname exit vars:; - %put _local_; -%end; -%else %do; - /* clear refs */ - filename &fname1 clear; - filename &fname2 clear; -%end; - -%mend mv_getjobcode; diff --git a/003_macros/mv_getjoblog.sas b/003_macros/mv_getjoblog.sas deleted file mode 100644 index 40e8cc4..0000000 --- a/003_macros/mv_getjoblog.sas +++ /dev/null @@ -1,290 +0,0 @@ -/*** HELP START ***//** - @file - @brief Extract the log from a completed SAS Viya Job - @details Extracts log from a Viya job and writes it out to a fileref. - - To query the job, you need the URI. Sample code for achieving this - is provided below. - - ## Example - - %* First, compile the macros; - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - %* Next, create a job (in this case, a web service); - filename ft15f001 temp; - parmcards4; - data ; - rand=ranuni(0)*1000; - do x=1 to rand; - y=rand*4; - output; - end; - run; - proc sort data=&syslast - by descending y; - run; - ;;;; - %mv_createwebservice(path=/Public/temp,name=demo) - - %* Execute it; - %mv_jobexecute(path=/Public/temp - ,name=demo - ,outds=work.info - ) - - %* Wait for it to finish; - data work.info; - set work.info; - where method='GET' and rel='state'; - run; - %mv_jobwaitfor(ALL,inds=work.info,outds=work.jobstates) - - %* and grab the uri; - data _null_; - set work.jobstates; - call symputx('uri',uri); - run; - - %* Finally, fetch the log; - %mv_getjoblog(uri=&uri,outref=mylog) - - This macro is used by the mv_jobwaitfor.sas macro, which is generally a more - convenient way to wait for the job to finish before fetching the log. - - If the remote session calls `endsas` then it is not possible to get the log - from the provided uri, and so the log from the parent session is fetched - instead. This happens for a 400 response, eg below: - - ErrorResponse[version=2,status=400,err=5113,id=,message=The session - requested is currently in a failed or stopped state.,detail=[path: - /compute/sessions/LONGURI-ses0006/jobs/LONGURI/log/content, traceId: 63 - 51aa617d01fd2b],remediation=Correct the errors in the session request, - and create a new session.,targetUri=,errors=[],links=[]] - - @param [in] access_token_var= The global macro variable to contain the access - token - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - @param [in] grant_type= valid values: - @li password - @li authorization_code - @li detect - will check if access_token exists, if not will use sas_services - if a SASStudioV session else authorization_code. Default option. - @li sas_services - will use oauth_bearer=sas_services. - @param [in] uri= The uri of the running job for which to fetch the status, - in the format `/jobExecution/jobs/$UUID` (unquoted). - @param [out] outref= The output fileref to which to APPEND the log (is always - appended). - - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mf_getplatform.sas - @li mf_existfileref.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - -**//*** HELP END ***/ - -%macro mv_getjoblog(uri=0,outref=0 - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ,mdebug=0 - ); -%local dbg libref1 libref2 loglocation fname1 fname2; -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; -%end; -%else %let dbg=*; - -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; - -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) - -/* validation in datastep for better character safety */ -%local errmsg errflg; -data _null_; - uri=symget('uri'); - if length(uri)<12 then do; - call symputx('errflg',1); - call symputx('errmsg',"URI is invalid (too short) - '&uri'",'l'); - end; - if scan(uri,-1)='state' or scan(uri,1) ne 'jobExecution' then do; - call symputx('errflg',1); - call symputx('errmsg', - "URI should be in format /jobExecution/jobs/$$$$UUID$$$$" - !!" but is actually like:"!!uri,'l'); - end; -run; - -%mp_abort(iftrue=(&errflg=1) - ,mac=&sysmacroname - ,msg=%str(&errmsg) -) - -%mp_abort(iftrue=(&outref=0) - ,mac=&sysmacroname - ,msg=%str(Output fileref should be provided) -) - -%if %mf_existfileref(&outref) ne 1 %then %do; - filename &outref temp; -%end; - -options noquotelenmax; -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -/* prepare request*/ -%let fname1=%mf_getuniquefileref(); -%let fname2=%mf_getuniquefileref(); -proc http method='GET' out=&fname1 &oauth_bearer - url="&base_uri&uri"; - headers - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; - ; -run; -%if &mdebug=1 %then %do; - %put &sysmacroname: fetching log loc from &uri; - data _null_;infile &fname1;input;putlog _infile_;run; -%end; -%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then -%do; - data _null_;infile &fname1;input;putlog _infile_;run; - %mp_abort(mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) - ) -%end; - -%let libref1=%mf_getuniquelibref(); -libname &libref1 JSON fileref=&fname1; -data _null_; - set &libref1..root; - call symputx('loglocation',loglocation,'l'); -run; - -/* validate log path*/ -%let errflg=1; -%let errmsg=No loglocation entry in &fname1 fileref; -data _null_; - uri=symget('loglocation'); - if length(uri)<12 then do; - call symputx('errflg',1); - call symputx('errmsg',"URI is invalid (too short) - '&uri'",'l'); - end; - else if (scan(uri,1,'/') ne 'compute' or scan(uri,2,'/') ne 'sessions') - and (scan(uri,1,'/') ne 'files' or scan(uri,2,'/') ne 'files') - then do; - call symputx('errflg',1); - call symputx('errmsg', - "URI should be in format /compute/sessions/$$$$UUID$$$$/jobs/$$$$UUID$$$$" - !!" or /files/files/$$$$UUID$$$$" - !!" but is actually like:"!!uri,'l'); - end; - else do; - call symputx('errflg',0,'l'); - call symputx('logloc',uri,'l'); - end; -run; - -%mp_abort(iftrue=(%str(&errflg)=1) - ,mac=&sysmacroname - ,msg=%str(&errmsg) -) - -/* we have a log uri - now fetch the log */ -%&dbg.put &sysmacroname: querying &base_uri&logloc/content; -proc http method='GET' out=&fname2 &oauth_bearer - url="&base_uri&logloc/content?limit=10000"; - headers - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; - ; -run; - -%if &mdebug=1 %then %do; - %put &sysmacroname: fetching log content from &base_uri&logloc/content; - data _null_;infile &fname2;input;putlog _infile_;run; -%end; - -%if &SYS_PROCHTTP_STATUS_CODE=400 %then %do; - /* fetch log from parent session */ - %let logloc=%substr(&logloc,1,%index(&logloc,%str(/jobs/))-1); - %&dbg.put &sysmacroname: Now querying &base_uri&logloc/log/content; - proc http method='GET' out=&fname2 &oauth_bearer - url="&base_uri&logloc/log/content?limit=10000"; - headers - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; - ; - run; - %if &mdebug=1 %then %do; - %put &sysmacroname: fetching log content from &base_uri&logloc/log/content; - data _null_;infile &fname2;input;putlog _infile_;run; - %end; -%end; - -%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201 -%then %do; - %if &mdebug ne 1 %then %do; /* have already output above */ - data _null_;infile &fname2;input;putlog _infile_;run; - %end; - %mp_abort(mac=&sysmacroname - ,msg=%str(logfetch: &SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) - ) -%end; - -%let libref2=%mf_getuniquelibref(); -libname &libref2 JSON fileref=&fname2; -data _null_; - file &outref mod; - if _n_=1 then do; - put "/** SASJS Viya Job Log Extract start: &uri **/"; - end; - set &libref2..items end=last; - %if &mdebug=1 %then %do; - putlog line; - %end; - put line; - if last then do; - put "/** SASJS Viya Job Log Extract end: &uri **/"; - end; -run; - -%if &mdebug=0 %then %do; - filename &fname1 clear; - filename &fname2 clear; - libname &libref1 clear; - libname &libref2 clear; -%end; -%else %do; - %put &sysmacroname exit vars:; - %put _local_; -%end; -%mend mv_getjoblog; - - - diff --git a/003_macros/mv_getjobresult.sas b/003_macros/mv_getjobresult.sas deleted file mode 100644 index a830482..0000000 --- a/003_macros/mv_getjobresult.sas +++ /dev/null @@ -1,232 +0,0 @@ -/*** HELP START ***//** - @file - @brief Extract the result from a completed SAS Viya Job - @details Extracts result from a Viya job and writes it out to a fileref - and/or a JSON-engine library. - - To query the job, you need the URI. Sample code for achieving this - is provided below. - - ## Example - - First, compile the macros: - - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - Next, create a job (in this case, a web service): - - filename ft15f001 temp; - parmcards4; - data test; - rand=ranuni(0)*1000; - do x=1 to rand; - y=rand*4; - output; - end; - run; - proc sort data=&syslast - by descending y; - run; - %webout(OPEN) - %webout(OBJ, test) - %webout(CLOSE) - ;;;; - %mv_createwebservice(path=/Public/temp,name=demo) - - Execute it: - - %mv_jobexecute(path=/Public/temp - ,name=demo - ,outds=work.info - ) - - Wait for it to finish, and grab the uri: - - data _null_; - set work.info; - if method='GET' and rel='self'; - call symputx('uri',uri); - run; - - Finally, fetch the result (In this case, WEBOUT): - - %mv_getjobresult(uri=&uri,result=WEBOUT_JSON,outref=myweb,outlib=myweblib) - - - @param [in] access_token_var= The global macro variable containing the access - token - @param [in] mdebug= set to 1 to enable DEBUG messages - @param [in] grant_type= valid values: - @li password - @li authorization_code - @li detect - will check if access_token exists, if not will use sas_services - if a SASStudioV session else authorization_code. Default option. - @li sas_services - will use oauth_bearer=sas_services. - @param [in] uri= The uri of the running job for which to fetch the status, - in the format `/jobExecution/jobs/$UUID` (unquoted). - - @param [out] result= (WEBOUT_JSON) The result type to capture. Resolves - to "_[column name]" from the results table when parsed with the JSON libname - engine. Example values: - @li WEBOUT_JSON - @li WEBOUT_TXT - - @param [out] outref= (0) The output fileref to which to write the results - @param [out] outlib= (0) The output library to which to assign the results - (assumes the data is in JSON format) - - - @version VIYA V.03.05 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mp_binarycopy.sas - @li mf_getplatform.sas - @li mf_existfileref.sas - -**//*** HELP END ***/ - -%macro mv_getjobresult(uri=0 - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ,mdebug=0 - ,result=WEBOUT_JSON - ,outref=0 - ,outlib=0 - ); -%local dbg; -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; -%end; -%else %let dbg=*; - -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; - -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) - - -/* validation in datastep for better character safety */ -%local errmsg errflg; -data _null_; - uri=symget('uri'); - if length(uri)<12 then do; - call symputx('errflg',1); - call symputx('errmsg',"URI is invalid (too short) - '&uri'",'l'); - end; - if scan(uri,-1)='state' or scan(uri,1) ne 'jobExecution' then do; - call symputx('errflg',1); - call symputx('errmsg', - "URI should be in format /jobExecution/jobs/$$$$UUID$$$$" - !!" but is actually like: &uri",'l'); - end; -run; - -%mp_abort(iftrue=(&errflg=1) - ,mac=&sysmacroname - ,msg=%str(&errmsg) -) - -%if &outref ne 0 and %mf_existfileref(&outref) ne 1 %then %do; - filename &outref temp; -%end; - -options noquotelenmax; -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -/* fetch job info */ -%local fname1; -%let fname1=%mf_getuniquefileref(); -proc http method='GET' out=&fname1 &oauth_bearer - url="&base_uri&uri"; - headers "Accept"="application/json" - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; - ; -run; -%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then -%do; - data _null_;infile &fname1;input;putlog _infile_;run; - %mp_abort(mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) - ) -%end; -%if &mdebug=1 %then %do; - data _null_; - infile &fname1 lrecl=32767; - input; - putlog _infile_; - run; -%end; - -/* extract results link */ -%local lib1 resuri; -%let lib1=%mf_getuniquelibref(); -libname &lib1 JSON fileref=&fname1; -data _null_; - set &lib1..results; - call symputx('resuri',_&result,'l'); - &dbg putlog "&sysmacroname results: " (_all_)(=); -run; -%mp_abort(iftrue=("&resuri"=".") - ,mac=&sysmacroname - ,msg=%str(Variable _&result did not exist in the response json) -) - -/* extract results */ -%local fname2; -%let fname2=%mf_getuniquefileref(); -proc http method='GET' out=&fname2 &oauth_bearer - url="&base_uri&resuri/content?limit=10000"; - headers "Accept"="application/json" - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; - ; -run; -%if &mdebug=1 %then %do; - /* send one char at a time as the json can be very wide */ - data _null_; - infile &fname2 recfm=n; - input char $char1. ; - putlog char $char1. @; - run; -%end; - -%if &outref ne 0 %then %do; - filename &outref temp; - %mp_binarycopy(inref=&fname2,outref=&outref) -%end; -%if &outlib ne 0 %then %do; - libname &outlib JSON fileref=&fname2; -%end; - -%if &mdebug=0 %then %do; - filename &fname1 clear; - filename &fname2 clear; - libname &lib1 clear; -%end; -%else %do; - %put &sysmacroname exit vars:; - %put _local_; -%end; - -%mend mv_getjobresult; diff --git a/003_macros/mv_getjobstate.sas b/003_macros/mv_getjobstate.sas deleted file mode 100644 index 18a602a..0000000 --- a/003_macros/mv_getjobstate.sas +++ /dev/null @@ -1,171 +0,0 @@ -/*** HELP START ***//** - @file - @brief Extract the status from a running SAS Viya job - @details Extracts the status from a running job and appends it to an output - dataset with the following structure: - - | uri | state | timestamp | - |---------------------------------------------------------------|---------|--------------------| - | /jobExecution/jobs/5cebd840-2063-42c1-be0c-421ec3e1c175/state | running | 15JAN2021:12:35:08 | - - To query the running job, you need the URI. Sample code for achieving this - is provided below. - - ## Example - - First, compile the macros: - - filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - Next, create a long running job (in this case, a web service): - - filename ft15f001 temp; - parmcards4; - data ; - rand=ranuni(0)*1000; - do x=1 to rand; - y=rand*4; - output; - end; - run; - data _null_; - call sleep(5,1); - run; - ;;;; - %mv_createwebservice(path=/Public/temp,name=demo) - - Execute it, grab the uri, and finally, check the job status: - - %mv_jobexecute(path=/Public/temp - ,name=demo - ,outds=work.info - ) - - data _null_; - set work.info; - if method='GET' and rel='state'; - call symputx('uri',uri); - run; - - %mv_getjobstate(uri=&uri,outds=results) - - You can run this macro as part of a loop to await the final 'completed' - status. - The full list of status values is: - - @li idle - @li pending - @li running - @li canceled - @li completed - @li failed - - If you have one or more jobs that you'd like to wait for completion you can - also use the [mv_jobwaitfor](/mv__jobwaitfor_8sas.html) macro. - - @param [in] access_token_var= (ACCESS_TOKEN) - The global macro variable to contain the access token - @param [in] grant_type= valid values: - @li password - @li authorization_code - @li detect - will check if access_token exists, if not will use sas_services - if a SASStudioV session else authorization_code. - @li sas_services - will use oauth_bearer=sas_services. - @param [in] uri= The uri of the running job for which to fetch the status, - in the format `/jobExecution/jobs/$UUID/state` (unquoted). - @param [out] outds= The output dataset in which to APPEND the status. Three - fields are appended: `CHECK_TM`, `URI` and `STATE`. If the dataset does not - exist, it is created. - - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mf_getplatform.sas - @li mf_getuniquefileref.sas - -**//*** HELP END ***/ - -%macro mv_getjobstate(uri=0,outds=work.mv_getjobstate - ,contextName=SAS Job Execution compute context - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ); -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; - -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) - -/* validation in datastep for better character safety */ -%local errmsg errflg; -data _null_; - uri=symget('uri'); - if length(uri)<12 then do; - call symputx('errflg',1); - call symputx('errmsg',"URI is invalid (too short) - '&uri'",'l'); - end; - if scan(uri,-1) ne 'state' or scan(uri,1) ne 'jobExecution' then do; - - call symputx('errflg',1); - call symputx('errmsg', - "URI should be in format /jobExecution/jobs/$$$$UUID$$$$/state" - !!" but is actually like: &uri",'l'); - end; -run; - -%mp_abort(iftrue=(&errflg=1) - ,mac=&sysmacroname - ,msg=%str(&errmsg) -) - -options noquotelenmax; -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -%local fname0; -%let fname0=%mf_getuniquefileref(); - -proc http method='GET' out=&fname0 &oauth_bearer url="&base_uri/&uri"; - headers "Accept"="text/plain" - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; ; -run; -%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then -%do; - data _null_;infile &fname0;input;putlog _infile_;run; - %mp_abort(mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) - ) -%end; - -data; - format uri $128. state $32. timestamp datetime19.; - infile &fname0; - uri="&uri"; - timestamp=datetime(); - input; - state=_infile_; -run; - -proc append base=&outds data=&syslast; -run; - -filename &fname0 clear; - -%mend mv_getjobstate; diff --git a/003_macros/mv_getusergroups.sas b/003_macros/mv_getusergroups.sas deleted file mode 100644 index 9e5c75c..0000000 --- a/003_macros/mv_getusergroups.sas +++ /dev/null @@ -1,95 +0,0 @@ -/*** HELP START ***//** - @file mv_getusergroups.sas - @brief Creates a dataset with a list of groups for a particular user - @details If using outside of Viya SPRE, then an access token is needed. - - Compile the macros here: - - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - Then run the macro! - - %mv_getusergroups(&sysuserid,outds=users) - - @param [in] user The username for which to return the list of groups - @param [in] access_token_var= (ACCESS_TOKEN) - The global macro variable to contain the access token - @param [in] grant_type= (sas_services) - Valid values are "password" or "authorization_code" (unquoted). - @param [out] outds= (work.mv_getusergroups) - The library.dataset to be created that contains the list of groups - - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - -**//*** HELP END ***/ - -%macro mv_getusergroups(user - ,outds=work.mv_getusergroups - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ); -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; -%put &sysmacroname: grant_type=&grant_type; -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) -options noquotelenmax; - -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -/* fetching folder details for provided path */ -%local fname1; -%let fname1=%mf_getuniquefileref(); -%let libref1=%mf_getuniquelibref(); - -proc http method='GET' out=&fname1 &oauth_bearer - url="&base_uri/identities/users/&user/memberships?limit=10000"; - headers -%if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" -%end; - "Accept"="application/json"; -run; -/*data _null_;infile &fname1;input;putlog _infile_;run;*/ -%if &SYS_PROCHTTP_STATUS_CODE=404 %then %do; - %put NOTE: User &user not found!!; -%end; -%else %do; - %mp_abort(iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200) - ,mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) - ) -%end; -libname &libref1 JSON fileref=&fname1; - -data &outds; - set &libref1..items; -run; - -/* clear refs */ -filename &fname1 clear; -libname &libref1 clear; - -%mend mv_getusergroups; diff --git a/003_macros/mv_getusers.sas b/003_macros/mv_getusers.sas deleted file mode 100644 index 982f981..0000000 --- a/003_macros/mv_getusers.sas +++ /dev/null @@ -1,118 +0,0 @@ -/*** HELP START ***//** - @file mv_getusers.sas - @brief Creates a dataset with a list of users - @details First, be sure you have an access token (which requires an app token) - - Using the macros here: - - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - An administrator needs to set you up with an access code: - - %mv_registerclient(outds=client) - - Navigate to the url from the log (opting in to the groups) and paste the - access code below: - - %mv_tokenauth(inds=client,code=wKDZYTEPK6) - - Now we can run the macro! - - %mv_getusers(outds=users) - - Output (lengths are dynamic): - - ordinal_root num, - ordinal_items num, - version num, - id char(20), - name char(23), - providerId char(4), - type char(4), - creationTimeStamp char(24), - modifiedTimeStamp char(24), - state char(6) - - @param [in] access_token_var= (ACCESS_TOKEN) - The global macro variable to contain the access token - @param [in] grant_type= (sas_services) Valid values: - * password - * authorization_code - * detect - will check if access_token exists, if not will use sas_services - if a SASStudioV session else authorization_code. - * sas_services - will use oauth_bearer=sas_services - - @param [out] outds= (work.mv_getusers) - The library.dataset to be created that contains the list of groups - - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - -**//*** HELP END ***/ - -%macro mv_getusers(outds=work.mv_getusers - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ); -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; -%put &sysmacroname: grant_type=&grant_type; -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) - -options noquotelenmax; - -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -/* fetching folder details for provided path */ -%local fname1; -%let fname1=%mf_getuniquefileref(); -%let libref1=%mf_getuniquelibref(); - -proc http method='GET' out=&fname1 &oauth_bearer - url="&base_uri/identities/users?limit=10000"; -%if &grant_type=authorization_code %then %do; - headers "Authorization"="Bearer &&&access_token_var" - "Accept"="application/json"; -%end; -%else %do; - headers "Accept"="application/json"; -%end; -run; -/*data _null_;infile &fname1;input;putlog _infile_;run;*/ -%mp_abort(iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200) - ,mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) -) -libname &libref1 JSON fileref=&fname1; - -data &outds; - set &libref1..items; -run; - -/* clear refs */ -filename &fname1 clear; -libname &libref1 clear; - -%mend mv_getusers; diff --git a/003_macros/mv_jobexecute.sas b/003_macros/mv_jobexecute.sas deleted file mode 100644 index 8515012..0000000 --- a/003_macros/mv_jobexecute.sas +++ /dev/null @@ -1,188 +0,0 @@ -/*** HELP START ***//** - @file - @brief Executes a SAS Viya Job - @details Triggers a SAS Viya Job, with optional URL parameters, using - the JES web app. - - First, compile the macros: - - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - Then, execute the job! - - %mv_jobexecute(path=/Public/folder - ,name=somejob - ) - - Example with parameters: - - %mv_jobexecute(path=/Public/folder - ,name=somejob - ,paramstring=%str("macvarname":"macvarvalue","answer":42) - ) - - @param [in] access_token_var= The global macro variable to contain the access - token - @param [in] grant_type= valid values: - @li password - @li authorization_code - @li detect - will check if access_token exists, if not will use sas_services - if a SASStudioV session else authorization_code. Default option. - @li sas_services - will use oauth_bearer=sas_services - - @param [in] path= The SAS Drive path to the job being executed - @param [in] name= The name of the job to execute - @param [in] paramstring= A JSON fragment with name:value pairs, eg: - `"name":"value"` or "name":"value","name2":42`. This will need to be - wrapped in `%str()`. - - @param [in] contextName= Context name with which to run the job. - Default = `SAS Job Execution compute context` - @param [in] mdebug= set to 1 to enable DEBUG messages - @param [out] outds= (work.mv_jobexecute) The output dataset containing links - - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - @li mv_getfoldermembers.sas - -**//*** HELP END ***/ - -%macro mv_jobexecute(path=0 - ,name=0 - ,contextName=SAS Job Execution compute context - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ,paramstring=0 - ,outds=work.mv_jobexecute - ,mdebug=0 - ); -%local dbg; -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; -%end; -%else %let dbg=*; - -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; - -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) - -%mp_abort(iftrue=("&path"="0") - ,mac=&sysmacroname - ,msg=%str(Path not provided) -) -%mp_abort(iftrue=("&name"="0") - ,mac=&sysmacroname - ,msg=%str(Job Name not provided) -) - -options noquotelenmax; - -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -data;run; -%local foldermembers; -%let foldermembers=&syslast; -%mv_getfoldermembers(root=&path - ,access_token_var=&access_token_var - ,grant_type=&grant_type - ,outds=&foldermembers -) - -%local joburi; -%let joburi=0; -data _null_; - length name uri $512; - call missing(name,uri); - set &foldermembers; - if name="&name" and uri=:'/jobDefinitions/definitions' - then call symputx('joburi',uri); -run; - -%mp_abort(iftrue=("&joburi"="0") - ,mac=&sysmacroname - ,msg=%str(Job &path/&name not found) -) - -/* prepare request*/ -%local fname0 fname1; -%let fname0=%mf_getuniquefileref(); -%let fname1=%mf_getuniquefileref(); - -data _null_; - file &fname0; - length joburi contextname $128 paramstring $32765; - joburi=quote(trim(symget('joburi'))); - contextname=quote(trim(symget('contextname'))); - _program=quote("&path/&name"); - paramstring=symget('paramstring'); - put '{"jobDefinitionUri":' joburi ; - put ' ,"arguments":{"_contextName":' contextname; - put ' ,"_program":' _program; - if paramstring ne "0" then do; - put ' ,' paramstring; - end; - put '}}'; -run; - -proc http method='POST' in=&fname0 out=&fname1 &oauth_bearer - url="&base_uri/jobExecution/jobs"; - headers "Content-Type"="application/vnd.sas.job.execution.job.request+json" - "Accept"="application/vnd.sas.job.execution.job+json" - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; - ; -run; -%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then -%do; - data _null_;infile &fname0;input;putlog _infile_;run; - data _null_;infile &fname1;input;putlog _infile_;run; - %mp_abort(mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) - ) -%end; - -%local libref; -%let libref=%mf_getuniquelibref(); -libname &libref JSON fileref=&fname1; - -data &outds; - set &libref..links; - _program="&path/&name"; -run; - -%if &mdebug=1 %then %do; - %put &sysmacroname exit vars:; - %put _local_; -%end; -%else %do; - /* clear refs */ - filename &fname0 clear; - filename &fname1 clear; - libname &libref; -%end; -%mend mv_jobexecute; diff --git a/003_macros/mv_jobflow.sas b/003_macros/mv_jobflow.sas deleted file mode 100644 index 7de858d..0000000 --- a/003_macros/mv_jobflow.sas +++ /dev/null @@ -1,395 +0,0 @@ -/*** HELP START ***//** - @file - @brief Execute a series of job flows - @details Very (very) simple flow manager. Jobs execute in sequential waves, - all previous waves must finish successfully. - - The input table is formed as per below. Each observation represents one job. - Each variable is converted into a macro variable with the same name. - - ## Input table (minimum variables needed) - - @li _PROGRAM - Provides the path to the job itself - @li FLOW_ID - Numeric value, provides sequential ordering capability. Is - optional, will default to 0 if not provided. - @li _CONTEXTNAME - Dictates which context should be used to run the job. If - blank, or not provided, will default to `SAS Job Execution compute context`. - - Any additional variables provided in this table are converted into macro - variables and passed into the relevant job. - - |_PROGRAM| FLOW_ID (optional)| _CONTEXTNAME (optional) | - |---|---|---| - |/Public/jobs/somejob1|0|SAS Job Execution compute context| - |/Public/jobs/somejob2|0|SAS Job Execution compute context| - - ## Output table (minimum variables produced) - - @li _PROGRAM - the SAS Drive path of the job - @li URI - the URI of the executed job - @li STATE - the completed state of the job - @li TIMESTAMP - the datetime that the job completed - @li JOBPARAMS - the parameters that were passed to the job - @li FLOW_ID - the id of the flow in which the job was executed - - ![https://i.imgur.com/nZE9PvT.png](https://i.imgur.com/nZE9PvT.png) - - To avoid hammering the box with many hits in rapid succession, a one - second pause is made between every request. - - - ## Example - - First, compile the macros: - - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - Next, create some jobs (in this case, as web services): - - filename ft15f001 temp; - parmcards4; - %put this is job: &_program; - %put this was run in flow &flow_id; - data ; - rand=ranuni(0)*¯ovar1; - do x=1 to rand; - y=rand*¯ovar2; - if y=100 then abort; - output; - end; - run; - ;;;; - %mv_createwebservice(path=/Public/temp,name=demo1) - %mv_createwebservice(path=/Public/temp,name=demo2) - - Prepare an input table with 60 executions: - - data work.inputjobs; - _contextName='SAS Job Execution compute context'; - do flow_id=1 to 3; - do i=1 to 20; - _program='/Public/temp/demo1'; - macrovar1=10*i; - macrovar2=4*i; - output; - i+1; - _program='/Public/temp/demo2'; - macrovar1=40*i; - macrovar2=44*i; - output; - end; - end; - run; - - Trigger the flow - - %mv_jobflow(inds=work.inputjobs - ,maxconcurrency=4 - ,outds=work.results - ,outref=myjoblog - ) - - data _null_; - infile myjoblog; - input; put _infile_; - run; - - - @param [in] access_token_var= The global macro variable to contain the - access token - @param [in] grant_type= valid values: - @li password - @li authorization_code - @li detect - will check if access_token exists, if not will use - sas_services if a SASStudioV session else authorization_code. Default - option. - @li sas_services - will use oauth_bearer=sas_services - @param [in] inds= The input dataset containing a list of jobs and parameters - @param [in] maxconcurrency= The max number of parallel jobs to run. Default=8. - @param [in] raise_err=0 Set to 1 to raise SYSCC when a job does not complete - succcessfully - @param [in] mdebug= set to 1 to enable DEBUG messages - @param [out] outds= The output dataset containing the results - @param [out] outref= The output fileref to which to append the log file(s). - - @version VIYA V.03.05 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mf_nobs.sas - @li mp_abort.sas - @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mf_existvarlist.sas - @li mv_jobwaitfor.sas - @li mv_jobexecute.sas - -**//*** HELP END ***/ - -%macro mv_jobflow(inds=0,outds=work.mv_jobflow - ,maxconcurrency=8 - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ,outref=0 - ,raise_err=0 - ,mdebug=0 - ); -%local dbg; -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; - %put inds vars:; - data _null_; - set &inds; - putlog (_all_)(=); - run; -%end; -%else %let dbg=*; - -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; - -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) - -%mp_abort(iftrue=("&inds"="0") - ,mac=&sysmacroname - ,msg=%str(Input dataset was not provided) -) -%mp_abort(iftrue=(%mf_existVarList(&inds,_PROGRAM)=0) - ,mac=&sysmacroname - ,msg=%str(The _PROGRAM column must exist on input dataset &inds) -) -%mp_abort(iftrue=(&maxconcurrency<1) - ,mac=&sysmacroname - ,msg=%str(The maxconcurrency variable should be a positive integer) -) - -/* set defaults if not provided */ -%if %mf_existVarList(&inds,_CONTEXTNAME FLOW_ID)=0 %then %do; - data &inds; - %if %mf_existvarList(&inds,_CONTEXTNAME)=0 %then %do; - length _CONTEXTNAME $128; - retain _CONTEXTNAME "SAS Job Execution compute context"; - %end; - %if %mf_existvarList(&inds,FLOW_ID)=0 %then %do; - retain FLOW_ID 0; - %end; - set &inds; - &dbg. putlog (_all_)(=); - run; -%end; - -%local missings; -proc sql noprint; -select count(*) into: missings - from &inds - where flow_id is null or _program is null; -%mp_abort(iftrue=(&missings>0) - ,mac=&sysmacroname - ,msg=%str(input dataset has &missings missing values for FLOW_ID or _PROGRAM) -) - -%if %mf_nobs(&inds)=0 %then %do; - %put No observations in &inds! Leaving macro &sysmacroname; - %return; -%end; - -/* ensure output table is available */ -data &outds;run; -proc sql; -drop table &outds; - -options noquotelenmax; -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - - -/* get flows */ -proc sort data=&inds; - by flow_id; -run; -data _null_; - set &inds (keep=flow_id) end=last; - by flow_id; - if last.flow_id then do; - cnt+1; - call symputx(cats('flow',cnt),flow_id,'l'); - end; - if last then call symputx('flowcnt',cnt,'l'); -run; - -/* prepare temporary datasets and frefs */ -%local fid jid jds jjson jdsapp jdsrunning jdswaitfor jfref; -data;run;%let jds=&syslast; -data;run;%let jjson=&syslast; -data;run;%let jdsapp=&syslast; -data;run;%let jdsrunning=&syslast; -data;run;%let jdswaitfor=&syslast; -%let jfref=%mf_getuniquefileref(); - -/* start loop */ -%do fid=1 %to &flowcnt; - - %if not ( &raise_err and &syscc ) %then %do; - - %put preparing job attributes for flow &&flow&fid; - %local jds jcnt; - data &jds(drop=_contextName _program); - set &inds(where=(flow_id=&&flow&fid)); - if _contextName='' then _contextName="SAS Job Execution compute context"; - call symputx(cats('job',_n_),_program,'l'); - call symputx(cats('context',_n_),_contextName,'l'); - call symputx('jcnt',_n_,'l'); - &dbg. if _n_= 1 then putlog "Loop &fid"; - &dbg. putlog (_all_)(=); - run; - %put exporting job variables in json format; - %do jid=1 %to &jcnt; - data &jjson; - set &jds; - if _n_=&jid then do; - output; - stop; - end; - run; - proc json out=&jfref; - export &jjson / nosastags fmtnumeric; - run; - data _null_; - infile &jfref lrecl=32767; - input; - jparams=cats('jparams',symget('jid')); - call symputx(jparams,substr(_infile_,3,length(_infile_)-4)); - run; - %local jobuid&jid; - %let jobuid&jid=0; /* used in next loop */ - %end; - %local concurrency completed; - %let concurrency=0; - %let completed=0; - proc sql; drop table &jdsrunning; - %do jid=1 %to &jcnt; - /** - * now we can execute the jobs up to the maxconcurrency setting - */ - %if "&&job&jid" ne "0" %then %do; /* this var is zero if job finished */ - - /* check to see if the job finished in the previous round */ - %if %sysfunc(exist(&outds))=1 %then %do; - %local jobcheck; %let jobcheck=0; - proc sql noprint; - select count(*) into: jobcheck - from &outds where uuid="&&jobuid&jid"; - %if &jobcheck>0 %then %do; - %put &&job&jid in flow &fid with uid &&jobuid&jid completed!; - %let job&jid=0; - %end; - %end; - - /* check if job was triggered and, if - so, if we have enough slots to run? */ - %if ("&&jobuid&jid"="0") and (&concurrency<&maxconcurrency) %then %do; - - /* But only start if no issues detected so far */ - %if not ( &raise_err and &syscc ) %then %do; - - %local jobname jobpath; - %let jobname=%scan(&&job&jid,-1,/); - %let jobpath= - %substr(&&job&jid,1,%length(&&job&jid)-%length(&jobname)-1); - - %put executing &jobpath/&jobname with paramstring &&jparams&jid; - %mv_jobexecute(path=&jobpath - ,name=&jobname - ,paramstring=%superq(jparams&jid) - ,outds=&jdsapp - ,contextname=&&context&jid - ) - data &jdsapp; - format jobparams $32767.; - set &jdsapp(where=(method='GET' and rel='state')); - jobparams=symget("jparams&jid"); - /* uri here has the /state suffix */ - uuid=scan(uri,-2,'/'); - call symputx("jobuid&jid",uuid,'l'); - run; - proc append base=&jdsrunning data=&jdsapp; - run; - %let concurrency=%eval(&concurrency+1); - /* sleep one second after every request to smooth the impact */ - data _null_; - call sleep(1,1); - run; - - %end; - %else %do; /* Job was skipped due to problems */ - - %put jobid &&job&jid in flow &fid skipped due to SYSCC (&syscc); - %let completed = %eval(&completed+1); - %let job&jid=0; /* Indicate job has finished */ - - %end; - - %end; - %end; - %if &jid=&jcnt %then %do; - /* we are at the end of the loop - check which jobs have finished */ - %mv_jobwaitfor(ANY,inds=&jdsrunning,outds=&jdswaitfor,outref=&outref - ,raise_err=&raise_err,mdebug=&mdebug) - %local done; - %let done=%mf_nobs(&jdswaitfor); - %if &done>0 %then %do; - %let completed=%eval(&completed+&done); - %let concurrency=%eval(&concurrency-&done); - data &jdsapp; - set &jdswaitfor; - flow_id=&&flow&fid; - uuid=scan(uri,-1,'/'); - run; - proc append base=&outds data=&jdsapp; - run; - %end; - proc sql; - delete from &jdsrunning - where uuid in (select uuid from &outds - where state in ('canceled','completed','failed') - ); - - /* loop again if jobs are left */ - %if &completed < &jcnt %then %do; - %let jid=0; - %put looping flow &fid again; - %put &completed of &jcnt jobs completed, &concurrency jobs running; - %end; - %end; - %end; - - %end; - %else %do; - - %put Flow &&flow&fid skipped due to SYSCC (&syscc); - - %end; - /* back up and execute the next flow */ -%end; - -%if &mdebug=1 %then %do; - %put &sysmacroname exit vars:; - %put _local_; -%end; - -%mend mv_jobflow; diff --git a/003_macros/mv_jobwaitfor.sas b/003_macros/mv_jobwaitfor.sas deleted file mode 100644 index 21896ed..0000000 --- a/003_macros/mv_jobwaitfor.sas +++ /dev/null @@ -1,263 +0,0 @@ -/*** HELP START ***//** - @file - @brief Takes a table of running jobs and waits for ANY/ALL of them to complete - @details Will poll `/jobs/{jobId}/state` at set intervals until ANY or ALL - jobs are completed. Completion is determined by reference to the returned - _state_, as per the following table: - - | state | Wait? | Notes| - |-----------|-------|------| - | idle | yes | We assume processing will continue. Beware of idle sessions with no code submitted! | - | pending | yes | Job is preparing to run | - | running | yes | Job is running| - | canceled | no | Job was cancelled| - | completed | no | Job finished - does not mean it was successful. Check stateDetails| - | failed | no | Job failed to execute, could be a problem when calling the apis| - - - ## Example - - First, compile the macros: - - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - Next, create a job (in this case, as a web service): - - filename ft15f001 temp; - parmcards4; - data ; - rand=ranuni(0)*1000000; - do x=1 to rand; - y=rand*x; - output; - end; - run; - ;;;; - %mv_createwebservice(path=/Public/temp,name=demo) - - Then, execute the job,multiple times, and wait for them all to finish: - - %mv_jobexecute(path=/Public/temp,name=demo,outds=work.ds1) - %mv_jobexecute(path=/Public/temp,name=demo,outds=work.ds2) - %mv_jobexecute(path=/Public/temp,name=demo,outds=work.ds3) - %mv_jobexecute(path=/Public/temp,name=demo,outds=work.ds4) - - data work.jobs; - set work.ds1 work.ds2 work.ds3 work.ds4; - where method='GET' and rel='state'; - run; - - %mv_jobwaitfor(ALL,inds=work.jobs,outds=work.jobstates) - - Delete the job: - - %mv_deletejes(path=/Public/temp,name=demo) - - @param [in] access_token_var= The global macro variable to contain the access - token - @param [in] grant_type= valid values: - - - password - - authorization_code - - detect - will check if access_token exists, if not will use sas_services - if a SASStudioV session else authorization_code. Default option. - - sas_services - will use oauth_bearer=sas_services - - @param [in] action=Either ALL (to wait for every job) or ANY (if one job - completes, processing will continue). Default=ALL. - @param [in] inds= The input dataset containing the list of job uris, in the - following format: `/jobExecution/jobs/&JOBID./state` and the corresponding - job name. The uri should be in a `uri` variable, and the job path/name - should be in a `_program` variable. - @param [in] raise_err=0 Set to 1 to raise SYSCC when a job does not complete - succcessfully - @param [in] mdebug= set to 1 to enable DEBUG messages - @param [out] outds= The output dataset containing the list of states by job - (default=work.mv_jobexecute) - @param [out] outref= A fileref to which the spawned job logs should be - appended. - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

Dependencies

- @li mp_abort.sas - @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - @li mf_existvar.sas - @li mf_nobs.sas - @li mv_getjoblog.sas - -**//*** HELP END ***/ - -%macro mv_jobwaitfor(action - ,access_token_var=ACCESS_TOKEN - ,grant_type=sas_services - ,inds=0 - ,outds=work.mv_jobwaitfor - ,outref=0 - ,raise_err=0 - ,mdebug=0 - ); -%local dbg; -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; -%end; -%else %let dbg=*; - -%local oauth_bearer; -%if &grant_type=detect %then %do; - %if %symexist(&access_token_var) %then %let grant_type=authorization_code; - %else %let grant_type=sas_services; -%end; -%if &grant_type=sas_services %then %do; - %let oauth_bearer=oauth_bearer=sas_services; - %let &access_token_var=; -%end; - -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password - and &grant_type ne sas_services - ) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) - -%mp_abort(iftrue=("&inds"="0") - ,mac=&sysmacroname - ,msg=%str(input dataset not provided) -) -%mp_abort(iftrue=(%mf_existvar(&inds,uri)=0) - ,mac=&sysmacroname - ,msg=%str(The URI variable was not found in the input dataset(&inds)) -) -%mp_abort(iftrue=(%mf_existvar(&inds,_program)=0) - ,mac=&sysmacroname - ,msg=%str(The _PROGRAM variable was not found in the input dataset(&inds)) -) - -%if %mf_nobs(&inds)=0 %then %do; - %put NOTE: Zero observations in &inds, &sysmacroname will now exit; - %return; -%end; - -options noquotelenmax; -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -data _null_; - length jobparams $32767; - set &inds end=last; - call symputx(cats('joburi',_n_),substr(uri,1,55),'l'); - call symputx(cats('jobname',_n_),_program,'l'); - call symputx(cats('jobparams',_n_),jobparams,'l'); - if last then call symputx('uricnt',_n_,'l'); -run; - -%local runcnt; -%if &action=ALL %then %let runcnt=&uricnt; -%else %if &action=ANY %then %let runcnt=1; -%else %let runcnt=&uricnt; - -%local fname0 ; -%let fname0=%mf_getuniquefileref(); - -data &outds; - format _program uri $128. state $32. stateDetails $32. timestamp datetime19. - jobparams $32767.; - call missing (of _all_); - stop; -run; - -%local i; -%do i=1 %to &uricnt; - %if "&&joburi&i" ne "0" %then %do; - proc http method='GET' out=&fname0 &oauth_bearer url="&base_uri/&&joburi&i"; - headers "Accept"="application/json" - %if &grant_type=authorization_code %then %do; - "Authorization"="Bearer &&&access_token_var" - %end; ; - run; - %if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201 - %then %do; - data _null_;infile &fname0;input;putlog _infile_;run; - %mp_abort(mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) - ) - %end; - - %let status=notset; - - %local libref1; - %let libref1=%mf_getuniquelibref(); - libname &libref1 json fileref=&fname0; - - data _null_; - length state stateDetails $32; - set &libref1..root; - call symputx('status',state,'l'); - call symputx('stateDetails',stateDetails,'l'); - run; - - libname &libref1 clear; - - %if &status=completed or &status=failed or &status=canceled %then %do; - %local plainuri; - %let plainuri=%substr(&&joburi&i,1,55); - proc sql; - insert into &outds set - _program="&&jobname&i", - uri="&plainuri", - state="&status", - stateDetails=symget("stateDetails"), - timestamp=datetime(), - jobparams=symget("jobparams&i"); - %let joburi&i=0; /* do not re-check */ - /* fetch log */ - %if %str(&outref) ne 0 %then %do; - %mv_getjoblog(uri=&plainuri,outref=&outref,mdebug=&mdebug) - %end; - %end; - %else %if &status=idle or &status=pending or &status=running %then %do; - data _null_; - call sleep(1,1); - run; - %end; - %else %do; - %mp_abort(mac=&sysmacroname - ,msg=%str(status &status not expected!!) - ) - %end; - - %if (&raise_err) %then %do; - %if (&status = canceled or &status = failed or %length(&stateDetails)>0) - %then %do; - %if ("&stateDetails" = "%str(war)ning") %then %let SYSCC=4; - %else %let SYSCC=5; - %put %str(ERR)OR: Job &&jobname&i. did not complete. &stateDetails; - %return; - %end; - %end; - - %end; - %if &i=&uricnt %then %do; - %local goback; - %let goback=0; - proc sql noprint; - select count(*) into:goback from &outds; - %if &goback lt &runcnt %then %let i=0; - %end; -%end; - -%if &mdebug=1 %then %do; - %put &sysmacroname exit vars:; - %put _local_; -%end; -%else %do; - /* clear refs */ - filename &fname0 clear; -%end; -%mend mv_jobwaitfor; diff --git a/003_macros/mv_registerclient.sas b/003_macros/mv_registerclient.sas deleted file mode 100644 index 36c7f57..0000000 --- a/003_macros/mv_registerclient.sas +++ /dev/null @@ -1,287 +0,0 @@ -/*** HELP START ***//** - @file mv_registerclient.sas - @brief Register Client and Secret (admin task) - @details When building apps on SAS Viya, a client id and secret are usually - required. In order to generate them, the Consul Token is required. To access - this token, you need to be a system administrator (it is not enough to be in - the SASAdministrator group in SAS Environment Manager). - - If you are registering a lot of clients / secrets, you may find it more - convenient to use the [Viya Token Generator] - (https://sasjs.io/apps/#viya-client-token-generator) (a SASjs Web App to - automate the generation of clients & secrets with various settings). - - For further information on clients / secrets, see; - @li https://developer.sas.com/reference/auth/#register - @li https://proc-x.com/2019/01/authentication-to-sas-viya-a-couple-of-approaches - @li https://cli.sasjs.io/faq/#how-can-i-obtain-a-viya-client-and-secret - - The default viyaroot location is: `/opt/sas/viya/config` - - Usage: - - %* compile macros; - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - %* generate random client details with openid scope; - %mv_registerclient(scopes=openid ) - - %* generate random client using consul token as input parameter; - %mv_registerclient(consul_token=12x34sa43v2345n234lasd) - - %* specific client with just openid scope; - %mv_registerclient(client_id=YourClient - ,client_secret=YourSecret - ,scopes=openid - ) - - %* generate random client with 90/180 second access/refresh token expiry; - %mv_registerclient(scopes=openid - ,access_token_validity=90 - ,refresh_token_validity=180 - ) - - @param [in,out] client_id= The client name. Auto generated if blank. - @param [in,out] client_secret= Client secret. Auto generated if client is - blank. - @param [in] consul_token= (0) Provide the actual consul token value here if - using Viya 4 or above. - @param [in] scopes= (openid) List of space-seperated unquoted scopes - @param [in] grant_type= (authorization_code|refresh_token) Valid values are - "password" or "authorization_code" (unquoted). Pipe seperated. - @param [out] outds=(mv_registerclient) The dataset to contain the registered - client id and secret - @param [in] access_token_validity= (DEFAULT) The access token duration in - seconds. A value of DEFAULT will omit the entry (and use system default) - @param [in] refresh_token_validity= (DEFAULT) The duration of validity of the - refresh token in seconds. A value of DEFAULT will omit the entry (and use - system default) - @param [in] client_name= (DEFAULT) An optional, human readable name for the - client. - @param [in] required_user_groups= A list of group names. If a user does not - belong to all the required groups, the user will not be authenticated and no - tokens are issued to this client for that user. If this field is not - specified, authentication and token issuance proceeds normally. - @param [in] autoapprove= During the auth step the user can choose which scope - to apply. Setting this to true will autoapprove all the client scopes. - @param [in] use_session= If true, access tokens issued to this client will be - associated with an HTTP session and revoked upon logout or time-out. - @param [out] outjson= (_null_) A dataset containing the lines of JSON - submitted. Useful for debugging. - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - @li mf_loc.sas - @li mf_getquotedstr.sas - @li mf_getuser.sas - @li mp_abort.sas - -**//*** HELP END ***/ - -%macro mv_registerclient(client_id= - ,client_secret= - ,consul_token=0 - ,client_name=DEFAULT - ,scopes=openid - ,grant_type=authorization_code|refresh_token - ,required_user_groups= - ,autoapprove= - ,use_session= - ,outds=mv_registerclient - ,access_token_validity=DEFAULT - ,refresh_token_validity=DEFAULT - ,outjson=_null_ - ); -%local fname1 fname2 fname3 libref access_token url tokloc msg; - -%if client_name=DEFAULT %then %let client_name= - Generated by %mf_getuser() (&sysuserid) on %sysfunc(datetime(),datetime19. - ) using SASjs; - -options noquotelenmax; - -%if "&consul_token"="0" %then %do; - /* first, get consul token needed to get client id / secret */ - %let tokloc=/etc/SASSecurityCertificateFramework/tokens/consul/default; - %let tokloc=%mf_loc(VIYACONFIG)&tokloc/client.token; - - %if %sysfunc(fileexist(&tokloc))=0 %then %do; - %let msg=Unable to access the consul token at &tokloc; - %put &sysmacroname: &msg; - %put Try passing the value in the consul= macro parameter; - %put See docs: https://core.sasjs.io/mv__registerclient_8sas.html; - %mp_abort(mac=mv_registerclient,msg=%str(&msg)) - %end; - - data _null_; - infile "&tokloc"; - input token:$64.; - call symputx('consul_token',token); - run; - - %if "&consul_token"="0" %then %do; - %put &sysmacroname: Unable to source the consul token from &tokloc; - %put It seems your account (&sysuserid) does not have admin rights; - %put Please speak with your platform adminstrator; - %put Docs: https://core.sasjs.io/mv__registerclient_8sas.html; - %abort; - %end; -%end; - -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -/* request the client details */ -%let fname1=%mf_getuniquefileref(); -proc http method='POST' out=&fname1 - url="&base_uri/SASLogon/oauth/clients/consul?callback=false%str(&)%trim( - )serviceId=app"; - headers "X-Consul-Token"="&consul_token"; -run; - -%put &=SYS_PROCHTTP_STATUS_CODE; -%put &=SYS_PROCHTTP_STATUS_PHRASE; - -%let libref=%mf_getuniquelibref(); -libname &libref JSON fileref=&fname1; - -/* extract the token */ -data _null_; - set &libref..root; - call symputx('access_token',access_token,'l'); -run; - -/** - * register the new client - */ -%let fname2=%mf_getuniquefileref(); -%if x&client_id.x=xx %then %do; - %let client_id=client_%sysfunc(ranuni(0),hex16.); - %let client_secret=secret_%sysfunc(ranuni(0),hex16.); -%end; - -%let scopes=%sysfunc(coalescec(&scopes,openid)); -%let scopes=%mf_getquotedstr(&scopes,QUOTE=D,indlm=|); -%let grant_type=%mf_getquotedstr(&grant_type,QUOTE=D,indlm=|); -%let required_user_groups= - %mf_getquotedstr(&required_user_groups,QUOTE=D,indlm=|); - -data _null_; - file &fname2; - length clientid clientsecret clientname scope grant_types reqd_groups - autoapprove $256.; - clientid='"client_id":'!!quote(trim(symget('client_id'))); - clientsecret=',"client_secret":'!!quote(trim(symget('client_secret'))); - clientname=',"name":'!!quote(trim(symget('client_name'))); - scope=',"scope":['!!symget('scopes')!!']'; - grant_types=symget('grant_type'); - if grant_types = '""' then grant_types =''; - grant_types=cats(',"authorized_grant_types": [',grant_types,']'); - reqd_groups=symget('required_user_groups'); - if reqd_groups = '""' then reqd_groups =''; - else reqd_groups=cats(',"required_user_groups":[',reqd_groups,']'); - autoapprove=trim(symget('autoapprove')); - if not missing(autoapprove) then autoapprove= - cats(',"autoapprove":',autoapprove); - use_session=trim(symget('use_session')); - if not missing(use_session) then use_session= - cats(',"use_session":',use_session); - - put '{' clientid ; - put clientsecret ; - put clientname; - put scope; - put grant_types; - if not missing(reqd_groups) then put reqd_groups; - put autoapprove; - put use_session; -%if &access_token_validity ne DEFAULT %then %do; - put ',"access_token_validity":' "&access_token_validity"; -%end; -%if &refresh_token_validity ne DEFAULT %then %do; - put ',"refresh_token_validity":' "&refresh_token_validity"; -%end; - - put ',"redirect_uri": "urn:ietf:wg:oauth:2.0:oob"'; - put '}'; -run; - -%let fname3=%mf_getuniquefileref(); -proc http method='POST' in=&fname2 out=&fname3 - url="&base_uri/SASLogon/oauth/clients"; - headers "Content-Type"="application/json" - "Authorization"="Bearer &access_token"; -run; - -/* show response */ -%local err; -%let err=NONE; -data _null_; - infile &fname3; - input; - if _infile_=:'{"err'!!'or":' then do; - length message $32767; - message=scan(_infile_,-2,'"'); - call symputx('err',message,'l'); - end; -run; -%if "&err" ne "NONE" %then %do; - %put %str(ERR)OR: &err; -%end; - -/* prepare url */ -%if %index(%superq(grant_type),authorization_code) %then %do; - data _null_; - if symexist('_baseurl') then do; - url=symget('_baseurl'); - if subpad(url,length(url)-9,9)='SASStudio' - then url=substr(url,1,length(url)-11); - else url="&systcpiphostname"; - end; - else url="&systcpiphostname"; - call symputx('url',url); - run; -%end; - -%put Please provide the following details to the developer:; -%put ; -%put CLIENT_ID=&client_id; -%put CLIENT_SECRET=&client_secret; -%put GRANT_TYPE=&grant_type; -%put; -%if %index(%superq(grant_type),authorization_code) %then %do; - /* cannot use base_uri here as it includes the protocol which may be incorrect - externally */ - %put NOTE: Visit the link below and select 'openid' to get the grant code:; - %put NOTE- ; - %put NOTE- &url/SASLogon/oauth/authorize?client_id=&client_id%str(&)%trim( - )response_type=code; - %put NOTE- ; -%end; - -data &outds; - client_id=symget('client_id'); - client_secret=symget('client_secret'); - error=symget('err'); -run; - -data &outjson; - infile &fname2; - input; - line=_infile_; -run; - -/* clear refs */ -filename &fname1 clear; -filename &fname2 clear; -filename &fname3 clear; -libname &libref clear; - -%mend mv_registerclient; diff --git a/003_macros/mv_tokenauth.sas b/003_macros/mv_tokenauth.sas deleted file mode 100644 index 7714352..0000000 --- a/003_macros/mv_tokenauth.sas +++ /dev/null @@ -1,149 +0,0 @@ -/*** HELP START ***//** - @file mv_tokenauth.sas - @brief Get initial Refresh and Access Tokens - @details Before a Refresh Token can be obtained, the client must be - registered by an administrator. This can be done using the - `mv_registerclient` macro, after which the user must visit a URL to get an - additional code (if using oauth). - - That code (or username / password) is used here to get the Refresh Token - (and an initial Access Token). THIS MACRO CAN ONLY BE USED ONCE - further - access tokens can be obtained using the `mv_gettokenrefresh` macro. - - Access tokens expire frequently (every 10 hours or so) whilst refresh tokens - expire periodically (every month or so). This is all configurable. - - Usage: - - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - - %mv_registerclient(outds=clientinfo) - - %mv_tokenauth(inds=clientinfo,code=LD39EpalOf) - - A great article for explaining all these steps is available here: - - https://blogs.sas.com/content/sgf/2019/01/25/authentication-to-sas-viya/ - - @param [in] inds= A dataset containing client_id, client_secret, and auth_code - @param [in] outds= A dataset containing access_token and refresh_token - @param [in] client_id= The client name - @param [in] client_secret= client secret - @param [in] grant_type= valid values are "password" or "authorization_code" - (unquoted). The default is authorization_code. - @param [in] code= () - If grant_type=authorization_code then provide the necessary code here - @param [in] user= If grant_type=password then provide the username here - @param [in] pass= If grant_type=password then provide the password here - @param [in] access_token_var= (ACCESS_TOKEN) - The global macro variable to contain the access token - @param [in] refresh_token_var= (REFRESH_TOKEN) - The global macro variable to contain the refresh token - @param [in] base_uri= The Viya API server location - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - @li mf_existds.sas - -**//*** HELP END ***/ - -%macro mv_tokenauth(inds=mv_registerclient - ,outds=mv_tokenauth - ,client_id=someclient - ,client_secret=somesecret - ,grant_type=authorization_code - ,code= - ,user= - ,pass= - ,access_token_var=ACCESS_TOKEN - ,refresh_token_var=REFRESH_TOKEN - ,base_uri=#NOTSET# - ); -%global &access_token_var &refresh_token_var; - -%local fref1 fref2 libref; - -/* test the validity of inputs */ -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) - -%if %mf_existds(&inds) %then %do; - data _null_; - set &inds; - call symputx('client_id',client_id,'l'); - call symputx('client_secret',client_secret,'l'); - if not missing(auth_code) then call symputx('code',auth_code,'l'); - run; -%end; - -%mp_abort(iftrue=(&grant_type=authorization_code and %str(&code)=%str()) - ,mac=&sysmacroname - ,msg=%str(Authorization code required) -) - -%mp_abort(iftrue=( - &grant_type=password and (%str(&user)=%str() or %str(&pass)=%str())) - ,mac=&sysmacroname - ,msg=%str(username / password required) -) - -/* prepare appropriate grant type */ -%let fref1=%mf_getuniquefileref(); - -data _null_; - file &fref1; - if "&grant_type"='authorization_code' then string=cats( - 'grant_type=authorization_code&code=',symget('code')); - else string=cats('grant_type=password&username=',symget('user') - ,'&password=',symget(pass)); - call symputx('grantstring',cats("'",string,"'")); -run; -/*data _null_;infile &fref1;input;put _infile_;run;*/ - -/** - * Request access token - */ -%if &base_uri=#NOTSET# %then %let base_uri=%mf_getplatform(VIYARESTAPI); - -%let fref2=%mf_getuniquefileref(); -proc http method='POST' in=&grantstring out=&fref2 - url="&base_uri/SASLogon/oauth/token" - WEBUSERNAME="&client_id" - WEBPASSWORD="&client_secret" - AUTH_BASIC; - headers "Accept"="application/json" - "Content-Type"="application/x-www-form-urlencoded"; -run; -/*data _null_;infile &fref2;input;put _infile_;run;*/ - -/** - * Extract access / refresh tokens - */ - -%let libref=%mf_getuniquelibref(); -libname &libref JSON fileref=&fref2; - -/* extract the tokens */ -data &outds; - set &libref..root; - call symputx("&access_token_var",access_token); - call symputx("&refresh_token_var",refresh_token); -run; - - -libname &libref clear; -filename &fref1 clear; -filename &fref2 clear; - -%mend mv_tokenauth; diff --git a/003_macros/mv_tokenrefresh.sas b/003_macros/mv_tokenrefresh.sas deleted file mode 100644 index b655713..0000000 --- a/003_macros/mv_tokenrefresh.sas +++ /dev/null @@ -1,134 +0,0 @@ -/*** HELP START ***//** - @file mv_tokenrefresh.sas - @brief Get an additional access token using a refresh token - @details Before an access token can be obtained, a refresh token is required - For that, check out the `mv_tokenauth` macro. - - Usage: - - * prep work - register client, get refresh token, save it for later use ; - %mv_registerclient(outds=client) - %mv_tokenauth(inds=client,code=wKDZYTEPK6) - data _null_; - file "~/refresh.token"; - put "&refresh_token"; - run; - - * now do the things n stuff; - data _null_; - infile "~/refresh.token"; - input; - call symputx('refresh_token',_infile_); - run; - %mv_tokenrefresh(client_id=&client - ,client_secret=&secret - ) - - A great article for explaining all these steps is available here: - - https://blogs.sas.com/content/sgf/2019/01/25/authentication-to-sas-viya/ - - @param [in] inds= A dataset containing client_id and client_secret - @param [in] outds= A dataset containing access_token and refresh_token - @param [in] client_id= The client name (alternative to inds) - @param [in] client_secret= client secret (alternative to inds) - @param [in] grant_type= valid values are "password" or "authorization_code" - (unquoted). The default is authorization_code. - @param [in] user= If grant_type=password then provide the username here - @param [in] pass= If grant_type=password then provide the password here - @param [in] access_token_var= (ACCESS_TOKEN) - The global macro variable to contain the access token - @param [in] refresh_token_var= (REFRESH_TOKEN) - The global macro variable containing the refresh token - - @version VIYA V.03.04 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - @li mf_existds.sas - -**//*** HELP END ***/ - -%macro mv_tokenrefresh(inds=mv_registerclient - ,outds=mv_tokenrefresh - ,client_id=someclient - ,client_secret=somesecret - ,grant_type=authorization_code - ,user= - ,pass= - ,access_token_var=ACCESS_TOKEN - ,refresh_token_var=REFRESH_TOKEN - ); -%global &access_token_var &refresh_token_var; -options noquotelenmax; - -%local fref1 libref; - -/* test the validity of inputs */ -%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password) - ,mac=&sysmacroname - ,msg=%str(Invalid value for grant_type: &grant_type) -) - -%mp_abort( - iftrue=(&grant_type=password and (%str(&user)=%str() or %str(&pass)=%str())) - ,mac=&sysmacroname - ,msg=%str(username / password required) -) - -%if %mf_existds(&inds) %then %do; - data _null_; - set &inds; - call symputx('client_id',client_id,'l'); - call symputx('client_secret',client_secret,'l'); - call symputx("&refresh_token_var",&refresh_token_var,'l'); - run; -%end; - -%mp_abort(iftrue=(%str(&client_id)=%str() or %str(&client_secret)=%str()) - ,mac=&sysmacroname - ,msg=%str(client / secret must both be provided) -) - -/** - * Request access token - */ -%local base_uri; /* location of rest apis */ -%let base_uri=%mf_getplatform(VIYARESTAPI); - -%let fref1=%mf_getuniquefileref(); -proc http method='POST' - in="grant_type=refresh_token%nrstr(&)refresh_token=&&&refresh_token_var" - out=&fref1 - url="&base_uri/SASLogon/oauth/token" - WEBUSERNAME="&client_id" - WEBPASSWORD="&client_secret" - AUTH_BASIC; - headers "Accept"="application/json" - "Content-Type"="application/x-www-form-urlencoded"; -run; -/*data _null_;infile &fref1;input;put _infile_;run;*/ - -/** - * Extract access / refresh tokens - */ - -%let libref=%mf_getuniquelibref(); -libname &libref JSON fileref=&fref1; - -/* extract the token */ -data &outds; - set &libref..root; - call symputx("&access_token_var",access_token); - call symputx("&refresh_token_var",refresh_token); -run; - - -libname &libref clear; -filename &fref1 clear; - -%mend mv_tokenrefresh; diff --git a/003_macros/mv_webout.sas b/003_macros/mv_webout.sas deleted file mode 100644 index 71e27d0..0000000 --- a/003_macros/mv_webout.sas +++ /dev/null @@ -1,271 +0,0 @@ -/*** HELP START ***//** - @file - @brief Send data to/from the SAS Viya Job Execution Service - @details This macro should be added to the start of each Job Execution - Service, **immediately** followed by a call to: - - %mv_webout(FETCH) - - This will read all the input data and create same-named SAS datasets in the - WORK library. You can then insert your code, and send data back using the - following syntax: - - data some datasets; * make some data ; - retain some columns; - run; - - %mv_webout(OPEN) - %mv_webout(ARR,some) * Array format, fast, suitable for large tables ; - %mv_webout(OBJ,datasets) * Object format, easier to work with ; - %mv_webout(CLOSE) - - - @param [in] action Either OPEN, ARR, OBJ or CLOSE - @param [in] ds The dataset to send back to the frontend - @param [in] _webout= fileref for returning the json - @param [out] fref=(_mvwtemp) Temp fileref to which to write the output - @param [out] dslabel= value to use instead of table name for sending to JSON - @param [in] fmt= (N) Setting Y converts all vars to their formatted values - @param [in] stream=(Y) Change to N if not streaming to _webout - @param [in] missing= (NULL) Special numeric missing values can be sent as NULL - (eg `null`) or as STRING values (eg `".a"` or `".b"`) - @param [in] showmeta= (N) Set to Y to output metadata alongside each table, - such as the column formats and types. The metadata is contained inside an - object with the same name as the table but prefixed with a dollar sign - ie, - `,"$tablename":{"formats":{"col1":"$CHAR1"},"types":{"COL1":"C"}}` - @param [in] maxobs= (MAX) Provide an integer to limit the number of input rows - that should be converted to output JSON - @param [in] workobs= (0) When set to a positive integer, will create a new - output object (WORK) which contains this number of observations from all - tables in the WORK library. - -

SAS Macros

- @li mp_jsonout.sas - @li mf_getuser.sas - -

Related Macros

- @li ms_webout.sas - @li mm_webout.sas - - @version Viya 3.3 - @author Allan Bowe, source: https://github.com/sasjs/core - -**//*** HELP END ***/ -%macro mv_webout(action,ds,fref=_mvwtemp,dslabel=,fmt=N,stream=Y,missing=NULL - ,showmeta=N,maxobs=MAX,workobs=0 -); -%global _webin_file_count _webin_fileuri _debug _omittextlog _webin_name - sasjs_tables SYS_JES_JOB_URI; -%if %index("&_debug",log) %then %let _debug=131; - -%local i tempds table; -%let action=%upcase(&action); - -%if &action=FETCH %then %do; - %if %upcase(&_omittextlog)=FALSE or %str(&_debug) ge 131 %then %do; - options mprint notes mprintnest; - %end; - - %if not %symexist(_webin_fileuri1) %then %do; - %let _webin_file_count=%eval(&_webin_file_count+0); - %let _webin_fileuri1=&_webin_fileuri; - %let _webin_name1=&_webin_name; - %end; - - /* if the sasjs_tables param is passed, we expect param based upload */ - %if %length(&sasjs_tables.X)>1 %then %do; - - /* convert data from macro variables to datasets */ - %do i=1 %to %sysfunc(countw(&sasjs_tables)); - %let table=%scan(&sasjs_tables,&i,%str( )); - %if %symexist(sasjs&i.data0)=0 %then %let sasjs&i.data0=1; - data _null_; - file "%sysfunc(pathname(work))/&table..csv" recfm=n; - retain nrflg 0; - length line $32767; - do i=1 to &&sasjs&i.data0; - if &&sasjs&i.data0=1 then line=symget("sasjs&i.data"); - else line=symget(cats("sasjs&i.data",i)); - if i=1 and substr(line,1,7)='%nrstr(' then do; - nrflg=1; - line=substr(line,8); - end; - if i=&&sasjs&i.data0 and nrflg=1 then do; - line=substr(line,1,length(line)-1); - end; - put line +(-1) @; - end; - run; - data _null_; - infile "%sysfunc(pathname(work))/&table..csv" termstr=crlf ; - input; - if _n_=1 then call symputx('input_statement',_infile_); - list; - data work.&table; - infile "%sysfunc(pathname(work))/&table..csv" firstobs=2 dsd - termstr=crlf; - input &input_statement; - run; - %end; - %end; - %else %do i=1 %to &_webin_file_count; - /* read in any files that are sent */ - /* this part needs refactoring for wide files */ - filename indata filesrvc "&&_webin_fileuri&i" lrecl=999999; - data _null_; - infile indata termstr=crlf lrecl=32767; - input; - if _n_=1 then call symputx('input_statement',_infile_); - %if %str(&_debug) ge 131 %then %do; - if _n_<20 then putlog _infile_; - else stop; - %end; - %else %do; - stop; - %end; - run; - data &&_webin_name&i; - infile indata firstobs=2 dsd termstr=crlf ; - input &input_statement; - run; - %let sasjs_tables=&sasjs_tables &&_webin_name&i; - %end; -%end; -%else %if &action=OPEN %then %do; - /* setup webout */ - OPTIONS NOBOMFILE; - %if "X&SYS_JES_JOB_URI.X"="XX" %then %do; - filename _webout temp lrecl=999999 mod; - %end; - %else %do; - filename _webout filesrvc parenturi="&SYS_JES_JOB_URI" - name="_webout.json" lrecl=999999 mod; - %end; - - /* setup temp ref */ - %if %upcase(&fref) ne _WEBOUT %then %do; - filename &fref temp lrecl=999999 permission='A::u::rwx,A::g::rw-,A::o::---'; - %end; - - /* setup json */ - data _null_;file &fref; - put '{"SYSDATE" : "' "&SYSDATE" '"'; - put ',"SYSTIME" : "' "&SYSTIME" '"'; - run; -%end; -%else %if &action=ARR or &action=OBJ %then %do; - %mp_jsonout(&action,&ds,dslabel=&dslabel,fmt=&fmt,jref=&fref - ,engine=DATASTEP,missing=&missing,showmeta=&showmeta,maxobs=&maxobs - ) -%end; -%else %if &action=CLOSE %then %do; - %if %str(&workobs) > 0 %then %do; - /* send back first XX records of each work table for debugging */ - data;run;%let tempds=%scan(&syslast,2,.); - ods output Members=&tempds; - proc datasets library=WORK memtype=data; - %local wtcnt;%let wtcnt=0; - data _null_; - set &tempds; - if not (upcase(name) =:"DATA"); /* ignore temp datasets */ - i+1; - call symputx(cats('wt',i),name,'l'); - call symputx('wtcnt',i,'l'); - data _null_; file &fref mod; put ",""WORK"":{"; - %do i=1 %to &wtcnt; - %let wt=&&wt&i; - data _null_; file &fref mod; - dsid=open("WORK.&wt",'is'); - nlobs=attrn(dsid,'NLOBS'); - nvars=attrn(dsid,'NVARS'); - rc=close(dsid); - if &i>1 then put ','@; - put " ""&wt"" : {"; - put '"nlobs":' nlobs; - put ',"nvars":' nvars; - %mp_jsonout(OBJ,&wt,jref=&fref,dslabel=first10rows,showmeta=Y - ,maxobs=&workobs - ) - data _null_; file &fref mod;put "}"; - %end; - data _null_; file &fref mod;put "}";run; - %end; - - /* close off json */ - data _null_;file &fref mod; - length SYSPROCESSNAME syserrortext syswarningtext autoexec $512; - put ",""_DEBUG"" : ""&_debug"" "; - _PROGRAM=quote(trim(resolve(symget('_PROGRAM')))); - put ',"_PROGRAM" : ' _PROGRAM ; - autoexec=quote(urlencode(trim(getoption('autoexec')))); - put ',"AUTOEXEC" : ' autoexec; - put ",""MF_GETUSER"" : ""%mf_getuser()"" "; - SYS_JES_JOB_URI=quote(trim(resolve(symget('SYS_JES_JOB_URI')))); - put ',"SYS_JES_JOB_URI" : ' SYS_JES_JOB_URI ; - put ",""SYSJOBID"" : ""&sysjobid"" "; - put ",""SYSCC"" : ""&syscc"" "; - syserrortext=cats(symget('syserrortext')); - if findc(syserrortext,'"\'!!'0A0D09000E0F010210111A'x) then do; - syserrortext='"'!!trim( - prxchange('s/"/\\"/',-1, /* double quote */ - prxchange('s/\x0A/\n/',-1, /* new line */ - prxchange('s/\x0D/\r/',-1, /* carriage return */ - prxchange('s/\x09/\\t/',-1, /* tab */ - prxchange('s/\x00/\\u0000/',-1, /* NUL */ - prxchange('s/\x0E/\\u000E/',-1, /* SS */ - prxchange('s/\x0F/\\u000F/',-1, /* SF */ - prxchange('s/\x01/\\u0001/',-1, /* SOH */ - prxchange('s/\x02/\\u0002/',-1, /* STX */ - prxchange('s/\x10/\\u0010/',-1, /* DLE */ - prxchange('s/\x11/\\u0011/',-1, /* DC1 */ - prxchange('s/\x1A/\\u001A/',-1, /* SUB */ - prxchange('s/\\/\\\\/',-1,syserrortext) - )))))))))))))!!'"'; - end; - else syserrortext=cats('"',syserrortext,'"'); - put ',"SYSERRORTEXT" : ' syserrortext; - put ",""SYSHOSTNAME"" : ""&syshostname"" "; - put ",""SYSPROCESSID"" : ""&SYSPROCESSID"" "; - put ",""SYSPROCESSMODE"" : ""&SYSPROCESSMODE"" "; - SYSPROCESSNAME=quote(urlencode(cats(SYSPROCESSNAME))); - put ",""SYSPROCESSNAME"" : " SYSPROCESSNAME; - put ",""SYSJOBID"" : ""&sysjobid"" "; - put ",""SYSSCPL"" : ""&sysscpl"" "; - put ",""SYSSITE"" : ""&syssite"" "; - put ",""SYSUSERID"" : ""&sysuserid"" "; - sysvlong=quote(trim(symget('sysvlong'))); - put ',"SYSVLONG" : ' sysvlong; - syswarningtext=cats(symget('syswarningtext')); - if findc(syswarningtext,'"\'!!'0A0D09000E0F010210111A'x) then do; - syswarningtext='"'!!trim( - prxchange('s/"/\\"/',-1, /* double quote */ - prxchange('s/\x0A/\n/',-1, /* new line */ - prxchange('s/\x0D/\r/',-1, /* carriage return */ - prxchange('s/\x09/\\t/',-1, /* tab */ - prxchange('s/\x00/\\u0000/',-1, /* NUL */ - prxchange('s/\x0E/\\u000E/',-1, /* SS */ - prxchange('s/\x0F/\\u000F/',-1, /* SF */ - prxchange('s/\x01/\\u0001/',-1, /* SOH */ - prxchange('s/\x02/\\u0002/',-1, /* STX */ - prxchange('s/\x10/\\u0010/',-1, /* DLE */ - prxchange('s/\x11/\\u0011/',-1, /* DC1 */ - prxchange('s/\x1A/\\u001A/',-1, /* SUB */ - prxchange('s/\\/\\\\/',-1,syswarningtext) - )))))))))))))!!'"'; - end; - else syswarningtext=cats('"',syswarningtext,'"'); - put ',"SYSWARNINGTEXT" : ' syswarningtext; - put ',"END_DTTM" : "' "%sysfunc(datetime(),E8601DT26.6)" '" '; - length memsize $32; - memsize="%sysfunc(INPUTN(%sysfunc(getoption(memsize)), best.),sizekmg.)"; - memsize=quote(cats(memsize)); - put ',"MEMSIZE" : ' memsize; - put "}"; - - %if %upcase(&fref) ne _WEBOUT and &stream=Y %then %do; - data _null_; rc=fcopy("&fref","_webout");run; - %end; - -%end; - -%mend mv_webout; diff --git a/002_macros/ml_gsubfile.sas b/004_macros/ml_gsubfile.sas similarity index 100% rename from 002_macros/ml_gsubfile.sas rename to 004_macros/ml_gsubfile.sas diff --git a/002_macros/ml_json.sas b/004_macros/ml_json.sas similarity index 100% rename from 002_macros/ml_json.sas rename to 004_macros/ml_json.sas diff --git a/005_macros/mf_isblank.sas b/005_macros/mf_isblank.sas deleted file mode 100644 index 7181689..0000000 --- a/005_macros/mf_isblank.sas +++ /dev/null @@ -1,27 +0,0 @@ -/*** HELP START ***//** - @file mf_isblank.sas - @brief Checks whether a macro variable is empty (blank) - @details Simply performs: - - %sysevalf(%superq(param)=,boolean) - - Usage: - - %put mf_isblank(&var); - - inspiration: - https://support.sas.com/resources/papers/proceedings09/022-2009.pdf - - @param [in] Param VALUE to be checked - - @return output returns 1 (if blank) else 0 - - @version 9.2 -**//*** HELP END ***/ - -%macro mf_isblank(param -)/*/STORE SOURCE*/; - - %sysevalf(%superq(param)=,boolean) - -%mend mf_isblank; diff --git a/005_macros/mfs_httpheader.sas b/005_macros/mfs_httpheader.sas deleted file mode 100644 index 5378da8..0000000 --- a/005_macros/mfs_httpheader.sas +++ /dev/null @@ -1,53 +0,0 @@ -/*** HELP START ***//** - @file - @brief Sets the http headers in the SASjs/server response - @details For GET requests, SASjs server will use the file generated by this - macro for setting the appropriate http headers in the response. - - It works by writing a file to the session directory, that is then ingested by - the server. - - The location of this file is driven by the global variable - `sasjs_stpsrv_header_loc` which is made available in the autoexec. - - Usage: - - %mfs_httpheader(Content-Type,application/csv) - - @param [in] header_name Name of the http header to set - @param [in] header_value Value of the http header to set - -

Related Macros

- @li mcf_stpsrv_header.sas - - @version 9.3 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mfs_httpheader(header_name - ,header_value -)/*/STORE SOURCE*/; -%global sasjs_stpsrv_header_loc; -%local fref fid i; - -%if %sysfunc(filename(fref,&sasjs_stpsrv_header_loc)) ne 0 %then %do; - %put &=fref &=sasjs_stpsrv_header_loc; - %put %str(ERR)OR: %sysfunc(sysmsg()); - %return; -%end; - -%let fid=%sysfunc(fopen(&fref,A)); - -%if &fid=0 %then %do; - %put %str(ERR)OR: %sysfunc(sysmsg()); - %return; -%end; - -%let rc=%sysfunc(fput(&fid,%str(&header_name): %str(&header_value))); -%let rc=%sysfunc(fwrite(&fid)); - -%let rc=%sysfunc(fclose(&fid)); -%let rc=%sysfunc(filename(&fref)); - -%mend mfs_httpheader; diff --git a/002_macros/mm_adduser2group.sas b/005_macros/mm_adduser2group.sas similarity index 100% rename from 002_macros/mm_adduser2group.sas rename to 005_macros/mm_adduser2group.sas diff --git a/003_macros/mm_assigndirectlib.sas b/005_macros/mm_assigndirectlib.sas similarity index 100% rename from 003_macros/mm_assigndirectlib.sas rename to 005_macros/mm_assigndirectlib.sas diff --git a/002_macros/mm_assignlib.sas b/005_macros/mm_assignlib.sas similarity index 100% rename from 002_macros/mm_assignlib.sas rename to 005_macros/mm_assignlib.sas diff --git a/002_macros/mm_createapplication.sas b/005_macros/mm_createapplication.sas similarity index 100% rename from 002_macros/mm_createapplication.sas rename to 005_macros/mm_createapplication.sas diff --git a/002_macros/mm_createdataset.sas b/005_macros/mm_createdataset.sas similarity index 100% rename from 002_macros/mm_createdataset.sas rename to 005_macros/mm_createdataset.sas diff --git a/002_macros/mm_createdocument.sas b/005_macros/mm_createdocument.sas similarity index 100% rename from 002_macros/mm_createdocument.sas rename to 005_macros/mm_createdocument.sas diff --git a/002_macros/mm_createfolder.sas b/005_macros/mm_createfolder.sas similarity index 100% rename from 002_macros/mm_createfolder.sas rename to 005_macros/mm_createfolder.sas diff --git a/002_macros/mm_createlibrary.sas b/005_macros/mm_createlibrary.sas similarity index 100% rename from 002_macros/mm_createlibrary.sas rename to 005_macros/mm_createlibrary.sas diff --git a/003_macros/mm_createstp.sas b/005_macros/mm_createstp.sas similarity index 100% rename from 003_macros/mm_createstp.sas rename to 005_macros/mm_createstp.sas diff --git a/002_macros/mm_createwebservice.sas b/005_macros/mm_createwebservice.sas similarity index 100% rename from 002_macros/mm_createwebservice.sas rename to 005_macros/mm_createwebservice.sas diff --git a/002_macros/mm_deletedocument.sas b/005_macros/mm_deletedocument.sas similarity index 100% rename from 002_macros/mm_deletedocument.sas rename to 005_macros/mm_deletedocument.sas diff --git a/002_macros/mm_deletelibrary.sas b/005_macros/mm_deletelibrary.sas similarity index 100% rename from 002_macros/mm_deletelibrary.sas rename to 005_macros/mm_deletelibrary.sas diff --git a/002_macros/mm_deletestp.sas b/005_macros/mm_deletestp.sas similarity index 100% rename from 002_macros/mm_deletestp.sas rename to 005_macros/mm_deletestp.sas diff --git a/002_macros/mm_getauthinfo.sas b/005_macros/mm_getauthinfo.sas similarity index 100% rename from 002_macros/mm_getauthinfo.sas rename to 005_macros/mm_getauthinfo.sas diff --git a/002_macros/mm_getcols.sas b/005_macros/mm_getcols.sas similarity index 100% rename from 002_macros/mm_getcols.sas rename to 005_macros/mm_getcols.sas diff --git a/002_macros/mm_getdetails.sas b/005_macros/mm_getdetails.sas similarity index 100% rename from 002_macros/mm_getdetails.sas rename to 005_macros/mm_getdetails.sas diff --git a/002_macros/mm_getdirectories.sas b/005_macros/mm_getdirectories.sas similarity index 100% rename from 002_macros/mm_getdirectories.sas rename to 005_macros/mm_getdirectories.sas diff --git a/002_macros/mm_getdocument.sas b/005_macros/mm_getdocument.sas similarity index 100% rename from 002_macros/mm_getdocument.sas rename to 005_macros/mm_getdocument.sas diff --git a/002_macros/mm_getfoldermembers.sas b/005_macros/mm_getfoldermembers.sas similarity index 100% rename from 002_macros/mm_getfoldermembers.sas rename to 005_macros/mm_getfoldermembers.sas diff --git a/002_macros/mm_getfoldertree.sas b/005_macros/mm_getfoldertree.sas similarity index 100% rename from 002_macros/mm_getfoldertree.sas rename to 005_macros/mm_getfoldertree.sas diff --git a/002_macros/mm_getgroupmembers.sas b/005_macros/mm_getgroupmembers.sas similarity index 100% rename from 002_macros/mm_getgroupmembers.sas rename to 005_macros/mm_getgroupmembers.sas diff --git a/003_macros/mm_getgroups.sas b/005_macros/mm_getgroups.sas similarity index 100% rename from 003_macros/mm_getgroups.sas rename to 005_macros/mm_getgroups.sas diff --git a/002_macros/mm_getlibmetadiffs.sas b/005_macros/mm_getlibmetadiffs.sas similarity index 100% rename from 002_macros/mm_getlibmetadiffs.sas rename to 005_macros/mm_getlibmetadiffs.sas diff --git a/002_macros/mm_getlibs.sas b/005_macros/mm_getlibs.sas similarity index 100% rename from 002_macros/mm_getlibs.sas rename to 005_macros/mm_getlibs.sas diff --git a/002_macros/mm_getobjects.sas b/005_macros/mm_getobjects.sas similarity index 100% rename from 002_macros/mm_getobjects.sas rename to 005_macros/mm_getobjects.sas diff --git a/002_macros/mm_getpublictypes.sas b/005_macros/mm_getpublictypes.sas similarity index 100% rename from 002_macros/mm_getpublictypes.sas rename to 005_macros/mm_getpublictypes.sas diff --git a/002_macros/mm_getrepos.sas b/005_macros/mm_getrepos.sas similarity index 100% rename from 002_macros/mm_getrepos.sas rename to 005_macros/mm_getrepos.sas diff --git a/002_macros/mm_getroles.sas b/005_macros/mm_getroles.sas similarity index 100% rename from 002_macros/mm_getroles.sas rename to 005_macros/mm_getroles.sas diff --git a/002_macros/mm_getservercontexts.sas b/005_macros/mm_getservercontexts.sas similarity index 100% rename from 002_macros/mm_getservercontexts.sas rename to 005_macros/mm_getservercontexts.sas diff --git a/002_macros/mm_getstpcode.sas b/005_macros/mm_getstpcode.sas similarity index 100% rename from 002_macros/mm_getstpcode.sas rename to 005_macros/mm_getstpcode.sas diff --git a/002_macros/mm_getstpinfo.sas b/005_macros/mm_getstpinfo.sas similarity index 100% rename from 002_macros/mm_getstpinfo.sas rename to 005_macros/mm_getstpinfo.sas diff --git a/002_macros/mm_getstps.sas b/005_macros/mm_getstps.sas similarity index 100% rename from 002_macros/mm_getstps.sas rename to 005_macros/mm_getstps.sas diff --git a/002_macros/mm_gettableid.sas b/005_macros/mm_gettableid.sas similarity index 100% rename from 002_macros/mm_gettableid.sas rename to 005_macros/mm_gettableid.sas diff --git a/002_macros/mm_gettables.sas b/005_macros/mm_gettables.sas similarity index 100% rename from 002_macros/mm_gettables.sas rename to 005_macros/mm_gettables.sas diff --git a/002_macros/mm_gettree.sas b/005_macros/mm_gettree.sas similarity index 100% rename from 002_macros/mm_gettree.sas rename to 005_macros/mm_gettree.sas diff --git a/002_macros/mm_gettypes.sas b/005_macros/mm_gettypes.sas similarity index 100% rename from 002_macros/mm_gettypes.sas rename to 005_macros/mm_gettypes.sas diff --git a/002_macros/mm_getusers.sas b/005_macros/mm_getusers.sas similarity index 100% rename from 002_macros/mm_getusers.sas rename to 005_macros/mm_getusers.sas diff --git a/002_macros/mm_getwebappsrvprops.sas b/005_macros/mm_getwebappsrvprops.sas similarity index 100% rename from 002_macros/mm_getwebappsrvprops.sas rename to 005_macros/mm_getwebappsrvprops.sas diff --git a/002_macros/mm_spkexport.sas b/005_macros/mm_spkexport.sas similarity index 100% rename from 002_macros/mm_spkexport.sas rename to 005_macros/mm_spkexport.sas diff --git a/002_macros/mm_tree.sas b/005_macros/mm_tree.sas similarity index 100% rename from 002_macros/mm_tree.sas rename to 005_macros/mm_tree.sas diff --git a/002_macros/mm_updateappextension.sas b/005_macros/mm_updateappextension.sas similarity index 100% rename from 002_macros/mm_updateappextension.sas rename to 005_macros/mm_updateappextension.sas diff --git a/002_macros/mm_updatedocument.sas b/005_macros/mm_updatedocument.sas similarity index 100% rename from 002_macros/mm_updatedocument.sas rename to 005_macros/mm_updatedocument.sas diff --git a/002_macros/mm_updatestpservertype.sas b/005_macros/mm_updatestpservertype.sas similarity index 100% rename from 002_macros/mm_updatestpservertype.sas rename to 005_macros/mm_updatestpservertype.sas diff --git a/002_macros/mm_updatestpsourcecode.sas b/005_macros/mm_updatestpsourcecode.sas similarity index 100% rename from 002_macros/mm_updatestpsourcecode.sas rename to 005_macros/mm_updatestpsourcecode.sas diff --git a/002_macros/mm_webout.sas b/005_macros/mm_webout.sas similarity index 100% rename from 002_macros/mm_webout.sas rename to 005_macros/mm_webout.sas diff --git a/005_macros/mp_cntlout.sas b/005_macros/mp_cntlout.sas deleted file mode 100644 index 5326750..0000000 --- a/005_macros/mp_cntlout.sas +++ /dev/null @@ -1,94 +0,0 @@ -/*** HELP START ***//** - @file mp_cntlout.sas - @brief Creates a cntlout dataset in a consistent format - @details The dataset produced by proc format in the cntlout option will vary - according to its contents. - - When dealing with formats from an ETL perspective (eg in [Data Controller for - SAS](https://datacontroller.io)), it is important that the output dataset - has a consistent model (and compariable values). - - This macro makes use of mddl_sas_cntlout.sas to provide the consistent model, - and will left-align the start and end values when dealing with numeric ranges - to enable consistency when checking for differences. - - usage: - - %mp_cntlout(libcat=yourlib.cat,cntlout=work.formatexport) - - @param [in] libcat The library.catalog reference - @param [in] fmtlist= (0) provide a space separated list of specific formats to - extract - @param [in] iftrue= (1=1) A condition under which the macro should be executed - @param [out] cntlout= (work.fmtextract) Libds reference for the output dataset - -

SAS Macros

- @li mddl_sas_cntlout.sas - @li mf_getuniquename.sas - @li mp_aligndecimal.sas - -

Related Macros

- @li mf_getvarformat.sas - @li mp_getformats.sas - @li mp_loadformat.sas - @li mp_ds2fmtds.sas - - @version 9.2 - @author Allan Bowe - @cond -**//*** HELP END ***/ - -%macro mp_cntlout( - iftrue=(1=1) - ,libcat= - ,cntlout=work.fmtextract - ,fmtlist=0 -)/*/STORE SOURCE*/; -%local ddlds cntlds i; - -%if not(%eval(%unquote(&iftrue))) %then %return; - -%let ddlds=%mf_getuniquename(); -%let cntlds=%mf_getuniquename(); - -%mddl_sas_cntlout(libds=&ddlds) - -%if %index(&libcat,-)>0 and %scan(&libcat,2,-)=FC %then %do; - %let libcat=%scan(&libcat,1,-); -%end; - -proc format lib=&libcat cntlout=&cntlds; -%if "&fmtlist" ne "0" and "&fmtlist" ne "" %then %do; - select - %do i=1 %to %sysfunc(countw(&fmtlist,%str( ))); - %scan(&fmtlist,&i,%str( )) - %end; - ; -%end; -run; - -data &cntlout/nonote2err; - if 0 then set &ddlds; - set &cntlds; - by type fmtname notsorted; - - /* align the numeric values to avoid overlapping ranges */ - if type in ("I","N") then do; - %mp_aligndecimal(start,width=16) - %mp_aligndecimal(end,width=16) - end; - - /* create row marker. Data cannot be sorted without it! */ - if first.fmtname then fmtrow=0; - fmtrow+1; - -run; -proc sort; - by type fmtname fmtrow; -run; - -proc sql; -drop table &ddlds,&cntlds; - -%mend mp_cntlout; -/** @endcond */ diff --git a/005_macros/mp_dirlist.sas b/005_macros/mp_dirlist.sas deleted file mode 100644 index aef1e9a..0000000 --- a/005_macros/mp_dirlist.sas +++ /dev/null @@ -1,262 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns all files and subdirectories within a specified parent - @details When used with getattrs=NO, is not OS specific (uses dopen / dread). - - Credit for the rename approach: - https://communities.sas.com/t5/SAS-Programming/SAS-Function-to-convert-string-to-Legal-SAS-Name/m-p/27375/highlight/true#M5003 - - Usage: - - %mp_dirlist(path=/some/location, outds=myTable, maxdepth=MAX) - - %mp_dirlist(outds=cwdfileprops, getattrs=YES) - - %mp_dirlist(fref=MYFREF) - - @warning In a Unix environment, the existence of a named pipe will cause this - macro to hang. Therefore this tool should be used with caution in a SAS 9 web - application, as it can use up all available multibridge sessions if requests - are resubmitted. - If anyone finds a way to positively identify a named pipe using SAS (without - X CMD) do please raise an issue! - - - @param [in] path= (%sysfunc(pathname(work))) Path for which to return contents - @param [in] fref= (0) Provide a DISK engine fileref as an alternative to PATH - @param [in] maxdepth= (0) Set to a positive integer to indicate the level of - subdirectory scan recursion - eg 3, to go `./3/levels/deep`. For unlimited - recursion, set to MAX. - @param [in] showparent= (NO) By default, the initial parent directory is not - part of the results. Set to YES to include it. For this record only, - directory=filepath. - @param [out] outds= (work.mp_dirlist) The output dataset to create - @param [out] getattrs= (NO) If getattrs=YES then the doptname / foptname - functions are used to scan all properties - any characters that are not - valid in a SAS name (v7) are simply stripped, and the table is transposed - so theat each property is a column and there is one file per row. An - attempt is made to get all properties whether a file or folder, but some - files/folders cannot be accessed, and so not all properties can / will be - populated. - - - @returns outds contains the following variables: - - directory (containing folder) - - file_or_folder (file / folder) - - filepath (path/to/file.name) - - filename (just the file name) - - ext (.extension) - - msg (system message if any issues) - - level (depth of folder) - - OS SPECIFIC variables, if getattrs= is used. - -

SAS Macros

- @li mf_existds.sas - @li mf_getvarlist.sas - @li mf_wordsinstr1butnotstr2.sas - @li mp_dropmembers.sas - -

Related Macros

- @li mp_dirlist.test.sas - - @version 9.2 -**//*** HELP END ***/ - -%macro mp_dirlist(path=%sysfunc(pathname(work)) - , fref=0 - , outds=work.mp_dirlist - , getattrs=NO - , showparent=NO - , maxdepth=0 - , level=0 /* The level of recursion to perform. For internal use only. */ -)/*/STORE SOURCE*/; -%let getattrs=%upcase(&getattrs)XX; - -/* temp table */ -%local out_ds; -data;run; -%let out_ds=%str(&syslast); - -/* drop main (top) table if it exists */ -%if &level=0 %then %do; - %mp_dropmembers(%scan(&outds,-1,.), libref=WORK) -%end; - -data &out_ds(compress=no - keep=file_or_folder filepath filename ext msg directory level - ); - length directory filepath $500 fref fref2 $8 file_or_folder $6 filename $80 - ext $20 msg $200 foption $16; - if _n_=1 then call missing(of _all_); - retain level &level; - %if &fref=0 %then %do; - rc = filename(fref, "&path"); - %end; - %else %do; - fref="&fref"; - rc=0; - %end; - if rc = 0 then do; - did = dopen(fref); - if did=0 then do; - putlog "NOTE: This directory is empty, or does not exist - &path"; - msg=sysmsg(); - put (_all_)(=); - stop; - end; - /* attribute is OS-dependent - could be "Directory" or "Directory Name" */ - numopts=doptnum(did); - do i=1 to numopts; - foption=doptname(did,i); - if foption=:'Directory' then i=numopts; - end; - directory=dinfo(did,foption); - rc = filename(fref); - end; - else do; - msg=sysmsg(); - put _all_; - stop; - end; - dnum = dnum(did); - do i = 1 to dnum; - filename = dread(did, i); - filepath=cats(directory,'/',filename); - rc = filename(fref2,filepath); - midd=dopen(fref2); - dmsg=sysmsg(); - if did > 0 then file_or_folder='folder'; - rc=dclose(midd); - midf=fopen(fref2); - fmsg=sysmsg(); - if midf > 0 then file_or_folder='file'; - rc=fclose(midf); - - if index(fmsg,'File is in use') or index(dmsg,'is not a directory') - then file_or_folder='file'; - else if index(fmsg,'Insufficient authorization') then file_or_folder='file'; - else if file_or_folder='' then file_or_folder='locked'; - - if file_or_folder='file' then do; - ext = prxchange('s/.*\.{1,1}(.*)/$1/', 1, filename); - if filename = ext then ext = ' '; - end; - else do; - ext=''; - file_or_folder='folder'; - end; - output; - end; - rc = dclose(did); - %if &showparent=YES and &level=0 %then %do; - filepath=directory; - file_or_folder='folder'; - ext=''; - filename=scan(directory,-1,'/\'); - msg=''; - level=&level; - output; - %end; - stop; -run; - -%if %substr(&getattrs,1,1)=Y %then %do; - data &out_ds; - set &out_ds; - length infoname infoval $60 fref $8; - if _n_=1 then call missing(fref); - rc=filename(fref,filepath); - drop rc infoname fid i close fref; - if file_or_folder='file' then do; - fid=fopen(fref); - if fid le 0 then do; - msg=sysmsg(); - putlog "Could not open file:" filepath fid= ; - sasname='_MCNOTVALID_'; - output; - end; - else do i=1 to foptnum(fid); - infoname=foptname(fid,i); - infoval=finfo(fid,infoname); - sasname=compress(infoname, '_', 'adik'); - if anydigit(sasname)=1 then sasname=substr(sasname,anyalpha(sasname)); - if upcase(sasname) ne 'FILENAME' then output; - end; - close=fclose(fid); - end; - else do; - fid=dopen(fref); - if fid le 0 then do; - msg=sysmsg(); - putlog "Could not open folder:" filepath fid= ; - sasname='_MCNOTVALID_'; - output; - end; - else do i=1 to doptnum(fid); - infoname=doptname(fid,i); - infoval=dinfo(fid,infoname); - sasname=compress(infoname, '_', 'adik'); - if anydigit(sasname)=1 then sasname=substr(sasname,anyalpha(sasname)); - if upcase(sasname) ne 'FILENAME' then output; - end; - close=dclose(fid); - end; - run; - proc sort; - by filepath sasname; - proc transpose data=&out_ds out=&out_ds(drop=_:); - id sasname; - var infoval; - by filepath file_or_folder filename ext ; - run; -%end; - -data &out_ds; - set &out_ds(where=(filepath ne '')); -run; - -/** - * The above transpose can mean that some updates create additional columns. - * This necessitates the occasional use of datastep over proc append. - */ -%if %mf_existds(&outds) %then %do; - %local basevars appvars newvars; - %let basevars=%mf_getvarlist(&outds); - %let appvars=%mf_getvarlist(&out_ds); - %let newvars=%length(%mf_wordsinstr1butnotstr2(Str1=&appvars,Str2=&basevars)); - %if &newvars>0 %then %do; - data &outds; - set &outds &out_ds; - run; - %end; - %else %do; - proc append base=&outds data=&out_ds force nowarn; - run; - %end; -%end; -%else %do; - proc append base=&outds data=&out_ds; - run; -%end; - -/* recursive call */ -%if &maxdepth>&level or &maxdepth=MAX %then %do; - data _null_; - set &out_ds; - where file_or_folder='folder'; - %if &showparent=YES and &level=0 %then %do; - if filepath ne directory; - %end; - length code $10000; - code=cats('%nrstr(%mp_dirlist(path=',filepath,",outds=&outds" - ,",getattrs=&getattrs,level=%eval(&level+1),maxdepth=&maxdepth))"); - put code=; - call execute(code); - run; -%end; - -/* tidy up */ -proc sql; -drop table &out_ds; - -%mend mp_dirlist; diff --git a/005_macros/mp_ds2csv.sas b/005_macros/mp_ds2csv.sas deleted file mode 100644 index 4ee1529..0000000 --- a/005_macros/mp_ds2csv.sas +++ /dev/null @@ -1,212 +0,0 @@ -/*** HELP START ***//** - @file - @brief Export a dataset to a CSV file WITH leading blanks - @details Export a dataset to a file or fileref, retaining leading blanks. - - When using SASJS headerformat, the input statement is provided in the first - row of the CSV. - - Usage: - - %mp_ds2csv(sashelp.class,outref="%sysfunc(pathname(work))/file.csv") - - filename example temp; - %mp_ds2csv(sashelp.air,outref=example,headerformat=SASJS) - data; infile example; input;put _infile_; if _n_>5 then stop;run; - - data _null_; - infile example; - input; - call symputx('stmnt',_infile_); - stop; - run; - data work.want; - infile example dsd firstobs=2; - input &stmnt; - run; - - Why use mp_ds2csv over, say, proc export? - - 1. Ability to retain leading blanks (this is a major one) - 2. Control the header format - 3. Simple one-liner - - @param [in] ds The dataset to be exported - @param [in] dlm= (COMMA) The delimeter to apply. For SASJS, will always be - COMMA. Supported values: - @li COMMA - @li SEMICOLON - @param [in] headerformat= (LABEL) The format to use for the header section. - Valid values: - @li LABEL - Use the variable label (or name, if blank) - @li NAME - Use the variable name - @li SASJS - Used to create sasjs-formatted input CSVs, eg for use in - mp_testservice.sas. This format will supply an input statement in the - first row, making ingestion by datastep a breeze. Special misisng values - will be prefixed with a period (eg `.A`) to enable ingestion on both SAS 9 - and Viya. Dates / Datetimes etc are identified by the format type (lookup - with mcf_getfmttype.sas) and converted to human readable formats (not - numbers). - @param [out] outfile= The output filename - should be quoted. - @param [out] outref= (0) The output fileref (takes precedence if provided) - @param [in] outencoding= (0) The (quoted) output encoding to use, eg `"UTF-8"` - @param [in] termstr= (CRLF) The line seperator to use. For SASJS, will - always be CRLF. Valid values: - @li CRLF - @li LF - -

SAS Macros

- @li mcf_getfmttype.sas - @li mf_getuniquename.sas - @li mf_getvarformat.sas - @li mf_getvarlist.sas - @li mf_getvartype.sas - - @version 9.2 - @author Allan Bowe (credit mjsq) -**//*** HELP END ***/ - -%macro mp_ds2csv(ds - ,dlm=COMMA - ,outref=0 - ,outfile= - ,outencoding=0 - ,headerformat=LABEL - ,termstr=CRLF -)/*/STORE SOURCE*/; - -%local outloc delim i varlist var vcnt vat dsv vcom vmiss fmttype vfmt; - -%if not %sysfunc(exist(&ds)) %then %do; - %put %str(WARN)ING: &ds does not exist; - %return; -%end; - -%if %index(&ds,.)=0 %then %let ds=WORK.&ds; - -%if &outencoding=0 %then %let outencoding=; -%else %let outencoding=encoding=&outencoding; - -%if &outref=0 %then %let outloc=&outfile; -%else %let outloc=&outref; - -%if &headerformat=SASJS %then %do; - %let delim=","; - %let termstr=CRLF; - %mcf_getfmttype(wrap=YES) -%end; -%else %if &dlm=COMMA %then %let delim=","; -%else %let delim=";"; - -/* credit to mjsq - https://stackoverflow.com/a/55642267 */ - -/* first get headers */ -data _null_; - file &outloc &outencoding lrecl=32767 termstr=&termstr; - length header $ 2000 varnm vfmt $32 dlm $1 fmttype $8; - call missing(of _all_); - dsid=open("&ds.","i"); - num=attrn(dsid,"nvars"); - dlm=&delim; - do i=1 to num; - varnm=upcase(varname(dsid,i)); - if i=num then dlm=''; - %if &headerformat=NAME %then %do; - header=cats(varnm,dlm); - %end; - %else %if &headerformat=LABEL %then %do; - header = cats(coalescec(varlabel(dsid,i),varnm),dlm); - %end; - %else %if &headerformat=SASJS %then %do; - if vartype(dsid,i)='C' then header=cats(varnm,':$char',varlen(dsid,i),'.'); - else do; - vfmt=coalescec(varfmt(dsid,i),'0'); - fmttype=mcf_getfmttype(vfmt); - if fmttype='DATE' then header=cats(varnm,':date9.'); - else if fmttype='DATETIME' then header=cats(varnm,':E8601DT26.6'); - else if fmttype='TIME' then header=cats(varnm,':TIME12.'); - else header=cats(varnm,':best.'); - end; - %end; - %else %do; - %put &sysmacroname: Invalid headerformat value (&headerformat); - %return; - %end; - put header @; - end; - rc=close(dsid); -run; - -%let varlist=%mf_getvarlist(&ds); -%let vcnt=%sysfunc(countw(&varlist)); - -/** - * The $quote modifier (without a width) will take the length from the variable - * and increase by two. However this will lead to truncation where the value - * contains double quotes (which are doubled up). To get around this, scan the - * data to see the max number of double quotes, so that the appropriate width - * can be applied in the subsequent step. - */ -data _null_; - set &ds end=last; -%do i=1 %to &vcnt; - %let var=%scan(&varlist,&i); - %if %mf_getvartype(&ds,&var)=C %then %do; - %let dsv1=%mf_getuniquename(prefix=csvcol1_); - %let dsv2=%mf_getuniquename(prefix=csvcol2_); - retain &dsv1 0; - &dsv2=length(&var)+countc(&var,'"'); - if &dsv2>&dsv1 then &dsv1=&dsv2; - if last then call symputx( - "vlen&i" - /* should be no shorter than varlen, and no longer than 32767 */ - ,cats('$quote',min(&dsv1+2,32767),'.') - ,'l' - ); - %end; -%end; - -%let vat=@; -%let vcom=&delim; -%let vmiss=%mf_getuniquename(prefix=csvcol3_); -/* next, export data */ -data _null_; - set &ds.; - file &outloc mod dlm=&delim dsd &outencoding lrecl=32767 termstr=&termstr; - if _n_=1 then &vmiss=' '; - %do i=1 %to &vcnt; - %let var=%scan(&varlist,&i); - %if &i=&vcnt %then %do; - %let vat=; - %let vcom=; - %end; - %if %mf_getvartype(&ds,&var)=N %then %do; - %if &headerformat = SASJS %then %do; - %let vcom=&delim; - %let fmttype=%sysfunc(mcf_getfmttype(%mf_getvarformat(&ds,&var)0)); - %if &fmttype=DATE %then %let vfmt=DATE9.; - %else %if &fmttype=DATETIME %then %let vfmt=E8601DT26.6; - %else %if &fmttype=TIME %then %let vfmt=TIME12.; - %else %do; - %let vfmt=; - %let vcom=; - %end; - %end; - %else %let vcom=; - - /* must use period - in order to work in both 9.4 and Viya 3.5 */ - if missing(&var) and &var ne %sysfunc(getoption(MISSING)) then do; - &vmiss=cats('.',&var); - put &vmiss &vat; - end; - else put &var &vfmt &vcom &vat; - - %end; - %else %do; - %if &i ne &vcnt %then %let vcom=&delim; - put &var &&vlen&i &vcom &vat; - %end; - %end; -run; - -%mend mp_ds2csv; diff --git a/005_macros/mp_filtercheck.sas b/005_macros/mp_filtercheck.sas deleted file mode 100644 index 9a4bbff..0000000 --- a/005_macros/mp_filtercheck.sas +++ /dev/null @@ -1,253 +0,0 @@ -/*** HELP START ***//** - @file - @brief Checks an input filter table for validity - @details Performs checks on the input table to ensure it arrives in the - correct format. This is necessary to prevent code injection. Will update - SYSCC to 1008 if bad records are found, and call mp_abort.sas for a - graceful service exit (configurable). - - Used for dynamic filtering in [Data Controller for SAS®]( - https://datacontroller.io). - - Usage: - - %mp_filtercheck(work.filter,targetds=sashelp.class,outds=work.badrecords) - - The input table should have the following format: - - |GROUP_LOGIC:$3|SUBGROUP_LOGIC:$3|SUBGROUP_ID:8.|VARIABLE_NM:$32|OPERATOR_NM:$10|RAW_VALUE:$4000| - |---|---|---|---|---|---| - |AND|AND|1|AGE|=|12| - |AND|AND|1|SEX|<=|'M'| - |AND|OR|2|Name|NOT IN|('Jane','Alfred')| - |AND|OR|2|Weight|>=|7| - - Rules applied: - - @li GROUP_LOGIC - only AND/OR - @li SUBGROUP_LOGIC - only AND/OR - @li SUBGROUP_ID - only integers - @li VARIABLE_NM - must be in the target table - @li OPERATOR_NM - only =/>/=/BETWEEN/IN/NOT IN/NE/CONTAINS - @li RAW_VALUE - no unquoted values except integers, commas and spaces. - - @returns The &outds table containing any bad rows, plus a REASON_CD column. - - @param [in] inds The table to be checked, with the format above - @param [in] targetds= The target dataset against which to verify VARIABLE_NM. - This must be available (ie, the library must be assigned). - @param [out] abort= (YES) If YES will call mp_abort.sas on any exceptions - @param [out] outds= (work.badrecords) The output table, which is a copy of the - &inds. table plus a REASON_CD column, containing only bad records. - If bad records are found, the SYSCC value will be set to 1008 - (a general data problem). - Downstream processes should check this table (and return code) before - continuing. - -

SAS Macros

- @li mp_abort.sas - @li mf_getuniquefileref.sas - @li mf_getvarlist.sas - @li mf_getvartype.sas - @li mp_filtergenerate.sas - @li mp_filtervalidate.sas - -

Related Macros

- @li mp_filtergenerate.sas - @li mp_filtervalidate.sas - - @version 9.3 - @author Allan Bowe - - @todo Support date / hex / name literals and exponents in RAW_VALUE field -**//*** HELP END ***/ - -%macro mp_filtercheck(inds,targetds=,outds=work.badrecords,abort=YES); - -%mp_abort(iftrue= (&syscc ne 0) - ,mac=&sysmacroname - ,msg=%str(syscc=&syscc - on macro entry) -) - -/* Validate input column */ -%local vtype; -%let vtype=%mf_getvartype(&inds,RAW_VALUE); -%mp_abort(iftrue=(&abort=YES and &vtype ne C), - mac=&sysmacroname, - msg=%str(%str(ERR)OR: RAW_VALUE must be character) -) -%if &vtype ne C %then %do; - %put &sysmacroname: RAW_VALUE must be character; - %let syscc=42; - %return; -%end; - - -/** - * Sanitise the values based on valid value lists, then strip out - * quotes, commas, periods and spaces. - * Only numeric values should remain - */ -%local reason_cd nobs; -%let nobs=0; -data &outds; - /*length GROUP_LOGIC SUBGROUP_LOGIC $3 SUBGROUP_ID 8 VARIABLE_NM $32 - OPERATOR_NM $10 RAW_VALUE $4000;*/ - set &inds; - length reason_cd $4032 vtype $1 vnum dsid 8 tmp $4000; - drop tmp; - - /* quick check to ensure column exists */ - if upcase(VARIABLE_NM) not in - (%upcase(%mf_getvarlist(&targetds,dlm=%str(,),quote=SINGLE))) - then do; - REASON_CD="Variable "!!cats(variable_nm)!!" not in &targetds"; - putlog REASON_CD= VARIABLE_NM=; - call symputx('reason_cd',reason_cd,'l'); - call symputx('nobs',_n_,'l'); - output; - return; - end; - - /* need to open the dataset to get the column type */ - dsid=open("&targetds","i"); - if dsid>0 then do; - vnum=varnum(dsid,VARIABLE_NM); - if vnum<1 then do; - /* should not happen as was also tested for above */ - REASON_CD=cats("Variable (",VARIABLE_NM,") not found in &targetds"); - putlog REASON_CD= dsid=; - call symputx('reason_cd',reason_cd,'l'); - call symputx('nobs',_n_,'l'); - output; - return; - end; - /* now we can get the type */ - else vtype=vartype(dsid,vnum); - end; - - /* closed list checks */ - if GROUP_LOGIC not in ('AND','OR') then do; - REASON_CD='GROUP_LOGIC should be AND/OR, not:'!!cats(GROUP_LOGIC); - putlog REASON_CD= GROUP_LOGIC=; - call symputx('reason_cd',reason_cd,'l'); - call symputx('nobs',_n_,'l'); - output; - end; - if SUBGROUP_LOGIC not in ('AND','OR') then do; - REASON_CD='SUBGROUP_LOGIC should be AND/OR, not:'!!cats(SUBGROUP_LOGIC); - putlog REASON_CD= SUBGROUP_LOGIC=; - call symputx('reason_cd',reason_cd,'l'); - call symputx('nobs',_n_,'l'); - output; - end; - if mod(SUBGROUP_ID,1) ne 0 then do; - REASON_CD='SUBGROUP_ID should be integer, not '!!cats(subgroup_id); - putlog REASON_CD= SUBGROUP_ID=; - call symputx('reason_cd',reason_cd,'l'); - call symputx('nobs',_n_,'l'); - output; - end; - if OPERATOR_NM not in - ('=','>','<','<=','>=','NE','GE','LE','BETWEEN','IN','NOT IN','CONTAINS') - then do; - REASON_CD='Invalid OPERATOR_NM: '!!cats(OPERATOR_NM); - putlog REASON_CD= OPERATOR_NM=; - call symputx('reason_cd',reason_cd,'l'); - call symputx('nobs',_n_,'l'); - output; - end; - - /* special missing logic */ - if vtype='N' - and OPERATOR_NM in ('=','>','<','<=','>=','NE','GE','LE') - and cats(upcase(raw_value)) in ( - '.','.A','.B','.C','.D','.E','.F','.G','.H','.I','.J','.K','.L','.M','.N' - '.N','.O','.P','.Q','.R','.S','.T','.U','.V','.W','.X','.Y','.Z','._' - ) - then do; - /* valid numeric - exit data step loop */ - return; - end; - - /* special logic */ - if OPERATOR_NM in ('IN','NOT IN','BETWEEN') then do; - if OPERATOR_NM='BETWEEN' then raw_value1=tranwrd(raw_value,' AND ',','); - else do; - if substr(raw_value,1,1) ne '(' - or substr(cats(reverse(raw_value)),1,1) ne ')' - then do; - REASON_CD='Missing start/end bracket in RAW_VALUE'; - putlog REASON_CD= OPERATOR_NM= raw_value= raw_value1= ; - call symputx('reason_cd',reason_cd,'l'); - call symputx('nobs',_n_,'l'); - output; - end; - else raw_value1=substr(raw_value,2,max(length(raw_value)-2,0)); - end; - /* we now have a comma seperated list of values */ - if vtype='N' then do i=1 to countc(raw_value1, ',')+1; - tmp=scan(raw_value1,i,','); - if cats(tmp) ne '.' and input(tmp, ?? 8.) eq . then do; - REASON_CD='Non Numeric value provided'; - putlog REASON_CD= OPERATOR_NM= raw_value= raw_value1= ; - call symputx('reason_cd',reason_cd,'l'); - call symputx('nobs',_n_,'l'); - output; - end; - return; - end; - end; - else raw_value1=raw_value; - - /* remove nested literals eg '' */ - raw_value1=tranwrd(raw_value1,"''",''); - - /* now match string literals (always single quotes) */ - raw_value2=raw_value1; - regex = prxparse("s/(\').*?(\')//"); - call prxchange(regex,-1,raw_value2); - - /* remove commas and periods*/ - raw_value3=compress(raw_value2,',.'); - - /* output records that contain values other than digits and spaces */ - if notdigit(compress(raw_value3,' '))>0 then do; - putlog raw_value3= $hex32.; - REASON_CD=cats('Invalid RAW_VALUE:',raw_value); - putlog REASON_CD= raw_value= raw_value1= raw_value2= raw_value3=; - call symputx('reason_cd',reason_cd,'l'); - call symputx('nobs',_n_,'l'); - output; - end; - -run; - - -data _null_; - set &outds end=last; - putlog (_all_)(=); -run; - -%mp_abort(iftrue=(&abort=YES and &nobs>0), - mac=&sysmacroname, - msg=%str(Data issue: %superq(reason_cd)) -) - -%if &nobs>0 %then %do; - %let syscc=1008; - %return; -%end; - -/** - * syntax checking passed but it does not mean the filter is valid - * for that we can run a proc sql validate query - */ -%local fref1; -%let fref1=%mf_getuniquefileref(); -%mp_filtergenerate(&inds,outref=&fref1) - -/* this macro will also set syscc to 1008 if any issues found */ -%mp_filtervalidate(&fref1,&targetds,outds=&outds,abort=&abort) - -%mend mp_filtercheck; diff --git a/005_macros/mp_loadformat.sas b/005_macros/mp_loadformat.sas deleted file mode 100644 index 8b40a3c..0000000 --- a/005_macros/mp_loadformat.sas +++ /dev/null @@ -1,363 +0,0 @@ -/*** HELP START ***//** - @file - @brief Loads a format catalog from a staging dataset - @details When loading staged data, it is common to receive only the records - that have actually changed. However, when loading a format catalog, if - records are missing they are presumed to be no longer required. - - This macro will augment a staging dataset with other records from the same - format, to prevent loss of data - UNLESS the input dataset contains a marker - column, specifying that a particular row needs to be deleted (`delete_col=`). - - This macro can also be used to identify which records would be (or were) - considered new, modified or deleted (`loadtarget=`) by creating the following - tables: - - @li work.outds_add - @li work.outds_del - @li work.outds_mod - - For example usage, see mp_loadformat.test.sas - - @param [in] libcat The format catalog to be loaded - @param [in] libds The staging table to load - @param [in] loadtarget= (NO) Set to YES to actually load the target catalog - @param [in] delete_col= (_____DELETE__THIS__RECORD_____) The column used to - mark a record for deletion. Values should be "Yes" or "No". - @param [out] auditlibds= (0) For change tracking, set to the libds of an audit - table as defined in mddl_dc_difftable.sas - @param [in] locklibds= (0) For multi-user (parallel) situations, set to the - libds of the DC lock table as defined in the mddl_dc_locktable.sas macro. - @param [out] outds_add= (0) Set a libds here to see the new records added - @param [out] outds_del= (0) Set a libds here to see the records deleted - @param [out] outds_mod= (0) Set a libds here to see the modified records - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages and preserve outputs - -

SAS Macros

- @li mf_getuniquename.sas - @li mf_nobs.sas - @li mp_abort.sas - @li mp_aligndecimal.sas - @li mp_cntlout.sas - @li mp_lockanytable.sas - @li mp_storediffs.sas - -

Related Macros

- @li mddl_dc_difftable.sas - @li mddl_dc_locktable.sas - @li mp_loadformat.test.1.sas - @li mp_loadformat.test.2.sas - @li mp_lockanytable.sas - @li mp_stackdiffs.sas - - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_loadformat(libcat,libds - ,loadtarget=NO - ,auditlibds=0 - ,locklibds=0 - ,delete_col=_____DELETE__THIS__RECORD_____ - ,outds_add=0 - ,outds_del=0 - ,outds_mod=0 - ,mdebug=0 -); -/* set up local macro variables and temporary tables (with a prefix) */ -%local err msg prefix dslist i var fmtlist ibufsize; -%let dslist=base_fmts template inlibds ds1 stagedata storediffs del1 del2; -%if &outds_add=0 %then %let dslist=&dslist outds_add; -%if &outds_del=0 %then %let dslist=&dslist outds_del; -%if &outds_mod=0 %then %let dslist=&dslist outds_mod; -%let prefix=%substr(%mf_getuniquename(),1,21); -%do i=1 %to %sysfunc(countw(&dslist)); - %let var=%scan(&dslist,&i); - %local &var; - %let &var=%upcase(&prefix._&var); -%end; - -/* in DC, format catalogs maybe specified in the libds with a -FC extension */ -%let libcat=%scan(&libcat,1,-); - -/* perform input validations */ -%let err=0; -%let msg=0; -data _null_; - if _n_=1 then putlog "&sysmacroname entry vars:"; - set sashelp.vmacro; - where scope="&sysmacroname"; - value=upcase(value); - if &mdebug=0 then put name '=' value; - if name=:'LOAD' and value not in ('YES','NO') then do; - call symputx('msg',"invalid value for "!!name!!":"!!value); - call symputx('err',1); - stop; - end; - else if name='LIBCAT' then do; - if exist(value,'CATALOG') le 0 then do; - call symputx('msg',"Unable to open catalog: "!!value); - call symputx('err',1); - stop; - end; - end; - else if name='LIBDS' then do; - if exist(value) le 0 then do; - call symputx('msg',"Unable to open staging table: "!!value); - call symputx('err',1); - stop; - end; - end; - else if (name=:'OUTDS' or name in ('DELETE_COL','LOCKLIBDS','AUDITLIBDS')) - and missing(value) then do; - call symputx('msg',"missing value in var: "!!name); - call symputx('err',1); - stop; - end; -run; - -%mp_abort( - iftrue=(&err ne 0) - ,mac=&sysmacroname - ,msg=%str(&msg) -) - -/** - * First, extract only relevant formats from the catalog - */ -proc sql noprint; -select distinct - case - when type='N' then upcase(fmtname) - when type='C' then cats('$',upcase(fmtname)) - when type='I' then cats('@',upcase(fmtname)) - when type='J' then cats('@$',upcase(fmtname)) - else "&sysmacroname:UNHANDLED" - end - into: fmtlist separated by ' ' - from &libds; - -%mp_cntlout(libcat=&libcat,fmtlist=&fmtlist,cntlout=&base_fmts) - -/* get a hash of the row */ -%local cvars nvars; -%let cvars=TYPE FMTNAME START END LABEL PREFIX FILL SEXCL EEXCL HLO DECSEP - DIG3SEP DATATYPE LANGUAGE; -%let nvars=FMTROW MIN MAX DEFAULT LENGTH FUZZ MULT NOEDIT; -data &base_fmts/note2err; - set &base_fmts; - fmthash=%mp_md5(cvars=&cvars, nvars=&nvars); -run; - -/** - * Ensure input table and base_formats have consistent lengths and types - */ -data &inlibds/nonote2err; - length &delete_col $3 FMTROW 8 start end label $32767; - if 0 then set &base_fmts; - set &libds; - by type fmtname notsorted; - if &delete_col='' then &delete_col='No'; - fmtname=upcase(fmtname); - type=upcase(type); - if missing(type) then do; - if substr(fmtname,1,1)='@' then do; - if substr(fmtname,2,1)='$' then type='J'; - else type='I'; - end; - else do; - if substr(fmtname,1,1)='$' then type='C'; - else type='N'; - end; - end; - if type in ('N','I') then do; - %mp_aligndecimal(start,width=16) - %mp_aligndecimal(end,width=16) - end; - - /* update row marker - retain new var as fmtrow may already be in libds */ - if first.fmtname then row=1; - else row+1; - drop row; - fmtrow=row; - - fmthash=%mp_md5(cvars=&cvars, nvars=&nvars); -run; - -/** - * Identify new records - */ -proc sql; -create table &outds_add(drop=&delete_col) as - select a.* - from &inlibds a - left join &base_fmts b - on a.type=b.type and a.fmtname=b.fmtname and a.fmtrow=b.fmtrow - where b.fmtname is null - and upcase(a.&delete_col) ne "YES" - order by type, fmtname, fmtrow; - -/** - * Identify modified records - */ -create table &outds_mod (drop=&delete_col) as - select a.* - from &inlibds a - inner join &base_fmts b - on a.type=b.type and a.fmtname=b.fmtname and a.fmtrow=b.fmtrow - where upcase(a.&delete_col) ne "YES" - and a.fmthash ne b.fmthash - order by type, fmtname, fmtrow; - -/** - * Identify deleted records - */ -create table &outds_del(drop=&delete_col) as - select a.* - from &inlibds a - inner join &base_fmts b - on a.type=b.type and a.fmtname=b.fmtname and a.fmtrow=b.fmtrow - where upcase(a.&delete_col)="YES" - order by type, fmtname, fmtrow; - -/** - * Identify fully deleted formats (where every record is removed) - * These require to be explicitly deleted in proc format - * del1 - identify _partial_ deletes - * del2 - exclude these, and also formats that come with _additions_ - */ -create table &del1 as - select a.* - from &base_fmts a - left join &outds_del b - on a.type=b.type and a.fmtname=b.fmtname and a.fmtrow=b.fmtrow - where b.fmtrow is null; - -create table &del2 as - select * from &outds_del - where cats(type,fmtname) not in (select cats(type,fmtname) from &outds_add) - and cats(type,fmtname) not in (select cats(type,fmtname) from &del1); - - -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=&sysmacroname - ,msg=%str(SYSCC=&syscc prior to load prep) -) - -%if &loadtarget=YES %then %do; - /* new records plus base records that are not deleted or modified */ - data &ds1; - merge &base_fmts(in=base) - &outds_mod(in=mod) - &outds_add(in=add) - &outds_del(in=del); - if not del and not mod; - by type fmtname fmtrow; - run; - /* add back the modified records */ - data &stagedata; - set &ds1 &outds_mod; - run; - proc sort; - by type fmtname fmtrow; - run; -%end; -/* mp abort needs to run outside of conditional blocks */ -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=&sysmacroname - ,msg=%str(SYSCC=&syscc prior to actual load) -) -%if &loadtarget=YES %then %do; - %if %mf_nobs(&stagedata)=0 and %mf_nobs(&del2)=0 %then %do; - %put There are no changes to load in &libcat!; - %return; - %end; - %if &locklibds ne 0 %then %do; - /* prevent parallel updates */ - %mp_lockanytable(LOCK - ,lib=%scan(&libcat,1,.) - ,ds=%scan(&libcat,2,.)-FC - ,ref=MP_LOADFORMAT commencing format load - ,ctl_ds=&locklibds - ) - %end; - /* do the actual load */ - proc format lib=&libcat cntlin=&stagedata; - run; - /* apply any full deletes */ - %if %mf_nobs(&del2)>0 %then %do; - %local delfmtlist; - proc sql noprint; - select distinct case when type='N' then cats(fmtname,'.FORMAT') - when type='C' then cats(fmtname,'.FORMATC') - when type='J' then cats(fmtname,'.INFMTC') - when type='I' then cats(fmtname,'.INFMT') - else cats(fmtname,'.BADENTRY!!!') end - into: delfmtlist - separated by ' ' - from &del2; - proc catalog catalog=&libcat; - delete &delfmtlist; - quit; - %end; - %if &locklibds ne 0 %then %do; - /* unlock the table */ - %mp_lockanytable(UNLOCK - ,lib=%scan(&libcat,1,.) - ,ds=%scan(&libcat,2,.)-FC - ,ref=MP_LOADFORMAT completed format load - ,ctl_ds=&locklibds - ) - %end; - /* track the changes */ - %if &auditlibds ne 0 %then %do; - %if &locklibds ne 0 %then %do; - %mp_lockanytable(LOCK - ,lib=%scan(&auditlibds,1,.) - ,ds=%scan(&auditlibds,2,.) - ,ref=MP_LOADFORMAT commencing audit table load - ,ctl_ds=&locklibds - ) - %end; - - %mp_storediffs(&libcat-FC - ,&base_fmts - ,TYPE FMTNAME FMTROW - ,delds=&outds_del - ,modds=&outds_mod - ,appds=&outds_add - ,outds=&storediffs - ,mdebug=&mdebug - ) - - proc append base=&auditlibds data=&storediffs; - run; - - %if &locklibds ne 0 %then %do; - %mp_lockanytable(UNLOCK - ,lib=%scan(&auditlibds,1,.) - ,ds=%scan(&auditlibds,2,.) - ,ref=MP_LOADFORMAT commencing audit table load - ,ctl_ds=&locklibds - ) - %end; - %end; -%end; -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=&sysmacroname - ,msg=%str(SYSCC=&syscc after load) -) - -%if &mdebug=0 %then %do; - proc datasets lib=work; - delete &prefix:; - run; - %put &sysmacroname exit vars:; - %put _local_; -%end; -%mend mp_loadformat; diff --git a/005_macros/mp_stackdiffs.sas b/005_macros/mp_stackdiffs.sas deleted file mode 100644 index 24bb88b..0000000 --- a/005_macros/mp_stackdiffs.sas +++ /dev/null @@ -1,592 +0,0 @@ -/*** HELP START ***//** - @file - @brief Prepares an audit table for stacking (re-applying) the changes. - @details When the underlying data from a Base Table is refreshed, it can be - helpful to have any previously-applied changes, re-applied. - - Such situation might arise if you are applying those changes using a tool - like [Data Controller for SAS®](https://datacontroller.io) - which records - all such changes in an audit table. - It may also apply if you are preparing a series of specific cell-level - transactions, that you would like to apply to multiple sets of (similarly - structured) Base Tables. - - In both cases, it is necessary that the transactions are stored using - the mp_storediffs.sas macro, or at least that the underlying table is - structured as per the definition in mp_coretable.sas (DIFFTABLE entry) - - This macro is used to convert the stored changes (tall format) into - staged changes (wide format), with base table values incorporated (in the - case of modified rows), ready for the subsequent load process. - - Essentially then, what this macro does, is turn a table like this: - -|KEY_HASH:$32.|MOVE_TYPE:$1.|TGTVAR_NM:$32.|IS_PK:best.|IS_DIFF:best.|TGTVAR_TYPE:$1.|OLDVAL_NUM:best32.|NEWVAL_NUM:best32.|OLDVAL_CHAR:$32765.|NEWVAL_CHAR:$32765.| -|---|---|---|---|---|---|---|---|---|---| -|`27AA6F7581052E7FF48E1BCA901313FB `|`A `|`NAME `|`1 `|`-1 `|`C `|`. `|`. `|` `|`Newbie `| -|`27AA6F7581052E7FF48E1BCA901313FB `|`A `|`AGE `|`0 `|`-1 `|`N `|`. `|`13 `|` `|` `| -|`27AA6F7581052E7FF48E1BCA901313FB `|`A `|`HEIGHT `|`0 `|`-1 `|`N `|`. `|`65.3 `|` `|` `| -|`27AA6F7581052E7FF48E1BCA901313FB `|`A `|`SEX `|`0 `|`-1 `|`C `|`. `|`. `|` `|`F `| -|`27AA6F7581052E7FF48E1BCA901313FB `|`A `|`WEIGHT `|`0 `|`-1 `|`N `|`. `|`98 `|` `|` `| -|`86703FDE9E87DD5C0F8E1072545D0128 `|`D `|`NAME `|`1 `|`-1 `|`C `|`. `|`. `|`Alfred `|` `| -|`86703FDE9E87DD5C0F8E1072545D0128 `|`D `|`AGE `|`0 `|`-1 `|`N `|`14 `|`. `|` `|` `| -|`86703FDE9E87DD5C0F8E1072545D0128 `|`D `|`HEIGHT `|`0 `|`-1 `|`N `|`69 `|`. `|` `|` `| -|`86703FDE9E87DD5C0F8E1072545D0128 `|`D `|`SEX `|`0 `|`-1 `|`C `|`. `|`. `|`M `|` `| -|`86703FDE9E87DD5C0F8E1072545D0128 `|`D `|`WEIGHT `|`0 `|`-1 `|`N `|`112.5 `|`. `|` `|` `| -|`64489C85DC2FE0787B85CD87214B3810 `|`M `|`NAME `|`1 `|`0 `|`C `|`. `|`. `|`Alice `|`Alice `| -|`64489C85DC2FE0787B85CD87214B3810 `|`M `|`AGE `|`0 `|`1 `|`N `|`13 `|`99 `|` `|` `| -|`64489C85DC2FE0787B85CD87214B3810 `|`M `|`HEIGHT `|`0 `|`0 `|`N `|`56.5 `|`56.5 `|` `|` `| -|`64489C85DC2FE0787B85CD87214B3810 `|`M `|`SEX `|`0 `|`0 `|`C `|`. `|`. `|`F `|`F `| -|`64489C85DC2FE0787B85CD87214B3810 `|`M `|`WEIGHT `|`0 `|`0 `|`N `|`84 `|`84 `|` `|` `| - - Into three tables like this: - - `work.outmod`: - |NAME:$8.|SEX:$1.|AGE:best.|HEIGHT:best.|WEIGHT:best.| - |---|---|---|---|---| - |`Alice `|`F `|`99 `|`56.5 `|`84 `| - - `work.outadd`: - |NAME:$8.|SEX:$1.|AGE:best.|HEIGHT:best.|WEIGHT:best.| - |---|---|---|---|---| - |`Newbie `|`F `|`13 `|`65.3 `|`98 `| - - `work.outdel`: - |NAME:$8.|SEX:$1.|AGE:best.|HEIGHT:best.|WEIGHT:best.| - |---|---|---|---|---| - |`Alfred `|`M `|`14 `|`69 `|`112.5 `| - - As you might expect, there are a bunch of extra features and checks. - - The macro supports both SCD2 (TXTEMPORAL) and UPDATE loadtypes. If the - base table contains a PROCESSED_DTTM column (or similar), this can be - ignored by declaring it in the `processed_dttm_var` parameter. - - The macro is also flexible where columns have been added or removed from - the base table UNLESS there is a change to the primary key. - - Changes to the primary key fields are NOT supported, and are likely to cause - unexpected results. - - The following pre-flight checks are made: - - @li All primary key columns exist on the base table - @li There is no change in variable TYPE for any of the columns - @li There is no reduction in variable LENGTH below the max-length of the - supplied values - - Rules for stacking changes are as follows: - - - - - - - - - - - - - - - - - - - - -
Transaction TypeKey BehaviourColumn Behaviour
Deletes - The row is added to `&outDEL.` UNLESS it no longer exists - in the base table, in which case it is added to `&errDS.` instead. - - Deletes are unaffected by the addition or removal of non Primary-Key - columns. -
Inserts - Previously newly added rows are added to the `outADD` table UNLESS they - are present in the Base table.
In this case they are added to the - `&errDS.` table instead. -
- Inserts are unaffected by the addition of columns in the Base Table - (they are padded with blanks). Deleted columns are only a problem if - they appear on the previous insert - in which case the record is added - to `&errDS.`. -
Updates - Previously modified rows are merged with base table values such that - only the individual cells that were _previously_ changed are re-applied. - Where the row contains cells that were not marked as having changed in - the prior transaction, the 'blanks' are filled with base table values in - the `outMOD` table.
- If the row no longer exists on the base table, then the row is added to - the `errDS` table instead. -
- Updates are unaffected by the addition of columns in the Base Table - - the new cells are simply populated with Base Table values. Deleted - columns are only a problem if they relate to a modified cell - (`is_diff=1`) - in which case the record is added to `&errDS.`. -
- - To illustrate the above with a diagram: - - @dot - digraph { - rankdir="TB" - start[label="Transaction Type?" shape=Mdiamond] - del[label="Does Base Row exist?" shape=rectangle] - add [label="Does Base Row exist?" shape=rectangle] - mod [label="Does Base Row exist?" shape=rectangle] - chkmod [label="Do all modified\n(is_diff=1) cells exist?" shape=rectangle] - chkadd [label="Do all inserted cells exist?" shape=rectangle] - outmod [label="outMOD\nTable" shape=Msquare style=filled] - outadd [label="outADD\nTable" shape=Msquare style=filled] - outdel [label="outDEL\nTable" shape=Msquare style=filled] - outerr [label="ErrDS Table" shape=Msquare fillcolor=Orange style=filled] - start -> del [label="Delete"] - start -> add [label="Insert"] - start -> mod [label="Update"] - - del -> outdel [label="Yes"] - del -> outerr [label="No" color="Red" fontcolor="Red"] - add -> chkadd [label="No"] - add -> outerr [label="Yes" color="Red" fontcolor="Red"] - mod -> outerr [label="No" color="Red" fontcolor="Red"] - mod -> chkmod [label="Yes"] - chkmod -> outerr [label="No" color="Red" fontcolor="Red"] - chkmod -> outmod [label="Yes"] - chkadd -> outerr [label="No" color="Red" fontcolor="Red"] - chkadd -> outadd [label="Yes"] - - } - @enddot - - For examples of usage, check out the mp_stackdiffs.test.sas program. - - - @param [in] baselibds Base Table against which the changes will be applied, - in libref.dataset format. - @param [in] auditlibds Dataset with previously applied transactions, to be - re-applied. Use libref.dataset format. - DDL as follows: %mp_coretable(DIFFTABLE) - @param [in] key Space seperated list of key variables - @param [in] mdebug= Set to 1 to enable DEBUG messages and preserve outputs - @param [in] processed_dttm_var= (0) If a variable is being used to mark - the processed datetime, put the name of the variable here. It will NOT - be included in the staged dataset (the load process is expected to - provide this) - @param [out] errds= (work.errds) Output table containing problematic records. - The columns of this table are: - @li PK_VARS - Space separated list of primary key variable names - @li PK_VALS - Slash separted list of PK variable values - @li ERR_MSG - Explanation of why this record is problematic - @param [out] outmod= (work.outmod) Output table containing modified records - @param [out] outadd= (work.outadd) Output table containing additional records - @param [out] outdel= (work.outdel) Output table containing deleted records - -

SAS Macros

- @li mf_existvarlist.sas - @li mf_getquotedstr.sas - @li mf_getuniquefileref.sas - @li mf_getuniquename.sas - @li mf_islibds.sas - @li mf_nobs.sas - @li mf_wordsinstr1butnotstr2.sas - @li mp_abort.sas - @li mp_ds2squeeze.sas - -

Related Macros

- @li mp_coretable.sas - @li mp_stackdiffs.test.sas - @li mp_storediffs.sas - - @todo The current approach assumes that a variable called KEY_HASH is not on - the base table. This part will need to be refactored (eg using - mf_getuniquename.sas) when such a use case arises. - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ -/** @cond */ - -%macro mp_stackdiffs(baselibds - ,auditlibds - ,key - ,mdebug=0 - ,processed_dttm_var=0 - ,errds=work.errds - ,outmod=work.outmod - ,outadd=work.outadd - ,outdel=work.outdel -)/*/STORE SOURCE*/; -%local dbg; -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; -%end; -%else %let dbg=*; - -/* input parameter validations */ -%mp_abort(iftrue= (%mf_islibds(&baselibds) ne 1) - ,mac=&sysmacroname - ,msg=%str(Invalid baselibds: &baselibds) -) -%mp_abort(iftrue= (%mf_islibds(&auditlibds) ne 1) - ,mac=&sysmacroname - ,msg=%str(Invalid auditlibds: &auditlibds) -) -%mp_abort(iftrue= (%length(&key)=0) - ,mac=&sysmacroname - ,msg=%str(Missing key variables!) -) -%mp_abort(iftrue= ( - %mf_existVarList(&auditlibds,LIBREF DSN MOVE_TYPE KEY_HASH TGTVAR_NM IS_PK - IS_DIFF TGTVAR_TYPE OLDVAL_NUM NEWVAL_NUM OLDVAL_CHAR NEWVAL_CHAR)=0 - ) - ,mac=&sysmacroname - ,msg=%str(Input &auditlibds is missing required columns!) -) - - -/* set up macro vars */ -%local prefix dslist x var keyjoin commakey keepvars missvars fref; -%let prefix=%substr(%mf_getuniquename(),1,25); -%let dslist=ds1d ds2d ds3d ds1a ds2a ds3a ds1m ds2m ds3m pks dups base - delrec delerr addrec adderr modrec moderr; -%do x=1 %to %sysfunc(countw(&dslist)); - %let var=%scan(&dslist,&x); - %local &var; - %let &var=%upcase(&prefix._&var); -%end; - -%let key=%upcase(&key); -%let commakey=%mf_getquotedstr(&key,quote=N); - -%let keyjoin=1=1; -%do x=1 %to %sysfunc(countw(&key)); - %let var=%scan(&key,&x); - %let keyjoin=&keyjoin and a.&var=b.&var; -%end; - -data &errds; - length pk_vars $256 pk_vals $4098 err_msg $512; - call missing (of _all_); - stop; -run; - -/** - * Prepare raw DELETE table - * Records are in the OLDVAL_xxx columns - */ -%let keepvars=MOVE_TYPE KEY_HASH TGTVAR_NM TGTVAR_TYPE IS_PK - OLDVAL_NUM OLDVAL_CHAR - NEWVAL_NUM NEWVAL_CHAR; -proc sort data=&auditlibds(where=(move_type='D') keep=&keepvars) - out=&ds1d(drop=move_type); -by KEY_HASH TGTVAR_NM; -run; -proc transpose data=&ds1d(where=(tgtvar_type='N')) - out=&ds2d(drop=_name_); - by KEY_HASH; - id TGTVAR_NM; - var OLDVAL_NUM; -run; -proc transpose data=&ds1d(where=(tgtvar_type='C')) - out=&ds3d(drop=_name_); - by KEY_HASH; - id TGTVAR_NM; - var OLDVAL_CHAR; -run; -%mp_ds2squeeze(&ds2d,outds=&ds2d) -%mp_ds2squeeze(&ds3d,outds=&ds3d) -data &outdel; - if 0 then set &baselibds; - set &ds2d; - set &ds3d; - drop key_hash; - if not missing(%scan(&key,1)); -run; -proc sort; - by &key; -run; - -/** - * Prepare raw APPEND table - * Records are in the NEWVAL_xxx columns - */ -proc sort data=&auditlibds(where=(move_type='A') keep=&keepvars) - out=&ds1a(drop=move_type); - by KEY_HASH TGTVAR_NM; -run; -proc transpose data=&ds1a(where=(tgtvar_type='N')) - out=&ds2a(drop=_name_); - by KEY_HASH; - id TGTVAR_NM; - var NEWVAL_NUM; -run; -proc transpose data=&ds1a(where=(tgtvar_type='C')) - out=&ds3a(drop=_name_); - by KEY_HASH; - id TGTVAR_NM; - var NEWVAL_CHAR; -run; -%mp_ds2squeeze(&ds2a,outds=&ds2a) -%mp_ds2squeeze(&ds3a,outds=&ds3a) -data &outadd; - if 0 then set &baselibds; - set &ds2a; - set &ds3a; - drop key_hash; - if not missing(%scan(&key,1)); -run; -proc sort; - by &key; -run; - -/** - * Prepare raw MODIFY table - * Keep only primary key - will add modified values later - */ -proc sort data=&auditlibds( - where=(move_type='M' and is_pk=1) keep=&keepvars - ) out=&ds1m(drop=move_type); - by KEY_HASH TGTVAR_NM; -run; -proc transpose data=&ds1m(where=(tgtvar_type='N')) - out=&ds2m(drop=_name_); - by KEY_HASH ; - id TGTVAR_NM; - var NEWVAL_NUM; -run; -proc transpose data=&ds1m(where=(tgtvar_type='C')) - out=&ds3m(drop=_name_); - by KEY_HASH; - id TGTVAR_NM; - var NEWVAL_CHAR; -run; -%mp_ds2squeeze(&ds2m,outds=&ds2m) -%mp_ds2squeeze(&ds3m,outds=&ds3m) -data &outmod; - if 0 then set &baselibds; - set &ds2m; - set &ds3m; - if not missing(%scan(&key,1)); -run; -proc sort; - by &key; -run; - -/** - * Extract matching records from the base table - * Do this in one join for efficiency. - * At a later date, this should be optimised for large database tables by using - * passthrough and a temporary table. - */ -data &pks; - if 0 then set &baselibds; - set &outadd &outmod &outdel; - keep &key; -run; - -proc sort noduprec dupout=&dups; -by &key; -run; -data _null_; - set &dups; - putlog (_all_)(=); -run; -%mp_abort(iftrue= (%mf_nobs(&dups) ne 0) - ,mac=&sysmacroname - ,msg=%str(duplicates (%mf_nobs(&dups)) found on &auditlibds!) -) - -proc sql; -create table &base as - select a.* - from &baselibds a, &pks b - where &keyjoin; - -/** - * delete check - * This is straightforward as it relates to records only - */ -proc sql; -create table &delrec as - select a.* - from &outdel a - left join &base b - on &keyjoin - where b.%scan(&key,1) is null - order by &commakey; - -data &delerr; - if 0 then set &errds; - set &delrec; - PK_VARS="&key"; - PK_VALS=catx('/',&commakey); - ERR_MSG="Rows cannot be deleted as they do not exist on the Base dataset"; - keep PK_VARS PK_VALS ERR_MSG; -run; -proc append base=&errds data=&delerr; -run; - -data &outdel; - merge &outdel (in=a) &delrec (in=b); - by &key; - if not b; -run; - -/** - * add check - * Problems - where record already exists, or base table has columns missing - */ -%let missvars=%mf_wordsinstr1butnotstr2( - Str1=%upcase(%mf_getvarlist(&outadd)), - Str2=%upcase(%mf_getvarlist(&baselibds)) -); -%if %length(&missvars)>0 %then %do; - /* add them to the err table */ - data &adderr; - if 0 then set &errds; - set &outadd; - PK_VARS="&key"; - PK_VALS=catx('/',&commakey); - ERR_MSG="Rows cannot be added due to missing base vars: &missvars"; - keep PK_VARS PK_VALS ERR_MSG; - run; - proc append base=&errds data=&adderr; - run; - proc sql; - delete * from &outadd; -%end; -%else %do; - proc sql; - /* find records that already exist on base table */ - create table &addrec as - select a.* - from &outadd a - inner join &base b - on &keyjoin - order by &commakey; - - /* add them to the err table */ - data &adderr; - if 0 then set &errds; - set &addrec; - PK_VARS="&key"; - PK_VALS=catx('/',&commakey); - ERR_MSG="Rows cannot be added as they already exist on the Base dataset"; - keep PK_VARS PK_VALS ERR_MSG; - run; - proc append base=&errds data=&adderr; - run; - - /* remove invalid rows from the outadd table */ - data &outadd; - merge &outadd (in=a) &addrec (in=b); - by &key; - if not b; - run; -%end; - -/** - * mod check - * Problems - where record does not exist or baseds has modified cols missing - */ -proc sql noprint; -select distinct tgtvar_nm into: missvars separated by ' ' - from &auditlibds - where move_type='M' and is_diff=1; -%let missvars=%mf_wordsinstr1butnotstr2( - Str1=&missvars, - Str2=%upcase(%mf_getvarlist(&baselibds)) -); -%if %length(&missvars)>0 %then %do; - /* add them to the err table */ - data &moderr; - if 0 then set &errds; - set &outmod; - PK_VARS="&key"; - PK_VALS=catx('/',&commakey); - ERR_MSG="Rows cannot be modified due to missing base vars: &missvars"; - keep PK_VARS PK_VALS ERR_MSG; - run; - proc append base=&errds data=&moderr; - run; - proc sql; - delete * from &outmod; -%end; -%else %do; - /* now check for records that do not exist (therefore cannot be modified) */ - proc sql; - create table &modrec as - select a.* - from &outmod a - left join &base b - on &keyjoin - where b.%scan(&key,1) is null - order by &commakey; - data &moderr; - if 0 then set &errds; - set &modrec; - PK_VARS="&key"; - PK_VALS=catx('/',&commakey); - ERR_MSG="Rows cannot be modified as they do not exist on the Base dataset"; - keep PK_VARS PK_VALS ERR_MSG; - run; - proc append base=&errds data=&moderr; - run; - /* delete the above records from the outmod table */ - data &outmod; - merge &outmod (in=a) &modrec (in=b); - by &key; - if not b; - run; - /* now - we can prepare the final MOD table (which is currently PK only) */ - proc sql undo_policy=none; - create table &outmod as - select a.key_hash - ,b.* - from &outmod a - inner join &base b - on &keyjoin - order by &commakey; - /* now - to update outmod with modified (is_diff=1) values */ - %let fref=%mf_getuniquefileref(); - data _null_; - file &fref; - set &auditlibds(where=(move_type='M')) end=lastobs; - by key_hash; - retain comma 'N'; - if _n_=1 then put 'proc sql;'; - if first.key_hash then do; - comma='N'; - put "update &outmod set " @@; - end; - if is_diff=1 then do; - if comma='N' then do; - put ' '@@; - comma='Y'; - end; - else put ' ,'@@; - if tgtvar_type='C' then do; - length qstr $32767; - qstr=quote(trim(NEWVAL_CHAR)); - put tgtvar_nm '=' qstr; - end; - else put tgtvar_nm '=' newval_num; - if comma=' ' then comma=' ,'; - end; - if last.key_hash then put ' where key_hash=trim("' key_hash '");'; - if lastobs then put "alter table &outmod drop key_hash;"; - run; - %inc &fref/source2; -%end; - -%if &mdebug=0 %then %do; - proc datasets lib=work; - delete &prefix:; - run; - %put &sysmacroname exit vars:; - %put _local_; -%end; -%mend mp_stackdiffs; -/** @endcond */ diff --git a/005_macros/mp_storediffs.sas b/005_macros/mp_storediffs.sas deleted file mode 100644 index ffad990..0000000 --- a/005_macros/mp_storediffs.sas +++ /dev/null @@ -1,229 +0,0 @@ -/*** HELP START ***//** - @file - @brief Converts deletes/changes/appends into a single audit table. - @details When tracking changes to data over time, it can be helpful to have - a single base table to track ALL modifications - enabling audit trail, - data recovery, and change re-application. This macro is one of many - data management utilities used in [Data Controller for SAS]( - https:datacontroller.io) - a comprehensive data ingestion solution, which - works on any SAS platform (Viya, SAS 9, Foundation). - - NOTE - this macro does not validate the inputs. It is assumed that the - datasets containing the new / changed / deleted rows are CORRECT, contain - no additional (or missing columns), and that the originals dataset contains - all relevant base records (and no additionals). - - Usage: - - data work.orig work.deleted work.changed work.appended; - set sashelp.class; - if _n_=1 then do; - output work.orig work.deleted; - end; - else if _n_=2 then do; - output work.orig; - age=99; - output work.changed; - end; - else do; - name='Newbie'; - output work.appended; - stop; - end; - run; - - %mp_storediffs(sashelp.class,work.orig,NAME - ,delds=work.deleted - ,modds=work.changed - ,appds=work.appended - ,outds=work.final - ,mdebug=1 - ) - - @param [in] libds Target table against which the changes were applied - @param [in] origds Dataset with original (unchanged) records. Can be empty if - only appending. - @param [in] key Space seperated list of key variables - @param [in] delds= (0) Dataset with deleted records - @param [in] appds= (0) Dataset with appended records - @param [in] modds= (0) Dataset with modified records - @param [out] outds= (work.mp_storediffs) Output table containing stored data. - DDL as follows: %mp_coretable(DIFFTABLE) - - @param [in] processed_dttm= (0) Provide a datetime constant in relation to - the actual load time. If not provided, current timestamp is used. - @param [in] mdebug= set to 1 to enable DEBUG messages and preserve outputs - @param [out] loadref= (0) Provide a unique key to reference the load, - otherwise a UUID will be generated. - -

SAS Macros

- @li mf_getquotedstr.sas - @li mf_getuniquename.sas - @li mf_getvarlist.sas - -

Related Macros

- @li mp_stackdiffs.sas - @li mp_storediffs.test.sas - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ -/** @cond */ - -%macro mp_storediffs(libds - ,origds - ,key - ,delds=0 - ,appds=0 - ,modds=0 - ,outds=work.mp_storediffs - ,loadref=0 - ,processed_dttm=0 - ,mdebug=0 -)/*/STORE SOURCE*/; -%local dbg; -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; -%end; -%else %let dbg=*; - -/* set up unique and temporary vars */ -%local ds1 ds2 ds3 ds4 hashkey inds_auto inds_keep dslist vlist; -%let ds1=%upcase(work.%mf_getuniquename(prefix=mpsd_ds1)); -%let ds2=%upcase(work.%mf_getuniquename(prefix=mpsd_ds2)); -%let ds3=%upcase(work.%mf_getuniquename(prefix=mpsd_ds3)); -%let ds4=%upcase(work.%mf_getuniquename(prefix=mpsd_ds4)); -%let hashkey=%upcase(%mf_getuniquename(prefix=mpsd_hashkey)); -%let inds_auto=%upcase(%mf_getuniquename(prefix=mpsd_inds_auto)); -%let inds_keep=%upcase(%mf_getuniquename(prefix=mpsd_inds_keep)); - -%let dslist=&origds; -%if &delds ne 0 %then %do; - %let delds=%upcase(&delds); - %if %scan(&delds,-1,.)=&delds %then %let delds=WORK.&delds; - %let dslist=&dslist &delds; -%end; -%if &appds ne 0 %then %do; - %let appds=%upcase(&appds); - %if %scan(&appds,-1,.)=&appds %then %let appds=WORK.&appds; - %let dslist=&dslist &appds; -%end; -%if &modds ne 0 %then %do; - %let modds=%upcase(&modds); - %if %scan(&modds,-1,.)=&modds %then %let modds=WORK.&modds; - %let dslist=&dslist &modds; -%end; - -%let origds=%upcase(&origds); -%if %scan(&origds,-1,.)=&origds %then %let origds=WORK.&origds; - -%let key=%upcase(&key); - -/* hash the key and append all the tables (marking the source) */ -data &ds1; - set &dslist indsname=&inds_auto; - &hashkey=put(md5(catx('|',%mf_getquotedstr(&key,quote=N))),$hex32.); - &inds_keep=upcase(&inds_auto); -proc sort; - by &inds_keep &hashkey; -run; - -/* transpose numeric & char vars */ -proc transpose data=&ds1 - out=&ds2(rename=(&hashkey=key_hash _name_=tgtvar_nm col1=newval_num)); - by &inds_keep &hashkey; - var _numeric_; -run; -proc transpose data=&ds1 - out=&ds3( - rename=(&hashkey=key_hash _name_=tgtvar_nm col1=newval_char) - where=(tgtvar_nm not in ("&hashkey","&inds_keep")) - ); - by &inds_keep &hashkey; - var _character_; -run; - -%if %index(&libds,-)>0 and %scan(&libds,2,-)=FC %then %do; - /* this is a format catalog - cannot query cols directly */ - %let vlist="TYPE","FMTNAME","FMTROW","START","END","LABEL","MIN","MAX" - ,"DEFAULT","LENGTH","FUZZ","PREFIX","MULT","FILL","NOEDIT","SEXCL" - ,"EEXCL","HLO","DECSEP","DIG3SEP","DATATYPE","LANGUAGE"; -%end; -%else %let vlist=%mf_getvarlist(&libds,dlm=%str(,),quote=DOUBLE); - -data &ds4; - length &inds_keep $41 tgtvar_nm $32 _label_ $256; - if _n_=1 then call missing(_label_); - drop _label_; - set &ds2 &ds3 indsname=&inds_auto; - - tgtvar_nm=upcase(tgtvar_nm); - if tgtvar_nm in (%upcase(&vlist)); - - if upcase(&inds_auto)="&ds2" then tgtvar_type='N'; - else if upcase(&inds_auto)="&ds3" then tgtvar_type='C'; - else do; - putlog 'ERR' +(-1) "OR: unidentified vartype input!" &inds_auto; - call symputx('syscc',98); - end; - - if &inds_keep="&appds" then move_type='A'; - else if &inds_keep="&delds" then move_type='D'; - else if &inds_keep="&modds" then move_type='M'; - else if &inds_keep="&origds" then move_type='O'; - else do; - putlog 'ERR' +(-1) "OR: unidentified movetype input!" &inds_keep; - call symputx('syscc',99); - end; - tgtvar_nm=upcase(tgtvar_nm); - if tgtvar_nm in (%mf_getquotedstr(&key)) then is_pk=1; - else is_pk=0; - drop &inds_keep; -run; - -%if "&loadref"="0" %then %let loadref=%sysfunc(uuidgen()); -%if &processed_dttm=0 %then %let processed_dttm=%sysfunc(datetime()); -%let libds=%upcase(&libds); - -/* join orig vals for modified & deleted */ -proc sql; -create table &outds as - select "&loadref" as load_ref length=36 - ,&processed_dttm as processed_dttm format=E8601DT26.6 - ,"%scan(&libds,1,.)" as libref length=8 - ,"%scan(&libds,2,.)" as dsn length=32 - ,b.key_hash length=32 - ,b.move_type length=1 - ,b.tgtvar_nm length=32 - ,b.is_pk - ,case when b.move_type ne 'M' then -1 - when a.newval_num=b.newval_num and a.newval_char=b.newval_char then 0 - else 1 - end as is_diff - ,b.tgtvar_type length=1 - ,case when b.move_type='D' then b.newval_num - else a.newval_num - end as oldval_num format=best32. - ,case when b.move_type='D' then . - else b.newval_num - end as newval_num format=best32. - ,case when b.move_type='D' then b.newval_char - else a.newval_char - end as oldval_char length=32765 - ,case when b.move_type='D' then '' - else b.newval_char - end as newval_char length=32765 - from &ds4(where=(move_type='O')) as a - right join &ds4(where=(move_type ne 'O')) as b - on a.tgtvar_nm=b.tgtvar_nm - and a.key_hash=b.key_hash - order by move_type, key_hash,is_pk desc, tgtvar_nm; - -%if &mdebug=0 %then %do; - proc sql; - drop table &ds1, &ds2, &ds3, &ds4; -%end; - -%mend mp_storediffs; -/** @endcond */ diff --git a/005_macros/mp_validatecol.sas b/005_macros/mp_validatecol.sas deleted file mode 100644 index 440501d..0000000 --- a/005_macros/mp_validatecol.sas +++ /dev/null @@ -1,97 +0,0 @@ -/*** HELP START ***//** - @file - @brief Used to validate variables in a dataset - @details Useful when sanitising inputs, to ensure that they arrive with a - certain pattern. - Usage: - - data test; - infile datalines4 dsd; - input; - libds=_infile_; - %mp_validatecol(libds,LIBDS,is_libds) - datalines4; - some.libname - !lib.blah - %abort - definite.ok - not.ok! - nineletrs._ - ;;;; - run; - - For more examples, see mp_validatecol.test.sas - - Tip - when contributing, use https://regex101.com to test the regex validity! - - @param [in] incol The column to be validated - @param [in] rule The rule to apply. Current rules: - @li ISINT - checks if the variable is an integer - @li ISNUM - checks if the variable is numeric - @li LIBDS - matches LIBREF.DATASET format - @li FORMAT - checks if the provided format is syntactically valid - @param [out] outcol The variable to create, with the results of the match - -

SAS Macros

- @li mf_getuniquename.sas - -

Related Macros

- @li mp_validatecol.test.sas - - @version 9.3 -**//*** HELP END ***/ - -%macro mp_validatecol(incol,rule,outcol); - -/* tempcol is given a unique name with every invocation */ -%local tempcol; -%let tempcol=%mf_getuniquename(); - -%if &rule=ISINT %then %do; - &outcol=0; - if not missing(&incol) then do; - &tempcol=input(&incol,?? best32.); - if not missing(&tempcol) then if mod(&tempcol,1)=0 then &outcol=1; - end; - drop &tempcol; -%end; -%else %if &rule=ISNUM %then %do; - /* - credit SØREN LASSEN - https://sasmacro.blogspot.com/2009/06/welcome-isnum-macro.html - */ - &tempcol=input(&incol,?? best32.); - if missing(&tempcol) then &outcol=0; - else &outcol=1; - drop &tempcol; -%end; -%else %if &rule=LIBDS %then %do; - /* match libref.dataset */ - if _n_=1 then do; - retain &tempcol; - &tempcol=prxparse('/^[_a-z]\w{0,7}\.[_a-z]\w{0,31}$/i'); - if missing(&tempcol) then do; - putlog 'ERR' +(-1) "OR: Invalid expression for LIBDS"; - stop; - end; - drop &tempcol; - end; - if prxmatch(&tempcol, trim(&incol)) then &outcol=1; - else &outcol=0; -%end; -%else %if &rule=FORMAT %then %do; - /* match valid format - regex could probably be improved */ - if _n_=1 then do; - retain &tempcol; - &tempcol=prxparse('/^[_a-z\$]\w{0,31}\.[0-9]*$/i'); - if missing(&tempcol) then do; - putlog 'ERR' +(-1) "OR: Invalid expression for FORMAT"; - stop; - end; - drop &tempcol; - end; - if prxmatch(&tempcol, trim(&incol)) then &outcol=1; - else &outcol=0; -%end; - -%mend mp_validatecol; diff --git a/005_macros/ms_adduser2group.sas b/005_macros/ms_adduser2group.sas deleted file mode 100644 index ff09744..0000000 --- a/005_macros/ms_adduser2group.sas +++ /dev/null @@ -1,122 +0,0 @@ -/*** HELP START ***//** - @file - @brief Adds a user to a group on SASjs Server - @details Adds a user to a group based on userid and groupid. Both user and - group must already exist. - - Examples: - - %ms_adduser2group(uid=1,gid=1) - - - @param [in] uid= (0) The User ID to be added - @param [in] gid= (0) The Group ID to contain the new user - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - @param [out] outds= (work.ms_adduser2group) This output dataset will contain - the new list of group members, eg: -|DISPLAYNAME:$18.|USERNAME:$10.|ID:best.| -|---|---|---| -|`Super Admin `|`secretuser `|`1`| -|`Sabir Hassan`|`sabir`|`2`| -|`Mihajlo Medjedovic `|`mihajlo `|`3`| -|`Ivor Townsend `|`ivor `|`4`| -|`New User `|`newuser `|`5`| - - - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - @li mp_abort.sas - -

Related Files

- @li ms_creategroup.sas - @li ms_createuser.sas - -**//*** HELP END ***/ - -%macro ms_adduser2group(uid=0 - ,gid=0 - ,outds=work.ms_adduser2group - ,mdebug=0 - ); - -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=ms_adduser2group.sas - ,msg=%str(syscc=&syscc on macro entry) -) - -%local fref0 fref1 fref2 libref optval rc msg; -%let fref0=%mf_getuniquefileref(); -%let fref1=%mf_getuniquefileref(); -%let libref=%mf_getuniquelibref(); - -/* avoid sending bom marker to API */ -%let optval=%sysfunc(getoption(bomfile)); -options nobomfile; - -data _null_; - file &fref0 lrecl=1000; - infile "&_sasjs_tokenfile" lrecl=1000; - input; - if _n_=1 then put "accept: application/json"; - put _infile_; -run; - -%if &mdebug=1 %then %do; - %put _local_; - data _null_; - infile &fref0; - input; - put _infile_; - run; -%end; - -proc http method='POST' headerin=&fref0 out=&fref1 - url="&_sasjs_apiserverurl/SASjsApi/group/&gid/&uid"; -%if &mdebug=1 %then %do; - debug level=1; -%end; -run; - -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=ms_adduser2group.sas - ,msg=%str(Issue submitting query to SASjsApi/group) -) - -libname &libref JSON fileref=&fref1; - -data &outds; - set &libref..users; - drop ordinal_root ordinal_users; -%if &mdebug=1 %then %do; - putlog _all_; -%end; -run; - - -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=ms_creategroup.sas - ,msg=%str(Issue reading response JSON) -) - -/* reset options */ -options &optval; - -%if &mdebug=0 %then %do; - filename &fref0 clear; - filename &fref1 clear; - libname &libref clear; -%end; -%else %do; - data _null_; - infile &fref1; - input; - putlog _infile_; - run; -%end; - -%mend ms_adduser2group; diff --git a/005_macros/ms_createfile.sas b/005_macros/ms_createfile.sas deleted file mode 100644 index 711284c..0000000 --- a/005_macros/ms_createfile.sas +++ /dev/null @@ -1,112 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a file on SASjs Drive - @details Creates a file on SASjs Drive. To use the file as a Stored Program, - it must have a ".sas" extension. - - Example: - - filename stpcode temp; - data _null_; - file stpcode; - put '%put hello world;'; - run; - %ms_createfile(/some/stored/program.sas, inref=stpcode) - - @param [in] driveloc The full path to the file in SASjs Drive - @param [in] inref= (0) The fileref containing the file to create. - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mf_getuniquename.sas - @li mp_abort.sas - @li ms_deletefile.sas - -**//*** HELP END ***/ - -%macro ms_createfile(driveloc - ,inref=0 - ,mdebug=0 - ); - -/* first, delete in case it exists */ -%ms_deletefile(&driveloc,mdebug=&mdebug) - -%local fname0 fname1 fname2 boundary fname statcd msg optval; -%let fname0=%mf_getuniquefileref(); -%let fname1=%mf_getuniquefileref(); -%let fname2=%mf_getuniquefileref(); -%let boundary=%mf_getuniquename(); - -/* avoid sending bom marker to API */ -%let optval=%sysfunc(getoption(bomfile)); -options nobomfile; - -data _null_; - file &fname0 termstr=crlf lrecl=32767; - infile &inref end=eof lrecl=32767; - if _n_ = 1 then do; - put "--&boundary."; - put 'Content-Disposition: form-data; name="filePath"'; - put ; - put "&driveloc"; - put "--&boundary"; - put 'Content-Disposition: form-data; name="file"; filename="ignore.sas"'; - put "Content-Type: text/plain"; - put ; - end; - input; - put _infile_; /* add the actual file to be sent */ - if eof then do; - put ; - put "--&boundary--"; - end; -run; - -data _null_; - file &fname1 lrecl=1000; - infile "&_sasjs_tokenfile" lrecl=1000; - input; - if _n_=1 then put "Content-Type: multipart/form-data; boundary=&boundary"; - put _infile_; -run; - -%if &mdebug=1 %then %do; - data _null_; - infile &fname0 lrecl=32767; - input; - put _infile_; - data _null_; - infile &fname1 lrecl=32767; - input; - put _infile_; - run; -%end; - -proc http method='POST' in=&fname0 headerin=&fname1 out=&fname2 - url="&_sasjs_apiserverurl/SASjsApi/drive/file"; -%if &mdebug=1 %then %do; - debug level=1; -%end; -run; - -%let statcd=0; -data _null_; - infile &fname2; - input; - putlog _infile_; - if _infile_='{"status":"success"}' then call symputx('statcd',1,'l'); - else call symputx('msg',_infile_,'l'); -run; - -%mp_abort( - iftrue=(&statcd=0) - ,mac=ms_createfile.sas - ,msg=%superq(msg) -) - -/* reset options */ -options &optval; - -%mend ms_createfile; diff --git a/005_macros/ms_creategroup.sas b/005_macros/ms_creategroup.sas deleted file mode 100644 index d78e149..0000000 --- a/005_macros/ms_creategroup.sas +++ /dev/null @@ -1,149 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a group on SASjs Server - @details Creates a group on SASjs Server with the following attributes: - - @li name - @li description - @li isActive - - Examples: - - %ms_creategroup(mynewgroup) - - %ms_creategroup(mynewergroup, desc=The group description) - - @param [in] groupname The group name to create. No spaces or special chars. - @param [in] desc= (0) If no description provided, group name will be used. - @param [in] isactive= (true) Set to false to create an inactive group. - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - @param [out] outds= (work.ms_creategroup) This output dataset will contain the - values from the JSON response (such as the id of the new group) -|DESCRIPTION:$1.|GROUPID:best.|ISACTIVE:best.|NAME:$11.| -|---|---|---|---| -|`The group description`|`2 `|`1 `|`mynewergroup `| - - - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - @li mp_abort.sas - -

Related Files

- @li ms_creategroup.test.sas - @li ms_getgroups.sas - -**//*** HELP END ***/ - -%macro ms_creategroup(groupname - ,desc=0 - ,isactive=true - ,outds=work.ms_creategroup - ,mdebug=0 - ); - -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=ms_creategroup.sas - ,msg=%str(syscc=&syscc on macro entry) -) - -%local fref0 fref1 fref2 libref optval rc msg; -%let fref0=%mf_getuniquefileref(); -%let fref1=%mf_getuniquefileref(); -%let fref2=%mf_getuniquefileref(); -%let libref=%mf_getuniquelibref(); - -/* avoid sending bom marker to API */ -%let optval=%sysfunc(getoption(bomfile)); -options nobomfile; - -data _null_; - file &fref0 termstr=crlf; - name=quote(cats(symget('groupname'))); - description=quote(cats(symget('desc'))); - if cats(description)='"0"' then description=name; - isactive=symget('isactive'); -%if &mdebug=1 %then %do; - putlog _all_; -%end; - - put '{'@; - put '"name":' name @; - put ',"description":' description @; - put ',"isActive":' isactive @; - put '}'; -run; - -data _null_; - file &fref1 lrecl=1000; - infile "&_sasjs_tokenfile" lrecl=1000; - input; - if _n_=1 then do; - put "Content-Type: application/json"; - put "accept: application/json"; - end; - put _infile_; -run; - -%if &mdebug=1 %then %do; - data _null_; - infile &fref0; - input; - put _infile_; - data _null_; - infile &fref1; - input; - put _infile_; - run; -%end; - -proc http method='POST' in=&fref0 headerin=&fref1 out=&fref2 - url="&_sasjs_apiserverurl/SASjsApi/group"; -%if &mdebug=1 %then %do; - debug level=1; -%end; -run; - -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=ms_creategroup.sas - ,msg=%str(Issue submitting query to SASjsApi/group) -) - -libname &libref JSON fileref=&fref2; - -data &outds; - set &libref..root; - drop ordinal_root; -%if &mdebug=1 %then %do; - putlog _all_; -%end; -run; - - -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=ms_creategroup.sas - ,msg=%str(Issue reading response JSON) -) - -/* reset options */ -options &optval; - -%if &mdebug=0 %then %do; - filename &fref0 clear; - filename &fref1 clear; - filename &fref2 clear; - libname &libref clear; -%end; -%else %do; - data _null_; - infile &fref2; - input; - putlog _infile_; - run; -%end; - -%mend ms_creategroup; diff --git a/005_macros/ms_createuser.sas b/005_macros/ms_createuser.sas deleted file mode 100644 index 397ed7c..0000000 --- a/005_macros/ms_createuser.sas +++ /dev/null @@ -1,146 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a user on SASjs Server - @details Creates a user on SASjs Server with the following attributes: - - @li UserName - @li Password - @li isAdmin - @li displayName - - The userid is created by sasjs/server. All users are created with `isActive` - set to `true`. - - Example: - - %ms_createuser(newuser,secretpass,displayname=New User!) - - @param [in] username The username to apply. No spaces or special characters. - @param [in] password The initial password to set. - @param [in] isadmin= (false) Set to true to give the user admin rights - @param [in] displayName= (0) Set a friendly name (spaces & special characters - are ok). If not provided, username will be used instead. - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - @param [out] outds= (work.ms_createuser) This output dataset will contain the - values from the JSON response (such as the id of the new user) -|ID:best.|DISPLAYNAME:$8.|USERNAME:$8.|ISACTIVE:best.|ISADMIN:best.| -|---|---|---|---|---| -|`6 `|`New User `|`newuser `|`1 `|`0 `| - - - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - @li mp_abort.sas - -

Related Files

- @li ms_createuser.test.sas - @li ms_getusers.sas - -**//*** HELP END ***/ - -%macro ms_createuser(username,password - ,isadmin=false - ,displayname=0 - ,outds=work.ms_createuser - ,mdebug=0 - ); - -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=ms_createuser.sas - ,msg=%str(syscc=&syscc on macro entry) -) - -%local fref0 fref1 fref2 libref optval rc msg; -%let fref0=%mf_getuniquefileref(); -%let fref1=%mf_getuniquefileref(); -%let fref2=%mf_getuniquefileref(); -%let libref=%mf_getuniquelibref(); - -/* avoid sending bom marker to API */ -%let optval=%sysfunc(getoption(bomfile)); -options nobomfile; - -data _null_; - file &fref0 termstr=crlf; - username=quote(cats(symget('username'))); - password=quote(cats(symget('password'))); - isadmin=symget('isadmin'); - displayname=quote(cats(symget('displayname'))); - if displayname='"0"' then displayname=username; - -%if &mdebug=1 %then %do; - putlog _all_; -%end; - - put '{'@; - put '"displayName":' displayname @; - put ',"username":' username @; - put ',"password":' password @; - put ',"isAdmin":' isadmin @; - put ',"isActive": true }'; -run; - -data _null_; - file &fref1 lrecl=1000; - infile "&_sasjs_tokenfile" lrecl=1000; - input; - if _n_=1 then do; - put "Content-Type: application/json"; - put "accept: application/json"; - end; - put _infile_; -run; - -%if &mdebug=1 %then %do; - data _null_; - infile &fref0; - input; - put _infile_; - data _null_; - infile &fref1; - input; - put _infile_; - run; -%end; - -proc http method='POST' in=&fref0 headerin=&fref1 out=&fref2 - url="&_sasjs_apiserverurl/SASjsApi/user"; -%if &mdebug=1 %then %do; - debug level=1; -%end; -run; - -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=ms_createuser.sas - ,msg=%str(Issue submitting query to SASjsApi/user) -) - -libname &libref JSON fileref=&fref2; - -data &outds; - set &libref..root; - drop ordinal_root; -run; - - -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=ms_createuser.sas - ,msg=%str(Issue reading response JSON) -) - -/* reset options */ -options &optval; - -%if &mdebug=1 %then %do; - filename &fref0 clear; - filename &fref1 clear; - filename &fref2 clear; - libname &libref clear; -%end; - -%mend ms_createuser; diff --git a/005_macros/ms_createwebservice.sas b/005_macros/ms_createwebservice.sas deleted file mode 100644 index 04f64e3..0000000 --- a/005_macros/ms_createwebservice.sas +++ /dev/null @@ -1,689 +0,0 @@ -/*** HELP START ***//** - @file ms_createwebservice.sas - @brief Create a Web-Ready Stored Program - @details This macro creates a Stored Program along with the necessary precode - to enable the %webout() macro - - Usage: - - %* compile macros ; - filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - %* parmcards lets us write to a text file from open code ; - filename ft15f001 temp; - parmcards4; - %webout(FETCH) - %* do some sas, any inputs are now already WORK tables; - data example1 example2; - set sashelp.class; - run; - %* send data back; - %webout(OPEN) - %webout(ARR,example1) * Array format, fast, suitable for large tables ; - %webout(OBJ,example2) * Object format, easier to work with ; - %webout(CLOSE) - ;;;; - %ms_createwebservice(path=/Public/app/common,name=appInit,code=ft15f001) - - - - For more examples of using these web services with the SASjs Adapter, see: - https://github.com/sasjs/adapter#readme - - @param [in] path= (0) The full SASjs Drive path in which to create the service - @param [in] name= Stored Program name - @param [in] desc= The description of the service (not implemented yet) - @param [in] precode= Space separated list of filerefs, pointing to the code - that needs to be attached to the beginning of the service (optional) - @param [in] code= (ft15f001) Space seperated fileref(s) of the actual code to - be added - @param [in] mDebug= (0) set to 1 to show debug messages in the log - -

SAS Macros

- @li ms_createfile.sas - @li mf_getuser.sas - @li mf_getuniquename.sas - @li mf_getuniquefileref.sas - @li mp_abort.sas - -

Related Files

- @li ms_createwebservice.test.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro ms_createwebservice(path=0 - ,name=initService - ,precode= - ,code=ft15f001 - ,desc=Not currently used - ,mDebug=0 -)/*/STORE SOURCE*/; - -%local dbg; -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; -%end; -%else %let dbg=*; - -%mp_abort(iftrue=(&syscc ge 4 ) - ,mac=ms_createwebservice - ,msg=%str(syscc=&syscc on macro entry) -) -%mp_abort(iftrue=("&path"="0") - ,mac=ms_createwebservice - ,msg=%str(Path not provided) -) - -/* remove any trailing slash */ -%if "%substr(&path,%length(&path),1)" = "/" %then - %let path=%substr(&path,1,%length(&path)-1); - -/** - * Add webout macro - * These put statements are auto generated - to change the macro, change the - * source (ms_webout) and run `build.py` - */ -%local sasjsref; -%let sasjsref=%mf_getuniquefileref(); -data _null_; - file &sasjsref termstr=crlf lrecl=512; - put "/* Created on %sysfunc(datetime(),datetime19.) by %mf_getuser() */"; -/* WEBOUT BEGIN */ - put '%macro mp_jsonout(action,ds,jref=_webout,dslabel=,fmt=Y '; - put ' ,engine=DATASTEP '; - put ' ,missing=NULL '; - put ' ,showmeta=N '; - put ' ,maxobs=MAX '; - put ')/*/STORE SOURCE*/; '; - put '%local tempds colinfo fmtds i numcols numobs stmt_obs lastobs optval '; - put ' tmpds1 tmpds2 tmpds3 tmpds4; '; - put '%let numcols=0; '; - put '%if &maxobs ne MAX %then %let stmt_obs=%str(if _n_>&maxobs then stop;); '; - put ' '; - put '%if &action=OPEN %then %do; '; - put ' options nobomfile; '; - put ' data _null_;file &jref encoding=''utf-8'' lrecl=200; '; - put ' put ''{"PROCESSED_DTTM" : "'' "%sysfunc(datetime(),E8601DT26.6)" ''"''; '; - put ' run; '; - put '%end; '; - put '%else %if (&action=ARR or &action=OBJ) %then %do; '; - put ' /* force variable names to always be uppercase in the JSON */ '; - put ' options validvarname=upcase; '; - put ' /* To avoid issues with _webout on EBI - such as encoding diffs and truncation '; - put ' (https://support.sas.com/kb/49/325.html) we use temporary files */ '; - put ' filename _sjs1 temp lrecl=200 ; '; - put ' data _null_; file _sjs1 encoding=''utf-8''; '; - put ' put ", ""%lowcase(%sysfunc(coalescec(&dslabel,&ds)))"":"; '; - put ' run; '; - put ' /* now write to _webout 1 char at a time */ '; - put ' data _null_; '; - put ' infile _sjs1 lrecl=1 recfm=n; '; - put ' file &jref mod lrecl=1 recfm=n; '; - put ' input sourcechar $char1. @@; '; - put ' format sourcechar hex2.; '; - put ' put sourcechar char1. @@; '; - put ' run; '; - put ' filename _sjs1 clear; '; - put ' '; - put ' /* grab col defs */ '; - put ' proc contents noprint data=&ds '; - put ' out=_data_(keep=name type length format formatl formatd varnum label); '; - put ' run; '; - put ' %let colinfo=%scan(&syslast,2,.); '; - put ' proc sort data=&colinfo; '; - put ' by varnum; '; - put ' run; '; - put ' /* move meta to mac vars */ '; - put ' data &colinfo; '; - put ' if _n_=1 then call symputx(''numcols'',nobs,''l''); '; - put ' set &colinfo end=last nobs=nobs; '; - put ' name=upcase(name); '; - put ' /* fix formats */ '; - put ' if type=2 or type=6 then do; '; - put ' typelong=''char''; '; - put ' length fmt $49.; '; - put ' if format='''' then fmt=cats(''$'',length,''.''); '; - put ' else if formatl=0 then fmt=cats(format,''.''); '; - put ' else fmt=cats(format,formatl,''.''); '; - put ' end; '; - put ' else do; '; - put ' typelong=''num''; '; - put ' if format='''' then fmt=''best.''; '; - put ' else if formatl=0 then fmt=cats(format,''.''); '; - put ' else if formatd=0 then fmt=cats(format,formatl,''.''); '; - put ' else fmt=cats(format,formatl,''.'',formatd); '; - put ' end; '; - put ' /* 32 char unique name */ '; - put ' newname=''sasjs''!!substr(cats(put(md5(name),$hex32.)),1,27); '; - put ' '; - put ' call symputx(cats(''name'',_n_),name,''l''); '; - put ' call symputx(cats(''newname'',_n_),newname,''l''); '; - put ' call symputx(cats(''length'',_n_),length,''l''); '; - put ' call symputx(cats(''fmt'',_n_),fmt,''l''); '; - put ' call symputx(cats(''type'',_n_),type,''l''); '; - put ' call symputx(cats(''typelong'',_n_),typelong,''l''); '; - put ' call symputx(cats(''label'',_n_),coalescec(label,name),''l''); '; - put ' /* overwritten when fmt=Y and a custom format exists in catalog */ '; - put ' if typelong=''num'' then call symputx(cats(''fmtlen'',_n_),200,''l''); '; - put ' else call symputx(cats(''fmtlen'',_n_),min(32767,ceil((length+10)*1.5)),''l''); '; - put ' run; '; - put ' '; - put ' %let tempds=%substr(_%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); '; - put ' proc sql; '; - put ' select count(*) into: lastobs from &ds; '; - put ' %if &maxobs ne MAX %then %let lastobs=%sysfunc(min(&lastobs,&maxobs)); '; - put ' '; - put ' %if &engine=PROCJSON %then %do; '; - put ' %if &missing=STRING %then %do; '; - put ' %put &sysmacroname: Special Missings not supported in proc json.; '; - put ' %put &sysmacroname: Switching to DATASTEP engine; '; - put ' %goto datastep; '; - put ' %end; '; - put ' data &tempds; '; - put ' set &ds; '; - put ' &stmt_obs; '; - put ' %if &fmt=N %then format _numeric_ best32.;; '; - put ' /* PRETTY is necessary to avoid line truncation in large files */ '; - put ' filename _sjs2 temp lrecl=131068 encoding=''utf-8''; '; - put ' proc json out=_sjs2 pretty '; - put ' %if &action=ARR %then nokeys ; '; - put ' ;export &tempds / nosastags fmtnumeric; '; - put ' run; '; - put ' /* send back to webout */ '; - put ' data _null_; '; - put ' infile _sjs2 lrecl=1 recfm=n; '; - put ' file &jref mod lrecl=1 recfm=n; '; - put ' input sourcechar $char1. @@; '; - put ' format sourcechar hex2.; '; - put ' put sourcechar char1. @@; '; - put ' run; '; - put ' filename _sjs2 clear; '; - put ' %end; '; - put ' %else %if &engine=DATASTEP %then %do; '; - put ' %datastep: '; - put ' %if %sysfunc(exist(&ds)) ne 1 & %sysfunc(exist(&ds,VIEW)) ne 1 '; - put ' %then %do; '; - put ' %put &sysmacroname: &ds NOT FOUND!!!; '; - put ' %return; '; - put ' %end; '; - put ' '; - put ' %if &fmt=Y %then %do; '; - put ' /** '; - put ' * Extract format definitions '; - put ' * First, by getting library locations from dictionary.formats '; - put ' * Then, by exporting the width using proc format '; - put ' * Cannot use maxw from sashelp.vformat as not always populated '; - put ' * Cannot use fmtinfo() as not supported in all flavours '; - put ' */ '; - put ' %let tmpds1=%substr(fmtsum%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); '; - put ' %let tmpds2=%substr(cntl%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); '; - put ' %let tmpds3=%substr(cntl%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); '; - put ' %let tmpds4=%substr(col%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); '; - put ' proc sql noprint; '; - put ' create table &tmpds1 as '; - put ' select cats(libname,''.'',memname) as FMTCAT, '; - put ' FMTNAME '; - put ' from dictionary.formats '; - put ' where fmttype=''F'' and libname is not null '; - put ' and fmtname in (select format from &colinfo where format is not null) '; - put ' order by 1; '; - put ' create table &tmpds2( '; - put ' FMTNAME char(32), '; - put ' LENGTH num '; - put ' ); '; - put ' %local catlist cat fmtlist i; '; - put ' select distinct fmtcat into: catlist separated by '' '' from &tmpds1; '; - put ' %do i=1 %to %sysfunc(countw(&catlist,%str( ))); '; - put ' %let cat=%scan(&catlist,&i,%str( )); '; - put ' proc sql; '; - put ' select distinct fmtname into: fmtlist separated by '' '' '; - put ' from &tmpds1 where fmtcat="&cat"; '; - put ' proc format lib=&cat cntlout=&tmpds3(keep=fmtname length); '; - put ' select &fmtlist; '; - put ' run; '; - put ' proc sql; '; - put ' insert into &tmpds2 select distinct fmtname,length from &tmpds3; '; - put ' %end; '; - put ' '; - put ' proc sql; '; - put ' create table &tmpds4 as '; - put ' select a.*, b.length as MAXW '; - put ' from &colinfo a '; - put ' left join &tmpds2 b '; - put ' on cats(a.format)=cats(upcase(b.fmtname)) '; - put ' order by a.varnum; '; - put ' data _null_; '; - put ' set &tmpds4; '; - put ' if not missing(maxw); '; - put ' call symputx( '; - put ' cats(''fmtlen'',_n_), '; - put ' /* vars need extra padding due to JSON escaping of special chars */ '; - put ' min(32767,ceil((max(length,maxw)+10)*1.5)) '; - put ' ,''l'' '; - put ' ); '; - put ' run; '; - put ' '; - put ' /* configure varlenchk - as we are explicitly shortening the variables */ '; - put ' %let optval=%sysfunc(getoption(varlenchk)); '; - put ' options varlenchk=NOWARN; '; - put ' data _data_(compress=char); '; - put ' /* shorten the new vars */ '; - put ' length '; - put ' %do i=1 %to &numcols; '; - put ' &&name&i $&&fmtlen&i '; - put ' %end; '; - put ' ; '; - put ' /* rename on entry */ '; - put ' set &ds(rename=( '; - put ' %do i=1 %to &numcols; '; - put ' &&name&i=&&newname&i '; - put ' %end; '; - put ' )); '; - put ' &stmt_obs; '; - put ' '; - put ' drop '; - put ' %do i=1 %to &numcols; '; - put ' &&newname&i '; - put ' %end; '; - put ' ; '; - put ' %do i=1 %to &numcols; '; - put ' %if &&typelong&i=num %then %do; '; - put ' &&name&i=cats(put(&&newname&i,&&fmt&i)); '; - put ' %end; '; - put ' %else %do; '; - put ' &&name&i=put(&&newname&i,&&fmt&i); '; - put ' %end; '; - put ' %end; '; - put ' if _error_ then do; '; - put ' call symputx(''syscc'',1012); '; - put ' stop; '; - put ' end; '; - put ' run; '; - put ' %let fmtds=&syslast; '; - put ' options varlenchk=&optval; '; - put ' %end; '; - put ' '; - put ' proc format; /* credit yabwon for special null removal */ '; - put ' value bart (default=40) '; - put ' %if &missing=NULL %then %do; '; - put ' ._ - .z = null '; - put ' %end; '; - put ' %else %do; '; - put ' ._ = [quote()] '; - put ' . = null '; - put ' .a - .z = [quote()] '; - put ' %end; '; - put ' other = [best.]; '; - put ' '; - put ' data &tempds; '; - put ' attrib _all_ label=''''; '; - put ' %do i=1 %to &numcols; '; - put ' %if &&typelong&i=char or &fmt=Y %then %do; '; - put ' length &&name&i $&&fmtlen&i...; '; - put ' format &&name&i $&&fmtlen&i...; '; - put ' %end; '; - put ' %end; '; - put ' %if &fmt=Y %then %do; '; - put ' set &fmtds; '; - put ' %end; '; - put ' %else %do; '; - put ' set &ds; '; - put ' %end; '; - put ' &stmt_obs; '; - put ' format _numeric_ bart.; '; - put ' %do i=1 %to &numcols; '; - put ' %if &&typelong&i=char or &fmt=Y %then %do; '; - put ' if findc(&&name&i,''"\''!!''0A0D09000E0F010210111A''x) then do; '; - put ' &&name&i=''"''!!trim( '; - put ' prxchange(''s/"/\\"/'',-1, /* double quote */ '; - put ' prxchange(''s/\x0A/\n/'',-1, /* new line */ '; - put ' prxchange(''s/\x0D/\r/'',-1, /* carriage return */ '; - put ' prxchange(''s/\x09/\\t/'',-1, /* tab */ '; - put ' prxchange(''s/\x00/\\u0000/'',-1, /* NUL */ '; - put ' prxchange(''s/\x0E/\\u000E/'',-1, /* SS */ '; - put ' prxchange(''s/\x0F/\\u000F/'',-1, /* SF */ '; - put ' prxchange(''s/\x01/\\u0001/'',-1, /* SOH */ '; - put ' prxchange(''s/\x02/\\u0002/'',-1, /* STX */ '; - put ' prxchange(''s/\x10/\\u0010/'',-1, /* DLE */ '; - put ' prxchange(''s/\x11/\\u0011/'',-1, /* DC1 */ '; - put ' prxchange(''s/\x1A/\\u001A/'',-1, /* SUB */ '; - put ' prxchange(''s/\\/\\\\/'',-1,&&name&i) '; - put ' )))))))))))))!!''"''; '; - put ' end; '; - put ' else &&name&i=quote(cats(&&name&i)); '; - put ' %end; '; - put ' %end; '; - put ' run; '; - put ' '; - put ' filename _sjs3 temp lrecl=131068 ; '; - put ' data _null_; '; - put ' file _sjs3 encoding=''utf-8''; '; - put ' if _n_=1 then put "["; '; - put ' set &tempds; '; - put ' if _n_>1 then put "," @; put '; - put ' %if &action=ARR %then "[" ; %else "{" ; '; - put ' %do i=1 %to &numcols; '; - put ' %if &i>1 %then "," ; '; - put ' %if &action=OBJ %then """&&name&i"":" ; '; - put ' "&&name&i"n /* name literal for reserved variable names */ '; - put ' %end; '; - put ' %if &action=ARR %then "]" ; %else "}" ; ; '; - put ' '; - put ' /* close out the table */ '; - put ' data _null_; '; - put ' file _sjs3 mod encoding=''utf-8''; '; - put ' put '']''; '; - put ' run; '; - put ' data _null_; '; - put ' infile _sjs3 lrecl=1 recfm=n; '; - put ' file &jref mod lrecl=1 recfm=n; '; - put ' input sourcechar $char1. @@; '; - put ' format sourcechar hex2.; '; - put ' put sourcechar char1. @@; '; - put ' run; '; - put ' filename _sjs3 clear; '; - put ' %end; '; - put ' '; - put ' proc sql; '; - put ' drop table &colinfo, &tempds; '; - put ' '; - put ' %if %substr(&showmeta,1,1)=Y %then %do; '; - put ' filename _sjs4 temp lrecl=131068 encoding=''utf-8''; '; - put ' data _null_; '; - put ' file _sjs4; '; - put ' length label $350; '; - put ' put ", ""$%lowcase(%sysfunc(coalescec(&dslabel,&ds)))"":{""vars"":{"; '; - put ' do i=1 to &numcols; '; - put ' name=quote(trim(symget(cats(''name'',i)))); '; - put ' format=quote(trim(symget(cats(''fmt'',i)))); '; - put ' label=quote(prxchange(''s/\\/\\\\/'',-1,trim(symget(cats(''label'',i))))); '; - put ' length=quote(trim(symget(cats(''length'',i)))); '; - put ' type=quote(trim(symget(cats(''typelong'',i)))); '; - put ' if i>1 then put "," @@; '; - put ' put name '':{"format":'' format '',"label":'' label '; - put ' '',"length":'' length '',"type":'' type ''}''; '; - put ' end; '; - put ' put ''}}''; '; - put ' run; '; - put ' /* send back to webout */ '; - put ' data _null_; '; - put ' infile _sjs4 lrecl=1 recfm=n; '; - put ' file &jref mod lrecl=1 recfm=n; '; - put ' input sourcechar $char1. @@; '; - put ' format sourcechar hex2.; '; - put ' put sourcechar char1. @@; '; - put ' run; '; - put ' filename _sjs4 clear; '; - put ' %end; '; - put '%end; '; - put ' '; - put '%else %if &action=CLOSE %then %do; '; - put ' data _null_; file &jref encoding=''utf-8'' mod ; '; - put ' put "}"; '; - put ' run; '; - put '%end; '; - put '%mend mp_jsonout; '; - put ' '; - put '%macro mf_getuser( '; - put ')/*/STORE SOURCE*/; '; - put ' %local user; '; - put ' '; - put ' %if %symexist(_sasjs_username) %then %let user=&_sasjs_username; '; - put ' %else %if %symexist(SYS_COMPUTE_SESSION_OWNER) %then %do; '; - put ' %let user=&SYS_COMPUTE_SESSION_OWNER; '; - put ' %end; '; - put ' %else %if %symexist(_metaperson) %then %do; '; - put ' %if %length(&_metaperson)=0 %then %let user=&sysuserid; '; - put ' /* sometimes SAS will add @domain extension - remove for consistency */ '; - put ' /* but be sure to quote in case of usernames with commas */ '; - put ' %else %let user=%unquote(%scan(%quote(&_metaperson),1,@)); '; - put ' %end; '; - put ' %else %let user=&sysuserid; '; - put ' '; - put ' %quote(&user) '; - put ' '; - put '%mend mf_getuser; '; - put ' '; - put '%macro ms_webout(action,ds,dslabel=,fref=_webout,fmt=N,missing=NULL '; - put ' ,showmeta=N,maxobs=MAX,workobs=0 '; - put '); '; - put '%global _webin_file_count _webin_fileref1 _webin_name1 _program _debug '; - put ' sasjs_tables; '; - put ' '; - put '%local i tempds; '; - put '%let action=%upcase(&action); '; - put ' '; - put '%if &action=FETCH %then %do; '; - put ' %if %str(&_debug) ge 131 %then %do; '; - put ' options mprint notes mprintnest; '; - put ' %end; '; - put ' %let _webin_file_count=%eval(&_webin_file_count+0); '; - put ' /* now read in the data */ '; - put ' %do i=1 %to &_webin_file_count; '; - put ' %if &_webin_file_count=1 %then %do; '; - put ' %let _webin_fileref1=&_webin_fileref; '; - put ' %let _webin_name1=&_webin_name; '; - put ' %end; '; - put ' data _null_; '; - put ' infile &&_webin_fileref&i termstr=crlf lrecl=32767; '; - put ' input; '; - put ' call symputx(''input_statement'',_infile_); '; - put ' putlog "&&_webin_name&i input statement: " _infile_; '; - put ' stop; '; - put ' data &&_webin_name&i; '; - put ' infile &&_webin_fileref&i firstobs=2 dsd termstr=crlf encoding=''utf-8'' '; - put ' lrecl=32767; '; - put ' input &input_statement; '; - put ' %if %str(&_debug) ge 131 %then %do; '; - put ' if _n_<20 then putlog _infile_; '; - put ' %end; '; - put ' run; '; - put ' %let sasjs_tables=&sasjs_tables &&_webin_name&i; '; - put ' %end; '; - put '%end; '; - put ' '; - put '%else %if &action=OPEN %then %do; '; - put ' /* fix encoding and ensure enough lrecl */ '; - put ' OPTIONS NOBOMFILE lrecl=32767; '; - put ' '; - put ' /* set the header */ '; - put ' %mfs_httpheader(Content-type,application/json) '; - put ' '; - put ' /* setup json. */ '; - put ' data _null_;file &fref encoding=''utf-8'' termstr=lf ; '; - put ' put ''{"SYSDATE" : "'' "&SYSDATE" ''"''; '; - put ' put '',"SYSTIME" : "'' "&SYSTIME" ''"''; '; - put ' run; '; - put ' '; - put '%end; '; - put ' '; - put '%else %if &action=ARR or &action=OBJ %then %do; '; - put ' %if "%substr(&sysver,1,1)"="4" or "%substr(&sysver,1,1)"="5" %then %do; '; - put ' /* functions in formats unsupported */ '; - put ' %put &sysmacroname: forcing missing back to NULL as feature not supported; '; - put ' %let missing=NULL; '; - put ' %end; '; - put ' %mp_jsonout(&action,&ds,dslabel=&dslabel,fmt=&fmt,jref=&fref '; - put ' ,engine=DATASTEP,missing=&missing,showmeta=&showmeta,maxobs=&maxobs '; - put ' ) '; - put '%end; '; - put '%else %if &action=CLOSE %then %do; '; - put ' %if %str(&workobs) > 0 %then %do; '; - put ' /* if debug mode, send back first XX records of each work table also */ '; - put ' data;run;%let tempds=%scan(&syslast,2,.); '; - put ' ods output Members=&tempds; '; - put ' proc datasets library=WORK memtype=data; '; - put ' %local wtcnt;%let wtcnt=0; '; - put ' data _null_; '; - put ' set &tempds; '; - put ' if not (upcase(name) =:"DATA"); /* ignore temp datasets */ '; - put ' if not (upcase(name)=:"_DATA_"); '; - put ' i+1; '; - put ' call symputx(cats(''wt'',i),name,''l''); '; - put ' call symputx(''wtcnt'',i,''l''); '; - put ' data _null_; file &fref mod encoding=''utf-8'' termstr=lf; '; - put ' put ",""WORK"":{"; '; - put ' %do i=1 %to &wtcnt; '; - put ' %let wt=&&wt&i; '; - put ' data _null_; file &fref mod encoding=''utf-8'' termstr=lf; '; - put ' dsid=open("WORK.&wt",''is''); '; - put ' nlobs=attrn(dsid,''NLOBS''); '; - put ' nvars=attrn(dsid,''NVARS''); '; - put ' rc=close(dsid); '; - put ' if &i>1 then put '',''@; '; - put ' put " ""&wt"" : {"; '; - put ' put ''"nlobs":'' nlobs; '; - put ' put '',"nvars":'' nvars; '; - put ' %mp_jsonout(OBJ,&wt,jref=&fref,dslabel=first10rows,showmeta=Y '; - put ' ,maxobs=&workobs '; - put ' ) '; - put ' data _null_; file &fref mod encoding=''utf-8'' termstr=lf; '; - put ' put "}"; '; - put ' %end; '; - put ' data _null_; file &fref mod encoding=''utf-8'' termstr=lf; '; - put ' put "}"; '; - put ' run; '; - put ' %end; '; - put ' /* close off json */ '; - put ' data _null_;file &fref mod encoding=''utf-8'' termstr=lf lrecl=32767; '; - put ' length SYSPROCESSNAME syserrortext syswarningtext autoexec $512; '; - put ' put ",""_DEBUG"" : ""&_debug"" "; '; - put ' _PROGRAM=quote(trim(resolve(symget(''_PROGRAM'')))); '; - put ' put '',"_PROGRAM" : '' _PROGRAM ; '; - put ' autoexec=quote(urlencode(trim(getoption(''autoexec'')))); '; - put ' put '',"AUTOEXEC" : '' autoexec; '; - put ' put ",""MF_GETUSER"" : ""%mf_getuser()"" "; '; - put ' put ",""SYSCC"" : ""&syscc"" "; '; - put ' put ",""SYSENCODING"" : ""&sysencoding"" "; '; - put ' syserrortext=cats(symget(''syserrortext'')); '; - put ' if findc(syserrortext,''"\''!!''0A0D09000E0F010210111A''x) then do; '; - put ' syserrortext=''"''!!trim( '; - put ' prxchange(''s/"/\\"/'',-1, /* double quote */ '; - put ' prxchange(''s/\x0A/\n/'',-1, /* new line */ '; - put ' prxchange(''s/\x0D/\r/'',-1, /* carriage return */ '; - put ' prxchange(''s/\x09/\\t/'',-1, /* tab */ '; - put ' prxchange(''s/\x00/\\u0000/'',-1, /* NUL */ '; - put ' prxchange(''s/\x0E/\\u000E/'',-1, /* SS */ '; - put ' prxchange(''s/\x0F/\\u000F/'',-1, /* SF */ '; - put ' prxchange(''s/\x01/\\u0001/'',-1, /* SOH */ '; - put ' prxchange(''s/\x02/\\u0002/'',-1, /* STX */ '; - put ' prxchange(''s/\x10/\\u0010/'',-1, /* DLE */ '; - put ' prxchange(''s/\x11/\\u0011/'',-1, /* DC1 */ '; - put ' prxchange(''s/\x1A/\\u001A/'',-1, /* SUB */ '; - put ' prxchange(''s/\\/\\\\/'',-1,syserrortext) '; - put ' )))))))))))))!!''"''; '; - put ' end; '; - put ' else syserrortext=cats(''"'',syserrortext,''"''); '; - put ' put '',"SYSERRORTEXT" : '' syserrortext; '; - put ' SYSHOSTINFOLONG=quote(trim(symget(''SYSHOSTINFOLONG''))); '; - put ' put '',"SYSHOSTINFOLONG" : '' SYSHOSTINFOLONG; '; - put ' put ",""SYSHOSTNAME"" : ""&syshostname"" "; '; - put ' put ",""SYSPROCESSID"" : ""&SYSPROCESSID"" "; '; - put ' put ",""SYSPROCESSMODE"" : ""&SYSPROCESSMODE"" "; '; - put ' SYSPROCESSNAME=quote(urlencode(cats(SYSPROCESSNAME))); '; - put ' put ",""SYSPROCESSNAME"" : " SYSPROCESSNAME; '; - put ' put ",""SYSJOBID"" : ""&sysjobid"" "; '; - put ' put ",""SYSSCPL"" : ""&sysscpl"" "; '; - put ' put ",""SYSSITE"" : ""&syssite"" "; '; - put ' put ",""SYSTCPIPHOSTNAME"" : ""&SYSTCPIPHOSTNAME"" "; '; - put ' put ",""SYSUSERID"" : ""&sysuserid"" "; '; - put ' sysvlong=quote(trim(symget(''sysvlong''))); '; - put ' put '',"SYSVLONG" : '' sysvlong; '; - put ' syswarningtext=cats(symget(''syswarningtext'')); '; - put ' if findc(syswarningtext,''"\''!!''0A0D09000E0F010210111A''x) then do; '; - put ' syswarningtext=''"''!!trim( '; - put ' prxchange(''s/"/\\"/'',-1, /* double quote */ '; - put ' prxchange(''s/\x0A/\n/'',-1, /* new line */ '; - put ' prxchange(''s/\x0D/\r/'',-1, /* carriage return */ '; - put ' prxchange(''s/\x09/\\t/'',-1, /* tab */ '; - put ' prxchange(''s/\x00/\\u0000/'',-1, /* NUL */ '; - put ' prxchange(''s/\x0E/\\u000E/'',-1, /* SS */ '; - put ' prxchange(''s/\x0F/\\u000F/'',-1, /* SF */ '; - put ' prxchange(''s/\x01/\\u0001/'',-1, /* SOH */ '; - put ' prxchange(''s/\x02/\\u0002/'',-1, /* STX */ '; - put ' prxchange(''s/\x10/\\u0010/'',-1, /* DLE */ '; - put ' prxchange(''s/\x11/\\u0011/'',-1, /* DC1 */ '; - put ' prxchange(''s/\x1A/\\u001A/'',-1, /* SUB */ '; - put ' prxchange(''s/\\/\\\\/'',-1,syswarningtext) '; - put ' )))))))))))))!!''"''; '; - put ' end; '; - put ' else syswarningtext=cats(''"'',syswarningtext,''"''); '; - put ' put '',"SYSWARNINGTEXT" : '' syswarningtext; '; - put ' put '',"END_DTTM" : "'' "%sysfunc(datetime(),E8601DT26.6)" ''" ''; '; - put ' length memsize $32; '; - put ' memsize="%sysfunc(INPUTN(%sysfunc(getoption(memsize)), best.),sizekmg.)"; '; - put ' memsize=quote(cats(memsize)); '; - put ' put '',"MEMSIZE" : '' memsize; '; - put ' put "}" @; '; - put ' run; '; - put '%end; '; - put ' '; - put '%mend ms_webout; '; - put ' '; - put '%macro mfs_httpheader(header_name '; - put ' ,header_value '; - put ')/*/STORE SOURCE*/; '; - put '%global sasjs_stpsrv_header_loc; '; - put '%local fref fid i; '; - put ' '; - put '%if %sysfunc(filename(fref,&sasjs_stpsrv_header_loc)) ne 0 %then %do; '; - put ' %put &=fref &=sasjs_stpsrv_header_loc; '; - put ' %put %str(ERR)OR: %sysfunc(sysmsg()); '; - put ' %return; '; - put '%end; '; - put ' '; - put '%let fid=%sysfunc(fopen(&fref,A)); '; - put ' '; - put '%if &fid=0 %then %do; '; - put ' %put %str(ERR)OR: %sysfunc(sysmsg()); '; - put ' %return; '; - put '%end; '; - put ' '; - put '%let rc=%sysfunc(fput(&fid,%str(&header_name): %str(&header_value))); '; - put '%let rc=%sysfunc(fwrite(&fid)); '; - put ' '; - put '%let rc=%sysfunc(fclose(&fid)); '; - put '%let rc=%sysfunc(filename(&fref)); '; - put ' '; - put '%mend mfs_httpheader; '; -/* WEBOUT END */ - put '%macro webout(action,ds,dslabel=,fmt=,missing=NULL,showmeta=NO'; - put ' ,maxobs=MAX'; - put ');'; - put ' %ms_webout(&action,ds=&ds,dslabel=&dslabel,fmt=&fmt,missing=&missing'; - put ' ,showmeta=&showmeta,maxobs=&maxobs'; - put ' )'; - put '%mend;'; -run; - -/* add precode and code */ -%local x fref freflist; -%let freflist=&precode &code ; -%do x=1 %to %sysfunc(countw(&freflist)); - %let fref=%scan(&freflist,&x); - %put &sysmacroname: adding &fref; - data _null_; - file &sasjsref lrecl=3000 termstr=crlf mod; - infile &fref lrecl=3000; - input; - put _infile_; - run; -%end; - -/* create the web service */ -%ms_createfile(&path/&name..sas, inref=&sasjsref, mdebug=&mdebug) - -%put ;%put ;%put ;%put ;%put ;%put ; -%put &sysmacroname: STP &name successfully created in &path; -%put ;%put ;%put ; -%put Check it out here:; -%put ;%put ;%put ; -%put &_sasjs_apiserverurl.&_sasjs_apipath?_PROGRAM=&path/&name; -%put ;%put ;%put ;%put ;%put ;%put ; - -%mend ms_createwebservice; diff --git a/005_macros/ms_deletefile.sas b/005_macros/ms_deletefile.sas deleted file mode 100644 index 08d53fd..0000000 --- a/005_macros/ms_deletefile.sas +++ /dev/null @@ -1,48 +0,0 @@ -/*** HELP START ***//** - @file - @brief Deletes a file from SASjs Drive - @details Deletes a file from SASjs Drive, if it exists. - - Example: - - filename stpcode temp; - data _null_; - file stpcode; - put '%put hello world;'; - run; - %ms_createfile(/some/stored/program.sas, inref=stpcode) - - %ms_deletefile(/some/stored/program.sas) - - @param [in] driveloc The full path to the file in SASjs Drive - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - -

SAS Macros

- @li mf_getuniquefileref.sas - -**//*** HELP END ***/ - -%macro ms_deletefile(driveloc - ,mdebug=0 - ); - -%local headref; -%let headref=%mf_getuniquefileref(); - -data _null_; - file &headref lrecl=1000; - infile "&_sasjs_tokenfile" lrecl=1000; - input; - put _infile_; -run; - -proc http method='DELETE' headerin=&headref - url="&_sasjs_apiserverurl/SASjsApi/drive/file?_filePath=&driveloc"; -%if &mdebug=1 %then %do; - debug level=2; -%end; -run; - -filename &headref clear; - -%mend ms_deletefile; diff --git a/005_macros/ms_getfile.sas b/005_macros/ms_getfile.sas deleted file mode 100644 index 40aab2c..0000000 --- a/005_macros/ms_getfile.sas +++ /dev/null @@ -1,50 +0,0 @@ -/*** HELP START ***//** - @file - @brief Gets a file from SASjs Drive - @details Fetches a file on SASjs Drive and stores it in the output fileref. - - Example: - - %ms_getfile(/Public/app/dc/services/public/settings.sas, outref=myfile) - - @param [in] driveloc The full path to the file in SASjs Drive - @param [out] outref= (msgetfil) The fileref to contain the file. - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mf_getuniquename.sas - -**//*** HELP END ***/ - -%macro ms_getfile(driveloc - ,outref=msgetfil - ,mdebug=0 - ); - -/* use the recfm in a separate fileref to avoid issues with subsequent reads */ -%local binaryfref floc headref; -%let binaryfref=%mf_getuniquefileref(); -%let headref=%mf_getuniquefileref(); -%let floc=%sysfunc(pathname(work))/%mf_getuniquename().txt; -filename &outref "&floc" lrecl=32767; -filename &binaryfref "&floc" recfm=n; - -data _null_; - file &headref lrecl=1000; - infile "&_sasjs_tokenfile" lrecl=1000; - input; - put _infile_; -run; - -proc http method='GET' out=&binaryfref headerin=&headref - url="&_sasjs_apiserverurl/SASjsApi/drive/file?_filePath=&driveloc"; -%if &mdebug=1 %then %do; - debug level=2; -%end; -run; - -filename &binaryfref clear; -filename &headref clear; - -%mend ms_getfile; diff --git a/005_macros/ms_getgroups.sas b/005_macros/ms_getgroups.sas deleted file mode 100644 index bb54dcb..0000000 --- a/005_macros/ms_getgroups.sas +++ /dev/null @@ -1,147 +0,0 @@ -/*** HELP START ***//** - @file - @brief Fetches the list of groups from SASjs Server - @details Fetches the list of groups from SASjs Server and writes them to an - output dataset. Provide a username to filter for the groups for a particular - user. - - Example: - - %ms_getgroups(outds=userlist) - - With filter on username: - - %ms_getgroups(outds=userlist, user=James) - - With filter on userid: - - %ms_getgroups(outds=userlist, uid=1) - - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - @param [in] user= (0) Provide the username on which to filter - @param [in] uid= (0) Provide the userid on which to filter - @param [out] outds= (work.ms_getgroups) This output dataset will contain the - list of groups. Format: -|NAME:$32.|DESCRIPTION:$256.|GROUPID:best.| -|---|---|---| -|`SomeGroup `|`A group `|`1`| -|`Another Group`|`this is a different group`|`2`| -|`admin`|`Administrators `|`3`| - - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - @li mp_abort.sas - -

Related Files

- @li ms_creategroup.sas - @li ms_getgroups.test.sas - -**//*** HELP END ***/ - -%macro ms_getgroups( - user=0, - uid=0, - outds=work.ms_getgroups, - mdebug=0 -); - -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=ms_getgroups.sas - ,msg=%str(syscc=&syscc on macro entry) -) - -%local fref0 fref1 libref optval rc msg url; - -%if %sysget(MODE)=desktop %then %do; - /* groups api does not exist in desktop mode */ - data &outds; - length NAME $32 DESCRIPTION $256. GROUPID 8; - name="&sysuserid"; - description="&sysuserid (group - desktop mode)"; - groupid=1; - output; - stop; - run; - %return; -%end; - -%let fref0=%mf_getuniquefileref(); -%let fref1=%mf_getuniquefileref(); -%let libref=%mf_getuniquelibref(); - -/* avoid sending bom marker to API */ -%let optval=%sysfunc(getoption(bomfile)); -options nobomfile; - -data _null_; - file &fref0 lrecl=1000; - infile "&_sasjs_tokenfile" lrecl=1000; - input; - if _n_=1 then put "accept: application/json"; - put _infile_; -run; - -%if &mdebug=1 %then %do; - data _null_; - infile &fref0; - input; - put _infile_; - run; -%end; - -%if "&user" ne "0" %then %let url=/SASjsApi/user/by/username/&user; -%else %if "&uid" ne "0" %then %let url=/SASjsApi/user/&uid; -%else %let url=/SASjsApi/group; - - -proc http method='GET' headerin=&fref0 out=&fref1 - url="&_sasjs_apiserverurl.&url"; -%if &mdebug=1 %then %do; - debug level=1; -%end; -run; - -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=ms_getgroups.sas - ,msg=%str(Issue submitting GET query to SASjsApi) -) - -libname &libref JSON fileref=&fref1; - -%if "&user"="0" and "&uid"="0" %then %do; - data &outds; - length NAME $32 DESCRIPTION $256. GROUPID 8; - if _n_=1 then call missing(of _all_); - set &libref..root; - drop ordinal_root; - run; -%end; -%else %do; - data &outds; - length NAME $32 DESCRIPTION $256. GROUPID 8; - if _n_=1 then call missing(of _all_); - set &libref..groups; - drop ordinal_:; - run; -%end; - -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=ms_getgroups.sas - ,msg=%str(Issue reading response JSON) -) - -/* reset options */ -options &optval; - -%if &mdebug=1 %then %do; - filename &fref0 clear; - filename &fref1 clear; - libname &libref clear; -%end; - -%mend ms_getgroups; diff --git a/005_macros/ms_getusers.sas b/005_macros/ms_getusers.sas deleted file mode 100644 index 23366fa..0000000 --- a/005_macros/ms_getusers.sas +++ /dev/null @@ -1,148 +0,0 @@ -/*** HELP START ***//** - @file - @brief Fetches the list of users from SASjs Server - @details Fetches the list of users from SASjs Server and writes them to an - output dataset. Can also be filtered, for a particular group. - - Example: - - %ms_getusers(outds=userlist) - - Filtering for a group by group name: - - %ms_getusers(outds=work.groupmembers, group=GROUPNAME) - - Filtering for a group by group id: - - %ms_getusers(outds=work.groupmembers, gid=1) - - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - @param [in] group= (0) Set to a group name to filter members for that group - @param [in] gid= (0) Set to a group id to filter members for that group - @param [out] outds= (work.ms_getusers) This output dataset will contain the - list of user accounts. Format: -|DISPLAYNAME:$60.|USERNAME:$30.|ID:best.| -|---|---|---| -|`Super Admin `|`secretuser `|`1`| -|`Sabir Hassan`|`sabir`|`2`| -|`Mihajlo Medjedovic `|`mihajlo `|`3`| -|`Ivor Townsend `|`ivor `|`4`| -|`New User `|`newuser `|`5`| - - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mf_getuniquelibref.sas - @li mp_abort.sas - -

Related Files

- @li ms_createuser.sas - @li ms_getgroups.sas - @li ms_getusers.test.sas - -**//*** HELP END ***/ - -%macro ms_getusers( - outds=work.ms_getusers, - group=0, - gid=0, - mdebug=0 -); - -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=ms_getusers.sas - ,msg=%str(syscc=&syscc on macro entry) -) - -%local fref0 fref1 libref optval rc msg url; -%let fref0=%mf_getuniquefileref(); -%let fref1=%mf_getuniquefileref(); -%let libref=%mf_getuniquelibref(); - -%if %sysget(MODE)=desktop %then %do; - /* users api does not exist in desktop mode */ - data &outds; - length DISPLAYNAME $60 USERNAME:$30 ID 8; - USERNAME="&sysuserid"; - DISPLAYNAME="&sysuserid (desktop mode)"; - ID=1; - output; - stop; - run; - %return; -%end; - -/* avoid sending bom marker to API */ -%let optval=%sysfunc(getoption(bomfile)); -options nobomfile; - -data _null_; - file &fref0 lrecl=1000; - infile "&_sasjs_tokenfile" lrecl=1000; - input; - if _n_=1 then put "accept: application/json"; - put _infile_; -run; - -%if &mdebug=1 %then %do; - data _null_; - infile &fref0; - input; - put _infile_; - run; -%end; - -%if "&group" ne "0" %then %let url=/SASjsApi/group/by/groupname/&group; -%else %if "&gid" ne "0" %then %let url=/SASjsApi/group/&gid; -%else %let url=/SASjsApi/user; - -proc http method='GET' headerin=&fref0 out=&fref1 - url="&_sasjs_apiserverurl.&url"; -%if &mdebug=1 %then %do; - debug level=1; -%end; -run; - - -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=ms_getusers.sas - ,msg=%str(Issue submitting API query) -) - -libname &libref JSON fileref=&fref1; - -%if "&group"="0" and "&gid"="0" %then %do; - data &outds; - length DISPLAYNAME $60 USERNAME:$30 ID 8; - if nobs=0 then call missing(of _all_); - set &libref..root nobs=nobs; - drop ordinal_root; - run; -%end; -%else %do; - data &outds; - length DISPLAYNAME $60 USERNAME:$30 ID 8; - if nobs=0 then call missing(of _all_); - set &libref..users nobs=nobs; - drop ordinal_root ordinal_users; - run; -%end; - -%mp_abort( - iftrue=(&syscc ne 0) - ,mac=ms_getusers.sas - ,msg=%str(Issue reading response JSON) -) - -/* reset options */ -options &optval; - -%if &mdebug=1 %then %do; - filename &fref0 clear; - filename &fref1 clear; - libname &libref clear; -%end; - -%mend ms_getusers; diff --git a/005_macros/ms_runstp.sas b/005_macros/ms_runstp.sas deleted file mode 100644 index aed946e..0000000 --- a/005_macros/ms_runstp.sas +++ /dev/null @@ -1,215 +0,0 @@ -/*** HELP START ***//** - @file - @brief Executes a SASjs Server Stored Program - @details Runs a Stored Program (using POST method) and extracts the webout and - log from the response JSON. - - Example: - - %ms_runstp(/some/stored/program - ,debug=131 - ,outref=weboot - ) - - @param [in] pgm The full path to the Stored Program in SASjs Drive (_program - parameter) - @param [in] debug= (131) The value to supply to the _debug URL parameter - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - @param [in] inputparams=(_null_) A dataset containing name/value pairs in the - following format: - |name:$32|value:$10000| - |---|---| - |stpmacname|some value| - |mustbevalidname|can be anything, oops, %abort!!| - @param [in] inputfiles= (_null_) A dataset containing fileref/name/filename in - the following format: - |fileref:$8|name:$32|filename:$256| - |---|---|--| - |someref|some_name|some_filename.xls| - |fref2|another_file|zyx_v2.csv| - - @param [out] outref= (outweb) The output fileref to contain the response JSON - (will be created using temp engine) - @param [out] outlogds= (_null_) Set to the name of a dataset to contain the - log. Table format: - |line:$2000| - |---| - |log line 1| - |log line 2| - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mf_getuniquename.sas - @li mp_abort.sas - @li mp_chop.sas - -**//*** HELP END ***/ - -%macro ms_runstp(pgm - ,debug=131 - ,inputparams=_null_ - ,inputfiles=_null_ - ,outref=outweb - ,outlogds=_null_ - ,mdebug=0 - ); -%local dbg mainref authref boundary; -%let mainref=%mf_getuniquefileref(); -%let authref=%mf_getuniquefileref(); -%let boundary=%mf_getuniquename(); -%if &inputparams=0 %then %let inputparams=_null_; - -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; -%end; -%else %let dbg=*; - - -%mp_abort(iftrue=("&pgm"="") - ,mac=&sysmacroname - ,msg=%str(Program not provided) -) - -/* avoid sending bom marker to API */ -%local optval; -%let optval=%sysfunc(getoption(bomfile)); -options nobomfile; - -/* add params */ -data _null_; - file &mainref termstr=crlf lrecl=32767 mod; - length line $1000 name $32 value $32767; - if _n_=1 then call missing(of _all_); - set &inputparams; - put "--&boundary"; - line=cats('Content-Disposition: form-data; name="',name,'"'); - put line; - put ; - put value; -run; - -/* parse input file list */ -%local webcount; -%let webcount=0; -data _null_; - set &inputfiles end=last; - length fileref $8 name $32 filename $256; - call symputx(cats('webref',_n_),fileref,'l'); - call symputx(cats('webname',_n_),name,'l'); - call symputx(cats('webfilename',_n_),filename,'l'); - if last then do; - call symputx('webcount',_n_); - call missing(of _all_); - end; -run; - -/* write out the input files */ -%local i; -%do i=1 %to &webcount; - data _null_; - file &mainref termstr=crlf lrecl=32767 mod; - infile &&webref&i lrecl=32767; - if _n_ = 1 then do; - length line $32767; - line=cats( - 'Content-Disposition: form-data; name="' - ,"&&webname&i" - ,'"; filename="' - ,"&&webfilename&i" - ,'"' - ); - put "--&boundary"; - put line; - put "Content-Type: text/plain"; - put ; - end; - input; - put _infile_; /* add the actual file to be sent */ - run; -%end; - -data _null_; - file &mainref termstr=crlf mod; - put "--&boundary--"; -run; - -data _null_; - file &authref lrecl=1000; - infile "&_sasjs_tokenfile" lrecl=1000; - input; - if _n_=1 then put "Content-Type: multipart/form-data; boundary=&boundary"; - put _infile_; -run; - -%if &mdebug=1 %then %do; - data _null_; - infile &authref; - input; - put _infile_; - data _null_; - infile &mainref; - input; - put _infile_; - run; -%end; - -%local resp_path; -%let resp_path=%sysfunc(pathname(work))/%mf_getuniquename(); -filename &outref "&resp_path" lrecl=32767; - -/* prepare request*/ -proc http method='POST' headerin=&authref in=&mainref out=&outref - url="&_sasjs_apiserverurl.&_sasjs_apipath?_program=&pgm%str(&)_debug=131"; -%if &mdebug=1 %then %do; - debug level=2; -%end; -run; - -%if (&SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201) -or &mdebug=1 -%then %do; - data _null_;infile &outref;input;putlog _infile_;run; -%end; -%mp_abort( - iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201) - ,mac=&sysmacroname - ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) -) - -/* reset options */ -options &optval; - -%if &outlogds ne _null_ or &mdebug=1 %then %do; - %local matchstr chopout; - %let matchstr=SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784; - %let chopout=%sysfunc(pathname(work))/%mf_getuniquename(prefix=chop); - - %mp_chop("&resp_path" - ,matchvar=matchstr - ,keep=LAST - ,matchpoint=END - ,outfile="&chopout" - ,mdebug=&mdebug - ) - - data &outlogds; - infile "&chopout" lrecl=2000; - length line $2000; - line=_infile_; - %if &mdebug=1 %then %do; - putlog line=; - %end; - run; -%end; - -%if &mdebug=1 %then %do; - %put &sysmacroname exit vars:; - %put _local_; -%end; -%else %do; - /* clear refs */ - filename &authref; - filename &mainref; -%end; -%mend ms_runstp; diff --git a/005_macros/ms_testservice.sas b/005_macros/ms_testservice.sas deleted file mode 100644 index caca8c7..0000000 --- a/005_macros/ms_testservice.sas +++ /dev/null @@ -1,141 +0,0 @@ -/*** HELP START ***//** - @file - @brief Will execute a SASjs web service on SASjs Server - @details Prepares the input files and retrieves the resulting datasets from - the response JSON. - - @param [in] program The Stored Program endpoint to test - @param [in] inputfiles=(0) A list of space seperated fileref:filename pairs as - follows: - inputfiles=inref:filename inref2:filename2 - @param [in] inputdatasets= (0) All datasets in this space seperated list are - converted into SASJS-formatted CSVs (see mp_ds2csv.sas) files and added to - the list of `inputfiles` for ingestion. The dataset will be sent with the - same name (no need for a colon modifier). - @param [in] inputparams=(0) A dataset containing name/value pairs in the - following format: - |name:$32|value:$1000| - |---|---| - |stpmacname|some value| - |mustbevalidname|can be anything, oops, %abort!!| - - @param [in] debug= (131) Provide the _debug value to pass to the STP - @param [in] mdebug= (0) Set to 1 to provide macro debugging (this macro) - @param [out] outlib= (0) Output libref to contain the final tables. Set to - 0 if the service output is not in JSON format. - @param [out] outref= (0) Output fileref to create, to contain the full _webout - response. - @param [out] outlogds= (_null_) Set to the name of a dataset to contain the - log. Table format: - |line:$2000| - |---| - |log line 1| - |log line 2| - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mf_getuniquename.sas - @li mp_abort.sas - @li mp_binarycopy.sas - @li mp_chop.sas - @li mp_ds2csv.sas - @li ms_runstp.sas - -

Related Programs

- @li mp_testservice.test.sas - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro ms_testservice(program, - inputfiles=0, - inputdatasets=0, - inputparams=0, - debug=0, - mdebug=0, - outlib=0, - outref=0, - outlogds=_null_ -)/*/STORE SOURCE*/; -%local dbg i var ds1 fref1 chopout1 chopout2; -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; -%end; -%else %let dbg=*; - -/* convert inputdatasets to filerefs */ -%if "&inputdatasets" ne "0" %then %do; - %if %quote(&inputfiles)=0 %then %let inputfiles=; - %do i=1 %to %sysfunc(countw(&inputdatasets,%str( ))); - %let var=%scan(&inputdatasets,&i,%str( )); - %local dsref&i; - %let dsref&i=%mf_getuniquefileref(); - %mp_ds2csv(&var,outref=&&dsref&i,headerformat=SASJS) - %let inputfiles=&inputfiles &&dsref&i:%scan(&var,-1,.); - %end; -%end; - -/* parse the filerefs - convert to a dataset */ -%let ds1=%mf_getuniquename(); -data &ds1; - length fileref $8 name $32 filename $256 var $300; - if "&inputfiles" ne "0" then do; - webcount=countw("&inputfiles"); - do i=1 to webcount; - var=scan("&inputfiles",i,' '); - fileref=scan(var,1,':'); - name=scan(var,2,':'); - filename=cats(name,'.csv'); - output; - end; - end; -run; - - -/* execute the STP */ -%let fref1=%mf_getuniquefileref(); - -%ms_runstp(&program - ,debug=&debug - ,inputparams=&inputparams - ,inputfiles=&ds1 - ,outref=&fref1 - ,mdebug=&mdebug - ,outlogds=&outlogds -) - - -/* chop out JSON section */ -%local matchstr chopout; -%let matchstr=SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784; -%let chopout=%sysfunc(pathname(work))/%mf_getuniquename(prefix=chop); - -%mp_chop("%sysfunc(pathname(&fref1,F))" - ,matchvar=matchstr - ,keep=FIRST - ,matchpoint=START - ,offset=-1 - ,outfile="&chopout" - ,mdebug=&mdebug -) - -%if &outlib ne 0 %then %do; - libname &outlib json "&chopout"; -%end; -%if &outref ne 0 %then %do; - filename &outref "&chopout"; -%end; - -%if &mdebug=0 %then %do; - filename &webref clear; - filename &fref1 clear; -%end; -%else %do; - %put &sysmacroname exit vars:; - %put _local_; -%end; - -%mend ms_testservice; diff --git a/005_macros/ms_webout.sas b/005_macros/ms_webout.sas deleted file mode 100644 index aa85895..0000000 --- a/005_macros/ms_webout.sas +++ /dev/null @@ -1,228 +0,0 @@ -/*** HELP START ***//** - @file - @brief Send data to/from sasjs/server - @details This macro should be added to the start of each web service, - **immediately** followed by a call to: - - %ms_webout(FETCH) - - This will read all the input data and create same-named SAS datasets in the - WORK library. You can then insert your code, and send data back using the - following syntax: - - data some datasets; * make some data ; - retain some columns; - run; - - %ms_webout(OPEN) - %ms_webout(ARR,some) * Array format, fast, suitable for large tables ; - %ms_webout(OBJ,datasets) * Object format, easier to work with ; - %ms_webout(CLOSE) - - - @param [in] action Either FETCH, OPEN, ARR, OBJ or CLOSE - @param [in] ds The dataset to send back to the frontend - @param [out] dslabel= value to use instead of table name for sending to JSON - @param [in] fmt= (N) Setting Y converts all vars to their formatted values - @param [out] fref= (_webout) The fileref to which to write the JSON - @param [in] missing= (NULL) Special numeric missing values can be sent as NULL - (eg `null`) or as STRING values (eg `".a"` or `".b"`) - @param [in] showmeta= (N) Set to Y to output metadata alongside each table, - such as the column formats and types. The metadata is contained inside an - object with the same name as the table but prefixed with a dollar sign - ie, - `,"$tablename":{"formats":{"col1":"$CHAR1"},"types":{"COL1":"C"}}` - @param [in] workobs= (0) When set to a positive integer, will create a new - output object (WORK) which contains this number of observations from all - tables in the WORK library. - @param [in] maxobs= (MAX) Provide an integer to limit the number of input rows - that should be converted to output JSON - -

SAS Macros

- @li mf_getuser.sas - @li mp_jsonout.sas - @li mfs_httpheader.sas - -

Related Macros

- @li mv_webout.sas - @li mm_webout.sas - - @version 9.3 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro ms_webout(action,ds,dslabel=,fref=_webout,fmt=N,missing=NULL - ,showmeta=N,maxobs=MAX,workobs=0 -); -%global _webin_file_count _webin_fileref1 _webin_name1 _program _debug - sasjs_tables; - -%local i tempds; -%let action=%upcase(&action); - -%if &action=FETCH %then %do; - %if %str(&_debug) ge 131 %then %do; - options mprint notes mprintnest; - %end; - %let _webin_file_count=%eval(&_webin_file_count+0); - /* now read in the data */ - %do i=1 %to &_webin_file_count; - %if &_webin_file_count=1 %then %do; - %let _webin_fileref1=&_webin_fileref; - %let _webin_name1=&_webin_name; - %end; - data _null_; - infile &&_webin_fileref&i termstr=crlf lrecl=32767; - input; - call symputx('input_statement',_infile_); - putlog "&&_webin_name&i input statement: " _infile_; - stop; - data &&_webin_name&i; - infile &&_webin_fileref&i firstobs=2 dsd termstr=crlf encoding='utf-8' - lrecl=32767; - input &input_statement; - %if %str(&_debug) ge 131 %then %do; - if _n_<20 then putlog _infile_; - %end; - run; - %let sasjs_tables=&sasjs_tables &&_webin_name&i; - %end; -%end; - -%else %if &action=OPEN %then %do; - /* fix encoding and ensure enough lrecl */ - OPTIONS NOBOMFILE lrecl=32767; - - /* set the header */ - %mfs_httpheader(Content-type,application/json) - - /* setup json. */ - data _null_;file &fref encoding='utf-8' termstr=lf ; - put '{"SYSDATE" : "' "&SYSDATE" '"'; - put ',"SYSTIME" : "' "&SYSTIME" '"'; - run; - -%end; - -%else %if &action=ARR or &action=OBJ %then %do; - %if "%substr(&sysver,1,1)"="4" or "%substr(&sysver,1,1)"="5" %then %do; - /* functions in formats unsupported */ - %put &sysmacroname: forcing missing back to NULL as feature not supported; - %let missing=NULL; - %end; - %mp_jsonout(&action,&ds,dslabel=&dslabel,fmt=&fmt,jref=&fref - ,engine=DATASTEP,missing=&missing,showmeta=&showmeta,maxobs=&maxobs - ) -%end; -%else %if &action=CLOSE %then %do; - %if %str(&workobs) > 0 %then %do; - /* if debug mode, send back first XX records of each work table also */ - data;run;%let tempds=%scan(&syslast,2,.); - ods output Members=&tempds; - proc datasets library=WORK memtype=data; - %local wtcnt;%let wtcnt=0; - data _null_; - set &tempds; - if not (upcase(name) =:"DATA"); /* ignore temp datasets */ - if not (upcase(name)=:"_DATA_"); - i+1; - call symputx(cats('wt',i),name,'l'); - call symputx('wtcnt',i,'l'); - data _null_; file &fref mod encoding='utf-8' termstr=lf; - put ",""WORK"":{"; - %do i=1 %to &wtcnt; - %let wt=&&wt&i; - data _null_; file &fref mod encoding='utf-8' termstr=lf; - dsid=open("WORK.&wt",'is'); - nlobs=attrn(dsid,'NLOBS'); - nvars=attrn(dsid,'NVARS'); - rc=close(dsid); - if &i>1 then put ','@; - put " ""&wt"" : {"; - put '"nlobs":' nlobs; - put ',"nvars":' nvars; - %mp_jsonout(OBJ,&wt,jref=&fref,dslabel=first10rows,showmeta=Y - ,maxobs=&workobs - ) - data _null_; file &fref mod encoding='utf-8' termstr=lf; - put "}"; - %end; - data _null_; file &fref mod encoding='utf-8' termstr=lf; - put "}"; - run; - %end; - /* close off json */ - data _null_;file &fref mod encoding='utf-8' termstr=lf lrecl=32767; - length SYSPROCESSNAME syserrortext syswarningtext autoexec $512; - put ",""_DEBUG"" : ""&_debug"" "; - _PROGRAM=quote(trim(resolve(symget('_PROGRAM')))); - put ',"_PROGRAM" : ' _PROGRAM ; - autoexec=quote(urlencode(trim(getoption('autoexec')))); - put ',"AUTOEXEC" : ' autoexec; - put ",""MF_GETUSER"" : ""%mf_getuser()"" "; - put ",""SYSCC"" : ""&syscc"" "; - put ",""SYSENCODING"" : ""&sysencoding"" "; - syserrortext=cats(symget('syserrortext')); - if findc(syserrortext,'"\'!!'0A0D09000E0F010210111A'x) then do; - syserrortext='"'!!trim( - prxchange('s/"/\\"/',-1, /* double quote */ - prxchange('s/\x0A/\n/',-1, /* new line */ - prxchange('s/\x0D/\r/',-1, /* carriage return */ - prxchange('s/\x09/\\t/',-1, /* tab */ - prxchange('s/\x00/\\u0000/',-1, /* NUL */ - prxchange('s/\x0E/\\u000E/',-1, /* SS */ - prxchange('s/\x0F/\\u000F/',-1, /* SF */ - prxchange('s/\x01/\\u0001/',-1, /* SOH */ - prxchange('s/\x02/\\u0002/',-1, /* STX */ - prxchange('s/\x10/\\u0010/',-1, /* DLE */ - prxchange('s/\x11/\\u0011/',-1, /* DC1 */ - prxchange('s/\x1A/\\u001A/',-1, /* SUB */ - prxchange('s/\\/\\\\/',-1,syserrortext) - )))))))))))))!!'"'; - end; - else syserrortext=cats('"',syserrortext,'"'); - put ',"SYSERRORTEXT" : ' syserrortext; - SYSHOSTINFOLONG=quote(trim(symget('SYSHOSTINFOLONG'))); - put ',"SYSHOSTINFOLONG" : ' SYSHOSTINFOLONG; - put ",""SYSHOSTNAME"" : ""&syshostname"" "; - put ",""SYSPROCESSID"" : ""&SYSPROCESSID"" "; - put ",""SYSPROCESSMODE"" : ""&SYSPROCESSMODE"" "; - SYSPROCESSNAME=quote(urlencode(cats(SYSPROCESSNAME))); - put ",""SYSPROCESSNAME"" : " SYSPROCESSNAME; - put ",""SYSJOBID"" : ""&sysjobid"" "; - put ",""SYSSCPL"" : ""&sysscpl"" "; - put ",""SYSSITE"" : ""&syssite"" "; - put ",""SYSTCPIPHOSTNAME"" : ""&SYSTCPIPHOSTNAME"" "; - put ",""SYSUSERID"" : ""&sysuserid"" "; - sysvlong=quote(trim(symget('sysvlong'))); - put ',"SYSVLONG" : ' sysvlong; - syswarningtext=cats(symget('syswarningtext')); - if findc(syswarningtext,'"\'!!'0A0D09000E0F010210111A'x) then do; - syswarningtext='"'!!trim( - prxchange('s/"/\\"/',-1, /* double quote */ - prxchange('s/\x0A/\n/',-1, /* new line */ - prxchange('s/\x0D/\r/',-1, /* carriage return */ - prxchange('s/\x09/\\t/',-1, /* tab */ - prxchange('s/\x00/\\u0000/',-1, /* NUL */ - prxchange('s/\x0E/\\u000E/',-1, /* SS */ - prxchange('s/\x0F/\\u000F/',-1, /* SF */ - prxchange('s/\x01/\\u0001/',-1, /* SOH */ - prxchange('s/\x02/\\u0002/',-1, /* STX */ - prxchange('s/\x10/\\u0010/',-1, /* DLE */ - prxchange('s/\x11/\\u0011/',-1, /* DC1 */ - prxchange('s/\x1A/\\u001A/',-1, /* SUB */ - prxchange('s/\\/\\\\/',-1,syswarningtext) - )))))))))))))!!'"'; - end; - else syswarningtext=cats('"',syswarningtext,'"'); - put ',"SYSWARNINGTEXT" : ' syswarningtext; - put ',"END_DTTM" : "' "%sysfunc(datetime(),E8601DT26.6)" '" '; - length memsize $32; - memsize="%sysfunc(INPUTN(%sysfunc(getoption(memsize)), best.),sizekmg.)"; - memsize=quote(cats(memsize)); - put ',"MEMSIZE" : ' memsize; - put "}" @; - run; -%end; - -%mend ms_webout; diff --git a/006_macros/mcf_getfmttype.sas b/006_macros/mcf_getfmttype.sas deleted file mode 100644 index 782b5ef..0000000 --- a/006_macros/mcf_getfmttype.sas +++ /dev/null @@ -1,118 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns the type of the format - @details - Returns the type, eg DATE / DATETIME / TIME (based on hard-coded lookup) - else CHAR / NUM. - - This macro may be extended in the future to support custom formats - this - would necessitate a call to `dosubl()` for running a proc format with cntlout. - - The function itself takes the following (positional) parameters: - - | PARAMETER | DESCRIPTION | - |---|---| - |fmtnm| Format name to be tested. Can be with or without the w.d extension.| - - Usage: - - %mcf_getfmttype(wrap=YES, insert_cmplib=YES) - - data _null_; - fmt1=mcf_getfmttype('DATE9.'); - fmt2=mcf_getfmttype('DATETIME'); - put (fmt:)(=); - run; - %put fmt3=%sysfunc(mcf_getfmttype(TIME9.)); - - Returns: - - > fmt1=DATE fmt2=DATETIME - > fmt3=TIME - - @param [out] wrap= (NO) Choose YES to add the proc fcmp wrapper. - @param [out] lib= (work) The output library in which to create the catalog. - @param [out] cat= (sasjs) The output catalog in which to create the package. - @param [out] pkg= (utils) The output package in which to create the function. - Uses a 3 part format: libref.catalog.package - @param [out] insert_cmplib= DEPRECATED - The CMPLIB option is checked and - values inserted only if needed. - -

SAS Macros

- @li mcf_init.sas - -

Related Programs

- @li mcf_getfmttype.test.sas - @li mp_init.sas - - @todo "Custom Format Lookups" To enable site-specific formats, make - use of a set of SASJS_FMTLIST_(DATATYPE) global variables. - -**//*** HELP END ***/ - -%macro mcf_getfmttype(wrap=NO - ,insert_cmplib=DEPRECATED - ,lib=WORK - ,cat=SASJS - ,pkg=UTILS -)/*/STORE SOURCE*/; -%local i var cmpval found; - -%if %mcf_init(mcf_getfmttype)=1 %then %return; - -%if &wrap=YES %then %do; - proc fcmp outlib=&lib..&cat..&pkg; -%end; - -function mcf_getfmttype(fmtnm $) $8; - if substr(fmtnm,1,1)='$' then return('CHAR'); - else do; - /* extract NAME */ - length fmt $32; - fmt=scan(fmtnm,1,'.'); - do while ( - substr(fmt,length(fmt),1) in ('1','2','3','4','5','6','7','8','9','0') - ); - if length(fmt)=1 then fmt='W'; - else fmt=substr(fmt,1,length(fmt)-1); - end; - - /* apply lookups */ - if cats(fmt) in ('DATETIME','B8601DN','B8601DN','B8601DT','B8601DT' - ,'B8601DZ','B8601DZ','DATEAMPM','DTDATE','DTMONYY','DTWKDATX','DTYEAR' - ,'DTYYQC','E8601DN','E8601DN','E8601DT','E8601DT','E8601DZ','E8601DZ') - then return('DATETIME'); - else if fmt in ('DATE','YYMMDD','B8601DA','B8601DA','DAY','DDMMYY' - ,'DDMMYYB','DDMMYYC','DDMMYYD','DDMMYYN','DDMMYYP','DDMMYYS','DDMMYYx' - ,'DOWNAME','E8601DA','E8601DA','JULDAY','JULIAN','MMDDYY','MMDDYYB' - ,'MMDDYYC','MMDDYYD','MMDDYYN','MMDDYYP','MMDDYYS','MMDDYYx','MMYY' - ,'MMYYC','MMYYD','MMYYN','MMYYP','MMYYS','MMYYx','MONNAME','MONTH' - ,'MONYY','PDJULG','PDJULI','QTR','QTRR','WEEKDATE','WEEKDATX','WEEKDAY' - ,'WEEKU','WEEKV','WEEKW','WORDDATE','WORDDATX','YEAR','YYMM','YYMMC' - ,'YYMMD','YYMMDDB','YYMMDDC','YYMMDDD','YYMMDDN','YYMMDDP','YYMMDDS' - ,'YYMMDDx','YYMMN','YYMMP','YYMMS','YYMMx','YYMON','YYQ','YYQC','YYQD' - ,'YYQN','YYQP','YYQR','YYQRC','YYQRD','YYQRN','YYQRP','YYQRS','YYQRx' - ,'YYQS','YYQx','YYQZ') then return('DATE'); - else if fmt in ('TIME','B8601LZ','B8601LZ','B8601TM','B8601TM','B8601TZ' - ,'B8601TZ','E8601LZ','E8601LZ','E8601TM','E8601TM','E8601TZ','E8601TZ' - ,'HHMM','HOUR','MMSS','TIMEAMPM','TOD') then return('TIME'); - else return('NUM'); - end; -endsub; - -%if &wrap=YES %then %do; - quit; -%end; - -/* insert the CMPLIB if not already there */ -%let cmpval=%sysfunc(getoption(cmplib)); -%let found=0; -%do i=1 %to %sysfunc(countw(&cmpval,%str( %(%)))); - %let var=%scan(&cmpval,&i,%str( %(%))); - %if &var=&lib..&cat %then %let found=1; -%end; -%if &found=0 %then %do; - options insert=(CMPLIB=(&lib..&cat)); -%end; - -%mend mcf_getfmttype; diff --git a/006_macros/mcf_init.sas b/006_macros/mcf_init.sas deleted file mode 100644 index d581d5d..0000000 --- a/006_macros/mcf_init.sas +++ /dev/null @@ -1,44 +0,0 @@ -/*** HELP START ***//** - @file - @brief Sets up the mcf_xx functions - @details - There is no (efficient) way to determine if an mcf_xx macro has already been - invoked. So, we make use of a global macro variable list to keep track. - - Usage: - - %mcf_init(MCF_LENGTH) - - Returns: - - > 1 (if already initialised) else 0 - - @param [in] func The function to be initialised - -

Related Macros

- @li mcf_init.test.sas - -**//*** HELP END ***/ - -%macro mcf_init(func -)/*/STORE SOURCE*/; - -%if not (%symexist(SASJS_PREFIX)) %then %do; - %global SASJS_PREFIX; - %let SASJS_PREFIX=SASJS; -%end; - -%let func=%upcase(&func); - -/* the / character is just a seperator */ -%global &sasjs_prefix._FUNCTIONS; -%if %index(&&&sasjs_prefix._FUNCTIONS,&func/)>0 %then %do; - 1 - %return; -%end; -%else %do; - %let &sasjs_prefix._FUNCTIONS=&&&sasjs_prefix._FUNCTIONS &func/; - 0 -%end; - -%mend mcf_init; diff --git a/006_macros/mcf_length.sas b/006_macros/mcf_length.sas deleted file mode 100644 index 5e8ec3d..0000000 --- a/006_macros/mcf_length.sas +++ /dev/null @@ -1,90 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns the length of a numeric value - @details - Returns the length, in bytes, of a numeric value. If the value is - missing, then 0 is returned. - - The function itself takes the following (positional) parameters: - - | PARAMETER | DESCRIPTION | - |---|---| - | var | variable (or value) to be tested| - - Usage: - - %mcf_length(wrap=YES, insert_cmplib=YES) - - data _null_; - ina=1; - inb=10000000; - inc=12345678; - ind=.; - outa=mcf_length(ina); - outb=mcf_length(inb); - outc=mcf_length(inc); - outd=mcf_length(ind); - put (out:)(=); - run; - - Returns: - - > outa=3 outb=4 outc=5 outd=0 - - @param [out] wrap= (NO) Choose YES to add the proc fcmp wrapper. - @param [out] lib= (work) The output library in which to create the catalog. - @param [out] cat= (sasjs) The output catalog in which to create the package. - @param [out] pkg= (utils) The output package in which to create the function. - Uses a 3 part format: libref.catalog.package - @param [out] insert_cmplib= DEPRECATED - The CMPLIB option is checked and - values inserted only if needed. - -

SAS Macros

- @li mcf_init.sas - -

Related Programs

- @li mcf_length.test.sas - @li mp_init.sas - -**//*** HELP END ***/ - -%macro mcf_length(wrap=NO - ,insert_cmplib=DEPRECATED - ,lib=WORK - ,cat=SASJS - ,pkg=UTILS -)/*/STORE SOURCE*/; -%local i var cmpval found; -%if %mcf_init(mcf_length)=1 %then %return; - -%if &wrap=YES %then %do; - proc fcmp outlib=&lib..&cat..&pkg; -%end; - -function mcf_length(var); - if var=. then len=0; - else if missing(var) or trunc(var,3)=var then len=3; - else if trunc(var,4)=var then len=4; - else if trunc(var,5)=var then len=5; - else if trunc(var,6)=var then len=6; - else if trunc(var,7)=var then len=7; - else len=8; - return(len); -endsub; - -%if &wrap=YES %then %do; - quit; -%end; - -/* insert the CMPLIB if not already there */ -%let cmpval=%sysfunc(getoption(cmplib)); -%let found=0; -%do i=1 %to %sysfunc(countw(&cmpval,%str( %(%)))); - %let var=%scan(&cmpval,&i,%str( %(%))); - %if &var=&lib..&cat %then %let found=1; -%end; -%if &found=0 %then %do; - options insert=(CMPLIB=(&lib..&cat)); -%end; - -%mend mcf_length; diff --git a/006_macros/mcf_string2file.sas b/006_macros/mcf_string2file.sas deleted file mode 100644 index 1e0d2d6..0000000 --- a/006_macros/mcf_string2file.sas +++ /dev/null @@ -1,95 +0,0 @@ -/*** HELP START ***//** - @file - @brief Adds a string to a file - @details Creates an fcmp function for appending a string to an external file. - If the file does not exist, it is created. - - The function itself takes the following (positional) parameters: - - | PARAMETER | DESCRIPTION | - |------------|-------------| - | filepath $ | full path to the file| - | string $ | string to add to the file | - | mode $ | mode of the output - either APPEND (default) or CREATE | - - It returns 0 if successful, or -1 if an error occured. - - Usage: - - %mcf_string2file(wrap=YES, insert_cmplib=YES) - - data _null_; - rc=mcf_string2file( - "%sysfunc(pathname(work))/newfile.txt" - , "This is a test" - , "CREATE"); - run; - - data _null_; - infile "%sysfunc(pathname(work))/newfile.txt"; - input; - putlog _infile_; - run; - - @param [out] wrap= (NO) Choose YES to add the proc fcmp wrapper. - @param [out] lib= (work) The output library in which to create the catalog. - @param [out] cat= (sasjs) The output catalog in which to create the package. - @param [out] pkg= (utils) The output package in which to create the function. - Uses a 3 part format: libref.catalog.package - @param [out] insert_cmplib= DEPRECATED - The CMPLIB option is checked and - values inserted only if needed. - -

SAS Macros

- @li mcf_init.sas - -

Related Programs

- @li mcf_stpsrv_header.test.sas - @li mp_init.sas - -**//*** HELP END ***/ - -%macro mcf_string2file(wrap=NO - ,insert_cmplib=DEPRECATED - ,lib=WORK - ,cat=SASJS - ,pkg=UTILS -)/*/STORE SOURCE*/; -%local i var cmpval found; -%if %mcf_init(mcf_string2file)=1 %then %return; - -%if &wrap=YES %then %do; - proc fcmp outlib=&lib..&cat..&pkg; -%end; - -function mcf_string2file(filepath $, string $, mode $); - if mode='APPEND' then fmode='a'; - else fmode='o'; - length fref $8; - rc=filename(fref,filepath); - if (rc ne 0) then return( -1 ); - fid = fopen(fref,fmode); - if (fid = 0) then return( -1 ); - rc=fput(fid, string); - rc=fwrite(fid); - rc=fclose(fid); - rc=filename(fref); - return(0); -endsub; - - -%if &wrap=YES %then %do; - quit; -%end; - -/* insert the CMPLIB if not already there */ -%let cmpval=%sysfunc(getoption(cmplib)); -%let found=0; -%do i=1 %to %sysfunc(countw(&cmpval,%str( %(%)))); - %let var=%scan(&cmpval,&i,%str( %(%))); - %if &var=&lib..&cat %then %let found=1; -%end; -%if &found=0 %then %do; - options insert=(CMPLIB=(&lib..&cat)); -%end; - -%mend mcf_string2file; diff --git a/006_macros/mddl_dc_difftable.sas b/006_macros/mddl_dc_difftable.sas deleted file mode 100644 index 4df0222..0000000 --- a/006_macros/mddl_dc_difftable.sas +++ /dev/null @@ -1,44 +0,0 @@ -/*** HELP START ***//** - @file - @brief Difftable DDL - @details Used to store changes to tables. Used by mp_storediffs.sas - and mp_stackdiffs.sas - -**//*** HELP END ***/ - - -%macro mddl_dc_difftable(libds=WORK.DIFFTABLE); - - proc sql; - create table &libds( - load_ref char(36) label='unique load reference', - processed_dttm num format=E8601DT26.6 label='Processed at timestamp', - libref char(8) label='Library Reference (8 chars)', - dsn char(32) label='Dataset Name (32 chars)', - key_hash char(32) label= - 'MD5 Hash of primary key values (pipe seperated)', - move_type char(1) label='Either (A)ppended, (D)eleted or (M)odified', - is_pk num label='Is Primary Key Field? (1/0)', - is_diff num label= - 'Did value change? (1/0/-1). Always -1 for appends and deletes.', - tgtvar_type char(1) label='Either (C)haracter or (N)umeric', - tgtvar_nm char(32) label='Target variable name (32 chars)', - oldval_num num format=best32. label='Old (numeric) value', - newval_num num format=best32. label='New (numeric) value', - oldval_char char(32765) label='Old (character) value', - newval_char char(32765) label='New (character) value' - ); - - %local lib; - %let libds=%upcase(&libds); - %if %index(&libds,.)=0 %then %let lib=WORK; - %else %let lib=%scan(&libds,1,.); - - proc datasets lib=&lib noprint; - modify %scan(&libds,-1,.); - index create - pk_mpe_audit=(load_ref libref dsn key_hash tgtvar_nm) - /nomiss unique; - quit; - -%mend mddl_dc_difftable; diff --git a/006_macros/mddl_dc_filterdetail.sas b/006_macros/mddl_dc_filterdetail.sas deleted file mode 100644 index 3148f01..0000000 --- a/006_macros/mddl_dc_filterdetail.sas +++ /dev/null @@ -1,40 +0,0 @@ -/*** HELP START ***//** - @file - @brief Filtertable DDL - @details For storing detailed filter values. Used by - mp_filterstore.sas. - -**//*** HELP END ***/ - - -%macro mddl_dc_filterdetail(libds=WORK.FILTER_DETAIL); - -%local nn lib; -%if "%substr(&sysver,1,1)" ne "4" and "%substr(&sysver,1,1)" ne "5" %then %do; - %let nn=not null; -%end; -%else %let nn=; - - proc sql; - create table &libds( - filter_hash char(32) &nn, - filter_line num &nn, - group_logic char(3) &nn, - subgroup_logic char(3) &nn, - subgroup_id num &nn, - variable_nm varchar(32) &nn, - operator_nm varchar(12) &nn, - raw_value varchar(4000) &nn, - processed_dttm num &nn format=E8601DT26.6 - ); - - %let libds=%upcase(&libds); - %if %index(&libds,.)=0 %then %let lib=WORK; - %else %let lib=%scan(&libds,1,.); - - proc datasets lib=&lib noprint; - modify %scan(&libds,-1,.); - index create pk_mpe_filterdetail=(filter_hash filter_line)/nomiss unique; - quit; - -%mend mddl_dc_filterdetail; diff --git a/006_macros/mddl_dc_filtersummary.sas b/006_macros/mddl_dc_filtersummary.sas deleted file mode 100644 index e3858e7..0000000 --- a/006_macros/mddl_dc_filtersummary.sas +++ /dev/null @@ -1,35 +0,0 @@ -/*** HELP START ***//** - @file - @brief Filtersummary DDL - @details For storing summary filter values. Used by - mp_filterstore.sas. - -**//*** HELP END ***/ - - -%macro mddl_dc_filtersummary(libds=WORK.FILTER_SUMMARY); - -%local nn lib; -%if "%substr(&sysver,1,1)" ne "4" and "%substr(&sysver,1,1)" ne "5" %then %do; - %let nn=not null; -%end; -%else %let nn=; - - proc sql; - create table &libds( - filter_rk num &nn, - filter_hash char(32) &nn, - filter_table char(41) &nn, - processed_dttm num &nn format=E8601DT26.6 - ); - - %let libds=%upcase(&libds); - %if %index(&libds,.)=0 %then %let lib=WORK; - %else %let lib=%scan(&libds,1,.); - - proc datasets lib=&lib noprint; - modify %scan(&libds,-1,.); - index create filter_rk /nomiss unique; - quit; - -%mend mddl_dc_filtersummary; diff --git a/006_macros/mddl_dc_locktable.sas b/006_macros/mddl_dc_locktable.sas deleted file mode 100644 index dd42340..0000000 --- a/006_macros/mddl_dc_locktable.sas +++ /dev/null @@ -1,41 +0,0 @@ -/*** HELP START ***//** - @file - @brief Locktable DDL - @details For "locking" tables prior to multipass loads. Used by - mp_lockanytable.sas - -**//*** HELP END ***/ - - -%macro mddl_dc_locktable(libds=WORK.LOCKTABLE); - -%local nn lib; -%if "%substr(&sysver,1,1)" ne "4" and "%substr(&sysver,1,1)" ne "5" %then %do; - %let nn=not null; -%end; -%else %let nn=; - - proc sql; - create table &libds( - lock_lib char(8), - lock_ds char(32), - lock_status_cd char(10) &nn, - lock_user_nm char(100) &nn , - lock_ref char(200), - lock_pid char(10), - lock_start_dttm num format=E8601DT26.6, - lock_end_dttm num format=E8601DT26.6 - ); - - %let libds=%upcase(&libds); - %if %index(&libds,.)=0 %then %let lib=WORK; - %else %let lib=%scan(&libds,1,.); - - proc datasets lib=&lib noprint; - modify %scan(&libds,-1,.); - index create - pk_mp_lockanytable=(lock_lib lock_ds) - /nomiss unique; - quit; - -%mend mddl_dc_locktable; diff --git a/006_macros/mddl_dc_maxkeytable.sas b/006_macros/mddl_dc_maxkeytable.sas deleted file mode 100644 index 07e634e..0000000 --- a/006_macros/mddl_dc_maxkeytable.sas +++ /dev/null @@ -1,33 +0,0 @@ -/*** HELP START ***//** - @file - @brief Maxkeytable DDL - @details For storing the maximum retained key information. Used - by mp_retainedkey.sas - -**//*** HELP END ***/ - - -%macro mddl_dc_maxkeytable(libds=WORK.MAXKEYTABLE); - - proc sql; - create table &libds( - keytable varchar(41) label='Base table in libref.dataset format', - keycolumn char(32) format=$32. - label='The Retained key field containing the key values.', - max_key num label= - 'Integer representing current max RK or SK value in the KEYTABLE', - processed_dttm num format=E8601DT26.6 - label='Datetime this value was last updated' - ); - - %local lib; - %let libds=%upcase(&libds); - %if %index(&libds,.)=0 %then %let lib=WORK; - %else %let lib=%scan(&libds,1,.); - - proc datasets lib=&lib noprint; - modify %scan(&libds,-1,.); - index create keytable /nomiss unique; - quit; - -%mend mddl_dc_maxkeytable; diff --git a/006_macros/mddl_sas_cntlout.sas b/006_macros/mddl_sas_cntlout.sas deleted file mode 100644 index e0164b7..0000000 --- a/006_macros/mddl_sas_cntlout.sas +++ /dev/null @@ -1,57 +0,0 @@ -/*** HELP START ***//** - @file - @brief The CNTLOUT table generated by proc format - @details The actual CNTLOUT table may have varying variable lengths, - depending on the data values, therefore the max possible lengths - (given various practical restrictions) are described here to enable - consistency when dealing with format data. - -**//*** HELP END ***/ - - -%macro mddl_sas_cntlout(libds=WORK.CNTLOUT); - - proc sql; - create table &libds( - TYPE char(1) label='Type of format - either N (num fmt), C (char fmt), I (num infmt) or J (char infmt)' - ,FMTNAME char(32) label='Format name' - ,FMTROW num label='CALCULATED Position of record by FMTNAME (reqd for multilabel formats)' - ,START char(32767) label='Starting value for format' - /* - Keep lengths of START and END the same to avoid this err: - "Start is greater than end: -<." - Similar usage note: https://support.sas.com/kb/69/330.html - */ - ,END char(32767) label='Ending value for format' - ,LABEL char(32767) label='Format value label' - ,MIN num length=3 label='Minimum length' - ,MAX num length=3 label='Maximum length' - ,DEFAULT num length=3 label='Default length' - ,LENGTH num length=3 label='Format length' - ,FUZZ num label='Fuzz value' - ,PREFIX char(2) label='Prefix characters' - ,MULT num label='Multiplier' - ,FILL char(1) label='Fill character' - ,NOEDIT num length=3 label='Is picture string noedit?' - ,SEXCL char(1) label='Start exclusion' - ,EEXCL char(1) label='End exclusion' - ,HLO char(13) label='Additional information. M=MultiLabel' - ,DECSEP char(1) label='Decimal separator' - ,DIG3SEP char(1) label='Three-digit separator' - ,DATATYPE char(8) label='Date/time/datetime?' - ,LANGUAGE char(8) label='Language for date strings' - ); - - %local lib; - %let libds=%upcase(&libds); - %if %index(&libds,.)=0 %then %let lib=WORK; - %else %let lib=%scan(&libds,1,.); - - proc datasets lib=&lib noprint; - modify %scan(&libds,-1,.); - index create - pk_cntlout=(type fmtname fmtrow) - /nomiss unique; - quit; - -%mend mddl_sas_cntlout; diff --git a/004_macros/mmx_createmetafolder.sas b/006_macros/mmx_createmetafolder.sas similarity index 100% rename from 004_macros/mmx_createmetafolder.sas rename to 006_macros/mmx_createmetafolder.sas diff --git a/004_macros/mmx_deletemetafolder.sas b/006_macros/mmx_deletemetafolder.sas similarity index 100% rename from 004_macros/mmx_deletemetafolder.sas rename to 006_macros/mmx_deletemetafolder.sas diff --git a/004_macros/mmx_spkexport.sas b/006_macros/mmx_spkexport.sas similarity index 100% rename from 004_macros/mmx_spkexport.sas rename to 006_macros/mmx_spkexport.sas diff --git a/007_macros/mx_createwebservice.sas b/007_macros/mx_createwebservice.sas deleted file mode 100644 index 007f22e..0000000 --- a/007_macros/mx_createwebservice.sas +++ /dev/null @@ -1,98 +0,0 @@ -/*** HELP START ***//** - @file mx_createwebservice.sas - @brief Create a web service in SAS 9, Viya or SASjs - @details Creates a SASJS ready Stored Process in SAS 9, a Job Execution - Service in SAS Viya, or a Stored Program on SASjs Server - depending on the - executing environment. - -Usage: - - %* compile macros ; - filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - %* write some code; - filename ft15f001 temp; - parmcards4; - %* fetch any data from frontend ; - %webout(FETCH) - data example1 example2; - set sashelp.class; - run; - %* send data back; - %webout(OPEN) - %webout(ARR,example1) * Array format, fast, suitable for large tables ; - %webout(OBJ,example2) * Object format, easier to work with ; - %webout(CLOSE) - ;;;; - - %* create the service (including webout macro and dependencies); - %mx_createwebservice(path=/Public/app/common,name=appInit,replace=YES) - -

SAS Macros

- @li mf_getplatform.sas - @li mm_createwebservice.sas - @li ms_createwebservice.sas - @li mv_createwebservice.sas - - @param [in,out] path= The full folder path where the service will be created - @param [in,out] name= Service name. Avoid spaces. - @param [in] desc= The description of the service (optional) - @param [in] precode= Space separated list of filerefs, pointing to the code - that needs to be attached to the beginning of the service (optional) - @param [in] code= (ft15f001) Space seperated fileref(s) of the actual code to - be added - @param [in] replace= (YES) Select YES to replace any existing service in that - location - @param [in] mDebug= (0) set to 1 to show debug messages in the log - - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mx_createwebservice(path=HOME - ,name=initService - ,precode= - ,code=ft15f001 - ,desc=This service was created by the mp_createwebservice macro - ,replace=YES - ,mdebug=0 -)/*/STORE SOURCE*/; - -%if &syscc ge 4 %then %do; - %put syscc=&syscc - &sysmacroname will not execute in this state; - %return; -%end; - -%local platform; %let platform=%mf_getplatform(); -%if &platform=SASVIYA %then %do; - %if "&path"="HOME" %then %let path=/Users/&sysuserid/My Folder; - %mv_createwebservice(path=&path - ,name=&name - ,code=&code - ,precode=&precode - ,desc=&desc - ,replace=&replace - ) -%end; -%else %if &platform=SASJS %then %do; - %if "&path"="HOME" %then %let path=/Users/&_sasjs_username/My Folder; - %ms_createwebservice(path=&path - ,name=&name - ,code=&code - ,precode=&precode - ,mdebug=&mdebug - ) -%end; -%else %do; - %if "&path"="HOME" %then %let path=/User Folders/&_METAPERSON/My Folder; - %mm_createwebservice(path=&path - ,name=&name - ,code=&code - ,precode=&precode - ,desc=&desc - ,replace=&replace - ) -%end; - -%mend mx_createwebservice; diff --git a/007_macros/mx_getcode.sas b/007_macros/mx_getcode.sas deleted file mode 100644 index f035c85..0000000 --- a/007_macros/mx_getcode.sas +++ /dev/null @@ -1,57 +0,0 @@ -/*** HELP START ***//** - @file - @brief Fetches code from Viya Job, SAS 9 STP, or SASjs Server STP - @details When building applications that run on multiple flavours of SAS, it - is convenient to use a single macro (like this one) to fetch the source - code from a Viya Job, SAS 9 Stored Process, or SASjs Stored Program. - - The alternative would be to compile a generic macro in target-specific - folders (SASVIYA, SAS9 and SASJS). This avoids compiling unnecessary macros - at the expense of a more complex sasjsconfig.json setup. - - - @param [in] loc The full path to the Viya Job, SAS 9 Stored Process or SASjs - Stored Program in Drive or Metadata, WITHOUT the .sas extension (SASjs only) - @param [out] outref= (0) The fileref to create, which will contain the source - code. - -

SAS Macros

- @li mf_getplatform.sas - @li mm_getstpcode.sas - @li ms_getfile.sas - @li mv_getjobcode.sas - - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mx_getcode(loc,outref=0 -)/*/STORE SOURCE*/; - -%local platform name shortloc; -%let platform=%mf_getplatform(); - -%if &platform=SASJS %then %do; - %ms_getfile(&loc..sas, outref=&outref) -%end; -%else %if &platform=SAS9 or &platform=SASMETA %then %do; - %mm_getstpcode(tree=&loc,outref=&outref) -%end; -%else %if &platform=SASVIYA %then %do; - /* extract name & path from &loc */ - data _null_; - loc=symget('loc'); - name=scan(loc,-1,'/'); - shortloc=substr(loc,1,length(loc)-length(name)-1); - call symputx('name',name,'l'); - call symputx('shortloc',shortloc,'l'); - run; - %mv_getjobcode( - path=&shortloc, - name=&name, - outref=&outref - ) -%end; -%else %put &sysmacroname: &platform is unsupported!!!; - -%mend mx_getcode; diff --git a/007_macros/mx_testservice.sas b/007_macros/mx_testservice.sas deleted file mode 100644 index 7234791..0000000 --- a/007_macros/mx_testservice.sas +++ /dev/null @@ -1,299 +0,0 @@ -/*** HELP START ***//** - @file - @brief Will execute a SASjs web service on SAS 9, Viya or SASjs Server - @details Prepares the input files and retrieves the resulting datasets from - the response JSON. - - Note - the _webout fileref should NOT be assigned prior to running this macro. - - @param [in] program The _PROGRAM endpoint to test - @param [in] inputfiles=(0) A list of space seperated fileref:filename pairs as - follows: - inputfiles=inref:filename inref2:filename2 - @param [in] inputdatasets= (0) All datasets in this space seperated list are - converted into SASJS-formatted CSVs (see mp_ds2csv.sas) files and added to - the list of `inputfiles` for ingestion. The dataset will be sent with the - same name (no need for a colon modifier). - @param [in] inputparams=(0) A dataset containing name/value pairs in the - following format: - |name:$32|value:$1000| - |---|---| - |stpmacname|some value| - |mustbevalidname|can be anything, oops, %abort!!| - - @param [in] debug= (log) Provide the _debug value - @param [in] mdebug= (0) Set to 1 to provide macro debugging - @param [in] viyaresult= (WEBOUT_JSON) The Viya result type to return. For - more info, see mv_getjobresult.sas - @param [in] viyacontext= (SAS Job Execution compute context) The Viya compute - context on which to run the service - @param [out] outlib= (0) Output libref to contain the final tables. Set to - 0 if the service output is not in JSON format. - @param [out] outref= (0) Output fileref to create, to contain the full _webout - response. - -

SAS Macros

- @li mf_getplatform.sas - @li mf_getuniquefileref.sas - @li mf_getuniquename.sas - @li mp_abort.sas - @li mp_binarycopy.sas - @li mp_chop.sas - @li mp_ds2csv.sas - @li ms_testservice.sas - @li mv_getjobresult.sas - @li mv_jobflow.sas - -

Related Programs

- @li mx_testservice.test.sas - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mx_testservice(program, - inputfiles=0, - inputdatasets=0, - inputparams=0, - debug=log, - mdebug=0, - outlib=0, - outref=0, - viyaresult=WEBOUT_JSON, - viyacontext=SAS Job Execution compute context -); -%local dbg pcnt fref1 fref2 webref webrefpath i webcount var platform; -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; -%end; -%else %let dbg=*; - -/* sanitise inputparams */ -%let pcnt=0; -%if &inputparams ne 0 %then %do; - data _null_; - set &inputparams; - if not nvalid(name,'v7') then putlog (_all_)(=); - else if name in ( - 'program','inputfiles','inputparams','debug','outlib','outref' - ) then putlog (_all_)(=); - else do; - x+1; - call symputx(name,quote(cats(value)),'l'); - call symputx(cats('pval',x),name,'l'); - call symputx('pcnt',x,'l'); - end; - run; - %mp_abort(iftrue= (%mf_nobs(&inputparams) ne &pcnt) - ,mac=&sysmacroname - ,msg=%str(Invalid values in &inputparams) - ) -%end; - -/* convert inputdatasets to filerefs */ -%if "&inputdatasets" ne "0" %then %do; - %if %quote(&inputfiles)=0 %then %let inputfiles=; - %do i=1 %to %sysfunc(countw(&inputdatasets,%str( ))); - %let var=%scan(&inputdatasets,&i,%str( )); - %local dsref&i; - %let dsref&i=%mf_getuniquefileref(); - %mp_ds2csv(&var,outref=&&dsref&i,headerformat=SASJS) - %let inputfiles=&inputfiles &&dsref&i:%scan(&var,-1,.); - %end; -%end; - -%let platform=%mf_getplatform(); -%let fref1=%mf_getuniquefileref(); -%let fref2=%mf_getuniquefileref(); -%let webref=%mf_getuniquefileref(); -%let webrefpath=%sysfunc(pathname(work))/%mf_getuniquename(); -/* mp_chop requires a physical path as input */ -filename &webref "&webrefpath"; - -%if &platform=SASMETA %then %do; - - /* parse the input files */ - %if %quote(&inputfiles) ne 0 %then %do; - %let webcount=%sysfunc(countw(&inputfiles)); - %put &=webcount; - %do i=1 %to &webcount; - %let var=%scan(&inputfiles,&i,%str( )); - %local webfref&i webname&i; - %let webref&i=%scan(&var,1,%str(:)); - %let webname&i=%scan(&var,2,%str(:)); - %put webref&i=&&webref&i; - %put webname&i=&&webname&i; - %end; - %end; - %else %let webcount=0; - - proc stp program="&program"; - inputparam _program="&program" - %do i=1 %to &webcount; - %if &webcount=1 %then %do; - _webin_fileref="&&webref&i" - _webin_name="&&webname&i" - %end; - %else %do; - _webin_fileref&i="&&webref&i" - _webin_name&i="&&webname&i" - %end; - %end; - _webin_file_count="&webcount" - _debug="&debug" - %do i=1 %to &pcnt; - /* resolve name only, proc stp fetches value */ - &&pval&i=&&&&&&pval&i - %end; - ; - %do i=1 %to &webcount; - inputfile &&webref&i; - %end; - outputfile _webout=&webref; - run; - - data _null_; - infile &webref; - file &fref1; - input; - length line $10000; - if index(_infile_,'>>weboutBEGIN<<') then do; - line=tranwrd(_infile_,'>>weboutBEGIN<<',''); - put line; - end; - else if index(_infile_,'>>weboutEND<<') then do; - line=tranwrd(_infile_,'>>weboutEND<<',''); - put line; - stop; - end; - else put _infile_; - run; - data _null_; - infile &fref1; - input; - put _infile_; - run; - %if &outlib ne 0 %then %do; - libname &outlib json (&fref1); - %end; - %if &outref ne 0 %then %do; - filename &outref temp; - %mp_binarycopy(inref=&webref,outref=&outref) - %end; - -%end; -%else %if &platform=SASVIYA %then %do; - - /* prepare inputparams */ - %local ds1; - %let ds1=%mf_getuniquename(); - %if "&inputparams" ne "0" %then %do; - proc transpose data=&inputparams out=&ds1; - id name; - var value; - run; - %end; - %else %do; - data &ds1;run; - %end; - - /* parse the input files - convert to sasjs params */ - %local webcount i var sasjs_tables; - %if %quote(&inputfiles) ne 0 %then %do; - %let webcount=%sysfunc(countw(&inputfiles)); - %put &=webcount; - %do i=1 %to &webcount; - %let var=%scan(&inputfiles,&i,%str( )); - %local webfref&i webname&i sasjs&i.data; - %let webref&i=%scan(&var,1,%str(:)); - %let webname&i=%scan(&var,2,%str(:)); - %put webref&i=&&webref&i; - %put webname&i=&&webname&i; - - %let sasjs_tables=&sasjs_tables &&webname&i; - data _null_; - infile &&webref&i lrecl=32767; - input; - if _n_=1 then call symputx("sasjs&i.data",_infile_); - else call symputx( - "sasjs&i.data",cats(symget("sasjs&i.data"),'0D0A'x,_infile_) - ); - putlog "&sysmacroname infile: " _infile_; - run; - data &ds1; - set &ds1; - length sasjs&i.data $32767 sasjs_tables $1000; - sasjs&i.data=symget("sasjs&i.data"); - sasjs_tables=symget("sasjs_tables"); - run; - %end; - %end; - %else %let webcount=0; - - data &ds1; - retain _program "&program"; - retain _contextname "&viyacontext"; - set &ds1; - putlog "&sysmacroname inputparams:"; - putlog (_all_)(=); - run; - - %mv_jobflow(inds=&ds1 - ,maxconcurrency=1 - ,outds=work.results - ,outref=&fref1 - ,mdebug=&mdebug - ) - /* show the log */ - data _null_; - infile &fref1; - input; - putlog _infile_; - run; - /* get the uri to fetch results */ - data _null_; - set work.results; - call symputx('uri',uri); - putlog "&sysmacroname: fetching results for " uri; - run; - /* fetch results from webout.json */ - %mv_getjobresult(uri=&uri, - result=&viyaresult, - outref=&outref, - outlib=&outlib, - mdebug=&mdebug - ) - -%end; -%else %if &platform=SASJS %then %do; - - %ms_testservice(&program - ,inputfiles=&inputfiles - ,inputdatasets=&inputdatasets - ,inputparams=&inputparams - ,debug=&debug - ,mdebug=&mdebug - ,outlib=&outlib - ,outref=&outref - ) - -%end; -%else %do; - %put %str(ERR)OR: Unrecognised platform: &platform; -%end; - -%if &mdebug=0 %then %do; - filename &fref1 clear; - %if &platform ne SASJS %then %do; - filename &fref2 clear; - filename &webref clear; - %end; -%end; -%else %do; - %put &sysmacroname exit vars:; - %put _local_; -%end; - -%mend mx_testservice; diff --git a/001_macros/mfv_existfile.sas b/008_macros/mfv_existfile.sas similarity index 100% rename from 001_macros/mfv_existfile.sas rename to 008_macros/mfv_existfile.sas diff --git a/001_macros/mfv_existfolder.sas b/008_macros/mfv_existfolder.sas similarity index 100% rename from 001_macros/mfv_existfolder.sas rename to 008_macros/mfv_existfolder.sas diff --git a/001_macros/mfv_existsashdat.sas b/008_macros/mfv_existsashdat.sas similarity index 100% rename from 001_macros/mfv_existsashdat.sas rename to 008_macros/mfv_existsashdat.sas diff --git a/008_macros/ml_gsubfile.sas b/008_macros/ml_gsubfile.sas deleted file mode 100644 index b4bc904..0000000 --- a/008_macros/ml_gsubfile.sas +++ /dev/null @@ -1,52 +0,0 @@ -/*** HELP START ***//** - @file ml_gsubfile.sas - @brief Compiles the gsubfile.lua lua file - @details Writes gsubfile.lua to the work directory - and then includes it. - Usage: - - %ml_gsubfile() - -**//*** HELP END ***/ - -%macro ml_gsubfile(); -data _null_; - file "%sysfunc(pathname(work))/ml_gsubfile.lua"; - put 'local fpath, outpath, file, fcontent '; - put ' '; - put '-- configure in / out paths '; - put 'fpath = sas.symget("file") '; - put 'outpath = sas.symget("outfile") '; - put 'if ( outpath == 0 ) '; - put 'then '; - put ' outpath=fpath '; - put 'end '; - put ' '; - put '-- open file and perform the substitution '; - put 'file = io.open(fpath,"r") '; - put 'fcontent = file:read("*all") '; - put 'file:close() '; - put 'fcontent = string.gsub( '; - put ' fcontent, '; - put ' sas.symget(sas.symget("patternvar")), '; - put ' sas.symget(sas.symget("replacevar")) '; - put ') '; - put ' '; - put '-- write the file back out '; - put 'file = io.open(outpath, "w+") '; - put 'io.output(file) '; - put 'io.write(fcontent) '; - put 'io.close(file) '; -run; - -/* ensure big enough lrecl to avoid lua compilation issues */ -%local optval; -%let optval=%sysfunc(getoption(lrecl)); -options lrecl=1024; - -/* execute the lua code by using a .lua extension */ -%inc "%sysfunc(pathname(work))/ml_gsubfile.lua" /source2; - -options lrecl=&optval; - -%mend ml_gsubfile; diff --git a/008_macros/ml_json.sas b/008_macros/ml_json.sas deleted file mode 100644 index 005d1dd..0000000 --- a/008_macros/ml_json.sas +++ /dev/null @@ -1,402 +0,0 @@ -/*** HELP START ***//** - @file ml_json.sas - @brief Compiles the json.lua lua file - @details Writes json.lua to the work directory - and then includes it. - Usage: - - %ml_json() - -**//*** HELP END ***/ - -%macro ml_json(); -data _null_; - file "%sysfunc(pathname(work))/ml_json.lua"; - put '-- NOTE - THE COPYRIGHT BELOW IS IN RELATION TO THE JSON.LUA FILE ONLY '; - put '-- THIS FILE STARTS ON THE NEXT LINE AND WILL FINISH WITH "JSON.LUA ENDS HERE" '; - put '-- '; - put '-- json.lua '; - put '-- '; - put '-- Copyright (c) 2019 rxi '; - put '-- '; - put '-- Permission is hereby granted, free of charge, to any person obtaining a copy of '; - put '-- this software and associated documentation files (the "Software"), to deal in '; - put '-- the Software without restriction, including without limitation the rights to '; - put '-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies '; - put '-- of the Software, and to permit persons to whom the Software is furnished to do '; - put '-- so, subject to the following conditions: '; - put '-- '; - put '-- The above copyright notice and this permission notice shall be included in all '; - put '-- copies or substantial portions of the Software. '; - put '-- '; - put '-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR '; - put '-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, '; - put '-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE '; - put '-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER '; - put '-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, '; - put '-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE '; - put '-- SOFTWARE. '; - put '-- '; - put ' '; - put 'json = { _version = "0.1.2" } '; - put ' '; - put '------------------------------------------------------------------------------- '; - put '-- Encode '; - put '------------------------------------------------------------------------------- '; - put ' '; - put 'local encode '; - put ' '; - put 'local escape_char_map = { '; - put ' [ "\\" ] = "\\\\", '; - put ' [ "\"" ] = "\\\"", '; - put ' [ "\b" ] = "\\b", '; - put ' [ "\f" ] = "\\f", '; - put ' [ "\n" ] = "\\n", '; - put ' [ "\r" ] = "\\r", '; - put ' [ "\t" ] = "\\t", '; - put '} '; - put ' '; - put 'local escape_char_map_inv = { [ "\\/" ] = "/" } '; - put 'for k, v in pairs(escape_char_map) do '; - put ' escape_char_map_inv[v] = k '; - put 'end '; - put ' '; - put 'local function escape_char(c) '; - put ' return escape_char_map[c] or string.format("\\u%04x", c:byte()) '; - put 'end '; - put ' '; - put 'local function encode_nil(val) '; - put ' return "null" '; - put 'end '; - put ' '; - put 'local function encode_table(val, stack) '; - put ' local res = {} '; - put ' stack = stack or {} '; - put ' '; - put ' -- Circular reference? '; - put ' if stack[val] then error("circular reference") end '; - put ' '; - put ' stack[val] = true '; - put ' '; - put ' if rawget(val, 1) ~= nil or next(val) == nil then '; - put ' -- Treat as array -- check keys are valid and it is not sparse '; - put ' local n = 0 '; - put ' for k in pairs(val) do '; - put ' if type(k) ~= "number" then '; - put ' error("invalid table: mixed or invalid key types") '; - put ' end '; - put ' n = n + 1 '; - put ' end '; - put ' if n ~= #val then '; - put ' error("invalid table: sparse array") '; - put ' end '; - put ' -- Encode '; - put ' for i, v in ipairs(val) do '; - put ' table.insert(res, encode(v, stack)) '; - put ' end '; - put ' stack[val] = nil '; - put ' return "[" .. table.concat(res, ",") .. "]" '; - put ' else '; - put ' -- Treat as an object '; - put ' for k, v in pairs(val) do '; - put ' if type(k) ~= "string" then '; - put ' error("invalid table: mixed or invalid key types") '; - put ' end '; - put ' table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) '; - put ' end '; - put ' stack[val] = nil '; - put ' return "{" .. table.concat(res, ",") .. "}" '; - put ' end '; - put 'end '; - put ' '; - put 'local function encode_string(val) '; - put ' return ''"'' .. val:gsub(''[%z\1-\31\\"]'', escape_char) .. ''"'' '; - put 'end '; - put ' '; - put 'local function encode_number(val) '; - put ' -- Check for NaN, -inf and inf '; - put ' if val ~= val or val <= -math.huge or val >= math.huge then '; - put ' error("unexpected number value ''" .. tostring(val) .. "''") '; - put ' end '; - put ' return string.format("%.14g", val) '; - put 'end '; - put ' '; - put 'local type_func_map = { '; - put ' [ "nil" ] = encode_nil, '; - put ' [ "table" ] = encode_table, '; - put ' [ "string" ] = encode_string, '; - put ' [ "number" ] = encode_number, '; - put ' [ "boolean" ] = tostring, '; - put '} '; - put ' '; - put 'encode = function(val, stack) '; - put ' local t = type(val) '; - put ' local f = type_func_map[t] '; - put ' if f then '; - put ' return f(val, stack) '; - put ' end '; - put ' error("unexpected type ''" .. t .. "''") '; - put 'end '; - put ' '; - put 'function json.encode(val) '; - put ' return ( encode(val) ) '; - put 'end '; - put ' '; - put '------------------------------------------------------------------------------- '; - put '-- Decode '; - put '------------------------------------------------------------------------------- '; - put 'local parse '; - put 'local function create_set(...) '; - put ' local res = {} '; - put ' for i = 1, select("#", ...) do '; - put ' res[ select(i, ...) ] = true '; - put ' end '; - put ' return res '; - put 'end '; - put ' '; - put 'local space_chars = create_set(" ", "\t", "\r", "\n") '; - put 'local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") '; - put 'local escape_chars = create_set("\\", "/", ''"'', "b", "f", "n", "r", "t", "u") '; - put 'local literals = create_set("true", "false", "null") '; - put ' '; - put 'local literal_map = { '; - put ' [ "true" ] = true, '; - put ' [ "false" ] = false, '; - put ' [ "null" ] = nil, '; - put '} '; - put ' '; - put 'local function next_char(str, idx, set, negate) '; - put ' for i = idx, #str do '; - put ' if set[str:sub(i, i)] ~= negate then '; - put ' return i '; - put ' end '; - put ' end '; - put ' return #str + 1 '; - put 'end '; - put ' '; - put 'local function decode_error(str, idx, msg) '; - put ' local line_count = 1 '; - put ' local col_count = 1 '; - put ' for i = 1, idx - 1 do '; - put ' col_count = col_count + 1 '; - put ' if str:sub(i, i) == "\n" then '; - put ' line_count = line_count + 1 '; - put ' col_count = 1 '; - put ' end '; - put ' end '; - put ' error( string.format("%s at line %d col %d", msg, line_count, col_count) ) '; - put 'end '; - put ' '; - put 'local function codepoint_to_utf8(n) '; - put ' -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa '; - put ' local f = math.floor '; - put ' if n <= 0x7f then '; - put ' return string.char(n) '; - put ' elseif n <= 0x7ff then '; - put ' return string.char(f(n / 64) + 192, n % 64 + 128) '; - put ' elseif n <= 0xffff then '; - put ' return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) '; - put ' elseif n <= 0x10ffff then '; - put ' return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, '; - put ' f(n % 4096 / 64) + 128, n % 64 + 128) '; - put ' end '; - put ' error( string.format("invalid unicode codepoint ''%x''", n) ) '; - put 'end '; - put ' '; - put 'local function parse_unicode_escape(s) '; - put ' local n1 = tonumber( s:sub(3, 6), 16 ) '; - put ' local n2 = tonumber( s:sub(9, 12), 16 ) '; - put ' -- Surrogate pair? '; - put ' if n2 then '; - put ' return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) '; - put ' else '; - put ' return codepoint_to_utf8(n1) '; - put ' end '; - put 'end '; - put ' '; - put 'local function parse_string(str, i) '; - put ' local has_unicode_escape = false '; - put ' local has_surrogate_escape = false '; - put ' local has_escape = false '; - put ' local last '; - put ' for j = i + 1, #str do '; - put ' local x = str:byte(j) '; - put ' if x < 32 then '; - put ' decode_error(str, j, "control character in string") '; - put ' end '; - put ' if last == 92 then -- "\\" (escape char) '; - put ' if x == 117 then -- "u" (unicode escape sequence) '; - put ' local hex = str:sub(j + 1, j + 5) '; - put ' if not hex:find("%x%x%x%x") then '; - put ' decode_error(str, j, "invalid unicode escape in string") '; - put ' end '; - put ' if hex:find("^[dD][89aAbB]") then '; - put ' has_surrogate_escape = true '; - put ' else '; - put ' has_unicode_escape = true '; - put ' end '; - put ' else '; - put ' local c = string.char(x) '; - put ' if not escape_chars[c] then '; - put ' decode_error(str, j, "invalid escape char ''" .. c .. "'' in string") '; - put ' end '; - put ' has_escape = true '; - put ' end '; - put ' last = nil '; - put ' elseif x == 34 then -- ''"'' (end of string) '; - put ' local s = str:sub(i + 1, j - 1) '; - put ' if has_surrogate_escape then '; - put ' s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) '; - put ' end '; - put ' if has_unicode_escape then '; - put ' s = s:gsub("\\u....", parse_unicode_escape) '; - put ' end '; - put ' if has_escape then '; - put ' s = s:gsub("\\.", escape_char_map_inv) '; - put ' end '; - put ' return s, j + 1 '; - put ' else '; - put ' last = x '; - put ' end '; - put ' end '; - put ' decode_error(str, i, "expected closing quote for string") '; - put 'end '; - put ' '; - put 'local function parse_number(str, i) '; - put ' local x = next_char(str, i, delim_chars) '; - put ' local s = str:sub(i, x - 1) '; - put ' local n = tonumber(s) '; - put ' if not n then '; - put ' decode_error(str, i, "invalid number ''" .. s .. "''") '; - put ' end '; - put ' return n, x '; - put 'end '; - put ' '; - put 'local function parse_literal(str, i) '; - put ' local x = next_char(str, i, delim_chars) '; - put ' local word = str:sub(i, x - 1) '; - put ' if not literals[word] then '; - put ' decode_error(str, i, "invalid literal ''" .. word .. "''") '; - put ' end '; - put ' return literal_map[word], x '; - put 'end '; - put ' '; - put 'local function parse_array(str, i) '; - put ' local res = {} '; - put ' local n = 1 '; - put ' i = i + 1 '; - put ' while 1 do '; - put ' local x '; - put ' i = next_char(str, i, space_chars, true) '; - put ' -- Empty / end of array? '; - put ' if str:sub(i, i) == "]" then '; - put ' i = i + 1 '; - put ' break '; - put ' end '; - put ' -- Read token '; - put ' x, i = parse(str, i) '; - put ' res[n] = x '; - put ' n = n + 1 '; - put ' -- Next token '; - put ' i = next_char(str, i, space_chars, true) '; - put ' local chr = str:sub(i, i) '; - put ' i = i + 1 '; - put ' if chr == "]" then break end '; - put ' if chr ~= "," then decode_error(str, i, "expected '']'' or '',''") end '; - put ' end '; - put ' return res, i '; - put 'end '; - put ' '; - put 'local function parse_object(str, i) '; - put ' local res = {} '; - put ' i = i + 1 '; - put ' while 1 do '; - put ' local key, val '; - put ' i = next_char(str, i, space_chars, true) '; - put ' -- Empty / end of object? '; - put ' if str:sub(i, i) == "}" then '; - put ' i = i + 1 '; - put ' break '; - put ' end '; - put ' -- Read key '; - put ' if str:sub(i, i) ~= ''"'' then '; - put ' decode_error(str, i, "expected string for key") '; - put ' end '; - put ' key, i = parse(str, i) '; - put ' -- Read '':'' delimiter '; - put ' i = next_char(str, i, space_chars, true) '; - put ' if str:sub(i, i) ~= ":" then '; - put ' decode_error(str, i, "expected '':'' after key") '; - put ' end '; - put ' i = next_char(str, i + 1, space_chars, true) '; - put ' -- Read value '; - put ' val, i = parse(str, i) '; - put ' -- Set '; - put ' res[key] = val '; - put ' -- Next token '; - put ' i = next_char(str, i, space_chars, true) '; - put ' local chr = str:sub(i, i) '; - put ' i = i + 1 '; - put ' if chr == "}" then break end '; - put ' if chr ~= "," then decode_error(str, i, "expected ''}'' or '',''") end '; - put ' end '; - put ' return res, i '; - put 'end '; - put ' '; - put 'local char_func_map = { '; - put ' [ ''"'' ] = parse_string, '; - put ' [ "0" ] = parse_number, '; - put ' [ "1" ] = parse_number, '; - put ' [ "2" ] = parse_number, '; - put ' [ "3" ] = parse_number, '; - put ' [ "4" ] = parse_number, '; - put ' [ "5" ] = parse_number, '; - put ' [ "6" ] = parse_number, '; - put ' [ "7" ] = parse_number, '; - put ' [ "8" ] = parse_number, '; - put ' [ "9" ] = parse_number, '; - put ' [ "-" ] = parse_number, '; - put ' [ "t" ] = parse_literal, '; - put ' [ "f" ] = parse_literal, '; - put ' [ "n" ] = parse_literal, '; - put ' [ "[" ] = parse_array, '; - put ' [ "{" ] = parse_object, '; - put '} '; - put ' '; - put 'parse = function(str, idx) '; - put ' local chr = str:sub(idx, idx) '; - put ' local f = char_func_map[chr] '; - put ' if f then '; - put ' return f(str, idx) '; - put ' end '; - put ' decode_error(str, idx, "unexpected character ''" .. chr .. "''") '; - put 'end '; - put ' '; - put 'function json.decode(str) '; - put ' if type(str) ~= "string" then '; - put ' error("expected argument of type string, got " .. type(str)) '; - put ' end '; - put ' local res, idx = parse(str, next_char(str, 1, space_chars, true)) '; - put ' idx = next_char(str, idx, space_chars, true) '; - put ' if idx <= #str then '; - put ' decode_error(str, idx, "trailing garbage") '; - put ' end '; - put ' return res '; - put 'end '; - put ' '; - put 'return json '; - put ' '; - put '-- JSON.LUA ENDS HERE '; -run; - -/* ensure big enough lrecl to avoid lua compilation issues */ -%local optval; -%let optval=%sysfunc(getoption(lrecl)); -options lrecl=1024; - -/* execute the lua code by using a .lua extension */ -%inc "%sysfunc(pathname(work))/ml_json.lua" /source2; - -options lrecl=&optval; - -%mend ml_json; diff --git a/008_macros/mmx_createmetafolder.sas b/008_macros/mmx_createmetafolder.sas deleted file mode 100644 index 38beb35..0000000 --- a/008_macros/mmx_createmetafolder.sas +++ /dev/null @@ -1,50 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a metadata folder - @details Creates a metadata folder using the batch tools - - Usage: - - %mmx_createmetafolder(loc=/some/meta/folder,user=sasdemo,pass=mars345) - -

SAS Macros

- @li mf_loc.sas - @li mp_abort.sas - - @param [in] loc= the metadata folder to delete - @param [in] user= username - @param [in] pass= password - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mmx_createmetafolder(loc=,user=,pass=); - -%local host port path connx_string msg; -%let host=%sysfunc(getoption(metaserver)); -%let port=%sysfunc(getoption(metaport)); -%let path=%mf_loc(POF)/tools; - -%let connx_string= -host &host -port &port -user '&user' -password '&pass'; -/* remove directory */ -data _null_; - infile " &path/sas-make-folder &connx_string ""&loc"" -makeFullPath 2>&1" - pipe lrecl=10000; - input; - putlog _infile_; -run; - -data _null_; /* check tree exists */ - length type uri $256; - rc=metadata_pathobj("","&loc","Folder",type,uri); - call symputx('foldertype',type,'l'); -run; -%let msg=Location (&loc) was not created!!; -%mp_abort(iftrue= (&foldertype ne Tree) - ,mac=&_program..sas - ,msg=%superq(msg) -) - -%mend mmx_createmetafolder; diff --git a/008_macros/mmx_deletemetafolder.sas b/008_macros/mmx_deletemetafolder.sas deleted file mode 100644 index 762bbe3..0000000 --- a/008_macros/mmx_deletemetafolder.sas +++ /dev/null @@ -1,40 +0,0 @@ -/*** HELP START ***//** - @file - @brief Deletes a metadata folder - @details Deletes a metadata folder (and contents) using the batch tools, as - documented here: - https://documentation.sas.com/?docsetId=bisag&docsetTarget=p0zqp8fmgs4o0kn1tt7j8ho829fv.htm&docsetVersion=9.4&locale=en - - Usage: - - %mmx_deletemetafolder(loc=/some/meta/folder,user=sasdemo,pass=mars345) - -

SAS Macros

- @li mf_loc.sas - - @param [in] loc= the metadata folder to delete - @param [in] user= username - @param [in] pass= password - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mmx_deletemetafolder(loc=,user=,pass=); - -%local host port path connx_string; -%let host=%sysfunc(getoption(metaserver)); -%let port=%sysfunc(getoption(metaport)); -%let path=%mf_loc(POF)/tools; - -%let connx_string= -host &host -port &port -user '&user' -password '&pass'; -/* remove directory */ -data _null_; - infile " &path/sas-delete-objects &connx_string ""&loc"" -deleteContents 2>&1" - pipe lrecl=10000; - input; - putlog _infile_; -run; - -%mend mmx_deletemetafolder; diff --git a/008_macros/mmx_spkexport.sas b/008_macros/mmx_spkexport.sas deleted file mode 100644 index ddde21a..0000000 --- a/008_macros/mmx_spkexport.sas +++ /dev/null @@ -1,94 +0,0 @@ -/*** HELP START ***//** - @file mmx_spkexport.sas - @brief Exports everything in a particular metadata folder - @details Will export everything in a metadata folder to a specified location. - Note - the batch tools require a username and password. For security, - these are expected to have been provided in a protected directory. - -Usage: - - %* import the macros (or make them available some other way); - filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - %* create sample text file as input to the macro; - filename tmp temp; - data _null_; - file tmp; - put '%let mmxuser=sasdemo;'; - put '%let mmxpass=Mars321'; - run; - - filename outref "%sysfunc(pathname(work))"; - %mmx_spkexport( - metaloc=%str(/30.Projects/3001.Internal/300115.DataController/dc1) - ,secureref=tmp - ,outspkpath=%str(/tmp) - ) - -

SAS Macros

- @li mf_loc.sas - @li mm_tree.sas - @li mf_getuniquefileref.sas - @li mp_abort.sas - - @param [in] metaloc= the metadata folder to export - @param [in] secureref= () fileref containing the username / password - (should point to a file in a secure location) - @param [in] outspkname= name of the spk to be created (default is mmxport). - @param [in] outspkpath= ((%sysfunc(pathname(WORK))) - directory in which to create the SPK. - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mmx_spkexport(metaloc= - ,secureref= - ,outspkname=mmxport - ,outspkpath=%sysfunc(pathname(work)) -); - -%local host port platform_object_path connx_string; -%let host=%sysfunc(getoption(metaserver)); -%let port=%sysfunc(getoption(metaport)); -%let platform_object_path=%mf_loc(POF); - -/* get creds */ -%inc &secureref/nosource; - -%let connx_string= - %str(-host &host -port &port -user '&mmxuser' -password '&mmxpass'); - -%mm_tree(root=%str(&metaloc) ,types=EXPORTABLE ,outds=exportable) - -%local fref1; -%let fref1=%mf_getuniquefileref(); -data ; - set exportable end=last; - file &fref1 lrecl=32767; - length str $32767; - if _n_=1 then do; - put 'data _null_;'; - put 'infile "cd ""&platform_object_path"" %trim('; - put ') cd ""&platform_object_path"" %trim('; - put '); ./ExportPackage &connx_string -disableX11 %trim('; - put ') -package ""&outspkpath/&outspkname..spk"" %trim('; - end; - str=') -objects '!!cats('""',path,'/',name,"(",publictype,')"" %trim('); - put str; - if last then do; - put ') -log ""&outspkpath/&outspkname..log"" 2>&1" pipe lrecl=10000;'; - put 'input;putlog _infile_;run;'; - end; -run; - -%mp_abort(iftrue= (&syscc ne 0) - ,mac=&sysmacroname - ,msg=%str(syscc=&syscc) -) - -%inc &fref1; - -%mend mmx_spkexport; diff --git a/001_macros/mv_createfile.sas b/008_macros/mv_createfile.sas similarity index 100% rename from 001_macros/mv_createfile.sas rename to 008_macros/mv_createfile.sas diff --git a/001_macros/mv_createfolder.sas b/008_macros/mv_createfolder.sas similarity index 100% rename from 001_macros/mv_createfolder.sas rename to 008_macros/mv_createfolder.sas diff --git a/001_macros/mv_createjob.sas b/008_macros/mv_createjob.sas similarity index 100% rename from 001_macros/mv_createjob.sas rename to 008_macros/mv_createjob.sas diff --git a/001_macros/mv_createwebservice.sas b/008_macros/mv_createwebservice.sas similarity index 100% rename from 001_macros/mv_createwebservice.sas rename to 008_macros/mv_createwebservice.sas diff --git a/001_macros/mv_deletefoldermember.sas b/008_macros/mv_deletefoldermember.sas similarity index 100% rename from 001_macros/mv_deletefoldermember.sas rename to 008_macros/mv_deletefoldermember.sas diff --git a/001_macros/mv_deletejes.sas b/008_macros/mv_deletejes.sas similarity index 100% rename from 001_macros/mv_deletejes.sas rename to 008_macros/mv_deletejes.sas diff --git a/001_macros/mv_deleteviyafolder.sas b/008_macros/mv_deleteviyafolder.sas similarity index 100% rename from 001_macros/mv_deleteviyafolder.sas rename to 008_macros/mv_deleteviyafolder.sas diff --git a/001_macros/mv_getclients.sas b/008_macros/mv_getclients.sas similarity index 100% rename from 001_macros/mv_getclients.sas rename to 008_macros/mv_getclients.sas diff --git a/001_macros/mv_getfoldermembers.sas b/008_macros/mv_getfoldermembers.sas similarity index 100% rename from 001_macros/mv_getfoldermembers.sas rename to 008_macros/mv_getfoldermembers.sas diff --git a/001_macros/mv_getgroupmembers.sas b/008_macros/mv_getgroupmembers.sas similarity index 100% rename from 001_macros/mv_getgroupmembers.sas rename to 008_macros/mv_getgroupmembers.sas diff --git a/001_macros/mv_getgroups.sas b/008_macros/mv_getgroups.sas similarity index 100% rename from 001_macros/mv_getgroups.sas rename to 008_macros/mv_getgroups.sas diff --git a/001_macros/mv_getjobcode.sas b/008_macros/mv_getjobcode.sas similarity index 100% rename from 001_macros/mv_getjobcode.sas rename to 008_macros/mv_getjobcode.sas diff --git a/001_macros/mv_getjoblog.sas b/008_macros/mv_getjoblog.sas similarity index 100% rename from 001_macros/mv_getjoblog.sas rename to 008_macros/mv_getjoblog.sas diff --git a/001_macros/mv_getjobresult.sas b/008_macros/mv_getjobresult.sas similarity index 100% rename from 001_macros/mv_getjobresult.sas rename to 008_macros/mv_getjobresult.sas diff --git a/001_macros/mv_getjobstate.sas b/008_macros/mv_getjobstate.sas similarity index 100% rename from 001_macros/mv_getjobstate.sas rename to 008_macros/mv_getjobstate.sas diff --git a/001_macros/mv_getusergroups.sas b/008_macros/mv_getusergroups.sas similarity index 100% rename from 001_macros/mv_getusergroups.sas rename to 008_macros/mv_getusergroups.sas diff --git a/001_macros/mv_getusers.sas b/008_macros/mv_getusers.sas similarity index 100% rename from 001_macros/mv_getusers.sas rename to 008_macros/mv_getusers.sas diff --git a/001_macros/mv_jobexecute.sas b/008_macros/mv_jobexecute.sas similarity index 100% rename from 001_macros/mv_jobexecute.sas rename to 008_macros/mv_jobexecute.sas diff --git a/001_macros/mv_jobflow.sas b/008_macros/mv_jobflow.sas similarity index 100% rename from 001_macros/mv_jobflow.sas rename to 008_macros/mv_jobflow.sas diff --git a/001_macros/mv_jobwaitfor.sas b/008_macros/mv_jobwaitfor.sas similarity index 100% rename from 001_macros/mv_jobwaitfor.sas rename to 008_macros/mv_jobwaitfor.sas diff --git a/001_macros/mv_registerclient.sas b/008_macros/mv_registerclient.sas similarity index 100% rename from 001_macros/mv_registerclient.sas rename to 008_macros/mv_registerclient.sas diff --git a/001_macros/mv_tokenauth.sas b/008_macros/mv_tokenauth.sas similarity index 100% rename from 001_macros/mv_tokenauth.sas rename to 008_macros/mv_tokenauth.sas diff --git a/001_macros/mv_tokenrefresh.sas b/008_macros/mv_tokenrefresh.sas similarity index 100% rename from 001_macros/mv_tokenrefresh.sas rename to 008_macros/mv_tokenrefresh.sas diff --git a/001_macros/mv_webout.sas b/008_macros/mv_webout.sas similarity index 100% rename from 001_macros/mv_webout.sas rename to 008_macros/mv_webout.sas diff --git a/009_macros/mf_abort.sas b/009_macros/mf_abort.sas deleted file mode 100644 index 6b64b4c..0000000 --- a/009_macros/mf_abort.sas +++ /dev/null @@ -1,32 +0,0 @@ -/*** HELP START ***//** - @file - @brief Abort, ungracefully - @details Will abort with a straightforward %abort if the condition is true. - - @param [in] mac= (mf_abort.sas) Name of calling macro (is printed to the log) - @param [in] msg= ( ) Additional string to print to the log - @param [in] iftrue= (%str(1=1)) Conditional logic under which to perform the - abort - -

Related Macros

- @li mp_abort.sas - - @version 9.2 - @author Allan Bowe - @cond -**//*** HELP END ***/ - -%macro mf_abort(mac=mf_abort.sas, msg=, iftrue=%str(1=1) -)/des='ungraceful abort' /*STORE SOURCE*/; - - %if not(%eval(%unquote(&iftrue))) %then %return; - - %put NOTE: /// mf_abort macro executing //; - %if %length(&mac)>0 %then %put NOTE- called by &mac; - %put NOTE - &msg; - - %abort; - -%mend mf_abort; - -/** @endcond */ diff --git a/009_macros/mf_dedup.sas b/009_macros/mf_dedup.sas deleted file mode 100644 index b335f49..0000000 --- a/009_macros/mf_dedup.sas +++ /dev/null @@ -1,53 +0,0 @@ -/*** HELP START ***//** - @file - @brief de-duplicates a macro string - @details Removes all duplicates from a string of words. A delimeter can be - chosen. Is inspired heavily by this excellent [macro]( - https://github.com/scottbass/SAS/blob/master/Macro/dedup_mstring.sas) from - [Scott Base](https://www.linkedin.com/in/scottbass). Case sensitive. - - Usage: - - %let str=One two one two and through and through; - %put %mf_dedup(&str); - %put %mf_dedup(&str,outdlm=%str(,)); - - Which returns: - - > One two one and through - > One,two,one,and,through - - @param [in] str String to be deduplicated - @param [in] indlm= ( ) Delimeter of the input string - @param [out] outdlm= ( ) Delimiter of the output string - -

Related Macros

- @li mf_trimstr.sas - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_dedup(str - ,indlm=%str( ) - ,outdlm=%str( ) -)/*/STORE SOURCE*/; - -%local num word i pos out; - -%* loop over each token, searching the target for that token ; -%let num=%sysfunc(countc(%superq(str),%str(&indlm))); -%do i=1 %to %eval(&num+1); - %let word=%scan(%superq(str),&i,%str(&indlm)); - %let pos=%sysfunc(indexw(&out,&word,%str(&outdlm))); - %if (&pos eq 0) %then %do; - %if (&i gt 1) %then %let out=&out%str(&outdlm); - %let out=&out&word; - %end; -%end; - -%unquote(&out) - -%mend mf_dedup; - - diff --git a/009_macros/mf_deletefile.sas b/009_macros/mf_deletefile.sas deleted file mode 100644 index e271f67..0000000 --- a/009_macros/mf_deletefile.sas +++ /dev/null @@ -1,31 +0,0 @@ -/*** HELP START ***//** - @file - @brief Deletes a physical file, if it exists - @details Usage: - - %mf_writefile(&sasjswork/myfile.txt,l1=some content) - - %mf_deletefile(&sasjswork/myfile.txt) - - %mf_deletefile(&sasjswork/myfile.txt) - - - @param [in] file Full path to the target file - - @returns The return code from the fdelete() invocation - -

Related Macros

- @li mf_deletefile.test.sas - @li mf_writefile.sas - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_deletefile(file -)/*/STORE SOURCE*/; - %local rc fref; - %let rc= %sysfunc(filename(fref,&file)); - %if %sysfunc(fdelete(&fref)) ne 0 %then %put %sysfunc(sysmsg()); - %let rc= %sysfunc(filename(fref)); -%mend mf_deletefile; diff --git a/009_macros/mf_existds.sas b/009_macros/mf_existds.sas deleted file mode 100644 index 1c06ff2..0000000 --- a/009_macros/mf_existds.sas +++ /dev/null @@ -1,30 +0,0 @@ -/*** HELP START ***//** - @file mf_existds.sas - @brief Checks whether a dataset OR a view exists. - @details Can be used in open code, eg as follows: - - %if %mf_existds(libds=work.someview) %then %put yes it does!; - - NOTE - some databases have case sensitive tables, for instance POSTGRES - with the preserve_tab_names=yes libname setting. This may impact - expected results (depending on whether you 'expect' the result to be - case insensitive in this context!) - - @param [in] libds library.dataset - @return output returns 1 or 0 - -

Related Macros

- @li mf_existds.test.sas - - @warning Untested on tables registered in metadata but not physically present - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_existds(libds -)/*/STORE SOURCE*/; - - %if %sysfunc(exist(&libds)) ne 1 & %sysfunc(exist(&libds,VIEW)) ne 1 %then 0; - %else 1; - -%mend mf_existds; diff --git a/009_macros/mf_existfeature.sas b/009_macros/mf_existfeature.sas deleted file mode 100644 index e447425..0000000 --- a/009_macros/mf_existfeature.sas +++ /dev/null @@ -1,59 +0,0 @@ -/*** HELP START ***//** - @file - @brief Checks whether a feature exists - @details Check to see if a feature is supported in your environment. - Run without arguments to see a list of detectable features. - Note - this list is based on known versions of SAS rather than - actual feature detection, as that is tricky / impossible to do - without generating errs in most cases. - - %put %mf_existfeature(PROCLUA); - - @param [in] feature The feature to detect. - - @return output returns 1 or 0 (or -1 if not found) - -

SAS Macros

- @li mf_getplatform.sas - - @version 8 - @author Allan Bowe -**//*** HELP END ***/ -/** @cond */ -%macro mf_existfeature(feature -)/*/STORE SOURCE*/; - %let feature=%upcase(&feature); - %local platform; - %let platform=%mf_getplatform(); - - %if &feature= %then %do; - %put No feature was requested for detection; - %end; - %else %if &feature=COLCONSTRAINTS %then %do; - %if "%substr(&sysver,1,1)"="4" or "%substr(&sysver,1,1)"="5" %then 0; - %else 1; - %end; - %else %if &feature=PROCLUA %then %do; - /* https://blogs.sas.com/content/sasdummy/2015/08/03/using-lua-within-your-sas-programs */ - %if &platform=SASVIYA %then 1; - %else %if "&sysver"="9.2" or "&sysver"="9.3" %then 0; - %else %if "&SYSVLONG" < "9.04.01M3" %then 0; - %else 1; - %end; - %else %if &feature=DBMS_MEMTYPE %then %do; - /* does dbms_memtype exist in dictionary.tables? */ - %if "%substr(&sysver,1,1)"="4" or "%substr(&sysver,1,1)"="5" %then 0; - %else 1; - %end; - %else %if &feature=EXPORTXLS %then %do; - /* is it possible to PROC EXPORT an excel file? */ - %if "%substr(&sysver,1,1)"="4" or "%substr(&sysver,1,1)"="5" %then 1; - %else %if %sysfunc(sysprod(SAS/ACCESS Interface to PC Files)) = 1 %then 1; - %else 0; - %end; - %else %do; - -1 - %put &sysmacroname: &feature not found; - %end; -%mend mf_existfeature; -/** @endcond */ diff --git a/009_macros/mf_existfileref.sas b/009_macros/mf_existfileref.sas deleted file mode 100644 index f78a774..0000000 --- a/009_macros/mf_existfileref.sas +++ /dev/null @@ -1,33 +0,0 @@ -/*** HELP START ***//** - @file - @brief Checks whether a fileref exists - @details You can probably do without this macro as it is just a one liner. - Mainly it is here as a convenient way to remember the syntax! - - @param [in] fref the fileref to detect - - @return output Returns 1 if found and 0 if not found. Note - it is possible - that the fileref is found, but the file does not (yet) exist. If you need - to test for this, you may as well use the fileref function directly. - - @version 8 - @author [Allan Bowe](https://www.linkedin.com/in/allanbowe/) -**//*** HELP END ***/ - -%macro mf_existfileref(fref -)/*/STORE SOURCE*/; - - %local rc; - %let rc=%sysfunc(fileref(&fref)); - %if &rc=0 %then %do; - 1 - %end; - %else %if &rc<0 %then %do; - %put &sysmacroname: Fileref &fref exists but the underlying file does not; - 1 - %end; - %else %do; - 0 - %end; - -%mend mf_existfileref; diff --git a/009_macros/mf_existfunction.sas b/009_macros/mf_existfunction.sas deleted file mode 100644 index cae475b..0000000 --- a/009_macros/mf_existfunction.sas +++ /dev/null @@ -1,37 +0,0 @@ -/*** HELP START ***//** - @file - @brief Checks if a function exists - @details Returns 1 if the function exists, else 0. Note that this function - can be slow as it needs to open the sashelp.vfuncs table. - - Usage: - - %put %mf_existfunction(CAT); - %put %mf_existfunction(DOG); - - Full credit to [Bart](https://sasensei.com/user/305) for the vfunc pointer - and the tidy approach for pure macro data set filtering. - Check out his [SAS Packages](https://github.com/yabwon/SAS_PACKAGES) - framework! Where you can find the same [function]( -https://github.com/yabwon/SAS_PACKAGES/blob/main/packages/baseplus.md#functionexists-macro - ). - - @param [in] name function name - - @author Allan Bowe -**//*** HELP END ***/ -/** @cond */ -%macro mf_existfunction(name -)/*/STORE SOURCE*/; - - %local dsid rc exist; - %let dsid=%sysfunc(open(sashelp.vfunc(where=(fncname="%upcase(&name)")))); - %let exist=1; - %let exist=%sysfunc(fetch(&dsid, NOSET)); - %let rc=%sysfunc(close(&dsid)); - - %sysevalf(0 = &exist) - -%mend mf_existfunction; - -/** @endcond */ diff --git a/009_macros/mf_existvar.sas b/009_macros/mf_existvar.sas deleted file mode 100644 index 8498421..0000000 --- a/009_macros/mf_existvar.sas +++ /dev/null @@ -1,43 +0,0 @@ -/*** HELP START ***//** - @file - @brief Checks if a variable exists in a data set. - @details Returns 0 if the variable does NOT exist, and the position of the var - if it does. - Usage: - - %put %mf_existvar(work.someds, somevar) - - @param [in] libds 2 part dataset or view reference - @param [in] var variable name - -

Related Macros

- @li mf_existvar.test.sas - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ -/** @cond */ - -%macro mf_existvar(libds /* 2 part dataset name */ - , var /* variable name */ -)/*/STORE SOURCE*/; - - %local dsid rc; - %let dsid=%sysfunc(open(&libds,is)); - - %if &dsid=0 %then %do; - %put %sysfunc(sysmsg()); - 0 - %end; - %else %if %length(&var)=0 %then %do; - 0 - %let rc=%sysfunc(close(&dsid)); - %end; - %else %do; - %sysfunc(varnum(&dsid,&var)) - %let rc=%sysfunc(close(&dsid)); - %end; - -%mend mf_existvar; - -/** @endcond */ diff --git a/009_macros/mf_existvarlist.sas b/009_macros/mf_existvarlist.sas deleted file mode 100644 index 3a2d2b3..0000000 --- a/009_macros/mf_existvarlist.sas +++ /dev/null @@ -1,56 +0,0 @@ -/*** HELP START ***//** - @file - @brief Checks if a set of variables ALL exist in a data set. - @details Returns 0 if ANY of the variables do not exist, or 1 if they ALL do. - Usage: - - %put %mf_existVarList(sashelp.class, age sex name dummyvar); - - @param [in] libds 2 part dataset or view reference - @param [in] varlist space separated variable names - - @version 9.2 - @author Allan Bowe - @cond -**//*** HELP END ***/ - -%macro mf_existvarlist(libds, varlist -)/*/STORE SOURCE*/; - - %if %str(&libds)=%str() or %str(&varlist)=%str() %then %do; - %mf_abort(msg=No value provided to libds(&libds) or varlist (&varlist)! - ,mac=mf_existvarlist.sas) - %end; - - %local dsid rc i var found; - %let dsid=%sysfunc(open(&libds,is)); - - %if &dsid=0 %then %do; - %put %str(WARN)ING: unable to open &libds in mf_existvarlist (&dsid); - %end; - - %if %sysfunc(attrn(&dsid,NVARS))=0 %then %do; - %put MF_EXISTVARLIST: No variables in &libds ; - 0 - %return; - %end; - - %else %do i=1 %to %sysfunc(countw(&varlist)); - %let var=%scan(&varlist,&i); - - %if %sysfunc(varnum(&dsid,&var))=0 %then %do; - %let found=&found &var; - %end; - %end; - - %let rc=%sysfunc(close(&dsid)); - %if %str(&found)=%str() %then %do; - 1 - %end; - %else %do; - 0 - %put Vars not found: &found; - %end; -%mend mf_existvarlist; - -/** @endcond */ diff --git a/009_macros/mf_fmtdttm.sas b/009_macros/mf_fmtdttm.sas deleted file mode 100644 index 7602c1d..0000000 --- a/009_macros/mf_fmtdttm.sas +++ /dev/null @@ -1,42 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns E8601DT26.6 if compatible else DATETIME19.3 - @details From our experience in [Data Controller for SAS] - (https://datacontroller.io) deployments, the E8601DT26.6 datetime format has - the widest support when it comes to pass-through SQL queries. - - However, it is not supported in WPS or early versions of SAS 9 (M3 and below) - when used as a datetime literal, eg: - - data _null_; - demo="%sysfunc(datetime(),E8601DT26.6)"dt; - demo=; - run; - - This macro will therefore return DATEITME19.3 as an alternative format - based on the runtime environment so that it can be used in such cases, eg: - - data _null_; - demo="%sysfunc(datetime(),%mf_fmtdttm())"dt; - demo=; - run; - -

Related Macros

- @li mf_fmtdttm.test.sas - - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_fmtdttm( -)/*/STORE SOURCE*/; - -%if "&sysver"="9.2" or "&sysver"="9.3" - or ("&sysver"="9.4" and "%substr(&SYSVLONG,9,1)" le "3") - or "%substr(&sysver,1,1)"="4" - or "%substr(&sysver,1,1)"="5" -%then %do;DATETIME19.3%end; -%else %do;E8601DT26.6%end; - -%mend mf_fmtdttm; - - diff --git a/009_macros/mf_getapploc.sas b/009_macros/mf_getapploc.sas deleted file mode 100644 index 76702b1..0000000 --- a/009_macros/mf_getapploc.sas +++ /dev/null @@ -1,79 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns the appLoc from the _program variable - @details When working with SASjs apps, web services / tests / jobs are always - deployed to a root (app) location in the SAS logical folder tree. - - When building apps for use in other environments, you do not necessarily know - where the backend services will be deployed. Therefore a function like this - is handy in order to dynamically figure out the appLoc, and enable other - services to be connected by a relative reference. - - SASjs apps always have the same immediate substructure (one or more of the - following): - - @li /data - @li /jobs - @li /services - @li /tests - @li /tests/jobs - @li /tests/services - @li /tests/macros - - This function works by testing for the existence of any of the above in the - automatic _program variable, and returning the part to the left of it. - - Usage: - - %put %mf_getapploc(&_program) - - %put %mf_getapploc(/some/location/services/admin/myservice); - %put %mf_getapploc(/some/location/jobs/extract/somejob/); - %put %mf_getapploc(/some/location/tests/jobs/somejob/); - - @param [in] pgm The _program value from which to extract the appLoc - - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_getapploc(pgm); -%if "&pgm"="" %then %do; - %if %symexist(_program) %then %let pgm=&_program; - %else %do; - %put &sysmacroname: No value provided and no _program variable available; - %return; - %end; -%end; -%local root; - -/** - * First check we are not in the tests/macros folder (which has no subfolders) - * or specifically in the testsetup or testteardown services - */ -%if %index(&pgm,/tests/macros/) -or %index(&pgm,/tests/testsetup) -or %index(&pgm,/tests/testteardown) -%then %do; - %let root=%substr(&pgm,1,%index(&pgm,/tests)-1); - &root - %return; -%end; - -/** - * Next, move up two levels to avoid matches on subfolder or service name - */ -%let root=%substr(&pgm,1,%length(&pgm)-%length(%scan(&pgm,-1,/))-1); -%let root=%substr(&root,1,%length(&root)-%length(%scan(&root,-1,/))-1); - -%if %index(&root,/tests/) %then %do; - %let root=%substr(&root,1,%index(&root,/tests/)-1); -%end; -%else %if %index(&root,/services) %then %do; - %let root=%substr(&root,1,%index(&root,/services)-1); -%end; -%else %if %index(&root,/jobs) %then %do; - %let root=%substr(&root,1,%index(&root,/jobs)-1); -%end; -%else %put &sysmacroname: Could not find an app location from &pgm; - &root -%mend mf_getapploc ; diff --git a/009_macros/mf_getattrc.sas b/009_macros/mf_getattrc.sas deleted file mode 100644 index af4743a..0000000 --- a/009_macros/mf_getattrc.sas +++ /dev/null @@ -1,34 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns a character attribute of a dataset. - @details Can be used in open code, eg as follows: - - %put Dataset label = %mf_getattrc(sashelp.class,LABEL); - %put Member Type = %mf_getattrc(sashelp.class,MTYPE); - - @param [in] libds library.dataset - @param [in] attr full list in [documentation]( - https://support.sas.com/documentation/cdl/en/lrdict/64316/HTML/default/viewer.htm#a000147794.htm) - @return output returns result of the attrc value supplied, or -1 and log - message if err. - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_getattrc( - libds - ,attr -)/*/STORE SOURCE*/; - %local dsid rc; - %let dsid=%sysfunc(open(&libds,is)); - %if &dsid = 0 %then %do; - %put %str(WARN)ING: Cannot open %trim(&libds), system message below; - %put %sysfunc(sysmsg()); - -1 - %end; - %else %do; - %sysfunc(attrc(&dsid,&attr)) - %let rc=%sysfunc(close(&dsid)); - %end; -%mend mf_getattrc; diff --git a/009_macros/mf_getattrn.sas b/009_macros/mf_getattrn.sas deleted file mode 100644 index 7f8af44..0000000 --- a/009_macros/mf_getattrn.sas +++ /dev/null @@ -1,34 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns a numeric attribute of a dataset. - @details Can be used in open code, eg as follows: - - %put Number of observations=%mf_getattrn(sashelp.class,NLOBS); - %put Number of variables = %mf_getattrn(sashelp.class,NVARS); - - @param [in] libds library.dataset - @param [in] attr Common values are NLOBS and NVARS, full list in [documentation]( - http://support.sas.com/documentation/cdl/en/lrdict/64316/HTML/default/viewer.htm#a000212040.htm) - @return output returns result of the attrn value supplied, or -1 and log - message if err. - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_getattrn( - libds - ,attr -)/*/STORE SOURCE*/; - %local dsid rc; - %let dsid=%sysfunc(open(&libds,is)); - %if &dsid = 0 %then %do; - %put %str(WARN)ING: Cannot open %trim(&libds), system message below; - %put %sysfunc(sysmsg()); - -1 - %end; - %else %do; - %sysfunc(attrn(&dsid,&attr)) - %let rc=%sysfunc(close(&dsid)); - %end; -%mend mf_getattrn; diff --git a/009_macros/mf_getengine.sas b/009_macros/mf_getengine.sas deleted file mode 100644 index 87741a0..0000000 --- a/009_macros/mf_getengine.sas +++ /dev/null @@ -1,54 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns the engine type of a SAS library - @details Usage: - - %put %mf_getengine(SASHELP); - - returns: - > V9 - - A note is also written to the log. The credit for this macro goes to the - contributors of Chris Hemedingers blog [post]( - http://blogs.sas.com/content/sasdummy/2013/06/04/find-a-sas-library-engine/) - - @param [in] libref Library reference (also accepts a 2 level libds ref). - - @return output returns the library engine (uppercase) for the FIRST library - encountered. - - @warning will only return the FIRST library engine - for concatenated - libraries, with different engines, inconsistent results may be encountered. - - @version 9.2 - @author Allan Bowe - -

Related Macros

- @li mf_getxengine.sas - -**//*** HELP END ***/ -/** @cond */ - -%macro mf_getengine(libref -)/*/STORE SOURCE*/; - %local dsid engnum rc engine; - - /* in case the parameter is a libref.tablename, pull off just the libref */ - %let libref = %upcase(%scan(&libref, 1, %str(.))); - - %let dsid=%sysfunc( - open(sashelp.vlibnam(where=(libname="%upcase(&libref)")),i) - ); - %if (&dsid ^= 0) %then %do; - %let engnum=%sysfunc(varnum(&dsid,ENGINE)); - %let rc=%sysfunc(fetch(&dsid)); - %let engine=%sysfunc(getvarc(&dsid,&engnum)); - %put &libref. ENGINE is &engine.; - %let rc= %sysfunc(close(&dsid)); - %end; - - %upcase(&engine) - -%mend mf_getengine; - -/** @endcond */ diff --git a/009_macros/mf_getfilesize.sas b/009_macros/mf_getfilesize.sas deleted file mode 100644 index fb7b93c..0000000 --- a/009_macros/mf_getfilesize.sas +++ /dev/null @@ -1,64 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns the size of a file in bytes. - @details Provide full path/filename.extension to the file, eg: - - %put %mf_getfilesize(fpath=C:\temp\myfile.txt); - - or, provide a libds value as follows: - - data x;do x=1 to 100000;y=x;output;end;run; - %put %mf_getfilesize(libds=work.x,format=yes); - - Which gives: - - > 2mb - - @param [in] fpath= Full path and filename. Provide this OR the libds value. - @param [in] libds= (0) Library.dataset value (assumes library is BASE engine) - @param [in] format= (NO) Set to yes to apply sizekmg. format - - @returns bytes - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_getfilesize(fpath=,libds=0,format=NO -)/*/STORE SOURCE*/; - - %local rc fid fref bytes dsid lib vnum; - - %if &libds ne 0 %then %do; - %let libds=%upcase(&libds); - %if %index(&libds,.)=0 %then %let lib=WORK; - %else %let lib=%scan(&libds,1,.); - %let dsid=%sysfunc(open( - sashelp.vtable(where=(libname="&lib" and memname="%scan(&libds,-1,.)") - keep=libname memname filesize - ) - )); - %if (&dsid ^= 0) %then %do; - %let vnum=%sysfunc(varnum(&dsid,FILESIZE)); - %let rc=%sysfunc(fetch(&dsid)); - %let bytes=%sysfunc(getvarn(&dsid,&vnum)); - %let rc= %sysfunc(close(&dsid)); - %end; - %else %put &sysmacroname: &libds could not be opened! %sysfunc(sysmsg()); - %end; - %else %do; - %let rc=%sysfunc(filename(fref,&fpath)); - %let fid=%sysfunc(fopen(&fref)); - %let bytes=%sysfunc(finfo(&fid,File Size (bytes))); - %let rc=%sysfunc(fclose(&fid)); - %let rc=%sysfunc(filename(fref)); - %end; - - %if &format=NO %then %do; - &bytes - %end; - %else %do; - %sysfunc(INPUTN(&bytes, best.),sizekmg.) - %end; - -%mend mf_getfilesize ; diff --git a/009_macros/mf_getfmtlist.sas b/009_macros/mf_getfmtlist.sas deleted file mode 100644 index cd150ae..0000000 --- a/009_macros/mf_getfmtlist.sas +++ /dev/null @@ -1,60 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns a distinct list of formats from a table - @details Reads the dataset header and returns a distinct list of formats - applied. - - %put NOTE- %mf_getfmtlist(sashelp.prdsale); - %put NOTE- %mf_getfmtlist(sashelp.shoes); - %put NOTE- %mf_getfmtlist(sashelp.demographics); - - returns: - - DOLLAR $CHAR W MONNAME - $CHAR BEST DOLLAR - BEST Z $CHAR COMMA PERCENTN - - @param [in] libds Two part library.dataset reference. - -

SAS Macros

- @li mf_getfmtname.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mf_getfmtlist(libds -)/*/STORE SOURCE*/; -/* declare local vars */ -%local out dsid nvars x rc fmt; - -/* open dataset in macro */ -%let dsid=%sysfunc(open(&libds)); - -/* continue if dataset exists */ -%if &dsid %then %do; - /* loop each variable in the dataset */ - %let nvars=%sysfunc(attrn(&dsid,NVARS)); - %do x=1 %to &nvars; - /* grab format and check it exists */ - %let fmt=%sysfunc(varfmt(&dsid,&x)); - %if %quote(&fmt) ne %quote() %then %let fmt=%mf_getfmtname(&fmt); - %else %do; - /* assign default format depending on variable type */ - %if %sysfunc(vartype(&dsid, &x))=C %then %let fmt=$CHAR; - %else %let fmt=BEST; - %end; - /* concatenate unique list of formats */ - %if %sysfunc(indexw(&out,&fmt,%str( )))=0 %then %let out=&out &fmt; - %end; - %let rc=%sysfunc(close(&dsid)); -%end; -%else %do; - %put &sysmacroname: Unable to open &libds (rc=&dsid); - %put &sysmacroname: SYSMSG= %sysfunc(sysmsg()); - %let rc=%sysfunc(close(&dsid)); -%end; -/* send them out without spaces or quote markers */ -%do;%unquote(&out)%end; -%mend mf_getfmtlist; diff --git a/009_macros/mf_getfmtname.sas b/009_macros/mf_getfmtname.sas deleted file mode 100644 index 1998f5b..0000000 --- a/009_macros/mf_getfmtname.sas +++ /dev/null @@ -1,44 +0,0 @@ -/*** HELP START ***//** - @file - @brief Extracts a format name from a fully defined format - @details Converts formats in like $thi3. and th13.2 $THI and TH. - Usage: - - %put %mf_getfmtname(8.); - %put %mf_getfmtname($4.); - %put %mf_getfmtname(comma14.10); - - Returns: - - > W - > $CHAR - > COMMA - - Note that system defaults are inferred from the values provided. - - @param [in] fmt The fully defined format. If left blank, nothing is returned. - - @returns The name (without width or decimal) of the format. - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mf_getfmtname(fmt -)/*/STORE SOURCE*/ /minoperator mindelimiter=' '; - -%local out dsid nvars x rc fmt; - -/* extract actual format name from the format definition */ -%let fmt=%scan(&fmt,1,.); -%do %while(%substr(&fmt,%length(&fmt),1) in 1 2 3 4 5 6 7 8 9 0); - %if %length(&fmt)=1 %then %let fmt=W; - %else %let fmt=%substr(&fmt,1,%length(&fmt)-1); -%end; - -%if &fmt=$ %then %let fmt=$CHAR; - -/* send them out without spaces or quote markers */ -%do;%unquote(%upcase(&fmt))%end; -%mend mf_getfmtname; diff --git a/009_macros/mf_getgitbranch.sas b/009_macros/mf_getgitbranch.sas deleted file mode 100644 index 486d8d6..0000000 --- a/009_macros/mf_getgitbranch.sas +++ /dev/null @@ -1,37 +0,0 @@ -/*** HELP START ***//** - @file - @brief Retrieves the current branch from a local GIT repo - @details In a local git repository, the current branch is always available in - the `.git/HEAD` file in a format like this: `ref: refs/heads/master` - - This macro simply reads the file and returns the last word (eg `master`). - - Example usage: - - %let gitdir=%sysfunc(pathname(work))/core; - %let repo=https://github.com/sasjs/core; - %put source clone rc=%sysfunc(GITFN_CLONE(&repo,&gitdir)); - - %put The current branch is %mf_getgitbranch(&gitdir); - - @param [in] gitdir The directory containing the GIT repository - -

SAS Macros

- @li mf_readfile.sas - -

Related Macros

- @li mp_gitadd.sas - @li mp_gitlog.sas - @li mp_gitreleaseinfo.sas - @li mp_gitstatus.sas - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_getgitbranch(gitdir -)/*/STORE SOURCE*/; - - %scan(%mf_readfile(&gitdir/.git/HEAD),-1) - -%mend mf_getgitbranch; diff --git a/009_macros/mf_getkeyvalue.sas b/009_macros/mf_getkeyvalue.sas deleted file mode 100644 index 59fb355..0000000 --- a/009_macros/mf_getkeyvalue.sas +++ /dev/null @@ -1,33 +0,0 @@ -/*** HELP START ***//** - @file - @brief retrieves a key value pair from a control dataset - @details By default, control dataset is work.mp_setkeyvalue. Usage: - - %mp_setkeyvalue(someindex,22,type=N) - %put %mf_getkeyvalue(someindex) - - - @param [in] key Provide a key on which to perform the lookup - @param [in] libds= (work.mp_setkeyvalue) The library.dataset which holds the - parameters - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_getkeyvalue(key,libds=work.mp_setkeyvalue -)/*/STORE SOURCE*/; -%local ds dsid key valc valn type rc; -%let dsid=%sysfunc(open(&libds(where=(key="&key")))); -%syscall set(dsid); -%let rc = %sysfunc(fetch(&dsid)); -%let rc = %sysfunc(close(&dsid)); - -%if &type=N %then %do; - &valn -%end; -%else %if &type=C %then %do; - &valc -%end; -%else %put %str(ERR)OR: Unable to find key &key in ds &libds; -%mend mf_getkeyvalue; diff --git a/009_macros/mf_getplatform.sas b/009_macros/mf_getplatform.sas deleted file mode 100644 index ec5d62a..0000000 --- a/009_macros/mf_getplatform.sas +++ /dev/null @@ -1,75 +0,0 @@ -/*** HELP START ***//** - @file mf_getplatform.sas - @brief Returns platform specific variables - @details Enables platform specific variables to be returned - - %put %mf_getplatform(); - - returns one of: - - @li SASMETA - @li SASVIYA - @li SASJS - @li BASESAS - - @param [in] switch the param for which to return a platform specific variable - -

SAS Macros

- @li mf_mval.sas - @li mf_trimstr.sas - - @version 9.4 / 3.4 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_getplatform(switch -)/*/STORE SOURCE*/; -%local a b c; -%if &switch.NONE=NONE %then %do; - %if %symexist(sasjsprocessmode) %then %do; - %if &sasjsprocessmode=Stored Program %then %do; - SASJS - %return; - %end; - %end; - %if %symexist(sysprocessmode) %then %do; - %if "&sysprocessmode"="SAS Object Server" - or "&sysprocessmode"= "SAS Compute Server" %then %do; - SASVIYA - %end; - %else %if "&sysprocessmode"="SAS Stored Process Server" - or "&sysprocessmode"="SAS Workspace Server" - %then %do; - SASMETA - %return; - %end; - %else %do; - BASESAS - %return; - %end; - %end; - %else %if %symexist(_metaport) or %symexist(_metauser) %then %do; - SASMETA - %return; - %end; - %else %do; - BASESAS - %return; - %end; -%end; -%else %if &switch=SASSTUDIO %then %do; - /* return the version of SAS Studio else 0 */ - %if %mf_mval(_CLIENTAPP)=%str(SAS Studio) %then %do; - %let a=%mf_mval(_CLIENTVERSION); - %let b=%scan(&a,1,.); - %if %eval(&b >2) %then %do; - &b - %end; - %else 0; - %end; - %else 0; -%end; -%else %if &switch=VIYARESTAPI %then %do; - %mf_trimstr(%sysfunc(getoption(servicesbaseurl)),/) -%end; -%mend mf_getplatform; diff --git a/009_macros/mf_getquotedstr.sas b/009_macros/mf_getquotedstr.sas deleted file mode 100644 index 98ab31b..0000000 --- a/009_macros/mf_getquotedstr.sas +++ /dev/null @@ -1,57 +0,0 @@ -/*** HELP START ***//** - @file - @brief Adds custom quotes / delimiters to a delimited string - @details Can be used in open code, eg as follows: - - %put %mf_getquotedstr(blah blah blah); - - which returns: -> 'blah','blah','blah' - - Alternatively: - - %put %mf_getquotedstr(these words are double quoted,quote=D) - - for: -> "these","words","are","double","quoted" - - @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 - @author Allan Bowe -**//*** HELP END ***/ - - -%macro mf_getquotedstr(IN_STR - ,DLM=%str(,) - ,QUOTE=S - ,indlm=%str( ) -)/*/STORE SOURCE*/; - /* 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() ) ; - %let item=%qscan(&IN_STR,&i,%str(&indlm)); - %if %bquote("E) ne %then %let item="E%qtrim(&item)"E; - %else %let item=%qtrim(&item); - - %if (&i = 1) %then %let buffer =%qtrim(&item); - %else %let buffer =&buffer&DLM%qtrim(&item); - - %let i = %eval(&i+1); - %end; - - %let buffer=%sysfunc(coalescec(%qtrim(&buffer),"E"E)); - - &buffer - -%mend mf_getquotedstr; diff --git a/009_macros/mf_getschema.sas b/009_macros/mf_getschema.sas deleted file mode 100644 index fec8f88..0000000 --- a/009_macros/mf_getschema.sas +++ /dev/null @@ -1,43 +0,0 @@ -/*** HELP START ***//** - @file mf_getschema.sas - @brief Returns the database schema of a SAS library - @details Usage: - - %put %mf_getschema(MYDB); - - returns: - > dbo - - @param [in] libref Library reference (also accepts a 2 level libds ref). - - @return output returns the library schema for the FIRST library encountered - - @warning will only return the FIRST library schema - for concatenated - libraries, with different schemas, inconsistent results may be encountered. - - @version 9.2 - @author Allan Bowe - @cond -**//*** HELP END ***/ - -%macro mf_getschema(libref -)/*/STORE SOURCE*/; - %local dsid vnum rc schema; - /* in case the parameter is a libref.tablename, pull off just the libref */ - %let libref = %upcase(%scan(&libref, 1, %str(.))); - %let dsid=%sysfunc(open(sashelp.vlibnam(where=( - libname="%upcase(&libref)" and sysname='Schema/Owner' - )),i)); - %if (&dsid ^= 0) %then %do; - %let vnum=%sysfunc(varnum(&dsid,SYSVALUE)); - %let rc=%sysfunc(fetch(&dsid)); - %let schema=%sysfunc(getvarc(&dsid,&vnum)); - %put &libref. schema is &schema.; - %let rc= %sysfunc(close(&dsid)); - %end; - - &schema - -%mend mf_getschema; - -/** @endcond */ diff --git a/009_macros/mf_getuniquefileref.sas b/009_macros/mf_getuniquefileref.sas deleted file mode 100644 index de84935..0000000 --- a/009_macros/mf_getuniquefileref.sas +++ /dev/null @@ -1,60 +0,0 @@ -/*** HELP START ***//** - @file - @brief Assigns and returns an unused fileref - @details Using the native approach for assigning filerefs fails as some - procedures (such as proc http) do not recognise the temporary names (starting - with a hash), returning a message such as: - - > ERROR 22-322: Expecting a name. - - This macro works by attempting a random fileref (with a prefix), seeing if it - is already assigned, and if not - returning the fileref. - - If your process can accept filerefs with the hash (#) prefix, then set - `prefix=0` to revert to the native approach - which is significantly faster - when there are a lot of filerefs in a session. - - Use as follows: - - %let fileref1=%mf_getuniquefileref(); - %let fileref2=%mf_getuniquefileref(prefix=0); - %put &fileref1 &fileref2; - - which returns filerefs similar to: - -> _7432233 #LN00070 - - @param [in] prefix= (_) first part of fileref. Remember that filerefs can only - be 8 characters, so a 7 letter prefix would mean `maxtries` should be 10. - if using zero (0) as the prefix, a native assignment is used. - @param [in] maxtries= (1000) the last part of the libref. Must be an integer. - @param [in] lrecl= (32767) Provide a default lrecl with which to initialise - the generated fileref. - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_getuniquefileref(prefix=_,maxtries=1000,lrecl=32767); - %local rc fname; - %if &prefix=0 %then %do; - %let rc=%sysfunc(filename(fname,,temp,lrecl=&lrecl)); - %if &rc %then %put %sysfunc(sysmsg()); - &fname - %end; - %else %do; - %local x len; - %let len=%eval(8-%length(&prefix)); - %let x=0; - %do x=0 %to &maxtries; - %let fname=&prefix%substr(%sysfunc(ranuni(0)),3,&len); - %if %sysfunc(fileref(&fname)) > 0 %then %do; - %let rc=%sysfunc(filename(fname,,temp,lrecl=&lrecl)); - %if &rc %then %put %sysfunc(sysmsg()); - &fname - %return; - %end; - %end; - %put unable to find available fileref after &maxtries attempts; - %end; -%mend mf_getuniquefileref; diff --git a/009_macros/mf_getuniquelibref.sas b/009_macros/mf_getuniquelibref.sas deleted file mode 100644 index 2524ce2..0000000 --- a/009_macros/mf_getuniquelibref.sas +++ /dev/null @@ -1,57 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns an unused libref - @details Use as follows: - - libname mclib0 (work); - libname mclib1 (work); - libname mclib2 (work); - - %let libref=%mf_getuniquelibref(); - %put &=libref; - - which returns: - -> mclib3 - - A blank value is returned if no usable libname is determined. - - @param [in] prefix= (mclib) first part of the returned libref. As librefs can - be as long as 8 characters, a maximum length of 7 characters is premitted - for this prefix. - @param [in] maxtries= (1000) Deprecated parameter. Remains here to ensure a - non-breaking change. Will be removed in v5. - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_getuniquelibref(prefix=mclib,maxtries=1000); - %local x; - - %if ( %length(&prefix) gt 7 ) %then %do; - %put %str(ERR)OR: The prefix parameter cannot exceed 7 characters.; - 0 - %return; - %end; - %else %if (%sysfunc(NVALID(&prefix,v7))=0) %then %do; - %put %str(ERR)OR: Invalid prefix (&prefix); - 0 - %return; - %end; - - /* Set maxtries equal to '10 to the power of [# unused characters] - 1' */ - %let maxtries=%eval(10**(8-%length(&prefix))-1); - - %do x = 0 %to &maxtries; - %if %sysfunc(libref(&prefix&x)) ne 0 %then %do; - &prefix&x - %return; - %end; - %let x = %eval(&x + 1); - %end; - - %put %str(ERR)OR: No usable libref in range &prefix.0-&maxtries; - %put %str(ERR)OR- Try reducing the prefix or deleting some libraries!; - 0 -%mend mf_getuniquelibref; diff --git a/009_macros/mf_getuniquename.sas b/009_macros/mf_getuniquename.sas deleted file mode 100644 index 6df6bff..0000000 --- a/009_macros/mf_getuniquename.sas +++ /dev/null @@ -1,22 +0,0 @@ -/*** HELP START ***//** - @file mf_getuniquename.sas - @brief Returns a shortened (32 char) GUID as a valid SAS name - @details Use as follows: - - %let myds=%mf_getuniquename(); - %put &=myds; - - which returns: - -> MCc59c750610321d4c8bf75faadbcd22 - - @param [in] prefix= (MC) Sets a prefix for the new name - - @version 9.3 - @author Allan Bowe -**//*** HELP END ***/ - - -%macro mf_getuniquename(prefix=MC); -&prefix.%substr(%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32-%length(&prefix)) -%mend mf_getuniquename; diff --git a/009_macros/mf_getuser.sas b/009_macros/mf_getuser.sas deleted file mode 100644 index 64b44fc..0000000 --- a/009_macros/mf_getuser.sas +++ /dev/null @@ -1,42 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns a userid according to session context - @details In a workspace session, a user is generally represented by - &sysuserid or SYS_COMPUTE_SESSION_OWNER if it exists. - In a Stored Process session, &sysuserid - resolves to a system account (default=sassrv) and instead there are several - metadata username variables to choose from (_metauser, _metaperson - ,_username, _secureusername). The OS account is represented by - _secureusername whilst the metadata account is under - _metaperson. - - %let user= %mf_getUser(); - %put &user; - - @return SYSUSERID (if workspace server) - @return _METAPERSON (if stored process server) - @return SYS_COMPUTE_SESSION_OWNER (if Viya compute session) - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_getuser( -)/*/STORE SOURCE*/; - %local user; - - %if %symexist(_sasjs_username) %then %let user=&_sasjs_username; - %else %if %symexist(SYS_COMPUTE_SESSION_OWNER) %then %do; - %let user=&SYS_COMPUTE_SESSION_OWNER; - %end; - %else %if %symexist(_metaperson) %then %do; - %if %length(&_metaperson)=0 %then %let user=&sysuserid; - /* sometimes SAS will add @domain extension - remove for consistency */ - /* but be sure to quote in case of usernames with commas */ - %else %let user=%unquote(%scan(%quote(&_metaperson),1,@)); - %end; - %else %let user=&sysuserid; - - %quote(&user) - -%mend mf_getuser; diff --git a/009_macros/mf_getvalue.sas b/009_macros/mf_getvalue.sas deleted file mode 100644 index 6239126..0000000 --- a/009_macros/mf_getvalue.sas +++ /dev/null @@ -1,36 +0,0 @@ -/*** HELP START ***//** - @file - @brief Retrieves a value from a dataset. If no filter supplied, then first - record is used. - @details Be sure to %quote() your where clause. Example usage: - - %put %mf_getvalue(sashelp.class,name,filter=%quote(age=15)); - %put %mf_getvalue(sashelp.class,name); - -

SAS Macros

- @li mf_getattrn.sas - -

Related Macros

- @li mp_setkeyvalue.sas - - @param [in] libds dataset to query - @param [in] variable the variable which contains the value to return. - @param [in] filter= (1) contents of where clause - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_getvalue(libds,variable,filter=1 -)/*/STORE SOURCE*/; - %if %mf_getattrn(&libds,NLOBS)>0 %then %do; - %local dsid rc &variable; - %let dsid=%sysfunc(open(&libds(where=(&filter)))); - %syscall set(dsid); - %let rc = %sysfunc(fetch(&dsid)); - %let rc = %sysfunc(close(&dsid)); - - %trim(&&&variable) - - %end; -%mend mf_getvalue; diff --git a/009_macros/mf_getvarcount.sas b/009_macros/mf_getvarcount.sas deleted file mode 100644 index 8b1a8f7..0000000 --- a/009_macros/mf_getvarcount.sas +++ /dev/null @@ -1,49 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns number of variables in a dataset - @details Useful to identify those renagade datasets that have no columns! - Can also be used to count for numeric, or character columns - - %put Number of Variables=%mf_getvarcount(sashelp.class); - %put Character Variables=%mf_getvarcount(sashelp.class,typefilter=C); - %put Numeric Variables = %mf_getvarcount(sashelp.class,typefilter=N); - - returns: - > Number of Variables=4 - - - @param [in] libds Two part dataset (or view) reference. - @param [in] typefilter= (A) Filter for certain types of column. Valid values: - @li A Count All columns - @li C Count Character columns only - @li N Count Numeric columns only - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mf_getvarcount(libds,typefilter=A -)/*/STORE SOURCE*/; - %local dsid nvars rc outcnt x; - %let dsid=%sysfunc(open(&libds)); - %let nvars=.; - %let outcnt=0; - %let typefilter=%upcase(&typefilter); - %if &dsid %then %do; - %let nvars=%sysfunc(attrn(&dsid,NVARS)); - %if &typefilter=A %then %let outcnt=&nvars; - %else %if &nvars>0 %then %do x=1 %to &nvars; - /* increment based on variable type */ - %if %sysfunc(vartype(&dsid,&x))=&typefilter %then %do; - %let outcnt=%eval(&outcnt+1); - %end; - %end; - %let rc=%sysfunc(close(&dsid)); - %end; - %else %do; - %put unable to open &libds (rc=&dsid); - %let rc=%sysfunc(close(&dsid)); - %end; - &outcnt -%mend mf_getvarcount; diff --git a/009_macros/mf_getvarformat.sas b/009_macros/mf_getvarformat.sas deleted file mode 100644 index eb77b27..0000000 --- a/009_macros/mf_getvarformat.sas +++ /dev/null @@ -1,73 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns the format of a variable - @details Uses varfmt function to identify the format of a particular variable. - Usage: - - data test; - format str1 $1. num1 datetime19.; - str2='hello mum!'; num2=666; - stop; - run; - %put %mf_getVarFormat(test,str1); - %put %mf_getVarFormat(work.test,num1); - %put %mf_getVarFormat(test,str2,force=1); - %put %mf_getVarFormat(work.test,num2,force=1); - %put %mf_getVarFormat(test,renegade); - - returns: - - $1. - DATETIME19. - $10. - 8. - NOTE: Variable renegade does not exist in test - - @param [in] libds Two part dataset (or view) reference. - @param [in] var Variable name for which a format should be returned - @param [in] force= (0) Set to 1 to supply a default if the variable has no - format - @returns outputs format - - @author Allan Bowe - @version 9.2 -**//*** HELP END ***/ - -%macro mf_getVarFormat(libds /* two level ds name */ - , var /* variable name from which to return the format */ - , force=0 -)/*/STORE SOURCE*/; - %local dsid vnum vformat rc vlen vtype; - /* Open dataset */ - %let dsid = %sysfunc(open(&libds)); - %if &dsid > 0 %then %do; - /* Get variable number */ - %let vnum = %sysfunc(varnum(&dsid, &var)); - /* Get variable format */ - %if(&vnum > 0) %then %let vformat=%sysfunc(varfmt(&dsid, &vnum)); - %else %do; - %put NOTE: Variable &var does not exist in &libds; - %let rc = %sysfunc(close(&dsid)); - %return; - %end; - %end; - %else %do; - %put &sysmacroname: dataset &libds not opened! (rc=&dsid); - %put &sysmacroname: %sysfunc(sysmsg()); - %return; - %end; - - /* supply a default if no format available */ - %if %length(&vformat)<2 & &force=1 %then %do; - %let vlen = %sysfunc(varlen(&dsid, &vnum)); - %let vtype = %sysfunc(vartype(&dsid, &vnum.)); - %if &vtype=C %then %let vformat=$&vlen..; - %else %let vformat=best.; - %end; - - - /* Close dataset */ - %let rc = %sysfunc(close(&dsid)); - /* Return variable format */ - &vformat -%mend mf_getVarFormat; diff --git a/009_macros/mf_getvarlen.sas b/009_macros/mf_getvarlen.sas deleted file mode 100644 index 05af96c..0000000 --- a/009_macros/mf_getvarlen.sas +++ /dev/null @@ -1,56 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns the length of a variable - @details Uses varlen function to identify the length of a particular variable. - Usage: - - data test; - format str $1. num datetime19.; - stop; - run; - %put %mf_getVarLen(test,str); - %put %mf_getVarLen(work.test,num); - %put %mf_getVarLen(test,renegade); - - returns: - - 1 - 8 - NOTE: Variable renegade does not exist in test - - @param [in] libds Two part dataset (or view) reference. - @param [in] var Variable name for which a length should be returned - @returns outputs length - - @author Allan Bowe - @version 9.2 - -**//*** HELP END ***/ - -%macro mf_getVarLen(libds /* two level ds name */ - , var /* variable name from which to return the length */ -)/*/STORE SOURCE*/; - %local dsid vnum vlen rc; - /* Open dataset */ - %let dsid = %sysfunc(open(&libds)); - %if &dsid > 0 %then %do; - /* Get variable number */ - %let vnum = %sysfunc(varnum(&dsid, &var)); - /* Get variable format */ - %if(&vnum > 0) %then %let vlen = %sysfunc(varlen(&dsid, &vnum)); - %else %do; - %put NOTE: Variable &var does not exist in &libds; - %let vlen = %str( ); - %end; - %end; - %else %do; - %put &sysmacroname: dataset &libds not opened! (rc=&dsid); - %put &sysmacroname: %sysfunc(sysmsg()); - %return; - %end; - - /* Close dataset */ - %let rc = %sysfunc(close(&dsid)); - /* Return variable format */ - &vlen -%mend mf_getVarLen; diff --git a/009_macros/mf_getvarlist.sas b/009_macros/mf_getvarlist.sas deleted file mode 100644 index 428efeb..0000000 --- a/009_macros/mf_getvarlist.sas +++ /dev/null @@ -1,74 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns dataset variable list direct from header - @details WAY faster than dictionary tables or sas views, and can - also be called in macro logic (is pure macro). Can be used in open code, - eg as follows: - - %put List of Variables=%mf_getvarlist(sashelp.class); - - returns: - > List of Variables=Name Sex Age Height Weight - - For a seperated list of column values: - - %put %mf_getvarlist(sashelp.class,dlm=%str(,),quote=double); - - returns: - > "Name","Sex","Age","Height","Weight" - - @param [in] libds Two part dataset (or view) reference. - @param [in] dlm= ( ) Provide a delimiter (eg comma or space) to separate the - variables - @param [in] quote= (none) use either DOUBLE or SINGLE to quote the results - @param [in] typefilter= (A) Filter for certain types of column. Valid values: - @li A Return All columns - @li C Return Character columns - @li N Return Numeric columns - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mf_getvarlist(libds - ,dlm=%str( ) - ,quote=no - ,typefilter=A -)/*/STORE SOURCE*/; - /* declare local vars */ - %local outvar dsid nvars x rc dlm q var vtype; - - /* credit Rowland Hale - byte34 is double quote, 39 is single quote */ - %if %upcase("e)=DOUBLE %then %let q=%qsysfunc(byte(34)); - %else %if %upcase("e)=SINGLE %then %let q=%qsysfunc(byte(39)); - /* open dataset in macro */ - %let dsid=%sysfunc(open(&libds)); - - %if &dsid %then %do; - %let nvars=%sysfunc(attrn(&dsid,NVARS)); - %if &nvars>0 %then %do; - /* add variables with supplied delimeter */ - %do x=1 %to &nvars; - /* get variable type */ - %let vtype=%sysfunc(vartype(&dsid,&x)); - %if &vtype=&typefilter or &typefilter=A %then %do; - %let var=&q.%sysfunc(varname(&dsid,&x))&q.; - %if &var=&q&q %then %do; - %put &sysmacroname: Empty column found in &libds!; - %let var=&q. &q.; - %end; - %if %quote(&outvar)=%quote() %then %let outvar=&var; - %else %let outvar=&outvar.&dlm.&var.; - %end; - %end; - %end; - %let rc=%sysfunc(close(&dsid)); - %end; - %else %do; - %put &sysmacroname: Unable to open &libds (rc=&dsid); - %put &sysmacroname: SYSMSG= %sysfunc(sysmsg()); - %let rc=%sysfunc(close(&dsid)); - %end; - %do;%unquote(&outvar)%end; -%mend mf_getvarlist; diff --git a/009_macros/mf_getvarnum.sas b/009_macros/mf_getvarnum.sas deleted file mode 100644 index a2dd307..0000000 --- a/009_macros/mf_getvarnum.sas +++ /dev/null @@ -1,58 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns the position of a variable in dataset (varnum attribute). - @details Uses varnum function to determine position. - -Usage: - - data work.test; - format str $1. num datetime19.; - stop; - run; - %put %mf_getVarNum(work.test,str); - %put %mf_getVarNum(work.test,num); - %put %mf_getVarNum(work.test,renegade); - -returns: - - > 1 - - > 2 - - > NOTE: Variable renegade does not exist in test - - @param [in] libds Two part dataset (or view) reference. - @param [in] var Variable name for which a position should be returned - - @author Allan Bowe - @version 9.2 - -**//*** HELP END ***/ - -%macro mf_getVarNum(libds /* two level ds name */ - , var /* variable name from which to return the format */ -)/*/STORE SOURCE*/; - %local dsid vnum rc; - /* Open dataset */ - %let dsid = %sysfunc(open(&libds)); - %if &dsid > 0 %then %do; - /* Get variable number */ - %let vnum = %sysfunc(varnum(&dsid, &var)); - %if(&vnum <= 0) %then %do; - %put NOTE: Variable &var does not exist in &libds; - %let vnum = %str( ); - %end; - %end; - %else %do; - %put &sysmacroname: dataset &libds not opened! (rc=&dsid); - %put &sysmacroname: %sysfunc(sysmsg()); - %return; - %end; - - /* Close dataset */ - %let rc = %sysfunc(close(&dsid)); - - /* Return variable number */ - &vnum. - -%mend mf_getVarNum; diff --git a/009_macros/mf_getvartype.sas b/009_macros/mf_getvartype.sas deleted file mode 100644 index 27b77d9..0000000 --- a/009_macros/mf_getvartype.sas +++ /dev/null @@ -1,52 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns variable type - Character (C) or Numeric (N) - @details -Usage: - - data test; - length str $1. num 8.; - stop; - run; - %put %mf_getvartype(test,str); - %put %mf_getvartype(work.test,num); - - - - @param [in] libds Two part dataset (or view) reference. - @param [in] var the variable name to be checked - @return output returns C or N depending on variable type. If variable - does not exist then a blank is returned and a note is written to the log. - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mf_getvartype(libds /* two level name */ - , var /* variable name from which to return the type */ -)/*/STORE SOURCE*/; - %local dsid vnum vtype rc; - /* Open dataset */ - %let dsid = %sysfunc(open(&libds)); - %if &dsid. > 0 %then %do; - /* Get variable number */ - %let vnum = %sysfunc(varnum(&dsid, &var)); - /* Get variable type (C/N) */ - %if(&vnum. > 0) %then %let vtype = %sysfunc(vartype(&dsid, &vnum.)); - %else %do; - %put NOTE: Variable &var does not exist in &libds; - %let vtype = %str( ); - %end; - %end; - %else %do; - %put &sysmacroname: dataset &libds not opened! (rc=&dsid); - %put &sysmacroname: %sysfunc(sysmsg()); - %return; - %end; - - /* Close dataset */ - %let rc = %sysfunc(close(&dsid)); - /* Return variable type */ - &vtype -%mend mf_getvartype; diff --git a/009_macros/mf_getxengine.sas b/009_macros/mf_getxengine.sas deleted file mode 100644 index b6c5044..0000000 --- a/009_macros/mf_getxengine.sas +++ /dev/null @@ -1,43 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns the engine type of a SAS fileref - @details Queries sashelp.vextfl to get the xengine value. - Usage: - - filename feng temp; - %put %mf_getxengine(feng); - - returns: - > TEMP - - @param [in] fref The fileref to check - - @returns The XENGINE value in sashelp.vextfl or 0 if not found. - - @version 9.2 - @author Allan Bowe - -

Related Macros

- @li mf_getengine.sas - -**//*** HELP END ***/ - -%macro mf_getxengine(fref -)/*/STORE SOURCE*/; - %local dsid engnum rc engine; - - %let dsid=%sysfunc( - open(sashelp.vextfl(where=(fileref="%upcase(&fref)")),i) - ); - %if (&dsid ^= 0) %then %do; - %let engnum=%sysfunc(varnum(&dsid,XENGINE)); - %let rc=%sysfunc(fetch(&dsid)); - %let engine=%sysfunc(getvarc(&dsid,&engnum)); - %* put &fref. ENGINE is &engine.; - %let rc= %sysfunc(close(&dsid)); - %end; - %else %let engine=0; - - &engine - -%mend mf_getxengine; diff --git a/009_macros/mf_increment.sas b/009_macros/mf_increment.sas deleted file mode 100644 index 77d4f16..0000000 --- a/009_macros/mf_increment.sas +++ /dev/null @@ -1,29 +0,0 @@ -/*** HELP START ***//** - @file - @brief Increments a macro variable - @details Useful outside of do-loops - will increment a macro variable every - time it is called. - - Example: - - %let cnt=1; - %put We have run %mf_increment(cnt) lines; - %put Now we have run %mf_increment(cnt) lines; - %put There are %mf_increment(cnt) lines in total; - - @param [in] macro_name The name of the macro variable to increment - @param [in] incr= (1) The amount to add or subtract to the macro - -

Related Files

- @li mf_increment.test.sas - -**//*** HELP END ***/ - -%macro mf_increment(macro_name,incr=1); - - /* iterate the value */ - %let ¯o_name=%eval(&&¯o_name+&incr); - /* return the value */ - &&¯o_name - -%mend mf_increment; diff --git a/009_macros/mf_isdir.sas b/009_macros/mf_isdir.sas deleted file mode 100644 index f0633e0..0000000 --- a/009_macros/mf_isdir.sas +++ /dev/null @@ -1,34 +0,0 @@ -/*** HELP START ***//** - @file - @brief Checks whether a path is a valid directory - @details - Usage: - - %let isdir=%mf_isdir(/tmp); - - With thanks and full credit to Andrea Defronzo - - https://www.linkedin.com/in/andrea-defronzo-b1a47460/ - - @param [in] path Full path of the file/directory to be checked - - @return output returns 1 if path is a directory, 0 if it is not - - @version 9.2 -**//*** HELP END ***/ - -%macro mf_isdir(path -)/*/STORE SOURCE*/; - %local rc did is_directory fref_t; - - %let is_directory = 0; - %let rc = %sysfunc(filename(fref_t, %superq(path))); - %let did = %sysfunc(dopen(&fref_t.)); - %if &did. ^= 0 %then %do; - %let is_directory = 1; - %let rc = %sysfunc(dclose(&did.)); - %end; - %let rc = %sysfunc(filename(fref_t)); - - &is_directory - -%mend mf_isdir; diff --git a/009_macros/mf_isint.sas b/009_macros/mf_isint.sas deleted file mode 100644 index 002b3ee..0000000 --- a/009_macros/mf_isint.sas +++ /dev/null @@ -1,36 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns 1 if the variable contains only digits 0-9, else 0 - @details Note that numerics containing any punctuation (including decimals - or exponents) will be flagged zero. - - If you'd like support for this, then do raise an issue (or even better, a - pull request!) - - Usage: - - %put %mf_isint(1) returns 1; - %put %mf_isint(1.1) returns 0; - %put %mf_isint(%str(1,1)) returns 0; - - @param [in] arg input value to check - - @version 9.2 -**//*** HELP END ***/ - -%macro mf_isint(arg -)/*/STORE SOURCE*/; - - /* blank val is not an integer */ - %if "&arg"="" %then %do;0%return;%end; - - /* remove minus sign if exists */ - %local val; - %if "%substr(%str(&arg),1,1)"="-" %then %let val=%substr(%str(&arg),2); - %else %let val=&arg; - - /* check remaining chars */ - %if %sysfunc(findc(%str(&val),,kd)) %then %do;0%end; - %else %do;1%end; - -%mend mf_isint; diff --git a/009_macros/mf_islibds.sas b/009_macros/mf_islibds.sas deleted file mode 100644 index 5ec382a..0000000 --- a/009_macros/mf_islibds.sas +++ /dev/null @@ -1,40 +0,0 @@ -/*** HELP START ***//** - @file - @brief Checks whether a string follows correct library.dataset format - @details Many macros in the core library accept a library.dataset parameter - referred to as 'libds'. This macro validates the structure of that parameter, - eg: - - @li 8 character libref? - @li 32 character dataset? - @li contains a period? - - It does NOT check whether the dataset exists, or if the library is assigned. - - Usage: - - %put %mf_islibds(work.something)=1; - %put %mf_islibds(nolib)=0; - %put %mf_islibds(badlibref.ds)=0; - %put %mf_islibds(w.t.f)=0; - - @param [in] libds The string to be checked - - @return output Returns 1 if libds is valid, 0 if it is not - -

Related Macros

- @li mf_islibds.test.sas - @li mp_validatecol.sas - - @version 9.2 -**//*** HELP END ***/ - -%macro mf_islibds(libds -)/*/STORE SOURCE*/; - -%local regex; -%let regex=%sysfunc(prxparse(%str(/^[_a-z]\w{0,7}\.[_a-z]\w{0,31}$/i))); - -%sysfunc(prxmatch(®ex,&libds)) - -%mend mf_islibds; diff --git a/009_macros/mf_loc.sas b/009_macros/mf_loc.sas deleted file mode 100644 index caa354b..0000000 --- a/009_macros/mf_loc.sas +++ /dev/null @@ -1,34 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns physical location of various SAS items - @details Returns location of the PlatformObjectFramework tools - Usage: - - %put %mf_loc(POF); %*location of PlatformObjectFramework tools; - - @param [in] loc The item to locate, eg: - @li PLAATFORMOBJECTFRAMEWORK (or POF) - @li VIYACONFG - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_loc(loc); -%let loc=%upcase(&loc); -%local root; - -%if &loc=POF or &loc=PLATFORMOBJECTFRAMEWORK %then %do; - %let root=%sysget(SASROOT); - %let root=%substr(&root,1,%index(&root,SASFoundation)-2); - %let root=&root/SASPlatformObjectFramework/&sysver; - %put Batch tools located at: &root; - &root -%end; -%else %if &loc=VIYACONFIG %then %do; - %let root=/opt/sas/viya/config; - %put Viya Config located at: &root; - &root -%end; - -%mend mf_loc; diff --git a/009_macros/mf_mkdir.sas b/009_macros/mf_mkdir.sas deleted file mode 100644 index 860537f..0000000 --- a/009_macros/mf_mkdir.sas +++ /dev/null @@ -1,67 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a directory, including any intermediate directories - @details Works on windows and unix environments via dcreate function. -Usage: - - %mf_mkdir(/some/path/name) - - - @param [in] dir Relative or absolute pathname. Unquoted. - @version 9.2 - -**//*** HELP END ***/ - -%macro mf_mkdir(dir -)/*/STORE SOURCE*/; - - %local lastchar child parent; - - %let lastchar = %substr(&dir, %length(&dir)); - %if (%bquote(&lastchar) eq %str(:)) %then %do; - /* Cannot create drive mappings */ - %return; - %end; - - %if (%bquote(&lastchar)=%str(/)) or (%bquote(&lastchar)=%str(\)) %then %do; - /* last char is a slash */ - %if (%length(&dir) eq 1) %then %do; - /* one single slash - root location is assumed to exist */ - %return; - %end; - %else %do; - /* strip last slash */ - %let dir = %substr(&dir, 1, %length(&dir)-1); - %end; - %end; - - %if (%sysfunc(fileexist(%bquote(&dir))) = 0) %then %do; - /* directory does not exist so prepare to create */ - /* first get the childmost directory */ - %let child = %scan(&dir, -1, %str(/\:)); - - /* - If child name = path name then there are no parents to create. Else - they must be recursively scanned. - */ - - %if (%length(&dir) gt %length(&child)) %then %do; - %let parent = %substr(&dir, 1, %length(&dir)-%length(&child)); - %mf_mkdir(&parent) - %end; - - /* - Now create the directory. Complain loudly of any errs. - */ - - %let dname = %sysfunc(dcreate(&child, &parent)); - %if (%bquote(&dname) eq ) %then %do; - %put %str(ERR)OR: could not create &parent + &child; - %abort cancel; - %end; - %else %do; - %put Directory created: &dir; - %end; - %end; - /* exit quietly if directory did exist.*/ -%mend mf_mkdir; diff --git a/009_macros/mf_mval.sas b/009_macros/mf_mval.sas deleted file mode 100644 index b2ed792..0000000 --- a/009_macros/mf_mval.sas +++ /dev/null @@ -1,21 +0,0 @@ -/*** HELP START ***//** - @file mf_mval.sas - @brief Returns a macro variable value if the variable exists - @details - Use this macro to avoid repetitive use of `%if %symexist(MACVAR) %then` - type logic. - Usage: - - %if %mf_mval(maynotexist)=itdid %then %do; - - @param [in] var The macro variable NAME to return the (possible) value for - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mf_mval(var); - %if %symexist(&var) %then %do; - %superq(&var) - %end; -%mend mf_mval; diff --git a/009_macros/mf_nobs.sas b/009_macros/mf_nobs.sas deleted file mode 100644 index 2f2ab25..0000000 --- a/009_macros/mf_nobs.sas +++ /dev/null @@ -1,26 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns number of logical (undeleted) observations. - @details Beware - will not work on external database tables! - Is just a convenience macro for calling %mf_getattrn(). - - %put Number of observations=%mf_nobs(sashelp.class); - -

SAS Macros

- @li mf_getattrn.sas - - @param [in] libds library.dataset - - @return output returns result of the attrn value supplied, or log message - if err. - - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mf_nobs(libds -)/*/STORE SOURCE*/; - %mf_getattrn(&libds,NLOBS) -%mend mf_nobs; diff --git a/009_macros/mf_readfile.sas b/009_macros/mf_readfile.sas deleted file mode 100644 index 0b3ff48..0000000 --- a/009_macros/mf_readfile.sas +++ /dev/null @@ -1,63 +0,0 @@ -/*** HELP START ***//** - @file - @brief Reads the first line of a file using pure macro - @details Reads the first line of a file and returns it. Future versions may - read each line into a macro variable array. - - Generally, reading data into macro variables is not great as certain - nonprintable characters (such as CR, LF) may be dropped in the conversion. - - Usage: - - %mf_writefile(&sasjswork/myfile.txt,l1=some content,l2=more content) - - %put %mf_readfile(&sasjswork/myfile.txt); - - - @param [in] fpath Full path to file to be read - -

Related Macros

- @li mf_deletefile.sas - @li mf_writefile.sas - @li mf_readfile.test.sas - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ -/** @cond */ - -%macro mf_readfile(fpath -)/*/STORE SOURCE*/; -%local fref rc fid fcontent; - -/* check file exists */ -%if %sysfunc(filename(fref,&fpath)) ne 0 %then %do; - %put &=fref &=fpath; - %put %str(ERR)OR: %sysfunc(sysmsg()); - %return; -%end; - -%let fid=%sysfunc(fopen(&fref,I)); - -%if &fid=0 %then %do; - %put %str(ERR)OR: %sysfunc(sysmsg()); - %return; -%end; - -%if %sysfunc(fread(&fid)) = 0 %then %do; - %let rc=%sysfunc(fget(&fid,fcontent,65534)); - &fcontent -%end; - -/* -%do %while(%sysfunc(fread(&fid)) = 0); - %let rc=%sysfunc(fget(&fid,fcontent,65534)); - &fcontent -%end; -*/ - -%let rc=%sysfunc(fclose(&fid)); -%let rc=%sysfunc(filename(&fref)); - -%mend mf_readfile; -/** @endcond */ diff --git a/009_macros/mf_trimstr.sas b/009_macros/mf_trimstr.sas deleted file mode 100644 index 3e2a2de..0000000 --- a/009_macros/mf_trimstr.sas +++ /dev/null @@ -1,50 +0,0 @@ -/*** HELP START ***//** - @file mf_trimstr.sas - @brief Removes character(s) from the end, if they exist - @details If the designated characters exist at the end of the string, they - are removed - - %put %mf_trimstr(/blah/,/); * /blah; - %put %mf_trimstr(/blah/,h); * /blah/; - %put %mf_trimstr(/blah/,h/);* /bla; - -

SAS Macros

- - - @param [in] basestr The string to be modified - @param [in] trimstr The string to be removed from the end of `basestr`, if it - exists - - @return output returns result with the value of `trimstr` removed from the end - - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mf_trimstr(basestr,trimstr); -%local baselen trimlen trimval; - -/* return if basestr is shorter than trimstr (or 0) */ -%let baselen=%length(%superq(basestr)); -%let trimlen=%length(%superq(trimstr)); -%if &baselen < &trimlen or &baselen=0 %then %return; - -/* obtain the characters from the end of basestr */ -%let trimval=%qsubstr(%superq(basestr) - ,%length(%superq(basestr))-&trimlen+1 - ,&trimlen); - -/* compare and if matching, chop it off! */ -%if %superq(basestr)=%superq(trimstr) %then %do; - %return; -%end; -%else %if %superq(trimval)=%superq(trimstr) %then %do; - %qsubstr(%superq(basestr),1,%length(%superq(basestr))-&trimlen) -%end; -%else %do; - &basestr -%end; - -%mend mf_trimstr; diff --git a/009_macros/mf_uid.sas b/009_macros/mf_uid.sas deleted file mode 100644 index 48a2177..0000000 --- a/009_macros/mf_uid.sas +++ /dev/null @@ -1,21 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a unique ID based on system time in friendly format - @details format = YYYYMMDD_HHMMSSmmm__<3randomDigits> - - %put %mf_uid(); - - @version 9.3 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mf_uid( -)/*/STORE SOURCE*/; - %local today now; - %let today=%sysfunc(today(),yymmddn8.); - %let now=%sysfunc(compress(%sysfunc(time(),tod12.3),:.)); - - &today._&now._&sysjobid._%sysevalf(%sysfunc(ranuni(0))*999,CEIL) - -%mend mf_uid; diff --git a/009_macros/mf_verifymacvars.sas b/009_macros/mf_verifymacvars.sas deleted file mode 100644 index c22aa13..0000000 --- a/009_macros/mf_verifymacvars.sas +++ /dev/null @@ -1,72 +0,0 @@ -/*** HELP START ***//** - @file - @brief Checks if a set of macro variables exist AND contain values. - @details Writes ERROR to log if abortType is SOFT, else will call %mf_abort. - Usage: - - %let var1=x; - %let var2=y; - %put %mf_verifymacvars(var1 var2); - - Returns: - > 1 - -

SAS Macros

- @li mf_abort.sas - - @param [in] verifyvars Space separated list of macro variable names - @param [in] makeupcase= (NO) Set to YES to convert all variable VALUES to - uppercase. - @param [in] mAbort= (SOFT) Abort Type. When SOFT, simply writes an err - message to the log. - Set to any other value to call mf_abort (which can be configured to abort in - various fashions according to context). - - @warning will not be able to verify the following variables due to - naming clash! - - verifyVars - - verifyVar - - verifyIterator - - makeUpcase - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - - -%macro mf_verifymacvars( - verifyVars /* list of macro variable NAMES */ - ,makeUpcase=NO /* set to YES to make all the variable VALUES uppercase */ - ,mAbort=SOFT -)/*/STORE SOURCE*/; - - %local verifyIterator verifyVar abortmsg; - %do verifyIterator=1 %to %sysfunc(countw(&verifyVars,%str( ))); - %let verifyVar=%qscan(&verifyVars,&verifyIterator,%str( )); - %if not %symexist(&verifyvar) %then %do; - %let abortmsg= Variable &verifyVar is MISSING; - %goto exit_err; - %end; - %if %length(%trim(&&&verifyVar))=0 %then %do; - %let abortmsg= Variable &verifyVar is EMPTY; - %goto exit_err; - %end; - %if &makeupcase=YES %then %do; - %let &verifyVar=%upcase(&&&verifyvar); - %end; - %end; - - %goto exit_success; - %exit_err: - %put &abortmsg; - %mf_abort(iftrue=(&mabort ne SOFT), - mac=mf_verifymacvars, - msg=%str(&abortmsg) - ) - 0 - %return; - %exit_success: - 1 - -%mend mf_verifymacvars; diff --git a/009_macros/mf_wordsinstr1andstr2.sas b/009_macros/mf_wordsinstr1andstr2.sas deleted file mode 100644 index 44f2f49..0000000 --- a/009_macros/mf_wordsinstr1andstr2.sas +++ /dev/null @@ -1,53 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns words that are in both string 1 and string 2 - @details Compares two space separated strings and returns the words that are - in both. - Usage: - - %put %mf_wordsInStr1andStr2( - Str1=blah sss blaaah brah bram boo - ,Str2= blah blaaah brah ssss - ); - - returns: - > blah blaaah brah - - @param [in] str1= () string containing words to extract - @param [in] str2= () used to compare with the extract string - - @warning CASE SENSITIVE! - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mf_wordsInStr1andStr2( - Str1= /* string containing words to extract */ - ,Str2= /* used to compare with the extract string */ -)/*/STORE SOURCE*/; - -%local count_base count_extr i i2 extr_word base_word match outvar; -%if %length(&str1)=0 or %length(&str2)=0 %then %do; - %put base string (str1)= &str1; - %put compare string (str2) = &str2; - %return; -%end; -%let count_base=%sysfunc(countw(&Str2)); -%let count_extr=%sysfunc(countw(&Str1)); - -%do i=1 %to &count_extr; - %let extr_word=%scan(&Str1,&i,%str( )); - %let match=0; - %do i2=1 %to &count_base; - %let base_word=%scan(&Str2,&i2,%str( )); - %if &extr_word=&base_word %then %let match=1; - %end; - %if &match=1 %then %let outvar=&outvar &extr_word; -%end; - - &outvar - -%mend mf_wordsInStr1andStr2; - diff --git a/009_macros/mf_wordsinstr1butnotstr2.sas b/009_macros/mf_wordsinstr1butnotstr2.sas deleted file mode 100644 index 5d665f2..0000000 --- a/009_macros/mf_wordsinstr1butnotstr2.sas +++ /dev/null @@ -1,54 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns words that are in string 1 but not in string 2 - @details Compares two space separated strings and returns the words that are - in the first but not in the second. - - Note - case sensitive! - - Usage: - - %let x= %mf_wordsInStr1ButNotStr2( - Str1=blah sss blaaah brah bram boo - ,Str2= blah blaaah brah ssss - ); - - returns: - > sss bram boo - - @param [in] str1= () String containing words to extract - @param [in] str2= () Used to compare with the extract string - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mf_wordsInStr1ButNotStr2( - Str1= /* string containing words to extract */ - ,Str2= /* used to compare with the extract string */ -)/*/STORE SOURCE*/; - -%local count_base count_extr i i2 extr_word base_word match outvar; -%if %length(&str1)=0 or %length(&str2)=0 %then %do; - %put base string (str1)= &str1; - %put compare string (str2) = &str2; - %return; -%end; -%let count_base=%sysfunc(countw(&Str2)); -%let count_extr=%sysfunc(countw(&Str1)); - -%do i=1 %to &count_extr; - %let extr_word=%scan(&Str1,&i,%str( )); - %let match=0; - %do i2=1 %to &count_base; - %let base_word=%scan(&Str2,&i2,%str( )); - %if &extr_word=&base_word %then %let match=1; - %end; - %if &match=0 %then %let outvar=&outvar &extr_word; -%end; - - &outvar - -%mend mf_wordsInStr1ButNotStr2; - diff --git a/009_macros/mf_writefile.sas b/009_macros/mf_writefile.sas deleted file mode 100644 index f1d4d8b..0000000 --- a/009_macros/mf_writefile.sas +++ /dev/null @@ -1,67 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a text file using pure macro - @details Creates a text file of up to 10 lines. If further lines are - desired, feel free to [create an issue]( - https://github.com/sasjs/core/issues/new), or make a pull request! - - The use of PARMBUFF was considered for this macro, but it would have made - things problematic for writing lines containing commas. - - Usage: - - %mf_writefile(&sasjswork/myfile.txt,l1=some content,l2=more content) - data _null_; - infile "&sasjswork/myfile.txt"; - input; - list; - run; - - @param [in] fpath Full path to file to be created or appended to - @param [in] mode= (O) Available options are A or O as follows: - @li A APPEND mode, writes new records after the current end of the file. - @li O OUTPUT mode, writes new records from the beginning of the file. - @param [in] l1= () First line - @param [in] l2= () Second line (etc through to l10) - -

Related Macros

- @li mf_writefile.test.sas - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ -/** @cond */ - -%macro mf_writefile(fpath,mode=O,l1=,l2=,l3=,l4=,l5=,l6=,l7=,l8=,l9=,l10= -)/*/STORE SOURCE*/; -%local fref rc fid i total_lines; - -/* find number of lines by reference to first non-blank param */ -%do i=10 %to 1 %by -1; - %if %str(&&l&i) ne %str() %then %goto continue; -%end; -%continue: -%let total_lines=&i; - -%if %sysfunc(filename(fref,&fpath)) ne 0 %then %do; - %put &=fref &=fpath; - %put %str(ERR)OR: %sysfunc(sysmsg()); - %return; -%end; - -%let fid=%sysfunc(fopen(&fref,&mode)); - -%if &fid=0 %then %do; - %put %str(ERR)OR: %sysfunc(sysmsg()); - %return; -%end; - -%do i=1 %to &total_lines; - %let rc=%sysfunc(fput(&fid, &&l&i)); - %let rc=%sysfunc(fwrite(&fid)); -%end; -%let rc=%sysfunc(fclose(&fid)); -%let rc=%sysfunc(filename(&fref)); - -%mend mf_writefile; -/** @endcond */ diff --git a/009_macros/mp_abort.sas b/009_macros/mp_abort.sas deleted file mode 100644 index 801e238..0000000 --- a/009_macros/mp_abort.sas +++ /dev/null @@ -1,338 +0,0 @@ -/*** HELP START ***//** - @file - @brief abort gracefully according to context - @details Configures an abort mechanism according to site specific policies or - the particulars of an environment. For instance, can stream custom - results back to the client in an STP Web App context, or completely stop - in the case of a batch run. For STP sessions - - The method used varies according to the context. Important points: - - @li should not use endsas or abort cancel in 9.4m3 WIN environments as this - can cause hung multibridge sessions and result in a frozen STP server - @li The use of endsas in 9.4m6+ windows environments for POST requests to the - STP server can result in an empty response body - @li should not use endsas in viya 3.5 as this destroys the session and cannot - fetch results (although both mv_getjoblog.sas and the @sasjs/adapter will - recognise this and fetch the log of the parent session instead) - @li STP environments must finish cleanly to avoid the log being sent to - _webout. To assist with this, we also run stpsrvset('program error', 0) - and set SYSCC=0. - Where possible, we take a unique "soft abort" approach - we open a macro - but don't close it! This works everywhere EXCEPT inside a \%include inside - a macro. For that, we recommend you use mp_include.sas to perform the - include, and then call \%mp_abort(mode=INCLUDE) from the source program (ie, - OUTSIDE of the top-parent macro). - The soft abort has become ineffective in 9.4m6 WINDOWS environments. We are - currently investigating approaches to deal with this. - - - @param [in] mac= (mp_abort.sas) To contain the name of the calling macro. Do - not use &sysmacroname as this will always resolve to MP_ABORT. - @param [out] msg= message to be returned - @param [in] iftrue= (1=1) Condition under which the macro should be executed - @param [in] errds= (work.mp_abort_errds) There is no clean way to end a - process within a %include called within a macro. Furthermore, there is no - way to test if a macro is called within a %include. To handle this - particular scenario, the %include should be switched for the mp_include.sas - macro. - This provides an indicator that we are running a macro within a \%include - (`_SYSINCLUDEFILEDEVICE`) and allows us to provide a dataset with the abort - values (msg, mac). - We can then run an abort cancel FILE to stop the include running, and pass - the dataset back to the calling program to run a regular \%mp_abort(). - The dataset will contain the following fields: - @li iftrue (1=1) - @li msg (the message) - @li mac (the mac param) - - @param [in] mode= (REGULAR) If mode=INCLUDE then the &errds dataset is checked - for an abort status. - Valid values: - @li REGULAR (default) - @li INCLUDE - - @version 9.4 - @author Allan Bowe - -

Related Macros

- @li mp_include.sas - - @cond -**//*** HELP END ***/ - -%macro mp_abort(mac=mp_abort.sas, type=, msg=, iftrue=%str(1=1) - , errds=work.mp_abort_errds - , mode=REGULAR -)/*/STORE SOURCE*/; - -%global sysprocessmode sysprocessname sasjs_stpsrv_header_loc sasjsprocessmode; -%local fref fid i; - -%if not(%eval(%unquote(&iftrue))) %then %return; - -%put NOTE: /// mp_abort macro executing //; -%if %length(&mac)>0 %then %put NOTE- called by &mac; -%put NOTE - &msg; - -%if %symexist(_SYSINCLUDEFILEDEVICE) -/* abort cancel FILE does not restart outside the INCLUDE on Viya 3.5 */ -and %superq(SYSPROCESSNAME) ne %str(Compute Server) -%then %do; - %if "*&_SYSINCLUDEFILEDEVICE*" ne "**" %then %do; - data &errds; - iftrue='1=1'; - length mac $100 msg $5000; - mac=symget('mac'); - msg=symget('msg'); - run; - data _null_; - abort cancel FILE; - run; - %return; - %end; -%end; - -/* Web App Context */ -%if %symexist(_PROGRAM) - or %superq(SYSPROCESSNAME) = %str(Compute Server) - or &mode=INCLUDE -%then %do; - options obs=max replace mprint; - %if "%substr(&sysver,1,1)" ne "4" and "%substr(&sysver,1,1)" ne "5" - %then %do; - options nosyntaxcheck; - %end; - - %if &mode=INCLUDE %then %do; - %if %sysfunc(exist(&errds))=1 %then %do; - data _null_; - set &errds; - call symputx('iftrue',iftrue,'l'); - call symputx('mac',mac,'l'); - call symputx('msg',msg,'l'); - putlog (_all_)(=); - run; - %if (&iftrue)=0 %then %return; - %end; - %else %do; - %put &sysmacroname: No include errors found; - %return; - %end; - %end; - - /* extract log errs / warns, if exist */ - %local logloc logline; - %global logmsg; /* capture global messages */ - %if %symexist(SYSPRINTTOLOG) %then %let logloc=&SYSPRINTTOLOG; - %else %let logloc=%qsysfunc(getoption(LOG)); - proc printto log=log;run; - %let logline=0; - %if %length(&logloc)>0 %then %do; - data _null_; - infile &logloc lrecl=5000; - input; putlog _infile_; - i=1; - retain logonce 0; - if ( - _infile_=:"%str(WARN)ING" or _infile_=:"%str(ERR)OR" - ) and logonce=0 then - do; - call symputx('logline',_n_); - logonce+1; - end; - run; - /* capture log including lines BEFORE the err */ - %if &logline>0 %then %do; - data _null_; - infile &logloc lrecl=5000; - input; - i=1; - stoploop=0; - if _n_ ge &logline-15 and stoploop=0 then do until (i>22); - call symputx('logmsg',catx('\n',symget('logmsg'),_infile_)); - input; - i+1; - stoploop=1; - end; - if stoploop=1 then stop; - run; - %end; - %end; - - %if %symexist(SYS_JES_JOB_URI) %then %do; - /* setup webout for Viya */ - options nobomfile; - %if "X&SYS_JES_JOB_URI.X"="XX" %then %do; - filename _webout temp lrecl=999999 mod; - %end; - %else %do; - filename _webout filesrvc parenturi="&SYS_JES_JOB_URI" - name="_webout.json" lrecl=999999 mod; - %end; - %end; - %else %if %sysfunc(filename(fref,&sasjs_stpsrv_header_loc))=0 %then %do; - options nobomfile; - /* set up http header for SASjs Server */ - %let fid=%sysfunc(fopen(&fref,A)); - %if &fid=0 %then %do; - %put %str(ERR)OR: %sysfunc(sysmsg()); - %return; - %end; - %let rc=%sysfunc(fput(&fid,%str(Content-Type: application/json))); - %let rc=%sysfunc(fwrite(&fid)); - %let rc=%sysfunc(fclose(&fid)); - %let rc=%sysfunc(filename(&fref)); - %end; - - /* send response in SASjs JSON format */ - data _null_; - file _webout mod lrecl=32000 encoding='utf-8'; - length msg syswarningtext syserrortext $32767 mode $10 ; - sasdatetime=datetime(); - msg=symget('msg'); - %if &logline>0 %then %do; - msg=cats(msg,'\n\nLog Extract:\n',symget('logmsg')); - %end; - /* escape the escapes */ - msg=tranwrd(msg,'\','\\'); - /* escape the quotes */ - msg=tranwrd(msg,'"','\"'); - /* ditch the CRLFs as chrome complains */ - msg=compress(msg,,'kw'); - /* quote without quoting the quotes (which are escaped instead) */ - msg=cats('"',msg,'"'); - if symexist('_debug') then debug=quote(trim(symget('_debug'))); - else debug='""'; - if symget('sasjsprocessmode')='Stored Program' then mode='SASJS'; - if mode ne 'SASJS' then put '>>weboutBEGIN<<'; - put '{"SYSDATE" : "' "&SYSDATE" '"'; - put ',"SYSTIME" : "' "&SYSTIME" '"'; - put ',"sasjsAbort" : [{'; - put ' "MSG":' msg ; - put ' ,"MAC": "' "&mac" '"}]'; - put ",""SYSUSERID"" : ""&sysuserid"" "; - put ',"_DEBUG":' debug ; - if symexist('_metauser') then do; - _METAUSER=quote(trim(symget('_METAUSER'))); - put ",""_METAUSER"": " _METAUSER; - _METAPERSON=quote(trim(symget('_METAPERSON'))); - put ',"_METAPERSON": ' _METAPERSON; - end; - if symexist('SYS_JES_JOB_URI') then do; - SYS_JES_JOB_URI=quote(trim(symget('SYS_JES_JOB_URI'))); - put ',"SYS_JES_JOB_URI": ' SYS_JES_JOB_URI; - end; - _PROGRAM=quote(trim(resolve(symget('_PROGRAM')))); - put ',"_PROGRAM" : ' _PROGRAM ; - put ",""SYSCC"" : ""&syscc"" "; - syserrortext=cats(symget('syserrortext')); - if findc(syserrortext,'"\'!!'0A0D09000E0F010210111A'x) then do; - syserrortext='"'!!trim( - prxchange('s/"/\\"/',-1, /* double quote */ - prxchange('s/\x0A/\n/',-1, /* new line */ - prxchange('s/\x0D/\r/',-1, /* carriage return */ - prxchange('s/\x09/\\t/',-1, /* tab */ - prxchange('s/\x00/\\u0000/',-1, /* NUL */ - prxchange('s/\x0E/\\u000E/',-1, /* SS */ - prxchange('s/\x0F/\\u000F/',-1, /* SF */ - prxchange('s/\x01/\\u0001/',-1, /* SOH */ - prxchange('s/\x02/\\u0002/',-1, /* STX */ - prxchange('s/\x10/\\u0010/',-1, /* DLE */ - prxchange('s/\x11/\\u0011/',-1, /* DC1 */ - prxchange('s/\x1A/\\u001A/',-1, /* SUB */ - prxchange('s/\\/\\\\/',-1,syserrortext) - )))))))))))))!!'"'; - end; - else syserrortext=cats('"',syserrortext,'"'); - put ',"SYSERRORTEXT" : ' syserrortext; - put ",""SYSHOSTNAME"" : ""&syshostname"" "; - put ",""SYSJOBID"" : ""&sysjobid"" "; - put ",""SYSSCPL"" : ""&sysscpl"" "; - put ",""SYSSITE"" : ""&syssite"" "; - sysvlong=quote(trim(symget('sysvlong'))); - put ',"SYSVLONG" : ' sysvlong; - syswarningtext=cats(symget('syswarningtext')); - if findc(syswarningtext,'"\'!!'0A0D09000E0F010210111A'x) then do; - syswarningtext='"'!!trim( - prxchange('s/"/\\"/',-1, /* double quote */ - prxchange('s/\x0A/\n/',-1, /* new line */ - prxchange('s/\x0D/\r/',-1, /* carriage return */ - prxchange('s/\x09/\\t/',-1, /* tab */ - prxchange('s/\x00/\\u0000/',-1, /* NUL */ - prxchange('s/\x0E/\\u000E/',-1, /* SS */ - prxchange('s/\x0F/\\u000F/',-1, /* SF */ - prxchange('s/\x01/\\u0001/',-1, /* SOH */ - prxchange('s/\x02/\\u0002/',-1, /* STX */ - prxchange('s/\x10/\\u0010/',-1, /* DLE */ - prxchange('s/\x11/\\u0011/',-1, /* DC1 */ - prxchange('s/\x1A/\\u001A/',-1, /* SUB */ - prxchange('s/\\/\\\\/',-1,syswarningtext) - )))))))))))))!!'"'; - end; - else syswarningtext=cats('"',syswarningtext,'"'); - put ",""SYSWARNINGTEXT"" : " syswarningtext; - put ',"END_DTTM" : "' "%sysfunc(datetime(),E8601DT26.6)" '" '; - put "}" ; - if mode ne 'SASJS' then put '>>weboutEND<<'; - run; - - %put _all_; - - %if "&sysprocessmode " = "SAS Stored Process Server " %then %do; - data _null_; - putlog 'stpsrvset program err and syscc'; - rc=stpsrvset('program error', 0); - call symputx("syscc",0,"g"); - run; - %if &sysscp=WIN - and 1=0 /* deprecating this logic until we figure out a consistent abort */ - and "%substr(%str(&sysvlong ),1,8)"="9.04.01M" - and "%substr(%str(&sysvlong ),9,1)">"5" %then %do; - /* skip approach (below) does not work in windows m6+ envs */ - endsas; - %end; - %else %do; - /** - * endsas kills 9.4m3 deployments by orphaning multibridges. - * Abort variants are ungraceful (non zero return code) - * This approach lets SAS run silently until the end :-) - * Caution - fails when called within a %include within a macro - * Use mp_include() to handle this. - */ - filename skip temp; - data _null_; - file skip; - put '%macro skip();'; - comment '%mend skip; -> fix lint '; - put '%macro skippy();'; - comment '%mend skippy; -> fix lint '; - run; - %inc skip; - %end; - %end; - %else %if "&sysprocessmode " = "SAS Compute Server " %then %do; - /* endsas kills the session making it harder to fetch results */ - data _null_; - syswarningtext=symget('syswarningtext'); - syserrortext=symget('syserrortext'); - abort_msg=symget('msg'); - syscc=symget('syscc'); - sysuserid=symget('sysuserid'); - iftrue=symget('iftrue'); - put (_all_)(/=); - call symputx('syscc',0); - abort cancel nolist; - run; - %end; - %else %do; - %abort cancel; - %end; -%end; -%else %do; - %put _all_; - %abort cancel; -%end; -%mend mp_abort; - -/** @endcond */ diff --git a/009_macros/mp_aligndecimal.sas b/009_macros/mp_aligndecimal.sas deleted file mode 100644 index 5e90f59..0000000 --- a/009_macros/mp_aligndecimal.sas +++ /dev/null @@ -1,95 +0,0 @@ -/*** HELP START ***//** - @file - @brief Apply leading blanks to align numbers vertically in a char variable - @details This is particularly useful when storing numbers (as character) that - need to be sorted. - - It works by splitting the number left and right of the decimal place, and - aligning it accordingly. A temporary variable is created as part of this - process (which is automatically dropped) - - The macro can be used only in data step, eg as follows: - - data _null_; - length myvar $50; - do i=1 to 1000 by 50; - if mod(i,2)=0 then j=ranuni(0)*i*100; - else j=i*100; - - %mp_aligndecimal(myvar,width=7) - - leading_spaces=length(myvar)-length(cats(myvar)); - putlog +leading_spaces myvar; - end; - run; - - The generated code will look something like this: - - length aligndp4e49996 $7; - if index(myvar,'.') then do; - aligndp4e49996=cats(scan(myvar,1,'.')); - aligndp4e49996=right(aligndp4e49996); - myvar=aligndp4e49996!!'.'!!cats(scan(myvar,2,'.')); - end; - else do; - aligndp4e49996=myvar; - aligndp4e49996=right(aligndp4e49996); - myvar=aligndp4e49996; - end; - drop aligndp4e49996; - - Results (myvar variable): - - 0.7683559324 - 122.8232796 - 99419.50552 - 42938.5143414 - 763.3799189 - 15170.606073 - 15083.285773 - 85443.198707 - 2022999.2251 - 12038.658867 - 1350582.6734 - 52777.258221 - 11723.347628 - 33101.268376 - 6181622.8603 - 7390614.0669 - 73384.537893 - 1788362.1016 - 2774586.2219 - 7998580.8415 - - - @param [in] var The (data step, character) variable to modify - @param [in] width= (8) The number of characters BEFORE the decimal point - -

SAS Macros

- @li mf_getuniquename.sas - -

Related Programs

- @li mp_aligndecimal.test.sas - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mp_aligndecimal(var,width=8); - - %local tmpvar; - %let tmpvar=%mf_getuniquename(prefix=aligndp); - length &tmpvar $&width; - if index(&var,'.') then do; - &tmpvar=cats(scan(&var,1,'.')); - &tmpvar=right(&tmpvar); - &var=&tmpvar!!'.'!!cats(scan(&var,2,'.')); - end; - else do; - &tmpvar=cats(&var); - &tmpvar=right(&tmpvar); - &var=&tmpvar; - end; - drop &tmpvar; - -%mend mp_aligndecimal; diff --git a/009_macros/mp_appendfile.sas b/009_macros/mp_appendfile.sas deleted file mode 100644 index 4ebe478..0000000 --- a/009_macros/mp_appendfile.sas +++ /dev/null @@ -1,57 +0,0 @@ -/*** HELP START ***//** - @file - @brief Append (concatenate) two or more files. - @details Will append one more more `appendrefs` (filerefs) to a `baseref`. - Uses a binary mechanism, so will work with any file type. For that reason - - use with care! And supply your own trailing carriage returns in each file.. - - Usage: - - filename tmp1 temp; - filename tmp2 temp; - filename tmp3 temp; - data _null_; file tmp1; put 'base file'; - data _null_; file tmp2; put 'append1'; - data _null_; file tmp3; put 'append2'; - run; - %mp_appendfile(baseref=tmp1, appendrefs=tmp2 tmp3) - - - @param [in] baseref= (0) Fileref of the base file (should exist) - @param [in] appendrefs= (0) One or more filerefs to be appended to the base - fileref. Space separated. - - @version 9.2 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - @li mp_binarycopy.sas - - -**//*** HELP END ***/ - -%macro mp_appendfile( - baseref=0, - appendrefs=0 -)/*/STORE SOURCE*/; - -%mp_abort(iftrue= (&baseref=0) - ,mac=&sysmacroname - ,msg=%str(Baseref NOT specified!) -) -%mp_abort(iftrue= (&appendrefs=0) - ,mac=&sysmacroname - ,msg=%str(Appendrefs NOT specified!) -) - -%local i; -%do i=1 %to %sysfunc(countw(&appendrefs)); - %mp_abort(iftrue= (&syscc>0) - ,mac=&sysmacroname - ,msg=%str(syscc=&syscc) - ) - %mp_binarycopy(inref=%scan(&appendrefs,&i), outref=&baseref, mode=APPEND) -%end; - -%mend mp_appendfile; diff --git a/009_macros/mp_applyformats.sas b/009_macros/mp_applyformats.sas deleted file mode 100644 index 0e4f883..0000000 --- a/009_macros/mp_applyformats.sas +++ /dev/null @@ -1,181 +0,0 @@ -/*** HELP START ***//** - @file - @brief Apply a set of formats to a table - @details Applies a set of formats to the metadata of one or more SAS datasets. - Can be used to migrate formats from one table to another. The input table - must contain the following columns: - - @li lib - the libref of the table to be updated - @li ds - the dataset to be updated - @li var - the variable to be updated - @li fmt - the format to apply. Missing or default ($CHAR, 8.) formats are - ignored. - - The macro will abort in the following scenarios: - - @li Libref not assigned - @li Dataset does not exist - @li Input table contains null or invalid values - - Example usage: - - data work.example; - set sashelp.prdsale; - format _all_ clear; - run; - - %mp_getcols(sashelp.prdsale,outds=work.cols) - - data work.cols2; - set work.cols; - lib='WORK'; - ds='EXAMPLE'; - var=name; - fmt=format; - keep lib ds var fmt; - run; - - %mp_applyformats(work.cols2) - - @param [in] inds The input dataset containing the formats to apply (and where - to apply them). Example structure: - |LIB:$8.|DS:$32.|VAR:$32.|FMT:$49.| - |---|---|---|---| - |`WORK `|`EXAMPLE `|`ACTUAL `|`DOLLAR12.2 `| - |`WORK `|`EXAMPLE `|`COUNTRY `|`$CHAR10. `| - |`WORK `|`EXAMPLE `|`DIVISION `|`$CHAR10. `| - |`WORK `|`EXAMPLE `|`MONTH `|`MONNAME3. `| - |`WORK `|`EXAMPLE `|`PREDICT `|`DOLLAR12.2 `| - |`WORK `|`EXAMPLE `|`PRODTYPE `|`$CHAR10. `| - |`WORK `|`EXAMPLE `|`PRODUCT `|`$CHAR10. `| - |`WORK `|`EXAMPLE `|`QUARTER `|`8. `| - |`WORK `|`EXAMPLE `|`REGION `|`$CHAR10. `| - |`WORK `|`EXAMPLE `|`YEAR `|`8. `| - - @param [out] errds= (0) Provide a libds reference here to export the - error messages to a table. In this case, they will not be printed to the - log. - -

SAS Macros

- @li mf_getengine.sas - @li mf_getuniquefileref.sas - @li mf_getuniquename.sas - @li mf_nobs.sas - @li mp_validatecol.sas - - -

Related Macros

- @li mp_getformats.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_applyformats(inds,errds=0 -)/*/STORE SOURCE*/; -%local outds liblist i engine lib msg ; - -/** - * Validations - */ -proc sort data=&inds; - by lib ds var fmt; -run; - -%if &errds=0 %then %let outds=%mf_getuniquename(prefix=mp_applyformats); -%else %let outds=&errds; - -data &outds; - set &inds; - where fmt not in ('','.', '$', '$CHAR.','8.'); - length msg $128; - by lib ds var fmt; - if libref(lib) ne 0 then do; - msg=catx(' ','libref',lib,'is not assigned!'); - %if &errds=0 %then %do; - putlog 'ERR' +(-1) "OR: " msg; - %end; - output; - return; - end; - if exist(cats(lib,'.',ds)) ne 1 then do; - msg=catx(' ','libds',lib,'.',ds,'does not exist!'); - %if &errds=0 %then %do; - putlog 'ERR' +(-1) "OR: " msg; - %end; - output; - return; - end; - %mp_validatecol(fmt,FORMAT,is_fmt) - if is_fmt=0 then do; - msg=catx(' ','format',fmt,'on libds',lib,'.',ds,'.',var,'is not valid!'); - %if &errds=0 %then %do; - putlog 'ERR' +(-1) "OR: " msg; - %end; - output; - return; - end; - - if first.ds then do; - retain dsid; - dsid=open(cats(lib,'.',ds)); - if dsid=0 then do; - msg=catx(' ','libds',lib,'.',ds,' could not be opened!'); - %if &errds=0 %then %do; - putlog 'ERR' +(-1) "OR: " msg; - %end; - output; - return; - end; - if varnum(dsid,var)<1 then do; - msg=catx(' ','Variable',lib,'.',ds,'.',var,' was not found!'); - %if &errds=0 %then %do; - putlog 'ERR' +(-1) "OR: " msg; - %end; - output; - end; - end; - if last.ds then rc=close(dsid); -run; - -proc sql noprint; -select distinct lib into: liblist separated by ' ' from &inds; -%put &=liblist; -%if %length(&liblist)>0 %then %do i=1 %to %sysfunc(countw(&liblist)); - %let lib=%scan(&liblist,1); - %let engine=%mf_getengine(&lib); - %if &engine ne V9 and &engine ne BASE %then %do; - %let msg=&lib has &engine engine - formats cannot be applied; - insert into &outds set lib="&lib",ds="_all_",var="_all", msg="&msg" ; - %if &errds=0 %then %put %str(ERR)OR: &msg; - %end; -%end; -quit; - -%if %mf_nobs(&outds)>0 %then %return; - -/** - * Validations complete - now apply the actual formats! - */ -%let fref=%mf_getuniquefileref(); -data _null_; - set &inds; - by lib ds var fmt; - where fmt not in ('','.', '$', '$CHAR.','8.'); - file &fref; - if first.lib then put 'proc datasets nolist lib=' lib ';'; - if first.ds then put ' modify ' ds ';'; - put ' format ' var fmt ';'; - if last.ds then put ' run;'; - if last.lib then put 'quit;'; -run; - -%inc &fref/source2; - -%if &errds=0 %then %do; - proc sql; - drop table &outds; -%end; - -%mend mp_applyformats; diff --git a/009_macros/mp_assert.sas b/009_macros/mp_assert.sas deleted file mode 100644 index fe7614b..0000000 --- a/009_macros/mp_assert.sas +++ /dev/null @@ -1,56 +0,0 @@ -/*** HELP START ***//** - @file - @brief Generic assertion - @details Useful in the context of writing sasjs tests. The results of the - test are _appended_ to the &outds. table. - - Example usage: - - %mp_assert(iftrue=(1=1), - desc=Obviously true - ) - - %mp_assert(iftrue=(1=0), - desc=Will fail - ) - - @param [in] iftrue= (1=1) A condition where, if true, the test is a PASS. - Else, the test is a fail. - - @param [in] desc= (Testing observations) The user provided test description - @param [out] outds= (work.test_results) The output dataset to contain the - results. If it does not exist, it will be created, with the following format: - |TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256| - |---|---|---| - |User Provided description|PASS|Dataset &inds contained ALL columns| - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_assert(iftrue=(1=1), - desc=0, - outds=work.test_results -)/*/STORE SOURCE*/; - - data ; - length test_description $256 test_result $4 test_comments $256; - test_description=symget('desc'); - test_comments="&sysmacroname: Test result of "!!symget('iftrue'); - %if %eval(%unquote(&iftrue)) %then %do; - test_result='PASS'; - %end; - %else %do; - test_result='FAIL'; - %end; - run; - - %local ds ; - %let ds=&syslast; - proc append base=&outds data=&ds; - run; - proc sql; - drop table &ds; - -%mend mp_assert; diff --git a/009_macros/mp_assertcols.sas b/009_macros/mp_assertcols.sas deleted file mode 100644 index 4b4ee49..0000000 --- a/009_macros/mp_assertcols.sas +++ /dev/null @@ -1,145 +0,0 @@ -/*** HELP START ***//** - @file - @brief Asserts the existence (or not) of columns - @details Useful in the context of writing sasjs tests. The results of the - test are _appended_ to the &outds. table. - - Example usage: - - %mp_assertcols(sashelp.class, - cols=name age sex, - test=ALL, - desc=check all columns exist - ) - - %mp_assertcols(sashelp.class, - cols=a b c, - test=NONE - ) - - %mp_assertcols(sashelp.class, - cols=age depth, - test=ANY - ) - -

SAS Macros

- @li mf_existds.sas - @li mf_existvarlist.sas - @li mf_getvarlist.sas - @li mf_wordsinstr1butnotstr2.sas - @li mp_abort.sas - - - @param [in] inds The input library.dataset to test for values - @param [in] cols= (0) The list of columns to check for - @param [in] desc= (0) The user provided test description - @param [in] test= (ALL) The test to apply. Valid values are: - @li ALL - Test is a PASS if ALL columns exist in &inds - @li ANY - Test is a PASS if ANY of the columns exist in &inds - @li NONE - Test is a PASS if NONE of the columns exist in &inds - @param [out] outds= (work.test_results) The output dataset to contain the - results. If it does not exist, it will be created, with the following format: - |TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256| - |---|---|---| - |User Provided description|PASS|Dataset &inds contained ALL columns| - - -

Related Macros

- @li mp_assertdsobs.sas - @li mp_assertcolvals.sas - @li mp_assertdsobs.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_assertcols(inds, - cols=0, - test=ALL, - desc=0, - outds=work.test_results -)/*/STORE SOURCE*/; - - %mp_abort(iftrue= (&syscc ne 0) - ,mac=&sysmacroname - ,msg=%str(syscc=&syscc - on macro entry) - ) - - %local lib ds ; - %let lib=%scan(&inds,1,%str(.)); - %let ds=%scan(&inds,2,%str(.)); - %let cols=%upcase(&cols); - - %mp_abort(iftrue= (%mf_existds(&lib..&ds)=0) - ,mac=&sysmacroname - ,msg=%str(&lib..&ds not found!) - ) - - %mp_abort(iftrue= (&cols=0) - ,mac=&sysmacroname - ,msg=%str(No cols provided) - ) - - - %let test=%upcase(&test); - - %if &test ne ANY and &test ne ALL and &test ne NONE %then %do; - %mp_abort( - mac=&sysmacroname, - msg=%str(Invalid test - &test) - ) - %end; - - /** - * now do the actual test! - */ - %local result; - %if %mf_existVarList(&inds,&cols)=1 %then %let result=ALL; - %else %do; - %local targetcols compare; - %let targetcols=%upcase(%mf_getvarlist(&inds)); - %let compare=%mf_wordsinstr1butnotstr2( - Str1=&cols, - Str2=&targetcols - ); - %if %cmpres(&compare)=%cmpres(&cols) %then %let result=NONE; - %else %let result=SOME; - %end; - - data; - length test_description $256 test_result $4 test_comments $256; - test_description=symget('desc'); - if test_description='0' - then test_description="Testing &inds for existence of &test of: &cols"; - - test_result='FAIL'; - test_comments="&sysmacroname: &inds has &result columns "; - %if &test=ALL %then %do; - %if &result=ALL %then %do; - test_result='PASS'; - %end; - %end; - %else %if &test=ANY %then %do; - %if &result=SOME %then %do; - test_result='PASS'; - %end; - %end; - %else %if &test=NONE %then %do; - %if &result=NONE %then %do; - test_result='PASS'; - %end; - %end; - %else %do; - test_comments="&sysmacroname: Unsatisfied test condition - &test"; - %end; - run; - - %local ds; - %let ds=&syslast; - proc append base=&outds data=&ds; - run; - proc sql; - drop table &ds; - -%mend mp_assertcols; diff --git a/009_macros/mp_assertcolvals.sas b/009_macros/mp_assertcolvals.sas deleted file mode 100644 index e210a3b..0000000 --- a/009_macros/mp_assertcolvals.sas +++ /dev/null @@ -1,172 +0,0 @@ -/*** HELP START ***//** - @file - @brief Asserts the values in a column - @details Useful in the context of writing sasjs tests. The results of the - test are _appended_ to the &outds. table. - - Example usage: - - data work.checkds; - do checkval='Jane','James','Jill'; - output; - end; - run; - %mp_assertcolvals(sashelp.class.name, - checkvals=work.checkds.checkval, - desc=At least one value has a match, - test=ANYVAL - ) - - data work.check; - do val='M','F'; - output; - end; - run; - %mp_assertcolvals(sashelp.class.sex, - checkvals=work.check.val, - desc=All values have a match, - test=ALLVALS - ) - -

SAS Macros

- @li mf_existds.sas - @li mf_getuniquename.sas - @li mf_nobs.sas - @li mp_abort.sas - - - @param [in] indscol The input library.dataset.column to test for values - @param [in] checkvals= (0) A library.dataset.column value containing a UNIQUE - list of values to be compared against the source (indscol). - @param [in] desc= (Testing observations) The user provided test description - @param [in] test= (ALLVALS) The test to apply. Valid values are: - @li ALLVALS - Test is a PASS if ALL values have a match in checkvals - @li ANYVAL - Test is a PASS if at least 1 value has a match in checkvals - @li NOVAL - Test is a PASS if there are NO matches in checkvals - @param [out] outds= (work.test_results) The output dataset to contain the - results. If it does not exist, it will be created, with the following format: - |TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256| - |---|---|---| - |User Provided description|PASS|Column &indscol contained ALL target vals| - - -

Related Macros

- @li mp_assertdsobs.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_assertcolvals(indscol, - checkvals=0, - test=ALLVALS, - desc=mp_assertcolvals - no desc provided, - outds=work.test_results -)/*/STORE SOURCE*/; - - %mp_abort(iftrue= (&syscc ne 0) - ,mac=&sysmacroname - ,msg=%str(syscc=&syscc - on macro entry) - ) - - %local lib ds col clib cds ccol nobs; - %let lib=%scan(&indscol,1,%str(.)); - %let ds=%scan(&indscol,2,%str(.)); - %let col=%scan(&indscol,3,%str(.)); - %mp_abort(iftrue= (%mf_existds(&lib..&ds)=0) - ,mac=&sysmacroname - ,msg=%str(&lib..&ds not found!) - ) - - %mp_abort(iftrue= (&checkvals=0) - ,mac=&sysmacroname - ,msg=%str(Set CHECKVALS to a library.dataset.column containing check vals) - ) - %let clib=%scan(&checkvals,1,%str(.)); - %let cds=%scan(&checkvals,2,%str(.)); - %let ccol=%scan(&checkvals,3,%str(.)); - %mp_abort(iftrue= (%mf_existds(&clib..&cds)=0) - ,mac=&sysmacroname - ,msg=%str(&clib..&cds not found!) - ) - %let nobs=%mf_nobs(&clib..&cds); - %mp_abort(iftrue= (&nobs=0) - ,mac=&sysmacroname - ,msg=%str(&clib..&cds is empty!) - ) - - %let test=%upcase(&test); - - %if &test ne ALLVALS and &test ne ANYVAL and &test ne NOVAL %then %do; - %mp_abort( - mac=&sysmacroname, - msg=%str(Invalid test - &test) - ) - %end; - - %local result orig; - %let result=-1; - %let orig=-1; - proc sql noprint; - select count(*) into: result trimmed - from &lib..&ds - where &col not in ( - select &ccol from &clib..&cds - ); - select count(*) into: orig trimmed from &lib..&ds; - quit; - - %local notfound tmp1 tmp2; - %let tmp1=%mf_getuniquename(); - %let tmp2=%mf_getuniquename(); - - /* this is a bit convoluted - but using sql outobs=10 throws warnings */ - proc sql noprint; - create view &tmp1 as - select distinct &col - from &lib..&ds - where &col not in ( - select &ccol from &clib..&cds - ); - data &tmp2; - set &tmp1; - if _n_>10 then stop; - run; - proc sql; - select distinct &col into: notfound separated by ' ' from &tmp2; - - - %mp_abort(iftrue= (&syscc ne 0) - ,mac=&sysmacroname - ,msg=%str(syscc=&syscc after macro query) - ) - - data; - length test_description $256 test_result $4 test_comments $256; - test_description=symget('desc'); - test_result='FAIL'; - test_comments="&sysmacroname: &lib..&ds..&col has &result/&orig values " - !!"not in &clib..&cds..&ccol.. First 10 vals:"!!symget('notfound'); - %if &test=ANYVAL %then %do; - if &result < &orig then test_result='PASS'; - %end; - %else %if &test=ALLVALS %then %do; - if &result=0 then test_result='PASS'; - %end; - %else %if &test=NOVAL %then %do; - if &result=&orig then test_result='PASS'; - %end; - %else %do; - test_comments="&sysmacroname: Unsatisfied test condition - &test"; - %end; - run; - - %local ds; - %let ds=&syslast; - proc append base=&outds data=&ds; - run; - proc sql; - drop table &ds; - -%mend mp_assertcolvals; diff --git a/009_macros/mp_assertdsobs.sas b/009_macros/mp_assertdsobs.sas deleted file mode 100644 index ad49cd5..0000000 --- a/009_macros/mp_assertdsobs.sas +++ /dev/null @@ -1,121 +0,0 @@ -/*** HELP START ***//** - @file - @brief Asserts the number of observations in a dataset - @details Useful in the context of writing sasjs tests. The results of the - test are _appended_ to the &outds. table. - - Example usage: - - %mp_assertdsobs(sashelp.class) %* tests if any observations are present; - - %mp_assertdsobs(sashelp.class,test=ATLEAST 10) %* pass if >9 obs present; - - %mp_assertdsobs(sashelp.class,test=ATMOST 20) %* pass if <21 obs present; - - - @param [in] inds input dataset to test for presence of observations - @param [in] desc= (Testing observations) The user provided test description - @param [in] test= (HASOBS) The test to apply. Valid values are: - @li HASOBS - Test is a PASS if the input dataset has any observations - @li EMPTY - Test is a PASS if input dataset is empty - @li EQUALS [integer] - Test passes if row count matches the provided integer - @li ATLEAST [integer] - Test passes if row count is more than or equal to - the provided integer - @li ATMOST [integer] - Test passes if row count is less than or equal to - the provided integer - @param [out] outds= (work.test_results) The output dataset to contain the - results. If it does not exist, it will be created, with the following format: - |TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256| - |---|---|---| - |User Provided description|PASS|Dataset &inds has XX obs| - -

SAS Macros

- @li mf_getuniquename.sas - @li mf_nobs.sas - @li mp_abort.sas - -

Related Macros

- @li mp_assertcolvals.sas - @li mp_assert.sas - @li mp_assertcols.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_assertdsobs(inds, - test=HASOBS, - desc=Testing observations, - outds=work.test_results -)/*/STORE SOURCE*/; - - %local nobs ds; - %let nobs=%mf_nobs(&inds); - %let test=%upcase(&test); - %let ds=%mf_getuniquename(prefix=mp_assertdsobs); - - %if %substr(&test.xxxxx,1,6)=EQUALS %then %do; - %let val=%scan(&test,2,%str( )); - %mp_abort(iftrue= (%DATATYP(&val)=CHAR) - ,mac=&sysmacroname - ,msg=%str(Invalid test - &test, expected EQUALS [integer]) - ) - %let test=EQUALS; - %end; - %else %if %substr(&test.xxxxxxx,1,7)=ATLEAST %then %do; - %let val=%scan(&test,2,%str( )); - %mp_abort(iftrue= (%DATATYP(&val)=CHAR) - ,mac=&sysmacroname - ,msg=%str(Invalid test - &test, expected ATLEAST [integer]) - ) - %let test=ATLEAST; - %end; - %else %if %substr(&test.xxxxxxx,1,7)=ATMOST %then %do; - %let val=%scan(&test,2,%str( )); - %mp_abort(iftrue= (%DATATYP(&val)=CHAR) - ,mac=&sysmacroname - ,msg=%str(Invalid test - &test, expected ATMOST [integer]) - ) - %let test=ATMOST; - %end; - %else %if &test ne HASOBS and &test ne EMPTY %then %do; - %mp_abort( - mac=&sysmacroname, - msg=%str(Invalid test - &test) - ) - %end; - - data &ds; - length test_description $256 test_result $4 test_comments $256; - test_description=symget('desc'); - test_result='FAIL'; - test_comments="&sysmacroname: Dataset &inds has &nobs observations."; - test_comments=test_comments!!" Test was "!!symget('test'); - %if &test=HASOBS %then %do; - if &nobs>0 then test_result='PASS'; - %end; - %else %if &test=EMPTY %then %do; - if &nobs=0 then test_result='PASS'; - %end; - %else %if &test=EQUALS %then %do; - if &nobs=&val then test_result='PASS'; - %end; - %else %if &test=ATLEAST %then %do; - if &nobs ge &val then test_result='PASS'; - %end; - %else %if &test=ATMOST %then %do; - if &nobs le &val then test_result='PASS'; - %end; - %else %do; - test_comments="&sysmacroname: Unsatisfied test condition - &test"; - %end; - run; - - proc append base=&outds data=&ds; - run; - - proc sql; - drop table &ds; - -%mend mp_assertdsobs; diff --git a/009_macros/mp_assertscope.sas b/009_macros/mp_assertscope.sas deleted file mode 100644 index a92fd4c..0000000 --- a/009_macros/mp_assertscope.sas +++ /dev/null @@ -1,147 +0,0 @@ -/*** HELP START ***//** - @file - @brief Used to capture scope leakage of macro variables - @details - - A common 'difficult to detect' bug in macros is where a nested macro - over-writes variables in a higher level macro. - - This assertion takes a snapshot of the macro variables before and after - a macro invocation. Differences are captured in the `&outds` table. This - makes it easy to detect whether any macro variables were modified or - changed. - - The following variables are NOT tested (as they are known, global variables - used in SASjs): - - @li &sasjs_prefix._FUNCTIONS - - Global variables are initialised in mp_init.sas - which will also trigger - "strict mode" in your SAS session. Whilst this is a default in SASjs - produced apps, if you prefer not to use this mode, simply instantiate the - following variable to prevent the macro from running: `SASJS_PREFIX` - - Example usage: - - %mp_assertscope(SNAPSHOT) - - %let oops=I did it again; - - %mp_assertscope(COMPARE, - desc=Checking macro variables against previous snapshot - ) - - This macro is designed to work alongside `sasjs test` - for more information - about this facility, visit [cli.sasjs.io/test](https://cli.sasjs.io/test). - - @param [in] action (SNAPSHOT) The action to take. Valid values: - @li SNAPSHOT - take a copy of the current macro variables - @li COMPARE - compare the current macro variables against previous values - @param [in] scope= (GLOBAL) The scope of the variables to be checked. This - corresponds to the values in the SCOPE column in `sashelp.vmacro`. - @param [in] desc= (Testing scope leakage) The user provided test description - @param [in] ignorelist= Provide a list of macro variable names to ignore from - the comparison - @param [in,out] scopeds= (work.mp_assertscope) The dataset to contain the - scope snapshot - @param [out] outds= (work.test_results) The output dataset to contain the - results. If it does not exist, it will be created, with the following format: - |TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256| - |---|---|---| - |User Provided description|PASS|No out of scope variables created or modified| - -

SAS Macros

- @li mf_getquotedstr.sas - @li mp_init.sas - -

Related Macros

- @li mp_assert.sas - @li mp_assertcols.sas - @li mp_assertcolvals.sas - @li mp_assertdsobs.sas - @li mp_assertscope.test.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_assertscope(action, - desc=Testing Scope Leakage, - scope=GLOBAL, - scopeds=work.mp_assertscope, - ignorelist=, - outds=work.test_results -)/*/STORE SOURCE*/; -%local ds test_result test_comments del add mod ilist; -%let ilist=%upcase(&sasjs_prefix._FUNCTIONS SYS_PROCHTTP_STATUS_CODE - SYS_PROCHTTP_STATUS_CODE SYS_PROCHTTP_STATUS_PHRASE &ignorelist); - -/** - * this sets up the global vars, it will also enter STRICT mode. If this - * behaviour is not desired, simply initiate the following global macro - * variable to prevent the macro from running: SASJS_PREFIX - */ -%mp_init() - -/* get current variables */ -%if &action=SNAPSHOT %then %do; - proc sql; - create table &scopeds as - select name,offset,value - from dictionary.macros - where scope="&scope" and upcase(name) not in (%mf_getquotedstr(&ilist)) - order by name,offset; -%end; -%else %if &action=COMPARE %then %do; - - proc sql; - create table _data_ as - select name,offset,value - from dictionary.macros - where scope="&scope" and upcase(name) not in (%mf_getquotedstr(&ilist)) - order by name,offset; - - %let ds=&syslast; - - proc compare - base=&scopeds(where=(upcase(name) not in (%mf_getquotedstr(&ilist)))) - compare=&ds noprint; - run; - - %if &sysinfo=0 %then %do; - %let test_result=PASS; - %let test_comments=&scope Variables Unmodified; - %end; - %else %do; - proc sql noprint undo_policy=none; - select distinct name into: del separated by ' ' from &scopeds - where name not in (select name from &ds); - select distinct name into: add separated by ' ' from &ds - where name not in (select name from &scopeds); - select distinct a.name into: mod separated by ' ' - from &scopeds a - inner join &ds b - on a.name=b.name - and a.offset=b.offset - where a.value ne b.value; - %let test_result=FAIL; - %let test_comments=%str(Mod:(&mod) Add:(&add) Del:(&del)); - %end; - - - data ; - length test_description $256 test_result $4 test_comments $256; - test_description=symget('desc'); - test_comments=symget('test_comments'); - test_result=symget('test_result'); - run; - - %let ds=&syslast; - proc append base=&outds data=&ds; - run; - proc sql; - drop table &ds; -%end; - -%mend mp_assertscope; diff --git a/009_macros/mp_base64copy.sas b/009_macros/mp_base64copy.sas deleted file mode 100644 index 1dafeeb..0000000 --- a/009_macros/mp_base64copy.sas +++ /dev/null @@ -1,117 +0,0 @@ -/*** HELP START ***//** - @file - @brief Convert a file to/from base64 format - @details Creates a new version of a file either encoded or decoded using - Base64. Inspired by this post by Michael Dixon: - https://support.selerity.com.au/hc/en-us/articles/223345708-Tip-SAS-and-Base64 - - Usage: - - filename tmp temp; - data _null_; - file tmp; - put 'base ik ally'; - run; - %mp_base64copy(inref=tmp, outref=myref, action=ENCODE) - - data _null_; - infile myref; - input; - put _infile_; - run; - - %mp_base64copy(inref=myref, outref=mynewref, action=DECODE) - - data _null_; - infile mynewref; - input; - put _infile_; - run; - - @param [in] inref= (0) Fileref of the input file (should exist) - @param [out] outref= (0) Output fileref. If it does not exist, it is created. - @param [in] action= (ENCODE) The action to take. Valid values: - @li ENCODE - Convert the file to base64 format - @li DECODE - Decode the file from base64 format - - @version 9.2 - @author Allan Bowe, source: https://github.com/sasjs/core - -

SAS Macros

- @li mp_abort.sas - - -**//*** HELP END ***/ - -%macro mp_base64copy( - inref=0, - outref=0, - action=ENCODE -)/*/STORE SOURCE*/; - -%let inref=%upcase(&inref); -%let outref=%upcase(&outref); -%let action=%upcase(&action); -%local infound outfound; -%let infound=0; -%let outfound=0; -data _null_; - set sashelp.vextfl(where=(fileref="&inref" or fileref="&outref")); - if fileref="&inref" then call symputx('infound',1,'l'); - if fileref="&outref" then call symputx('outfound',1,'l'); -run; - -%mp_abort(iftrue= (&infound=0) - ,mac=&sysmacroname - ,msg=%str(INREF &inref NOT FOUND!) -) -%mp_abort(iftrue= (&outref=0) - ,mac=&sysmacroname - ,msg=%str(OUTREF NOT PROVIDED!) -) -%mp_abort(iftrue= (&action ne ENCODE and &action ne DECODE) - ,mac=&sysmacroname - ,msg=%str(Invalid action! Should be ENCODE OR DECODE) -) - -%if &outfound=0 %then %do; - filename &outref temp lrecl=2097088; -%end; - -%if &action=ENCODE %then %do; - data _null_; - length b64 $ 76 line $ 57; - retain line ""; - infile &inref recfm=F lrecl= 1 end=eof; - input @1 stream $char1.; - file &outref recfm=N; - substr(line,(_N_-(CEIL(_N_/57)-1)*57),1) = byte(rank(stream)); - if mod(_N_,57)=0 or EOF then do; - if eof then b64=put(trim(line),$base64X76.); - else b64=put(line, $base64X76.); - put b64 + (-1) @; - line=""; - end; - run; -%end; -%else %if &action=DECODE %then %do; - data _null_; - length filein 8 fileout 8; - filein = fopen("&inref",'I',4,'B'); - fileout = fopen("&outref",'O',3,'B'); - char= '20'x; - do while(fread(filein)=0); - length raw $4; - do i=1 to 4; - rc=fget(filein,char,1); - substr(raw,i,1)=char; - end; - rc = fput(fileout,input(raw,$base64X4.)); - rc = fwrite(fileout); - end; - rc = fclose(filein); - rc = fclose(fileout); - run; -%end; - -%mend mp_base64copy; diff --git a/009_macros/mp_binarycopy.sas b/009_macros/mp_binarycopy.sas deleted file mode 100644 index 4d35417..0000000 --- a/009_macros/mp_binarycopy.sas +++ /dev/null @@ -1,80 +0,0 @@ -/*** HELP START ***//** - @file - @brief Copy any file using binary input / output streams - @details Reads in a file byte by byte and writes it back out. Is an - os-independent method to copy files. In case of naming collision, the - default filerefs can be modified. - Note that if you have a new enough version of SAS, and you don't need features - such as APPEND, you may be better of using the fcopy() function instead. - - %mp_binarycopy(inloc="/home/me/blah.txt", outref=_webout) - - To append to a file, use the mode option, eg: - - filename tmp1 temp; - filename tmp2 temp; - data _null_; - file tmp1; - put 'stacking'; - run; - - %mp_binarycopy(inref=tmp1, outref=tmp2, mode=APPEND) - %mp_binarycopy(inref=tmp1, outref=tmp2, mode=APPEND) - - - @param [in] inloc= () quoted "path/and/filename.ext" of the file to be copied - @param [out] outloc= () quoted "path/and/filename.ext" of the file to create - @param [in] inref= (____in) If provided, this fileref takes precedence over - the `inloc` parameter - @param [out] outref= (____in) If provided, this fileref takes precedence - over the `outloc` parameter. It must already exist! - @param [in] mode= (CREATE) Valid values: - @li CREATE - Create the file (even if it already exists) - @li APPEND - Append to the file (don't overwrite) - @param [in] iftrue= (1=1) - Supply a condition for which the macro should be executed - - @returns nothing - - @version 9.2 - -**//*** HELP END ***/ - -%macro mp_binarycopy( - inloc= /* full path and filename of the object to be copied */ - ,outloc= /* full path and filename of object to be created */ - ,inref=____in /* override default to use own filerefs */ - ,outref=____out /* override default to use own filerefs */ - ,mode=CREATE - ,iftrue=%str(1=1) -)/*/STORE SOURCE*/; - %local mod; - - %if not(%eval(%unquote(&iftrue))) %then %return; - - %if &mode=APPEND %then %let mod=mod; - - /* these IN and OUT filerefs can point to anything */ - %if &inref = ____in %then %do; - filename &inref &inloc lrecl=1048576 ; - %end; - %if &outref=____out %then %do; - filename &outref &outloc lrecl=1048576 &mod; - %end; - - /* copy the file byte-for-byte */ - data _null_; - infile &inref lrecl=1 recfm=n; - file &outref &mod recfm=n; - input sourcechar $char1. @@; - format sourcechar hex2.; - put sourcechar char1. @@; - run; - - %if &inref = ____in %then %do; - filename &inref clear; - %end; - %if &outref=____out %then %do; - filename &outref clear; - %end; -%mend mp_binarycopy; diff --git a/009_macros/mp_chop.sas b/009_macros/mp_chop.sas deleted file mode 100644 index e51e8cf..0000000 --- a/009_macros/mp_chop.sas +++ /dev/null @@ -1,195 +0,0 @@ -/*** HELP START ***//** - @file - @brief Splits a file of ANY SIZE by reference to a search string. - @details Provide a fileref and a search string to chop off part of a file. - - Works by reading in the file byte by byte, then marking the beginning and end - of each matched string, before finally doing the chop. - - Choose whether to keep the FIRST or the LAST section of the file. Optionally, - use an OFFSET to fix the precise chop point. - - Usage: - - %let src="%sysfunc(pathname(work))/file.txt"; - %let str=Chop here!; - %let out1="%sysfunc(pathname(work))/file1.txt"; - %let out2="%sysfunc(pathname(work))/file2.txt"; - %let out3="%sysfunc(pathname(work))/file3.txt"; - %let out4="%sysfunc(pathname(work))/file4.txt"; - - data _null_; - file &src; - put "startsection&str.endsection"; - run; - - %mp_chop(&src, matchvar=str, keep=FIRST, outfile=&out1) - %mp_chop(&src, matchvar=str, keep=LAST, outfile=&out2) - %mp_chop(&src, matchvar=str, keep=FIRST, matchpoint=END, outfile=&out3) - %mp_chop(&src, matchvar=str, keep=LAST, matchpoint=END, outfile=&out4) - - filename results (&out1 &out2 &out3 &out4); - data _null_; - infile results; - input; - list; - run; - - Results: - @li `startsection` - @li `Chop here!endsection` - @li `startsectionChop here!` - @li `endsection` - - For more examples, see mp_chop.test.sas - - @param [in] infile The QUOTED path to the file on which to perform the chop - @param [in] matchvar= () Macro variable NAME containing the string to split by - @param [in] matchpoint= (START) Valid values: - @li START - chop at the beginning of the string in `matchvar`. - @li END - chop at the end of the string in `matchvar`. - @param [in] offset= (0) An adjustment to the precise chop location, by - by reference to the `matchpoint`. Should be a positive or negative integer. - @param [in] keep= (FIRST) Valid values: - @li FIRST - keep the section of the file before the chop - @li LAST - keep the section of the file after the chop - @param [in] mdebug= (0) Set to 1 to provide macro debugging - @param [out] outfile= (0) - Optional QUOTED path to the adjusted output file (avoids - overwriting the first file). - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mf_getuniquename.sas - -

Related Macros

- @li mp_abort.sas - @li mp_gsubfile.sas - @li mp_replace.sas - @li mp_chop.test.sas - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_chop(infile, - matchvar=, - matchpoint=START, - keep=FIRST, - offset=0, - mdebug=0, - outfile=0 -)/*/STORE SOURCE*/; - -%local fref0 dttm ds1 outref; -%let fref0=%mf_getuniquefileref(); -%let ds1=%mf_getuniquename(prefix=allchars); -%let ds2=%mf_getuniquename(prefix=startmark); - -%if &outfile=0 %then %let outfile=&infile; - -%mp_abort(iftrue= (%length(%superq(&matchvar))=0) - ,mac=mp_chop.sas - ,msg=%str(&matchvar is an empty variable) -) - -/* START */ -%let dttm=%sysfunc(datetime()); - -filename &fref0 &infile lrecl=1 recfm=n; - -/* create dataset with one char per row */ -data &ds1; - infile &fref0; - input sourcechar $char1. @@; - format sourcechar hex2.; -run; - -/* get start & stop position of first matchvar string (one row, two vars) */ -data &ds2; - /* set find string to length in bytes to cover trailing spaces */ - length string $ %length(%superq(&matchvar)); - string =symget("&matchvar"); - drop string; - - firstchar=char(string,1); - findlen=lengthm(string); /* <- for trailing bytes */ - - do _N_=1 to nobs; - set &ds1 nobs=nobs point=_N_; - if sourcechar=firstchar then do; - pos=1; - s=0; - do point=_N_ to min(_N_ + findlen -1,nobs); - set &ds1 point=point; - if sourcechar=char(string, pos) then s + 1; - else goto _leave_; - pos+1; - end; - _leave_: - if s=findlen then do; - START =_N_; - _N_ =_N_+ s - 1; - STOP =_N_; - output; - /* matched! */ - stop; - end; - end; - end; - stop; - keep START STOP; -run; - -%local split; -%let split=0; -data _null_; - set &ds2; - if "&matchpoint"='START' then do; - if "&keep"='FIRST' then mp=start; - else if "&keep"='LAST' then mp=start-1; - end; - else if "&matchpoint"='END' then do; - if "&keep"='FIRST' then mp=stop+1; - else if "&keep"='LAST' then mp=stop; - end; - split=mp+&offset; - call symputx('split',split,'l'); -%if &mdebug=1 %then %do; - put (_all_)(=); - %put &=offset; -%end; -run; -%if &split=0 %then %do; - %put &sysmacroname: No match found in &infile for string %superq(&matchvar); - %return; -%end; - -data _null_; - file &outfile recfm=n; - set &ds1; -%if &keep=FIRST %then %do; - if _n_ ge &split then stop; -%end; -%else %do; - if _n_ gt &split; -%end; - put sourcechar char1.; -run; - -%if &mdebug=0 %then %do; - filename &fref0 clear; -%end; -%else %do; - data _null_; - infile &outfile lrecl=32767; - input; - list; - if _n_>200 then stop; - run; -%end; -/* END */ -%put &sysmacroname took %sysevalf(%sysfunc(datetime())-&dttm) seconds to run; - -%mend mp_chop; diff --git a/009_macros/mp_cleancsv.sas b/009_macros/mp_cleancsv.sas deleted file mode 100644 index 9f749c9..0000000 --- a/009_macros/mp_cleancsv.sas +++ /dev/null @@ -1,79 +0,0 @@ -/*** HELP START ***//** - @file - @brief Fixes embedded cr / lf / crlf in CSV - @details CSVs will sometimes contain lf or crlf within quotes (eg when - saved by excel). When the termstr is ALSO lf or crlf that can be tricky - to process using SAS defaults. - This macro converts any csv to follow the convention of a windows excel file, - applying CRLF line endings and converting embedded cr and crlf to lf. - - Usage: - - fileref mycsv "/path/your/csv"; - %mp_cleancsv(in=mycsv,out=/path/new.csv) - - @param [in] in= (NOTPROVIDED) - Provide path or fileref to input csv. If a period is - found, it is assumed to be a file. - @param [in] out= (NOTPROVIDED) Output path or fileref to output csv. - If a period is found, it is assumed to be a file. - @param [in] qchar= ('22'x) Quote char - hex code 22 is the double quote. - - @version 9.2 - @author Allan Bowe - @cond -**//*** HELP END ***/ - -%macro mp_cleancsv(in=NOTPROVIDED,out=NOTPROVIDED,qchar='22'x); -%if "&in"="NOTPROVIDED" or "&out"="NOTPROVIDED" %then %do; - %put %str(ERR)OR: Please provide valid input (&in) & output (&out) locations; - %return; -%end; - -/* presence of a period(.) indicates a physical location */ -%if %index(&in,.) %then %let in="&in"; -%if %index(&out,.) %then %let out="&out"; - -/** - * convert all cr and crlf within quotes to lf - * convert all other cr or lf to crlf - */ - data _null_; - infile &in recfm=n ; - file &out recfm=n; - retain isq iscrlf 0 qchar &qchar; - input inchar $char1. ; - if inchar=qchar then isq = mod(isq+1,2); - if isq then do; - /* inside a quote change cr and crlf to lf */ - if inchar='0D'x then do; - put '0A'x; - input inchar $char1.; - if inchar ne '0A'x then do; - put inchar $char1.; - if inchar=qchar then isq = mod(isq+1,2); - end; - end; - else put inchar $char1.; - end; - else do; - /* outside a quote, change cr and lf to crlf */ - if inchar='0D'x then do; - crblank: - put '0D0A'x; - input inchar $char1.; - if inchar='0D'x then do; - /* multiple CR indicates CR formatted file with blank lines */ - goto crblank; - end; - else if inchar ne '0A'x then do; - put inchar $char1.; - if inchar=qchar then isq = mod(isq+1,2); - end; - end; - else if inchar='0A'x then put '0D0A'x; - else put inchar $char1.; - end; - run; -%mend mp_cleancsv; -/** @endcond */ diff --git a/009_macros/mp_copyfolder.sas b/009_macros/mp_copyfolder.sas deleted file mode 100644 index b96b477..0000000 --- a/009_macros/mp_copyfolder.sas +++ /dev/null @@ -1,84 +0,0 @@ -/*** HELP START ***//** - @file - @brief A macro to recursively copy a directory - @details Performs a recursive directory listing then works from top to bottom - copying files and creating subdirectories. - - Usage: - - %let rootdir=%sysfunc(pathname(work))/demo; - %let copydir=%sysfunc(pathname(work))/demo_copy; - %mf_mkdir(&rootdir) - %mf_mkdir(&rootdir/subdir) - %mf_mkdir(&rootdir/subdir/subsubdir) - data "&rootdir/subdir/example.sas7bdat"; - run; - - %mp_copyfolder(&rootdir,©dir) - - @param [in] source Unquoted path to the folder to copy from. - @param [out] target Unquoted path to the folder to copy to. - @param [in] copymax= (MAX) Set to a positive integer to indicate the level of - subdirectory copy recursion - eg 3, to go `./3/levels/deep`. For unlimited - recursion, set to MAX. - -

SAS Macros

- @li mf_getuniquename.sas - @li mf_isdir.sas - @li mf_mkdir.sas - @li mp_abort.sas - @li mp_dirlist.sas - -

Related Macros

- @li mp_copyfolder.test.sas - -**//*** HELP END ***/ - -%macro mp_copyfolder(source,target,copymax=MAX); - - %mp_abort(iftrue=(%mf_isdir(&source)=0) - ,mac=&sysmacroname - ,msg=%str(Source dir does not exist (&source)) - ) - - %mf_mkdir(&target) - - %mp_abort(iftrue=(%mf_isdir(&target)=0) - ,mac=&sysmacroname - ,msg=%str(Target dir could not be created (&target)) - ) - - /* prep temp table */ - %local tempds; - %let tempds=%mf_getuniquename(); - - /* recursive directory listing */ - %mp_dirlist(path=&source,outds=work.&tempds,maxdepth=©max) - - /* create folders and copy content */ - data _null_; - length msg $200; - call missing(msg); - set work.&tempds; - if _n_ = 1 then dpos+sum(length(directory),2); - filepath2="&target/"!!substr(filepath,dpos); - if file_or_folder='folder' then call execute('%mf_mkdir('!!filepath2!!')'); - else do; - length fref1 fref2 $8; - rc1=filename(fref1,filepath,'disk','recfm=n'); - rc2=filename(fref2,filepath2,'disk','recfm=n'); - if fcopy(fref1,fref2) ne 0 then do; - msg=sysmsg(); - putlog 'ERR' +(-1) "OR: Unable to copy " filepath " to " filepath2; - putlog msg=; - end; - end; - rc=filename(fref1); - rc=filename(fref2); - run; - - /* tidy up */ - proc sql; - drop table work.&tempds; - -%mend mp_copyfolder; diff --git a/009_macros/mp_coretable.sas b/009_macros/mp_coretable.sas deleted file mode 100644 index 33a5f3c..0000000 --- a/009_macros/mp_coretable.sas +++ /dev/null @@ -1,73 +0,0 @@ -/*** HELP START ***//** - @file - @brief Create the permanent Core tables - @details Several macros in the [core](https://github.com/sasjs/core) library - make use of permanent tables. To avoid duplication in definitions, this - macro provides a central location for managing the corresponding DDL. - - Note - this macro is likely to be deprecated in future in favour of a - dedicated "datamodel" folder (prefix mddl) - - Any corresponding data would go in a seperate repo, to avoid this one - ballooning in size! - - Example usage: - - %mp_coretable(LOCKTABLE,libds=work.locktable) - - @param [in] table_ref The type of table to create. Example values: - @li DIFFTABLE - @li FILTER_DETAIL - @li FILTER_SUMMARY - @li LOCKANYTABLE - @li MAXKEYTABLE - @param [in] libds= (0) The library.dataset reference used to create the table. - If not provided, then the DDL is simply printed to the log. - -

SAS Macros

- @li mddl_dc_difftable.sas - @li mddl_dc_filterdetail.sas - @li mddl_dc_filtersummary.sas - @li mddl_dc_locktable.sas - @li mddl_dc_maxkeytable.sas - @li mf_getuniquename.sas - -

Related Macros

- @li mp_filterstore.sas - @li mp_lockanytable.sas - @li mp_retainedkey.sas - @li mp_storediffs.sas - @li mp_stackdiffs.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_coretable(table_ref,libds=0 -)/*/STORE SOURCE*/; -%local outds ; -%let outds=%sysfunc(ifc(&libds=0,%mf_getuniquename(),&libds)); -proc sql; -%if &table_ref=DIFFTABLE %then %do; - %mddl_dc_difftable(libds=&outds) -%end; -%else %if &table_ref=LOCKTABLE %then %do; - %mddl_dc_locktable(libds=&outds) -%end; -%else %if &table_ref=FILTER_SUMMARY %then %do; - %mddl_dc_filtersummary(libds=&outds) -%end; -%else %if &table_ref=FILTER_DETAIL %then %do; - %mddl_dc_filterdetail(libds=&outds) -%end; -%else %if &table_ref=MAXKEYTABLE %then %do; - %mddl_dc_maxkeytable(libds=&outds) -%end; - -%if &libds=0 %then %do; - proc sql; - describe table &syslast; - drop table &syslast; -%end; -%mend mp_coretable; diff --git a/009_macros/mp_createconstraints.sas b/009_macros/mp_createconstraints.sas deleted file mode 100644 index fcc76db..0000000 --- a/009_macros/mp_createconstraints.sas +++ /dev/null @@ -1,70 +0,0 @@ -/*** HELP START ***//** - @file mp_createconstraints.sas - @brief Creates constraints - @details Takes the output from mp_getconstraints.sas as input - - 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 unq unique(tx_from, dd_type), - constraint nnn not null(DD_SHORTDESC) - ); - - %mp_getconstraints(lib=work,ds=example,outds=work.constraints) - %mp_deleteconstraints(inds=work.constraints,outds=dropped,execute=YES) - %mp_createconstraints(inds=work.constraints,outds=created,execute=YES) - - @param [in] inds= (work.mp_getconstraints) The input table containing the - constraint info - @param [out] outds= (work.mp_createconstraints) A table containing the create - statements (create_statement column) - @param [in] execute= (NO) To actually create, use YES. - -

Related Files

- @li mp_getconstraints.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_createconstraints(inds=mp_getconstraints - ,outds=work.mp_createconstraints - ,execute=NO -)/*/STORE SOURCE*/; - -proc sort data=&inds out=&outds; - by libref table_name constraint_name; -run; - -data &outds; - set &outds; - by libref table_name constraint_name; - length create_statement $500; - if _n_=1 and "&execute"="YES" then call execute('proc sql;'); - if first.constraint_name then do; - if constraint_type='PRIMARY' then type='PRIMARY KEY'; - else type=constraint_type; - create_statement=catx(" ","alter table",libref,".",table_name - ,"add constraint",constraint_name,type,"("); - if last.constraint_name then - create_statement=cats(create_statement,column_name,");"); - else create_statement=cats(create_statement,column_name,","); - if "&execute"="YES" then call execute(create_statement); - end; - else if last.constraint_name then do; - create_statement=cats(column_name,");"); - if "&execute"="YES" then call execute(create_statement); - end; - else do; - create_statement=cats(column_name,","); - if "&execute"="YES" then call execute(create_statement); - end; - output; -run; - -%mend mp_createconstraints; diff --git a/009_macros/mp_createwebservice.sas b/009_macros/mp_createwebservice.sas deleted file mode 100644 index 423a0b1..0000000 --- a/009_macros/mp_createwebservice.sas +++ /dev/null @@ -1,32 +0,0 @@ -/*** HELP START ***//** - @file mp_createwebservice.sas - @brief Create a web service in SAS 9, Viya or SASjs Server (legacy) - @details This is actually a wrapper for mx_createwebservice.sas, remaining - for legacy purposes. For new apps, use mx_createwebservice.sas. - - -

SAS Macros

- @li mx_createwebservice.sas - - -**//*** HELP END ***/ - -%macro mp_createwebservice(path=HOME - ,name=initService - ,precode= - ,code=ft15f001 - ,desc=This service was created by the mp_createwebservice macro - ,replace=YES - ,mdebug=0 -)/*/STORE SOURCE*/; - - %mx_createwebservice(path=&path - ,name=&name - ,precode=&precode - ,code=&code - ,desc=&desc - ,replace=&replace - ,mdebug=&mdebug - ) - -%mend mp_createwebservice; diff --git a/009_macros/mp_csv2ds.sas b/009_macros/mp_csv2ds.sas deleted file mode 100644 index f98f818..0000000 --- a/009_macros/mp_csv2ds.sas +++ /dev/null @@ -1,141 +0,0 @@ -/*** HELP START ***//** - @file mp_csv2ds.sas - @brief Efficient import of arbitrary CSV using a dataset as template - @details Used to import relevant columns from a large CSV using - a dataset to provide the types and lengths. Assumes that a header - row is provided, and datarows start on line 2. Extra columns in - both the CSV and base dataset are ignored. - - Usage: - - filename mycsv temp; - data _null_; - file mycsv; - put 'name,age,nickname'; - put 'John,48,Jonny'; - put 'Jennifer,23,Jen'; - run; - - %mp_csv2ds(inref=mycsv,outds=myds,baseds=sashelp.class) - - - @param [in] inref= (0) Fileref to the CSV - @param [out] outds= (0) Output ds (lib.ds format) - @param [in] view= (NO) Set to YES or NO to determine whether the output - should be a view or not. Default is NO (not a view). - @param [in] baseds= (0) - Template dataset on which to create the input statement. - Is used to determine types, lengths, and any informats. - - @version 9.2 - @author Allan Bowe - -

SAS Macros

- @li mp_abort.sas - @li mf_existds.sas - -**//*** HELP END ***/ - -%macro mp_csv2ds(inref=0,outds=0,baseds=0,view=NO); - -%mp_abort(iftrue=( &inref=0 ) - ,mac=&sysmacroname - ,msg=%str(the INREF variable must be provided) -) -%mp_abort(iftrue=( %superq(outds)=0 ) - ,mac=&sysmacroname - ,msg=%str(the OUTDS variable must be provided) -) -%mp_abort(iftrue=( &baseds=0 ) - ,mac=&sysmacroname - ,msg=%str(the BASEDS variable must be provided) -) -%mp_abort(iftrue=( %mf_existds(&baseds)=0 ) - ,mac=&sysmacroname - ,msg=%str(the BASEDS dataset (&baseds) needs to be assigned, and to exist) -) - -/* count rows */ -%local hasheader; %let hasheader=0; -data _null_; - if _N_ > 1 then do; - call symputx('hasheader',1,'l'); - stop; - end; - infile &inref; - input; -run; -%mp_abort(iftrue=( &hasheader=0 ) - ,mac=&sysmacroname - ,msg=%str(No header row in &inref) -) - -/* get the variables in the CSV */ -data _data_; - infile &inref; - input; - length name $32; - do i=1 to countc(_infile_,',')+1; - name=upcase(scan(_infile_,i,',')); - output; - end; - stop; -run; -%local csv_vars;%let csv_vars=&syslast; - -/* get the variables in the dataset */ -proc contents noprint data=&baseds - out=_data_ (keep=name type length format: informat); -run; -%local base_vars; %let base_vars=&syslast; - -proc sql undo_policy=none; -create table &csv_vars as - select a.* - ,b.type - ,b.length - ,b.format - ,b.formatd - ,b.formatl - ,b.informat - from &csv_vars a - left join &base_vars b - on a.name=upcase(b.name) - order by i; - -/* prepare the input statement */ -%local instat dropvars; -data _null_; - set &syslast end=last; - length in dropvars $32767; - retain in dropvars; - if missing(type) then do; - informat='$1.'; - dropvars=catx(' ',dropvars,name); - end; - else if missing(informat) then do; - if type=1 then informat='best.'; - else informat=cats('$',length,'.'); - end; - else informat=cats(informat,'.'); - in=catx(' ',in,name,':',informat); - if last then do; - call symputx('instat',in,'l'); - call symputx('dropvars',dropvars,'l'); - end; -run; - -/* import the CSV */ -data &outds - %if %upcase(&view)=YES %then %do; - /view=&outds - %end; - ; - infile &inref dsd firstobs=2; - input &instat; - %if %length(&dropvars)>0 %then %do; - drop &dropvars; - %end; -run; - -%mend mp_csv2ds; diff --git a/009_macros/mp_deleteconstraints.sas b/009_macros/mp_deleteconstraints.sas deleted file mode 100644 index a624d8b..0000000 --- a/009_macros/mp_deleteconstraints.sas +++ /dev/null @@ -1,54 +0,0 @@ -/*** HELP START ***//** - @file mp_deleteconstraints.sas - @brief Delete constraionts - @details Takes the output from mp_getconstraints.sas as input - - 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 unq unique(tx_from, dd_type), - constraint nnn not null(DD_SHORTDESC) - ); - - %mp_getconstraints(lib=work,ds=example,outds=work.constraints) - %mp_deleteconstraints(inds=work.constraints,outds=dropped,execute=YES) - - @param [in] inds= (mp_getconstraints) - The input table containing constraint info - @param [out] outds= (mp_deleteconstraints) - Table containing the drop statements (drop_statement column) - @param [in] execute= (NO) `YES|NO` - default is NO. To actually drop, use YES. - - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_deleteconstraints(inds=mp_getconstraints - ,outds=mp_deleteconstraints - ,execute=NO -)/*/STORE SOURCE*/; - -proc sort data=&inds out=&outds; - by libref table_name constraint_name; -run; - -data &outds; - set &outds; - by libref table_name constraint_name; - length drop_statement $500; - if _n_=1 and "&execute"="YES" then call execute('proc sql;'); - if first.constraint_name then do; - drop_statement=catx(" ","alter table",libref,".",table_name - ,"drop constraint",constraint_name,";"); - output; - if "&execute"="YES" then call execute(drop_statement); - end; -run; - -%mend mp_deleteconstraints; diff --git a/009_macros/mp_deletefolder.sas b/009_macros/mp_deletefolder.sas deleted file mode 100644 index b96501e..0000000 --- a/009_macros/mp_deletefolder.sas +++ /dev/null @@ -1,70 +0,0 @@ -/*** HELP START ***//** - @file - @brief A macro to delete a directory - @details Will delete all folder content (including subfolder content) and - finally, the folder itself. - - Usage: - - %let rootdir=%sysfunc(pathname(work))/demo; - %mf_mkdir(&rootdir) - %mf_mkdir(&rootdir/subdir) - %mf_mkdir(&rootdir/subdir/subsubdir) - data "&rootdir/subdir/example.sas7bdat"; - run; - - %mp_deletefolder(&rootdir) - - @param [in] folder Unquoted path to the folder to delete. - -

SAS Macros

- @li mf_getuniquename.sas - @li mf_isdir.sas - @li mp_dirlist.sas - -

Related Macros

- @li mp_deletefolder.test.sas - -**//*** HELP END ***/ - -%macro mp_deletefolder(folder); - /* proceed if valid directory */ - %if %mf_isdir(&folder)=1 %then %do; - - /* prep temp table */ - %local tempds; - %let tempds=%mf_getuniquename(); - - /* recursive directory listing */ - %mp_dirlist(path=&folder,outds=work.&tempds, maxdepth=MAX) - - /* sort descending level so can delete folder contents before folders */ - proc sort data=work.&tempds; - by descending level; - run; - - /* ensure top level folder is removed at the end */ - proc sql; - insert into work.&tempds set filepath="&folder"; - - /* delete everything */ - data _null_; - set work.&tempds end=last; - length fref $8; - fref=''; - rc=filename(fref,filepath); - rc=fdelete(fref); - if rc then do; - msg=sysmsg(); - put "&sysmacroname:" / rc= / msg= / filepath=; - end; - rc=filename(fref); - run; - - /* tidy up */ - proc sql; - drop table work.&tempds; - - %end; - %else %put &sysmacroname: &folder: is not a valid / accessible folder. ; -%mend mp_deletefolder; diff --git a/009_macros/mp_dictionary.sas b/009_macros/mp_dictionary.sas deleted file mode 100644 index 328c284..0000000 --- a/009_macros/mp_dictionary.sas +++ /dev/null @@ -1,52 +0,0 @@ -/*** HELP START ***//** - @file mp_dictionary.sas - @brief Creates a portal (libref) into the SQL Dictionary Views - @details Provide a libref and the macro will create a series of views against - each view in the special PROC SQL dictionary libref. - - This is useful if you would like to visualise (navigate) the views in a SAS - client such as Base SAS, Enterprise Guide, or Studio (or [Data Controller]( - https://datacontroller.io)). - - It works by extracting the dictionary.dictionaries view into - YOURLIB.dictionaries, then uses that to create a YOURLIB.{viewName} for every - other dictionary.view, eg: - - proc sql; - create view YOURLIB.columns as select * from dictionary.columns; - - Usage: - - libname demo "/lib/directory"; - %mp_dictionary(lib=demo) - - Or, to just create them in WORK: - - %mp_dictionary() - - If you'd just like to browse the dictionary data model, you can also check - out [this article](https://rawsas.com/dictionary-of-dictionaries/). - - ![](https://user-images.githubusercontent.com/4420615/188278365-2987db97-0594-4a39-ac81-dbacdef5cdc8.png) - - @param [in] lib= (WORK) The libref in which to create the views - -

Related Files

- @li mp_dictionary.test.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_dictionary(lib=WORK)/*/STORE SOURCE*/; - %local list i mem; - proc sql noprint; - create view &lib..dictionaries as select * from dictionary.dictionaries; - select distinct memname into: list separated by ' ' from &lib..dictionaries; - %do i=1 %to %sysfunc(countw(&list,%str( ))); - %let mem=%scan(&list,&i,%str( )); - create view &lib..&mem as select * from dictionary.&mem; - %end; - quit; -%mend mp_dictionary; diff --git a/009_macros/mp_distinctfmtvalues.sas b/009_macros/mp_distinctfmtvalues.sas deleted file mode 100644 index dcc803a..0000000 --- a/009_macros/mp_distinctfmtvalues.sas +++ /dev/null @@ -1,50 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a dataset containing distinct _formatted_ values - @details If no format is supplied, then the original value is used instead. - There is also a dependency on other macros within the Macro Core library. - Usage: - - %mp_distinctfmtvalues(libds=sashelp.class,var=age,outvar=age,outds=test) - - @param [in] libds= () input dataset - @param [in] var= (0) variable to get distinct values for - @param [out] outvar= (formatteed_value) variable to create. - @param [out] outds= (work.mp_distinctfmtvalues) dataset to create. - @param [in] varlen= (2000) length of variable to create - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_distinctfmtvalues( - libds= - ,var= - ,outvar=formatted_value - ,outds=work.mp_distinctfmtvalues - ,varlen=2000 -)/*/STORE SOURCE*/; - - %local fmt vtype; - %let fmt=%mf_getvarformat(&libds,&var); - %let vtype=%mf_getvartype(&libds,&var); - - proc sql; - create table &outds as - select distinct - %if &vtype=C & %trim(&fmt)=%str() %then %do; - &var - %end; - %else %if &vtype=C %then %do; - put(&var,&fmt) - %end; - %else %if %trim(&fmt)=%str() %then %do; - put(&var,32.) - %end; - %else %do; - put(&var,&fmt) - %end; - as &outvar length=&varlen - from &libds; -%mend mp_distinctfmtvalues; diff --git a/009_macros/mp_dropmembers.sas b/009_macros/mp_dropmembers.sas deleted file mode 100644 index 5634324..0000000 --- a/009_macros/mp_dropmembers.sas +++ /dev/null @@ -1,46 +0,0 @@ -/*** HELP START ***//** - @file - @brief Drops tables / views (if they exist) without warnings in the log - @details Useful for dropping tables when you're not sure they exist, or if - you are not sure whether they are a dataset or view. Also efficient for - dropping multiple tables / views. - - Example usage: - - proc sql; - create table data1 as select * from sashelp.class; - create view view2 as select * from sashelp.class; - %mp_dropmembers(data1 view2, libref=WORK) - - -

SAS Macros

- @li mf_isblank.sas - - - @param [in] list space separated list of datasets / views, WITHOUT libref - @param [in] libref= (WORK) Note - you can only drop from one library at a time - @param [in] iftrue= (1=1) Conditionally drop tables, eg if &debug=N - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_dropmembers( - list /* space separated list of datasets / views */ - ,libref=WORK /* can only drop from a single library at a time */ - ,iftrue=%str(1=1) -)/*/STORE SOURCE*/; - - %if not(%eval(%unquote(&iftrue))) %then %return; - - %if %mf_isblank(&list) %then %do; - %put NOTE: nothing to drop!; - %return; - %end; - - proc datasets lib=&libref nolist; - delete &list; - delete &list /mtype=view; - run; -%mend mp_dropmembers; diff --git a/009_macros/mp_ds2cards.sas b/009_macros/mp_ds2cards.sas deleted file mode 100644 index 705e031..0000000 --- a/009_macros/mp_ds2cards.sas +++ /dev/null @@ -1,291 +0,0 @@ -/*** HELP START ***//** - @file - @brief Create a CARDS file from a SAS dataset. - @details Uses dataset attributes to convert all data into datalines. - Running the generated file will rebuild the original dataset. Includes - support for large decimals, binary data, PROCESSED_DTTM columns, and - alternative encoding. If the input dataset is empty, the cards file will - still be created. - - Additional support to generate a random sample and max rows. - - Usage: - - %mp_ds2cards(sashelp.class - , tgt_ds=work.class - , cards_file= "C:\temp\class.sas" - , showlog=NO - , maxobs=5 - ) - - TODO: - - labelling the dataset - - explicity setting a unix LF - - constraints / indexes etc - - @param [in] base_ds Should be two level - eg work.blah. This is the table - that is converted to a cards file. - @param [in] tgt_ds= Table that the generated cards file would create. - Optional - if omitted, will be same as BASE_DS. - @param [out] cards_file= ("%sysfunc(pathname(work))/cardgen.sas") Location in - which to write the (.sas) cards file - @param [in] maxobs= (max) To limit output to the first maxobs - observations, enter an integer here. - @param [in] random_sample= (NO) Set to YES to generate a random sample of - data. Can be quite slow. - @param [in] showlog= (YES) Whether to show generated cards file in the SAS - log. Valid values: - @li YES - @li NO - @param [in] outencoding= Provide encoding value for file statement (eg utf-8) - @param [in] append= (NO) If NO then will rebuild the cards file if it already - exists, otherwise will append to it. Used by the mp_lib2cards.sas macro. - -

Related Macros

- @li mp_lib2cards.sas - @li mp_ds2inserts.sas - @li mp_mdtablewrite.sas - - @version 9.2 - @author Allan Bowe - @cond -**//*** HELP END ***/ - -%macro mp_ds2cards(base_ds, tgt_ds= - ,cards_file="%sysfunc(pathname(work))/cardgen.sas" - ,maxobs=max - ,random_sample=NO - ,showlog=YES - ,outencoding= - ,append=NO -)/*/STORE SOURCE*/; -%local i setds nvars; - -%if not %sysfunc(exist(&base_ds)) %then %do; - %put %str(WARN)ING: &base_ds does not exist; - %return; -%end; - -%if %index(&base_ds,.)=0 %then %let base_ds=WORK.&base_ds; -%if (&tgt_ds = ) %then %let tgt_ds=&base_ds; -%if %index(&tgt_ds,.)=0 %then %let tgt_ds=WORK.%scan(&base_ds,2,.); -%if ("&outencoding" ne "") %then %let outencoding=encoding="&outencoding"; -%if ("&append" = "" or "&append" = "NO") %then %let append=; -%else %let append=mod; - -/* get varcount */ -%let nvars=0; -proc sql noprint; -select count(*) into: nvars from dictionary.columns - where upcase(libname)="%scan(%upcase(&base_ds),1)" - and upcase(memname)="%scan(%upcase(&base_ds),2)"; -%if &nvars=0 %then %do; - %put %str(WARN)ING: Dataset &base_ds has no variables, will not be converted.; - %return; -%end; - -/* get indexes */ -proc sort - data=sashelp.vindex( - where=(upcase(libname)="%scan(%upcase(&base_ds),1)" - and upcase(memname)="%scan(%upcase(&base_ds),2)") - ) - out=_data_; - by indxname indxpos; -run; - -%local indexes; -data _null_; - set &syslast end=last; - if _n_=1 then call symputx('indexes','(index=(','l'); - by indxname indxpos; - length vars $32767 nom uni $8; - retain vars; - if first.indxname then do; - idxcnt+1; - nom=''; - uni=''; - vars=name; - end; - else vars=catx(' ',vars,name); - if last.indxname then do; - if nomiss='yes' then nom='/nomiss'; - if unique='yes' then uni='/unique'; - call symputx('indexes' - ,catx(' ',symget('indexes'),indxname,'=(',vars,')',nom,uni) - ,'l'); - end; - if last then call symputx('indexes',cats(symget('indexes'),'))'),'l'); -run; - - -data;run; -%let setds=&syslast; -proc sql -%if %datatyp(&maxobs)=NUMERIC %then %do; - outobs=&maxobs; -%end; - ; - create table &setds as select * from &base_ds -%if &random_sample=YES %then %do; - order by ranuni(42) -%end; - ; -reset outobs=max; -create table datalines1 as - select name,type,length,varnum,format,label from dictionary.columns - where upcase(libname)="%upcase(%scan(&base_ds,1))" - and upcase(memname)="%upcase(%scan(&base_ds,2))"; - -/** - Due to long decimals cannot use best. format - So - use bestd. format and then use character functions to strip trailing - zeros, if NOT an integer or missing!! Cannot use int() as it upsets - note2err when there are missings. - resolved code = ifc( mod(coalesce(VARIABLE,0),1)=0 - ,put(VARIABLE,best32.) - ,substrn(put(VARIABLE,bestd32.),1 - ,findc(put(VARIABLE,bestd32.),'0','TBK'))); -**/ - -data datalines_2; - format dataline $32000.; - set datalines1 (where=(upcase(name) not in - ('PROCESSED_DTTM','VALID_FROM_DTTM','VALID_TO_DTTM'))); - if type='num' then dataline= - cats('ifc(mod(coalesce(',name,',0),1)=0 - ,put(',name,',best32.-l) - ,substrn(put(',name,',bestd32.-l),1 - ,findc(put(',name,',bestd32.-l),"0","TBK")))'); - /** - * binary data must be converted, to store in text format. It is identified - * by the presence of the $HEX keyword in the format. - */ - else if upcase(format)=:'$HEX' then - dataline=cats('put(trim(',name,'),',format,')'); - /** - * There is no easy way to store line breaks in a cards file. - * To discuss this, use: https://github.com/sasjs/core/issues/80 - * Removing all nonprintables with kw (keep writeable) - */ - else dataline=cats('compress(',name,', ,"kw")'); -run; - -proc sql noprint; -select dataline into: datalines separated by ',' from datalines_2; - -%local - process_dttm_flg - valid_from_dttm_flg - valid_to_dttm_flg -; -%let process_dttm_flg = N; -%let valid_from_dttm_flg = N; -%let valid_to_dttm_flg = N; -data _null_; - set datalines1 ; -/* build attrib statement */ - if type='char' then type2='$'; - if strip(format) ne '' then format2=cats('format=',format); - if strip(label) ne '' then label2=cats('label=',quote(trim(label))); - str1=catx(' ',(put(name,$33.)||'length=') - ,put(cats(type2,length),$7.)||format2,label2); - - -/* Build input statement */ - if upcase(format)=:'$HEX' then type3=':'!!format; - else if type='char' then type3=':$char.'; - str2=put(name,$33.)||type3; - - - if(upcase(name) = "PROCESSED_DTTM") then - call symputx("process_dttm_flg", "Y", "L"); - if(upcase(name) = "VALID_FROM_DTTM") then - call symputx("valid_from_dttm_flg", "Y", "L"); - if(upcase(name) = "VALID_TO_DTTM") then - call symputx("valid_to_dttm_flg", "Y", "L"); - - - call symputx(cats("attrib_stmt_", put(_N_, 8.)), str1, "L"); - call symputx(cats("input_stmt_", put(_N_, 8.)) - , ifc(upcase(name) not in - ('PROCESSED_DTTM','VALID_FROM_DTTM','VALID_TO_DTTM'), str2, ""), "L"); -run; - -data _null_; - file &cards_file. &outencoding lrecl=32767 termstr=nl &append; - length __attrib $32767; - if _n_=1 then do; - put '/**'; - put ' @file'; - put " @brief Datalines for %upcase(%scan(&base_ds,2)) dataset"; - put " @details Generated by %nrstr(%%)mp_ds2cards()"; - put " Source: https://github.com/sasjs/core"; - put ' @cond '; - put '**/'; - put "data &tgt_ds &indexes;"; - put "attrib "; - %do i = 1 %to &nvars; - __attrib=symget("attrib_stmt_&i"); - put __attrib; - %end; - put ";"; - - %if &process_dttm_flg. eq Y %then %do; - put 'retain PROCESSED_DTTM %sysfunc(datetime());'; - %end; - %if &valid_from_dttm_flg. eq Y %then %do; - put 'retain VALID_FROM_DTTM &low_date;'; - %end; - %if &valid_to_dttm_flg. eq Y %then %do; - put 'retain VALID_TO_DTTM &high_date;'; - %end; - if __nobs=0 then do; - put 'call missing(of _all_);/* avoid uninitialised notes */'; - put 'stop;'; - put 'run;'; - end; - else do; - put "infile cards dsd;"; - put "input "; - %do i = 1 %to &nvars.; - %if(%length(&&input_stmt_&i..)) %then - put " &&input_stmt_&i.."; - ; - %end; - put ";"; - put 'missing a b c d e f g h i j k l m n o p q r s t u v w x y z _;'; - put "datalines4;"; - end; - end; - set &setds end=__lastobs nobs=__nobs; -/* remove all formats for write purposes - some have long underlying decimals */ - format _numeric_ best30.29; - length __dataline $32767; - __dataline=catq('cqtmb',&datalines); - put __dataline; - if __lastobs then do; - put ';;;;'; - put 'run;'; - put '/** @endcond **/'; - stop; - end; -run; -proc sql; - drop table &setds; -quit; - -%if &showlog=YES %then %do; - data _null_; - infile &cards_file lrecl=32767; - input; - put _infile_; - run; -%end; - -%put NOTE: CARDS FILE SAVED IN:; -%put NOTE-;%put NOTE-; -%put NOTE- %sysfunc(dequote(&cards_file.)); -%put NOTE-;%put NOTE-; -%mend mp_ds2cards; -/** @endcond **/ diff --git a/009_macros/mp_ds2ddl.sas b/009_macros/mp_ds2ddl.sas deleted file mode 100644 index 02aa6b7..0000000 --- a/009_macros/mp_ds2ddl.sas +++ /dev/null @@ -1,42 +0,0 @@ -/*** HELP START ***//** - @file - @brief Fetches DDL for a specific table - @details Uses mp_getddl under the hood - - @param [in] libds library.dataset to create ddl for - @param [in] fref= (getddl) the fileref to which to _append_ the DDL. If it - does not exist, it will be created. - @param [in] flavour= (SAS) The type of DDL to create. Options: - @li SAS - @li TSQL - - @param [in]showlog= (NO) Set to YES to show the DDL in the log - @param [in] schema= () Choose a preferred schema name (default is to use - actual schema, else libref) - @param [in] applydttm= (NO) For non SAS DDL, choose if columns are created with - native datetime2 format or regular decimal type - -

SAS Macros

- @li mp_getddl.sas - -**//*** HELP END ***/ - -%macro mp_ds2ddl(libds,fref=getddl,flavour=SAS,showlog=YES,schema= - ,applydttm=NO -)/*/STORE SOURCE*/; - -%local libref; -%let libds=%upcase(&libds); -%let libref=%scan(&libds,1,.); -%if &libref=&libds %then %let libds=WORK.&libds; - -%mp_getddl(%scan(&libds,1,.) - ,%scan(&libds,2,.) - ,fref=&fref - ,flavour=SAS - ,showlog=&showlog - ,schema=&schema - ,applydttm=&applydttm -) - -%mend mp_ds2ddl; diff --git a/009_macros/mp_ds2fmtds.sas b/009_macros/mp_ds2fmtds.sas deleted file mode 100644 index c553f3f..0000000 --- a/009_macros/mp_ds2fmtds.sas +++ /dev/null @@ -1,105 +0,0 @@ -/*** HELP START ***//** - @file - @brief Converts every value in a dataset to formatted value - @details Converts every value to it's formatted value. All variables will - become character, and will be in the same order as the original dataset. - - Lengths will be adjusted according to the format lengths, where applicable. - - Usage: - - %mp_ds2fmtds(sashelp.cars,work.cars) - %mp_ds2fmtds(sashelp.vallopt,vw_vallopt) - - @param [in] libds The library.dataset to be converted - @param [out] outds The dataset to create. - -

SAS Macros

- @li mf_existds.sas - -

Related Macros

- @li mp_jsonout.sas - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mp_ds2fmtds(libds, outds -)/*/STORE SOURCE*/; - -/* validations */ - -%if not %mf_existds(libds=&libds) %then %do; - %put %str(WARN)ING: &libds does not exist as either a VIEW or DATASET; - %return; -%end; -%if %index(&libds,.)=0 %then %let libds=WORK.&libds; - -/* grab metadata */ -proc contents noprint data=&libds - out=_data_(keep=name type length format formatl formatd varnum); -run; -proc sort; - by varnum; -run; - -/* prepare formats and varnames */ -data _null_; - set &syslast end=last; - name=upcase(name); - /* fix formats */ - if type=2 or type=6 then do; - length fmt $49.; - if format='' then fmt=cats('$',length,'.'); - else if formatl=0 then fmt=cats(format,'.'); - else fmt=cats(format,formatl,'.'); - newlen=max(formatl,length); - end; - else do; - if format='' then fmt='best.'; - else if formatl=0 then fmt=cats(format,'.'); - else if formatd=0 then fmt=cats(format,formatl,'.'); - else fmt=cats(format,formatl,'.',formatd); - /* needs to be wide, for datetimes etc */ - newlen=max(length,formatl,24); - end; - /* 32 char unique name */ - newname='sasjs'!!substr(cats(put(md5(name),$hex32.)),1,27); - - call symputx(cats('name',_n_),name,'l'); - call symputx(cats('newname',_n_),newname,'l'); - call symputx(cats('len',_n_),newlen,'l'); - call symputx(cats('fmt',_n_),fmt,'l'); - call symputx(cats('type',_n_),type,'l'); - if last then call symputx('nobs',_n_,'l'); -run; - -/* clean up */ -proc sql; -drop table &syslast; - -%if &nobs=0 %then %do; - %put Dataset &libds has no columns! - data &outds; - set &libds; - run; - %return; -%end; - -data &outds; - /* rename on entry */ - set &libds(rename=( -%local i; -%do i=1 %to &nobs; - &&name&i=&&newname&i -%end; - )); -%do i=1 %to &nobs; - length &&name&i $&&len&i; - &&name&i=left(put(&&newname&i,&&fmt&i)); - drop &&newname&i; -%end; - if _error_ then call symputx('syscc',1012); -run; - -%mend mp_ds2fmtds; diff --git a/009_macros/mp_ds2inserts.sas b/009_macros/mp_ds2inserts.sas deleted file mode 100644 index f24a621..0000000 --- a/009_macros/mp_ds2inserts.sas +++ /dev/null @@ -1,180 +0,0 @@ -/*** HELP START ***//** - @file - @brief Export a dataset to SQL insert statements - @details Converts dataset values to SQL insert statements for use across - multiple database types. - - Usage: - - %mp_ds2inserts(sashelp.class,outref=myref,outds=class) - data class; - set sashelp.class; - stop; - proc sql; - %inc myref; - - @param [in] ds The dataset to be exported - @param [in] maxobs= (max) The max number of inserts to create - @param [out] outref= (0) The output fileref. If it does not exist, it is - created. If it does exist, new records are APPENDED. - @param [out] schema= (0) The library (or schema) in which the target table is - located. If not provided, is ignored. - @param [out] outds= (0) The output table to load. If not provided, will - default to the table in the &ds parameter. - @param [in] flavour= (SAS) The SQL flavour to be applied to the output. Valid - options: - @li SAS (default) - suitable for regular proc sql - @li PGSQL - Used for Postgres databases - @param [in] applydttm= (YES) If YES, any columns using datetime formats will - be converted to native DB datetime literals - -

SAS Macros

- @li mf_existfileref.sas - @li mf_getvarcount.sas - @li mf_getvarformat.sas - @li mf_getvarlist.sas - @li mf_getvartype.sas - - @version 9.2 - @author Allan Bowe (credit mjsq) -**//*** HELP END ***/ - -%macro mp_ds2inserts(ds, outref=0,schema=0,outds=0,flavour=SAS,maxobs=max - ,applydttm=YES -)/*/STORE SOURCE*/; - -%if not %sysfunc(exist(&ds)) %then %do; - %put %str(WAR)NING: &ds does not exist; - %return; -%end; - -%if not %sysfunc(exist(&ds)) %then %do; - %put %str(WAR)NING: &ds does not exist; - %return; -%end; - -%if %index(&ds,.)=0 %then %let ds=WORK.&ds; - -%let flavour=%upcase(&flavour); -%if &flavour ne SAS and &flavour ne PGSQL %then %do; - %put %str(WAR)NING: &flavour is not supported; - %return; -%end; - -%if &outref=0 %then %do; - %put %str(WAR)NING: Please provide a fileref; - %return; -%end; -%if %mf_existfileref(&outref)=0 %then %do; - filename &outref temp lrecl=66000; -%end; - -%if &schema=0 %then %let schema=; -%else %let schema=&schema..; - -%if &outds=0 %then %let outds=%scan(&ds,2,.); - -%local nobs; -proc sql noprint; -select count(*) into: nobs TRIMMED from &ds; -%if &nobs=0 %then %do; - data _null_; - file &outref mod; - put "/* No rows found in &ds */"; - run; -%end; - -%local vars; -%let vars=%mf_getvarcount(&ds); -%if &vars=0 %then %do; - data _null_; - file &outref mod; - put "/* No columns found in &schema.&ds */"; - run; - %return; -%end; -%else %if &vars>1600 and &flavour=PGSQL %then %do; - data _null_; - file &fref mod; - put "/* &schema.&ds contains &vars vars */"; - put "/* Postgres cannot handle tables with over 1600 vars */"; - put "/* No inserts will be generated for this table */"; - run; - %return; -%end; - -%local varlist varlistcomma; -%let varlist=%mf_getvarlist(&ds); -%let varlistcomma=%mf_getvarlist(&ds,dlm=%str(,),quote=double); - -/* next, export data */ -data _null_; - file &outref mod ; - if _n_=1 then put "/* &schema.&outds (&nobs rows, &vars columns) */"; - set &ds; - %if &maxobs ne max %then %do; - if _n_>&maxobs then stop; - %end; - length _____str $32767; - call missing(_____str); - format _numeric_ best.; - format _character_ ; - %local i comma var vtype vfmt; - %do i=1 %to %sysfunc(countw(&varlist)); - %let var=%scan(&varlist,&i); - %let vtype=%mf_getvartype(&ds,&var); - %let vfmt=%upcase(%mf_getvarformat(&ds,&var,force=1)); - %if &i=1 %then %do; - %if &flavour=SAS %then %do; - put "insert into &schema.&outds set "; - put " &var="@; - %end; - %else %if &flavour=PGSQL %then %do; - _____str=cats( - "INSERT INTO &schema.&outds (" - ,symget('varlistcomma') - ,") VALUES (" - ); - put _____str; - put " "@; - %end; - %end; - %else %do; - %if &flavour=SAS %then %do; - put " ,&var="@; - %end; - %else %if &flavour=PGSQL %then %do; - put " ,"@; - %end; - %end; - %if &vtype=N %then %do; - %if &flavour=SAS %then %do; - put &var; - %end; - %else %if &flavour=PGSQL %then %do; - if missing(&var) then put 'NULL'; - %if &applydttm=YES and "%substr(&vfmt.xxxxxxxx,1,8)"="DATETIME" - %then %do; - else put "TIMESTAMP '" &var E8601DT25.6 "'"; - %end; - %else %do; - else put &var; - %end; - %end; - %end; - %else %do; - _____str="'"!!trim(tranwrd(&var,"'","''"))!!"'"; - put _____str; - %end; - %end; - %if &flavour=SAS %then %do; - put ';'; - %end; - %else %if &flavour=PGSQL %then %do; - put ');'; - %end; - - if _n_=&nobs then put /; -run; - -%mend mp_ds2inserts; diff --git a/009_macros/mp_ds2md.sas b/009_macros/mp_ds2md.sas deleted file mode 100644 index ca58489..0000000 --- a/009_macros/mp_ds2md.sas +++ /dev/null @@ -1,104 +0,0 @@ -/*** HELP START ***//** - @file - @brief Create a Markdown Table from a dataset - @details A markdown table is a simple table representation for use in - documents written in markdown format. - - An online generator is available here: - https://www.tablesgenerator.com/markdown_tables - - This structure is also used by the Macro Core library for documenting input/ - output datasets, as well as the sasjs/cli tool for documenting inputs/outputs - for web services. - - We take the standard definition one step further by embedding the informat - in the table header row, like so: - - |var1:$32|var2:best.|var3:date9.| - |---|---|---| - |some text|42|01JAN1960| - |blah|1|31DEC1999| - - Which resolves to: - - |var1:$32|var2:best.|var3:date9.| - |---|---|---| - |some text|42|01JAN1960| - |blah|1|31DEC1999| - - - Usage: - - %mp_ds2md(sashelp.class) - - @param [in] libds the library / dataset to create or read from. - @param [out] outref= (mdtable) Fileref to contain the markdown - @param [out] showlog= (YES) Set to NO to avoid printing markdown to the log - -

SAS Macros

- @li mf_getvarlist.sas - @li mf_getvarformat.sas - - @version 9.3 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mp_ds2md( - libds, - outref=mdtable, - showlog=YES -)/*/STORE SOURCE*/; - -/* check fileref is assigned */ -%if %sysfunc(fileref(&outref)) > 0 %then %do; - filename &outref temp; -%end; - -%local vars; -%let vars=%upcase(%mf_getvarlist(&libds)); - -%if %trim(X&vars)=X %then %do; - %put &sysmacroname: Table &libds has no columns!!; - %return; -%end; - -/* create the header row */ -data _null_; - file &outref; - length line $32767; - call missing(line); - put '|' -%local i var fmt; -%do i=1 %to %sysfunc(countw(&vars)); - %let var=%scan(&vars,&i); - %let fmt=%lowcase(%mf_getvarformat(&libds,&var,force=1)); - "&var:&fmt|" -%end; - ; - put '|' -%do i=1 %to %sysfunc(countw(&vars)); - "---|" -%end; - ; -run; - -/* write out the data */ -data _null_; - file &outref mod dlm='|' lrecl=32767; - set &libds ; - length line $32767; - line='|`'!!cats(%mf_getvarlist(&libds,dlm=%str(%)!!' `|`'!!cats%()))!!' `|'; - put line; -run; - -%if %upcase(&showlog)=YES %then %do; - options ps=max lrecl=max; - data _null_; - infile &outref; - if _n_=1 then putlog "# &libds" /; - input; - putlog _infile_; - run; -%end; - -%mend mp_ds2md; diff --git a/009_macros/mp_ds2squeeze.sas b/009_macros/mp_ds2squeeze.sas deleted file mode 100644 index 407a016..0000000 --- a/009_macros/mp_ds2squeeze.sas +++ /dev/null @@ -1,120 +0,0 @@ -/*** HELP START ***//** - @file - @brief Create a smaller version of a dataset, without data loss - @details This macro will scan the input dataset and create a new one, that - has the minimum variable lengths needed to store the data without data loss. - - Inspiration was taken from [How to Reduce the Disk Space Required by a - SAS® Data Set](https://www.lexjansen.com/nesug/nesug06/io/io18.pdf) by - Selvaratnam Sridharma. The end of the referenced paper presents a macro named - "squeeze", hence the nomenclature. - - Usage: - - data big; - length my big $32000; - do i=1 to 1e4; - my=repeat('oh my',100); - big='dawg'; - special=._; - output; - end; - run; - - %mp_ds2squeeze(work.big,outds=work.smaller) - - The following will also be printed to the log (exact values may differ - depending on your OS and COMPRESS settings): - - > MP_DS2SQUEEZE: work.big was 625MB - - > MP_DS2SQUEEZE: work.smaller is 5MB - - @param [in] libds The library.dataset to be squeezed - @param [out] outds= (work.mp_ds2squeeze) The squeezed dataset to create - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - -

SAS Macros

- @li mf_getfilesize.sas - @li mf_getuniquefileref.sas - @li mf_getuniquename.sas - @li mp_getmaxvarlengths.sas - -

Related Programs

- @li mp_ds2squeeze.test.sas - - @version 9.3 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mp_ds2squeeze( - libds, - outds=work.mp_ds2squeeze, - mdebug=0 -)/*/STORE SOURCE*/; -%local dbg source; -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; -%end; -%else %do; - %let dbg=*; - %let source=/source2; -%end; - -%local optval ds fref startsize; -%let ds=%mf_getuniquename(); -%let fref=%mf_getuniquefileref(); -%let startsize=%mf_getfilesize(libds=&libds,format=yes); - -%mp_getmaxvarlengths(&libds,outds=&ds) - -data _null_; - set &ds end=last; - file &fref; - /* grab the types */ - retain dsid; - if _n_=1 then dsid=open("&libds",'is'); - if dsid le 0 then do; - msg=sysmsg(); - put msg=; - stop; - end; - type=vartype(dsid,varnum(dsid, name)); - if last then rc=close(dsid); - /* write out the length statement */ - if _n_=1 then put 'length '; - length len $6; - if type='C' then do; - if maxlen=0 then len='$1'; - else len=cats('$',maxlen); - end; - else do; - if maxlen=0 then len='3'; - else len=cats(maxlen); - end; - put ' ' name ' ' len; - if last then put ';'; -run; - -/* configure varlenchk - as we are explicitly shortening the variables */ -%let optval=%sysfunc(getoption(varlenchk)); -options varlenchk=NOWARN; - -data &outds; - %inc &fref &source; - set &libds; -run; - -options varlenchk=&optval; - -%if &mdebug=0 %then %do; - proc sql; - drop table &ds; - filename &fref clear; -%end; - -%put &sysmacroname: &libds was &startsize; -%put &sysmacroname: &outds is %mf_getfilesize(libds=&outds,format=yes); - -%mend mp_ds2squeeze; diff --git a/009_macros/mp_dsmeta.sas b/009_macros/mp_dsmeta.sas deleted file mode 100644 index 9bed16c..0000000 --- a/009_macros/mp_dsmeta.sas +++ /dev/null @@ -1,110 +0,0 @@ -/*** HELP START ***//** - @file - @brief Export dataset metadata to a single output table - @details Exports the dataset attributes and enginehost information, then - converts the datasets into a single output table in the following format: - -|ODS_TABLE:$10.|NAME:$100.|VALUE:$1000.| -|---|---|---| -|`ATTRIBUTES `|`Data Set Name `|`SASHELP.CLASS `| -|`ATTRIBUTES `|`Observations `|`19 `| -|`ATTRIBUTES `|`Member Type `|`DATA `| -|`ATTRIBUTES `|`Variables `|`5 `| -|`ATTRIBUTES `|`Engine `|`V9 `| -|`ATTRIBUTES `|`Indexes `|`0 `| -|`ATTRIBUTES `|`Created `|`06/08/2020 00:59:14 `| -|`ATTRIBUTES `|`Observation Length `|`40 `| -|`ATTRIBUTES `|`Last Modified `|`06/08/2020 00:59:14 `| -|`ATTRIBUTES `|`Deleted Observations `|`0 `| -|`ATTRIBUTES `|`Protection `|`. `| -|`ATTRIBUTES `|`Compressed `|`NO `| -|`ATTRIBUTES `|`Data Set Type `|`. `| -|`ATTRIBUTES `|`Sorted `|`NO `| -|`ATTRIBUTES `|`Label `|`Student Data `| -|`ATTRIBUTES `|`Data Representation `|`SOLARIS_X86_64, LINUX_X86_64, ALPHA_TRU64, LINUX_IA64 `| -|`ATTRIBUTES `|`Encoding `|`us-ascii ASCII (ANSI) `| -|`ENGINEHOST `|`Data Set Page Size `|`65536 `| -|`ENGINEHOST `|`Number of Data Set Pages `|`1 `| -|`ENGINEHOST `|`First Data Page `|`1 `| -|`ENGINEHOST `|`Max Obs per Page `|`1632 `| -|`ENGINEHOST `|`Obs in First Data Page `|`19 `| -|`ENGINEHOST `|`Number of Data Set Repairs `|`0 `| -|`ENGINEHOST `|`Filename `|`/opt/sas/sas9/SASHome/SASFoundation/9.4/sashelp/class.sas7bdat `| -|`ENGINEHOST `|`Release Created `|`9.0401M7 `| -|`ENGINEHOST `|`Host Created `|`Linux `| -|`ENGINEHOST `|`Inode Number `|`28314616 `| -|`ENGINEHOST `|`Access Permission `|`rw-r--r-- `| -|`ENGINEHOST `|`Owner Name `|`sas `| -|`ENGINEHOST `|`File Size `|`128KB `| -|`ENGINEHOST `|`File Size (bytes) `|`131072 `| - - Example usage: - - %mp_dsmeta(sashelp.class,outds=work.mymeta) - proc print data=work.mymeta; - run; - - For more details on creating datasets from PROC CONTENTS check out this - excellent [paper]( - https://support.sas.com/resources/papers/proceedings14/1549-2014.pdf) by - [Louise Hadden](https://www.linkedin.com/in/louisehadden/). - - @param [in] libds The library.dataset to export the metadata for - @param [out] outds= (work.dsmeta) The output table to contain the metadata - -

Related Files

- @li mp_dsmeta.test.sas - @li mp_getcols.sas - @li mp_getdbml.sas - @li mp_getddl.sas - @li mp_getformats.sas - @li mp_getpk.sas - @li mp_guesspk.sas - -**//*** HELP END ***/ - -%macro mp_dsmeta(libds,outds=work.dsmeta); - -%local ds1 ds2; -data;run; %let ds1=&syslast; -data;run; %let ds2=&syslast; - -/* setup the ODS capture */ -ods output attributes=&ds1 enginehost=&ds2; - -/* export the metadata */ -proc contents data=&libds; -run; - -/* load it into a single table */ -data &outds (keep=ods_table name value); - length ods_table $10 name label2 label1 label $100 - value cvalue cvalue1 cvalue2 $1000 - nvalue nvalue1 nvalue2 8; - if _n_=1 then call missing (of _all_); - * putlog (_all_)(=); - set &ds1 (in=atrs) &ds2 (in=eng); - if atrs then do; - ods_table='ATTRIBUTES'; - name=coalescec(label1,label); - value=coalescec(cvalue1,cvalue,put(coalesce(nvalue1,nvalue),best.)); - output; - if label2 ne '' then do; - name=label2; - value=coalescec(cvalue2,put(nvalue2,best.)); - output; - end; - end; - else if eng then do; - ods_table='ENGINEHOST'; - name=coalescec(label1,label); - value=coalescec(cvalue1,cvalue,put(coalesce(nvalue1,nvalue),best.)); - output; - end; -run; - -proc sql; -drop table &ds1, &ds2; - -%mend mp_dsmeta; - diff --git a/009_macros/mp_filtergenerate.sas b/009_macros/mp_filtergenerate.sas deleted file mode 100644 index 659d6cc..0000000 --- a/009_macros/mp_filtergenerate.sas +++ /dev/null @@ -1,105 +0,0 @@ -/*** HELP START ***//** - @file - @brief Generates a filter clause from an input table, to a fileref - @details Uses the input table to generate an output filter clause. - This feature is used to create dynamic dropdowns in [Data Controller for SAS®]( - https://datacontroller.io). The input table should be in the format below: - - |GROUP_LOGIC:$3|SUBGROUP_LOGIC:$3|SUBGROUP_ID:8.|VARIABLE_NM:$32|OPERATOR_NM:$10|RAW_VALUE:$4000| - |---|---|---|---|---|---| - |AND|AND|1|AGE|=|12| - |AND|AND|1|SEX|<=|'M'| - |AND|OR|2|Name|NOT IN|('Jane','Alfred')| - |AND|OR|2|Weight|>=|7| - - Note - if the above table is received from an external client, the values - should first be validated using the mp_filtercheck.sas macro to avoid risk - of SQL injection. - - To generate the filter, run the following code: - - data work.filtertable; - infile datalines4 dsd; - input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32. - OPERATOR_NM:$10. RAW_VALUE:$4000.; - datalines4; - AND,AND,1,AGE,=,12 - AND,AND,1,SEX,<=,"'M'" - AND,OR,2,Name,NOT IN,"('Jane','Alfred')" - AND,OR,2,Weight,>=,7 - ;;;; - run; - - %mp_filtergenerate(work.filtertable,outref=myfilter) - - data _null_; - infile myfilter; - input; - put _infile_; - run; - - Will write the following query to the log: - - > ( - > AGE = 12 - > AND - > SEX <= 'M' - > ) AND ( - > Name NOT IN ('Jane','Alfred') - > OR - > Weight >= 7 - > ) - - @param [in] inds The input table with query values - @param [out] outref= (filter) The output fileref to contain the filter clause. - Will be created (or replaced). - -

Related Macros

- @li mp_filtercheck.sas - @li mp_filtervalidate.sas - -

SAS Macros

- @li mp_abort.sas - @li mf_nobs.sas - - @version 9.3 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_filtergenerate(inds,outref=filter); - -%mp_abort(iftrue= (&syscc ne 0) - ,mac=&sysmacroname - ,msg=%str(syscc=&syscc - on macro entry) -) - -filename &outref temp; - -%if %mf_nobs(&inds)=0 %then %do; - /* ensure we have a default filter */ - data _null_; - file &outref; - put '1=1'; - run; -%end; -%else %do; - proc sort data=&inds; - by SUBGROUP_ID; - run; - data _null_; - file &outref lrecl=32800; - set &inds end=last; - by SUBGROUP_ID; - if _n_=1 then put '(('; - else if first.SUBGROUP_ID then put +1 GROUP_LOGIC '('; - else put +2 SUBGROUP_LOGIC; - - put +4 VARIABLE_NM OPERATOR_NM RAW_VALUE; - - if last.SUBGROUP_ID then put ')'@; - if last then put ')'; - run; -%end; - -%mend mp_filtergenerate; diff --git a/009_macros/mp_filterstore.sas b/009_macros/mp_filterstore.sas deleted file mode 100644 index a1cdd1a..0000000 --- a/009_macros/mp_filterstore.sas +++ /dev/null @@ -1,255 +0,0 @@ -/*** HELP START ***//** - @file - @brief Checks & Stores an input filter table and returns the Filter Key - @details Used to generate a FILTER_RK from an input query dataset. This - process requires several permanent tables (names are configurable). The - benefit of storing query values at backend is to enable stored 'views' of - filtered tables at frontend (ie, when building [SAS-Powered Apps]( - https://sasapps.io)). This macro is also used in [Data Controller for SAS]( - https://datacontroller.io). - - A more recent feature of this macro is the ability to support filter queries - on Format Catalogs. This is achieved by adding a `-FC` suffix to the `libds` - parameter - where the "ds" in this case is the catalog name. - - - @param [in] libds= The target dataset to be filtered (lib should be assigned). - If filtering a format catalog, add the following suffix: `-FC`. - @param [in] queryds= (WORK.FILTERQUERY) The temporary input query dataset to - be validated. Has the following format: -|GROUP_LOGIC:$3|SUBGROUP_LOGIC:$3|SUBGROUP_ID:8.|VARIABLE_NM:$32|OPERATOR_NM:$10|RAW_VALUE:$32767| -|---|---|---|---|---|---| -|AND|AND|1|SOME_BESTNUM|>|1| -|AND|AND|1|SOME_TIME|=|77333| - @param [in] filter_summary= (PERM.FILTER_SUMMARY) Permanent table containing - summary filter values. The definition is available by running - mp_coretable.sas as follows: `mp_coretable(FILTER_SUMMARY)`. Example - values: -|FILTER_RK:best.|FILTER_HASH:$32.|FILTER_TABLE:$41.|PROCESSED_DTTM:datetime19.| -|---|---|---|---| -|`1 `|`540E96F566D194AB58DD4C413C99C9DB `|`VIYA6014.MPE_TABLES `|`1956084246 `| -|`2 `|`87737DB9EEE2650F5C89956CEAD0A14F `|`VIYA6014.MPE_X_TEST `|`1956084452.1`| -|`3 `|`8048BD908DBBD83D013560734E90D394 `|`VIYA6014.MPE_TABLES `|`1956093620.6`| - @param [in] filter_detail= (PERM.FILTER_DETAIL) Permanent table containing - detailed (raw) filter values. The definition is available by running - mp_coretable.sas as follows: `mp_coretable(FILTER_DETAIL)`. Example - values: -|FILTER_HASH:$32.|FILTER_LINE:best.|GROUP_LOGIC:$3.|SUBGROUP_LOGIC:$3.|SUBGROUP_ID:best.|VARIABLE_NM:$32.|OPERATOR_NM:$12.|RAW_VALUE:$4000.|PROCESSED_DTTM:datetime19.| -|---|---|---|---|---|---|---|---|---| -|`540E96F566D194AB58DD4C413C99C9DB `|`1 `|`AND `|`AND `|`1 `|`LIBREF `|`CONTAINS `|`DC`|`1956084245.8 `| -|`540E96F566D194AB58DD4C413C99C9DB `|`2 `|`AND `|`OR `|`2 `|`DSN `|`= `|` MPE_LOCK_ANYTABLE `|`1956084245.8 `| -|`87737DB9EEE2650F5C89956CEAD0A14F `|`1 `|`AND `|`AND `|`1 `|`PRIMARY_KEY_FIELD `|`IN `|`(1,2,3) `|`1956084451.9 `| - @param [in] lock_table= (PERM.LOCK_TABLE) Permanent locking table. Used to - manage concurrent access. The definition is available by running - mp_coretable.sas as follows: `mp_coretable(LOCKTABLE)`. - @param [in] maxkeytable= (0) Optional permanent reference table used for - retained key tracking. Described in mp_retainedkey.sas. - @param [in] mdebug= (1) set to 1 to enable DEBUG messages - @param [out] outresult= (work.result) The result table with the FILTER_RK - @param [out] outquery= (work.query) The original query, taken as extract - after table load - - -

SAS Macros

- @li mddl_sas_cntlout.sas - @li mf_getuniquename.sas - @li mf_getvalue.sas - @li mf_islibds.sas - @li mf_nobs.sas - @li mp_abort.sas - @li mp_filtercheck.sas - @li mp_hashdataset.sas - @li mp_retainedkey.sas - -

Related Macros

- @li mp_filtercheck.sas - @li mp_filtergenerate.sas - @li mp_filtervalidate.sas - @li mp_filterstore.test.sas - - @version 9.2 - @author [Allan Bowe](https://www.linkedin.com/in/allanbowe) - -**//*** HELP END ***/ - -%macro mp_filterstore(libds=, - queryds=work.filterquery, - filter_summary=PERM.FILTER_SUMMARY, - filter_detail=PERM.FILTER_DETAIL, - lock_table=PERM.LOCK_TABLE, - maxkeytable=PERM.MAXKEYTABLE, - outresult=work.result, - outquery=work.query, - mdebug=1 -); -%put &sysmacroname entry vars:; -%put _local_; - -%local ds0 ds1 ds2 ds3 ds4 filter_hash orig_libds; -%let libds=%upcase(&libds); -%let orig_libds=&libds; - -%mp_abort(iftrue= (&syscc ne 0) - ,mac=mp_filterstore - ,msg=%str(syscc=&syscc on macro entry) -) -%mp_abort(iftrue= (%mf_islibds(&filter_summary)=0) - ,mac=mp_filterstore - ,msg=%str(Invalid filter_summary value: &filter_summary) -) -%mp_abort(iftrue= (%mf_islibds(&filter_detail)=0) - ,mac=mp_filterstore - ,msg=%str(Invalid filter_detail value: &filter_detail) -) -%mp_abort(iftrue= (%mf_islibds(&lock_table)=0) - ,mac=mp_filterstore - ,msg=%str(Invalid lock_table value: &lock_table) -) - -/** - * validate query - * use format catalog export, if a format - */ -%if "%substr(&libds,%length(&libds)-2,3)"="-FC" %then %do; - %let libds=%scan(&libds,1,-); /* chop off -FC extension */ - %let ds0=%mf_getuniquename(prefix=fmtds_); - %let libds=&ds0; - /* - There is no need to export the entire format catalog here - the validations - are done against the data model, not the data values. So we can simply - hardcode the structure based on the cntlout dataset. - */ - %mddl_sas_cntlout(libds=&ds0) - -%end; -%mp_filtercheck(&queryds,targetds=&libds,abort=YES) - -/* hash the result */ -%let ds1=%mf_getuniquename(prefix=hashds); -%mp_hashdataset(&queryds,outds=&ds1,salt=&orig_libds) -%let filter_hash=%upcase(%mf_getvalue(&ds1,hashkey)); -%if &mdebug=1 %then %do; - data _null_; - putlog "filter_hash=&filter_hash"; - set &ds1; - putlog (_all_)(=); - run; -%end; - -/* check if data already exists for this hash */ -data &outresult; - set &filter_summary; - where filter_hash="&filter_hash"; -run; - -%mp_abort(iftrue= (&syscc ne 0) - ,mac=mp_filterstore - ,msg=%str(syscc=&syscc after hash check) -) -%mp_abort(iftrue= ("&filter_hash "=" ") - ,mac=mp_filterstore - ,msg=%str(problem with filter_hash generation) -) - -%if %mf_nobs(&outresult)=0 %then %do; - - /* first update summary table */ - %let ds3=%mf_getuniquename(prefix=filtersum); - data work.&ds3; - if 0 then set &filter_summary; - filter_table="&orig_libds"; - filter_hash="&filter_hash"; - PROCESSED_DTTM=%sysfunc(datetime()); - output; - stop; - run; - - %mp_lockanytable(LOCK, - lib=%scan(&filter_summary,1,.) - ,ds=%scan(&filter_summary,2,.) - ,ref=MP_FILTERSTORE summary update - &filter_hash - ,ctl_ds=&lock_table - ) - - %let ds4=%mf_getuniquename(prefix=filtersumappend); - %mp_retainedkey( - base_lib=%scan(&filter_summary,1,.) - ,base_dsn=%scan(&filter_summary,2,.) - ,append_lib=work - ,append_dsn=&ds3 - ,retained_key=filter_rk - ,business_key=filter_hash - ,maxkeytable=&maxkeytable - ,locktable=&lock_table - ,outds=work.&ds4 - ) - proc append base=&filter_summary data=&ds4; - run; - - %mp_lockanytable(UNLOCK, - lib=%scan(&filter_summary,1,.) - ,ds=%scan(&filter_summary,2,.) - ,ref=MP_FILTERSTORE summary update - &filter_hash - ,ctl_ds=&lock_table - ) - - %if &syscc ne 0 %then %do; - data _null_; - set &ds4; - putlog (_all_)(=); - run; - %goto err; - %end; - - data &outresult; - set &filter_summary; - where filter_hash="&filter_hash"; - run; - - /* Next, update detail table */ - %let ds2=%mf_getuniquename(prefix=filterdetail); - data &ds2; - if 0 then set &filter_detail; - set &queryds; - format filter_hash $hex32. filter_line 8.; - filter_hash="&filter_hash"; - filter_line=_n_; - PROCESSED_DTTM=%sysfunc(datetime()); - run; - %mp_lockanytable(LOCK, - lib=%scan(&filter_detail,1,.) - ,ds=%scan(&filter_detail,2,.) - ,ref=MP_FILTERSTORE update - &filter_hash - ,ctl_ds=&lock_table - ) - proc append base=&filter_detail data=&ds2; - run; - - %mp_lockanytable(UNLOCK, - lib=%scan(&filter_detail,1,.) - ,ds=%scan(&filter_detail,2,.) - ,ref=MP_FILTERSTORE detail update &filter_hash - ,ctl_ds=&lock_table - ) - - %if &syscc ne 0 %then %do; - data _null_; - set &ds2; - putlog (_all_)(=); - run; - %goto err; - %end; - -%end; - -proc sort data=&filter_detail(where=(filter_hash="&filter_hash")) out=&outquery; - by filter_line; -run; - -%err: -%mp_abort(iftrue= (&syscc ne 0) - ,mac=mp_filterstore - ,msg=%str(syscc=&syscc on macro exit) -) - -%mend mp_filterstore; diff --git a/009_macros/mp_filtervalidate.sas b/009_macros/mp_filtervalidate.sas deleted file mode 100644 index 947d173..0000000 --- a/009_macros/mp_filtervalidate.sas +++ /dev/null @@ -1,105 +0,0 @@ -/*** HELP START ***//** - @file - @brief Checks a generated filter query for validity - @details Runs a generated filter in proc sql with the validate option. - Used in mp_filtercheck.sas in an fcmp container. - - Built to support dynamic filtering in - [Data Controller for SAS®](https://datacontroller.io). - - Usage: - - data work.filtertable; - infile datalines4 dsd; - input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32. - OPERATOR_NM:$10. RAW_VALUE:$4000.; - datalines4; - AND,AND,1,AGE,=,12 - AND,AND,1,SEX,<=,"'M'" - AND,OR,2,Name,NOT IN,"('Jane','Alfred')" - AND,OR,2,Weight,>=,7 - ;;;; - run; - - %mp_filtergenerate(work.filtertable,outref=myfilter) - - %mp_filtervalidate(myfilter,sashelp.class) - - - @returns The SYSCC value will be 1008 if there are validation issues. - - @param [in] inref The input fileref to validate (generated by - mp_filtergenerate.sas) - @param [in] targetds The target dataset against which to verify the query - @param [out] abort= (YES) If YES will call mp_abort.sas on any exceptions - @param [out] outds= (work.mp_filtervalidate) Output dataset containing the - err / warning message, if one exists. If this table contains any rows, - there are problems! - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mf_nobs.sas - @li mp_abort.sas - -

Related Macros

- @li mp_filtercheck.sas - @li mp_filtergenerate.sas - - @version 9.3 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_filtervalidate(inref,targetds,abort=YES,outds=work.mp_filtervalidate); - -%mp_abort(iftrue= (&syscc ne 0 or &syserr ne 0) - ,mac=&sysmacroname - ,msg=%str(syscc=&syscc / syserr=&syserr - on macro entry) -) - -%local fref1; -%let fref1=%mf_getuniquefileref(); - -data _null_; - file &fref1; - infile &inref end=eof; - if _n_=1 then do; - put "proc sql;"; - put "validate select * from &targetds"; - put "where " ; - end; - input; - put _infile_; - putlog _infile_; - if eof then put ";quit;"; -run; - -%inc &fref1; - -data &outds; - if &sqlrc or &syscc or &syserr then do; - REASON_CD='VALIDATION_ERR'!!'OR: '!! - coalescec(symget('SYSERRORTEXT'),symget('SYSWARNINGTEXT')); - output; - end; - else stop; -run; - -filename &fref1 clear; - -%if %mf_nobs(&outds)>0 %then %do; - %if &abort=YES %then %do; - data _null_; - set &outds; - call symputx('REASON_CD',reason_cd,'l'); - stop; - run; - %mp_abort( - mac=&sysmacroname, - msg=%str(Filter validation issues.) - ) - %end; - %let syscc=1008; -%end; - -%mend mp_filtervalidate; diff --git a/009_macros/mp_getcols.sas b/009_macros/mp_getcols.sas deleted file mode 100644 index fb26036..0000000 --- a/009_macros/mp_getcols.sas +++ /dev/null @@ -1,68 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a dataset with column metadata. - @details This macro takes the `proc contents` output and "tidies it up" in the - following ways: - - @li Blank labels are filled in with column names - @li Formats are reconstructed with default values - @li Types such as DATE / TIME / DATETIME are inferred from the formats - - Example usage: - - %mp_getcols(sashelp.airline,outds=work.myds) - - @param [in] ds The dataset from which to obtain column metadata - @param [out] outds= (work.cols) The output dataset to create. Sample data: -|NAME:$32.|LENGTH:best.|VARNUM:best.|LABEL:$256.|FMTNAME:$32.|FORMAT:$49.|TYPE:$1.|DDTYPE:$9.| -|---|---|---|---|---|---|---|---| -|`AIR `|`8 `|`2 `|`international airline travel (thousands) `|` `|`8. `|`N `|`NUMERIC `| -|`DATE `|`8 `|`1 `|`DATE `|`MONYY `|`MONYY. `|`N `|`DATE `| -|`REGION `|`3 `|`3 `|`REGION `|` `|`$3. `|`C `|`CHARACTER `| - - -

Related Macros

- @li mf_getvarlist.sas - @li mm_getcols.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_getcols(ds, outds=work.cols); -%local dropds; -proc contents noprint data=&ds - out=_data_ (keep=name type length label varnum format:); -run; -%let dropds=&syslast; -data &outds(keep=name type length varnum format label ddtype fmtname); - set &dropds(rename=(format=fmtname type=type2)); - name=upcase(name); - if type2=2 then do; - length format $49.; - if fmtname='' then format=cats('$',length,'.'); - else if formatl=0 then format=cats(fmtname,'.'); - else format=cats(fmtname,formatl,'.'); - type='C'; - ddtype='CHARACTER'; - end; - else do; - if fmtname='' then format=cats(length,'.'); - else if formatl=0 then format=cats(fmtname,'.'); - else if formatd=0 then format=cats(fmtname,formatl,'.'); - else format=cats(fmtname,formatl,'.',formatd); - type='N'; - 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' - then ddtype='DATE'; - else if format=:'TIME' then ddtype='TIME'; - else ddtype='NUMERIC'; - end; - if label='' then label=name; -run; -proc sql; -drop table &dropds; -%mend mp_getcols; diff --git a/009_macros/mp_getconstraints.sas b/009_macros/mp_getconstraints.sas deleted file mode 100644 index 734eb1c..0000000 --- a/009_macros/mp_getconstraints.sas +++ /dev/null @@ -1,115 +0,0 @@ -/*** HELP START ***//** - @file mp_getconstraints.sas - @brief Get constraint details at column level - @details Useful for capturing constraints before they are dropped / reapplied - during an update. - - 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 unq unique(tx_from, dd_type), - constraint nnn not null(DD_SHORTDESC) - ); - - %mp_getconstraints(lib=work,ds=example,outds=work.constraints) - - @param [in] lib= (WORK) The target library - @param [in] ds= The target dataset. Leave blank (default) for all datasets. - @param [in] mdebug= (0) Set to 1 to preserve temp tables, print var values etc - @param [out] outds= (mp_getconstraints) the output dataset - -

SAS Macros

- @li mf_getuniquename.sas - @li mp_dropmembers.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_getconstraints(lib=WORK - ,ds= - ,outds=mp_getconstraints - ,mdebug=0 -)/*/STORE SOURCE*/; - -%let lib=%upcase(&lib); -%let ds=%upcase(&ds); - -/** - * Cater for environments where sashelp.vcncolu is not available - */ -%if %sysfunc(exist(sashelp.vcncolu,view))=0 %then %do; - proc sql; - create table &outds( - libref char(8) - ,TABLE_NAME char(32) - ,constraint_type char(8) label='Constraint Type' - ,constraint_name char(32) label='Constraint Name' - ,column_name char(32) label='Column' - ,constraint_order num - ); - %return; -%end; - -/** - * Neither dictionary tables nor sashelp provides a constraint order column, - * however they DO arrive in the correct order. So, create the col. - **/ -%local vw; -%let vw=%mf_getuniquename(prefix=mp_getconstraints_vw_); -data &vw /view=&vw; - set sashelp.vcncolu; - where table_catalog="&lib"; - - /* use retain approach to reset the constraint order with each constraint */ - length tmp $1000; - retain tmp; - drop tmp; - if tmp ne catx('|',table_catalog,table_name,constraint_name) then do; - constraint_order=1; - end; - else constraint_order+1; - tmp=catx('|',table_catalog, table_name,constraint_name); -run; - -/* must use SQL as proc datasets does not support length changes */ -proc sql noprint; -create table &outds as - select upcase(a.TABLE_CATALOG) as libref - ,upcase(a.TABLE_NAME) as TABLE_NAME - ,a.constraint_type - ,a.constraint_name - ,b.column_name - ,b.constraint_order - from dictionary.TABLE_CONSTRAINTS a - left join &vw b - on upcase(a.TABLE_CATALOG)=upcase(b.TABLE_CATALOG) - and upcase(a.TABLE_NAME)=upcase(b.TABLE_NAME) - and a.constraint_name=b.constraint_name -/** - * We cannot apply this clause to the underlying dictionary table. See: - * https://communities.sas.com/t5/SAS-Programming/Unexpected-Where-Clause-behaviour-in-dictionary-TABLE/m-p/771554#M244867 - * cannot use`where calculated libref="&lib"` either as it will STILL execute - * all the underlying constraint queries, causing exception errors in some - * cases: https://github.com/sasjs/core/issues/283 - */ - where a.TABLE_CATALOG="&lib" - %if "&ds" ne "" %then %do; - and upcase(a.TABLE_NAME)="&ds" - and upcase(b.TABLE_NAME)="&ds" - %end; - order by libref, table_name, constraint_name, constraint_order - ; - -/* tidy up */ -%mp_dropmembers( - &vw, - iftrue=(&mdebug=0) -) - -%mend mp_getconstraints; diff --git a/009_macros/mp_getdbml.sas b/009_macros/mp_getdbml.sas deleted file mode 100644 index 292d5a9..0000000 --- a/009_macros/mp_getdbml.sas +++ /dev/null @@ -1,335 +0,0 @@ -/*** HELP START ***//** - @file - @brief Extract DBML from SAS Libraries - @details DBML is an open source markup format to represent databases. - More details: https://www.dbml.org/home/ - - Usage: - - - %mp_getdbml(liblist=SASHELP WORK,outref=mydbml,showlog=YES) - - Take the log output and paste it into the renderer at https://dbdiagram.io - to view your data model diagram. The code takes a "best guess" at - the one to one and one to many relationships (based on constraints - and indexes, and assuming that the column names would match). - - You may need to adjust the rendered DBML to suit your needs. - - ![dbml for sas](https://i.imgur.com/8T1tIZp.gif) - - -

SAS Macros

- @li mf_getquotedstr.sas - @li mp_getconstraints.sas - - @param [in] liblist= (SASHELP) Space seperated list of librefs to take as - input - @param [out] outref= (getdbml) Fileref to contain the DBML - @param [in] showlog= (NO) set to YES to show the DBML in the log - - @version 9.3 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mp_getdbml(liblist=SASHELP,outref=getdbml,showlog=NO -)/*/STORE SOURCE*/; - -/* check fileref is assigned */ -%if %sysfunc(fileref(&outref)) > 0 %then %do; - filename &outref temp; -%end; - -%let liblist=%upcase(&liblist); - -proc sql noprint; -create table _data_ as - select * from dictionary.tables - where upcase(libname) in (%mf_getquotedstr(&liblist)) - order by libname,memname; -%local tabinfo; %let tabinfo=&syslast; - -create table _data_ as - select * from dictionary.columns - where upcase(libname) in (%mf_getquotedstr(&liblist)) - order by libname,memname,varnum; -%local colinfo; %let colinfo=&syslast; - -%local dsnlist; - select distinct upcase(cats(libname,'.',memname)) into: dsnlist - separated by ' ' - from &syslast -; - -create table _data_ as - select * from dictionary.indexes - where upcase(libname) in (%mf_getquotedstr(&liblist)) - order by idxusage, indxname, indxpos; -%local idxinfo; %let idxinfo=&syslast; - -/* Extract all Primary Key and Unique data constraints */ -%mp_getconstraints(lib=%scan(&liblist,1),outds=_data_) -%local colconst; %let colconst=&syslast; - -%do x=2 %to %sysfunc(countw(&liblist)); - %mp_getconstraints(lib=%scan(&liblist,&x),outds=_data_) - proc append base=&colconst data=&syslast; - run; -%end; - - - - -/* header info */ -data _null_; - file &outref; - put "// DBML generated by &sysuserid on %sysfunc(datetime(),datetime19.) "; - put "Project sasdbml {"; - put " database_type: 'SAS'"; - put " Note: 'Generated by the mp_getdbml() macro'"; - put "}"; -run; - -/* create table groups */ -data _null_; - file &outref mod; - set &tabinfo; - by libname; - if first.libname then put "TableGroup " libname "{"; - ds=quote(cats(libname,'.',memname)); - put ' ' ds; - if last.libname then put "}"; -run; - -/* table for pks */ -data _data_; - length curds const col $39; - call missing (of _all_); - stop; -run; -%let pkds=&syslast; - -%local x curds constraints_used constcheck; -%do x=1 %to %sysfunc(countw(&dsnlist,%str( ))); - %let curds=%scan(&dsnlist,&x,%str( )); - %let constraints_used=; - %let constcheck=0; - data _null_; - file &outref mod; - length lab $1024 typ $20; - set &colinfo (where=( - libname="%scan(&curds,1,.)" and upcase(memname)="%scan(&curds,2,.)" - )) end=last; - - if _n_=1 then do; - table='Table "'!!"&curds"!!'"{'; - put table; - end; - name=upcase(name); - lab=" note:"!!quote(trim(tranwrd(label,'"',"'"))); - if upcase(format)=:'DATETIME' then typ='datetime'; - else if type='char' then typ=cats('char(',length,')'); - else typ='num'; - - if notnull='yes' then notnul=' not null'; - if notnull='no' and missing(label) then put ' ' name typ; - else if notnull='yes' and missing(label) then do; - put ' ' name typ '[' notnul ']'; - end; - else if notnull='no' then put ' ' name typ '[' lab ']'; - else put ' ' name typ '[' notnul ',' lab ']'; - - run; - - data _data_(keep=curds const col); - length ctype $11 cols constraints_used $5000; - set &colconst (where=( - upcase(libref)="%scan(&curds,1,.)" - and upcase(table_name)="%scan(&curds,2,.)" - and constraint_type in ('PRIMARY','UNIQUE') - )) end=last; - file &outref mod; - by constraint_type constraint_name; - retain cols; - column_name=upcase(column_name); - - if _n_=1 then put / ' indexes {'; - - if upcase(strip(constraint_type)) = 'PRIMARY' then ctype='[pk]'; - else ctype='[unique]'; - - if first.constraint_name then cols = cats('(',column_name); - else cols=cats(cols,',',column_name); - - if last.constraint_name then do; - cols=cats(cols,')',ctype)!!' //'!!constraint_name; - put ' ' cols; - constraints_used=catx(' ',constraints_used, constraint_name); - call symputx('constcheck',1); - end; - - if last then call symput('constraints_used',cats(upcase(constraints_used))); - - length curds const col $39; - curds="&curds"; - const=constraint_name; - col=column_name; - run; - - proc append base=&pkds data=&syslast;run; - - /* Create Unique Indexes, but only if they were not already defined within - the Constraints section. */ - data _data_(keep=curds const col); - set &idxinfo (where=( - libname="%scan(&curds,1,.)" - and upcase(memname)="%scan(&curds,2,.)" - and unique='yes' - and upcase(indxname) not in (%mf_getquotedstr(&constraints_used)) - )); - file &outref mod; - by idxusage indxname; - name=upcase(name); - if &constcheck=1 then stop; /* we only care about PKs so stop if we have */ - if _n_=1 and &constcheck=0 then put / ' indexes {'; - - length cols $5000; - retain cols; - if first.indxname then cols = cats('(',name); - else cols=cats(cols,',',name); - - if last.indxname then do; - cols=cats(cols,')[unique]')!!' //'!!indxname; - put ' ' cols; - call symputx('constcheck',1); - end; - - length curds const col $39; - curds="&curds"; - const=indxname; - col=name; - run; - proc append base=&pkds data=&syslast;run; - - data _null_; - file &outref mod; - if &constcheck =1 then put ' }'; - put '}'; - run; - -%end; - -/** - * now we need to figure out the relationships - */ - -/* sort alphabetically so we can have one set of unique cols per table */ -proc sort data=&pkds nodupkey; - by curds const col; -run; - -data &pkds.1 (keep=curds col) - &pkds.2 (keep=curds cols); - set &pkds; - by curds const; - length retconst $39 cols $5000; - retain retconst cols; - if first.curds then do; - retconst=const; - cols=upcase(col); - end; - else cols=catx(' ',cols,upcase(col)); - if retconst=const then do; - output &pkds.1; - if last.const then output &pkds.2; - end; -run; - -%let curdslist="0"; -%do x=1 %to %sysfunc(countw(&dsnlist,%str( ))); - %let curds=%scan(&dsnlist,&x,%str( )); - - %let pkcols=0; - data _null_; - set &pkds.2(where=(curds="&curds")); - call symputx('pkcols',cols); - run; - %if &pkcols ne 0 %then %do; - %let curdslist=&curdslist,"&curds"; - - /* start with one2one */ - data &pkds.4; - file &outref mod; - set &pkds.2(where=(cols="&pkcols" and curds not in (&curdslist))); - line='Ref: "'!!"&curds" - !!cats('".(',"%mf_getquotedstr(&pkcols,dlm=%str(,),quote=%str( ))",')') - !!' - ' - !!cats(quote(trim(curds)) - ,'.(' - ,"%mf_getquotedstr(&pkcols,dlm=%str(,),quote=%str( ))" - ,')' - ); - put line; - run; - - /* now many2one */ - /* get table with one row per col */ - data &pkds.5; - set &pkds.1(where=(curds="&curds")); - run; - /* get tables which contain the PK columns */ - proc sql; - create table &pkds.5a as - select upcase(cats(b.libname,'.',b.memname)) as curds - ,b.name - from &pkds.5 a - inner join &colinfo b - on a.col=upcase(b.name); - /* count to make sure those tables contain ALL the columns */ - create table &pkds.5b as - select curds,count(*) as cnt - from &pkds.5a - where curds not in ( - select curds from &pkds.2 where cols="&pkcols" - ) /* not a one to one match */ - and curds ne "&curds" /* exclude self */ - group by 1; - create table &pkds.6 as - select a.* - ,b.cols - from &pkds.5b a - left join &pkds.4 b - on a.curds=b.curds; - - data _null_; - set &pkds.6; - file &outref mod; - colcnt=%sysfunc(countw(&pkcols)); - if cnt=colcnt then do; - /* table contains all the PK cols, and was not a direct / 121 match */ - line='Ref: "'!!"&curds" - !!'".(' - !!"%mf_getquotedstr(&pkcols,dlm=%str(,),quote=%str( ))" - !!') > ' - !!cats(quote(trim(curds)) - ,'.(' - ,"%mf_getquotedstr(&pkcols,dlm=%str(,),quote=%str( ))" - ,')' - ); - put line; - end; - run; - %end; -%end; - - -%if %upcase(&showlog)=YES %then %do; - options ps=max; - data _null_; - infile &outref; - input; - putlog _infile_; - run; -%end; - -%mend mp_getdbml; diff --git a/009_macros/mp_getddl.sas b/009_macros/mp_getddl.sas deleted file mode 100644 index 6bbf8f0..0000000 --- a/009_macros/mp_getddl.sas +++ /dev/null @@ -1,405 +0,0 @@ -/*** HELP START ***//** - @file mp_getddl.sas - @brief Extract DDL in various formats, by table or library - @details Data Definition Language relates to a set of SQL instructions used - to create tables in SAS or a database. The macro can be used at table or - library level. The default behaviour is to create DDL in SAS format. - - Note - views are not currently supported. - - Usage: - - data test(index=(pk=(x y)/unique /nomiss)); - x=1; - y='blah'; - label x='blah'; - run; - proc sql; describe table &syslast; - %mp_getddl(work,test,flavour=tsql,showlog=YES) - -

SAS Macros

- @li mf_existfileref.sas - @li mf_getvarcount.sas - @li mp_getconstraints.sas - - @param [in] libref Libref of the library to create DDL for. Should already - be assigned. - @param [in] ds dataset to create ddl for (optional) - @param [in] fref= (getddl) the fileref to which to _append_ the DDL. If it - does not exist, it will be created. - @param [in] flavour= (SAS) The type of DDL to create. Options: - @li SAS - @li TSQL - - @param [in]showlog= (NO) Set to YES to show the DDL in the log - @param [in] schema= () Choose a preferred schema name (default is to use - actual schema, else libref) - @param [in] applydttm= (NO) For non SAS DDL, choose if columns are created - with native datetime2 format or regular decimal type - - @version 9.3 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mp_getddl(libref,ds,fref=getddl,flavour=SAS,showlog=NO,schema= - ,applydttm=NO -)/*/STORE SOURCE*/; - -/* check fileref is assigned */ -%if %mf_existfileref(&fref)=0 %then %do; - filename &fref temp ; -%end; - -%if %length(&libref)=0 %then %let libref=WORK; -%let flavour=%upcase(&flavour); - -proc sql noprint; -create table _data_ as - select * from dictionary.tables - where upcase(libname)="%upcase(&libref)" - and memtype='DATA' /* views not currently supported */ - %if %length(&ds)>0 %then %do; - and upcase(memname)="%upcase(&ds)" - %end; - ; -%local tabinfo; %let tabinfo=&syslast; - -create table _data_ as - select * from dictionary.columns - where upcase(libname)="%upcase(&libref)" - %if %length(&ds)>0 %then %do; - and upcase(memname)="%upcase(&ds)" - %end; - ; -%local colinfo; %let colinfo=&syslast; - -%local dsnlist; - select distinct upcase(memname) into: dsnlist - separated by ' ' - from &syslast -; - -create table _data_ as - select * from dictionary.indexes - where upcase(libname)="%upcase(&libref)" - %if %length(&ds)>0 %then %do; - and upcase(memname)="%upcase(&ds)" - %end; - order by idxusage, indxname, indxpos - ; -%local idxinfo; %let idxinfo=&syslast; - -/* Extract all Primary Key and Unique data constraints */ -%mp_getconstraints(lib=%upcase(&libref),ds=%upcase(&ds),outds=_data_) -%local colconst; %let colconst=&syslast; - -%macro addConst(); - %global constraints_used; - data _null_; - length ctype $11 constraint_name_orig $256 constraints_used $5000; - set &colconst( - where=(table_name="&curds" and constraint_type in ('PRIMARY','UNIQUE')) - ) end=last; - file &fref mod; - by constraint_type constraint_name; - retain constraints_used; - constraint_name_orig=constraint_name; - if upcase(strip(constraint_type)) = 'PRIMARY' then ctype='PRIMARY KEY'; - else ctype=strip(constraint_type); - %if &flavour=TSQL %then %do; - column_name=catt('[',column_name,']'); - constraint_name=catt('[',constraint_name,']'); - %end; - %else %if &flavour=PGSQL %then %do; - column_name=catt('"',column_name,'"'); - constraint_name=catt('"',constraint_name,'"'); - %end; - if first.constraint_name then do; - constraints_used = catx(' ', constraints_used, constraint_name_orig); - put " ,CONSTRAINT " constraint_name ctype "(" ; - put ' ' column_name; - end; - else put ' ,' column_name; - if last.constraint_name then do; - put " )"; - call symput('constraints_used',strip(constraints_used)); - end; - run; - %put &=constraints_used; -%mend addConst; - -data _null_; - file &fref mod; - put "/* DDL generated by &sysuserid on %sysfunc(datetime(),datetime19.) */"; -run; - -%local x curds; -%if &flavour=SAS %then %do; - %do x=1 %to %sysfunc(countw(&dsnlist)); - %let curds=%scan(&dsnlist,&x); - data _null_; - file &fref mod; - put "/* SAS Flavour DDL for %upcase(&libref).&curds */"; - put "proc sql;"; - run; - data _null_; - file &fref mod; - length lab $1024 typ $20; - set &colinfo (where=(upcase(memname)="&curds")) end=last; - - if _n_=1 then do; - if memtype='DATA' then do; - put "create table &libref..&curds("; - end; - else do; - /* just a placeholder - we filter out views at the top */ - put "create view &libref..&curds("; - end; - put " "@@; - end; - else put " ,"@@; - if length(format)>1 then fmt=" format="!!cats(format); - if length(label)>1 then - lab=" label="!!cats("'",tranwrd(label,"'","''"),"'"); - if notnull='yes' then notnul=' not null'; - if type='char' then typ=cats('char(',length,')'); - else if length ne 8 then typ='num length='!!cats(length); - else typ='num'; - put name typ fmt notnul lab; - run; - - /* Extra step for data constraints */ - %addConst() - - data _null_; - file &fref mod; - put ');'; - run; - - /* Create Unique Indexes, but only if they were not already defined within - the Constraints section. */ - data _null_; - *length ds $128; - set &idxinfo( - where=( - memname="&curds" - and unique='yes' - and indxname not in ( - %sysfunc(tranwrd("&constraints_used",%str( ),%str(","))) - ) - ) - ); - file &fref mod; - by idxusage indxname; -/* ds=cats(libname,'.',memname); */ - if first.indxname then do; - put 'CREATE UNIQUE INDEX ' indxname "ON &libref..&curds (" ; - put ' ' name ; - end; - else put ' ,' name ; - *else put ' ,' name ; - if last.indxname then do; - put ');'; - end; - run; - -/* - ods output IntegrityConstraints=ic; - proc contents data=testali out2=info; - run; - */ - %end; -%end; -%else %if &flavour=TSQL %then %do; - /* if schema does not exist, set to be same as libref */ - %local schemaactual; - proc sql noprint; - select sysvalue into: schemaactual - from dictionary.libnames - where upcase(libname)="&libref" and engine='SQLSVR'; - %let schema=%sysfunc(coalescec(&schemaactual,&schema,&libref)); - - %do x=1 %to %sysfunc(countw(&dsnlist)); - %let curds=%scan(&dsnlist,&x); - data _null_; - file &fref mod; - put "/* TSQL Flavour DDL for &schema..&curds */"; - data _null_; - file &fref mod; - set &colinfo (where=(upcase(memname)="&curds")) end=last; - if _n_=1 then do; - if memtype='DATA' then do; - put "create table [&schema].[&curds]("; - end; - else do; - /* just a placeholder - we filter out views at the top */ - put "create view [&schema].[&curds]("; - end; - put " "@@; - end; - else put " ,"@@; - format=upcase(format); - if 1=0 then; /* dummy if */ - %if &applydttm=YES %then %do; - else if format=:'DATETIME' then fmt='[datetime2](7) '; - %end; - else if type='num' then fmt='[decimal](18,2)'; - else if length le 8000 then fmt='[varchar]('!!cats(length)!!')'; - else fmt=cats('[varchar](max)'); - if notnull='yes' then notnul=' NOT NULL'; - put "[" name +(-1) "]" fmt notnul; - run; - - /* Extra step for data constraints */ - %addConst() - - /* Create Unique Indexes, but only if they were not already defined within - the Constraints section. */ - data _null_; - *length ds $128; - set &idxinfo( - where=( - memname="&curds" - and unique='yes' - and indxname not in ( - %sysfunc(tranwrd("&constraints_used",%str( ),%str(","))) - ) - ) - ); - file &fref mod; - by idxusage indxname; - *ds=cats(libname,'.',memname); - if first.indxname then do; - /* add nonclustered in case of multiple unique indexes */ - put ' ,index [' indxname +(-1) '] UNIQUE NONCLUSTERED ('; - put ' [' name +(-1) ']'; - end; - else put ' ,[' name +(-1) ']'; - if last.indxname then do; - put ' )'; - end; - run; - - data _null_; - file &fref mod; - put ')'; - put 'GO'; - run; - - /* add extended properties for labels */ - data _null_; - file &fref mod; - length nm $64 lab $1024; - set &colinfo (where=(upcase(memname)="&curds" and label ne '')) end=last; - nm=cats("N'",tranwrd(name,"'","''"),"'"); - lab=cats("N'",tranwrd(label,"'","''"),"'"); - put ' '; - put "EXEC sys.sp_addextendedproperty "; - put " @name=N'MS_Description',@value=" lab ; - put " ,@level0type=N'SCHEMA',@level0name=N'&schema' "; - put " ,@level1type=N'TABLE',@level1name=N'&curds'"; - put " ,@level2type=N'COLUMN',@level2name=" nm ; - if last then put 'GO'; - run; - %end; -%end; -%else %if &flavour=PGSQL %then %do; - /* if schema does not exist, set to be same as libref */ - %local schemaactual; - proc sql noprint; - select sysvalue into: schemaactual - from dictionary.libnames - where upcase(libname)="&libref" and engine='POSTGRES'; - %let schema=%sysfunc(coalescec(&schemaactual,&schema,&libref)); - data _null_; - file &fref mod; - put "CREATE SCHEMA &schema;"; - %do x=1 %to %sysfunc(countw(&dsnlist)); - %let curds=%scan(&dsnlist,&x); - %local curdsvarcount; - %let curdsvarcount=%mf_getvarcount(&libref..&curds); - %if &curdsvarcount>1600 %then %do; - data _null_; - file &fref mod; - put "/* &libref..&curds contains &curdsvarcount vars */"; - put "/* Postgres cannot create tables with over 1600 vars */"; - put "/* No DDL will be generated for this table"; - run; - %end; - %else %do; - data _null_; - file &fref mod; - put "/* Postgres Flavour DDL for &schema..&curds */"; - data _null_; - file &fref mod; - set &colinfo (where=(upcase(memname)="&curds")) end=last; - length fmt $32; - if _n_=1 then do; - if memtype='DATA' then do; - put "CREATE TABLE &schema..&curds ("; - end; - else do; - /* just a placeholder - we filter out views at the top */ - put "CREATE VIEW &schema..&curds ("; - end; - put " "@@; - end; - else put " ,"@@; - format=upcase(format); - if 1=0 then; /* dummy if */ - %if &applydttm=YES %then %do; - else if format=:'DATETIME' then fmt=' TIMESTAMP '; - %end; - else if type='num' then fmt=' DOUBLE PRECISION'; - else fmt='VARCHAR('!!cats(length)!!')'; - if notnull='yes' then notnul=' NOT NULL'; - /* quote column names in case they represent reserved words */ - name2=quote(trim(name)); - put name2 fmt notnul; - run; - - /* Extra step for data constraints */ - %addConst() - - data _null_; - file &fref mod; - put ');'; - run; - - /* Create Unique Indexes, but only if they were not already defined within - the Constraints section. */ - data _null_; - *length ds $128; - set &idxinfo( - where=( - memname="&curds" - and unique='yes' - and indxname not in ( - %sysfunc(tranwrd("&constraints_used",%str( ),%str(","))) - ) - ) - ); - file &fref mod; - by idxusage indxname; - if first.indxname then do; - put 'CREATE UNIQUE INDEX "' indxname +(-1) '" ' "ON &schema..&curds("; - put ' "' name +(-1) '"' ; - end; - else put ' ,"' name +(-1) '"'; - if last.indxname then do; - put ');'; - end; - run; - %end; - %end; -%end; -%if %upcase(&showlog)=YES %then %do; - options ps=max; - data _null_; - infile &fref; - input; - putlog _infile_; - run; -%end; - -%mend mp_getddl; diff --git a/009_macros/mp_getformats.sas b/009_macros/mp_getformats.sas deleted file mode 100644 index 974e1a6..0000000 --- a/009_macros/mp_getformats.sas +++ /dev/null @@ -1,134 +0,0 @@ -/*** HELP START ***//** - @file - @brief Export format definitions - @details Formats are exported from the first (if any) catalog entry in the - FMTSEARCH path. - - Formats are taken from the library / dataset reference and / or a static - format list. - - Note - the source for this information is the dictionary.formats table. This - cannot show formats that are not already declared in the FMTSEARCH path. - - Example usage: - - %mp_getformats(lib=sashelp,ds=prdsale,outsummary=work.dictable) - - %mp_getformats(fmtlist=FORMAT1 $FORMAT2 @INFMT3,outsummary=work.table2) - - @param [in] lib= (0) The libref for which to return formats. - @todo Enable exporting of formats for an entire library - @param [in] ds= (0) The dataset from which to obtain format definitions - @param [in] fmtlist= (0) A list of additional format names - @param [out] outsummary= (work.mp_getformats_summary) Output dataset - containing summary definitions - structure taken from dictionary.formats as - follows: - - |libname:$8.|memname:$32.|path:$1024.|objname:$32.|fmtname:$32.|fmttype:$1.|source:$1.|minw:best.|mind:best.|maxw:best.|maxd:best.|defw:best.|defd:best.| - |---|---|---|---|---|---|---|---|---|---|---|---|---| - | | | | |$|F|B|1|0|32767|0|1|0| - | | | | |$|I|B|1|0|32767|0|1|0| - |` `|` `|/opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe|UWIANYDT|$ANYDTIF|I|U|1|0|60|0|19|0| - | | |/opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe|UWFASCII|$ASCII|F|U|1|0|32767|0|1|0| - | | |/opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe|UWIASCII|$ASCII|I|U|1|0|32767|0|1|0| - | | |/opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe|UWFBASE6|$BASE64X|F|U|1|0|32767|0|1|0| - - - @param [out] outdetail= (0) Provide an output dataset in which to export all - the custom format definitions (from proc format CNTLOUT). Definitions: -https://support.sas.com/documentation/cdl/en/proc/61895/HTML/default/viewer.htm#a002473477.htm - Sample data: - - |FMTNAME:$32.|START:$16.|END:$16.|LABEL:$256.|MIN:best.|MAX:best.|DEFAULT:best.|LENGTH:best.|FUZZ:best.|PREFIX:$2.|MULT:best.|FILL:$1.|NOEDIT:best.|TYPE:$1.|SEXCL:$1.|EEXCL:$1.|HLO:$13.|DECSEP:$1.|DIG3SEP:$1.|DATATYPE:$8.|LANGUAGE:$8.| - |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| - |`WHICHPATH `|`0 `|`0 `|`path1 `|`1 `|`40 `|`28 `|`28 `|`1E-12 `|` `|`0 `|` `|`0 `|`N `|`N `|`N `|` `|` `|` `|` `|` `| - |`WHICHPATH `|`**OTHER** `|`**OTHER** `|`big fat problem if not path1 `|`1 `|`40 `|`28 `|`28 `|`1E-12 `|` `|`0 `|` `|`0 `|`N `|`N `|`N `|`O `|` `|` `|` `|` `| - -

SAS Macros

- @li mddl_sas_cntlout.sas - @li mf_dedup.sas - @li mf_getfmtlist.sas - @li mf_getfmtname.sas - @li mf_getquotedstr.sas - @li mf_getuniquename.sas - - -

Related Macros

- @li mf_getfmtlist.sas - @li mp_applyformats.sas - @li mp_cntlout.sas - @li mp_getformats.test.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_getformats(lib=0 - ,ds=0 - ,fmtlist=0 - ,outsummary=work.mp_getformats_summary - ,outdetail=0 -); - -%local i fmt allfmts tempds fmtcnt; - -%if "&fmtlist" ne "0" %then %do i=1 %to %sysfunc(countw(&fmtlist,%str( ))); - /* ensure format list contains format _name_ only */ - %let fmt=%scan(&fmtlist,&i,%str( )); - %let fmt=%mf_getfmtname(&fmt); - %let allfmts=&allfmts &fmt; -%end; - -%if &ds=0 and &lib ne 0 %then %do; - /* grab formats from library */ - /* to do */ -%end; -%else %if &ds ne 0 and &lib ne 0 %then %do; - /* grab formats from dataset */ - %let allfmts=%mf_getfmtlist(&lib..&ds) &allfmts; -%end; - -/* ensure list is unique */ -%let allfmts=%mf_dedup(%upcase(&allfmts)); - -/* create summary table */ -%if %index(&outsummary,.)=0 %then %let outsummary=WORK.&outsummary; -proc sql; -create table &outsummary as - select * from dictionary.formats - where fmtname in (%mf_getquotedstr(&allfmts,quote=D)); - -%if "&outdetail" ne "0" %then %do; - /* ensure base table always exists */ - %mddl_sas_cntlout(libds=&outdetail) - /* grab the location of each format */ - %let fmtcnt=0; - data _null_; - set &outsummary; - if not missing(libname); - x+1; - call symputx(cats('fmtloc',x),cats(libname,'.',memname),'l'); - call symputx(cats('fmtname',x),fmtname,'l'); - call symputx('fmtcnt',x,'l'); - run; - /* export each format and append to the output table */ - %let tempds=%mf_getuniquename(prefix=mp_getformats); - %do i=1 %to &fmtcnt; - proc format library=&&fmtloc&i CNTLOUT=&tempds; - select &&fmtname&i; - run; - data &tempds; - if 0 then set &outdetail; - set &tempds; - /* set fmtrow (position of record within the format) */ - by type fmtname notsorted; - if first.fmtname then fmtrow=1; - else fmtrow+1; - run; - proc append base=&outdetail data=&tempds ; - run; - %end; -%end; - -%mend mp_getformats; diff --git a/009_macros/mp_getmaxvarlengths.sas b/009_macros/mp_getmaxvarlengths.sas deleted file mode 100644 index aff390a..0000000 --- a/009_macros/mp_getmaxvarlengths.sas +++ /dev/null @@ -1,130 +0,0 @@ -/*** HELP START ***//** - @file - @brief Scans a dataset to find the max length of the variable values - @details - This macro will scan a base dataset and produce an output dataset with two - columns: - - - NAME Name of the base dataset column - - MAXLEN Maximum length of the data contained therein. - - Character fields are often allocated very large widths (eg 32000) of which the - maximum value is likely to be much narrower. Identifying such cases can be - helpful in the following scenarios: - - @li Enabling a HTML table to be appropriately sized (`num2char=YES`) - @li Reducing the size of a dataset to save on storage (mp_ds2squeeze.sas) - @li Identifying columns containing nothing but missing values (`MAXLEN=0` in - the output table) - - If the entire column is made up of (non-special) missing values then a value - of 0 is returned. - - Usage: - - %mp_getmaxvarlengths(sashelp.class,outds=work.myds) - - @param [in] libds Two part dataset (or view) reference. - @param [in] num2char= (NO) When set to NO, numeric fields are sized according - to the number of bytes used (or set to zero in the case of non-special - missings). When YES, the numeric field is converted to character (using the - format, if available), and that is sized instead, using `lengthn()`. - @param [out] outds= The output dataset to create, eg: - |NAME:$8.|MAXLEN:best.| - |---|---| - |`Name `|`7 `| - |`Sex `|`1 `| - |`Age `|`3 `| - |`Height `|`8 `| - |`Weight `|`3 `| - -

SAS Macros

- @li mcf_length.sas - @li mf_getuniquename.sas - @li mf_getvarcount.sas - @li mf_getvarlist.sas - @li mf_getvartype.sas - @li mf_getvarformat.sas - - @version 9.2 - @author Allan Bowe - -

Related Macros

- @li mp_ds2squeeze.sas - @li mp_getmaxvarlengths.test.sas - -**//*** HELP END ***/ - -%macro mp_getmaxvarlengths( - libds - ,num2char=NO - ,outds=work.mp_getmaxvarlengths -)/*/STORE SOURCE*/; - -%local vars prefix x var fmt srcds; -%let vars=%mf_getvarlist(libds=&libds); -%let prefix=%substr(%mf_getuniquename(),1,25); -%let num2char=%upcase(&num2char); - -%if &num2char=NO %then %do; - /* compile length function for numeric fields */ - %mcf_length(wrap=YES, insert_cmplib=YES) -%end; - -%if &num2char=NO - and ("%substr(&sysver,1,1)"="4" or "%substr(&sysver,1,1)"="5") - and %mf_getvarcount(&libds,typefilter=N) gt 0 -%then %do; - /* custom functions not supported in summary operations */ - %let srcds=%mf_getuniquename(); - data &srcds/view=&srcds; - set &libds; - %do x=1 %to %sysfunc(countw(&vars,%str( ))); - %let var=%scan(&vars,&x); - %if %mf_getvartype(&libds,&var)=N %then %do; - &prefix.&x=mcf_length(&var); - %end; - %end; - run; -%end; -%else %let srcds=&libds; - -proc sql; -create table &outds (rename=( - %do x=1 %to %sysfunc(countw(&vars,%str( ))); - &prefix.&x=%scan(&vars,&x) - %end; - )) - as select - %do x=1 %to %sysfunc(countw(&vars,%str( ))); - %let var=%scan(&vars,&x); - %if &x>1 %then ,; - %if %mf_getvartype(&libds,&var)=C %then %do; - max(lengthn(&var)) as &prefix.&x - %end; - %else %if &num2char=YES %then %do; - %let fmt=%mf_getvarformat(&libds,&var); - %put fmt=&fmt; - %if %str(&fmt)=%str() %then %do; - max(lengthn(cats(&var))) as &prefix.&x - %end; - %else %do; - max(lengthn(put(&var,&fmt))) as &prefix.&x - %end; - %end; - %else %do; - %if "%substr(&sysver,1,1)"="4" or "%substr(&sysver,1,1)"="5" %then %do; - max(&prefix.&x) as &prefix.&x - %end; - %else %do; - max(mcf_length(&var)) as &prefix.&x - %end; - %end; - %end; - from &srcds; - - proc transpose data=&outds - out=&outds(rename=(_name_=NAME COL1=MAXLEN)); - run; - -%mend mp_getmaxvarlengths; diff --git a/009_macros/mp_getpk.sas b/009_macros/mp_getpk.sas deleted file mode 100644 index 05e6da4..0000000 --- a/009_macros/mp_getpk.sas +++ /dev/null @@ -1,281 +0,0 @@ -/*** HELP START ***//** - @file - @brief Extract the primary key fields from a table or library - @details Examines the constraints to identify primary key fields - indicated - by an explicit PK constraint, or a unique index that is also NOT NULL. - - Can be executed at both table and library level. Supports both BASE engine - libraries and SQL Server. - - 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 unq unique(tx_from, dd_type), - constraint nnn not null(DD_SHORTDESC) - ); - %mp_getpk(work,ds=example) - - 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 - @param [in] mdebug= (0) Set to 1 to preserve temp tables, print var values etc - @param [out] outds= (work.mp_getpk) The name of the output table to create. - -

SAS Macros

- @li mf_existfeature.sas - @li mf_getengine.sas - @li mf_getschema.sas - @li mp_dropmembers.sas - @li mp_getconstraints.sas - -

Related Macros

- @li mp_getpk.test.sas - @li mp_guesspk.sas - - @version 9.3 - @author Macro People Ltd -**//*** HELP END ***/ - -%macro mp_getpk( - lib, - ds=0, - outds=work.mp_getpk, - mdebug=0 -)/*/STORE SOURCE*/; - - -%local engine schema ds1 ds2 ds3 dsn tabs1 tabs2 sum pk4sure pkdefault finalpks - pkfromindex; - -%let lib=%upcase(&lib); -%let ds=%upcase(&ds); -%let engine=%mf_getengine(&lib); -%let schema=%mf_getschema(&lib); - -%let ds1=%mf_getuniquename(prefix=getpk_ds1); -%let ds2=%mf_getuniquename(prefix=getpk_ds2); -%let ds3=%mf_getuniquename(prefix=getpk_ds3); -%let tabs1=%mf_getuniquename(prefix=getpk_tabs1); -%let tabs2=%mf_getuniquename(prefix=getpk_tabs2); -%let sum=%mf_getuniquename(prefix=getpk_sum); -%let pk4sure=%mf_getuniquename(prefix=getpk_pk4sure); -%let pkdefault=%mf_getuniquename(prefix=getpk_pkdefault); -%let pkfromindex=%mf_getuniquename(prefix=getpk_pkfromindex); -%let finalpks=%mf_getuniquename(prefix=getpk_finalpks); - -%local dbg; -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; -%end; -%else %let dbg=*; - -proc sql; -create table &ds1 as - select libname as libref - ,upcase(memname) as dsn - ,memtype - ,upcase(name) as name - ,type - ,length - ,varnum - ,label - ,format - ,idxusage - ,notnull - from dictionary.columns - where upcase(libname)="&lib" -%if &ds ne 0 %then %do; - and upcase(memname)="&ds" -%end; - ; - - -%if &engine=SQLSVR %then %do; - proc sql; - connect using &lib; - create table work.&ds2 as - select * from connection to &lib( - select - s.name as SchemaName, - t.name as memname, - tc.name as name, - ic.key_ordinal as KeyOrderNr - from - sys.schemas s - inner join sys.tables t on s.schema_id=t.schema_id - inner join sys.indexes i on t.object_id=i.object_id - inner join sys.index_columns ic on i.object_id=ic.object_id - and i.index_id=ic.index_id - inner join sys.columns tc on ic.object_id=tc.object_id - and ic.column_id=tc.column_id - where i.is_primary_key=1 - and s.name=%str(%')&schema%str(%') - order by t.name, ic.key_ordinal ; - );disconnect from &lib; - create table &ds3 as - select a.* - ,case when b.name is not null then 1 else 0 end as pk_ind - from work.&ds1 a - left join work.&ds2 b - on a.dsn=b.memname - and upcase(a.name)=upcase(b.name) - order by libref,dsn; -%end; -%else %do; - - %if &ds = 0 %then %let dsn=; - - /* get all constraints, in constraint order*/ - %mp_getconstraints(lib=&lib,ds=&dsn,outds=work.&ds2) - - /* extract cols that are clearly primary keys */ - proc sql; - create table &pk4sure as - select libref - ,table_name - ,constraint_name - ,constraint_order - ,column_name as name - from work.&ds2 - where constraint_type='PRIMARY' - order by 1,2,3,4; - - /* extract unique constraints where every col is also NOT NULL */ - proc sql; - create table &sum as - select a.libref - ,a.table_name - ,a.constraint_name - ,count(a.column_name) as unq_cnt - ,count(b.column_name) as nul_cnt - from work.&ds2(where=(constraint_type ='UNIQUE')) a - left join work.&ds2(where=(constraint_type ='NOT NULL')) b - on a.libref=b.libref - and a.table_name=b.table_name - and a.column_name=b.column_name - group by 1,2,3 - having unq_cnt=nul_cnt; - - /* extract cols from the relevant unique constraints */ - create table &pkdefault as - select a.libref - ,a.table_name - ,a.constraint_name - ,b.constraint_order - ,b.column_name as name - from &sum a - left join &ds2(where=(constraint_type ='UNIQUE')) b - on a.libref=b.libref - and a.table_name=b.table_name - and a.constraint_name=b.constraint_name - order by 1,2,3,4; - - /* extract cols from the relevant unique INDEXES */ - create table &pkfromindex as - select libname as libref - ,memname as table_name - ,indxname as constraint_name - ,indxpos as constraint_order - ,name - from dictionary.indexes - where nomiss='yes' and unique='yes' and upcase(libname)="&lib" - %if &ds ne 0 %then %do; - and upcase(memname)="&ds" - %end; - order by 1,2,3,4; - - /* create one table */ - data &finalpks; - set &pkdefault &pk4sure &pkfromindex; - pk_ind=1; - /* if there are multiple unique constraints, take the first */ - by libref table_name constraint_name; - retain keepme; - if first.table_name then keepme=1; - if first.constraint_name and not first.table_name then keepme=0; - if keepme=1; - run; - - /* join back to starting table */ - proc sql; - create table &ds3 as - select a.* - ,b.constraint_order - ,case when b.pk_ind=1 then 1 else 0 end as pk_ind - from work.&ds1 a - left join work.&finalpks b - on a.libref=b.libref - and a.dsn=b.table_name - and upcase(a.name)=upcase(b.name) - order by libref,dsn,constraint_order; -%end; - - -/* prepare tables */ -proc sql; -create table work.&tabs1 as select - libname as libref - ,upcase(memname) as dsn - ,memtype -%if %mf_existfeature(DBMS_MEMTYPE)=1 %then %do; - ,dbms_memtype -%end; -%else %do; - ,'n/a' as dbms_memtype format=$32. -%end; - ,typemem - ,memlabel - ,nvar - ,compress -from dictionary.tables - where upcase(libname)="&lib" -%if &ds ne 0 %then %do; - and upcase(memname)="&ds" -%end; - ; -data &tabs2; - set &ds3; - length pk_fields $512; - retain pk_fields; - by libref dsn constraint_order; - if first.dsn then pk_fields=''; - if pk_ind=1 then pk_fields=catx(' ',pk_fields,name); - if last.dsn then output; -run; - -proc sql; -create table &outds as - select a.libref - ,a.dsn - ,a.memtype - ,a.dbms_memtype - ,a.typemem - ,a.memlabel - ,a.nvar - ,a.compress - ,b.pk_fields - from work.&tabs1 a - left join work.&tabs2 b - on a.libref=b.libref - and a.dsn=b.dsn; - -/* tidy up */ -%mp_dropmembers( - &ds1 &ds2 &ds3 &dsn &tabs1 &tabs2 &sum &pk4sure &pkdefault &finalpks, - iftrue=(&mdebug=0) -) - -%mend mp_getpk; diff --git a/009_macros/mp_gitadd.sas b/009_macros/mp_gitadd.sas deleted file mode 100644 index dcece31..0000000 --- a/009_macros/mp_gitadd.sas +++ /dev/null @@ -1,45 +0,0 @@ -/*** HELP START ***//** - @file - @brief Stages files in a GIT repo - @details Uses the output dataset from mp_gitstatus.sas to determine the files - that should be staged. - - If `STAGED ne "TRUE"` then the file is staged. - - Usage: - - %let dir=%sysfunc(pathname(work))/core; - %let repo=https://github.com/sasjs/core; - %put source clone rc=%sysfunc(GITFN_CLONE(&repo,&dir)); - %mf_writefile(&dir/somefile.txt,l1=some content) - %mf_deletefile(&dir/package.json) - %mp_gitstatus(&dir,outds=work.gitstatus) - - %mp_gitadd(&dir,inds=work.gitstatus) - - @param [in] gitdir The directory containing the GIT repository - @param [in] inds= (work.mp_gitadd) The input dataset with the list of files - to stage. Will accept the output from mp_gitstatus(), else just use a table - with the following columns: - @li path $1024 - relative path to the file in the repo - @li staged $32 - whether the file is staged (TRUE or FALSE) - @li status $64 - either new, deleted, or modified - - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - -

Related Files

- @li mp_gitadd.test.sas - @li mp_gitstatus.sas - -**//*** HELP END ***/ - -%macro mp_gitadd(gitdir,inds=work.mp_gitadd,mdebug=0); - -data _null_; - set &inds; - if STAGED ne "TRUE"; - rc=git_index_add("&gitdir",cats(path),status); - if rc ne 0 or &mdebug=1 then put rc=; -run; - -%mend mp_gitadd; diff --git a/009_macros/mp_gitlog.sas b/009_macros/mp_gitlog.sas deleted file mode 100644 index 71d04c4..0000000 --- a/009_macros/mp_gitlog.sas +++ /dev/null @@ -1,104 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a dataset with the commit history of a local repository - @details Returns the commit history from a local repository. The name of the - branch is also returned. - - More details here: -https://documentation.sas.com/doc/ko/pgmsascdc/v_033/lefunctionsref/n1qo5miyvry1nen111js203hlwrh.htm - - Usage: - - %let gitdir=%sysfunc(pathname(work))/core; - %let repo=https://github.com/sasjs/core; - %put source clone rc=%sysfunc(GITFN_CLONE(&repo,&dir)); - - %mp_gitlog(&gitdir,outds=work.mp_gitlog) - - @param [in] gitdir The directory containing the GIT repository - @param [in] filter= (BRANCHONLY) To return only the commits for the current - branch, use BRANCHONLY (the default). Anything else will return the entire - commit history. - @param [out] outds= (work.mp_gitlog) The output dataset to create. - All vars are $128 except `message` which is $4000. - @li author returns the author who submitted the commit. - @li children_ids returns a list of the children commit IDs - @li committer returns the name of the committer. - @li committer_email returns the email of the committer. - @li email returns the email of the commit author. - @li id returns the commit ID of the commit object. - @li in_current_branch returns "TRUE" or "FALSE" to indicate if the commit is - in the current branch. - @li message returns the commit message. - @li parent_ids returns a list of the parent commit IDs. - @li stash returns "TRUE" or "FALSE" to indicate if the commit is a stash - commit. - @li time returns the time of the commit as numeric string - @li commit_time_num time of the commit as numeric SAS datetime - @li commit_time_str the commit_time_num variable cast as string - - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - @param [in] nobs= (0) Set to an integer greater than 0 to restrict the number - of rows returned - -

SAS Macros

- @li mf_getgitbranch.sas - -

Related Files

- @li mp_gitadd.sas - @li mp_gitreleaseinfo.sas - @li mp_gitstatus.sas - -**//*** HELP END ***/ - -%macro mp_gitlog(gitdir,outds=work.mp_gitlog,mdebug=0,filter=BRANCHONLY,nobs=0); - -%local varlist i var; -%let varlist=author children_ids committer committer_email email id - in_current_branch parent_ids stash time ; - -data &outds; - LENGTH gitdir branch $ 1024 message $4000 &varlist $128 commit_time_num 8. - commit_time_str $32; - call missing (of _all_); - branch="%mf_getgitbranch(&gitdir)"; - gitdir=symget('gitdir'); - rc=git_status_free(trim(gitdir)); - if rc=-1 then do; - put "The libgit2 library is unavailable and no Git operations can be used."; - put "See: https://stackoverflow.com/questions/74082874"; - stop; - end; - else if rc=-2 then do; - put "The libgit2 library is available, but the status function failed."; - put "See the log for details."; - stop; - end; - entries=git_commit_log(trim(gitdir)); - do n=1 to entries; - - %do i=1 %to %sysfunc(countw(&varlist message)); - %let var=%scan(&varlist message,&i,%str( )); - rc=git_commit_get(n,trim(gitdir),"&var",&var); - %end; - /* convert unix time to SAS time - https://4gl.uk/corelink0 */ - /* Number of seconds between 01JAN1960 and 01JAN1970: 315619200 */ - format commit_time_num datetime19.; - commit_time_num=sum(input(cats(time),best.),315619200); - commit_time_str=put(commit_time_num,datetime19.); - %if &mdebug=1 %then %do; - putlog (_all_)(=); - %end; - if "&filter"="BRANCHONLY" then do; - if cats(in_current_branch)='TRUE' then output; - end; - else output; - %if &nobs>0 %then %do; - if n ge &nobs then stop; - %end; - end; - rc=git_commit_free(trim(gitdir)); - keep gitdir branch &varlist message time commit_time_num commit_time_str; -run; - -%mend mp_gitlog; diff --git a/009_macros/mp_gitreleaseinfo.sas b/009_macros/mp_gitreleaseinfo.sas deleted file mode 100644 index 006c977..0000000 --- a/009_macros/mp_gitreleaseinfo.sas +++ /dev/null @@ -1,74 +0,0 @@ -/*** HELP START ***//** - @file - @brief Pulls latest release info from a GIT repository - @details Useful for grabbing the latest version number or other attributes - from a GIT server. Supported providers are GitLab and GitHub. Pull requests - are welcome if you'd like to see additional providers! - - Note that each provider provides slightly different JSON output. Therefore - the macro simply extracts the JSON and assigns the libname (using the JSON - engine). - - Example usage (eg, to grab latest release version from github): - - %mp_gitreleaseinfo(GITHUB,sasjs/core,outlib=mylibref) - - data _null_; - set mylibref.root; - putlog TAG_NAME=; - run; - - @param [in] provider The GIT provider for the release info. Accepted values: - @li GITLAB - @li GITHUB - Tables include root, assets, author, alldata - @param [in] project The link to the repository. This has different formats - depending on the vendor: - @li GITHUB - org/repo, eg sasjs/core - @li GITLAB - project, eg 1343223 - @param [in] server= (0) If your repo is self-hosted, then provide the domain - here. Otherwise it will default to the provider domain (eg gitlab.com). - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - @param [out] outlib= (GITREL) The JSON-engine libref to be created, which will - point at the returned JSON - -

SAS Macros

- @li mf_getuniquefileref.sas - -

Related Files

- @li mp_gitreleaseinfo.test.sas - -**//*** HELP END ***/ - -%macro mp_gitreleaseinfo(provider,project,server=0,outlib=GITREL,mdebug=0); -%local url fref; - -%let provider=%upcase(&provider); - -%if &provider=GITHUB %then %do; - %if "&server"="0" %then %let server=https://api.github.com; - %let url=&server/repos/&project/releases/latest; -%end; -%else %if &provider=GITLAB %then %do; - %if "&server"="0" %then %let server=https://gitlab.com; - %let url=&server/api/v4/projects/&project/releases; -%end; - -%let fref=%mf_getuniquefileref(); - -proc http method='GET' out=&fref url="&url"; -%if &mdebug=1 %then %do; - debug level = 3; -%end; -run; - -libname &outlib JSON fileref=&fref; - -%if &mdebug=1 %then %do; - data _null_; - infile &fref; - input; - putlog _infile_; - run; -%end; - -%mend mp_gitreleaseinfo; diff --git a/009_macros/mp_gitstatus.sas b/009_macros/mp_gitstatus.sas deleted file mode 100644 index 758810d..0000000 --- a/009_macros/mp_gitstatus.sas +++ /dev/null @@ -1,67 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a dataset with the output from `GIT_STATUS()` - @details Uses `git_status()` to fetch the number of changed files, then - iterates with `git_status_get()`, inserting all attributes into an output - dataset. - - Usage: - - %let dir=%sysfunc(pathname(work))/core; - %let repo=https://github.com/sasjs/core; - %put source clone rc=%sysfunc(GITFN_CLONE(&repo,&dir)); - %mf_writefile(&dir/somefile.txt,l1=some content) - %mf_deletefile(&dir/package.json) - - %mp_gitstatus(&dir,outds=work.gitstatus) - - More info on these functions is in this [helpful paper] -(https://www.sas.com/content/dam/SAS/support/en/sas-global-forum-proceedings/2019/3057-2019.pdf) - by Danny Zimmerman. - - @param [in] gitdir The directory containing the GIT repository - @param [out] outds= (work.git_status) The output dataset to create. Vars: - @li gitdir $1024 - directory of repo - @li path $1024 - relative path to the file in the repo - @li staged $32 - whether the file is staged (TRUE or FALSE) - @li status $64 - either new, deleted, or modified - @li cnt - number of files - @li n - the "nth" file in the list from git_status() - - @param [in] mdebug= (0) Set to 1 to enable DEBUG messages - -

Related Files

- @li mp_gitstatus.test.sas - @li mp_gitadd.sas - -**//*** HELP END ***/ - -%macro mp_gitstatus(gitdir,outds=work.mp_gitstatus,mdebug=0); - -data &outds; - LENGTH gitdir path $ 1024 STATUS $ 64 STAGED $ 32; - call missing (of _all_); - gitdir=symget('gitdir'); - cnt=git_status(trim(gitdir)); - if cnt=-1 then do; - put "The libgit2 library is unavailable and no Git operations can be used."; - put "See: https://stackoverflow.com/questions/74082874"; - end; - else if cnt=-2 then do; - put "The libgit2 library is available, but the status function failed."; - put "See the log for details."; - end; - else do n=1 to cnt; - rc=GIT_STATUS_GET(n,gitdir,'PATH',path); - rc=GIT_STATUS_GET(n,gitdir,'STAGED',staged); - rc=GIT_STATUS_GET(n,gitdir,'STATUS',status); - output; - %if &mdebug=1 %then %do; - putlog (_all_)(=); - %end; - end; - rc=git_status_free(trim(gitdir)); - drop rc cnt; -run; - -%mend mp_gitstatus; diff --git a/009_macros/mp_gsubfile.sas b/009_macros/mp_gsubfile.sas deleted file mode 100644 index fa62655..0000000 --- a/009_macros/mp_gsubfile.sas +++ /dev/null @@ -1,58 +0,0 @@ -/*** HELP START ***//** - @file - @brief Performs a text substitution on a file - @details Makes use of the GSUB function in LUA to perform a text substitution - in a file - either in-place, or writing to a new location. The benefit of - using LUA is that the entire file can be loaded into a single variable, - thereby side stepping the 32767 character limit in a data step. - - Usage: - - %let file=%sysfunc(pathname(work))/file.txt; - %let str=replace/me; - %let rep=with/this; - data _null_; - file "&file"; - put "&str"; - run; - %mp_gsubfile(file=&file, patternvar=str, replacevar=rep) - data _null_; - infile "&file"; - input; - list; - run; - - @param [in] file= (0) The file to perform the substitution on - @param [in] patternvar= A macro variable containing the Lua - [pattern](https://www.lua.org/pil/20.2.html) to search for. Due to the use - of special (magic) characters in Lua patterns, it is safer to pass the NAME - of the macro variable containing the string, rather than the value itself. - @param [in] replacevar= () - The name of the macro variable containing the replacement _string_. - @param [out] outfile= (0) The file to write the output to. - If zero, then the file is overwritten in-place. - -

SAS Macros

- @li ml_gsubfile.sas - -

Related Macros

- @li mp_gsubfile.test.sas - - @version 9.4 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mp_gsubfile(file=0, - patternvar=, - replacevar=, - outfile=0 -)/*/STORE SOURCE*/; - - %if "%substr(&sysver.XX,1,4)"="V.04" %then %do; - %put %str(ERR)OR: Viya 4 does not support the IO library in lua; - %return; - %end; - - %ml_gsubfile() - -%mend mp_gsubfile; diff --git a/009_macros/mp_guesspk.sas b/009_macros/mp_guesspk.sas deleted file mode 100644 index 2106014..0000000 --- a/009_macros/mp_guesspk.sas +++ /dev/null @@ -1,332 +0,0 @@ -/*** HELP START ***//** - @file - @brief Guess the primary key of a table - @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 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"; - %inc mc; - %mp_guesspk(sashelp.class,outds=classpks) - - @param [in] baseds The dataset to analyse - @param [out] outds= (mp_guesspk) Output dataset to contain the possible PKs - @param [in] 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 [in] min_rows= (5) The minimum number of rows a table should have in - order to try and guess the PK. - @param [in] ignore_cols (0) Space seperated list of columns which you are - sure are not part of the primary key (helps to avoid false positives) - @param [in] mdebug= Set to 1 to enable DEBUG messages and preserve outputs - -

SAS Macros

- @li mf_getvarlist.sas - @li mf_getuniquename.sas - @li mf_wordsInstr1butnotstr2.sas - @li mf_nobs.sas - -

Related Macros

- @li mp_getpk.sas - - @version 9.3 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_guesspk(baseds - ,outds=mp_guesspk - ,max_guesses=3 - ,min_rows=5 - ,ignore_cols=0 - ,mdebug=0 -)/*/STORE SOURCE*/; -%local dbg; -%if &mdebug=1 %then %do; - %put &sysmacroname entry vars:; - %put _local_; -%end; -%else %let dbg=*; - -/* declare local vars */ -%local var vars vcnt i j k l tmpvar tmpds rows posspks ppkcnt; -%let vars=%upcase(%mf_getvarlist(&baseds)); -%let vars=%mf_wordsInStr1ButNotStr2(str1=&vars,str2=%upcase(&ignore_cols)); -%let vcnt=%sysfunc(countw(&vars)); - -%if &vcnt=0 %then %do; - %put &sysmacroname: &baseds has no variables! Exiting.; - %return; -%end; - -/* get null count and row count */ -%let tmpvar=%mf_getuniquename(); -proc sql noprint; -create table _data_ as select - count(*) as &tmpvar -%do i=1 %to &vcnt; - %let var=%scan(&vars,&i); - ,sum(case when &var is missing then 1 else 0 end) as &var -%end; - from &baseds; - -/* transpose table and scan for not null cols */ -proc transpose; -data _null_; - set &syslast end=last; - length vars $32767; - retain vars ; - if _name_="&tmpvar" then call symputx('rows',col1,'l'); - else if col1=0 then vars=catx(' ',vars,_name_); - if last then call symputx('posspks',vars,'l'); -run; - -%let ppkcnt=%sysfunc(countw(&posspks)); -%if &ppkcnt=0 %then %do; - %put &sysmacroname: &baseds has no non-missing variables! Exiting.; - %return; -%end; - -proc sort data=&baseds(keep=&posspks) out=_data_ noduprec; - by _all_; -run; -%local pkds; %let pkds=&syslast; - -%if &rows > %mf_nobs(&pkds) %then %do; - %put &sysmacroname: &baseds has no combination of unique records! Exiting.; - %return; -%end; - -/* now check cardinality */ -proc sql noprint; -create table _data_ as select -%do i=1 %to &ppkcnt; - %let var=%scan(&posspks,&i); - count(distinct &var) as &var - %if &i<&ppkcnt %then ,; -%end; - from &pkds; - -/* transpose and sort by cardinality */ -proc transpose; -proc sort; by descending col1; -run; - -/* create initial PK list and re-order posspks list */ -data &outds(keep=pkguesses); - length pkguesses $5000 vars $5000; - set &syslast end=last; - retain vars ; - vars=catx(' ',vars,_name_); - if col1=&rows then do; - pkguesses=_name_; - output; - end; - if last then call symputx('posspks',vars,'l'); -run; - -%if %mf_nobs(&outds) ge &max_guesses %then %do; - %put &sysmacroname: %mf_nobs(&outds) possible primary key values found; - %return; -%end; - -%if &ppkcnt=1 %then %do; - %put &sysmacroname: No more PK guess possible; - %return; -%end; - -/* begin scanning for uniques on pairs of PKs */ -%let tmpds=%mf_getuniquename(); -%local lev1 lev2; -%do i=1 %to &ppkcnt; - %let lev1=%scan(&posspks,&i); - %do j=2 %to &ppkcnt; - %let lev2=%scan(&posspks,&j); - %if &lev1 ne &lev2 %then %do; - /* check for two level uniqueness */ - proc sort data=&pkds(keep=&lev1 &lev2) out=&tmpds noduprec; - by _all_; - run; - %if %mf_nobs(&tmpds)=&rows %then %do; - proc sql; - insert into &outds values("&lev1 &lev2"); - %if %mf_nobs(&outds) ge &max_guesses %then %do; - %put &sysmacroname: Max PKs reached at Level 2 for &baseds; - %goto exit; - %end; - %end; - %end; - %end; -%end; - -%if &ppkcnt=2 %then %do; - %put &sysmacroname: No more PK guess possible; - %goto exit; -%end; - -/* begin scanning for uniques on PK triplets */ -%local lev3; -%do i=1 %to &ppkcnt; - %let lev1=%scan(&posspks,&i); - %do j=2 %to &ppkcnt; - %let lev2=%scan(&posspks,&j); - %if &lev1 ne &lev2 %then %do k=3 %to &ppkcnt; - %let lev3=%scan(&posspks,&k); - %if &lev1 ne &lev3 and &lev2 ne &lev3 %then %do; - /* check for three level uniqueness */ - proc sort data=&pkds(keep=&lev1 &lev2 &lev3) out=&tmpds noduprec; - by _all_; - run; - %if %mf_nobs(&tmpds)=&rows %then %do; - proc sql; - insert into &outds values("&lev1 &lev2 &lev3"); - %if %mf_nobs(&outds) ge &max_guesses %then %do; - %put &sysmacroname: Max PKs reached at Level 3 for &baseds; - %goto exit; - %end; - %end; - %end; - %end; - %end; -%end; - -%if &ppkcnt=3 %then %do; - %put &sysmacroname: No more PK guess possible; - %goto exit; -%end; - -/* scan for uniques on up to 4 PK fields */ -%local lev4; -%do i=1 %to &ppkcnt; - %let lev1=%scan(&posspks,&i); - %do j=2 %to &ppkcnt; - %let lev2=%scan(&posspks,&j); - %if &lev1 ne &lev2 %then %do k=3 %to &ppkcnt; - %let lev3=%scan(&posspks,&k); - %if &lev1 ne &lev3 and &lev2 ne &lev3 %then %do l=4 %to &ppkcnt; - %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; - by _all_; - run; - %if %mf_nobs(&tmpds)=&rows %then %do; - proc sql; - insert into &outds values("&lev1 &lev2 &lev3 &lev4"); - %if %mf_nobs(&outds) ge &max_guesses %then %do; - %put &sysmacroname: Max PKs reached at Level 4 for &baseds; - %goto exit; - %end; - %end; - %end; - %end; - %end; - %end; -%end; - -%if &ppkcnt=4 %then %do; - %put &sysmacroname: No more PK guess possible; - %goto exit; -%end; - -/* scan for uniques on up to 4 PK fields */ -%local lev5 m; -%do i=1 %to &ppkcnt; - %let lev1=%scan(&posspks,&i); - %do j=2 %to &ppkcnt; - %let lev2=%scan(&posspks,&j); - %if &lev1 ne &lev2 %then %do k=3 %to &ppkcnt; - %let lev3=%scan(&posspks,&k); - %if &lev1 ne &lev3 and &lev2 ne &lev3 %then %do l=4 %to &ppkcnt; - %let lev4=%scan(&posspks,&l); - %if &lev1 ne &lev4 and &lev2 ne &lev4 and &lev3 ne &lev4 %then - %do m=5 %to &ppkcnt; - %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; - by _all_; - run; - %if %mf_nobs(&tmpds)=&rows %then %do; - proc sql; - insert into &outds values("&lev1 &lev2 &lev3 &lev4 &lev5"); - %if %mf_nobs(&outds) ge &max_guesses %then %do; - %put &sysmacroname: Max PKs reached at Level 5 for &baseds; - %goto exit; - %end; - %end; - %end; - %end; - %end; - %end; - %end; -%end; - -%if &ppkcnt=5 %then %do; - %put &sysmacroname: No more PK guess possible; - %goto exit; -%end; - -/* scan for uniques on up to 4 PK fields */ -%local lev6 n; -%do i=1 %to &ppkcnt; - %let lev1=%scan(&posspks,&i); - %do j=2 %to &ppkcnt; - %let lev2=%scan(&posspks,&j); - %if &lev1 ne &lev2 %then %do k=3 %to &ppkcnt; - %let lev3=%scan(&posspks,&k); - %if &lev1 ne &lev3 and &lev2 ne &lev3 %then %do l=4 %to &ppkcnt; - %let lev4=%scan(&posspks,&l); - %if &lev1 ne &lev4 and &lev2 ne &lev4 and &lev3 ne &lev4 %then - %do m=5 %to &ppkcnt; - %let lev5=%scan(&posspks,&m); - %if &lev1 ne &lev5 & &lev2 ne &lev5 & &lev3 ne &lev5 & &lev4 ne &lev5 - %then %do n=6 %to &ppkcnt; - %let lev6=%scan(&posspks,&n); - %if &lev1 ne &lev6 & &lev2 ne &lev6 & &lev3 ne &lev6 - & &lev4 ne &lev6 & &lev5 ne &lev6 %then - %do; - /* check for four level uniqueness */ - proc sort data=&pkds(keep=&lev1 &lev2 &lev3 &lev4 &lev5 &lev6) - out=&tmpds noduprec; - by _all_; - run; - %if %mf_nobs(&tmpds)=&rows %then %do; - proc sql; - 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; - %goto exit; - %end; - %end; - %end; - %end; - %end; - %end; - %end; - %end; -%end; - -%if &ppkcnt=6 %then %do; - %put &sysmacroname: No more PK guess possible; - %goto exit; -%end; - -%exit: -%if &mdebug=0 %then %do; - proc sql; - drop table &tmpds; -%end; - -%mend mp_guesspk; diff --git a/009_macros/mp_hashdataset.sas b/009_macros/mp_hashdataset.sas deleted file mode 100644 index 3005dad..0000000 --- a/009_macros/mp_hashdataset.sas +++ /dev/null @@ -1,91 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns a unique hash for a dataset - @details Ignores metadata attributes, used only to hash values. If used to - compare datasets, they must have their columns and rows in the same order. - - %mp_hashdataset(sashelp.class,outds=myhash) - - data _null_; - set work.myhash; - put hashkey=; - run; - - ![sas md5 hash dataset log results](https://i.4gl.io/1/KorUKoyE05.png/raw) - -

SAS Macros

- @li mf_getattrn.sas - @li mf_getuniquename.sas - @li mf_getvarlist.sas - @li mp_md5.sas - -

Related Files

- @li mp_hashdataset.test.sas - @li mp_hashdirectory.sas - - @param [in] libds dataset to hash - @param [in] salt= () Provide a salt (could be, for instance, the dataset name) - @param [in] iftrue= (1=1) A condition under which the macro should be executed - @param [out] outds= (work._data_) The output dataset to create. This - will contain one column (hashkey) with one observation (a $hex32. - representation of the input hash) - |hashkey:$32.| - |---| - |28ABC74ABFC45F50794237BA5566E6CA| - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mp_hashdataset( - libds, - outds=work._data_, - salt=, - iftrue=%str(1=1) -)/*/STORE SOURCE*/; - -%local keyvar /* roll up the md5 */ - prevkeyvar /* retain prev record md5 */ - lastvar /* last var in input ds */ - cvars nvars; - -%if not(%eval(%unquote(&iftrue))) %then %return; - -/* avoid naming conflict for hash key vars */ -%let keyvar=%mf_getuniquename(); -%let prevkeyvar=%mf_getuniquename(); -%let lastvar=%mf_getuniquename(); - -%if %mf_getattrn(&libds,NLOBS)=0 %then %do; - data &outds; - length hashkey $32; - hashkey=put(md5("&salt"),$hex32.); - output; - stop; - run; - %put &sysmacroname: Dataset &libds is empty, or is not a dataset; - %put &sysmacroname: hashkey of &outds is based on salt (&salt) only; -%end; -%else %if %mf_getattrn(&libds,NLOBS)<0 %then %do; - %put %str(ERR)OR: Dataset &libds is not a dataset; -%end; -%else %do; - data &outds(rename=(&keyvar=hashkey) keep=&keyvar) - %if "%substr(&sysver,1,1)" ne "4" and "%substr(&sysver,1,1)" ne "5" %then %do; - /nonote2err - %end; - ; - length &prevkeyvar &keyvar $32; - retain &prevkeyvar; - if _n_=1 then &prevkeyvar=put(md5("&salt"),$hex32.); - set &libds end=&lastvar; - /* hash should include previous row */ - &keyvar=%mp_md5( - cvars=%mf_getvarlist(&libds,typefilter=C) &prevkeyvar, - nvars=%mf_getvarlist(&libds,typefilter=N) - ); - &prevkeyvar=&keyvar; - if &lastvar then output; - run; -%end; -%mend mp_hashdataset; diff --git a/009_macros/mp_hashdirectory.sas b/009_macros/mp_hashdirectory.sas deleted file mode 100644 index 36a3072..0000000 --- a/009_macros/mp_hashdirectory.sas +++ /dev/null @@ -1,164 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns a unique hash for each file in a directory - @details Hashes each file in each directory, and then hashes the hashes to - create a hash for each directory also. - - This makes use of the new `hashing_file()` and `hashing` functions, available - since 9.4m6. Interestingly, those functions can be used in pure macro, eg: - - %put %sysfunc(hashing_file(md5,/path/to/file.blob,0)); - - Actual usage: - - %let fpath=/some/directory; - - %mp_hashdirectory(&fpath,outds=myhash,maxdepth=2) - - data _null_; - set work.myhash; - put (_all_)(=); - run; - - Whilst files are hashed in their entirety, the logic for creating a folder - hash is as follows: - - @li Sort the files by filename (case sensitive, uppercase then lower) - @li Take the first 100 hashes, concatenate and hash - @li Concatenate this hash with another 100 hashes and hash again - @li Continue until the end of the folder. This is the folder hash - @li If a folder contains other folders, start from the bottom of the tree - - the folder hashes cascade upwards so you know immediately if there is a - change in a sub/sub directory - @li If a subfolder has no content (empty) then it is ignored. No hash created. - @li If the file is empty, it is also ignored / no hash created. - @li If the target directory (&inloc) is empty, &outds will also be empty - -

SAS Macros

- @li mp_dirlist.sas - -

Related Files

- @li mp_hashdataset.sas - @li mp_hashdirectory.test.sas - @li mp_md5.sas - - @param [in] inloc Full filepath of the file to be hashed (unquoted) - @param [in] iftrue= (1=1) A condition under which the macro should be executed - @param [in] maxdepth= (0) Set to a positive integer to indicate the level of - subdirectory scan recursion - eg 3, to go `./3/levels/deep`. For unlimited - recursion, set to MAX. - @param [in] method= (MD5) the hashing method to use. Available options: - @li MD5 - @li SH1 - @li SHA256 - @li SHA384 - @li SHA512 - @li CRC32 - @param [out] outds= (work.mp_hashdirectory) The output dataset. Contains: - @li directory - the parent folder - @li file_hash - the hash output - @li hash_duration - how long the hash took (first hash always takes longer) - @li file_path - /full/path/to/each/file.ext - @li file_or_folder - contains either "file" or "folder" - @li level - the depth of the directory (top level is 0) - - @version 9.4m6 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mp_hashdirectory(inloc, - outds=work.mp_hashdirectory, - method=MD5, - maxdepth=0, - iftrue=%str(1=1) -)/*/STORE SOURCE*/; - -%local curlevel tempds maxlevel; - -%if not(%eval(%unquote(&iftrue))) %then %return; - -/* get the directory listing */ -%mp_dirlist(path=&inloc, outds=&outds, maxdepth=&maxdepth, showparent=YES) - -/* create the hashes */ -data &outds; - set &outds (rename=(filepath=file_path)); - length FILE_HASH $32 HASH_DURATION 8; - keep directory file_hash hash_duration file_path file_or_folder level; - - ts=datetime(); - if file_or_folder='file' then do; - /* if file is empty, hashing_file will break - so ignore / delete */ - length fname val $8; - drop fname val fid is_empty; - rc=filename(fname,file_path); - fid=fopen(fname); - if fid > 0 then do; - rc=fread(fid); - is_empty=fget(fid,val); - end; - rc=fclose(fid); - rc=filename(fname); - if is_empty ne 0 then delete; - else file_hash=hashing_file("&method",cats(file_path),0); - end; - hash_duration=datetime()-ts; -run; - -proc sort data=&outds ; - by descending level directory file_path; -run; - -%let maxlevel=0; -data _null_; - set &outds; - call symputx('maxlevel',level,'l'); - stop; -run; - -/* now hash the hashes to populate folder hashes, starting from the bottom */ -%do curlevel=&maxlevel %to 0 %by -1; - data work._data_ (keep=directory file_hash); - set &outds; - where level=&curlevel; - by descending level directory file_path; - length str $32767 tmp_hash $32; - retain str tmp_hash ; - /* reset vars when starting a new directory */ - if first.directory then do; - str=''; - tmp_hash=''; - i=0; - end; - /* hash each chunk of 100 file paths */ - i+1; - str=cats(str,file_hash); - if mod(i,100)=0 or last.directory then do; - tmp_hash=hashing("&method",cats(tmp_hash,str)); - str=''; - end; - /* output the hash at directory level */ - if last.directory then do; - file_hash=tmp_hash; - output; - end; - if last.level then stop; - run; - %let tempds=&syslast; - /* join the hash back into the main table */ - proc sql undo_policy=none; - create table &outds as - select a.directory - ,coalesce(b.file_hash,a.file_hash) as file_hash - ,a.hash_duration - ,a.file_path - ,a.file_or_folder - ,a.level - from &outds a - left join &tempds b - on a.file_path=b.directory - order by level desc, directory, file_path; - drop table &tempds; -%end; - -%mend mp_hashdirectory; diff --git a/009_macros/mp_include.sas b/009_macros/mp_include.sas deleted file mode 100644 index 57386d8..0000000 --- a/009_macros/mp_include.sas +++ /dev/null @@ -1,106 +0,0 @@ -/*** HELP START ***//** - @file - @brief Performs a wrapped \%include - @details This macro wrapper is necessary if you need your included code to - know that it is being \%included. - - If you are using %include in a regular program, you could make use of the - following macro variables: - - @li SYSINCLUDEFILEDEVICE - @li SYSINCLUDEFILEDIR - @li SYSINCLUDEFILEFILEREF - @li SYSINCLUDEFILENAME - - However these variables are NOT available inside a macro, as documented here: -https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/mcrolref/n1j5tcc0n2xczyn1kg1o0606gsv9.htm - - This macro can be used in place of the %include statement, and will insert - the following (equivalent) global variables: - - @li _SYSINCLUDEFILEDEVICE - @li _SYSINCLUDEFILEDIR - @li _SYSINCLUDEFILEFILEREF - @li _SYSINCLUDEFILENAME - - These can be used whenever testing _within a macro_. Outside of the macro, - the regular automatic variables will still be available (thanks to a - concatenated file list in the include statement). - - Example usage: - - filename example temp; - data _null_; - file example; - put '%macro test();'; - put '%put &=_SYSINCLUDEFILEFILEREF;'; - put '%put &=SYSINCLUDEFILEFILEREF;'; - put '%mend; %test()'; - put '%put &=SYSINCLUDEFILEFILEREF;'; - run; - %mp_include(example) - - @param [in] fileref The fileref of the file to be included. Must be provided. - @param [in] prefix= (_) The prefix to apply to the global variables. - @param [in] opts= (SOURCE2) The options to apply to the %inc statement - @param [in] errds= (work.mp_abort_errds) There is no clean way to end a - process within a %include called within a macro. Furthermore, there is no - way to test if a macro is called within a %include. To handle this - particular scenario, the %mp_abort() macro will test for the existence of - the `_SYSINCLUDEFILEDEVICE` variable and return the outputs (msg,mac) inside - this dataset. - It will then run an abort cancel FILE to stop the include running, and pass - the dataset back. - - IMPORTANT NOTE - it is NOT possible to read this dataset as part of _this_ - macro! When running abort cancel FILE, ALL macros are closed, so instead it - is necessary to invoke "%mp_abort(mode=INCLUDE)" OUTSIDE of macro wrappers. - - - @version 9.4 - @author Allan Bowe - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mp_abort.sas - -**//*** HELP END ***/ - -%macro mp_include(fileref - ,prefix=_ - ,opts=SOURCE2 - ,errds=work.mp_abort_errds -)/*/STORE SOURCE*/; - -/* prepare precode */ -%local tempref; -%let tempref=%mf_getuniquefileref(); -data _null_; - file &tempref; - set sashelp.vextfl(where=(fileref="%upcase(&fileref)")); - put '%let _SYSINCLUDEFILEDEVICE=' xengine ';'; - name=scan(xpath,-1,'/\'); - put '%let _SYSINCLUDEFILENAME=' name ';'; - path=subpad(xpath,1,length(xpath)-length(name)-1); - put '%let _SYSINCLUDEFILEDIR=' path ';'; - put '%let _SYSINCLUDEFILEFILEREF=' "&fileref;"; -run; - -/* prepare the errds */ -data &errds; - length msg mac $1000; - call missing(msg,mac); - iftrue='1=0'; -run; - -/* include the include */ -%inc &tempref &fileref/&opts; - -%mp_abort(iftrue= (&syscc ne 0) - ,mac=%str(&_SYSINCLUDEFILEDIR/&_SYSINCLUDEFILENAME) - ,msg=%str(syscc=&syscc after executing &_SYSINCLUDEFILENAME) -) - -filename &tempref clear; - -%mend mp_include; diff --git a/009_macros/mp_init.sas b/009_macros/mp_init.sas deleted file mode 100644 index 78d82ff..0000000 --- a/009_macros/mp_init.sas +++ /dev/null @@ -1,76 +0,0 @@ -/*** HELP START ***//** - @file - @brief Initialise session with useful settings and variables - @details Implements a "strict" set of SAS options for use in defensive - programming. Highly recommended, if you want your code to run on some - other machine. - - This macro is recommended to be compiled and invoked in the `initProgram` - for SASjs [Jobs](https://cli.sasjs.io/sasjsconfig.html#jobConfig_initProgram - ), [Services]( - https://cli.sasjs.io/sasjsconfig.html#serviceConfig_initProgram) and [Tests] - (https://cli.sasjs.io/sasjsconfig.html#testConfig_initProgram). - - For non SASjs projects, you could invoke in the autoexec, or in your own - solution initialisation macro. - - - If you have a good idea for another useful option, setting, or global - variable - feel free to [raise an issue]( - https://github.com/sasjs/core/issues/new)! - - All global variables are prefixed with "SASJS" (unless modified with the - prefix parameter). - - @param [in] prefix= (SASJS) The prefix to apply to the global macro variables - - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_init(prefix=SASJS -)/*/STORE SOURCE*/; - -%if %symexist(SASJS_PREFIX) %then %return; /* only run once */ - -%global - SASJS_PREFIX /* the ONLY hard-coded global macro variable in SASjs */ - &prefix._FUNCTIONS /* used in mcf_init() to track core function compilation */ - &prefix._INIT_NUM /* initialisation time as numeric */ - &prefix._INIT_DTTM /* initialisation time in E8601DT26.6 format */ - &prefix.WORK /* avoid typing %sysfunc(pathname(work)) every time */ -; - -%let sasjs_prefix=&prefix; - -data _null_; - dttm=datetime(); - call symputx("&sasjs_prefix._init_num",dttm,'g'); - call symputx("&sasjs_prefix._init_dttm",put(dttm,E8601DT26.6),'g'); - call symputx("&sasjs_prefix.work",pathname('WORK'),'g'); -run; - -options - compress=CHAR /* default is none so ensure we have something! */ - datastmtchk=ALLKEYWORDS /* protection from overwriting input datasets */ - errorcheck=STRICT /* catch errs 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=. /* changing this can cause hard to detect errs */ - noquotelenmax /* avoid warnings for long strings */ - noreplace /* avoid overwriting permanent datasets */ - ps=max /* reduce log size slightly */ - ls=max /* reduce log even more and avoid word truncation */ - validmemname=COMPATIBLE /* avoid special characters etc in table names */ - validvarname=V7 /* avoid special characters etc in variable names */ - varinitchk=%str(ERR)OR /* avoid data mistakes from variable name typos */ - varlenchk=%str(ERR)OR /* fail hard if truncation (data loss) can result */ -%if "%substr(&sysver,1,1)" ne "4" and "%substr(&sysver,1,1)" ne "5" %then %do; - noautocorrect /* disallow misspelled procedure names */ - dsoptions=note2err /* undocumented - convert bad NOTEs to ERRs */ -%end; -; - -%mend mp_init; diff --git a/009_macros/mp_jsonout.sas b/009_macros/mp_jsonout.sas deleted file mode 100644 index 2821601..0000000 --- a/009_macros/mp_jsonout.sas +++ /dev/null @@ -1,406 +0,0 @@ -/*** HELP START ***//** - @file mp_jsonout.sas - @brief Writes JSON in SASjs format to a fileref - @details This macro can be used to OPEN a JSON stream and send one or more - tables as arrays of rows, where each row can be an object or a nested array. - - There are two engines available - DATASTEP or PROCJSON. - - PROC JSON is fast but will produce errs like the ones below if - special chars are encountered. - - > (ERR)OR: Some code points did not transcode. - - > An object or array close is not valid at this point in the JSON text. - - > Date value out of range - - If this happens, try running with ENGINE=DATASTEP. - - The DATASTEP engine is used to handle special SAS missing numerics, and - can also convert entire datasets to formatted values. Output JSON is always - in UTF-8. - - Usage: - - filename tmp temp; - data class; set sashelp.class;run; - - %mp_jsonout(OPEN,jref=tmp) - %mp_jsonout(OBJ,class,jref=tmp) - %mp_jsonout(OBJ,class,dslabel=class2,jref=tmp,showmeta=Y) - %mp_jsonout(CLOSE,jref=tmp) - - data _null_; - infile tmp; - input;putlog _infile_; - run; - - If you are building web apps with SAS then you are strongly encouraged to use - the mX_createwebservice macros in combination with the - [sasjs adapter](https://github.com/sasjs/adapter). - For more information see https://sasjs.io - - @param [in] action Valid values: - @li OPEN - opens the JSON - @li OBJ - sends a table with each row as an object - @li ARR - sends a table with each row in an array - @li CLOSE - closes the JSON - @param [in] ds The dataset to send. Must be a work table. - @param [out] jref= (_webout) The fileref to which to send the JSON - @param [out] dslabel= The name to give the table in the exported JSON - @param [in] fmt= (Y) Whether to keep (Y) or strip (N) formats from the table - @param [in] engine= (DATASTEP) Which engine to use to send the JSON. Options: - @li PROCJSON (default) - @li DATASTEP (more reliable when data has non standard characters) - @param [in] missing= (NULL) Special numeric missing values can be sent as NULL - (eg `null`) or as STRING values (eg `".a"` or `".b"`) - @param [in] showmeta= (N) Set to Y to output metadata alongside each table, - such as the column formats and types. The metadata is contained inside an - object with the same name as the table but prefixed with a dollar sign - ie, - `,"$tablename":{"formats":{"col1":"$CHAR1"},"types":{"COL1":"C"}}` - @param [in] maxobs= (MAX) Provide an integer to limit the number of input rows - that should be converted to JSON - -

Related Files

- @li mp_ds2fmtds.sas - - @version 9.2 - @author Allan Bowe - @source https://github.com/sasjs/core - -**//*** HELP END ***/ -%macro mp_jsonout(action,ds,jref=_webout,dslabel=,fmt=Y - ,engine=DATASTEP - ,missing=NULL - ,showmeta=N - ,maxobs=MAX -)/*/STORE SOURCE*/; -%local tempds colinfo fmtds i numcols numobs stmt_obs lastobs optval - tmpds1 tmpds2 tmpds3 tmpds4; -%let numcols=0; -%if &maxobs ne MAX %then %let stmt_obs=%str(if _n_>&maxobs then stop;); - -%if &action=OPEN %then %do; - options nobomfile; - data _null_;file &jref encoding='utf-8' lrecl=200; - put '{"PROCESSED_DTTM" : "' "%sysfunc(datetime(),E8601DT26.6)" '"'; - run; -%end; -%else %if (&action=ARR or &action=OBJ) %then %do; - /* force variable names to always be uppercase in the JSON */ - options validvarname=upcase; - /* To avoid issues with _webout on EBI - such as encoding diffs and truncation - (https://support.sas.com/kb/49/325.html) we use temporary files */ - filename _sjs1 temp lrecl=200 ; - data _null_; file _sjs1 encoding='utf-8'; - put ", ""%lowcase(%sysfunc(coalescec(&dslabel,&ds)))"":"; - run; - /* now write to _webout 1 char at a time */ - data _null_; - infile _sjs1 lrecl=1 recfm=n; - file &jref mod lrecl=1 recfm=n; - input sourcechar $char1. @@; - format sourcechar hex2.; - put sourcechar char1. @@; - run; - filename _sjs1 clear; - - /* grab col defs */ - proc contents noprint data=&ds - out=_data_(keep=name type length format formatl formatd varnum label); - run; - %let colinfo=%scan(&syslast,2,.); - proc sort data=&colinfo; - by varnum; - run; - /* move meta to mac vars */ - data &colinfo; - if _n_=1 then call symputx('numcols',nobs,'l'); - set &colinfo end=last nobs=nobs; - name=upcase(name); - /* fix formats */ - if type=2 or type=6 then do; - typelong='char'; - length fmt $49.; - if format='' then fmt=cats('$',length,'.'); - else if formatl=0 then fmt=cats(format,'.'); - else fmt=cats(format,formatl,'.'); - end; - else do; - typelong='num'; - if format='' then fmt='best.'; - else if formatl=0 then fmt=cats(format,'.'); - else if formatd=0 then fmt=cats(format,formatl,'.'); - else fmt=cats(format,formatl,'.',formatd); - end; - /* 32 char unique name */ - newname='sasjs'!!substr(cats(put(md5(name),$hex32.)),1,27); - - call symputx(cats('name',_n_),name,'l'); - call symputx(cats('newname',_n_),newname,'l'); - call symputx(cats('length',_n_),length,'l'); - call symputx(cats('fmt',_n_),fmt,'l'); - call symputx(cats('type',_n_),type,'l'); - call symputx(cats('typelong',_n_),typelong,'l'); - call symputx(cats('label',_n_),coalescec(label,name),'l'); - /* overwritten when fmt=Y and a custom format exists in catalog */ - if typelong='num' then call symputx(cats('fmtlen',_n_),200,'l'); - else call symputx(cats('fmtlen',_n_),min(32767,ceil((length+10)*1.5)),'l'); - run; - - %let tempds=%substr(_%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); - proc sql; - select count(*) into: lastobs from &ds; - %if &maxobs ne MAX %then %let lastobs=%sysfunc(min(&lastobs,&maxobs)); - - %if &engine=PROCJSON %then %do; - %if &missing=STRING %then %do; - %put &sysmacroname: Special Missings not supported in proc json.; - %put &sysmacroname: Switching to DATASTEP engine; - %goto datastep; - %end; - data &tempds; - set &ds; - &stmt_obs; - %if &fmt=N %then format _numeric_ best32.;; - /* PRETTY is necessary to avoid line truncation in large files */ - filename _sjs2 temp lrecl=131068 encoding='utf-8'; - proc json out=_sjs2 pretty - %if &action=ARR %then nokeys ; - ;export &tempds / nosastags fmtnumeric; - run; - /* send back to webout */ - data _null_; - infile _sjs2 lrecl=1 recfm=n; - file &jref mod lrecl=1 recfm=n; - input sourcechar $char1. @@; - format sourcechar hex2.; - put sourcechar char1. @@; - run; - filename _sjs2 clear; - %end; - %else %if &engine=DATASTEP %then %do; - %datastep: - %if %sysfunc(exist(&ds)) ne 1 & %sysfunc(exist(&ds,VIEW)) ne 1 - %then %do; - %put &sysmacroname: &ds NOT FOUND!!!; - %return; - %end; - - %if &fmt=Y %then %do; - /** - * Extract format definitions - * First, by getting library locations from dictionary.formats - * Then, by exporting the width using proc format - * Cannot use maxw from sashelp.vformat as not always populated - * Cannot use fmtinfo() as not supported in all flavours - */ - %let tmpds1=%substr(fmtsum%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); - %let tmpds2=%substr(cntl%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); - %let tmpds3=%substr(cntl%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); - %let tmpds4=%substr(col%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32); - proc sql noprint; - create table &tmpds1 as - select cats(libname,'.',memname) as FMTCAT, - FMTNAME - from dictionary.formats - where fmttype='F' and libname is not null - and fmtname in (select format from &colinfo where format is not null) - order by 1; - create table &tmpds2( - FMTNAME char(32), - LENGTH num - ); - %local catlist cat fmtlist i; - select distinct fmtcat into: catlist separated by ' ' from &tmpds1; - %do i=1 %to %sysfunc(countw(&catlist,%str( ))); - %let cat=%scan(&catlist,&i,%str( )); - proc sql; - select distinct fmtname into: fmtlist separated by ' ' - from &tmpds1 where fmtcat="&cat"; - proc format lib=&cat cntlout=&tmpds3(keep=fmtname length); - select &fmtlist; - run; - proc sql; - insert into &tmpds2 select distinct fmtname,length from &tmpds3; - %end; - - proc sql; - create table &tmpds4 as - select a.*, b.length as MAXW - from &colinfo a - left join &tmpds2 b - on cats(a.format)=cats(upcase(b.fmtname)) - order by a.varnum; - data _null_; - set &tmpds4; - if not missing(maxw); - call symputx( - cats('fmtlen',_n_), - /* vars need extra padding due to JSON escaping of special chars */ - min(32767,ceil((max(length,maxw)+10)*1.5)) - ,'l' - ); - run; - - /* configure varlenchk - as we are explicitly shortening the variables */ - %let optval=%sysfunc(getoption(varlenchk)); - options varlenchk=NOWARN; - data _data_(compress=char); - /* shorten the new vars */ - length - %do i=1 %to &numcols; - &&name&i $&&fmtlen&i - %end; - ; - /* rename on entry */ - set &ds(rename=( - %do i=1 %to &numcols; - &&name&i=&&newname&i - %end; - )); - &stmt_obs; - - drop - %do i=1 %to &numcols; - &&newname&i - %end; - ; - %do i=1 %to &numcols; - %if &&typelong&i=num %then %do; - &&name&i=cats(put(&&newname&i,&&fmt&i)); - %end; - %else %do; - &&name&i=put(&&newname&i,&&fmt&i); - %end; - %end; - if _error_ then do; - call symputx('syscc',1012); - stop; - end; - run; - %let fmtds=&syslast; - options varlenchk=&optval; - %end; - - proc format; /* credit yabwon for special null removal */ - value bart (default=40) - %if &missing=NULL %then %do; - ._ - .z = null - %end; - %else %do; - ._ = [quote()] - . = null - .a - .z = [quote()] - %end; - other = [best.]; - - data &tempds; - attrib _all_ label=''; - %do i=1 %to &numcols; - %if &&typelong&i=char or &fmt=Y %then %do; - length &&name&i $&&fmtlen&i...; - format &&name&i $&&fmtlen&i...; - %end; - %end; - %if &fmt=Y %then %do; - set &fmtds; - %end; - %else %do; - set &ds; - %end; - &stmt_obs; - format _numeric_ bart.; - %do i=1 %to &numcols; - %if &&typelong&i=char or &fmt=Y %then %do; - if findc(&&name&i,'"\'!!'0A0D09000E0F010210111A'x) then do; - &&name&i='"'!!trim( - prxchange('s/"/\\"/',-1, /* double quote */ - prxchange('s/\x0A/\n/',-1, /* new line */ - prxchange('s/\x0D/\r/',-1, /* carriage return */ - prxchange('s/\x09/\\t/',-1, /* tab */ - prxchange('s/\x00/\\u0000/',-1, /* NUL */ - prxchange('s/\x0E/\\u000E/',-1, /* SS */ - prxchange('s/\x0F/\\u000F/',-1, /* SF */ - prxchange('s/\x01/\\u0001/',-1, /* SOH */ - prxchange('s/\x02/\\u0002/',-1, /* STX */ - prxchange('s/\x10/\\u0010/',-1, /* DLE */ - prxchange('s/\x11/\\u0011/',-1, /* DC1 */ - prxchange('s/\x1A/\\u001A/',-1, /* SUB */ - prxchange('s/\\/\\\\/',-1,&&name&i) - )))))))))))))!!'"'; - end; - else &&name&i=quote(cats(&&name&i)); - %end; - %end; - run; - - filename _sjs3 temp lrecl=131068 ; - data _null_; - file _sjs3 encoding='utf-8'; - if _n_=1 then put "["; - set &tempds; - if _n_>1 then put "," @; put - %if &action=ARR %then "[" ; %else "{" ; - %do i=1 %to &numcols; - %if &i>1 %then "," ; - %if &action=OBJ %then """&&name&i"":" ; - "&&name&i"n /* name literal for reserved variable names */ - %end; - %if &action=ARR %then "]" ; %else "}" ; ; - - /* close out the table */ - data _null_; - file _sjs3 mod encoding='utf-8'; - put ']'; - run; - data _null_; - infile _sjs3 lrecl=1 recfm=n; - file &jref mod lrecl=1 recfm=n; - input sourcechar $char1. @@; - format sourcechar hex2.; - put sourcechar char1. @@; - run; - filename _sjs3 clear; - %end; - - proc sql; - drop table &colinfo, &tempds; - - %if %substr(&showmeta,1,1)=Y %then %do; - filename _sjs4 temp lrecl=131068 encoding='utf-8'; - data _null_; - file _sjs4; - length label $350; - put ", ""$%lowcase(%sysfunc(coalescec(&dslabel,&ds)))"":{""vars"":{"; - do i=1 to &numcols; - name=quote(trim(symget(cats('name',i)))); - format=quote(trim(symget(cats('fmt',i)))); - label=quote(prxchange('s/\\/\\\\/',-1,trim(symget(cats('label',i))))); - length=quote(trim(symget(cats('length',i)))); - type=quote(trim(symget(cats('typelong',i)))); - if i>1 then put "," @@; - put name ':{"format":' format ',"label":' label - ',"length":' length ',"type":' type '}'; - end; - put '}}'; - run; - /* send back to webout */ - data _null_; - infile _sjs4 lrecl=1 recfm=n; - file &jref mod lrecl=1 recfm=n; - input sourcechar $char1. @@; - format sourcechar hex2.; - put sourcechar char1. @@; - run; - filename _sjs4 clear; - %end; -%end; - -%else %if &action=CLOSE %then %do; - data _null_; file &jref encoding='utf-8' mod ; - put "}"; - run; -%end; -%mend mp_jsonout; diff --git a/009_macros/mp_lib2cards.sas b/009_macros/mp_lib2cards.sas deleted file mode 100644 index 5319ea5..0000000 --- a/009_macros/mp_lib2cards.sas +++ /dev/null @@ -1,78 +0,0 @@ -/*** HELP START ***//** - @file - @brief Convert all library members to CARDS files - @details Gets list of members then calls the %mp_ds2cards() macro. - Usage: - - %mp_lib2cards(lib=sashelp - , outloc= C:\temp ) - - The output will be one cards file in the `outloc` directory per dataset in the - input `lib` library. If the `outloc` directory does not exist, it is created. - - To create a single SAS file with the first 1000 records of each table in a - library you could use this syntax: - - %mp_lib2cards(lib=sashelp - , outloc= /tmp - , outfile= myfile.sas - , maxobs= 1000 - ) - -

SAS Macros

- @li mf_mkdir.sas - @li mf_trimstr.sas - @li mp_ds2cards.sas - - @param [in] lib= () Library in which to convert all datasets - @param [out] outloc= (%sysfunc(pathname(work))) Location in which to store - output. No quotes. - @param [out] outfile= (0) Optional output file NAME - if provided, then - will create a single output file instead of one file per input table. - @param [in] maxobs= (max) limit output to the first maxobs rows - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mp_lib2cards(lib= - ,outloc=%sysfunc(pathname(work)) - ,maxobs=max - ,random_sample=NO - ,outfile=0 -)/*/STORE SOURCE*/; - -/* Find the tables */ -%local x ds memlist; -proc sql noprint; -select distinct lowcase(memname) - into: memlist - separated by ' ' - from dictionary.tables - where upcase(libname)="%upcase(&lib)"; - -/* trim trailing slash, if provided */ -%let outloc=%mf_trimstr(&outloc,/); -%let outloc=%mf_trimstr(&outloc,\); - -/* create the output directory */ -%mf_mkdir(&outloc) - -/* create the cards files */ -%do x=1 %to %sysfunc(countw(&memlist)); - %let ds=%scan(&memlist,&x); - %mp_ds2cards(base_ds=&lib..&ds - ,maxobs=&maxobs - ,random_sample=&random_sample - %if "&outfile" ne "0" %then %do; - ,append=YES - ,cards_file="&outloc/&outfile" - %end; - %else %do; - ,append=NO - ,cards_file="&outloc/&ds..sas" - %end; - ) -%end; - -%mend mp_lib2cards; diff --git a/009_macros/mp_lib2inserts.sas b/009_macros/mp_lib2inserts.sas deleted file mode 100644 index fcf88e9..0000000 --- a/009_macros/mp_lib2inserts.sas +++ /dev/null @@ -1,77 +0,0 @@ -/*** HELP START ***//** - @file - @brief Convert all data in a library to SQL insert statements - @details Gets list of members then calls the %mp_ds2inserts() - macro. - Usage: - - %mp_getddl(sashelp, schema=work, fref=tempref) - - %mp_lib2inserts(sashelp, schema=work, outref=tempref) - - %inc tempref; - - - The output will be one file in the outref fileref. - - -

SAS Macros

- @li mp_ds2inserts.sas - - - @param [in] lib Library in which to convert all datasets to inserts - @param [in] flavour= (SAS) The SQL flavour to be applied to the output. Valid - options: - @li SAS (default) - suitable for regular proc sql - @li PGSQL - Used for Postgres databases - @param [in] maxobs= (max) The max number of observations (per table) to create - @param [out] outref= Output fileref in which to create the insert statements. - If it exists, it will be appended to, otherwise it will be created. - @param [out] schema= (0) The schema of the target database, or the libref. - @param [in] applydttm= (YES) If YES, any columns using datetime formats will - be converted to native DB datetime literals - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mp_lib2inserts(lib - ,flavour=SAS - ,outref=0 - ,schema=0 - ,maxobs=max - ,applydttm=YES -)/*/STORE SOURCE*/; - -/* Find the tables */ -%local x ds memlist; -proc sql noprint; -select distinct lowcase(memname) - into: memlist - separated by ' ' - from dictionary.tables - where upcase(libname)="%upcase(&lib)" - and memtype='DATA'; /* exclude views */ - - -%let flavour=%upcase(&flavour); -%if &flavour ne SAS and &flavour ne PGSQL %then %do; - %put %str(WAR)NING: &flavour is not supported; - %return; -%end; - - -/* create the inserts */ -%do x=1 %to %sysfunc(countw(&memlist)); - %let ds=%scan(&memlist,&x); - %mp_ds2inserts(&lib..&ds - ,outref=&outref - ,schema=&schema - ,outds=&ds - ,flavour=&flavour - ,maxobs=&maxobs - ,applydttm=&applydttm - ) -%end; - -%mend mp_lib2inserts; diff --git a/009_macros/mp_lockanytable.sas b/009_macros/mp_lockanytable.sas deleted file mode 100644 index 8c44d69..0000000 --- a/009_macros/mp_lockanytable.sas +++ /dev/null @@ -1,250 +0,0 @@ -/*** HELP START ***//** - @file - @brief Mechanism for locking tables to prevent parallel modifications - @details Uses a control table to enable ANY table to be locked for updates - (not just SAS datasets). - Only useful if every update uses the macro! Used heavily within - [Data Controller for SAS](https://datacontroller.io). - - @param [in] action The action to be performed. Valid values: - @li LOCK - Sets the lock flag, also confirms if a SAS lock is available - @li UNLOCK - Unlocks the table - @param [in] lib= (WORK) The libref of the table to lock. Should already be - assigned. - @param [in] ds= The dataset to lock - @param [in] ref= A meaningful reference to enable the lock to be traced. Max - length is 200 characters. - @param [out] ctl_ds= (0) The control table which controls the actual locking. - Should already be assigned and available. The definition is available by - running the mddl_dc_locktable.sas macro. - - @param [in] loops= (25) Number of times to check for a lock. - @param [in] loop_secs= (1) Seconds to wait between each lock attempt - -

SAS Macros

- @li mf_fmtdttm.sas - @li mp_abort.sas - @li mp_lockfilecheck.sas - @li mf_getuser.sas - -

Related Macros

- @li mp_lockanytable.test.sas - - @version 9.2 - -**//*** HELP END ***/ - -%macro mp_lockanytable( - action - ,lib= WORK - ,ds=0 - ,ref= - ,ctl_ds=0 - ,loops=25 - ,loop_secs=1 - ); -data _null_; - if _n_=1 then putlog "&sysmacroname entry vars:"; - set sashelp.vmacro; - where scope="&sysmacroname"; - put name '=' value; -run; - -%mp_abort(iftrue= ("&ds"="0" and &action ne MAKETABLE) - ,mac=&sysmacroname - ,msg=%str(dataset was not provided) -) -%mp_abort(iftrue= (&ctl_ds=0) - ,mac=&sysmacroname - ,msg=%str(Control dataset was not provided) -) - -/* set up lib & mac vars */ -%let lib=%upcase(&lib); -%let ds=%upcase(&ds); -%let action=%upcase(&action); -%local user x trans msg abortme; -%let user=%mf_getuser(); -%let abortme=0; - -%mp_abort(iftrue= (&action ne LOCK & &action ne UNLOCK & &action ne MAKETABLE) - ,mac=&sysmacroname - ,msg=%str(Invalid action (&action) provided) -) - -/* if an err condition exists, exit before we even begin */ -%mp_abort(iftrue= (&syscc>0 and &action=LOCK) - ,mac=&sysmacroname - ,msg=%str(aborting due to syscc=&syscc on LOCK entry) -) - -/* do not bother locking work tables (else may affect all WORK libraries) */ -%if (%upcase(&lib)=WORK or %str(&lib)=%str()) & &action ne MAKETABLE %then %do; - %put NOTE: WORK libraries will not be registered in the locking system.; - %return; -%end; - -/* do not proceed if no observations can be processed */ -%mp_abort(iftrue= (%sysfunc(getoption(OBS))=0) - ,mac=&sysmacroname - ,msg=%str(cannot continue when options obs = 0) -) - -%if &ACTION=LOCK %then %do; - - /* abort if a SAS lock is already in place, or cannot be applied */ - %mp_lockfilecheck(&lib..&ds) - - /* next, check there is a record for this table */ - %local record_exists_check; - proc sql noprint; - select count(*) into: record_exists_check from &ctl_ds - where LOCK_LIB ="&lib" and LOCK_DS="&ds"; - quit; - %if &syscc>0 %then %put syscc=&syscc sqlrc=&sqlrc; - %if &record_exists_check=0 %then %do; - data _null_; - putlog "&sysmacroname: adding record to lock table.."; - run; - - data ; - if 0 then set &ctl_ds; - LOCK_LIB ="&lib"; - LOCK_DS="&ds"; - LOCK_STATUS_CD='LOCKED'; - LOCK_START_DTTM="%sysfunc(datetime(),%mf_fmtdttm())"dt; - LOCK_USER_NM="&user"; - LOCK_PID="&sysjobid"; - LOCK_REF="&ref"; - output;stop; - run; - %let trans=&syslast; - proc append base=&ctl_ds data=&trans; - run; - %end; - /* if record does exist, perform lock attempts */ - %else %do x=1 %to &loops; - data _null_; - putlog "&sysmacroname: attempting lock (iteration &x) "@; - putlog "at %sysfunc(datetime(),datetime19.) .."; - run; - - proc sql; - update &ctl_ds - set LOCK_STATUS_CD='LOCKED' - , LOCK_START_DTTM="%sysfunc(datetime(),%mf_fmtdttm())"dt - , LOCK_USER_NM="&user" - , LOCK_PID="&sysjobid" - , LOCK_REF="&ref" - where LOCK_LIB ="&lib" and LOCK_DS="&ds"; - quit; - /** - * NOTE - occasionally SQL server will return an err code (deadlocked - * transaction). If so, ignore it, keep calm, and carry on.. - */ - %if &syscc>0 %then %do; - data _null_; - putlog 'NOTE-' / 'NOTE-'; - putlog "NOTE- &sysmacroname: Update failed. "@; - putlog "Resetting err conditions and re-attempting."; - putlog "NOTE- syscc=&syscc syserr=&syserr sqlrc=&sqlrc"; - putlog 'NOTE-' / 'NOTE-'; - run; - %let syscc=0; - %let sqlrc=0; - %end; - - /* now check if the record was successfully updated */ - %local success_check; - proc sql noprint; - select count(*) into: success_check from &ctl_ds - where LOCK_LIB ="&lib" and LOCK_DS="&ds" - and LOCK_PID="&sysjobid" and LOCK_STATUS_CD='LOCKED'; - quit; - %if &success_check=0 %then %do; - %if &x < &loops %then %do; - /* pause before next check */ - data _null_; - putlog 'NOTE-' / 'NOTE-'; - putlog "NOTE- &sysmacroname: table locked, waiting "@; - putlog "%sysfunc(sleep(&loop_secs)) seconds.. "; - putlog "NOTE- (iteration &x of &loops)"; - putlog 'NOTE-' / 'NOTE-'; - run; - %end; - %else %do; - %let msg=Unable to lock &lib..&ds via &ctl_ds after &loops attempts.\n - Please ask your administrator to investigate!; - %let abortme=1; - %end; - %end; - %else %do; - data _null_; - putlog 'NOTE-' / 'NOTE-'; - putlog "NOTE- &sysmacroname: Table &lib..&ds locked at "@; - putlog " %sysfunc(datetime(),datetime19.) (iteration &x)"@; - putlog 'NOTE-' / 'NOTE-'; - run; - %if &syscc>0 %then %do; - %put setting syscc(&syscc) back to 0; - %let syscc=0; - %end; - %let x=&loops; /* no more iterations needed */ - %end; - %end; -%end; -%else %if &ACTION=UNLOCK %then %do; - %local status cnt; - %let cnt=0; - proc sql noprint; - select count(*) into: cnt from &ctl_ds where LOCK_LIB ="&lib" & LOCK_DS="&ds"; - %if &cnt=0 %then %do; - %put %str(WAR)NING: &lib..&ds was not previously locked in &ctl_ds!; - %end; - %else %do; - select LOCK_STATUS_CD into: status from &ctl_ds - where LOCK_LIB ="&lib" and LOCK_DS="&ds"; - quit; - %if &syscc>0 %then %put syscc=&syscc sqlrc=&sqlrc; - %if &status=LOCKED %then %do; - data _null_; - putlog "&sysmacroname: unlocking &lib..&ds:"; - run; - proc sql; - update &ctl_ds - set LOCK_STATUS_CD='UNLOCKED' - , LOCK_END_DTTM="%sysfunc(datetime(),%mf_fmtdttm())"dt - , LOCK_USER_NM="&user" - , LOCK_PID="&sysjobid" - , LOCK_REF="&ref" - where LOCK_LIB ="&lib" and LOCK_DS="&ds"; - quit; - %end; - %else %if &status=UNLOCKED %then %do; - %put %str(WAR)NING: &lib..&ds is already unlocked!; - %end; - %else %do; - %put NOTE: Unrecognised STATUS_CD (&status) in &ctl_ds; - %let abortme=1; - %end; - %end; -%end; -%else %do; - %let msg=lock_anytable given unsupported action (&action); - %let abortme=1; -%end; - -/* catch errs - mp_abort must be called outside of a logic block */ -%mp_abort(iftrue=(&abortme=1), - msg=%superq(msg), - mac=&sysmacroname -) - -%exit_macro: -data _null_; - put "&sysmacroname: Exit vars: action=&action lib=&lib ds=&ds"; - put " syscc=&syscc sqlrc=&sqlrc syserr=&syserr"; -run; -%mend mp_lockanytable; - - diff --git a/009_macros/mp_lockfilecheck.sas b/009_macros/mp_lockfilecheck.sas deleted file mode 100644 index 3125fa0..0000000 --- a/009_macros/mp_lockfilecheck.sas +++ /dev/null @@ -1,103 +0,0 @@ -/*** HELP START ***//** - @file - @brief Aborts if a SAS lock file is in place, or if one cannot be applied. - @details Used in conjuction with the mp_lockanytable macro. - More info here: https://sasensei.com/flash/24 - - Usage: - - data work.test; a=1;run; - %mp_lockfilecheck(work.test) - - @param [in] libds The libref.dataset for which to check the lock status - -

SAS Macros

- @li mp_abort.sas - @li mf_getattrc.sas - -

Related Macros

- @li mp_lockanytable.sas - @li mp_lockfilecheck.test.sas - - @version 9.2 -**//*** HELP END ***/ - -%macro mp_lockfilecheck( - libds -)/*/STORE SOURCE*/; - -data _null_; - if _n_=1 then putlog "&sysmacroname entry vars:"; - set sashelp.vmacro; - where scope="&sysmacroname"; - put name '=' value; -run; - -%mp_abort(iftrue= (&syscc>0) - ,mac=checklock.sas - ,msg=Aborting with syscc=&syscc on entry. -) -%mp_abort(iftrue= ("&libds"="0") - ,mac=&sysmacroname - ,msg=%str(libds not provided) -) - -%local msg lib ds; -%let lib=%upcase(%scan(&libds,1,.)); -%let ds=%upcase(%scan(&libds,2,.)); - -/* in DC, format catalogs are passed with a -FC suffix. No saslock here! */ -%if %scan(&libds,2,-)=FC %then %do; - %put &sysmacroname: Format Catalog detected, no lockfile applied to &libds; - %return; -%end; - -/* do not proceed if no observations can be processed */ -%let msg=options obs = 0. syserrortext=%superq(syserrortext); -%mp_abort(iftrue= (%sysfunc(getoption(OBS))=0) - ,mac=checklock.sas - ,msg=%superq(msg) -) - -data _null_; - putlog "Checking engine & member type"; -run; -%local engine memtype; -%let memtype=%mf_getattrc(&libds,MTYPE); -%let engine=%mf_getattrc(&libds,ENGINE); - -%if &engine ne V9 and &engine ne BASE %then %do; - data _null_; - putlog "Lib &lib is not assigned using BASE engine - uses &engine instead"; - putlog "SAS lock check will not be performed"; - run; - %return; -%end; -%else %if &memtype ne DATA %then %do; - %put NOTE: Cannot lock a VIEW!! Memtype=&memtype; - %return; -%end; - -data _null_; - putlog "Engine = &engine, memtype=&memtype"; - putlog "Attempting lock statement"; -run; - -lock &libds; - -%local abortme; -%let abortme=0; -%if &syscc>0 or &SYSLCKRC ne 0 %then %do; - %let msg=Unable to apply lock on &libds (SYSLCKRC=&SYSLCKRC syscc=&syscc); - %put %str(ERR)OR: &sysmacroname: &msg; - %let abortme=1; -%end; - -lock &libds clear; - -%mp_abort(iftrue= (&abortme=1) - ,mac=&sysmacroname - ,msg=%superq(msg) -) - -%mend mp_lockfilecheck; diff --git a/009_macros/mp_makedata.sas b/009_macros/mp_makedata.sas deleted file mode 100644 index d08b192..0000000 --- a/009_macros/mp_makedata.sas +++ /dev/null @@ -1,107 +0,0 @@ -/*** HELP START ***//** - @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 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 (libref.dataset) in which to create data - @param [out] obs= (500) The maximum number of records to create. The table - is sorted with nodup on the primary key, so the actual number of records may - be lower than this. - -

SAS Macros

- @li mf_getuniquename.sas - @li mf_getvarlen.sas - @li mf_getvarlist.sas - @li mf_islibds.sas - @li mf_nobs.sas - @li mp_getcols.sas - @li mp_getpk.sas - -

Related Macros

- @li mp_makedata.test.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_makedata(libds - ,obs=500 - ,seed=1 -)/*/STORE SOURCE*/; - -%local ds1 ds2 lib ds pk_fields i col charvars numvars ispk; - -%if %mf_islibds(&libds)=0 %then %do; - %put &sysmacroname: Invalid libds (&libds) - should be library.dataset format; - %return; -%end; -%else %if %mf_nobs(&libds)>0 %then %do; - %put &sysmacroname: &libds has data, it will not be recreated; - %return; -%end; - -/* set up temporary vars */ -%let ds1=%mf_getuniquename(prefix=mp_makedatads1); -%let ds2=%mf_getuniquename(prefix=mp_makedatads2); -%let lib=%scan(&libds,1,.); -%let ds=%scan(&libds,2,.); - -/* grab the primary key vars */ -%mp_getpk(&lib,ds=&ds,outds=&ds1) - -proc sql noprint; -select coalescec(pk_fields,'_all_') into: pk_fields from &ds1; - -data &ds2; - if 0 then set &libds; - do _n_=1 to &obs; - %let charvars=%mf_getvarlist(&libds,typefilter=C); - %if &charvars ^= %then %do i=1 %to %sysfunc(countw(&charvars)); - %let col=%scan(&charvars,&i); - /* create random value based on observation number and colum length */ - &col=repeat(put(md5(cats(_n_)),$hex32.),%mf_getvarlen(&libds,&col)/32); - %end; - - %let numvars=%mf_getvarlist(&libds,typefilter=N); - %if &numvars ^= %then %do i=1 %to %sysfunc(countw(&numvars)); - %let col=%scan(&numvars,&i); - &col=_n_; - %end; - output; - end; - stop; -run; -proc sort data=&ds2 nodupkey; - by &pk_fields; -run; - -proc append base=&libds data=&ds2; -run; - -proc sql; -drop table &ds1, &ds2; - -%mend mp_makedata; diff --git a/009_macros/mp_md5.sas b/009_macros/mp_md5.sas deleted file mode 100644 index 85ca781..0000000 --- a/009_macros/mp_md5.sas +++ /dev/null @@ -1,57 +0,0 @@ -/*** HELP START ***//** - @file - @brief Generates an md5 expression for hashing a set of variables - @details This is the same algorithm used to hash records in - [Data Controller for SAS](https://datacontroller.io). - - It is not designed to be efficient - it is designed to be effective, - given the range of edge cases (large floating points, special missing - numerics, thousands of columns, very wide columns). - - It can be used only in data step, eg as follows: - - data _null_; - set sashelp.class; - hashvar=%mp_md5(cvars=name sex, nvars=age height weight); - put hashvar=; - run; - - Unfortunately it will not run in SQL - it fails with the following message: - - > The width value for HEX is out of bounds. It should be between 1 and 16 - - The macro will also cause errors if the data contains (non-special) missings - and the (undocumented) `options dsoptions=nonote2err;` is in effect. - - This can be avoided in two ways: - - @li Global option: `options dsoptions=nonote2err;` - @li Data step option: `data YOURLIB.YOURDATASET /nonote2err;` - - @param [in] cvars= () Space seperated list of character variables - @param [in] nvars= () Space seperated list of numeric variables - -

Related Programs

- @li mp_init.sas - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mp_md5(cvars=,nvars=); -%local i var sep; -put(md5( - %do i=1 %to %sysfunc(countw(&cvars)); - %let var=%scan(&cvars,&i,%str( )); - &sep put(md5(trim(&var)),$hex32.) - %let sep=!!; - %end; - %do i=1 %to %sysfunc(countw(&nvars)); - %let var=%scan(&nvars,&i,%str( )); - /* multiply by 1 to strip precision errors (eg 0 != 0) */ - /* but ONLY if not missing, else will lose any special missing values */ - &sep put(md5(trim(put(ifn(missing(&var),&var,&var*1),binary64.))),$hex32.) - %let sep=!!; - %end; -),$hex32.) -%mend mp_md5; diff --git a/009_macros/mp_perflog.sas b/009_macros/mp_perflog.sas deleted file mode 100644 index 0ccd0c8..0000000 --- a/009_macros/mp_perflog.sas +++ /dev/null @@ -1,43 +0,0 @@ -/*** HELP START ***//** - @file - @brief Logs a message in a dataset every time it is invoked - @details If the dataset does not exist, it is created. - Usage: - - %mp_perflog(started) - %mp_perflog() - %mp_perflog(startanew,libds=work.newdataset) - %mp_perflog(finished,libds=work.newdataset) - %mp_perflog(finished) - - - @param [in] label Provide label to go into the control dataset - @param [in] libds= (work.mp_perflog) Provide a dataset in which to store - performance stats. Default name is work.mp_perflog; - - @version 9.2 - @author Allan Bowe - @source https://github.com/sasjs/core - -**//*** HELP END ***/ - -%macro mp_perflog(label,libds=work.mp_perflog -)/*/STORE SOURCE*/; - - %if not (%mf_existds(&libds)) %then %do; - data &libds; - length sysjobid $10 label $256 dttm 8.; - format dttm datetime19.3; - call missing(of _all_); - stop; - run; - %end; - - proc sql; - insert into &libds - set sysjobid="&sysjobid" - ,label=symget('label') - ,dttm=%sysfunc(datetime()); - quit; - -%mend mp_perflog; diff --git a/009_macros/mp_prevobs.sas b/009_macros/mp_prevobs.sas deleted file mode 100644 index b4b06a3..0000000 --- a/009_macros/mp_prevobs.sas +++ /dev/null @@ -1,88 +0,0 @@ -/*** HELP START ***//** - @file - @brief Enables previous observations to be re-instated - @details Remembers the last X observations by storing them in a hash table. - Is a convenience over the use of lag() or retain, when an entire observation - needs to be restored. - - This macro will also restore automatic variables (such as _n_ and _error_). - - Example Usage: - - data example; - set sashelp.class; - calc_var=_n_*3; - %* initialise hash and save from PDV ; - %mp_prevobs(INIT,history=2) - if _n_ =10 then do; - %* fetch previous but 1 record; - %mp_prevobs(FETCH,-2) - put _n_= name= age= calc_var=; - %* fetch previous record; - %mp_prevobs(FETCH,-1) - put _n_= name= age= calc_var=; - %* reinstate current record ; - %mp_prevobs(FETCH,0) - put _n_= name= age= calc_var=; - end; - run; - - Result: - - mp_prevobs sas - - Credit is made to `data _null_` for authoring this very helpful paper: - https://www.lexjansen.com/pharmasug/2008/cc/CC08.pdf - - @param [in] action Either FETCH a current or previous record, or INITialise. - @param [in] record The relative (to current) position of the previous row - to return. - @param [in] history= (5) The number of records to retain in the hash table. - @param [in] prefix= (mp_prevobs) The prefix to give to the variables used to - store the hash name and index. - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_prevobs(action,record,history=5,prefix=mp_prevobs -)/*/STORE SOURCE*/; -%let action=%upcase(&action); -%let prefix=%upcase(&prefix); -%let record=%eval((&record+0) * -1); - -%if &action=INIT %then %do; - - if _n_ eq 1 then do; - attrib &prefix._VAR length=$64; - dcl hash &prefix._HASH(ordered:'Y'); - &prefix._KEY=0; - &prefix._HASH.defineKey("&prefix._KEY"); - do while(1); - call vnext(&prefix._VAR); - if &prefix._VAR='' then leave; - if &prefix._VAR eq "&prefix._VAR" then continue; - else if &prefix._VAR eq "&prefix._KEY" then continue; - &prefix._HASH.defineData(&prefix._VAR); - end; - &prefix._HASH.defineDone(); - end; - /* this part has to happen before FETCHing */ - &prefix._KEY+1; - &prefix._rc=&prefix._HASH.add(); - if &prefix._rc then putlog 'adding' &prefix._rc=; - %if &history>0 %then %do; - if &prefix._key>&history+1 then - &prefix._HASH.remove(key: &prefix._KEY - &history - 1); - if &prefix._rc then putlog 'removing' &prefix._rc=; - %end; -%end; -%else %if &action=FETCH %then %do; - if &record>&prefix._key then putlog "Not enough records in &Prefix._hash yet"; - else &prefix._rc=&prefix._HASH.find(key: &prefix._KEY - &record); - if &prefix._rc then putlog &prefix._rc= " when fetching " &prefix._KEY= - "with record &record and " _n_=; -%end; - -%mend mp_prevobs; diff --git a/009_macros/mp_recursivejoin.sas b/009_macros/mp_recursivejoin.sas deleted file mode 100644 index 4a15aa9..0000000 --- a/009_macros/mp_recursivejoin.sas +++ /dev/null @@ -1,91 +0,0 @@ -/*** HELP START ***//** - @file - @brief Returns all children from a hierarchy table for a specified parent - @details Where data stores hierarchies in a simple parent / child mapping, - it is not always straightforward to extract all the children for a - particular parent. This problem is known as a recursive self join. This - macro will extract all the descendents for a parent. - Usage: - - data have; - p=1;c=2;output; - p=2;c=3;output; - p=2;c=4;output; - p=3;c=5;output; - p=6;c=7;output; - p=8;c=9;output; - run; - - %mp_recursivejoin(base_ds=have - ,outds=want - ,matchval=1 - ,parentvar=p - ,childvar=c - ) - - @param [in] base_ds= base table containing hierarchy (not modified) - @param [out] outds= the output dataset to create with the generated hierarchy - @param [in] matchval= the ultimate parent from which to filter - @param [in] parentvar= name of the parent variable - @param [in] childvar= () name of the child variable (should be same type as - parent) - @param [in] mdebug= set to 1 to prevent temp tables being dropped - - - @returns outds contains the following variables: - - level (0 = top level) - - &parentvar - - &childvar (null if none found) - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_recursivejoin(base_ds= - ,outds= - ,matchval= - ,parentvar= - ,childvar= - ,iter= /* reserved for internal / recursive use by the macro itself */ - ,maxiter=500 /* avoid infinite loop */ - ,mDebug=0); - -%if &iter= %then %do; - proc sql; - create table &outds as - select 0 as level,&parentvar, &childvar - from &base_ds - where &parentvar=&matchval; - %if &sqlobs.=0 %then %do; - %put NOTE: &sysmacroname: No match for &parentvar=&matchval; - %return; - %end; - %let iter=1; -%end; -%else %if &iter>&maxiter %then %return; - -proc sql; -create table _data_ as - select &iter as level - ,curr.&childvar as &parentvar - ,base_ds.&childvar as &childvar - from &outds curr - left join &base_ds base_ds - on curr.&childvar=base_ds.&parentvar - where curr.level=%eval(&iter.-1) - & curr.&childvar is not null; -%local append_ds; %let append_ds=&syslast; -%local obs; %let obs=&sqlobs; -insert into &outds select distinct * from &append_ds; -%if &mdebug=0 %then drop table &append_ds;; - -%if &obs %then %do; - %mp_recursivejoin(iter=%eval(&iter.+1) - ,outds=&outds,parentvar=&parentvar - ,childvar=&childvar - ,base_ds=&base_ds - ) -%end; - -%mend mp_recursivejoin; diff --git a/009_macros/mp_replace.sas b/009_macros/mp_replace.sas deleted file mode 100644 index fd391f4..0000000 --- a/009_macros/mp_replace.sas +++ /dev/null @@ -1,152 +0,0 @@ -/*** HELP START ***//** - @file - @brief Performs a text substitution on a file - @details Performs a find and replace on a file, either in place or to a new - file. Can be used on files where lines are longer than 32767. - - Works by reading in the file byte by byte, then marking the beginning and end - of each matched string, before finally doing the replace. - - Full credit for this highly efficient and syntactically satisfying SAS logic - goes to [Bartosz Jabłoński](https://www.linkedin.com/in/yabwon), founder of - the [SAS Packages](https://github.com/yabwon/SAS_PACKAGES) framework. - - Usage: - - %let file="%sysfunc(pathname(work))/file.txt"; - %let str=replace/me; - %let rep=with/this; - data _null_; - file &file; - put 'blahblah'; - put "blahblah&str.blah"; - put 'blahblahblah'; - run; - %mp_replace(&file, findvar=str, replacevar=rep) - data _null_; - infile &file; - input; - list; - run; - - Note - if you are running a version of SAS that will allow the io package in - LUA, you can also use this macro: mp_gsubfile.sas - - @param [in] infile The QUOTED path to the file on which to perform the - substitution - @param [in] findvar= Macro variable NAME containing the string to search for - @param [in] replacevar= Macro variable NAME containing the replacement string - @param [out] outfile= (0) Optional QUOTED path to the adjusted output file (to - avoid overwriting the first file). - -

SAS Macros

- @li mf_getuniquefileref.sas - @li mf_getuniquename.sas - -

Related Macros

- @li mp_chop.sas - @li mp_gsubfile.sas - @li mp_replace.test.sas - - @version 9.4 - @author Bartosz Jabłoński - @author Allan Bowe -**//*** HELP END ***/ - -%macro mp_replace(infile, - findvar=, - replacevar=, - outfile=0 -)/*/STORE SOURCE*/; - -%local inref dttm ds1; -%let inref=%mf_getuniquefileref(); -%let outref=%mf_getuniquefileref(); -%if &outfile=0 %then %let outfile=&infile; -%let ds1=%mf_getuniquename(prefix=allchars); -%let ds2=%mf_getuniquename(prefix=startmark); - -/* START */ -%let dttm=%sysfunc(datetime()); - -filename &inref &infile lrecl=1 recfm=n; - -data &ds1; - infile &inref; - input sourcechar $char1. @@; - format sourcechar hex2.; -run; - -data &ds2; - /* set find string to length in bytes to cover trailing spaces */ - length string $ %length(%superq(&findvar)); - string =symget("&findvar"); - drop string; - - firstchar=char(string,1); - findlen=lengthm(string); /* <- for trailing bytes */ - - do _N_=1 to nobs; - set &ds1 nobs=nobs point=_N_; - if sourcechar=firstchar then do; - pos=1; - s=0; - do point=_N_ to min(_N_ + findlen -1,nobs); - set &ds1 point=point; - if sourcechar=char(string, pos) then s + 1; - else goto _leave_; - pos+1; - end; - _leave_: - if s=findlen then do; - START =_N_; - _N_ =_N_+ s - 1; - STOP =_N_; - output; - end; - end; - end; - stop; - keep START STOP; -run; - -data &ds1; - declare hash HS(dataset:"&ds2(keep=start)"); - HS.defineKey("start"); - HS.defineDone(); - declare hash HE(dataset:"&ds2(keep=stop)"); - HE.defineKey("stop"); - HE.defineDone(); - do until(eof); - set &ds1 end=eof curobs =n; - start = ^HS.check(key:n); - stop = ^HE.check(key:n); - length strt $ 1; - strt =put(start,best. -L); - retain out 1; - if out then output; - if start then out=0; - if stop then out=1; - end; - stop; - keep sourcechar strt; -run; - -filename &outref &outfile recfm=n; - -data _null_; - length replace $ %length(%superq(&replacevar)); - replace=symget("&replacevar"); - file &outref; - do until(eof); - set &ds1 end=eof; - if strt ="1" then put replace char.; - else put sourcechar char1.; - end; - stop; -run; - -/* END */ -%put &sysmacroname took %sysevalf(%sysfunc(datetime())-&dttm) seconds to run; - -%mend mp_replace; diff --git a/009_macros/mp_reseterror.sas b/009_macros/mp_reseterror.sas deleted file mode 100644 index c89cd79..0000000 --- a/009_macros/mp_reseterror.sas +++ /dev/null @@ -1,27 +0,0 @@ -/*** HELP START ***//** - @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 - -**//*** HELP END ***/ - -%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; diff --git a/009_macros/mp_resetoption.sas b/009_macros/mp_resetoption.sas deleted file mode 100644 index 0615214..0000000 --- a/009_macros/mp_resetoption.sas +++ /dev/null @@ -1,39 +0,0 @@ -/*** HELP START ***//** - @file - @brief Reset an option to original value - @details Inspired by the SAS Jedi - -https://blogs.sas.com/content/sastraining/2012/08/14/jedi-sas-tricks-reset-sas-system-options - - Called as follows: - - options obs=30 ps=max; - %mp_resetoption(OBS) - %mp_resetoption(PS) - - - @param [in] option the option to reset - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_resetoption(option /* the option to reset */ -)/*/STORE SOURCE*/; - -%if "%substr(&sysver,1,1)" ne "4" and "%substr(&sysver,1,1)" ne "5" %then %do; - data _null_; - length code $1500; - startup=getoption("&option",'startupvalue'); - current=getoption("&option"); - if startup ne current then do; - code =cat('OPTIONS ',getoption("&option",'keyword','startupvalue'),';'); - putlog "NOTE: Resetting system option: " code ; - call execute(code ); - end; - run; -%end; -%else %do; - %put &sysmacroname: reset option feature unavailable on &sysvlong; -%end; -%mend mp_resetoption; diff --git a/009_macros/mp_retainedkey.sas b/009_macros/mp_retainedkey.sas deleted file mode 100644 index 9fbad99..0000000 --- a/009_macros/mp_retainedkey.sas +++ /dev/null @@ -1,247 +0,0 @@ -/*** HELP START ***//** - @file - @brief Generate and apply retained key values to a staging table - @details This macro will populate a staging table with a Retained Key based on - a business key and a base (target) table. - - Definition of retained key ([source]( - http://bukhantsov.org/2012/04/what-is-data-vault/)): - - > The retained key is a key which is mapped to business key one-to-one. In - > comparison, the surrogate key includes time and there can be many surrogate - > keys corresponding to one business key. This explains the name of the key, - > it is retained with insertion of a new version of a row while surrogate key - > is increasing. - - This macro is designed to be used as part of a wider load / ETL process (such - as the one in [Data Controller for SAS](https://datacontroller.io)). - - Specifically, the macro assumes that the base table has already been 'locked' - (eg with the mp_lockanytable.sas macro) prior to invocation. Also, several - tables are assumed to exist (names are configurable): - - @li work.staging_table - the staged data, minus the retained key element - @li permlib.base_table - the target table to be loaded (**not** loaded by this - macro) - @li permlib.maxkeytable - optional, used to store load metaadata. - The definition is available by running mp_coretable.sas as follows: - `mp_coretable(MAXKEYTABLE)`. - @li permlib.locktable - Necessary if maxkeytable is being populated. The - definition is available by running mp_coretable.sas as follows: - `mp_coretable(LOCKTABLE)`. - - - @param [in] base_lib= (WORK) Libref of the base (target) table. - @param [in] base_dsn= (BASETABLE) Name of the base (target) table. - @param [in] append_lib= (WORK) Libref of the staging table - @param [in] append_dsn= (APPENDTABLE) Name of the staging table - @param [in] retained_key= (DEFAULT_RK) Name of RK to generate (should exist on - base table) - @param [in] business_key= (PK1 PK2) Business key against which to generate - RK values. Should be unique and not null on the staging table. - @param [in] check_uniqueness=(NO) Set to yes to perform a uniqueness check. - Recommended if there is a chance that the staging data is not unique on the - business key. - @param [in] maxkeytable= (0) Provide a maxkeytable libds reference here, to - store load metadata (maxkey val, load time). Set to zero if metadata is not - required, eg, when preparing a 'dummy' load. Structure is described above. - See below for sample data. - |KEYTABLE:$32.|KEYCOLUMN:$32.|MAX_KEY:best.|PROCESSED_DTTM:E8601DT26.6| - |---|---|---|---| - |`DC487173.MPE_SELECTBOX `|`SELECTBOX_RK `|`55 `|`1950427787.8 `| - |`DC487173.MPE_FILTERANYTABLE `|`filter_rk `|`14 `|`1951053886.8 `| - @param [in] locktable= (0) If updating the maxkeytable, provide the libds - reference to the lock table (per mp_lockanytable.sas macro) - @param [in] filter_str= Apply a filter - useful for SCD2 or BITEMPORAL loads. - Example: `filter_str=%str( (where=( &now < &tech_to)) )` - @param [out] outds= (WORK.APPEND) Output table (staging table + retained key) - -

SAS Macros

- @li mf_existvar.sas - @li mf_fmtdttm.sas - @li mf_getquotedstr.sas - @li mf_getuniquename.sas - @li mf_nobs.sas - @li mp_abort.sas - @li mp_lockanytable.sas - -

Related Macros

- @li mp_filterstore.sas - @li mp_retainedkey.test.sas - - @version 9.2 - -**//*** HELP END ***/ - -%macro mp_retainedkey( - base_lib=WORK - ,base_dsn=BASETABLE - ,append_lib=WORK - ,append_dsn=APPENDTABLE - ,retained_key=DEFAULT_RK - ,business_key= PK1 PK2 - ,check_uniqueness=NO - ,maxkeytable=0 - ,locktable=0 - ,outds=WORK.APPEND - ,filter_str= -); -%put &sysmacroname entry vars:; -%put _local_; - -%local base_libds app_libds key_field check maxkey idx_pk newkey_cnt iserr - msg x tempds1 tempds2 comma_pk appnobs checknobs dropvar tempvar idx_val; -%let base_libds=%upcase(&base_lib..&base_dsn); -%let app_libds=%upcase(&append_lib..&append_dsn); -%let tempds1=%mf_getuniquename(); -%let tempds2=%mf_getuniquename(); -%let comma_pk=%mf_getquotedstr(in_str=%str(&business_key),dlm=%str(,),quote=); -%let outds=%sysfunc(ifc(%index(&outds,.)=0,work.&outds,&outds)); -/* validation checks */ -%let iserr=0; -%if &syscc>0 %then %do; - %let iserr=1; - %let msg=%str(SYSCC=&syscc on macro entry); -%end; -%else %if %sysfunc(exist(&base_libds))=0 %then %do; - %let iserr=1; - %let msg=%str(Base LIBDS (&base_libds) expected but NOT FOUND); -%end; -%else %if %sysfunc(exist(&app_libds))=0 %then %do; - %let iserr=1; - %let msg=%str(Append LIBDS (&app_libds) expected but NOT FOUND); -%end; -%else %if &maxkeytable ne 0 and %sysfunc(exist(&maxkeytable))=0 %then %do; - %let iserr=1; - %let msg=%str(Maxkeytable (&maxkeytable) expected but NOT FOUND); -%end; -%else %if &maxkeytable ne 0 and %sysfunc(exist(&locktable))=0 %then %do; - %let iserr=1; - %let msg=%str(Locktable (&locktable) expected but NOT FOUND); -%end; -%else %if %length(&business_key)=0 %then %do; - %let iserr=1; - %let msg=%str(Business key (&business_key) expected but NOT FOUND); -%end; - -%do x=1 %to %sysfunc(countw(&business_key)); - /* check business key values exist */ - %let key_field=%scan(&business_key,&x,%str( )); - %if not %mf_existvar(&app_libds,&key_field) %then %do; - %let iserr=1; - %let msg=Business key (&key_field) not found on &app_libds!; - %goto err; - %end; - %else %if not %mf_existvar(&base_libds,&key_field) %then %do; - %let iserr=1; - %let msg=Business key (&key_field) not found on &base_libds!; - %goto err; - %end; -%end; -%err: -%if &iserr=1 %then %do; - /* err case so first perform an unlock of the base table before exiting */ - %mp_lockanytable( - UNLOCK,lib=&base_lib,ds=&base_dsn,ref=%superq(msg),ctl_ds=&locktable - ) -%end; -%mp_abort(iftrue=(&iserr=1),mac=mp_retainedkey,msg=%superq(msg)) - -proc sql noprint; -select sum(max(&retained_key),0) into: maxkey from &base_libds; - -/** - * get base table RK and bus field values for lookup - */ -proc sql noprint; -create table &tempds1 as - select distinct &comma_pk,&retained_key - from &base_libds &filter_str - order by &comma_pk,&retained_key; - -%if &check_uniqueness=YES %then %do; - select count(*) into:checknobs - from (select distinct &comma_pk from &app_libds); - select count(*) into: appnobs from &app_libds; /* might be view */ - %if &checknobs ne &appnobs %then %do; - %let msg=Source table &app_libds is not unique on (&business_key); - %let iserr=1; - %end; -%end; -%if &iserr=1 %then %do; - /* err case so first perform an unlock of the base table before exiting */ - %mp_lockanytable( - UNLOCK,lib=&base_lib,ds=&base_dsn,ref=%superq(msg),ctl_ds=&locktable - ) -%end; -%mp_abort(iftrue= (&iserr=1),mac=mp_retainedkey,msg=%superq(msg)) - -%if %mf_existvar(&app_libds,&retained_key) -%then %let dropvar=(drop=&retained_key); - -/* prepare interim table with retained key populated for matching keys */ -proc sql noprint; -create table &tempds2 as - select b.&retained_key, a.* - from &app_libds &dropvar a - left join &tempds1 b - on 1 - %do idx_pk=1 %to %sysfunc(countw(&business_key)); - %let idx_val=%scan(&business_key,&idx_pk); - and a.&idx_val=b.&idx_val - %end; - order by &retained_key; - -/* identify the number of entries without retained keys (new records) */ -select count(*) into: newkey_cnt - from &tempds2 - where missing(&retained_key); -quit; - -/** - * Update maxkey table if link provided - */ -%if &maxkeytable ne 0 %then %do; - proc sql noprint; - select count(*) into: check from &maxkeytable - where upcase(keytable)="&base_libds"; - - %mp_lockanytable(LOCK - ,lib=%scan(&maxkeytable,1,.) - ,ds=%scan(&maxkeytable,2,.) - ,ref=Updating maxkeyvalues with mp_retainedkey - ,ctl_ds=&locktable - ) - proc sql; - %if &check=0 %then %do; - insert into &maxkeytable - set keytable="&base_libds" - ,keycolumn="&retained_key" - ,max_key=%eval(&maxkey+&newkey_cnt) - ,processed_dttm="%sysfunc(datetime(),%mf_fmtdttm())"dt; - %end; - %else %do; - update &maxkeytable - set max_key=%eval(&maxkey+&newkey_cnt) - ,processed_dttm="%sysfunc(datetime(),%mf_fmtdttm())"dt - where keytable="&base_libds"; - %end; - %mp_lockanytable(UNLOCK - ,lib=%scan(&maxkeytable,1,.) - ,ds=%scan(&maxkeytable,2,.) - ,ref=Updating maxkeyvalues with maxkey=%eval(&maxkey+&newkey_cnt) - ,ctl_ds=&locktable - ) -%end; - -/* fill in the missing retained key values */ -%let tempvar=%mf_getuniquename(); -data &outds(drop=&tempvar); - retain &tempvar %eval(&maxkey+1); - set &tempds2; - if &retained_key =. then &retained_key=&tempvar; - &tempvar=&tempvar+1; -run; - -%mend mp_retainedkey; - diff --git a/009_macros/mp_runddl.sas b/009_macros/mp_runddl.sas deleted file mode 100644 index 0287b90..0000000 --- a/009_macros/mp_runddl.sas +++ /dev/null @@ -1,49 +0,0 @@ -/*** HELP START ***//** - @file mp_runddl.sas - @brief An opinionated way to execute DDL files in SAS. - @details When delivering projects there should be seperation between the DDL - used to generate the tables and the sample data used to populate them. - - This macro expects certain folder structure - eg: - - rootlib - |-- LIBREF1 - | |__ mytable.ddl - | |__ someothertable.ddl - |-- LIBREF2 - | |__ table1.ddl - | |__ table2.ddl - |-- LIBREF3 - |__ table3.ddl - |__ table4.ddl - - Only files with the .ddl suffix are executed. The parent folder name is used - as the libref. - Files should NOT contain the `proc sql` statement - this is to prevent - statements being executed if there is an err condition. - - Usage: - - %mp_runddl(/some/rootlib) * execute all libs ; - - %mp_runddl(/some/rootlib, inc=LIBREF1 LIBREF2) * include only these libs; - - %mp_runddl(/some/rootlib, exc=LIBREF3) * same as above ; - - - @param [in] path location of the DDL folder structure - @param [in] inc= list of librefs to include - @param [in] exc= list of librefs to exclude (takes precedence over inc=) - - @version 9.3 - @author Allan Bowe - @source https://github.com/sasjs/core - -**//*** HELP END ***/ - -%macro mp_runddl(path, inc=, exc= -)/*/STORE SOURCE*/; - - - -%mend mp_runddl; diff --git a/009_macros/mp_searchcols.sas b/009_macros/mp_searchcols.sas deleted file mode 100644 index 26e35ad..0000000 --- a/009_macros/mp_searchcols.sas +++ /dev/null @@ -1,104 +0,0 @@ -/*** HELP START ***//** - @file mp_searchcols.sas - @brief Searches all columns in a library - @details - Scans a set of libraries and creates a dataset containing all source tables - containing one or more of a particular set of columns - - Usage: - - %mp_searchcols(libs=sashelp work, cols=name sex age) - - @param [in] libs= (SASHELP) - Space separated list of libraries to search for columns - @param [in] cols= - Space separated list of column names to search for (not case sensitive) - @param [out] outds= (mp_searchcols) - The table to create with the results. Will have one line per table match. - @param [in] match= (ANY) The match type. Valid values: - @li ANY - The table contains at least one of the columns - @li WILD - The table contains a column with a name that partially matches - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mp_searchcols(libs=sashelp - ,cols= - ,outds=mp_searchcols - ,match=ANY -)/*/STORE SOURCE*/; - -%put &sysmacroname process began at %sysfunc(datetime(),datetime19.); - -/* get the list of tables in the library */ -proc sql; -create table _data_ as - select distinct upcase(libname) as libname - , upcase(memname) as memname - , upcase(name) as name - from dictionary.columns -%if %sysevalf(%superq(libs)=,boolean)=0 %then %do; - where upcase(libname) in ("IMPOSSIBLE", - %local x; - %do x=1 %to %sysfunc(countw(&libs)); - "%upcase(%scan(&libs,&x))" - %end; - ) -%end; - order by 1,2,3; - -%local tempds; -%let tempds=&syslast; -data &outds; - set &tempds; - length cols matchcols $32767; - cols=upcase(symget('cols')); - colcount=countw(cols); - by libname memname name; - if _n_=1 then do; - putlog "Searching libs: &libs"; - putlog "Searching cols: " cols; - end; - if first.memname then do; - sumcols=0; - retain matchcols; - matchcols=''; - end; -%if &match=ANY %then %do; - if findw(cols,name,,'spit') then do; - sumcols+1; - matchcols=cats(matchcols)!!' '!!cats(name); - end; -%end; -%else %if &match=WILD %then %do; - if _n_=1 then do; - retain wcount; - wcount=countw(cols); - drop wcount; - end; - do i=1 to wcount; - length curword $32; - curword=scan(cols,i,' '); - drop curword; - if index(name,cats(curword)) then do; - sumcols+1; - matchcols=cats(matchcols)!!' '!!cats(curword); - end; - end; -%end; - - if last.memname then do; - if sumcols>0 then output; - if sumcols=colcount then putlog "Full Match: " libname memname; - end; - keep libname memname sumcols matchcols; -run; - -proc sort; by descending sumcols memname libname; run; - -proc sql; -drop table &tempds; -%put &sysmacroname process finished at %sysfunc(datetime(),datetime19.); - -%mend mp_searchcols; diff --git a/009_macros/mp_searchdata.sas b/009_macros/mp_searchdata.sas deleted file mode 100644 index c795de2..0000000 --- a/009_macros/mp_searchdata.sas +++ /dev/null @@ -1,131 +0,0 @@ -/*** HELP START ***//** - @file - @brief Searches all data in a library - @details - Scans an entire library and creates a copy of any table - containing a specific string OR numeric value. Only - matching records are written out. - If both a string and numval are provided, the string - will take precedence. - - Usage: - - %mp_searchdata(lib=sashelp, string=Jan) - %mp_searchdata(lib=sashelp, ds=bird, numval=1) - %mp_searchdata(lib=sashelp, ds=class, string=l,outobs=5) - - - Outputs zero or more tables to an MPSEARCH library with specific records. - - @param [in] lib= The libref to search (should be already assigned) - @param [in] ds= The dataset to search (leave blank to search entire library) - @param [in] string= String value to search (case sensitive, can be partial) - @param [in] numval= Numeric value to search (must be exact) - @param [out] outloc= (0) Optionally specify the directory in which to - create the the output datasets with matching rows. By default it will - write them to a temporary subdirectory within the WORK folder. - @param [out] outlib= (MPSEARCH) Assign a different libref to the output - library containing the matching datasets / records - @param [in] outobs= set to a positive integer to restrict the number of - observations - @param [in] filter_text= (1=1) Add a (valid) filter clause to further filter - the results. - -

SAS Macros

- @li mf_getuniquename.sas - @li mf_getvarlist.sas - @li mf_getvartype.sas - @li mf_mkdir.sas - @li mf_nobs.sas - - @version 9.2 - @author Allan Bowe -**//*** HELP END ***/ - -%macro mp_searchdata(lib= - ,ds= - ,string= /* the query will use a contains (?) operator */ - ,numval= /* numeric must match exactly */ - ,outloc=0 - ,outlib=MPSEARCH - ,outobs=-1 - ,filter_text=%str(1=1) -)/*/STORE SOURCE*/; - -%local table_list table table_num table colnum col start_tm check_tm vars type - coltype; -%put process began at %sysfunc(datetime(),datetime19.); - -%if &syscc ge 4 %then %do; - %put %str(WAR)NING: SYSCC=&syscc on macro entry; - %return; -%end; - -%if &string = %then %let type=N; -%else %let type=C; - -%if "&outloc"="0" %then %do; - %let outloc=%sysfunc(pathname(work))/%mf_getuniquename(); -%end; - -%mf_mkdir(&outloc) -libname &outlib "&outloc"; - -/* get the list of tables in the library */ -proc sql noprint; -select distinct memname into: table_list separated by ' ' - from dictionary.tables - where upcase(libname)="%upcase(&lib)" -%if &ds ne %then %do; - and upcase(memname)=%upcase("&ds") -%end; - ; -/* check that we have something to check */ -%if %length(&table_list)=0 %then %put library &lib contains no tables!; -/* loop through each table */ -%else %do table_num=1 %to %sysfunc(countw(&table_list,%str( ))); - %let table=%scan(&table_list,&table_num,%str( )); - %let vars=%mf_getvarlist(&lib..&table); - %if %length(&vars)=0 %then %do; - %put NO COLUMNS IN &lib..&table! This will be skipped.; - %end; - %else %do; - %let check_tm=%sysfunc(datetime()); - /* prep input */ - data &outlib..&table; - set &lib..&table; - where %unquote(&filter_text) and ( 0 - /* loop through columns */ - %do colnum=1 %to %sysfunc(countw(&vars,%str( ))); - %let col=%scan(&vars,&colnum,%str( )); - %let coltype=%mf_getvartype(&lib..&table,&col); - %if &type=C and &coltype=C %then %do; - /* if a char column, see if it contains the string */ - or ("&col"n ? "&string") - %end; - %else %if &type=N and &coltype=N %then %do; - /* if numeric match exactly */ - or ("&col"n = &numval) - %end; - %end; - ); - %if &outobs>-1 %then %do; - if _n_ > &outobs then stop; - %end; - run; - %put Search query for &table took - %sysevalf(%sysfunc(datetime())-&check_tm) seconds; - %if &syscc ne 0 %then %do; - %put %str(ERR)ROR: SYSCC=&syscc when processing &lib..&table; - %return; - %end; - %if %mf_nobs(&outlib..&table)=0 %then %do; - proc sql; - drop table &outlib..&table; - %end; - %end; -%end; - -%put process finished at %sysfunc(datetime(),datetime19.); - -%mend mp_searchdata; diff --git a/009_macros/mp_setkeyvalue.sas b/009_macros/mp_setkeyvalue.sas deleted file mode 100644 index 197c6ec..0000000 --- a/009_macros/mp_setkeyvalue.sas +++ /dev/null @@ -1,55 +0,0 @@ -/*** HELP START ***//** - @file - @brief Logs a key value pair a control dataset - @details If the dataset does not exist, it is created. Usage: - - %mp_setkeyvalue(someindex,22,type=N) - %mp_setkeyvalue(somenewindex,somevalue) - -

SAS Macros

- @li mf_existds.sas - -

Related Macros

- @li mf_getvalue.sas - - @param [in] key Provide a key on which to perform the lookup - @param [in] value Provide a value - @param [in] type= either C or N will populate valc and valn respectively. - C is default. - @param [out] libds= define the target table to hold the parameters - - @version 9.2 - @author Allan Bowe - @source https://github.com/sasjs/core - -**//*** HELP END ***/ - -%macro mp_setkeyvalue(key,value,type=C,libds=work.mp_setkeyvalue -)/*/STORE SOURCE*/; - - %if not (%mf_existds(&libds)) %then %do; - data &libds (index=(key/unique)); - length key $64 valc $2048 valn 8 type $1; - call missing(of _all_); - stop; - run; - %end; - - proc sql; - delete from &libds - where key=symget('key'); - insert into &libds - set key=symget('key') - %if &type=C %then %do; - ,valc=symget('value') - ,type='C' - %end; - %else %do; - ,valn=symgetn('value') - ,type='N' - %end; - ; - - quit; - -%mend mp_setkeyvalue; diff --git a/009_macros/mp_sortinplace.sas b/009_macros/mp_sortinplace.sas deleted file mode 100644 index 622be12..0000000 --- a/009_macros/mp_sortinplace.sas +++ /dev/null @@ -1,122 +0,0 @@ -/*** HELP START ***//** - @file - @brief Sorts a SAS dataset in place, preserving constraints - @details Generally if a dataset contains indexes, then it is not necessary to - sort it before performing operations such as merges / joins etc. - That said, there are a few edge cases where it can be desirable: - - @li To allow adjacent records to be viewed directly in the dataset - @li To apply compression, or to remove deleted records - @li To improve performance for specific queries - - This macro will only work for BASE (V9) engine libraries. It works by - creating a copy of the dataset (without data, WITH constraints) in the same - library, appending a sorted view into it, and finally - renaming it. - - Example usage: - - proc sql; - create table work.example as - select * from sashelp.class; - alter table work.example - add constraint pk primary key(name); - %mp_sortinplace(work.example) - - @param [in] libds The libref.datasetname that needs to be sorted - -

SAS Macros

- @li mf_existds.sas - @li mf_getengine.sas - @li mf_getquotedstr.sas - @li mf_getuniquename.sas - @li mf_getvarlist.sas - @li mf_nobs.sas - @li mp_abort.sas - @li mp_getpk.sas - -

Related Macros

- @li mp_sortinplace.test.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_sortinplace(libds -)/*/STORE SOURCE*/; - -%local lib ds tempds1 tempds2 tempvw sortkey; - -/* perform validations */ -%mp_abort(iftrue=(%sysfunc(countc(&libds,.)) ne 1) - ,mac=mp_sortinplace - ,msg=%str(LIBDS (&libds) should have LIBREF.DATASET format) -) -%mp_abort(iftrue=(%mf_existds(&libds)=0) - ,mac=mp_sortinplace - ,msg=%str(&libds does not exist) -) - -%let lib=%scan(&libds,1,.); -%let ds=%scan(&libds,2,.); -%mp_abort(iftrue=(%mf_getengine(&lib) ne V9) - ,mac=mp_sortinplace - ,msg=%str(&lib is not a BASE engine library) -) - -/* grab a copy of the constraints so we know what to sort by */ -%let tempds1=%mf_getuniquename(prefix=&sysmacroname); -%mp_getpk(lib=&lib,ds=&ds,outds=work.&tempds1) - -%if %mf_nobs(work.&tempds1)=0 %then %do; - %put &sysmacroname: No PK found in &lib..&ds; - %put Sorting will not take place; - %return; -%end; - -/* fallback sortkey is all fields */ -%let sortkey=%mf_getvarlist(&libds); - -/* overlay actual sort key if it exists */ -data _null_; - set work.&tempds1; - call symputx('sortkey',coalescec(pk_fields,symget('sortkey'))); -run; - - -/* create empty copy, with ALL constraints, in the same library */ -%let tempds2=%mf_getuniquename(prefix=&sysmacroname); -proc append base=&lib..&tempds2 data=&libds(obs=0); -run; - -/* create sorted view */ -%let tempvw=%mf_getuniquename(prefix=&sysmacroname); -proc sql; -create view work.&tempvw as select * from &lib..&ds -order by %mf_getquotedstr(&sortkey,quote=N); - -/* append sorted data */ -proc append base=&lib..&tempds2 data=work.&tempvw; -run; - -/* do validations */ -%mp_abort(iftrue=(&syscc ne 0) - ,mac=mp_sortinplace - ,msg=%str(syscc=&syscc prior to replace operation) -) -%mp_abort(iftrue=(%mf_nobs(&lib..&tempds2) ne %mf_nobs(&lib..&ds)) - ,mac=mp_sortinplace - ,msg=%str(new dataset has a different number of logical obs to the old) -) - -/* drop old dataset */ -proc sql; -drop table &lib..&ds; - -/* rename the new dataset */ -proc datasets library=&lib; - change &tempds2=&ds; -run; - - -%mend mp_sortinplace; diff --git a/009_macros/mp_stprequests.sas b/009_macros/mp_stprequests.sas deleted file mode 100644 index 9957779..0000000 --- a/009_macros/mp_stprequests.sas +++ /dev/null @@ -1,75 +0,0 @@ -/*** HELP START ***//** - @file - @brief Capture session start / finish times and request details - @details For details, see - https://rawsas.com/event-logging-of-stored-process-server-sessions. - Requires a base table in the following structure (name can be changed): - - proc sql; - create table &libds( - request_dttm num not null format=datetime. - ,status_cd char(4) not null - ,_metaperson varchar(100) not null - ,_program varchar(500) - ,sysuserid varchar(50) - ,sysjobid varchar(12) - ,_sessionid varchar(50) - ); - - Called via STP init / term events (configurable in SMC) as follows: - - %mp_stprequests(status_cd=INIT, libds=YOURLIB.DATASET ) - - - @param [in] status_cd= Use INIT for INIT and TERM for TERM events - @param [in] libds= (somelib.stp_requests) Location of base table - (library.dataset). To minimise risk of table locks, we HIGHLY recommend - using a database (NOT a SAS dataset). - THE LIBRARY SHOULD BE ASSIGNED ALREADY - eg in autoexec or earlier in the - init program proper. - - @version 9.2 - @author Allan Bowe - @source https://github.com/sasjs/core - -**//*** HELP END ***/ - -%macro mp_stprequests(status_cd= /* $4 eg INIT or TERM */ - ,libds=somelib.stp_requests /* base table location */ -)/*/STORE SOURCE*/; - - /* set nosyntaxcheck so the code runs regardless */ - %local etls_syntaxcheck; - %let etls_syntaxcheck=%sysfunc(getoption(syntaxcheck)); - options nosyntaxcheck; - - data ; - if 0 then set &libds; - request_dttm=datetime(); - status_cd="&status_cd"; - _METAPERSON="&_metaperson"; - _PROGRAM="&_program"; - SYSUSERID="&sysuserid"; - SYSJOBID="&sysjobid"; - %if not %symexist(_SESSIONID) %then %do; - /* session id is stored in the replay variable but needs to be extracted */ - _replay=symget('_replay'); - _replay=subpad(_replay,index(_replay,'_sessionid=')+11,length(_replay)); - index=index(_replay,'&')-1; - if index=-1 then index=length(_replay); - _replay=substr(_replay,1,index); - _SESSIONID=_replay; - drop _replay index; - %end; - %else %do; - /* explicitly created sessions are automatically available */ - _SESSIONID=symget('_SESSIONID'); - %end; - output; - stop; - run; - - proc append base=&libds data=&syslast nowarn;run; - - options &etls_syntaxcheck; -%mend mp_stprequests; diff --git a/009_macros/mp_streamfile.sas b/009_macros/mp_streamfile.sas deleted file mode 100644 index f2ff172..0000000 --- a/009_macros/mp_streamfile.sas +++ /dev/null @@ -1,212 +0,0 @@ -/*** HELP START ***//** - @file - @brief Streams a file to _webout according to content type - @details Will set headers using appropriate functions per the server type - (Viya, EBI, [SASjs Server](https://github.com/sasjs/server)) and stream - content using mp_binarycopy(). - - Usage: - - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - %mp_streamfile(contenttype=csv,inloc=/some/where.txt,outname=myfile.txt) - - @param [in] contenttype= (TEXT) Supported: - @li CSV - @li EXCEL - @li MARKDOWN - @li TEXT - @li ZIP - Feel free to submit PRs to support more mime types! The official list is - here: https://www.iana.org/assignments/media-types/media-types.xhtml - @param [in] inloc= /path/to/file.ext to be sent - @param [in] inref= fileref of file to be sent (if provided, overrides `inloc`) - @param [in] iftrue= (1=1) Provide a condition under which to execute. - @param [out] outname= the name of the file, as downloaded by the browser - @param [out] outref= (_webout) The destination where the file should be - streamed. - -

SAS Macros

- @li mf_getplatform.sas - @li mfs_httpheader.sas - @li mp_binarycopy.sas - - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_streamfile( - contenttype=TEXT - ,inloc= - ,inref=0 - ,iftrue=%str(1=1) - ,outname= - ,outref=_webout -)/*/STORE SOURCE*/; - -%if not(%eval(%unquote(&iftrue))) %then %return; - -%let contentype=%upcase(&contenttype); -%let outref=%upcase(&outref); -%local platform; %let platform=%mf_getplatform(); - -/** - * check engine type to avoid the below err message: - * > Function is only valid for filerefs using the CACHE access method. - */ -%local streamweb; -%let streamweb=0; -data _null_; - set sashelp.vextfl(where=(upcase(fileref)="&outref")); - if xengine='STREAM' then call symputx('streamweb',1,'l'); -run; - -%if &contentype=CSV %then %do; - %if (&platform=SASMETA and &streamweb=1) %then %do; - data _null_; - rc=stpsrv_header('Content-Type','application/csv'); - rc=stpsrv_header('Content-disposition',"attachment; filename=&outname"); - run; - %end; - %else %if &platform=SASVIYA %then %do; - filename &outref filesrvc parenturi="&SYS_JES_JOB_URI" name='_webout.txt' - contenttype='application/csv' - contentdisp="attachment; filename=&outname"; - %end; - %else %if &platform=SASJS %then %do; - %mfs_httpheader(Content-Type,application/csv) - %mfs_httpheader(Content-disposition,%str(attachment; filename=&outname)) - %end; -%end; -%else %if &contentype=EXCEL %then %do; - /* suitable for XLS format */ - %if (&platform=SASMETA and &streamweb=1) %then %do; - data _null_; - rc=stpsrv_header('Content-Type','application/vnd.ms-excel'); - rc=stpsrv_header('Content-disposition',"attachment; filename=&outname"); - run; - %end; - %else %if &platform=SASVIYA %then %do; - filename &outref filesrvc parenturi="&SYS_JES_JOB_URI" name='_webout.xls' - contenttype='application/vnd.ms-excel' - contentdisp="attachment; filename=&outname"; - %end; - %else %if &platform=SASJS %then %do; - %mfs_httpheader(Content-Type,application/vnd.ms-excel) - %mfs_httpheader(Content-disposition,%str(attachment; filename=&outname)) - %end; -%end; -%else %if &contentype=GIF or &contentype=JPEG or &contentype=PNG %then %do; - %if (&platform=SASMETA and &streamweb=1) %then %do; - data _null_; - rc=stpsrv_header('Content-Type',"image/%lowcase(&contenttype)"); - run; - %end; - %else %if &platform=SASVIYA %then %do; - filename &outref filesrvc parenturi="&SYS_JES_JOB_URI" - contenttype="image/%lowcase(&contenttype)"; - %end; - %else %if &platform=SASJS %then %do; - %mfs_httpheader(Content-Type,image/%lowcase(&contenttype)) - %end; -%end; -%else %if &contentype=HTML or &contenttype=MARKDOWN %then %do; - %if (&platform=SASMETA and &streamweb=1) %then %do; - data _null_; - rc=stpsrv_header('Content-Type',"text/%lowcase(&contenttype)"); - rc=stpsrv_header('Content-disposition',"attachment; filename=&outname"); - run; - %end; - %else %if &platform=SASVIYA %then %do; - filename &outref filesrvc parenturi="&SYS_JES_JOB_URI" name="_webout.json" - contenttype="text/%lowcase(&contenttype)" - contentdisp="attachment; filename=&outname"; - %end; - %else %if &platform=SASJS %then %do; - %mfs_httpheader(Content-Type,text/%lowcase(&contenttype)) - %mfs_httpheader(Content-disposition,%str(attachment; filename=&outname)) - %end; -%end; -%else %if &contentype=TEXT %then %do; - %if (&platform=SASMETA and &streamweb=1) %then %do; - data _null_; - rc=stpsrv_header('Content-Type','application/text'); - rc=stpsrv_header('Content-disposition',"attachment; filename=&outname"); - run; - %end; - %else %if &platform=SASVIYA %then %do; - filename &outref filesrvc parenturi="&SYS_JES_JOB_URI" name='_webout.txt' - contenttype='application/text' - contentdisp="attachment; filename=&outname"; - %end; - %else %if &platform=SASJS %then %do; - %mfs_httpheader(Content-Type,application/text) - %mfs_httpheader(Content-disposition,%str(attachment; filename=&outname)) - %end; -%end; -%else %if &contentype=WOFF or &contentype=WOFF2 or &contentype=TTF %then %do; - %if (&platform=SASMETA and &streamweb=1) %then %do; - data _null_; - rc=stpsrv_header('Content-Type',"font/%lowcase(&contenttype)"); - run; - %end; - %else %if &platform=SASVIYA %then %do; - filename &outref filesrvc parenturi="&SYS_JES_JOB_URI" - contenttype="font/%lowcase(&contenttype)"; - %end; - %else %if &platform=SASJS %then %do; - %mfs_httpheader(Content-Type,font/%lowcase(&contenttype)) - %end; -%end; -%else %if &contentype=XLSX %then %do; - %if (&platform=SASMETA and &streamweb=1) %then %do; - data _null_; - rc=stpsrv_header('Content-Type', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); - rc=stpsrv_header('Content-disposition',"attachment; filename=&outname"); - run; - %end; - %else %if &platform=SASVIYA %then %do; - filename &outref filesrvc parenturi="&SYS_JES_JOB_URI" name='_webout.xls' - contenttype= - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - contentdisp="attachment; filename=&outname"; - %end; - %else %if &platform=SASJS %then %do; - %mfs_httpheader(Content-Type - ,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet - ) - %mfs_httpheader(Content-disposition,%str(attachment; filename=&outname)) - %end; -%end; -%else %if &contentype=ZIP %then %do; - %if (&platform=SASMETA and &streamweb=1) %then %do; - data _null_; - rc=stpsrv_header('Content-Type','application/zip'); - rc=stpsrv_header('Content-disposition',"attachment; filename=&outname"); - run; - %end; - %else %if &platform=SASVIYA %then %do; - filename &outref filesrvc parenturi="&SYS_JES_JOB_URI" name='_webout.zip' - contenttype='application/zip' - contentdisp="attachment; filename=&outname"; - %end; - %else %if &platform=SASJS %then %do; - %mfs_httpheader(Content-Type,application/zip) - %mfs_httpheader(Content-disposition,%str(attachment; filename=&outname)) - %end; -%end; -%else %do; - %put %str(ERR)OR: Content Type &contenttype NOT SUPPORTED by &sysmacroname!; -%end; - -%if &inref ne 0 %then %do; - %mp_binarycopy(inref=&inref,outref=&outref) -%end; -%else %do; - %mp_binarycopy(inloc="&inloc",outref=&outref) -%end; - -%mend mp_streamfile; diff --git a/009_macros/mp_testjob.sas b/009_macros/mp_testjob.sas deleted file mode 100644 index f5610b0..0000000 --- a/009_macros/mp_testjob.sas +++ /dev/null @@ -1,92 +0,0 @@ -/*** HELP START ***//** - @file - @brief Runs arbitrary code for a specified amount of time - @details Executes a series of procs and data steps to enable performance - testing of arbitrary jobs. - - %mp_testjob( - duration=60*5 - ) - - @param [in] duration= (30) The time in seconds which the job should run for. - Actual time may vary, as the check is done in between steps. - -

SAS Macros

- @li mf_getuniquelibref.sas - @li mf_getuniquename.sas - @li mf_mkdir.sas - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_testjob(duration=30 -)/*/STORE SOURCE*/; -%local lib dir ds1 ds2 ds3 start_tm i; - -%let start_tm=%sysfunc(datetime()); -%let duration=%sysevalf(&duration); - -/* create a temporary library in WORK */ -%let lib=%mf_getuniquelibref(); -%let dir=%mf_getuniquename(); -%mf_mkdir(%sysfunc(pathname(work))/&dir) -libname &lib "%sysfunc(pathname(work))/&dir"; - -/* loop through until time expires */ -%let ds1=%mf_getuniquename(); -%let ds2=%mf_getuniquename(); -%let ds3=%mf_getuniquename(); -%do i=0 %to 1; - - /* create big dataset */ - data &lib..&ds1(compress=no ); - do x=1 to 1000000; - randnum0=ranuni(0)*3; - randnum1=ranuni(0)*2; - bigchar=repeat('A',300); - output; - end; - run; - %if %sysevalf( (%sysfunc(datetime())-&start_tm)>&duration ) %then %goto gate; - - proc summary ; - class randnum0 randnum1; - output out=&lib..&ds2; - run;quit; - %if %sysevalf( (%sysfunc(datetime())-&start_tm)>&duration ) %then %goto gate; - - /* add more data */ - proc sql; - create table &lib..&ds3 as - select *, ranuni(0)*10 as randnum2 - from &lib..&ds1 - order by randnum1; - quit; - %if %sysevalf( (%sysfunc(datetime())-&start_tm)>&duration ) %then %goto gate; - - proc sort data=&lib..&ds3; - by descending x; - run; - %if %sysevalf( (%sysfunc(datetime())-&start_tm)>&duration ) %then %goto gate; - - /* wait 5 seconds */ - data _null_; - call sleep(5,1); - run; - %if %sysevalf( (%sysfunc(datetime())-&start_tm)>&duration ) %then %goto gate; - - %let i=0; - -%end; - -%gate: -%put time is up!; -proc datasets lib=&lib kill; -run; -quit; -libname &lib clear; - - -%mend mp_testjob; diff --git a/009_macros/mp_testservice.sas b/009_macros/mp_testservice.sas deleted file mode 100644 index 81a157a..0000000 --- a/009_macros/mp_testservice.sas +++ /dev/null @@ -1,39 +0,0 @@ -/*** HELP START ***//** - @file - @brief To be deprecated. Will execute a SASjs web service on SAS 9 or Viya - @details Use the mx_testservice.sas macro instead (documentation can be - found there) - -

SAS Macros

- @li mx_testservice.sas - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_testservice(program, - inputfiles=0, - inputdatasets=0, - inputparams=0, - debug=log, - mdebug=0, - outlib=0, - outref=0, - viyaresult=WEBOUT_JSON, - viyacontext=SAS Job Execution compute context -)/*/STORE SOURCE*/; - -%mx_testservice(&program, - inputfiles=&inputfiles, - inputdatasets=&inputdatasets, - inputparams=&inputparams, - debug=&debug, - mdebug=&mdebug, - outlib=&outlib, - outref=&outref, - viyaresult=&viyaresult, - viyacontext=&viyacontext -) - -%mend mp_testservice; diff --git a/009_macros/mp_testwritespeedlibrary.sas b/009_macros/mp_testwritespeedlibrary.sas deleted file mode 100644 index 468573f..0000000 --- a/009_macros/mp_testwritespeedlibrary.sas +++ /dev/null @@ -1,59 +0,0 @@ -/*** HELP START ***//** - @file mp_testwritespeedlibrary.sas - @brief Tests the write speed of a new table in a SAS library - @details Will create a new table of a certain size in an - existing SAS library. The table will have one column, - and will be subsequently deleted. - - %mp_testwritespeedlibrary( - lib=work - ,size=0.5 - ,outds=work.results - ) - - @param [in] lib= (WORK) The library in which to create the table - @param [in] size= (0.1) The size in GB of the table to create - @param [out] outds= (WORK.RESULTS) The output dataset to be created. - -

SAS Macros

- @li mf_getuniquename.sas - @li mf_existds.sas - - @version 9.4 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_testwritespeedlibrary(lib=WORK - ,outds=work.results - ,size=0.1 -)/*/STORE SOURCE*/; -%local ds start; - -/* find an unused, unique name for the new table */ -%let ds=%mf_getuniquename(); -%do %until(%mf_existds(&lib..&ds)=0); - %let ds=%mf_getuniquename(); -%end; - -%let start=%sysfunc(datetime()); - -data &lib..&ds(compress=no keep=x); - header=128*1024; - size=(1073741824/8 * &size) - header; - do x=1 to size; - output; - end; -run; - -proc sql; -drop table &lib..&ds; - -data &outds; - lib="&lib"; - start_dttm=put(&start,datetime19.); - end_dttm=put(datetime(),datetime19.); - duration_seconds=end_dttm-start_dttm; -run; - -%mend mp_testwritespeedlibrary; diff --git a/009_macros/mp_tree.sas b/009_macros/mp_tree.sas deleted file mode 100644 index 467e8ff..0000000 --- a/009_macros/mp_tree.sas +++ /dev/null @@ -1,71 +0,0 @@ -/*** HELP START ***//** - @file - @brief Recursively scans a directory tree to get all subfolders and content - @details - Usage: - - %mp_tree(dir=/tmp, outds=work.tree) - - Credits: - - Roger Deangelis: -https://communities.sas.com/t5/SAS-Programming/listing-all-files-within-a-directory-and-subdirectories/m-p/332616/highlight/true#M74887 - - Tom: -https://communities.sas.com/t5/SAS-Programming/listing-all-files-of-all-types-from-all-subdirectories/m-p/334113/highlight/true#M75419 - - - @param [in] dir= (/tmp) Directory to be scanned - @param [out] outds= (work.mp_tree) Dataset to create - - @returns outds contains the following variables: - - - `dir`: a flag (1/0) to say whether it is a directory or not. This is not - reliable - folders that you do not have permission to open will be flagged - as directories. - - `ext`: file extension - - `filename`: file name - - `dirname`: directory name - - `fullpath`: directory + file name - - @version 9.2 -**//*** HELP END ***/ - -%macro mp_tree(dir=/tmp - ,outds=work.mp_tree -)/*/STORE SOURCE*/; - -data &outds ; - length dir 8 ext filename dirname $256 fullpath $512 ; - call missing(of _all_); - fullpath = "&dir"; -run; - -%local sep; -%if &sysscp=WIN or &SYSSCP eq DNTHOST %then %let sep=\; -%else %let sep=/; - -data &outds ; - modify &outds ; - retain sep "&sep"; - rc=filename('tmp',fullpath); - dir_id=dopen('tmp'); - dir = (dir_id ne 0) ; - if dir then dirname=fullpath; - else do; - filename=scan(fullpath,-1,sep) ; - dirname =substrn(fullpath,1,length(fullpath)-length(filename)); - if index(filename,'.')>1 then ext=scan(filename,-1,'.'); - end; - replace; - if dir then do; - do i=1 to dnum(dir_id); - fullpath=cats(dirname,sep,dread(dir_id,i)); - output; - end; - rc=dclose(dir_id); - end; - rc=filename('tmp'); -run; - -%mend mp_tree; diff --git a/009_macros/mp_unzip.sas b/009_macros/mp_unzip.sas deleted file mode 100644 index 0e81659..0000000 --- a/009_macros/mp_unzip.sas +++ /dev/null @@ -1,91 +0,0 @@ -/*** HELP START ***//** - @file mp_unzip.sas - @brief Unzips a zip file - @details Opens the zip file and copies all the contents to another directory. - It is not possible to retain permissions / timestamps, also the BOF marker - is lost so it cannot extract binary files. - - Usage: - - filename mc url - "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; - %inc mc; - - %mp_unzip(ziploc="/some/file.zip",outdir=/some/folder) - - More info: - https://blogs.sas.com/content/sasdummy/2015/05/11/using-filename-zip-to-unzip-and-read-data-files-in-sas/ - - @param [in] ziploc= Fileref or quoted full path, eg: "/path/to/file.zip" - @param [out] outdir= (%sysfunc(pathname(work))) Directory in which to write - the outputs (created if needed) - -

SAS Macros

- @li mf_mkdir.sas - @li mf_getuniquefileref.sas - @li mp_binarycopy.sas - - @version 9.4 - @author Allan Bowe - @source https://github.com/sasjs/core - -**//*** HELP END ***/ - -%macro mp_unzip( - ziploc= - ,outdir=%sysfunc(pathname(work)) -)/*/STORE SOURCE*/; - -%local f1 f2 ; -%let f1=%mf_getuniquefileref(); -%let f2=%mf_getuniquefileref(); - -/* Macro variable &datazip would be read from the file */ -filename &f1 ZIP &ziploc; - -/* create target folder */ -%mf_mkdir(&outdir) - -/* Read the "members" (files) from the ZIP file */ -data _data_(keep=memname isFolder); - length memname $200 isFolder 8; - fid=dopen("&f1"); - if fid=0 then stop; - memcount=dnum(fid); - do i=1 to memcount; - memname=dread(fid,i); - /* check for trailing / in folder name */ - isFolder = (first(reverse(trim(memname)))='/'); - output; - end; - rc=dclose(fid); -run; - -filename &f2 temp; - -/* loop through each entry and either create the subfolder or extract member */ -data _null_; - set &syslast; - file &f2; - if isFolder then call execute('%mf_mkdir(&outdir/'!!memname!!')'); - else do; - qname=quote(cats("&outdir/",memname)); - bname=cats('(',memname,')'); - put '/* hat tip: "data _null_" on SAS-L */'; - put 'data _null_;'; - put ' infile &f1 ' bname ' lrecl=256 recfm=F length=length eof=eof unbuf;'; - put ' file ' qname ' lrecl=256 recfm=N;'; - put ' input;'; - put ' put _infile_ $varying256. length;'; - put ' return;'; - put 'eof:'; - put ' stop;'; - put 'run;'; - end; -run; - -%inc &f2/source2; - -filename &f2 clear; - -%mend mp_unzip; diff --git a/009_macros/mp_updatevarlength.sas b/009_macros/mp_updatevarlength.sas deleted file mode 100644 index 22d2e0c..0000000 --- a/009_macros/mp_updatevarlength.sas +++ /dev/null @@ -1,93 +0,0 @@ -/*** HELP START ***//** - @file mp_updatevarlength.sas - @brief Change the length of a variable - @details The library is assumed to be assigned. Simple character updates - currently supported, numerics are more complicated and will follow. - - data example; - a='1'; - b='12'; - c='123'; - run; - %mp_updatevarlength(example,a,3) - %mp_updatevarlength(example,c,1) - proc sql; - describe table example; - - @param [in] libds the library.dataset to be modified - @param [in] var The variable to modify - @param [in] len The new length to apply - -

SAS Macros

- @li mf_existds.sas - @li mp_abort.sas - @li mf_existvar.sas - @li mf_getvarlen.sas - @li mf_getvartype.sas - @li mf_getnobs.sas - @li mp_createconstraints.sas - @li mp_getconstraints.sas - @li mp_deleteconstraints.sas - - @version 9.2 - @author Allan Bowe - -**//*** HELP END ***/ - -%macro mp_updatevarlength(libds,var,len -)/*/STORE SOURCE*/; - -%if %index(&libds,.)=0 %then %let libds=WORK.&libds; - -%mp_abort(iftrue=(%mf_existds(&libds)=0) - ,mac=&sysmacroname - ,msg=%str(Table &libds not found!) -) - -%mp_abort(iftrue=(%mf_existvar(&libds,&var)=0) - ,mac=&sysmacroname - ,msg=%str(Variable &var not found on &libds!) -) - -/* not possible to in-place modify a numeric length, to add later */ -%mp_abort(iftrue=(%mf_getvartype(&libds,&var)=0) - ,mac=&sysmacroname - ,msg=%str(Only character resizings are currently supported) -) - -%local oldlen; -%let oldlen=%mf_getvarlen(&libds,&var); -%if &oldlen=&len %then %do; - %put &sysmacroname: Old and new lengths (&len) match!; - %return; -%end; - -%let libds=%upcase(&libds); - - -data;run; -%local dsconst; %let dsconst=&syslast; -%mp_getconstraints(lib=%scan(&libds,1,.),ds=%scan(&libds,2,.),outds=&dsconst) - -%mp_abort(iftrue=(&syscc ne 0) - ,mac=&sysmacroname - ,msg=%str(syscc=&syscc) -) - -%if %mf_getnobs(&dscont)=0 %then %do; - /* must use SQL as proc datasets does not support length changes */ - proc sql; - alter table &libds modify &var char(&len); - %return; -%end; - -/* we have constraints! */ - -%mp_deleteconstraints(inds=&dsconst,outds=&dsconst._dropd,execute=YES) - -proc sql; -alter table &libds modify &var char(&len); - -%mp_createconstraints(inds=&dsconst,outds=&dsconst._addd,execute=YES) - -%mend mp_updatevarlength; diff --git a/009_macros/mp_wait4file.sas b/009_macros/mp_wait4file.sas deleted file mode 100644 index f52dfe9..0000000 --- a/009_macros/mp_wait4file.sas +++ /dev/null @@ -1,35 +0,0 @@ -/*** HELP START ***//** - @file - @brief Wait until a file arrives before continuing execution - @details Loops with a `sleep()` command until a file arrives or the max wait - period expires. - - Example: Wait 3 minutes OR for /tmp/flag.txt to appear - - %mp_wait4file(/tmp/flag.txt , maxwait=60*3) - - @param [in] file The file to wait for. Must be provided. - @param [in] maxwait= (0) Number of seconds to wait. If set to zero, will - loop indefinitely (to a maximum of 46 days, per SAS [documentation]( - https://support.sas.com/documentation/cdl/en/lrdict/64316/HTML/default/viewer.htm#a001418809.htm - )). Otherwise, execution will proceed upon sleep expiry. - @param [in] interval= (1) The wait period between sleeps, in seconds - - -**//*** HELP END ***/ - -%macro mp_wait4file(file, maxwait=0, interval=1); - -%if %str(&file)=%str() %then %do; - %put %str(ERR)OR: file not provided; -%end; - -data _null_; - maxwait=&maxwait; - if maxwait=0 then maxwait=60*60*24*46; - do until (fileexist("&file") or slept>maxwait ); - slept=sum(slept,sleep(&interval,1)); - end; -run; - -%mend mp_wait4file; diff --git a/009_macros/mp_webin.sas b/009_macros/mp_webin.sas deleted file mode 100644 index 671d103..0000000 --- a/009_macros/mp_webin.sas +++ /dev/null @@ -1,59 +0,0 @@ -/*** HELP START ***//** - @file - @brief Fix the `_WEBIN` variables provided to SAS web services - @details When uploading files to SAS Stored Processes or Viya Jobs a number - of global macro variables are automatically created - however there are some - differences in behaviour both between SAS 9 and Viya, and also between a - single file upload and a multi-file upload. - - This macro "straightens" up the global macro variables to make it easier / - simpler to write code that works in both environments and with a variable - number of file inputs. - - After running this macro, the following global variables will *always* exist: - @li `_WEBIN_FILE_COUNT` - @li `_WEBIN_FILENAME1` - @li `_WEBIN_FILEREF1` - @li `_WEBIN_NAME1` - - Usage: - - %mp_webin() - - This was created as a macro procedure (over a macro function) as it will also - use the filename statement in Viya environments (where `_webin_fileuri` is - provided). - -

SAS Macros

- @li mf_getplatform.sas - @li mf_getuniquefileref.sas - -**//*** HELP END ***/ - -%macro mp_webin(); - -/* prepare global variables */ -%global _webin_file_count - _webin_filename _webin_filename1 - _webin_fileref _webin_fileref1 - _webin_fileuri _webin_fileuri1 - _webin_name _webin_name1 - ; - -/* create initial versions */ -%let _webin_file_count=%eval(&_webin_file_count+0); -%let _webin_filename1=%sysfunc(coalescec(&_webin_filename1,&_webin_filename)); -%let _webin_fileref1=%sysfunc(coalescec(&_webin_fileref1,&_webin_fileref)); -%let _webin_fileuri1=%sysfunc(coalescec(&_webin_fileuri1,&_webin_fileuri)); -%let _webin_name1=%sysfunc(coalescec(&_webin_name1,&_webin_name)); - - -/* If Viya, create temporary fileref(s) */ -%local i; -%if %mf_getplatform()=SASVIYA %then %do i=1 %to &_webin_file_count; - %let _webin_fileref&i=%mf_getuniquefileref(); - filename &&_webin_fileref&i filesrvc "&&_webin_fileuri&i"; -%end; - - -%mend mp_webin; diff --git a/009_macros/mp_zip.sas b/009_macros/mp_zip.sas deleted file mode 100644 index 76f9d76..0000000 --- a/009_macros/mp_zip.sas +++ /dev/null @@ -1,85 +0,0 @@ -/*** HELP START ***//** - @file - @brief Creates a zip file - @details For DIRECTORY usage, will ignore subfolders. For DATASET usage, - provide a column that contains the full file path to each file to be zipped. - - %mp_zip(in=myzips,type=directory,outname=myDir) - %mp_zip(in=/my/file/path.txt,type=FILE,outname=myFile) - %mp_zip(in=SOMEDS,incol=FPATH,type=DATASET,outname=myFile) - - If you are sending zipped output to the _webout destination as part of an STP - be sure that _debug is not set (else the SPWA will send non zipped content - as well). - -

SAS Macros

- @li mp_dirlist.sas - - @param [in] in= unquoted filepath, dataset of files or directory to zip - @param [in] type= (FILE) Valid values: - @li FILE - /full/path/and/filename.extension to a particular file - @li DATASET - a dataset containing a list of files to zip (see `incol`) - @li DIRECTORY - a directory to zip - @param [out] outname= (FILE) Output file to create, _without_ .zip extension - @param [out] outpath= (%sysfunc(pathname(WORK))) Parent folder for zip file - @param [in] incol= () If DATASET input, say which column contains the filepath - -

Related Macros

- @li mp_unzip.sas - @li mp_zip.test.sas - - @version 9.2 - @author Allan Bowe - @source https://github.com/sasjs/core - -**//*** HELP END ***/ - -%macro mp_zip( - in= - ,type=FILE - ,outname=FILE - ,outpath=%sysfunc(pathname(WORK)) - ,incol= - ,debug=NO -)/*/STORE SOURCE*/; - -%let type=%upcase(&type); -%local ds; - -ods package open nopf; - -%if &type=FILE %then %do; - ods package add file="&in" mimetype="application/x-compress"; -%end; -%else %if &type=DIRECTORY %then %do; - %mp_dirlist(path=&in,outds=_data_) - %let ds=&syslast; - data _null_; - set &ds; - length __command $4000; - if file_or_folder='file'; - __command=cats('ods package add file="',filepath - ,'" mimetype="application/x-compress";'); - call execute(__command); - run; - /* tidy up */ - %if &debug=NO %then %do; - proc sql; drop table &ds;quit; - %end; -%end; -%else %if &type=DATASET %then %do; - data _null_; - set ∈ - length __command $4000; - __command=cats('ods package add file="',&incol - ,'" mimetype="application/x-compress";'); - call execute(__command); - run; -%end; - - -ods package publish archive properties - (archive_name="&outname..zip" archive_path="&outpath"); -ods package close; - -%mend mp_zip; diff --git a/007_macros/mx_getgroups.sas b/009_macros/mx_getgroups.sas similarity index 100% rename from 007_macros/mx_getgroups.sas rename to 009_macros/mx_getgroups.sas diff --git a/description.sas b/description.sas index 6f91dab..f0a35c2 100644 --- a/description.sas +++ b/description.sas @@ -1,7 +1,7 @@ Type: Package Package: SASjsCore Title: SAS Macros for Application Development -Version: 4.52.2 +Version: 4.52.3 Author: Allan Bowe Maintainer: 4GL Ltd License: MIT diff --git a/sasjscore.zip b/sasjscore.zip index 79e20b703151122cf88a64cb688bc754420c3a8d..31dea3c0b46b0fc26b42ce98dc3c85af028cfb84 100755 GIT binary patch delta 38145 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 z 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