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
new file mode 100644
index 0000000..e4dcab6
--- /dev/null
+++ b/base/mp_lockanytable.sas
@@ -0,0 +1,255 @@
+/**
+ @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;
+
+
diff --git a/base/mp_lockfilecheck.sas b/base/mp_lockfilecheck.sas
new file mode 100644
index 0000000..0a2ae82
--- /dev/null
+++ b/base/mp_lockfilecheck.sas
@@ -0,0 +1,97 @@
+/**
+ @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;
\ 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..4b4633c 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;
+
+%loglevel()
\ No newline at end of file