The masking language

A policy for masking columns as rows are read, in S-expressions. quiver types it against every column before anything is read, compiles it and runs it on its render threads.

quiver copy … --mask-script policy.scm --mask-secret "$MASK_SECRET"
quiver copy … --mask 'shop.users.inn=expr:(hash-int value)'
quiver mask --mask-script policy.scm '(phone value)' '+7 (912) 345-67-89'

Values

A column of text, binary, JSON, ENUM, SET or DECIMAL is text; one of integers an integer; DATE a date; DATETIME and TIMESTAMP a time; other types take sql: instead. Every kind has NULL, which goes through functions as in SQL. A value given to a column of another kind, NULL in a NOT NULL column, or a time finer than its column stops the copy.

Forms

FormMeaning
valueThe column's value in this row.
(row COLUMN)Another column of the row, as read, before any masking.
"text" 42 true false nullConstants.
(if CONDITION THEN ELSE)THEN when CONDITION is true, ELSE when it is false or NULL.
(cond (CONDITION VALUE) … (else VALUE))The first VALUE whose CONDITION is true.
(let ((NAME VALUE) …) BODY)Names, each seeing those before it.
(define (NAME PARAMETER …) BODY)A function, calling only those defined above it: nothing recurses.
(mask SELECTOR BODY)The columns SELECTOR takes get BODY's value; a column takes the first rule naming it, from --mask first, then the scripts in order.
(keep SELECTOR)Its columns are copied as they are, and count as decided.
(permute SELECTOR [LOW HIGH])A keyed bijection of its columns, as --mask DB.TABLE.COLUMN=permute: keys stay keys, and every column a foreign key ties to one of them follows unless a rule names it. LOW and HIGH bound the integers.
(references "DB.TABLE.COLUMN" "DB.TABLE.COLUMN")The first column refers to the second though no foreign key says so: a permute of either spreads to the other, and masking them differently is an error.
(strict "DB.TABLE" …)Every column of these tables must be masked or kept by a rule; otherwise nothing is read, and the message names the columns.
(words NAME "FILE")The lines of FILE, beside the script, for pick-word; empty lines and lines starting with # are left out.
(after-load "DB.TABLE" "STATEMENT")Run on the target by copy once the rows and keys are in, {table} standing for the table there.

A SELECTOR is "DB.TABLE.COLUMN" with * and ?, names in backquotes taken as written, or (where (table "DB.TABLE") (column "NAME") (type TYPE …)) with any clause left out; TYPE is one of text, binary, json, integer, decimal, float, date, datetime, timestamp, time, year, enum, set, geometry.

Functions

Text

Characters are code points: Cyrillic, accents and emoji count one each.

(concat TEXT …) → text

Joins its texts.

ExpressionvalueGives
(concat "user" "-" (str 42))NULLuser-42

(length TEXT) → integer

How many characters.

ExpressionvalueGives
(length value)Иван text4

(substring TEXT START COUNT) → text

COUNT characters from START, counted from 0; what lies outside is left out.

ExpressionvalueGives
(substring value 2 3)Иван Петров textан

(left TEXT COUNT) → text

The first COUNT characters.

ExpressionvalueGives
(left value 3)Иван Петров textИва

(right TEXT COUNT) → text

The last COUNT characters.

ExpressionvalueGives
(right value 5)+7 (912) 345-67-89 text67-89

(upper TEXT) → text

Latin and Cyrillic letters in upper case.

ExpressionvalueGives
(upper value)ёлка Tree textЁЛКА TREE

(lower TEXT) → text

Latin and Cyrillic letters in lower case.

ExpressionvalueGives
(lower value)ЁЛКА Tree textёлка tree

(title TEXT) → text

Each word's first letter in upper case, the others in lower.

ExpressionvalueGives
(title value)иВАН петров-водкин textИван Петров-Водкин

(trim TEXT) → text

Without the spaces at either end.

ExpressionvalueGives
(trim value) a b texta b

(reverse TEXT) → text

The characters back to front.

ExpressionvalueGives
(reverse value)абв textвба

(replace TEXT FROM TO) → text

Every FROM, left to right, replaced by TO; an empty FROM changes nothing.

ExpressionvalueGives
(replace value "-" "")345-67-89 text3456789

(before TEXT SEPARATOR) → text

What comes before the first SEPARATOR; the whole text when there is none.

ExpressionvalueGives
(before value "@")anna@mail.ru textanna

(after TEXT SEPARATOR) → text

What comes after the first SEPARATOR; empty when there is none.

ExpressionvalueGives
(after value "@")anna@mail.ru textmail.ru

(before-last TEXT SEPARATOR) → text

As before, at the last SEPARATOR.

ExpressionvalueGives
(before-last value ".")a.b.c texta.b

(after-last TEXT SEPARATOR) → text

As after, at the last SEPARATOR.

ExpressionvalueGives
(after-last value ".")a.b.c textc

(split-part TEXT SEPARATOR N) → text

The N-th field between SEPARATORs, from 1; empty past the last.

ExpressionvalueGives
(split-part value "," 2)a,b,c textb

(index-of TEXT PART) → integer

