From 107ab836d622c5a079757d0d867276815c6a57e4 Mon Sep 17 00:00:00 2001 From: Allan Bowe Date: Thu, 25 Nov 2021 16:43:39 +0000 Subject: [PATCH 1/4] feat: mp_lockXXX macros f These macros provide a centralised approach for locking tables where updates can happen in multiple passes. The mp_lockfilecheck macro will also verify the ability to create a SAS lock (if a v9 library engine, and not a view) --- base/mp_lockanytable.sas | 252 ++++++++++++++++++ base/mp_lockfilecheck.sas | 96 +++++++ tests/crossplatform/mp_lockanytable.test.sas | 62 +++++ tests/crossplatform/mp_lockfilecheck.test.sas | 36 +++ tests/testinit.sas | 10 +- 5 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 base/mp_lockanytable.sas create mode 100644 base/mp_lockfilecheck.sas create mode 100644 tests/crossplatform/mp_lockanytable.test.sas create mode 100644 tests/crossplatform/mp_lockfilecheck.test.sas diff --git a/base/mp_lockanytable.sas b/base/mp_lockanytable.sas new file mode 100644 index 0000000..93e85fc --- /dev/null +++ b/base/mp_lockanytable.sas @@ -0,0 +1,252 @@ +/** + @file + @brief Mechanism for locking tables to prevent parallel modifications + @details Uses a control table to enable ANY table to be locked for updates. + Only useful if every update uses the macro! Used heavily within + [Data Controller for SAS](https://datacontroller.io). + + The underlying table is structured as per the MAKETABLE action. + + @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 + @li MAKETABLE - creates the control table (ctl_ds) + @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. + @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 mp_abort.sas + @li mp_lockfilecheck.sas + @li mf_getuser.sas + + @version 9.2 + +**/ + +%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(options obs = 0. syserrortext=&syserrortext) +) + +%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(),E8601DT26.6)"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(),E8601DT26.6)"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_inc)) 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; + proc sql noprint; + 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(),E8601DT26.6)"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; +%else %if &action=MAKETABLE %then %do; + proc sql; + create table &ctl_ds( + lock_lib char(8), + lock_ds char(32), + lock_status_cd char(10) not null, + lock_user_nm char(100) not null , + lock_ref char(200), + lock_pid char(10), + lock_start_dttm num format=E8601DT26.6, + lock_end_dttm num format=E8601DT26.6, + constraint pk_mp_lockanytable primary key(lock_lib,lock_ds)); +%end; +%else %do; + %let msg=lock_anytable given unsupported action (&action); + %let abortme=1; +%end; + +/* catch errors - 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/base/mp_lockfilecheck.sas b/base/mp_lockfilecheck.sas new file mode 100644 index 0000000..79e642d --- /dev/null +++ b/base/mp_lockfilecheck.sas @@ -0,0 +1,96 @@ +/** + @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 + + @version 9.2 +**/ + +%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,.)); + +/* 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; \ No newline at end of file diff --git a/tests/crossplatform/mp_lockanytable.test.sas b/tests/crossplatform/mp_lockanytable.test.sas new file mode 100644 index 0000000..0d1c158 --- /dev/null +++ b/tests/crossplatform/mp_lockanytable.test.sas @@ -0,0 +1,62 @@ +/** + @file + @brief Testing mp_lockfilecheck macro + +

SAS Macros

