A table's metadata columns are named per table - VAR_TXFROM, VAR_TXTO and
VAR_PROCESSED in MPE_TABLES hold those names, and they can be anything (the
bitemporal demo table uses VER_FROM_DTTM and VER_TO_DTTM). When they exist
they are physical columns of the target, so the engine's collision check
already refuses a VAR_NM that matches one, whatever it is called.
The hard-coded list treated three example names as special: it refused them on
tables that use other names, and told the reader something untrue about what
is reserved.
mpe_derived carried its own copy of the derived variable engine. That engine
now ships in @sasjs/core 5.7.0 as mp_derivedvars, so the local copy is gone and
mpe_derived is a wrapper that supplies what is Data Controller's own: the
control table, the bitemporal filter that picks the current active row, and the
list of metadata names that may not be used as a variable.
The macro goes from 511 lines to 102. The contract and the generated statement
format are unchanged, so the callers in the six services need no edit.
A test covers the wrapper's own responsibility: a closed version of a row and
an inactive row are both ignored.
A label at the 256 character limit produces an attrib statement of about 300
characters, which the default record length wraps, splitting the statement and
breaking the view.
A configured label replaces the default NAME=expression; when it is missing or
blank the default is used, so existing rows are unaffected.
- MPE_DERIVED gains VAR_LABEL varchar(256), in the ddl, mpe_makedatamodel and
the v7.19 migration
- mpe_derived uses it, through the same escaping and 256-char truncation as
the default label
- the hook trims it; the mock mirrors the fallback
- test 1 now sets a label on BAND and asserts it
The label had 256 chars of storage, so the doubled quotes were truncated on
assignment; a cut between the halves of an escape leaves an unterminated
literal and the view fails to build. The label now holds 512 chars, quotes
are doubled, then the 256 boundary is applied, retreating one character when
the cut lands on an odd quote count.
Allowing a derived variable to reuse a physical column's name was wrong: an
intra-row calculation (SUMMER = SUMMER + 1) implies a retained value, which
depends on the order rows are read. The same argument rules out a variable
listing itself as a dependency.
Reverted:
- the name collision is an error again ('VAR_NM collides with a column in the
target table')
- VAR_DEPS may not list the variable itself
- the physical column list is back to mf_getvarlist - the type/length check
only existed to support shadowing
- the droplist guards in stagedata / viewdata / getrawdata, and the duplicate
suppression in getdata, are no longer needed
- the docs and both test suites drop the shadowing case
SUMMER = SUMMER + 1 is a legitimate definition: the name matches an existing
column, the expression reads the physical value, and the view carries the
derived one under the same name.
- mpe_derived now takes the physical column list from mp_getcols, so it has
type and length in one pass; a shadowed name must match the column's type,
and its length may not exceed it (numeric lengths must be equal), as attrib
cannot change either on an existing variable
- a self reference is only an error when the name is not a physical column
- self references are not graph edges, so the dependency sort is unaffected
- the post edit hook no longer rejects a self reference, since it cannot see
the target library; it still rejects cycles between distinct names
- mpe_derived now uses mf_getuniquename for its working tables, matching the
core macro, and carries the same placement sequence fix
- test 4 covers shadowing, docs cover the rule
Same defect as the core review found in mp_derivedvars: the final assemble
merged the placement table by name, but that table is built in dependency
rounds and is not name ordered, so the BY-merge failed whenever the rounds
were not alphabetical, and seq was a BY-group counter rather than the
dependency order.
Each round now stamps seq=_n_+nplaced, the merge runs on the name-sorted
tables, and the result is sorted by seq. Also guards the physical-column
in-list for a zero-column table, and checks libds exists before running.
The cycle check was already in the post edit hook and in mpe_derived - the
topological sort places a variable only once its derived inputs are placed,
and if a round places nothing the remainder is a cycle. What it reported was
just the set of names that could not be placed, which does not tell the
administrator what to change.
The message now walks the edges from the first unresolved variable and names
the loop, eg:
VAR_DEPENDENCIES contains a cycle: A -> B -> C -> A
Names are collected in a dotted seen list, so a lead-in to the loop is shown
as well and the walk stops at the first repeat.
A sequence number has to be maintained by the administrator: inserting a
variable means renumbering everything after it, and a wrong number fails
silently - the derived value comes back missing, not wrong.
VAR_DEPENDENCIES names every input the expression uses, physical columns and
derived variables alike, eg 'REGION STATE'. The statements are emitted in
dependency order, so a variable can build on another one with no sequence to
maintain, and a cycle is rejected.
The declaration is checked against the expression in both directions: a
derived variable referenced by VAR_LOGIC must be declared, and a declared
name that the expression does not reference is an error; a declared name must
resolve to a column of the target table or to a derived variable of the same
table.
Changes:
- MPE_DERIVED: VAR_ORDER (num) becomes VAR_DEPENDENCIES (varchar(4000)), in
the ddl, mpe_makedatamodel and the v7.19 migration
- mpe_derived: dependency validation and a topological sort replace the
VAR_ORDER sort and the forward-reference check
- mpe_derived_postedit: checks self reference, undeclared derived references
and cycles at submit time, across the incoming rows plus the current rows
they do not replace
- MPE_VALIDATIONS: the VAR_ORDER MINVAL/MAXVAL rules are replaced by UPCASE
and a character pattern on VAR_DEPENDENCIES
- mocks: the dependency order is applied in JS, so a chained variable works
even when the rows are loaded child first
- docs and tests updated
Found running the macro on a real Viya compute server:
- verify() was applied to fixed width variables, so the blank padding was
always outside the allowed character list and every name was rejected. The
check now runs on strip(name) and strip(fmt).
- the empty outvars dataset used 'length name $32; stop;', which the Viya
compute server rejects with 'Variable name is uninitialized'. It now
assigns the variable and subsets with 'if 0;'.
- the label is now <VAR_NM>=<expression> rather than 'DERIVED: <expression>',
so the grid shows the variable name as well as the logic.
MPE_DERIVED columns:
VARIABLE_NM -> VAR_NM
VARIABLE_ORDER -> VAR_ORDER
VARIABLE_TYPE -> VAR_TYPE
VARIABLE_LENGTH -> VAR_LEN
VARIABLE_FORMAT -> VAR_FMT
VARIABLE_LOGIC -> VAR_LOGIC
Updated in the ddl, mpe_makedatamodel, the migration, the MPE_TABLES buskey,
the macro (reads and validation messages), the tests, the mocks and the docs.
Review follow-up.
- stagedata: build the derived droplist before the ULOAD filter, and drop the
derived columns in the badrecords step, so the row-level-security check runs
against physical columns only. A submitted derived value can no longer
satisfy an RLS clause that names a derived column.
- viewdata: the full-table search view now drops the derived columns too, so
a free-text search cannot match a value in an EDIT-only column.
- mpe_derived: upcase the libds when matching MPE_DERIVED, so a lower-case
table reference still finds its configuration instead of silently showing
no derived columns.
A derived variable may need to reference one defined before it (a label that
concatenates a derived region, for example), so the order has to be
configured rather than assumed.
- MPE_DERIVED: new VARIABLE_ORDER column (ddl, makedatamodel, migration).
- mpe_derived: statements are generated in VARIABLE_ORDER then name sequence.
VARIABLE_ORDER is required and must be a positive integer. A reference to a
derived variable with an equal or higher order is rejected, because SAS
would otherwise treat it as uninitialised and return a missing value with no
error; references to lower-order variables are allowed.
- mock: same ordering, each variable compiled against the physical columns
plus those defined earlier, so the frontend can exercise a chained variable.
- makedata: VARIABLE_ORDER on both demo rows plus a chained REGION_LABEL.
- cypress: asserts the chained value in the grid.
- tests: the getdata test seeds a second variable built on the first.
- docs: VARIABLE_ORDER documented, with the ordered rule replacing the
self-contained one.
Review follow-up.
- viewdata / getrawdata: apply the filter against the derived view when one
exists, so a saved filter that references a derived column no longer aborts
with "Variable ... is not on file" in the VIEW screen or a download. The
derived columns themselves are dropped from that output - they remain an
EDIT screen feature.
- mpe_derived: reject a VARIABLE_FORMAT containing a semicolon, macro trigger
or comment, and require it to look like a format name, so it cannot inject a
statement into the generated data step.
- mpe_derived: reject a VARIABLE_LOGIC that references another derived
variable (word-boundary match), which was documented but not enforced.
- mpe_derived: stmt length 4100 so a 4000-char expression plus the name and
assignment cannot lose its trailing semicolon.
- mock tokenizer: allowlist the punctuation the expression subset uses, so
anything else is rejected instead of being evaluated.
Adds user-defined computed variables, configured per table in the new
MPE_DERIVED control table. A derived variable is declared with a name,
type, length, optional format and a SAS expression.
The expression is applied in a DATA step view, and the filter is applied
by the step that reads the view, so the variables exist before the
filter is evaluated and can therefore be filtered on. A WHERE clause
cannot see a variable created in the same data step, hence the view.
- getdata: derived columns are added to the COLS payload (read only,
labelled DERIVED: <expression>) and to the data returned to the grid
- getcolvals: values are taken from the derived view, so filter dropdowns
work for derived columns
- validatefilter: the filter is validated against the view, and stored
against the physical table (needs mp_filterstore targetds=)
- stagedata: derived columns are dropped before the approval package
- mpe_makedata / mpe_makedatamodel / ddl / migration: new MPE_DERIVED
table, registered in MPE_TABLES
Values are display-time only - nothing derived is ever written back.