Where PART first starts, in characters from 0; -1 when it is absent, 0 when it is empty.

ExpressionvalueGives
(index-of value "Пе")Иван Петров text5

(pad-left TEXT WIDTH FILL) → text

FILL's first character put in front until the text is WIDTH characters long (at most 65535).

ExpressionvalueGives
(pad-left value 5 "0")42 text00042

(pad-right TEXT WIDTH FILL) → text

As pad-left, at the end.

ExpressionvalueGives
(pad-right value 5 ".")42 text42...

(repeat TEXT N) → text

N copies, at most 65535; empty past one mebibyte.

ExpressionvalueGives
(repeat "ab" 3)NULLababab

(format TEMPLATE [TEXT …]) → text

Each {} of TEMPLATE replaced by the next text, in order; the {} left over stay.

ExpressionvalueGives
(format "{}-{}" "a" "b")NULLa-b

(translit TEXT) → text

Cyrillic written in Latin letters, the rest kept.

ExpressionvalueGives
(translit value)Щука Ёжикова textShchuka Yozhikova

(only-digits TEXT) → text

Only the digits 0 to 9.

ExpressionvalueGives
(only-digits value)+7 (912) 345 text7912345

(redact TEXT LEFT RIGHT) → text

Each letter and digit but the first LEFT and the last RIGHT characters becomes *; spaces and punctuation stay.

ExpressionvalueGives
(redact value 2 2)Иван Петров textИв** ****ов

(str NUMBER) → text

An integer written in decimal.

ExpressionvalueGives
(str (+ value 1))41 integer42

(int TEXT) → integer

Decimal digits with an optional leading -, spaces around allowed, as an integer; NULL otherwise.

ExpressionvalueGives
(int value) 42 text42

(starts-with TEXT PREFIX) → truth

Whether the text starts with PREFIX.

ExpressionvalueGives
(starts-with value "+7")+7 912 texttrue

(ends-with TEXT SUFFIX) → truth

Whether the text ends with SUFFIX.

ExpressionvalueGives
(ends-with value ".ru")a@mail.ru texttrue

(contains TEXT PART) → truth

Whether PART is in the text.

ExpressionvalueGives
(contains value "@")a@b texttrue

(like TEXT PATTERN) → truth

SQL LIKE, with case: % any characters, _ one, a backslash makes the next one plain.

ExpressionvalueGives
(like value "%@%.ru")a@mail.ru texttrue

(luhn DIGITS) → text

The Luhn check digit of the text's digits, which card numbers end with.

ExpressionvalueGives
(luhn value)7992739871 text3

Numbers

Integers of 64 bits. A function given NULL gives NULL.

(+ A B …) → integer

The sum.

ExpressionvalueGives
(+ value 1)41 integer42

(- A [B]) → integer

A minus B, or minus A alone.

ExpressionvalueGives
(- value 7)49 integer42
(- value)42 integer-42

(* A B …) → integer

The product.

ExpressionvalueGives
(* value 2)21 integer42

(/ A B) → integer

The quotient, toward zero; NULL when B is 0.

ExpressionvalueGives
(/ value 2)85 integer42
(/ value 0)85 integer\N

(mod A B) → integer

The remainder, with A's sign; NULL when B is 0.

ExpressionvalueGives
(mod value 10)142 integer2

(abs A) → integer

Without its sign.

ExpressionvalueGives
(abs value)-42 integer42

(min VALUE …) → any

The least of values of one kind.

ExpressionvalueGives
(min value 100)42 integer42

(max VALUE …) → any

The greatest of values of one kind.

ExpressionvalueGives
(max value 0)-5 integer0

(round-to N STEP) → integer

To the nearest multiple of STEP, halves away from zero; NULL when STEP is not positive.

ExpressionvalueGives
(round-to value 1000)123456 integer123000

(floor-to N STEP) → integer

Down to a multiple of STEP.

ExpressionvalueGives
(floor-to value 10)-1 integer-10

(clamp N LOW HIGH) → integer

Kept between LOW and HIGH; NULL when LOW is above HIGH.

ExpressionvalueGives
(clamp value 0 100)150 integer100

Dates and times

A DATE column is a date, DATETIME and TIMESTAMP a date with a time. Years run from 0000 to 9999; what would leave them is NULL. A time finer than its column stops the copy.

(year DATE) → integer

The year.

ExpressionvalueGives
(year value)1985-07-14 date1985

(month DATE) → integer

The month, from 1.

ExpressionvalueGives
(month value)1985-07-14 date7

(day DATE) → integer

The day of the month.

ExpressionvalueGives
(day value)1985-07-14 date14

(make-date YEAR MONTH DAY) → date

The date; NULL for a day that does not exist.

ExpressionvalueGives
(make-date 2024 2 29)NULL2024-02-29
(make-date 2023 2 29)NULL\N

(add-days DATE DAYS) → date

So many days later, or earlier when negative.

ExpressionvalueGives
(add-days value 1)2000-02-28 date2000-02-29

(days-between FROM TO) → integer

Days from FROM to TO.

ExpressionvalueGives
(days-between value (make-date 2000 3 1))2000-01-01 date60