+ @li mp_lockanytable.sas + @li mp_assertcols.sas + @li mp_assertcolvals.sas + +**/ + +/* check create table */ + +%mp_lockanytable(MAKETABLE, ctl_ds=work.controller) + +%mp_assertcols(work.controller, + cols=lock_status_cd lock_lib lock_ds lock_user_nm lock_ref lock_pid + lock_start_dttm lock_end_dttm, + test=ALL, + desc=check all control columns exist +) + +/* check lock table */ +options dlcreatedir; +libname tmp "%sysfunc(pathname(work))/tmp"; +data tmp.sometable; + x=1; +run; + +%mp_lockanytable(LOCK,lib=tmp,ds=sometable,ref=This Ref, ctl_ds=work.controller) + +data work.checkds1; + checkval='SOMETABLE'; +run; +%mp_assertcolvals(work.controller.lock_ds, + checkvals=work.checkds1.checkval, + desc=table is captured in lock, + test=ANYVAL +) + +data work.checkds2; + checkval='LOCKED'; +run; +%mp_assertcolvals(work.controller.lock_status_cd, + checkvals=work.checkds2.checkval, + desc=code is captured in lock, + test=ANYVAL +) + + + +/* check for unsuccessful unlock */ +%mp_lockanytable(UNLOCK,lib=tmp,ds=sometable,ref=bye, ctl_ds=work.controller) + +data work.checkds3; + checkval='UNLOCKED'; +run; +%mp_assertcolvals(work.controller.lock_status_cd, + checkvals=work.checkds3.checkval, + desc=Ref is captured in unlock, + test=ANYVAL +) diff --git a/tests/crossplatform/mp_lockfilecheck.test.sas b/tests/crossplatform/mp_lockfilecheck.test.sas new file mode 100644 index 0000000..a6a612d --- /dev/null +++ b/tests/crossplatform/mp_lockfilecheck.test.sas @@ -0,0 +1,36 @@ +/** + @file + @brief Testing mp_lockfilecheck macro + +

SAS Macros

+ @li mp_lockfilecheck.sas + @li mp_assert.sas + +**/ + + +/* check for regular lock */ +data work.test; a=1;run; +%mp_lockfilecheck(work.test) + +%mp_assert( + iftrue=(&syscc=0), + desc=Checking regular table can be locked, + outds=work.test_results +) + + +/* check for unsuccessful lock */ +%global success abortme; +%let success=0; +%macro mp_abort(iftrue=,mac=,msg=); + %if &abortme=1 %then %let success=1; +%mend mp_abort; + +%mp_lockfilecheck(sashelp.class) + +%mp_assert( + iftrue=(&success=1), + desc=Checking sashelp table cannot be locked, + outds=work.test_results +) diff --git a/tests/testinit.sas b/tests/testinit.sas index 611a982..a0efbc3 100644 --- a/tests/testinit.sas +++ b/tests/testinit.sas @@ -5,4 +5,12 @@ **/ /* location in metadata or SAS Drive for temporary files */ -%let mcTestAppLoc=/Public/temp/macrocore; \ No newline at end of file +%let mcTestAppLoc=/Public/temp/macrocore; + +%macro loglevel(); + %if &_debug=2477 %then %do; + options mprint; + %end; +%mend; + +%loglevel() \ No newline at end of file From 2d81de58419223423c9d6636f1b5fb65318e8fc3 Mon Sep 17 00:00:00 2001 From: Allan Bowe Date: Thu, 25 Nov 2021 16:46:56 +0000 Subject: [PATCH 2/4] chore: generating all.sas and updating doc links --- all.sas | 351 ++++++++++++++++++++++++++++++++++++++ base/mp_lockanytable.sas | 3 + base/mp_lockfilecheck.sas | 1 + tests/testinit.sas | 2 +- 4 files changed, 356 insertions(+), 1 deletion(-) diff --git a/all.sas b/all.sas index fef4067..543fbcf 100644 --- a/all.sas +++ b/all.sas @@ -6204,6 +6204,357 @@ select distinct lowcase(memname) %end; %mend mp_lib2inserts;/** + @file + @brief Mechanism for locking tables to prevent parallel modifications + @details Uses a control table to enable ANY table to be locked for updates. + Only useful if every update uses the macro! Used heavily within + [Data Controller for SAS](https://datacontroller.io). + + The underlying table is structured as per the MAKETABLE action. + + @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 + @li MAKETABLE - creates the control table (ctl_ds) + @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. + @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 mp_abort.sas + @li mp_lockfilecheck.sas + @li mf_getuser.sas + +

Related Macros

+ @li mp_lockanytable.test.sas + + @version 9.2 + +**/ + +%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(options obs = 0. syserrortext=&syserrortext) +) + +%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(),E8601DT26.6)"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(),E8601DT26.6)"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_inc)) 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; + proc sql noprint; + 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(),E8601DT26.6)"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; +%else %if &action=MAKETABLE %then %do; + proc sql; + create table &ctl_ds( + lock_lib char(8), + lock_ds char(32), + lock_status_cd char(10) not null, + lock_user_nm char(100) not null , + lock_ref char(200), + lock_pid char(10), + lock_start_dttm num format=E8601DT26.6, + lock_end_dttm num format=E8601DT26.6, + constraint pk_mp_lockanytable primary key(lock_lib,lock_ds)); +%end; +%else %do; + %let msg=lock_anytable given unsupported action (&action); + %let abortme=1; +%end; + +/* catch errors - 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; + + +/** + @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 +**/ + +%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,.)); + +/* 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;/** @file @brief Create a Markdown Table from a dataset @details A markdown table is a simple table representation for use in diff --git a/base/mp_lockanytable.sas b/base/mp_lockanytable.sas index 93e85fc..e4dcab6 100644 --- a/base/mp_lockanytable.sas +++ b/base/mp_lockanytable.sas @@ -26,6 +26,9 @@ @li mp_lockfilecheck.sas @li mf_getuser.sas +

