Compare commits
1
Commits
a1513b871e
...
35c60941da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35c60941da |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,112 @@
|
|||||||
|
---
|
||||||
|
title: 'One Table, Two Rule Sets'
|
||||||
|
description: 'A single physical table can carry only one set of Data Controller validation rules. Two librefs over the same data - or a pair of hook scripts - give you as many rule sets as you need.'
|
||||||
|
date: '2026-09-24 15:30:00'
|
||||||
|
author: 'Data Controller'
|
||||||
|
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||||
|
tags:
|
||||||
|
- Data Quality
|
||||||
|
- Configuration
|
||||||
|
previewImg: './rule-set-1.png'
|
||||||
|
---
|
||||||
|
|
||||||
|
# One Table, Two Rule Sets
|
||||||
|
|
||||||
|
Most Data Controller sites settle into an obvious mapping: one table, one edit screen, one set of validation rules. But that is not always what the business wants. A table of orders might be edited from a finance report that only tolerates small adjustments, and from an operations report where much larger ones are routine. Same table, same columns, same approvers - different rules.
|
||||||
|
|
||||||
|
Data Controller's validation rules are configured per table, so this takes a little thought. There are two ways to do it: the one we recommend, and the one to reach for when the first is not available.
|
||||||
|
|
||||||
|
## Why one table is one rule set
|
||||||
|
|
||||||
|
Two configuration tables decide this.
|
||||||
|
|
||||||
|
`MPE_TABLES` is the list of editable tables, and its primary key is `(tx_from, libref, dsn)`. One physical table is one editable table.
|
||||||
|
|
||||||
|
`MPE_VALIDATIONS` holds the rules, and its primary key is `(tx_from, base_lib, base_ds, base_col, rule_type)`. Rules hang off a physical `libref.dataset`. There is no per-menu or per-report scoping anywhere in the schema, and the editor is handed exactly the rules whose `base_lib` and `base_ds` match the table being opened.
|
||||||
|
|
||||||
|
So two rule sets on one table need two distinct `libref.dataset` identities. The question is how to get them without copying the data.
|
||||||
|
|
||||||
|
## Option 1 (recommended): two librefs over the same data
|
||||||
|
|
||||||
|
A libref is just a name pointing at a location. Nothing stops you assigning two of them to the same place, and Data Controller will treat the two as separate tables:
|
||||||
|
|
||||||
|
```sas
|
||||||
|
libname ORDERS_EU '/data/orders';
|
||||||
|
libname ORDERS_US '/data/orders';
|
||||||
|
```
|
||||||
|
|
||||||
|
`ORDERS_EU.ORDERS` and `ORDERS_US.ORDERS` are now the same physical file, but they are different rows in `MPE_TABLES` and can therefore carry different rows in `MPE_VALIDATIONS`. Register both, give each its own rules, and point each report at its own editor URL - `#/editor/ORDERS_EU.ORDERS` and `#/editor/ORDERS_US.ORDERS`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Everything else works exactly as it always did. Filtering, search, the row cap, the approval diff and the audit trail all operate on the table as normal, because as far as Data Controller is concerned these are ordinary tables. The only difference is that the two names resolve to the same file, so an approval in either report updates the same data.
|
||||||
|
|
||||||
|
There is no copy to keep in sync, no hook to write and nothing to maintain. That is why it is the option we recommend.
|
||||||
|
|
||||||
|
### Things worth knowing
|
||||||
|
|
||||||
|
- Where the librefs are defined depends on your platform. On Viya, in the compute context's `autoexec.sas` (or `[DC Drive Path]/services/settings.sas`); on SAS 9, as metadata libraries or in the Data Controller Settings stored process; on SASjs Server, in `services/public/settings.sas`. The one requirement is that each library has a unique libref.
|
||||||
|
- `mp_lockanytable` keys on `libref.dataset`, so the two menus do not serialise against each other. Two people editing through different reports at the same moment can therefore collide at the database level. If that matters, add an explicit shared lock in a `PRE_APPROVE_HOOK`.
|
||||||
|
- The audit trail and approval queue record which libref a change came through, so `ORDERS_EU.ORDERS` and `ORDERS_US.ORDERS` stay distinguishable in history. For most people that is a feature - you can see which report a change originated from.
|
||||||
|
|
||||||
|
## Option 2: an empty mirror and a pair of hook scripts
|
||||||
|
|
||||||
|
Sometimes two librefs over one location are not available: a database library where the platform will not let you define the same object twice, or a site where adding a library definition is a change nobody wants to make. Then you can reach the same result with a mirror table and two hook scripts.
|
||||||
|
|
||||||
|
The idea is that the thing Data Controller edits is not the real table at all, but an empty table of the same shape, with hooks moving data in and out of it:
|
||||||
|
|
||||||
|
- a `PRE_EDIT_HOOK` fills the editor with the live rows of the real table, so the mirror never has to hold a copy
|
||||||
|
- a `POST_EDIT_HOOK` re-points the submitted changeset at the real table, so the approval is raised against the real table and the load writes there
|
||||||
|
|
||||||
|
The mirror exists purely to carry the rule set, and never stores any data.
|
||||||
|
|
||||||
|
### The pre-edit hook
|
||||||
|
|
||||||
|
`PRE_EDIT_HOOK` runs inside the `getdata` service, after the user's filter has been applied and the rows sorted, with the data in `work.OUT`. It may replace that dataset, which is all this needs:
|
||||||
|
|
||||||
|
```sas
|
||||||
|
data work.out;
|
||||||
|
set ORDERS.ORDERS;
|
||||||
|
run;
|
||||||
|
```
|
||||||
|
|
||||||
|
The registered table is `ORDERS.MIRROR`, which is empty - so without the hook the editor would show nothing at all. With it, the grid shows the live rows:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Note the title bar: the mirror really is empty. Everything on screen came from the hook, and the grid is validated against the mirror's own rules rather than the real table's.
|
||||||
|
|
||||||
|
### The post-edit hook
|
||||||
|
|
||||||
|
This is the part that surprises people. `POST_EDIT_HOOK` runs inside the `mpe_loader` macro at submit time, on the staged rows, before the submit record is written. It cannot choose the target table directly - but at that point `LIBREF` and `DS` are still ordinary macro variables, and the submit record is built from them. Reassigning them re-points the whole changeset:
|
||||||
|
|
||||||
|
```sas
|
||||||
|
data _null_;
|
||||||
|
call symputx('libref','ORDERS');
|
||||||
|
call symputx('ds','ORDERS');
|
||||||
|
run;
|
||||||
|
```
|
||||||
|
|
||||||
|
From that moment the changeset is an approval against `ORDERS.ORDERS`. The approver sees a diff against the real table, the load writes to the real table, and the mirror is never touched.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### The detail that will bite you
|
||||||
|
|
||||||
|
Use `call symputx`, not `%let`. `LIBREF` and `DS` are not declared `%local` in `mpe_loader`, and the hook is included into that scope - so `call symputx` finds the existing variable and updates it, while a `%let` creates a new variable in the hook's own scope and is silently discarded. The hook runs, the log looks clean, and the changeset goes to the mirror anyway.
|
||||||
|
|
||||||
|
### Other things to watch
|
||||||
|
|
||||||
|
- The filter has already been applied to the empty mirror by the time the pre-edit hook runs, so a hook that reads the real table ignores the user's filter unless it re-applies it (`where %inc filtref`). On a small table you will not notice; on a large one the `DC_MAXOBS_WEBEDIT` cap will stop the edit screen with "Table is too big".
|
||||||
|
- The hook's output must have the same columns the editor expects - the real table, minus any transaction or processing columns that Data Controller drops on load.
|
||||||
|
- The real table must itself be registered in `MPE_TABLES`. The approval screen resolves the table's audit settings from that row, so a changeset routed to a table with no registration cannot be reviewed - the submit is refused up front, naming the table.
|
||||||
|
- The mirror's `MPE_TABLES` row is read for the edit screen and the real table's for the load, so keep their `buskey`, `loadtype` and temporal column settings identical.
|
||||||
|
- At approval time the access checks run against the real table, so editors need `EDIT` on the mirror while approvers need `EDIT` and `APPROVE` on the real table.
|
||||||
|
|
||||||
|
## Which should you use?
|
||||||
|
|
||||||
|
If you can define two librefs over the same data, do that. It is configuration only, it leaves every other behaviour of the editor untouched, and there is nothing to maintain.
|
||||||
|
|
||||||
|
Reach for the hook scripts when the platform will not let you duplicate the library definition, or when you specifically want the rule set to be a property of the application rather than of the data.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Reference in New Issue
Block a user