(date->text DATE) → text

YYYY-MM-DD.

ExpressionvalueGives
(date->text value)1985-07-14 date1985-07-14

(text->date TEXT) → date

A date from YYYY-MM-DD; NULL otherwise.

ExpressionvalueGives
(text->date value)1985-07-14 text1985-07-14

(date-of TIME) → date

The day of a time.

ExpressionvalueGives
(date-of value)2024-03-01 13:45:10 time2024-03-01

(seconds-of-day TIME) → integer

Seconds since that day's midnight.

ExpressionvalueGives
(seconds-of-day value)2024-03-01 13:45:10 time49510

(make-datetime DATE SECONDS) → time

That day, SECONDS after midnight, from 0 to 86399.

ExpressionvalueGives
(make-datetime value 3600)2024-03-01 date2024-03-01 01:00:00

(add-seconds TIME SECONDS) → time

So many seconds later.

ExpressionvalueGives
(add-seconds value 90)2024-03-01 23:59:00 time2024-03-02 00:00:30

(datetime->text TIME) → text

YYYY-MM-DD HH:MM:SS, then the fraction of a second if any.

ExpressionvalueGives
(datetime->text value)2024-03-01 13:45:10 time2024-03-01 13:45:10

(text->datetime TEXT) → time

A time from YYYY-MM-DD, HH:MM:SS and up to six fractional digits; NULL otherwise.

ExpressionvalueGives
(text->datetime value)2024-03-01 13:45:10.5 text2024-03-01 13:45:10.500000

Logic

NULL compares to nothing; and, or and if take it as false.

(= A B) → truth

Whether A equals B.

ExpressionvalueGives
(= value "a")a texttrue

(<> A B) → truth

Whether A differs from B.

ExpressionvalueGives
(<> value 2)1 integertrue

(< A B) → truth

Whether A comes before B: text by bytes, numbers and dates by value.

ExpressionvalueGives
(< value 3)3 integerfalse

(<= A B) → truth

Whether A comes before B or equals it.

ExpressionvalueGives
(<= value 3)3 integertrue

(> A B) → truth

Whether A comes after B.

ExpressionvalueGives
(> value "a")b texttrue

(>= A B) → truth

Whether A comes after B or equals it.

ExpressionvalueGives
(>= value 3)2 integerfalse

(and TRUTH …) → truth

Whether all are true.

ExpressionvalueGives
(and (> value 0) (< value 10))5 integertrue

(or TRUTH …) → truth

Whether one is true.

ExpressionvalueGives
(or (< value 0) (> value 10))50 integertrue

(not TRUTH) → truth

The opposite.

ExpressionvalueGives
(not (is-null value))x texttrue

(is-null VALUE) → truth

Whether the value is NULL.

ExpressionvalueGives
(is-null value)NULLtrue

(coalesce VALUE …) → any

The first value that is not NULL.

ExpressionvalueGives
(coalesce value "none")NULLnone

Keyed

Keyed by --mask-secret with HMAC-SHA256: under one key a seed always gives the same result, in every column and every run, so masked keys still join; without the key the result tells nothing of the seed. The examples are under the key of the secret "example".

(shape TEXT) → text

Each letter and digit replaced by one of its kind (Latin, Cyrillic, digit), any other letter by a Latin one, the rest kept: as many characters, never more bytes.

ExpressionvalueGives
(shape value)+7 (912) 345-67-89 text+9 (331) 996-11-65

(email TEXT) → text

shape before the last @, the domain kept.

ExpressionvalueGives
(email value)anna.petrova@mail.ru textutlr.icmqljt@mail.ru

(hash TEXT) → text

As many hex digits as the text has characters.

ExpressionvalueGives
(hash value)Иван text39b4

(hash-bytes BYTES) → text

As many bytes as the value: for binary columns.

ExpressionvalueGives
(length (hash-bytes value))abc text3

(hash-int NUMBER) → integer

Any 64-bit integer.

ExpressionvalueGives
(hash-int value)42 integer-7709159480299872343

(keyed-int SEED LOW HIGH) → integer

An integer from LOW to HIGH.

ExpressionvalueGives
(keyed-int value 18 90)Иван text73

(keyed-date SEED FROM TO) → date

A date from FROM to TO.

ExpressionvalueGives
(keyed-date value (make-date 1985 1 1) (make-date 1985 12 31))id-1 text1985-03-29

(keyed-noise N PERCENT) → integer

N moved by up to PERCENT of itself either way, PERCENT from 0 to 100.

ExpressionvalueGives
(keyed-noise value 10)100000 integer92386

(keyed-uuid SEED) → text

A version 4 UUID.

ExpressionvalueGives
(keyed-uuid value)a text33e6f143-b8f1-4bcc-ba78-9920cc303b7f

(digits SEED COUNT) → text

COUNT decimal digits, at most 65535.

ExpressionvalueGives
(digits value 4)Иван text1463

(pick SEED CHOICE …) → any

One of the choices.

ExpressionvalueGives
(pick value "Анна" "Иван" "Олег")x textИван

(pick-word SEED WORDS) → text

One line of a (words NAME "FILE") list, named here without quotes.