Related Macros

+ @li mp_lockanytable.test.sas + @version 9.2 **/ diff --git a/base/mp_lockfilecheck.sas b/base/mp_lockfilecheck.sas index 79e642d..0a2ae82 100644 --- a/base/mp_lockfilecheck.sas +++ b/base/mp_lockfilecheck.sas @@ -17,6 +17,7 @@

Related Macros

@li mp_lockanytable.sas + @li mp_lockfilecheck.test.sas @version 9.2 **/ diff --git a/tests/testinit.sas b/tests/testinit.sas index a0efbc3..4b4633c 100644 --- a/tests/testinit.sas +++ b/tests/testinit.sas @@ -11,6 +11,6 @@ %if &_debug=2477 %then %do; options mprint; %end; -%mend; +%mend loglevel; %loglevel() \ No newline at end of file From a6e915881444889ceb4c245c55efb9a8ce5ec1bf Mon Sep 17 00:00:00 2001 From: Allan Bowe Date: Fri, 26 Nov 2021 11:52:48 +0000 Subject: [PATCH 3/4] chore: adding recursive option to the mp_dirlist macro, and updating all.sas --- all.sas | 79 +++++++++++++++++++------ base/mp_dirlist.sas | 46 +++++++------- tests/crossplatform/mp_dirlist.test.sas | 50 ++++++++++++++++ 3 files changed, 136 insertions(+), 39 deletions(-) create mode 100644 tests/crossplatform/mp_dirlist.test.sas diff --git a/all.sas b/all.sas index fef4067..34b55cb 100644 --- a/all.sas +++ b/all.sas @@ -3070,20 +3070,13 @@ run; @brief Returns all files and subdirectories within a specified parent @details When used with getattrs=NO, is not OS specific (uses dopen / dread). - 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. - 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) + %mp_dirlist(path=/some/location, outds=myTable, maxdepth=MAX) %mp_dirlist(outds=cwdfileprops, getattrs=YES) @@ -3097,11 +3090,19 @@ run; X CMD) do please raise an issue! - @param path= for which to return contents - @param fref= Provide a DISK engine fileref as an alternative to PATH - @param outds= the output dataset to create - @param getattrs= YES/NO (default=NO). Uses doptname and foptname to return - all attributes for each file / folder. + @param [in] path= for which to return contents + @param [in] fref= 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 [out] outds= 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: @@ -3111,8 +3112,12 @@ run; - 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 mp_dropmembers.sas + @version 9.2 @author Allan Bowe **/ @@ -3121,14 +3126,27 @@ run; , fref=0 , outds=work.mp_dirlist , getattrs=NO + , maxdepth=0 + , level=0 /* The level of recursion to perform. For internal use only. */ )/*/STORE SOURCE*/; %let getattrs=%upcase(&getattrs)XX; -data &outds(compress=no - keep=file_or_folder filepath filename ext msg directory +/* 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(&outds, 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; + retain level &level; %if &fref=0 %then %do; rc = filename(fref, "&path"); %end; @@ -3186,8 +3204,8 @@ data &outds(compress=no run; %if %substr(&getattrs,1,1)=Y %then %do; - data &outds; - set &outds; + data &out_ds; + set &out_ds; length infoname infoval $60 fref $8; rc=filename(fref,filepath); drop rc infoname fid i close fref; @@ -3228,12 +3246,37 @@ run; run; proc sort; by filepath sasname; - proc transpose data=&outds out=&outds(drop=_:); + proc transpose data=&out_ds out=&out_ds(drop=_:); id sasname; var infoval; by filepath file_or_folder filename ext ; run; %end; + +/* update main table */ +proc append base=&outds data=&out_ds; +run; + +data &outds; + set &outds(where=(filepath ne '')); +run; + +/* recursive call */ +%if &maxdepth>&level or &maxdepth=MAX %then %do; + data _null_; + set &out_ds; + where file_or_folder='folder'; + 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;/** @file @brief Creates a dataset containing distinct _formatted_ values diff --git a/base/mp_dirlist.sas b/base/mp_dirlist.sas index 6354542..b44a63d 100644 --- a/base/mp_dirlist.sas +++ b/base/mp_dirlist.sas @@ -3,20 +3,13 @@ @brief Returns all files and subdirectories within a specified parent @details When used with getattrs=NO, is not OS specific (uses dopen / dread). - 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. - 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) + %mp_dirlist(path=/some/location, outds=myTable, maxdepth=MAX) %mp_dirlist(outds=cwdfileprops, getattrs=YES) @@ -30,11 +23,19 @@ X CMD) do please raise an issue! - @param path= for which to return contents - @param fref= Provide a DISK engine fileref as an alternative to PATH - @param outds= the output dataset to create - @param getattrs= YES/NO (default=NO). Uses doptname and foptname to return - all attributes for each file / folder. + @param [in] path= for which to return contents + @param [in] fref= 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 [out] outds= 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: @@ -58,6 +59,7 @@ , fref=0 , outds=work.mp_dirlist , getattrs=NO + , maxdepth=0 , level=0 /* The level of recursion to perform. For internal use only. */ )/*/STORE SOURCE*/; %let getattrs=%upcase(&getattrs)XX; @@ -193,14 +195,16 @@ data &outds; run; /* recursive call */ -data _null_; - set &out_ds; - where file_or_folder='folder'; - code=cats('%nrstr(%mp_dirlist(path=',filepath,",outds=&outds" - ,",getattrs=&getattrs,level=%eval(&level+1)))"); - put code=; - call execute(code); -run; +%if &maxdepth>&level or &maxdepth=MAX %then %do; + data _null_; + set &out_ds; + where file_or_folder='folder'; + 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; diff --git a/tests/crossplatform/mp_dirlist.test.sas b/tests/crossplatform/mp_dirlist.test.sas new file mode 100644 index 0000000..ca0becc --- /dev/null +++ b/tests/crossplatform/mp_dirlist.test.sas @@ -0,0 +1,50 @@ +/** + @file + @brief Testing mp_ds2cards.sas macro + +

SAS Macros

+ @li mf_nobs.sas + @li mf_mkdir.sas + @li mp_dirlist.sas + @li mp_assert.sas + +**/ + +/** + * make a directory structure + */ + +%let root=%sysfunc(pathname(work))/top; +%mf_mkdir(&root) +%mf_mkdir(&root/a) +%mf_mkdir(&root/b) +%mf_mkdir(&root/a/d) +%mf_mkdir(&root/a/e) +%mf_mkdir(&root/a/e/f) +data "&root/a/e/f/ds1.sas7bdat"; + x=1; +run; + +%mp_dirlist(path=&root, outds=myTable, maxdepth=MAX) + +%mp_assert( + iftrue=(%mf_nobs(work.mytable)=6), + desc=All levels returned, + outds=work.test_results +) + +%mp_dirlist(path=&root, outds=myTable2, maxdepth=2) + +%mp_assert( + iftrue=(%mf_nobs(work.mytable2)=5), + desc=Top two levels returned, + outds=work.test_results +) + +%mp_dirlist(path=&root, outds=myTable3, maxdepth=0) + +%mp_assert( + iftrue=(%mf_nobs(work.mytable3)=2), + desc=Top level returned, + outds=work.test_results +) \ No newline at end of file From 7bb089e2699da92d4d50a8f1a58c496351619680 Mon Sep 17 00:00:00 2001 From: Allan Bowe Date: Fri, 26 Nov 2021 11:55:55 +0000 Subject: [PATCH 4/4] fix: moving cleanup to temp table for efficiency --- base/mp_dirlist.sas | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/base/mp_dirlist.sas b/base/mp_dirlist.sas index b44a63d..23b0df5 100644 --- a/base/mp_dirlist.sas +++ b/base/mp_dirlist.sas @@ -186,12 +186,12 @@ run; run; %end; -/* update main table */ -proc append base=&outds data=&out_ds; +data &out_ds; + set &out_ds(where=(filepath ne '')); run; -data &outds; - set &outds(where=(filepath ne '')); +/* update main table */ +proc append base=&outds data=&out_ds; run; /* recursive call */