Paths versus literals
An operand starting with$ is a path: it is looked up in the situation. Anything else is a literal: used as written.
Reaching inside
Dots go into objects and lists. Lists are numbered from 0:Missing is null
A missing value behaves likenull:
existsandtruthyare false;emptyis true.- equals with a missing side is true only if the other is also missing.
- The greater/less comparisons are false.
- is one of is false if the list is missing; is not one of is true.
- contains and the text tests are false.
None of and missing values
None of flips whatever its inside says, including for missing data:- None of these rules pass:
- Compare values:
$record.statusequalsarchived
- Compare values:
status, the inside is false, so None of is true: the record passes. If that isn’t what you want, say it positively: $record.status is one of [open, draft].
Prefer does not equal to None of with an equals inside: they behave the same, and it’s clearer.
Types
Rules compare what the data holds:
Case-insensitive compare isn’t built in. Store a normalised (lowercase) column and compare against that, or use
matches with (?i).
Lists
contains works on lists ($auth.roles) and text. in takes the value on the left and the list on the right.
$auth.user_idis one of$record.member_ids(is the caller in the record’s list)$record.member_idscontains$auth.user_id(the same, the other way round)
Ownership across types
If a table’sowner_id is an integer and tokens carry a string, eq still works, because ids are normalised before they are compared.
The empty string
An empty text is empty (is empty true, is set false) but it exists. A form that submits an empty text for an optional field makes exists true. Use is set for “has a real value”.Patterns that bite
The third is the classic: when the caller has no organization and a record has no org, “both null” is equal, so everyone matches. Always require the caller’s side to be set.
Operators
Each operator’s behaviour.
Testing
Prove edge cases.