Bumps @sasjs/core from 6.1.4 to 7.0.1 so that restoring a load which touched a special missing numeric works, adds a test that reproduces the failure (issue #368), and fixes two defects found in the test path while proving it.
The bug this fixes
mp_stripdiffs reverts a load by generating proc sql; update ... set <col> = <value> statements. PUT writes a special missing numeric as its letter only, so a value of ._ reached the statement as the bare token _:
update work.stage set SOME_BESTNUM =_ where ... ;
ERROR: The following columns were not found in the contributing tables: _.
ERROR: SOME_BESTNUM, a numeric column, can only be updated with a numeric expression.
PROC SQL then put the whole block into NOEXEC, so the revert ran against unchanged data and the service's syscc check is what surfaced it. Any table holding ._ or .A-.Z in a column that was changed, or in a row that was deleted, was affected - the restore simply could not complete.
Fixed upstream in sasjs/core#499, released as @sasjs/core 7.0.1: the period is written before the value, and .A-.Z/._ are covered (the guard is missing(v) and v ne ., because .A sorts above .).
Breaking changes between 6.1.4 and 7.0.1
v7.0.0 changed mf_existvarlist to warn and return 0 instead of aborting when libds/varlist is blank. DC does not call it - there are no direct references in sas/sasjs - and the core macros that do (mp_assertcols, mp_stackdiffs, mv_jobflow) are covered by the suite run below. v6 is already absorbed by the 6.1.4 pin.
Test
sas/sasjs/services/editors/restore.test.2.sas:
seeds some_bestnum=._ on mpe_x_test (the audit stores whatever the base holds, so this is the value the revert has to write back);
changes that cell to 42 through stagedata and approves it through postdata;
reverts that load through services/editors/restore and approves the reversion;
asserts the cell is ._ again - not blank, not 42;
puts the fixture back as makedata left it.
Steps 1-3 are the issue #368 path: the diff's old value is the special missing, so it is the value the generated UPDATE has to carry.
Two defects found while proving it
Both were in the way of the test, not in the bump, and both are fixed here:
mpe_execute lost a child's abort message. Its failfast abort was built with ,msg=%str(&program did not complete: state=&state &msg). %str masks the literal text, not the resolved value, so commas in the child's own message stayed delimiters: the call split into extra positional parameters and the caller got ERROR: All positional parameters must precede keyword parameters instead of the child's failure. Now %bquote, which masks the resolved value. The same shape in mpe_security_postedit (a submitted ACCESS_LEVEL value interpolated into an abort) is fixed the same way.
stagedata.test.4 cancelled itself on an expected abort. Test 8 submits an expression the derived-variable scan refuses, so the child is meant to abort - but its %mpe_execute call omitted failfast=0, which the file's other abort-expecting calls pass. Added.
Evidence
Run on a scratch appLoc with makedata for a fresh control library, sasjs cbd before each run.
with Checking the revert was submitted, Checking the reversion was approved and Checking the special missing was restored all FAIL. The pre-existing restore.test passes in both runs - its fixture has no special missings, which is why this went unnoticed.
Full SAS suite on 7.0.1: 63 suites PASS, 1 FAIL - stagedata.test.4. That failure is pre-existing and independent of the bump: it fails identically on 6.1.4, and it is not caused by anything in this PR's earlier state either. With the two fixes above it is reduced to one specific scenario - the child process aborts inside the hook and never writes a response, so the assertion has nothing to read - which is now tracked as #411. The other abort-expecting cases in that file pass.
Not run
SAS 9 and Viya lanes - no estate for them from this session. The change is a macro-library bump plus SAS-only regression tests, and the affected macro is base (all platforms).
## What
Bumps `@sasjs/core` from 6.1.4 to **7.0.1** so that restoring a load which touched a special missing numeric works, adds a test that reproduces the failure (issue #368), and fixes two defects found in the test path while proving it.
## The bug this fixes
`mp_stripdiffs` reverts a load by generating `proc sql; update ... set <col> = <value>` statements. `PUT` writes a special missing numeric as its letter only, so a value of `._` reached the statement as the bare token `_`:
```
update work.stage set SOME_BESTNUM =_ where ... ;
ERROR: The following columns were not found in the contributing tables: _.
ERROR: SOME_BESTNUM, a numeric column, can only be updated with a numeric expression.
```
PROC SQL then put the whole block into NOEXEC, so the revert ran against unchanged data and the service's `syscc` check is what surfaced it. Any table holding `._` or `.A`-`.Z` in a column that was changed, or in a row that was deleted, was affected - the restore simply could not complete.
Fixed upstream in [sasjs/core#499](https://github.com/sasjs/core/pull/499), released as `@sasjs/core` 7.0.1: the period is written before the value, and `.A`-`.Z`/`._` are covered (the guard is `missing(v) and v ne .`, because `.A` sorts above `.`).
## Breaking changes between 6.1.4 and 7.0.1
v7.0.0 changed `mf_existvarlist` to warn and return 0 instead of aborting when `libds`/`varlist` is blank. DC does not call it - there are no direct references in `sas/sasjs` - and the core macros that do (`mp_assertcols`, `mp_stackdiffs`, `mv_jobflow`) are covered by the suite run below. v6 is already absorbed by the 6.1.4 pin.
## Test
`sas/sasjs/services/editors/restore.test.2.sas`:
1. seeds `some_bestnum=._` on `mpe_x_test` (the audit stores whatever the base holds, so this is the value the revert has to write back);
2. changes that cell to `42` through `stagedata` and approves it through `postdata`;
3. reverts that load through `services/editors/restore` and approves the reversion;
4. asserts the cell is `._` again - not blank, not `42`;
5. puts the fixture back as `makedata` left it.
Steps 1-3 are the issue #368 path: the diff's old value is the special missing, so it is the value the generated UPDATE has to carry.
## Two defects found while proving it
Both were in the way of the test, not in the bump, and both are fixed here:
- **`mpe_execute` lost a child's abort message.** Its failfast abort was built with `,msg=%str(&program did not complete: state=&state &msg)`. `%str` masks the literal text, not the resolved value, so commas in the child's own message stayed delimiters: the call split into extra positional parameters and the caller got `ERROR: All positional parameters must precede keyword parameters` instead of the child's failure. Now `%bquote`, which masks the resolved value. The same shape in `mpe_security_postedit` (a submitted `ACCESS_LEVEL` value interpolated into an abort) is fixed the same way.
- **`stagedata.test.4` cancelled itself on an expected abort.** Test 8 submits an expression the derived-variable scan refuses, so the child is meant to abort - but its `%mpe_execute` call omitted `failfast=0`, which the file's other abort-expecting calls pass. Added.
## Evidence
Run on a scratch appLoc with `makedata` for a fresh control library, `sasjs cbd` before each run.
**Red** - core 6.1.4, the version being replaced:
```
aad23eaf-7720-4c9c-b08d-a66c8fe0aaa1 mpe_checkrestore PASS
abb9c448-67aa-4b7c-84b8-dab209b6bd0f restore FAIL
a312e6d5-0a96-40aa-bb12-defdad03883e restore PASS
```
with `Checking the revert was submitted`, `Checking the reversion was approved` and `Checking the special missing was restored` all FAIL. The pre-existing `restore.test` passes in both runs - its fixture has no special missings, which is why this went unnoticed.
**Green** - core 7.0.1:
```
0bd004f1-52d8-41c6-82b4-99e8feda3972 mpe_checkrestore PASS
0a95b4b3-b73f-4d6e-a3dd-19732594ef70 restore PASS
01fc6c68-7bfa-45e6-a344-86b28ee90977 restore PASS
```
**Full SAS suite** on 7.0.1: 63 suites PASS, 1 FAIL - `stagedata.test.4`. That failure is **pre-existing and independent of the bump**: it fails identically on 6.1.4, and it is not caused by anything in this PR's earlier state either. With the two fixes above it is reduced to one specific scenario - the child process aborts inside the hook and never writes a response, so the assertion has nothing to read - which is now tracked as #411. The other abort-expecting cases in that file pass.
## Not run
SAS 9 and Viya lanes - no estate for them from this session. The change is a macro-library bump plus SAS-only regression tests, and the affected macro is base (all platforms).
Closes #368
Restoring any load that touched a special missing numeric (._ or .A-.Z) failed
in mp_stripdiffs, which writes the old value into a generated UPDATE statement.
PUT writes a special missing as its letter only, so the statement carried
"set SOME_BESTNUM =_" and PROC SQL read the bare underscore as a column name:
ERROR: The following columns were not found in the contributing tables: _.
ERROR: SOME_BESTNUM, a numeric column, can only be updated with a numeric
expression.
Every statement in the generated block then failed NOEXEC, so the revert
produced the wrong data; the service's syscc check is what surfaced it.
Fixed upstream in sasjs/core#499 (the period is now written first) and
released as 7.0.1. v7.0.0's breaking change - mf_existvarlist warns and returns
0 instead of aborting on blank input - does not affect DC: there are no direct
callers, and the full SAS suite covers the core macros that do use it.
Adds restore.test.2.sas, which seeds some_bestnum=._ on mpe_x_test, changes it
to a number through stagedata and approves it, reverts that load, approves the
reversion, and asserts the cell is back to ._ rather than blank. It fails on
6.1.4 and passes on 7.0.1.
Reviewed the full diff: @sasjs/core 6.1.4 -> 7.0.1 pinned in sas/package.json with the lockfile updated, plus the new special-missing restore test.
sas/package-lock.json - the bump leaves @sasjs/core 6.1.4 nested at node_modules/@sasjs/cli/node_modules/@sasjs/core. If the compile resolves core from the CLI's own dependency tree rather than the project root, 6.1.4 is the macro that gets compiled and the fix never reaches the deployed makedata service. Confirm the compiled artifact carries the 7.0.1 mp_stripdiffs before merging.
1 finding above for review.
REVIEWED ON 2026-10-30 16:27 UTC
Reviewed the full diff: @sasjs/core 6.1.4 -> 7.0.1 pinned in sas/package.json with the lockfile updated, plus the new special-missing restore test.
- sas/package-lock.json - the bump leaves @sasjs/core 6.1.4 nested at node_modules/@sasjs/cli/node_modules/@sasjs/core. If the compile resolves core from the CLI's own dependency tree rather than the project root, 6.1.4 is the macro that gets compiled and the fix never reaches the deployed makedata service. Confirm the compiled artifact carries the 7.0.1 mp_stripdiffs before merging.
1 finding above for review.
Checked, and the concern does not apply here - the compiler takes core from the project root, not from the CLI's tree.
node_modules/@sasjs/core -> 7.0.1
node_modules/@sasjs/cli/node_modules/@sasjs/core -> 6.1.4 (as you saw)
The artifact built by sasjs cbd -t server at this head is sasjsbuild/server.sas (single file, what gets deployed), plus the per-service copies. Both carry the 7.0.1 guard - the period-prefixed special-missing branch that does not exist in 6.1.4:
$ grep -c "missing(oldval_num) and oldval_num ne\|missing(newval_num) and newval_num ne" \
sasjsbuild/services/editors/restore.sas
5 # the five sites in mp_stripdiffs: 4 oldval, 1 newval
node_modules/@sasjs/cli/node_modules/@sasjs/core is 6.1.4 because the CLI declares @sasjs/core: ^6.0.0 as its own dependency - that copy is the CLI's macro set, and no macro from it is inlined into a project build.
The behavioural proof is the pair of runs already in the PR body: with 6.1.4 installed at the root, restore.test.2 fails on "revert was submitted", "reversion was approved" and "special missing was restored"; with 7.0.1 at the root it passes. Both runs were rebuilt with sasjs cbd immediately beforehand, so what the deployed services executed changed with the root version and nothing else.
If you would rather the nested copy were not there at all, that is a CLI change, not this one: it needs either an npm overrides pin forcing @sasjs/cli onto a core it does not declare, or a CLI release on the 7.x line. Happy to raise it against @sasjs/cli as a follow-up if you want the tree flattened.
Checked, and the concern does not apply here - the compiler takes core from the project root, not from the CLI's tree.
```text
node_modules/@sasjs/core -> 7.0.1
node_modules/@sasjs/cli/node_modules/@sasjs/core -> 6.1.4 (as you saw)
```
The artifact built by `sasjs cbd -t server` at this head is `sasjsbuild/server.sas` (single file, what gets deployed), plus the per-service copies. Both carry the 7.0.1 guard - the period-prefixed special-missing branch that does not exist in 6.1.4:
```text
$ grep -c "missing(oldval_num) and oldval_num ne\|missing(newval_num) and newval_num ne" \
sasjsbuild/services/editors/restore.sas
5 # the five sites in mp_stripdiffs: 4 oldval, 1 newval
```
`node_modules/@sasjs/cli/node_modules/@sasjs/core` is 6.1.4 because the CLI declares `@sasjs/core: ^6.0.0` as its own dependency - that copy is the CLI's macro set, and no macro from it is inlined into a project build.
The behavioural proof is the pair of runs already in the PR body: with 6.1.4 installed at the root, `restore.test.2` fails on "revert was submitted", "reversion was approved" and "special missing was restored"; with 7.0.1 at the root it passes. Both runs were rebuilt with `sasjs cbd` immediately beforehand, so what the deployed services executed changed with the root version and nothing else.
If you would rather the nested copy were not there at all, that is a CLI change, not this one: it needs either an npm `overrides` pin forcing `@sasjs/cli` onto a core it does not declare, or a CLI release on the 7.x line. Happy to raise it against `@sasjs/cli` as a follow-up if you want the tree flattened.
mpe_execute reports a child that did not complete by aborting the caller with
"&program did not complete: state=&state &msg". The message was passed through
%str, which masks the literal text before resolution - so the commas in a
child's own abort message (mp_abort writes them as part of the reason) were
still delimiters when the call was parsed. The call split into extra positional
parameters and SAS raised
ERROR: All positional parameters must precede keyword parameters.
replacing the real failure with a parse error at the call site. The caller then
lost the child's message entirely.
%bquote masks the resolved value, so the message arrives as one argument and the
abort reports what the child actually said.
The same shape in mpe_security_postedit, where the submitted ACCESS_LEVEL value
is interpolated into the abort message, is fixed the same way.
Test 8 submits a VAR_LOGIC expression that the derived-variable scan refuses, so
the child is expected to abort and the assertion reads its message from the
response. Without failfast=0 mpe_execute aborted this program instead, so the
case could never reach its assertion. The file's other abort-expecting calls
already pass failfast=0.
This does not make the case pass on its own - the child still cancels without
writing a response, which is tracked separately - but it removes the wrong
failure in front of it.
Two further commits on the branch, both found while proving the regression test, plus an issue for the one thing they do not fix.
20dbf00 - mpe_execute passed a child's abort message through %str, so a comma in that message split its own abort call and the caller saw All positional parameters must precede keyword parameters instead of the child's failure. %bquote masks the resolved value; the same shape in mpe_security_postedit is fixed too.
dfc5c6e - stagedata.test.4 test 8 called %mpe_execute without failfast=0 although the child is expected to abort, so the test cancelled itself before its assertion. The file's other abort-expecting calls already passed it.
With those, stagedata.test.4 fails for exactly one reason: the child aborts inside the hook and writes no response, so the assertion has nothing to read. That is independent of the core bump - identical on 6.1.4 and 7.0.1 - and is now #411, with the log chain and the ruled-out causes.
PR body updated with the two commits and the corrected suite statement (63 pass, 1 fail, tracked).
Two further commits on the branch, both found while proving the regression test, plus an issue for the one thing they do not fix.
- `20dbf00` - `mpe_execute` passed a child's abort message through `%str`, so a comma in that message split its own abort call and the caller saw `All positional parameters must precede keyword parameters` instead of the child's failure. `%bquote` masks the resolved value; the same shape in `mpe_security_postedit` is fixed too.
- `dfc5c6e` - `stagedata.test.4` test 8 called `%mpe_execute` without `failfast=0` although the child is expected to abort, so the test cancelled itself before its assertion. The file's other abort-expecting calls already passed it.
With those, `stagedata.test.4` fails for exactly one reason: the child aborts inside the hook and writes no response, so the assertion has nothing to read. That is independent of the core bump - identical on 6.1.4 and 7.0.1 - and is now #411, with the log chain and the ruled-out causes.
PR body updated with the two commits and the corrected suite statement (63 pass, 1 fail, tracked).
allan
merged commit 8862798d65 into release/7.222026-10-30 17:23:32 +00:00
allan
deleted branch fix/restore-special-missings2026-10-30 17:23:33 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What
Bumps
@sasjs/corefrom 6.1.4 to 7.0.1 so that restoring a load which touched a special missing numeric works, adds a test that reproduces the failure (issue #368), and fixes two defects found in the test path while proving it.The bug this fixes
mp_stripdiffsreverts a load by generatingproc sql; update ... set <col> = <value>statements.PUTwrites a special missing numeric as its letter only, so a value of._reached the statement as the bare token_:PROC SQL then put the whole block into NOEXEC, so the revert ran against unchanged data and the service's
syscccheck is what surfaced it. Any table holding._or.A-.Zin a column that was changed, or in a row that was deleted, was affected - the restore simply could not complete.Fixed upstream in sasjs/core#499, released as
@sasjs/core7.0.1: the period is written before the value, and.A-.Z/._are covered (the guard ismissing(v) and v ne ., because.Asorts above.).Breaking changes between 6.1.4 and 7.0.1
v7.0.0 changed
mf_existvarlistto warn and return 0 instead of aborting whenlibds/varlistis blank. DC does not call it - there are no direct references insas/sasjs- and the core macros that do (mp_assertcols,mp_stackdiffs,mv_jobflow) are covered by the suite run below. v6 is already absorbed by the 6.1.4 pin.Test
sas/sasjs/services/editors/restore.test.2.sas:some_bestnum=._onmpe_x_test(the audit stores whatever the base holds, so this is the value the revert has to write back);42throughstagedataand approves it throughpostdata;services/editors/restoreand approves the reversion;._again - not blank, not42;makedataleft it.Steps 1-3 are the issue #368 path: the diff's old value is the special missing, so it is the value the generated UPDATE has to carry.
Two defects found while proving it
Both were in the way of the test, not in the bump, and both are fixed here:
mpe_executelost a child's abort message. Its failfast abort was built with,msg=%str(&program did not complete: state=&state &msg).%strmasks the literal text, not the resolved value, so commas in the child's own message stayed delimiters: the call split into extra positional parameters and the caller gotERROR: All positional parameters must precede keyword parametersinstead of the child's failure. Now%bquote, which masks the resolved value. The same shape inmpe_security_postedit(a submittedACCESS_LEVELvalue interpolated into an abort) is fixed the same way.stagedata.test.4cancelled itself on an expected abort. Test 8 submits an expression the derived-variable scan refuses, so the child is meant to abort - but its%mpe_executecall omittedfailfast=0, which the file's other abort-expecting calls pass. Added.Evidence
Run on a scratch appLoc with
makedatafor a fresh control library,sasjs cbdbefore each run.Red - core 6.1.4, the version being replaced:
with
Checking the revert was submitted,Checking the reversion was approvedandChecking the special missing was restoredall FAIL. The pre-existingrestore.testpasses in both runs - its fixture has no special missings, which is why this went unnoticed.Green - core 7.0.1:
Full SAS suite on 7.0.1: 63 suites PASS, 1 FAIL -
stagedata.test.4. That failure is pre-existing and independent of the bump: it fails identically on 6.1.4, and it is not caused by anything in this PR's earlier state either. With the two fixes above it is reduced to one specific scenario - the child process aborts inside the hook and never writes a response, so the assertion has nothing to read - which is now tracked as #411. The other abort-expecting cases in that file pass.Not run
SAS 9 and Viya lanes - no estate for them from this session. The change is a macro-library bump plus SAS-only regression tests, and the affected macro is base (all platforms).
Closes #368
Restoring any load that touched a special missing numeric (._ or .A-.Z) failed in mp_stripdiffs, which writes the old value into a generated UPDATE statement. PUT writes a special missing as its letter only, so the statement carried "set SOME_BESTNUM =_" and PROC SQL read the bare underscore as a column name: ERROR: The following columns were not found in the contributing tables: _. ERROR: SOME_BESTNUM, a numeric column, can only be updated with a numeric expression. Every statement in the generated block then failed NOEXEC, so the revert produced the wrong data; the service's syscc check is what surfaced it. Fixed upstream in sasjs/core#499 (the period is now written first) and released as 7.0.1. v7.0.0's breaking change - mf_existvarlist warns and returns 0 instead of aborting on blank input - does not affect DC: there are no direct callers, and the full SAS suite covers the core macros that do use it. Adds restore.test.2.sas, which seeds some_bestnum=._ on mpe_x_test, changes it to a number through stagedata and approves it, reverts that load, approves the reversion, and asserts the cell is back to ._ rather than blank. It fails on 6.1.4 and passes on 7.0.1.REVIEWED ON 2026-10-30 16:27 UTC
Reviewed the full diff: @sasjs/core 6.1.4 -> 7.0.1 pinned in sas/package.json with the lockfile updated, plus the new special-missing restore test.
1 finding above for review.
Checked, and the concern does not apply here - the compiler takes core from the project root, not from the CLI's tree.
The artifact built by
sasjs cbd -t serverat this head issasjsbuild/server.sas(single file, what gets deployed), plus the per-service copies. Both carry the 7.0.1 guard - the period-prefixed special-missing branch that does not exist in 6.1.4:node_modules/@sasjs/cli/node_modules/@sasjs/coreis 6.1.4 because the CLI declares@sasjs/core: ^6.0.0as its own dependency - that copy is the CLI's macro set, and no macro from it is inlined into a project build.The behavioural proof is the pair of runs already in the PR body: with 6.1.4 installed at the root,
restore.test.2fails on "revert was submitted", "reversion was approved" and "special missing was restored"; with 7.0.1 at the root it passes. Both runs were rebuilt withsasjs cbdimmediately beforehand, so what the deployed services executed changed with the root version and nothing else.If you would rather the nested copy were not there at all, that is a CLI change, not this one: it needs either an npm
overridespin forcing@sasjs/clionto a core it does not declare, or a CLI release on the 7.x line. Happy to raise it against@sasjs/clias a follow-up if you want the tree flattened.Two further commits on the branch, both found while proving the regression test, plus an issue for the one thing they do not fix.
20dbf00-mpe_executepassed a child's abort message through%str, so a comma in that message split its own abort call and the caller sawAll positional parameters must precede keyword parametersinstead of the child's failure.%bquotemasks the resolved value; the same shape inmpe_security_posteditis fixed too.dfc5c6e-stagedata.test.4test 8 called%mpe_executewithoutfailfast=0although the child is expected to abort, so the test cancelled itself before its assertion. The file's other abort-expecting calls already passed it.With those,
stagedata.test.4fails for exactly one reason: the child aborts inside the hook and writes no response, so the assertion has nothing to read. That is independent of the core bump - identical on 6.1.4 and 7.0.1 - and is now #411, with the log chain and the ruled-out causes.PR body updated with the two commits and the corrected suite statement (63 pass, 1 fail, tracked).