Repository navigation
propertyConstraints follow-ups after 4.2: previous values, more totals, shorthand #5122
Description
Basic explanation
propertyConstraints arrives in protocol version 14 (5.0). It lets a document type declare rules its documents must meet: arithmetic, comparisons, strings, identifiers, arrays, logic, system times and heights, and countOf / sumOf totals of other documents.
This issue collects the follow-ups. Protocol version 14 is still unreleased, so a follow-up can still land there by extending the current generation in place, as item 1 and the $revision reads of item 7 did. Once 14 ships, each one is a new generation selected by the next version's tables.
Follow-ups
1. Previous values on replace (done in protocol version 14)
Rules can compare a document's new version with its stored one. The stored version is read through $old. (the stored version):
- An
immutableentry with a condition (feat(platform)!: immutable properties frozen under a condition (PV14) #5217) freezes a property while the condition holds. "A counter only goes up" is{ "property": "counter", "when": { "lessThan": ["counter", "$old.counter"] } }. A violation isDocumentImmutablePropertyChangedError(40128), naming the property. - A
propertyConstraintsrule reads$old.too (feat(platform)!: propertyConstraints rules read the stored document ($old., $revision, PV14) #5373). It names the rule in the error (10422), can readcountOf/sumOftotals, and can read the stored owner, times, heights and revision ($old.$ownerId,$old.$updatedAt,$old.$revision). "A counter only goes up" is{ "greaterThanOrEqual": ["counter", { "ifAbsent": ["$old.counter", 0] }] }, "the price can only drop" is{ "lessThanOrEqual": ["price", { "ifAbsent": ["$old.price", 1000000] }] }, and "a closed listing never reopens" is{ "ifThen": [{ "equal": ["$old.status", { "const": "closed" }] }, { "equal": ["status", { "const": "closed" }] }] }(on a create$old.statusis absent and equals no string).
The open points are settled:
- On a create there is no stored version. A property under
$old.reads as absent,$old.$ownerIdequals no identifier, and a stored time, height or revision reads 0.ifAbsentandpresent/absenthandle it. - On a transfer, purchase or price update,
$old.is the document before the write, whose properties are the same. Such a write is judged only against the rules that read what it changes (the owner, the transfer or update time and heights,$revision, or an owner-dependent total), of either version. - On a moderator's change of fields,
$old.is the stored document, and only the rules reading a changed field,$revisionor a total are judged. On a moderator's restore, only the rules reading a total are judged, with$old.reading the restored document. - SDK pre-checks. JS
checkDocumentPropertyConstraints(document, storedDocument)takes the stored document for a replace andnullfor a create; withundefinedit leaves the rules reading$old.or$revisionunjudged, since the revision depends on the write. The FFI create check (Swift, Kotlin) judges$old.as absent. Rule listings reportreadsStored.
2. Unique indexes for totals
The first cut (#5109) refuses to answer a countOf or sumOf from a unique index. I could not confirm how a unique countable index stores its count: its terminal is a bare reference rather than a count tree.
Once that layout is verified (count_value_or_default gives 1 for a reference), the fix is to allow it. That enables "the owner has a profile" checks on the common unique-by-owner profile type: countOf ["profile", { "$ownerId": "$ownerId" }] >= 1.
3. Range filters
One filter key may take a comparison instead of a value.
"outbidsEveryone": {
"equal": [{ "countOf": ["bid", { "auctionId": "auctionId", "amount": { "greaterThanOrEqual": "amount" } }] }, 1]
}This reads: of the bids on this auction, the only one at or above this bid is this bid itself.
-
Comparisons: the existing four:
greaterThan,greaterThanOrEqual,lessThan,lessThanOrEqual. The bound is read from the document being written, like any filter value. -
Index: exactly one range key, which must be the index's last property. The other keys must be exactly its other properties, and the index must be
rangeCountable(for acountOf) orrangeSummable(for asumOf). That is the shape Drive's own range queries require. -
Cost: O(log n) through grovedb's
AggregateCountOnRange(query_aggregate_count), which Drive's no-proof range count already calls. The consensus read would keep its cost and bill it. -
The written document: it counts only if its own value meets the comparison. With
greaterThanOrEquala bid counts itself, and withgreaterThanit never does. The docs must spell this out. -
SDKs:
readsTotalsentries would say which key is the range and with which comparison. -
Extension, rate limits: "at most 5 posts per owner in the last 24 hours" needs two more things:
- a
$createdAtkey in the filter (today only$ownerIdis allowed as a system key); - a computed bound:
{ "greaterThan": { "subtract": ["$createdAt", 86400000] } }.
It would run over a
[$ownerId, $createdAt]index withrangeCountable. - a
4. Totals over another contract's types
Deferred from the first cut. It needs:
- a billed contract fetch, billed from the deterministic
.0FeeResultand never the cached fee; - registration checks against the other contract's types and indexes (their count and sum flags cannot change on an update);
- a decision on which version of the other contract a rule is judged against.
5. Pre-check reporting what it skipped
The SDK check (checkDocumentPropertyConstraints in JS; dash_sdk_data_contract_check_property_constraints under Swift and Kotlin) reads no state. It leaves rules reading a total or a block height unjudged, and reports "no violation" for them.
It could also return the names of the rules it could not judge, so an app can warn before sending instead of relying on readsTotals / readsSystem.
6. Contested types
A type with a contested index cannot total its own documents (#5115):
- a contested create waits in the contest's own storage, outside the count trees;
- the document a contest awards is stored without any rule judged.
Judging the rules when a contest awards its document would let such types in. That requires deciding what happens when the award would break a rule.
7. Small shorthand
oneOf/atLeast N of/atMost N of: "exactly one ofpriceDashorpriceTokenis set". Written withanyOf/allOf/nottoday, which grows fast.- Substring
includes: alongsidestartsWith/endsWith, e.g. "a note never containshttp". $creatorIdreads: e.g. "the seller is still the creator after a transfer". ($revisionreads landed in feat(platform)!: propertyConstraints rules read the stored document ($old., $revision, PV14) #5373: "at most 5 edits" is{ "lessThanOrEqual": ["$revision", 6] }.)
Housekeeping
Left from the #5109 review:
AggregateKeyKind::ofduplicates the property-kind checks inapply_property_constraints_v0;- the aggregates accessor, setter and a static empty map are copied into five action modules;
- boxing the totals leaves
BatchedTransitionAction::DocumentActionexactly at clippy's 200-bytelarge_enum_variantmargin, so the next field on the largest document action trips it again; - filter values are serialized three times per read (the check in
filter_values,contribution, and Drive's path-query builder).
Shipped in 4.2 (protocol version 14)
#4962 and the operator series through #5100, totals in #5109 and #5115, and the SDK updates through #5121.
Activity