Files
dc/sas/sasjs/macros/mpe_derived.sas
T
dc a4a0f9cb2f
Build / Build-and-ng-test (pull_request) Successful in 6m6s
Lighthouse Checks / lighthouse (pull_request) Successful in 20m15s
Build / Build-and-test-development (pull_request) Successful in 32m40s
feat(derived): apply the core field scan in the MPE_DERIVED hook
@sasjs/core 6.1.3 moved the derived variable field rules out of
mp_derivedvars into mp_derivedvarcheck, so the same scan can run before a
definition is used.  DC carried its own copy of that list in
mpe_derived_check.sas, which is now deleted: the post edit hook calls the
core macro instead, and the hook keeps only the rules that belong to the
control table rather than the expression - LIBREF/DSN required, and ACTIVE
within the picker pair.

The scan is stricter than the copy it replaces, in three ways:

- VAR_NM may not be _N_, _ERROR_ or _IORC_ (automatic variables of the data
  step)
- VAR_LOGIC may not exceed 4000 characters
- VAR_LOGIC may not call a function that submits code or reaches the file
  system, the environment or the macro symbol table - a function name cannot
  be assembled at run time, so this is a control the expression cannot evade

and it relaxes one: VAR_LABEL may carry a line break, which is flattened to a
space when the label is used, so only the remaining control characters are
refused.

The mock hook mirror is updated to the same rules, and stagedata.test.4
gains the two submit-time cases that pin the new wiring.
2026-10-29 10:51:00 +00:00

146 lines
5.7 KiB
SAS

/**
@file
@brief Generates the SAS statements that create Data Controller derived variables
@details Derived (computed) variables are configured in MPE_DERIVED. Each
row defines one variable, which is created in a DATA step view ahead of the
filter, so that it can be filtered on in the EDIT screen.
The engine lives in @sasjs/core as mp_derivedvars - this macro only supplies
what is specific to Data Controller: the control table name, the bitemporal
filter that picks the current version of a row, and the list of names that
belong to the metadata and may not be used as a variable.
The macro writes the generated statements to a fileref, so that callers can
compose them into a view:
%mpe_derived(libds=&libds,dclib=&mpelib,outref=dercode)
data work.vw_derived / view=work.vw_derived;
set &libds;
%include dercode;
run;
Alternatively, pass outview= and the macro will build the view itself.
The filter must be applied by the caller, in the step that reads the view:
data out;
set work.vw_derived;
where %inc filtref;;
run;
A WHERE statement applies to the data set named on the SET statement, so it
cannot reference a variable that is created in the same data step. Applying
it to the view puts the derived variables on the input, which is what makes
them filterable.
Each statement block is of the form:
attrib REGION length=$21 label='REGION=<expression>' format=$21.;
REGION = <expression>;
Type and length are declared by the administrator, so the result is
deterministic. VAR_LABEL is optional: when it is missing or blank the label
is the variable name, an equals sign and the expression, otherwise the
configured label is used as given.
VAR_DEPS lists every input the expression uses - physical columns and
derived variables alike. The statements are generated in dependency order,
so a derived variable may build on another one without a sequence number
being maintained. A cycle is rejected.
Semicolons, macro triggers and comments are rejected, so a row can only ever
be an expression, and an expression that would let the row submit code of its
own - DOSUBL, or a function reaching the file system, the environment or the
macro symbol table - is refused by name.
The field level rules themselves are core's: mp_derivedvarcheck scans the
definitions and mp_derivedvars calls it, which is the same macro the MPE_DERIVED
post edit hook runs at submit time (services/hooks/mpe_derived_postedit). A
row is therefore measured against one rule list, whether it is being staged
or read.
A derived variable may not reuse the name of a physical column, and may not
reference itself: an intra-row calculation would imply a retained value,
which depends on the order rows happen to arrive in.
<h4> SAS Macros </h4>
@li mp_derivedvars.sas
@li mp_abort.sas
@param [in] libds= The target table (libref.dataset, lib must be assigned)
@param [in] dclib= The Data Controller control library
@param [out] outref= (0) Fileref to receive the generated statements
@param [out] outvars= (work.derived_vars) Dataset of derived variable names
@param [in] outview= (0) Optional two-level name for the derived view
@version 9.2
@author 4GL Apps Ltd
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
and may not be re-distributed or re-sold without the express permission of
4GL Apps Ltd.
**/
%macro mpe_derived(libds=,
dclib=,
outref=0,
outvars=work.derived_vars,
outview=0
);
%local reserved;
/* the only name that has to be refused outright - the metadata columns of the
target table are physical columns, so the engine's collision check covers
them whatever MPE_TABLES calls them */
%let reserved=%str(_____DELETE__THIS__RECORD_____);
/* dc_dttmtfmt is the bitemporal cut-off used in the where= below - callers get
it from %mpeinit(). Unset, the filter degenerates to "lt tx_to" and the
control-table step fails with a syntax error a long way from the cause. */
%mp_abort(iftrue= (%length(&libds)=0 or %length(&dclib)=0 or %length(&dc_dttmtfmt)=0)
,mac=&sysmacroname
,msg=%str(Missing: libds dclib dc_dttmtfmt)
)
/* A format catalog is addressed as LIBREF.CATALOGNAME-FC (the form
mpe_validatecol accepts, and the one viewtables returns). A catalog has no
observations, so it can have no derived variables - and it is not a dataset,
so the engine's existence check would abort the caller. Return an empty
variable list and an empty statement file instead, and leave outview alone:
a caller only reads the view when the list is not empty. */
%if %length(&libds)>3 and "%upcase(%substr(&libds,%length(&libds)-2,3))"="-FC" %then %do;
/* the file has to exist even though it stays empty - callers %include it
unconditionally, and a fileref with no file behind it fails there */
%if "%upcase(&outref)" ne "0" %then %do;
filename &outref temp lrecl=32767;
data _null_;
file &outref;
run;
%end;
/* The shape is the engine's own: mp_derivedvars builds
`data &outvars(keep=name); length name $32; name=''; if 0; run;` before it
returns (empty case, and again from the sorted list in the populated case),
so NAME is the only column either path can produce. Every caller reads
NAME alone - getdata's `where a.NAME in (select NAME from
work.derived_vars)` is the only projection of this dataset in the SAS tree. */
data &outvars(keep=name);
length name $32;
name='';
if 0;
run;
%return;
%end;
%mp_derivedvars(
libds=&libds
,control=&dclib..mpe_derived
,where=%str(&dc_dttmtfmt lt tx_to and active=1)
,outref=&outref
,outvars=&outvars
,outview=&outview
,reserved=&reserved
)
%mend mpe_derived;