Skip to content

Commit fc4ea5d

Browse files
Add a .?>link operator to dereference link while suppressing "hidden by policy" (#9101)
Users (and us) occasionally have problems dealing with "required link hidden by access policy" errors. They can be worked around often by using backlinks and an explicit join, but that that is kind of horrible and seems to sometimes be slow. Issue #8522 proposes adding an intrinsic function to mask the error, but I really hate that. I don't think that a simple function call ought to be impacting the semantics of the expression contained within, and I think it's really unclear what its behavior would be in more complex cases. Instead, add an operator, `Obj.?>link` that does an *optional* dereference of `link`. The result is always optional, and it suppresses any "hidden by policy" error. I'm happy to argue about syntax if anyone has something they like more. I also considered `.?` but that was worse. Closes #8522, but not in the way proposed there. --------- Co-authored-by: Aljaž Mur Eržen <aljazerzen@users.noreply.github.com>
1 parent 1680013 commit fc4ea5d

File tree

14 files changed

+118
-17
lines changed

14 files changed

+118
-17
lines changed

edb/edgeql-parser/edgeql-parser-python/src/normalize.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -215,10 +215,10 @@ fn is_operator(token: &Token) -> bool {
215215
use edgeql_parser::tokenizer::Kind::*;
216216
match token.kind {
217217
Assign | SubAssign | AddAssign | Arrow | Coalesce | Namespace | DoubleSplat
218-
| BackwardLink | FloorDiv | Concat | GreaterEq | LessEq | NotEq | NotDistinctFrom
219-
| DistinctFrom | Comma | OpenParen | CloseParen | OpenBracket | CloseBracket
220-
| OpenBrace | CloseBrace | Dot | Semicolon | Colon | Add | Sub | Mul | Div | Modulo
221-
| Pow | Less | Greater | Eq | Ampersand | Pipe | At => true,
218+
| BackwardLink | OptionalLink | FloorDiv | Concat | GreaterEq | LessEq | NotEq
219+
| NotDistinctFrom | DistinctFrom | Comma | OpenParen | CloseParen | OpenBracket
220+
| CloseBracket | OpenBrace | CloseBrace | Dot | Semicolon | Colon | Add | Sub | Mul
221+
| Div | Modulo | Pow | Less | Greater | Eq | Ampersand | Pipe | At => true,
222222
DecimalConst | FloatConst | IntConst | BigIntConst | BinStr | Parameter
223223
| ParameterAndType | Str | BacktickName | Keyword(_) | Ident | Substitution | EOI
224224
| Epsilon | StartBlock | StartExtension | StartFragment | StartMigration

edb/edgeql-parser/src/parser/spec.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ pub(super) fn get_token_kind(token_name: &str) -> Kind {
6767
"&" => Ampersand,
6868
"@" => At,
6969
".<" => BackwardLink,
70+
".?>" => OptionalLink,
7071
"}" => CloseBrace,
7172
"]" => CloseBracket,
7273
")" => CloseParen,

edb/edgeql-parser/src/tokenizer.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ pub enum Kind {
7878
Coalesce, // ??
7979
Namespace, // ::
8080
BackwardLink, // .<
81+
OptionalLink, // .?>
8182
FloorDiv, // //
8283
Concat, // ++
8384
GreaterEq, // >=
@@ -356,6 +357,16 @@ impl<'a> Tokenizer<'a> {
356357
},
357358
'.' => match iter.next() {
358359
Some((_, '<')) => Ok((BackwardLink, 2)),
360+
Some((_, '?')) => {
361+
if let Some((_, '>')) = iter.next() {
362+
Ok((OptionalLink, 3))
363+
} else {
364+
Err(Error::new(
365+
"`.?` is not an operator, \
366+
did you mean `.?>` ?",
367+
))
368+
}
369+
}
359370
_ => Ok((Dot, 1)),
360371
},
361372
'?' => match iter.next() {
@@ -1063,6 +1074,7 @@ impl Kind {
10631074
Ampersand => "&",
10641075
At => "@",
10651076
BackwardLink => ".<",
1077+
OptionalLink => ".?>",
10661078
CloseBrace => "}",
10671079
CloseBracket => "]",
10681080
CloseParen => ")",

edb/edgeql/ast.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,9 @@ class TypeIntersection(Base):
406406
class Ptr(Base):
407407
name: str
408408
direction: typing.Optional[str] = None
409-
type: typing.Optional[str] = None
409+
# @ptr has type 'property'
410+
# .?>ptr has type 'optional'
411+
type: typing.Optional[typing.Literal['optional', 'property']] = None
410412

411413

412414
class Splat(Base):

edb/edgeql/codegen.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,8 @@ def _visit_shape(
600600
def visit_Ptr(self, node: qlast.Ptr, *, quote: bool = True) -> None:
601601
if node.type == 'property':
602602
self.write('@')
603+
elif node.type == 'optional':
604+
self.write('?>')
603605
elif node.direction and node.direction != '>':
604606
self.write(node.direction)
605607

edb/edgeql/compiler/inference/cardinality.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,10 @@ def _infer_set_inner(
629629

630630
card = cartesian_cardinality((source_card, rptrref_card))
631631

632+
# "Optional derefs" (.?>) always produce an optional result.
633+
if ptr.optional_deref:
634+
card = cartesian_cardinality((AT_MOST_ONE, card))
635+
632636
elif sub_expr is not None:
633637
card = expr_card
634638
else:

edb/edgeql/compiler/setgen.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -621,12 +621,13 @@ def compile_path(expr: qlast.Path, *, ctx: context.ContextLevel) -> irast.Set:
621621
direction=direction,
622622
upcoming_intersections=upcoming_intersections,
623623
ignore_computable=True,
624+
optional_deref=step.type == 'optional',
624625
span=step.span, ctx=ctx)
625626

626627
assert isinstance(path_tip.expr, irast.Pointer)
627628
ptrcls = typegen.ptrcls_from_ptrref(
628629
path_tip.expr.ptrref, ctx=ctx)
629-
if _is_computable_ptr(ptrcls, direction, ctx=ctx):
630+
if _is_computable_ptr(ptrcls, path_tip.expr, ctx=ctx):
630631
is_computable = True
631632

632633
elif isinstance(step, qlast.TypeIntersection):
@@ -815,6 +816,7 @@ def ptr_step_set(
815816
direction: PtrDir = PtrDir.Outbound,
816817
span: Optional[qlast.Span],
817818
ignore_computable: bool = False,
819+
optional_deref: bool = False,
818820
ctx: context.ContextLevel,
819821
) -> irast.Set:
820822
ptrcls, path_id_ptrcls = resolve_ptr_with_intersections(
@@ -829,7 +831,9 @@ def ptr_step_set(
829831
return extend_path(
830832
path_tip, ptrcls, direction,
831833
path_id_ptrcls=path_id_ptrcls,
832-
ignore_computable=ignore_computable, span=span,
834+
ignore_computable=ignore_computable,
835+
optional_deref=optional_deref,
836+
span=span,
833837
ctx=ctx)
834838

835839

@@ -1116,6 +1120,7 @@ def extend_path(
11161120
path_id_ptrcls: Optional[s_pointers.Pointer] = None,
11171121
ignore_computable: bool = False,
11181122
same_computable_scope: bool = False,
1123+
optional_deref: bool = False,
11191124
span: Optional[qlast.Span]=None,
11201125
ctx: context.ContextLevel,
11211126
) -> irast.SetE[irast.Pointer]:
@@ -1169,11 +1174,12 @@ def extend_path(
11691174
direction=direction,
11701175
ptrref=typegen.ptr_to_ptrref(ptrcls, ctx=ctx),
11711176
is_definition=False,
1177+
optional_deref=optional_deref,
11721178
)
11731179
target_set = new_set(
11741180
stype=target, path_id=path_id, span=span, expr=ptr, ctx=ctx)
11751181

1176-
is_computable = _is_computable_ptr(ptrcls, direction, ctx=ctx)
1182+
is_computable = _is_computable_ptr(ptrcls, ptr, ctx=ctx)
11771183
if not ignore_computable and is_computable:
11781184
target_set = computable_ptr_set(
11791185
ptr,
@@ -1189,7 +1195,7 @@ def extend_path(
11891195

11901196
def needs_rewrite_existence_assertion(
11911197
ptrcls: s_pointers.PointerLike,
1192-
direction: PtrDir,
1198+
rptr: irast.Pointer,
11931199
*,
11941200
ctx: context.ContextLevel,
11951201
) -> bool:
@@ -1201,8 +1207,10 @@ def needs_rewrite_existence_assertion(
12011207

12021208
return bool(
12031209
not ctx.suppress_rewrites
1210+
# We *don't* need to do the rewrite when using .?>
1211+
and not rptr.optional_deref
12041212
and ptrcls.get_required(ctx.env.schema)
1205-
and direction == PtrDir.Outbound
1213+
and rptr.direction == PtrDir.Outbound
12061214
and (target := ptrcls.get_target(ctx.env.schema))
12071215
and ctx.env.type_rewrites.get((target, False))
12081216
and ptrcls.get_shortname(ctx.env.schema).name != '__type__'
@@ -1211,7 +1219,7 @@ def needs_rewrite_existence_assertion(
12111219

12121220
def is_injected_computable_ptr(
12131221
ptrcls: s_pointers.PointerLike,
1214-
direction: PtrDir,
1222+
rptr: irast.Pointer,
12151223
*,
12161224
ctx: context.ContextLevel,
12171225
) -> bool:
@@ -1220,14 +1228,14 @@ def is_injected_computable_ptr(
12201228
and ptrcls not in ctx.active_computeds
12211229
and (
12221230
bool(ptrcls.get_schema_reflection_default(ctx.env.schema))
1223-
or needs_rewrite_existence_assertion(ptrcls, direction, ctx=ctx)
1231+
or needs_rewrite_existence_assertion(ptrcls, rptr, ctx=ctx)
12241232
)
12251233
)
12261234

12271235

12281236
def _is_computable_ptr(
12291237
ptrcls: s_pointers.PointerLike,
1230-
direction: PtrDir,
1238+
rptr: irast.Pointer,
12311239
*,
12321240
ctx: context.ContextLevel,
12331241
) -> bool:
@@ -1240,7 +1248,7 @@ def _is_computable_ptr(
12401248

12411249
return (
12421250
bool(ptrcls.get_expr(ctx.env.schema))
1243-
or is_injected_computable_ptr(ptrcls, direction, ctx=ctx)
1251+
or is_injected_computable_ptr(ptrcls, rptr, ctx=ctx)
12441252
)
12451253

12461254

@@ -1711,8 +1719,7 @@ def computable_ptr_set(
17111719
op='??',
17121720
)
17131721

1714-
if needs_rewrite_existence_assertion(
1715-
ptrcls, PtrDir.Outbound, ctx=ctx):
1722+
if needs_rewrite_existence_assertion(ptrcls, rptr, ctx=ctx):
17161723
# Wrap it in a dummy select so that we can't optimize away
17171724
# the assert_exists.
17181725
# TODO: do something less bad

edb/edgeql/parser/grammar/expressions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,6 +1912,15 @@ def reduce_DOTBW_PathStepName(self, *kids):
19121912
direction=s_pointers.PointerDirection.Inbound
19131913
)
19141914

1915+
def reduce_DOTQ_PathStepName(self, *kids):
1916+
from edb.schema import pointers as s_pointers
1917+
1918+
self.val = qlast.Ptr(
1919+
name=kids[1].val.name,
1920+
direction=s_pointers.PointerDirection.Outbound,
1921+
type='optional',
1922+
)
1923+
19151924
def reduce_AT_PathNodeName(self, *kids):
19161925
from edb.schema import pointers as s_pointers
19171926

edb/edgeql/parser/grammar/precedence.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ class P_PAREN(Precedence, assoc='left', tokens=('LPAREN', 'RPAREN')):
147147
pass
148148

149149

150-
class P_DOT(Precedence, assoc='left', tokens=('DOT', 'DOTBW')):
150+
class P_DOT(Precedence, assoc='left', tokens=('DOT', 'DOTBW', 'DOTQ')):
151151
pass
152152

153153

edb/edgeql/parser/grammar/tokens.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ class T_DOTBW(Token, lextoken='.<'):
8787
pass
8888

8989

90+
class T_DOTQ(Token, lextoken='.?>'):
91+
pass
92+
93+
9094
class T_LBRACKET(Token, lextoken='['):
9195
pass
9296

0 commit comments

Comments
 (0)