def derive_ptr(ptr: s_pointers.Pointer, source: s_sources.Source, target: Optional[s_types.Type] = None, *qualifiers: str, derived_name: Optional[sn.SchemaName] = None, derived_name_quals: Optional[Sequence[str]] = (), derived_name_base: Optional[str] = None, preserve_shape: bool = False, preserve_path_id: bool = False, is_insert: bool = False, is_update: bool = False, inheritance_merge: bool = True, attrs: Optional[Dict[str, Any]] = None, ctx: context.ContextLevel) -> s_pointers.Pointer: if derived_name is None and ctx.derived_target_module: derived_name = derive_view_name(stype=ptr, derived_name_quals=derived_name_quals, derived_name_base=derived_name_base, ctx=ctx) if ptr.get_name(ctx.env.schema) == derived_name: qualifiers = qualifiers + (ctx.aliases.get('d'), ) ctx.env.schema, derived = ptr.derive_ref( ctx.env.schema, source, *qualifiers, target=target, name=derived_name, inheritance_merge=inheritance_merge, inheritance_refdicts={'pointers'}, mark_derived=True, transient=True, preserve_path_id=preserve_path_id, attrs=attrs) if not ptr.generic(ctx.env.schema): if isinstance(derived, s_sources.Source): ptr = cast(s_links.Link, ptr) scls_pointers = ptr.get_pointers(ctx.env.schema) derived_own_pointers = derived.get_pointers(ctx.env.schema) for pn, ptr in derived_own_pointers.items(ctx.env.schema): # This is a view of a view. Make sure query-level # computable expressions for pointers are carried over. src_ptr = scls_pointers.get(ctx.env.schema, pn) # mypy somehow loses the type argument in the # "pointers" ObjectIndex. assert isinstance(src_ptr, s_pointers.Pointer) computable_data = ctx.source_map.get(src_ptr) if computable_data is not None: ctx.source_map[ptr] = computable_data if preserve_shape and ptr in ctx.env.view_shapes: ctx.env.view_shapes[derived] = ctx.env.view_shapes[ptr] ctx.env.created_schema_objects.add(derived) return derived
def derive_dummy_ptr( ptr: s_pointers.Pointer, *, ctx: context.ContextLevel, ) -> s_pointers.Pointer: stdobj = cast(s_objtypes.ObjectType, ctx.env.schema.get('std::Object')) derived_obj_name = stdobj.get_derived_name(ctx.env.schema, stdobj, module='__derived__') derived_obj = ctx.env.schema.get(derived_obj_name, None) if derived_obj is None: ctx.env.schema, derived_obj = stdobj.derive_subtype( ctx.env.schema, name=derived_obj_name) ctx.env.created_schema_objects.add(derived_obj) derived_name = ptr.get_derived_name(ctx.env.schema, derived_obj) derived: s_pointers.Pointer derived = cast(s_pointers.Pointer, ctx.env.schema.get(derived_name, None)) if derived is None: ctx.env.schema, derived = ptr.derive_ref(ctx.env.schema, derived_obj, derived_obj, attrs={ 'cardinality': qltypes.Cardinality.MANY, }, name=derived_name, mark_derived=True) ctx.env.created_schema_objects.add(derived) return derived
def extend_path( source_set: irast.Set, ptrcls: s_pointers.Pointer, direction: PtrDir = PtrDir.Outbound, *, ignore_computable: bool = False, hoist_iterators: bool = False, unnest_fence: bool = False, same_computable_scope: bool = False, ctx: context.ContextLevel, ) -> irast.Set: """Return a Set node representing the new path tip.""" if ptrcls.is_link_property(ctx.env.schema): src_path_id = source_set.path_id.ptr_path() else: if direction is not s_pointers.PointerDirection.Inbound: source = ptrcls.get_near_endpoint(ctx.env.schema, direction) assert isinstance(source, s_types.Type) stype = get_set_type(source_set, ctx=ctx) if not stype.issubclass(ctx.env.schema, source): # Polymorphic link reference source_set = type_intersection_set( source_set, source, optional=True, ctx=ctx) src_path_id = source_set.path_id expr_type = get_set_type(source_set, ctx=ctx).get_expr_type(ctx.env.schema) path_id = pathctx.extend_path_id( src_path_id, ptrcls=ptrcls, direction=direction, ns=ctx.path_id_namespace, include_descendants_in_ptrref=expr_type is s_types.ExprType.Update, ctx=ctx, ) target = ptrcls.get_far_endpoint(ctx.env.schema, direction) assert isinstance(target, s_types.Type) target_set = new_set(stype=target, path_id=path_id, ctx=ctx) ptr = irast.Pointer( source=source_set, target=target_set, direction=direction, ptrref=path_id.rptr(), ) target_set.rptr = ptr is_computable = _is_computable_ptr(ptrcls, ctx=ctx) if not ignore_computable and is_computable: target_set = computable_ptr_set( ptr, unnest_fence=unnest_fence, hoist_iterators=hoist_iterators, same_computable_scope=same_computable_scope, ctx=ctx, ) return target_set
def extend_path(source_set: irast.Set, ptrcls: s_pointers.Pointer, direction: PtrDir = PtrDir.Outbound, target: typing.Optional[s_types.Type] = None, *, ignore_computable: bool = False, is_mut_assign: bool = False, unnest_fence: bool = False, same_computable_scope: bool = False, ctx: context.ContextLevel) -> irast.Set: """Return a Set node representing the new path tip.""" if ptrcls.is_link_property(ctx.env.schema): src_path_id = source_set.path_id.ptr_path() else: if direction != s_pointers.PointerDirection.Inbound: source = ptrcls.get_near_endpoint(ctx.env.schema, direction) stype = get_set_type(source_set, ctx=ctx) if not stype.issubclass(ctx.env.schema, source): # Polymorphic link reference source_set = class_indirection_set(source_set, source, optional=True, ctx=ctx) src_path_id = source_set.path_id if target is None: target = ptrcls.get_far_endpoint(ctx.env.schema, direction) path_id = pathctx.extend_path_id(src_path_id, ptrcls=ptrcls, direction=direction, target=target, ns=ctx.path_id_namespace, ctx=ctx) target_set = new_set(stype=target, path_id=path_id, ctx=ctx) ptr = irast.Pointer( source=source_set, target=target_set, direction=direction, ptrref=path_id.rptr(), ) target_set.rptr = ptr is_computable = _is_computable_ptr(ptrcls, is_mut_assign=is_mut_assign, ctx=ctx) if not ignore_computable and is_computable: target_set = computable_ptr_set( ptr, unnest_fence=unnest_fence, from_default_expr=is_mut_assign, same_computable_scope=same_computable_scope, ctx=ctx, ) return target_set
def _infer_pointer_cardinality( *, ptrcls: s_pointers.Pointer, irexpr: irast.Set, specified_card: Optional[qltypes.Cardinality] = None, is_mut_assignment: bool = False, source_ctx: Optional[parsing.ParserContext] = None, ctx: context.ContextLevel, ) -> None: # Infer cardinality and convert it back to schema values of "ONE/MANY". inferred_card = infer_expr_cardinality(irexpr=irexpr, ctx=ctx) if specified_card is None: ptr_card = inferred_card else: if inf_card.is_subset_cardinality(inferred_card, specified_card): # The inferred cardinality is within the boundaries of # specified cardinality, use the maximum lower and upper bounds. ptr_card = inf_card.max_cardinality( (specified_card, inferred_card), ) else: sp_req, sp_card = specified_card.to_schema_value() ic_req, ic_card = inferred_card.to_schema_value() # Specified cardinality is stricter than inferred (e.g. # ONE vs MANY), this is an error. if sp_req and not ic_req: if is_mut_assignment: # For mutations we punt the lower cardinality bound # check to the runtime constraint. Doing it statically # is impractical because it is impossible to prove # non-emptiness of object-selecting expressions bound # for required links. ptr_card = inf_card.cartesian_cardinality( (specified_card, inferred_card), ) else: raise errors.QueryError( f'possibly an empty set returned by an ' f'expression for a computable ' f'{ptrcls.get_verbosename(ctx.env.schema)} ' f"declared as 'required'", context=source_ctx) else: raise errors.QueryError( f'possibly more than one element returned by an ' f'expression for a computable ' f'{ptrcls.get_verbosename(ctx.env.schema)} ' f"declared as 'single'", context=source_ctx) required, card = ptr_card.to_schema_value() ctx.env.schema = ptrcls.set_field_value(ctx.env.schema, 'cardinality', card) ctx.env.schema = ptrcls.set_field_value(ctx.env.schema, 'required', required) _update_cardinality_in_derived(ptrcls, ctx=ctx) _update_cardinality_callbacks(ptrcls, ctx=ctx)
def derive_ptr(ptr: s_pointers.Pointer, source: s_sources.Source, target: typing.Optional[s_types.Type] = None, *qualifiers, derived_name: typing.Optional[sn.SchemaName] = None, derived_name_quals: typing.Optional[typing.Sequence[str]] = (), derived_name_base: typing.Optional[str] = None, preserve_shape: bool = False, preserve_path_id: bool = False, is_insert: bool = False, is_update: bool = False, inheritance_merge: bool = True, attrs: typing.Optional[dict] = None, ctx: context.ContextLevel) -> s_pointers.Pointer: if derived_name is None and ctx.derived_target_module: derived_name = derive_view_name(stype=ptr, derived_name_quals=derived_name_quals, derived_name_base=derived_name_base, ctx=ctx) if ptr.get_name(ctx.env.schema) == derived_name: qualifiers = qualifiers + (ctx.aliases.get('d'), ) ctx.env.schema, derived = ptr.derive_ref( ctx.env.schema, source, target, *qualifiers, name=derived_name, inheritance_merge=inheritance_merge, refdict_whitelist={'pointers'}, mark_derived=True, preserve_path_id=preserve_path_id, attrs=attrs) if not ptr.generic(ctx.env.schema): if isinstance(derived, s_sources.Source): ptr = typing.cast(s_links.Link, ptr) scls_pointers = ptr.get_pointers(ctx.env.schema) derived_own_pointers = derived.get_pointers(ctx.env.schema) for pn, ptr in derived_own_pointers.items(ctx.env.schema): # This is a view of a view. Make sure query-level # computable expressions for pointers are carried over. src_ptr = scls_pointers.get(ctx.env.schema, pn) computable_data = ctx.source_map.get(src_ptr) if computable_data is not None: ctx.source_map[ptr] = computable_data if preserve_shape and ptr in ctx.env.view_shapes: ctx.env.view_shapes[derived] = ctx.env.view_shapes[ptr] return derived
def _get_base_ptr_cardinality( ptrcls: s_pointers.Pointer, *, ctx: context.ContextLevel, ) -> Optional[qltypes.SchemaCardinality]: ptr_name = ptrcls.get_name(ctx.env.schema) if ptr_name in { sn.QualName('std', 'link'), sn.QualName('std', 'property') }: return None else: return ptrcls.get_cardinality(ctx.env.schema)
def _update_cardinality_in_derived(ptrcls: s_pointers.Pointer, *, env: context.Environment) -> None: children = env.pointer_derivation_map.get(ptrcls) if children: ptrcls_cardinality = ptrcls.get_cardinality(env.schema) ptrcls_required = ptrcls.get_required(env.schema) assert ptrcls_cardinality.is_known() for child in children: env.schema = child.set_field_value(env.schema, 'cardinality', ptrcls_cardinality) env.schema = child.set_field_value(env.schema, 'required', ptrcls_required) _update_cardinality_in_derived(child, env=env)
def _infer_pointer_cardinality( *, ptrcls: s_pointers.Pointer, irexpr: irast.Expr, specified_card: typing.Optional[qltypes.Cardinality] = None, source_ctx: typing.Optional[parsing.ParserContext] = None, ctx: context.ContextLevel) -> None: inferred_card = infer_expr_cardinality(irexpr=irexpr, ctx=ctx) if specified_card is None or inferred_card is specified_card: ptr_card = inferred_card else: if specified_card is qltypes.Cardinality.MANY: # Explicit many foo := <expr>, just take it. ptr_card = specified_card else: # Specified cardinality is ONE, but we inferred MANY, this # is an error. raise errors.QueryError( f'possibly more than one element returned by an ' f'expression for a computable ' f'{ptrcls.get_verbosename(ctx.env.schema)} ' f"declared as 'single'", context=source_ctx ) ctx.env.schema = ptrcls.set_field_value( ctx.env.schema, 'cardinality', ptr_card) _update_cardinality_in_derived(ptrcls, ctx=ctx) _update_cardinality_callbacks(ptrcls, ctx=ctx)
def _infer_pointer_cardinality( *, ptrcls: s_pointers.Pointer, irexpr: irast.Set, specified_card: Optional[qltypes.Cardinality] = None, source_ctx: Optional[parsing.ParserContext] = None, ctx: context.ContextLevel) -> None: # Infer cardinality and convert it back to schema values of "ONE/MANY". inferred_card = infer_expr_cardinality(irexpr=irexpr, ctx=ctx) if specified_card is None: ptr_card = inferred_card else: if inf_card.is_subset_cardinality(inferred_card, specified_card): # The inferred cardinality is within the boundaries of # specified cardinality. ptr_card = specified_card else: sp_req, sp_card = specified_card.to_schema_value() ic_req, ic_card = inferred_card.to_schema_value() # Specified cardinality is stricter than inferred (e.g. # ONE vs MANY), this is an error. if sp_req and not ic_req: raise errors.QueryError( f'possibly an empty set returned by an ' f'expression for a computable ' f'{ptrcls.get_verbosename(ctx.env.schema)} ' f"declared as 'required'", context=source_ctx) else: raise errors.QueryError( f'possibly more than one element returned by an ' f'expression for a computable ' f'{ptrcls.get_verbosename(ctx.env.schema)} ' f"declared as 'single'", context=source_ctx) required, card = ptr_card.to_schema_value() ctx.env.schema = ptrcls.set_field_value(ctx.env.schema, 'cardinality', card) ctx.env.schema = ptrcls.set_field_value(ctx.env.schema, 'required', required) _update_cardinality_in_derived(ptrcls, ctx=ctx) _update_cardinality_callbacks(ptrcls, ctx=ctx)
def _update_cardinality_in_derived( ptrcls: s_pointers.Pointer, *, ctx: context.ContextLevel) -> None: children = ctx.pointer_derivation_map.get(ptrcls) if children: ptrcls_cardinality = ptrcls.get_cardinality(ctx.env.schema) for child in children: ctx.env.schema = child.set_field_value( ctx.env.schema, 'cardinality', ptrcls_cardinality) _update_cardinality_in_derived(child, ctx=ctx) _update_cardinality_callbacks(child, ctx=ctx)
def from_pointer( cls, schema: s_schema.Schema, pointer: s_pointers.Pointer, *, namespace: AbstractSet[AnyNamespace] = frozenset(), ) -> PathId: """Return a ``PathId`` instance for a given link or property. The specified *pointer* argument must be a concrete link or property. The returned ``PathId`` instance describes a set variable of all objects represented by the pointer (i.e, for a link, a set of all link targets). Args: schema: A schema instance where the type *t* is defined. pointer: An instance of a concrete link or property. namespace: Optional namespace in which the variable is defined. Returns: A ``PathId`` instance. """ if pointer.generic(schema): raise ValueError(f'invalid PathId: {pointer} is not concrete') source = pointer.get_source(schema) if isinstance(source, s_pointers.Pointer): prefix = cls.from_pointer(schema, source, namespace=namespace) prefix = prefix.ptr_path() elif isinstance(source, s_types.Type): prefix = cls.from_type(schema, source, namespace=namespace) else: raise AssertionError(f'unexpected pointer source: {source!r}') ptrref = typeutils.ptrref_from_ptrcls(schema=schema, ptrcls=pointer) return prefix.extend(ptrref=ptrref)
def _link_has_shape(ptrcls: s_pointers.Pointer, *, ctx: context.ContextLevel) -> bool: if not isinstance(ptrcls, s_links.Link): return False for p in ptrcls.get_pointers(ctx.env.schema).objects(ctx.env.schema): if (p.is_special_pointer(ctx.env.schema) or p not in ctx.env.view_shapes[ptrcls]): continue else: return True return False
def derive_dummy_ptr( ptr: s_pointers.Pointer, *, ctx: context.ContextLevel, ) -> s_pointers.Pointer: stdobj = ctx.env.schema.get('std::BaseObject', type=s_objtypes.ObjectType) derived_obj_name = stdobj.get_derived_name(ctx.env.schema, stdobj, module='__derived__') derived_obj = ctx.env.schema.get(derived_obj_name, None, type=s_obj.QualifiedObject) if derived_obj is None: ctx.env.schema, derived_obj = stdobj.derive_subtype( ctx.env.schema, name=derived_obj_name) ctx.env.created_schema_objects.add(derived_obj) derived_name = ptr.get_derived_name(ctx.env.schema, derived_obj) derived: s_pointers.Pointer derived = cast(s_pointers.Pointer, ctx.env.schema.get(derived_name, None)) if derived is None: ctx.env.schema, derived = ptr.derive_ref( ctx.env.schema, derived_obj, target=derived_obj, attrs={ 'cardinality': qltypes.SchemaCardinality.One, }, name=derived_name, mark_derived=True, transient=True, ) ctx.env.created_schema_objects.add(derived) return derived
def get_pointer_path_id(ptr: s_pointers.Pointer, *, ctx: context.ContextLevel) -> irast.PathId: src = ptr.get_source(ctx.env.schema) assert isinstance(src, s_types.Type) return extend_path_id(get_path_id(src, ctx=ctx), ptrcls=ptr, ctx=ctx)
def _infer_pointer_cardinality( *, ptrcls: s_pointers.Pointer, ptrref: Optional[irast.BasePointerRef], irexpr: irast.Base, specified_required: Optional[bool] = None, specified_card: Optional[qltypes.SchemaCardinality] = None, is_mut_assignment: bool = False, shape_op: qlast.ShapeOp = qlast.ShapeOp.ASSIGN, source_ctx: Optional[parsing.ParserContext] = None, scope_tree: irast.ScopeTreeNode, ctx: inference_context.InfCtx, ) -> None: env = ctx.env if specified_required is None: spec_lower_bound = None else: spec_lower_bound = CardinalityBound.from_required(specified_required) if specified_card is None: spec_upper_bound = None else: spec_upper_bound = CardinalityBound.from_schema_value(specified_card) expr_card = infer_cardinality(irexpr, scope_tree=scope_tree, ctx=ctx) ptrcls_schema_card = ptrcls.get_cardinality(env.schema) # Infer cardinality and convert it back to schema values of "ONE/MANY". if shape_op is qlast.ShapeOp.APPEND: # += in shape always means MANY inferred_card = qltypes.Cardinality.MANY elif shape_op is qlast.ShapeOp.SUBTRACT: # -= does not increase cardinality, but it may result in an empty set, # hence AT_MOST_ONE. inferred_card = qltypes.Cardinality.AT_MOST_ONE else: # Pull cardinality from the ptrcls, if it exists. # (This generally will have been populated by the source_map # handling in infer_toplevel_cardinality().) if ptrcls_schema_card.is_known(): inferred_card = qltypes.Cardinality.from_schema_value( not expr_card.can_be_zero(), ptrcls_schema_card) else: inferred_card = expr_card if spec_upper_bound is None and spec_lower_bound is None: # Common case of no explicit specifier and no overloading. ptr_card = inferred_card else: # Verify that the explicitly specified (or inherited) cardinality is # within the cardinality bounds inferred from the expression, except # for mutations we punt the lower cardinality bound check to the # runtime DML constraint as that would produce a more meaningful error. inf_lower_bound, inf_upper_bound = _card_to_bounds(inferred_card) if spec_upper_bound is None: upper_bound = inf_upper_bound else: if inf_upper_bound > spec_upper_bound: desc = ptrcls.get_verbosename(env.schema) if not is_mut_assignment: desc = f"computed {desc}" raise errors.QueryError( f"possibly more than one element returned by an " f"expression for a {desc} declared as 'single'", context=source_ctx, ) upper_bound = spec_upper_bound if spec_lower_bound is None: lower_bound = inf_lower_bound else: if inf_lower_bound < spec_lower_bound: if is_mut_assignment: lower_bound = inf_lower_bound else: desc = f"computed {ptrcls.get_verbosename(env.schema)}" raise errors.QueryError( f"possibly an empty set returned by an " f"expression for a {desc} declared as 'required'", context=source_ctx, ) else: lower_bound = spec_lower_bound ptr_card = _bounds_to_card(lower_bound, upper_bound) if (not ptrcls_schema_card.is_known() or ptrcls in ctx.env.pointer_specified_info): if ptrcls_schema_card.is_known(): # If we are overloading an existing pointer, take the _maximum_ # of the cardinalities. In practice this only means that we might # raise the lower bound, since the other redefinitions of bounds # are prohibited above and in viewgen. ptrcls_card = qltypes.Cardinality.from_schema_value( ptrcls.get_required(env.schema), ptrcls_schema_card, ) if is_mut_assignment: ptr_card = cartesian_cardinality((ptrcls_card, ptr_card)) else: ptr_card = max_cardinality((ptrcls_card, ptr_card)) required, card = ptr_card.to_schema_value() env.schema = ptrcls.set_field_value(env.schema, 'cardinality', card) env.schema = ptrcls.set_field_value(env.schema, 'required', required) _update_cardinality_in_derived(ptrcls, env=ctx.env) if ptrref: out_card, in_card = typeutils.cardinality_from_ptrcls( env.schema, ptrcls) assert in_card is not None assert out_card is not None ptrref.in_cardinality = in_card ptrref.out_cardinality = out_card
def _infer_pointer_cardinality( *, ptrcls: s_pointers.Pointer, ptrref: irast.BasePointerRef, irexpr: irast.Base, specified_required: bool = False, specified_card: Optional[qltypes.SchemaCardinality] = None, is_mut_assignment: bool = False, shape_op: qlast.ShapeOp = qlast.ShapeOp.ASSIGN, source_ctx: Optional[parsing.ParserContext] = None, scope_tree: irast.ScopeTreeNode, ctx: inference_context.InfCtx, ) -> None: env = ctx.env # Convert the SchemaCardinality into Cardinality used for inference. if not specified_required and specified_card is None: ir_specified_card = None else: ir_specified_card = qltypes.Cardinality.from_schema_value( specified_required, specified_card or qltypes.SchemaCardinality.One) expr_card = infer_cardinality(irexpr, scope_tree=scope_tree, ctx=ctx) # Infer cardinality and convert it back to schema values of "ONE/MANY". if shape_op is qlast.ShapeOp.APPEND: # += in shape always means MANY inferred_card = qltypes.Cardinality.MANY elif shape_op is qlast.ShapeOp.SUBTRACT: # -= does not increase cardinality, but it may result in an empty set, # hence AT_MOST_ONE. inferred_card = qltypes.Cardinality.AT_MOST_ONE else: inferred_card = expr_card if ir_specified_card is None: ptr_card = inferred_card else: if is_subset_cardinality(inferred_card, ir_specified_card): # The inferred cardinality is within the boundaries of # specified cardinality, use the maximum lower and upper bounds. ptr_card = max_cardinality((ir_specified_card, inferred_card), ) else: sp_req, sp_card = ir_specified_card.to_schema_value() ic_req, ic_card = inferred_card.to_schema_value() # Specified cardinality is stricter than inferred (e.g. # ONE vs MANY), this is an error. if sp_req and not ic_req: if is_mut_assignment: # For mutations we punt the lower cardinality bound # check to the runtime constraint. Doing it statically # is impractical because it is impossible to prove # non-emptiness of object-selecting expressions bound # for required links. ptr_card = cartesian_cardinality( (ir_specified_card, inferred_card), ) else: raise errors.QueryError( f'possibly an empty set returned by an ' f'expression for a computable ' f'{ptrcls.get_verbosename(env.schema)} ' f"declared as 'required'", context=source_ctx) else: raise errors.QueryError( f'possibly more than one element returned by an ' f'expression for a computable ' f'{ptrcls.get_verbosename(env.schema)} ' f"declared as 'single'", context=source_ctx) ptrcls_schema_card = ptrcls.get_cardinality(env.schema) if (not ptrcls_schema_card.is_known() or ptrcls in ctx.env.pointer_specified_info): if ptrcls_schema_card.is_known(): # If we are overloading an existing pointer, take the _maximum_ # of the cardinalities. In practice this only means that we might # raise the lower bound, since the other redefinitions of bounds # are prohibited above and in viewgen. ptrcls_card = qltypes.Cardinality.from_schema_value( ptrcls.get_required(env.schema), ptrcls_schema_card, ) if is_mut_assignment: ptr_card = cartesian_cardinality((ptrcls_card, ptr_card)) else: ptr_card = max_cardinality((ptrcls_card, ptr_card)) required, card = ptr_card.to_schema_value() env.schema = ptrcls.set_field_value(env.schema, 'cardinality', card) env.schema = ptrcls.set_field_value(env.schema, 'required', required) _update_cardinality_in_derived(ptrcls, env=ctx.env) out_card, dir_card = typeutils.cardinality_from_ptrcls( env.schema, ptrcls, direction=ptrref.direction) assert dir_card is not None assert out_card is not None ptrref.dir_cardinality = dir_card ptrref.out_cardinality = out_card