diff --git a/tests/viyaonly/mv_castabload.test.sas b/tests/viyaonly/mv_castabload.test.sas
new file mode 100644
index 0000000..d8dd200
--- /dev/null
+++ b/tests/viyaonly/mv_castabload.test.sas
@@ -0,0 +1,148 @@
+/**
+ @file
+ @brief Testing mv_castabload macro
+
+
SAS Macros
+ @li mf_uid.sas
+ @li mp_assert.sas
+ @li mp_assertscope.sas
+ @li mv_castabload.sas
+ @li mv_createfile.sas
+
+**/
+
+options mprint;
+
+/* ------------------------------------------------------------------------ */
+/* Setup: start a CAS session and stage a source file in the Public caslib */
+/* ------------------------------------------------------------------------ */
+cas mysess;
+caslib _all_ assign;
+
+%let testcaslib = Public; /* change this if Public isn't available */
+proc cas;
+ table.caslibInfo result=r / ;
+ found = 0;
+ do row over r.CASLibInfo;
+ if upcase(row.Name) = upcase("&testcaslib") then found = 1;
+ end;
+ if found = 0 then do;
+ print "ERROR: caslib &testcaslib not available";
+ exit;
+ end;
+quit;
+%put NOTE: Using testcaslib=&testcaslib;
+
+%let tab1=T%mf_uid();
+%let tab2=T%mf_uid();
+%let tab3=T%mf_uid();
+
+/* Create a SASHDAT source file in the Public caslib from SASHELP.BASEBALL
+ so that subsequent LOAD operations have something real to pick up. */
+proc casutil;
+ load data=sashelp.baseball outcaslib="&testcaslib" casout="&tab1" replace;
+ save casdata="&tab1" incaslib="&testcaslib"
+ casout="&tab1..sashdat" outcaslib="&testcaslib" replace;
+ droptable casdata="&tab1" incaslib="&testcaslib" quiet;
+quit;
+
+/* And a second hdat, with a name different from the table name, so that we
+ can exercise the explicit srcfile= path. */
+proc casutil;
+ load data=sashelp.cars outcaslib="&testcaslib" casout="&tab2" replace;
+ save casdata="&tab2" incaslib="&testcaslib"
+ casout="src_&tab2..sashdat" outcaslib="&testcaslib" replace;
+ droptable casdata="&tab2" incaslib="&testcaslib" quiet;
+quit;
+
+
+/* ------------------------------------------------------------------------ */
+%put TEST 1 - missing required parameters returns without setting RC to 0/1;
+/* ------------------------------------------------------------------------ */
+%let MV_CASTABLOAD_RC=;
+%mv_castabload(caslib=,table=,srcfile=)
+
+%mp_assert(
+ iftrue=(&MV_CASTABLOAD_RC=3),
+ desc=Check RC=3 (initial/failure value) when required params are missing
+)
+
+
+/* ------------------------------------------------------------------------ */
+%put TEST 2 - load a table that does not yet exist (default srcfile=table.hdat);
+/* ------------------------------------------------------------------------ */
+%mv_castabload(caslib=&testcaslib,table=&tab1,mdebug=1)
+
+%mp_assert(
+ iftrue=(&MV_CASTABLOAD_RC=1),
+ desc=Check RC=1 when table is loaded and promoted for the first time
+)
+
+
+/* ------------------------------------------------------------------------ */
+%put TEST 3 - calling again for the same table should be a no-op (RC=0);
+/* also verify no scope leakage of macro variables */
+/* ------------------------------------------------------------------------ */
+%mp_assertscope(SNAPSHOT)
+
+%mv_castabload(caslib=&testcaslib,table=&tab1,mdebug=1)
+
+%mp_assertscope(COMPARE,
+ desc=Check mv_castabload does not leak macro variables into GLOBAL scope,
+ ignorelist=MV_CASTABLOAD_RC
+)
+
+%mp_assert(
+ iftrue=(&MV_CASTABLOAD_RC=0),
+ desc=Check RC=0 when table is already in-memory (skip load)
+)
+
+
+/* ------------------------------------------------------------------------ */
+%put TEST 4 - explicit srcfile= where file name differs from table name;
+/* ------------------------------------------------------------------------ */
+%mv_castabload(
+ caslib=&testcaslib,
+ table=&tab2,
+ srcfile=src_&tab2..sashdat,
+ mdebug=1
+)
+
+%mp_assert(
+ iftrue=(&MV_CASTABLOAD_RC=1),
+ desc=Check RC=1 when loading with explicit srcfile= parameter
+)
+
+
+/* ------------------------------------------------------------------------ */
+%put TEST 5 - load failure when srcfile does not exist in the caslib;
+/* ------------------------------------------------------------------------ */
+%mv_castabload(
+ caslib=&testcaslib,
+ table=&tab3,
+ srcfile=doesnotexist_%mf_uid..sashdat,
+ mdebug=1
+)
+
+%mp_assert(
+ iftrue=(&MV_CASTABLOAD_RC=3),
+ desc=Check RC=3 when source file cannot be found / load fails
+)
+
+/* reset so that a downstream failure RC does not break testterm */
+%let syscc=0;
+
+
+/* ------------------------------------------------------------------------ */
+/* Teardown: drop promoted tables and remove source files */
+/* ------------------------------------------------------------------------ */
+proc casutil;
+ droptable casdata="&tab1" incaslib="&testcaslib" quiet;
+ droptable casdata="&tab2" incaslib="&testcaslib" quiet;
+ deletesource casdata="&tab1..sashdat" incaslib="&testcaslib" quiet;
+ deletesource casdata="src_&tab2..sashdat" incaslib="&testcaslib" quiet;
+quit;
+
+cas mysess terminate;
+
+%let syscc=0;
diff --git a/viya/mv_castabload.sas b/viya/mv_castabload.sas
new file mode 100644
index 0000000..bc141fe
--- /dev/null
+++ b/viya/mv_castabload.sas
@@ -0,0 +1,102 @@
+/**
+ @file mv_castabload.sas
+ @brief Checks if a CAS table exists in a CASLIB; if not, loads & promotes it
+ @details Runs in SPRE against an active CAS session. Uses
+ `table.tableExists` to check whether the table is already in-memory,
+ and PROC CASUTIL LOAD with the PROMOTE option to load it if not.
+ CASUTIL infers the file type from the source file extension.
+
+ A CAS session must already be established by the caller, eg:
+
+ cas mysess;
+ %mv_castabload(caslib=Public, table=BASEBALL)
+
+ or (if not a hdat source with the same name as the table):
+
+ %mv_castabload(caslib=Public, table=BASEBALL,
+ srcfile=MYBASEBALL.parquet)
+
+ @param [in] caslib= CASLIB containing the source file
+ @param [in] table= Name to give the in-memory CAS table
+ @param [in] srcfile= (0) Source file name.ext in the caslib. If not provided,
+ the code assumes that srcfile=&table..hdat
+ @param [in] mdebug= (0) Set to 1 to enable verbose logging:
+ - echoes resolved parameters
+ - prints tableExists result
+ - enables mprint/notes during PROC calls
+
+ @returns Sets global macro variable `MV_CASTABLOAD_RC`:
+ 0 = table already existed (no load performed)
+ 1 = table was loaded & promoted successfully
+ 3 = action failed
+**/
+
+%macro mv_castabload(
+ caslib=
+ ,table=
+ ,srcfile=0
+ ,mdebug=0
+);
+
+%global MV_CASTABLOAD_RC;
+%let MV_CASTABLOAD_RC=3;
+
+%local _sysopts;
+%let _sysopts=%sysfunc(getoption(mprint)) %sysfunc(getoption(notes));
+
+/* ---- input validation -------------------------------------------------- */
+%if "&caslib"="" or "&table"="" or "&srcfile"="" %then %do;
+ %put %str(ERR)OR: caslib=, table= and srcfile= are all required;
+ %return;
+%end;
+%if "&srcfile"="0" %then %let srcfile=&table..hdat;
+
+%if &mdebug=1 %then %do;
+ %put &=caslib;
+ %put &=table;
+ %put &=srcfile;
+ options mprint notes;
+%end;
+
+/* ---- existence check --------------------------------------------------- */
+proc cas;
+ table.tableExists result=r /
+ caslib="&caslib"
+ name="&table";
+ %if &mdebug=1 %then %do;
+ print r;
+ %end;
+ if r.exists = 0 then rc = 9;
+ else rc = 0;
+ symputx('MV_CASTABLOAD_RC', rc, 'G');
+quit;
+
+
+/* ---- load if absent ---------------------------------------------------- */
+%if &MV_CASTABLOAD_RC=9 %then %do;
+
+ proc casutil;
+ load casdata="&srcfile"
+ incaslib="&caslib"
+ casout="&table"
+ outcaslib="&caslib"
+ promote;
+ quit;
+
+ %if &syserr=0 %then %let MV_CASTABLOAD_RC=1;
+ %else %let MV_CASTABLOAD_RC=3;
+
+%end;
+
+%if &MV_CASTABLOAD_RC=0 %then
+ %put NOTE: Table &caslib..&table already loaded - skipping;
+%else %if &MV_CASTABLOAD_RC=1 %then
+ %put NOTE: Table &caslib..&table loaded and promoted;
+%else %put ERROR: load failed for &caslib..&table;
+
+/* ---- restore options --------------------------------------------------- */
+%if &mdebug=1 %then %do;
+ options &_sysopts;
+%end;
+
+%mend mv_castabload;