Link property challenges #8492
-
|
I'm running into a few challenges related to link properties in many-to-many relationships. Here is a minimal schema that demonstrate these challenges: The challenges are annotated in the code above with their numbers, e.g.
|
Beta Was this translation helpful? Give feedback.
Replies: 5 comments 10 replies
-
|
Figured out (3) and (4), by changing the trigger from: to: I'd appreciate some help with (1) and (2). |
Beta Was this translation helpful? Give feedback.
-
|
About (2) - rewrite mutations on link properties, I see that #6466 (merged) should show a more helpful message, yet I still see the original error: If rewrite mutations are not supported on link properties, are there any alternatives? I'd think the |
Beta Was this translation helpful? Give feedback.
-
|
I would recommend sticking with the intermediate object
Correct, triggers only activate when their object type is somehow used directly in a query. So when you run The misunderstanding here is what So I suggest the following schema: module default {
scalar type UserRole extending enum<Owner, Admin, Manager, Viewer>;
abstract type Timestamped {
required created_at: datetime {
readonly := true;
default := datetime_current();
}
required updated_at: datetime {
rewrite insert, update using (datetime_current());
}
}
type User extending Timestamped {
required name: str {
constraint exclusive;
}
required is_admin: bool {
default := false;
}
}
type Business extending Timestamped {
required name: str {
constraint exclusive;
}
# Backlinks
required multi users := assert_exists(.<business[is BusinessUser].user);
required multi business_users := assert_exists(.<business[is BusinessUser]);
# Trigger ensuring that `required` computed links are in fact created during insert
trigger check_users after insert for each do (
assert_exists(
# Do not use the computed link since accessing it will first activate
# the assertion that's part of the computed expression. Instead check
# that a BusinessUser has been linked by using the backlink directly,
# bypassing computed expression.
__new__.<business[is BusinessUser],
message := "A business must have exactly one owner."
)
)
}
type BusinessUser extending Timestamped {
required user_role: UserRole;
# Links
required business: Business {
on source delete allow;
};
required user: User {
on source delete allow;
};
# Constraints
constraint exclusive on ((.business, .user));
# Triggers
trigger ensure_single_owner after insert, update for each do (
assert(
count((
select BusinessUser filter .business = __new__.business and .user_role = UserRole.Owner
)) = 1,
message := "A business must have exactly one owner."
)
);
trigger ensure_single_owner_on_delete after delete, update for each do (
assert(
count((
select BusinessUser filter .business = __old__.business and .user_role = UserRole.Owner
)) = 1,
message := "A business must have exactly one owner."
)
);
}
} |
Beta Was this translation helpful? Give feedback.
-
|
To illustrate why an automatic check is not as straightforward as it sounds imagine the following schema: type HeartBeat {
required created_at: datetime {
readonly := true;
default := datetime_current();
}
}
type Foo {
required multi recent_beats := assert_exists(
(select HeartBeat filter .created_at > (datetime_current() - <duration>'10m')),
'HeartBeat lost for more than 10 minutes'
)
}There is nothing that can be checked when Hope this helps to understand why
|
Beta Was this translation helpful? Give feedback.
-
TL;DR Actually, coalescing them is probably not going to help you "consolidate" triggers in many cases, here's why: Just for fun, imagine that trigger xxx after insert, delete, update for each do (
assert(
all((
with
# Don't use coalesce, we need both potentially distinct businesses
B := {__old__, __new__}.business,
B_w_count := B {
# for each Business count the owners, don't use computeds to avoid asserts in them
num_owners := count((
select .<business[is BusinessUser]
filter .user_role = UserRole.Owner
)),
},
# check whether the count is 1 for each Business
select B_w_count.num_owners = 1
)),
# ^^^ The whole expression of having 1 owner is wrapped in `all` because it must be true for all Businesses.
# Luckily, the `business` link is `required` and so we can guarantee that `all` will never be applied to an empty set.
# This is good because `all({}) = True` and we'd need a separate check for emptiness if the links weren't required.
message := "A business must have exactly one owner."
)
);Arguably, the above consolidated expression is not "intuitive" or "obvious", and thus harder to read and write than two much simpler non-consolidated trigger expressions for |
Beta Was this translation helpful? Give feedback.
I would recommend sticking with the intermediate object
BusinessUserthat points to bothBusinessandUserbecause then you can keep theconstraint exclusive on ((.business, .user))that ensures that the same User cannot be associated with a Business twice.Correct, triggers only activate when their object type is somehow used directly in a query. So when you run
insert Business {name:="B1"};, only theBusinessobject triggers activate.The misunderstanding here is what
required