def get_rvar_path_var(rvar: pgast.PathRangeVar, path_id: irast.PathId, aspect: str, *, env: context.Environment) -> pgast.OutputVar: """Return ColumnRef for a given *path_id* in a given *range var*.""" if (path_id, aspect) in rvar.query.path_outputs: outvar = rvar.query.path_outputs[path_id, aspect] elif is_relation_rvar(rvar): if ((rptr := path_id.rptr()) is not None and rvar.typeref is not None and rvar.query.path_id != path_id and (not rvar.query.path_id.is_type_intersection_path() or rvar.query.path_id.src_path() != path_id)): actual_rptr = irtyputils.find_actual_ptrref( rvar.typeref, rptr, ) if actual_rptr is not None: ptr_si = pg_types.get_ptrref_storage_info(actual_rptr) else: ptr_si = None else: ptr_si = None outvar = _get_rel_path_output(rvar.query, path_id, ptr_info=ptr_si, aspect=aspect, env=env)
def _find_rvar_in_intersection_by_typeref( path_id: irast.PathId, component_rvars: Sequence[pgast.PathRangeVar], ) -> pgast.PathRangeVar: assert component_rvars pid_rptr = path_id.rptr() if pid_rptr is not None: if pid_rptr.material_ptr is not None: pid_rptr = pid_rptr.material_ptr tref = pid_rptr.out_source else: tref = path_id.target for component_rvar in component_rvars: if (component_rvar.typeref is not None and irtyputils.type_contains(tref, component_rvar.typeref)): rel_rvar = component_rvar break else: raise AssertionError( f'no rvar in intersection matches path id {path_id}') return rel_rvar
def get_path_output_alias(path_id: irast.PathId, aspect: str, *, env: context.Environment) -> str: rptr = path_id.rptr() if rptr is not None: alias_base = rptr.shortname.name elif path_id.is_collection_path(): alias_base = path_id.target.collection else: _, _, alias_base = path_id.target_name_hint.rpartition('::') return env.aliases.get(f'{alias_base}_{aspect}')
def _find_in_output_tuple( rel: pgast.Query, path_id: irast.PathId, aspect: str, env: context.Environment) -> Optional[pgast.BaseExpr]: """Try indirecting a source tuple already present as an output. Normally tuple indirections are handled by process_set_as_tuple_indirection, but UNIONing an explicit tuple with a tuple coming from a base relation (like `(Foo.bar UNION (1,2)).0`) can lead to us looking for a tuple path in relations that only have the actual full tuple. (See test_edgeql_coalesce_tuple_{08,09}). We handle this by checking whether some prefix of the tuple path is present in the path_outputs. This is sufficient because the relevant cases are all caused by set ops, and the "fixup" done in set op cases ensures that the tuple will be already present. """ steps = [] src_path_id = path_id.src_path() ptrref = path_id.rptr() while ( src_path_id and src_path_id.is_tuple_path() and isinstance(ptrref, irast.TupleIndirectionPointerRef) ): steps.append((ptrref.shortname.name, src_path_id)) if ( (var := rel.path_namespace.get((src_path_id, aspect))) and not isinstance(var, pgast.TupleVarBase) ): for name, src in reversed(steps): var = astutils.tuple_getattr(var, src.target, name) put_path_var(rel, path_id, var, aspect=aspect, env=env) return var ptrref = src_path_id.rptr() src_path_id = src_path_id.src_path()
def _find_rvar_in_intersection( path_id: irast.PathId, component_rvars: Sequence[pgast.PathRangeVar], ) -> pgast.PathRangeVar: assert component_rvars pid_rptr = path_id.rptr() if pid_rptr is not None: if pid_rptr.material_ptr is not None: pid_rptr = pid_rptr.material_ptr tref = pid_rptr.out_source else: tref = path_id.target for component_rvar in component_rvars: assert component_rvar.typeref is not None if irtyputils.type_contains(tref, component_rvar.typeref): rel_rvar = component_rvar break else: rel_rvar = component_rvars[0] return rel_rvar
def _normalize_view_ptr_expr( shape_el: qlast.ShapeElement, view_scls: s_objtypes.ObjectType, *, path_id: irast.PathId, path_id_namespace: Optional[irast.WeakNamespace]=None, is_insert: bool=False, is_update: bool=False, from_default: bool=False, view_rptr: Optional[context.ViewRPtr]=None, ctx: context.ContextLevel) -> s_pointers.Pointer: steps = shape_el.expr.steps is_linkprop = False is_polymorphic = False is_mutation = is_insert or is_update # Pointers may be qualified by the explicit source # class, which is equivalent to Expr[IS Type]. plen = len(steps) ptrsource: s_sources.Source = view_scls qlexpr: Optional[qlast.Expr] = None target_typexpr = None source: qlast.Base base_ptrcls_is_alias = False if plen >= 2 and isinstance(steps[-1], qlast.TypeIntersection): # Target type intersection: foo: Type target_typexpr = steps[-1].type plen -= 1 steps = steps[:-1] if plen == 1: # regular shape lexpr = steps[0] assert isinstance(lexpr, qlast.Ptr) is_linkprop = lexpr.type == 'property' if is_linkprop: if view_rptr is None or view_rptr.ptrcls is None: raise errors.QueryError( 'invalid reference to link property ' 'in top level shape', context=lexpr.context) assert isinstance(view_rptr.ptrcls, s_links.Link) ptrsource = view_rptr.ptrcls source = qlast.Source() elif plen == 2 and isinstance(steps[0], qlast.TypeIntersection): # Source type intersection: [IS Type].foo source = qlast.Path(steps=[ qlast.Source(), steps[0], ]) lexpr = steps[1] ptype = steps[0].type if not isinstance(ptype, qlast.TypeName): raise errors.QueryError( 'complex type expressions are not supported here', context=ptype.context, ) source_spec = schemactx.get_schema_type(ptype.maintype, ctx=ctx) if not isinstance(source_spec, s_objtypes.ObjectType): raise errors.QueryError( f'expected object type, got ' f'{source_spec.get_verbosename(ctx.env.schema)}', context=ptype.context, ) ptrsource = source_spec is_polymorphic = True else: # pragma: no cover raise RuntimeError( f'unexpected path length in view shape: {len(steps)}') assert isinstance(lexpr, qlast.Ptr) ptrname = lexpr.ptr.name compexpr: Optional[qlast.Expr] = shape_el.compexpr if compexpr is None and is_insert and shape_el.elements: # Short shape form in INSERT, e.g # INSERT Foo { bar: Spam { name := 'name' }} # is prohibited. raise errors.EdgeQLSyntaxError( "unexpected ':'", context=steps[-1].context) ptrcls: Optional[s_pointers.Pointer] if compexpr is None: ptrcls = setgen.resolve_ptr( ptrsource, ptrname, track_ref=lexpr, ctx=ctx) if is_polymorphic: ptrcls = schemactx.derive_ptr( ptrcls, view_scls, is_insert=is_insert, is_update=is_update, ctx=ctx) base_ptrcls = ptrcls.get_bases(ctx.env.schema).first(ctx.env.schema) base_ptr_is_computable = base_ptrcls in ctx.source_map ptr_name = sn.QualName( module='__', name=ptrcls.get_shortname(ctx.env.schema).name, ) base_cardinality = _get_base_ptr_cardinality(base_ptrcls, ctx=ctx) base_is_singleton = False if base_cardinality is not None and base_cardinality.is_known(): base_is_singleton = base_cardinality.is_single() if ( shape_el.where or shape_el.orderby or shape_el.offset or shape_el.limit or base_ptr_is_computable or is_polymorphic or target_typexpr is not None or (ctx.implicit_limit and not base_is_singleton) ): if target_typexpr is None: qlexpr = qlast.Path(steps=[source, lexpr]) else: qlexpr = qlast.Path(steps=[ source, lexpr, qlast.TypeIntersection(type=target_typexpr), ]) qlexpr = astutils.ensure_qlstmt(qlexpr) assert isinstance(qlexpr, qlast.SelectQuery) qlexpr.where = shape_el.where qlexpr.orderby = shape_el.orderby if shape_el.offset or shape_el.limit: qlexpr = qlast.SelectQuery(result=qlexpr, implicit=True) qlexpr.offset = shape_el.offset qlexpr.limit = shape_el.limit if ( (ctx.expr_exposed or ctx.stmt is ctx.toplevel_stmt) and not qlexpr.limit and ctx.implicit_limit and not base_is_singleton ): qlexpr.limit = qlast.IntegerConstant( value=str(ctx.implicit_limit), ) if target_typexpr is not None: assert isinstance(target_typexpr, qlast.TypeName) intersector_type = schemactx.get_schema_type( target_typexpr.maintype, ctx=ctx) int_result = schemactx.apply_intersection( ptrcls.get_target(ctx.env.schema), # type: ignore intersector_type, ctx=ctx, ) ptr_target = int_result.stype else: _ptr_target = ptrcls.get_target(ctx.env.schema) assert _ptr_target ptr_target = _ptr_target ptr_cardinality = base_cardinality if ptr_cardinality is None or not ptr_cardinality.is_known(): # We do not know the parent's pointer cardinality yet. ctx.env.pointer_derivation_map[base_ptrcls].append(ptrcls) ctx.env.pointer_specified_info[ptrcls] = ( shape_el.cardinality, shape_el.required, shape_el.context) implicit_tid = has_implicit_type_computables( ptr_target, is_mutation=is_mutation, ctx=ctx, ) if shape_el.elements or implicit_tid: sub_view_rptr = context.ViewRPtr( ptrsource if is_linkprop else view_scls, ptrcls=ptrcls, is_insert=is_insert, is_update=is_update) sub_path_id = pathctx.extend_path_id( path_id, ptrcls=base_ptrcls, ns=ctx.path_id_namespace, ctx=ctx) ctx.path_scope.attach_path(sub_path_id, context=shape_el.context) if not isinstance(ptr_target, s_objtypes.ObjectType): raise errors.QueryError( f'shapes cannot be applied to ' f'{ptr_target.get_verbosename(ctx.env.schema)}', context=shape_el.context, ) if is_update: for subel in shape_el.elements or []: is_prop = ( isinstance(subel.expr.steps[0], qlast.Ptr) and subel.expr.steps[0].type == 'property' ) if not is_prop: raise errors.QueryError( 'only references to link properties are allowed ' 'in nested UPDATE shapes', context=subel.context) ptr_target = _process_view( stype=ptr_target, path_id=sub_path_id, path_id_namespace=path_id_namespace, view_rptr=sub_view_rptr, elements=shape_el.elements, is_update=True, parser_context=shape_el.context, ctx=ctx) else: ptr_target = _process_view( stype=ptr_target, path_id=sub_path_id, path_id_namespace=path_id_namespace, view_rptr=sub_view_rptr, elements=shape_el.elements, parser_context=shape_el.context, ctx=ctx) else: base_ptrcls = ptrcls = None if (is_mutation and ptrname not in ctx.special_computables_in_mutation_shape): # If this is a mutation, the pointer must exist. ptrcls = setgen.resolve_ptr( ptrsource, ptrname, track_ref=lexpr, ctx=ctx) base_ptrcls = ptrcls.get_bases( ctx.env.schema).first(ctx.env.schema) ptr_name = sn.QualName( module='__', name=ptrcls.get_shortname(ctx.env.schema).name, ) else: ptr_name = sn.QualName( module='__', name=ptrname, ) try: ptrcls = setgen.resolve_ptr( ptrsource, ptrname, track_ref=False, ctx=ctx, ) base_ptrcls = ptrcls.get_bases( ctx.env.schema).first(ctx.env.schema) except errors.InvalidReferenceError: # This is a NEW computable pointer, it's fine. pass qlexpr = astutils.ensure_qlstmt(compexpr) if ((ctx.expr_exposed or ctx.stmt is ctx.toplevel_stmt) and ctx.implicit_limit and isinstance(qlexpr, qlast.OffsetLimitMixin) and not qlexpr.limit): qlexpr.limit = qlast.IntegerConstant(value=str(ctx.implicit_limit)) with ctx.newscope(fenced=True) as shape_expr_ctx: # Put current pointer class in context, so # that references to link properties in sub-SELECT # can be resolved. This is necessary for proper # evaluation of link properties on computable links, # most importantly, in INSERT/UPDATE context. shape_expr_ctx.view_rptr = context.ViewRPtr( ptrsource if is_linkprop else view_scls, ptrcls=ptrcls, ptrcls_name=ptr_name, ptrcls_is_linkprop=is_linkprop, is_insert=is_insert, is_update=is_update, ) shape_expr_ctx.defining_view = view_scls shape_expr_ctx.path_scope.unnest_fence = True shape_expr_ctx.partial_path_prefix = setgen.class_set( view_scls.get_bases(ctx.env.schema).first(ctx.env.schema), path_id=path_id, ctx=shape_expr_ctx) prefix_rptrref = path_id.rptr() if prefix_rptrref is not None: # Source path seems to contain multiple steps, # so set up a rptr for abbreviated link property # paths. src_path_id = path_id.src_path() assert src_path_id is not None ctx.env.schema, src_t = irtyputils.ir_typeref_to_type( shape_expr_ctx.env.schema, src_path_id.target, ) prefix_rptr = irast.Pointer( source=setgen.class_set( src_t, path_id=src_path_id, ctx=shape_expr_ctx, ), target=shape_expr_ctx.partial_path_prefix, ptrref=prefix_rptrref, direction=s_pointers.PointerDirection.Outbound, ) shape_expr_ctx.partial_path_prefix.rptr = prefix_rptr if is_mutation and ptrcls is not None: shape_expr_ctx.expr_exposed = True shape_expr_ctx.empty_result_type_hint = \ ptrcls.get_target(ctx.env.schema) shape_expr_ctx.stmt_metadata[qlexpr] = context.StatementMetadata( iterator_target=True, ) irexpr = dispatch.compile(qlexpr, ctx=shape_expr_ctx) if ( shape_el.operation.op is qlast.ShapeOp.APPEND or shape_el.operation.op is qlast.ShapeOp.SUBTRACT ): if not is_update: op = ( '+=' if shape_el.operation.op is qlast.ShapeOp.APPEND else '-=' ) raise errors.EdgeQLSyntaxError( f"unexpected '{op}'", context=shape_el.operation.context, ) irexpr.context = compexpr.context if base_ptrcls is None: base_ptrcls = shape_expr_ctx.view_rptr.base_ptrcls base_ptrcls_is_alias = shape_expr_ctx.view_rptr.ptrcls_is_alias if ptrcls is not None: ctx.env.schema = ptrcls.set_field_value( ctx.env.schema, 'owned', True) ptr_cardinality = None ptr_target = inference.infer_type(irexpr, ctx.env) if ( isinstance(ptr_target, s_types.Collection) and not ctx.env.orig_schema.get_by_id(ptr_target.id, default=None) ): # Record references to implicitly defined collection types, # so that the alias delta machinery can pick them up. ctx.env.created_schema_objects.add(ptr_target) anytype = ptr_target.find_any(ctx.env.schema) if anytype is not None: raise errors.QueryError( 'expression returns value of indeterminate type', context=ctx.env.type_origins.get(anytype), ) # Validate that the insert/update expression is # of the correct class. if is_mutation and ptrcls is not None: base_target = ptrcls.get_target(ctx.env.schema) assert base_target is not None if ptr_target.assignment_castable_to( base_target, schema=ctx.env.schema): # Force assignment casts if the target type is not a # subclass of the base type and the cast is not to an # object type. if not ( base_target.is_object_type() or s_types.is_type_compatible( base_target, ptr_target, schema=ctx.env.schema ) ): qlexpr = astutils.ensure_qlstmt(qlast.TypeCast( type=typegen.type_to_ql_typeref(base_target, ctx=ctx), expr=compexpr, )) ptr_target = base_target else: expected = [ repr(str(base_target.get_displayname(ctx.env.schema))) ] ercls: Type[errors.EdgeDBError] if ptrcls.is_property(ctx.env.schema): ercls = errors.InvalidPropertyTargetError else: ercls = errors.InvalidLinkTargetError ptr_vn = ptrcls.get_verbosename(ctx.env.schema, with_parent=True) raise ercls( f'invalid target for {ptr_vn}: ' f'{str(ptr_target.get_displayname(ctx.env.schema))!r} ' f'(expecting {" or ".join(expected)})' ) if qlexpr is not None or ptrcls is None: src_scls: s_sources.Source if is_linkprop: # Proper checking was done when is_linkprop is defined. assert view_rptr is not None assert isinstance(view_rptr.ptrcls, s_links.Link) src_scls = view_rptr.ptrcls else: src_scls = view_scls if ptr_target.is_object_type(): base = ctx.env.get_track_schema_object( sn.QualName('std', 'link'), expr=None) else: base = ctx.env.get_track_schema_object( sn.QualName('std', 'property'), expr=None) if base_ptrcls is not None: derive_from = base_ptrcls else: derive_from = base derived_name = schemactx.derive_view_name( base_ptrcls, derived_name_base=ptr_name, derived_name_quals=[str(src_scls.get_name(ctx.env.schema))], ctx=ctx, ) existing = ctx.env.schema.get( derived_name, default=None, type=s_pointers.Pointer) if existing is not None: existing_target = existing.get_target(ctx.env.schema) assert existing_target is not None if ctx.recompiling_schema_alias: ptr_cardinality = existing.get_cardinality(ctx.env.schema) if ptr_target == existing_target: ptrcls = existing elif ptr_target.implicitly_castable_to( existing_target, ctx.env.schema): ctx.env.schema = existing.set_target( ctx.env.schema, ptr_target) ptrcls = existing else: vnp = existing.get_verbosename( ctx.env.schema, with_parent=True) t1_vn = existing_target.get_verbosename(ctx.env.schema) t2_vn = ptr_target.get_verbosename(ctx.env.schema) if compexpr is not None: source_context = compexpr.context else: source_context = shape_el.expr.steps[-1].context raise errors.SchemaError( f'cannot redefine {vnp} as {t2_vn}', details=f'{vnp} is defined as {t1_vn}', context=source_context, ) else: ptrcls = schemactx.derive_ptr( derive_from, src_scls, ptr_target, is_insert=is_insert, is_update=is_update, derived_name=derived_name, ctx=ctx) elif ptrcls.get_target(ctx.env.schema) != ptr_target: ctx.env.schema = ptrcls.set_target(ctx.env.schema, ptr_target) assert ptrcls is not None if qlexpr is None: # This is not a computable, just a pointer # to a nested shape. Have it reuse the original # pointer name so that in `Foo.ptr.name` and # `Foo { ptr: {name}}` are the same path. path_id_name = base_ptrcls.get_name(ctx.env.schema) ctx.env.schema = ptrcls.set_field_value( ctx.env.schema, 'path_id_name', path_id_name ) if qlexpr is not None: ctx.source_map[ptrcls] = irast.ComputableInfo( qlexpr=qlexpr, context=ctx, path_id=path_id, path_id_ns=path_id_namespace, shape_op=shape_el.operation.op, ) if compexpr is not None or is_polymorphic: ctx.env.schema = ptrcls.set_field_value( ctx.env.schema, 'computable', True, ) ctx.env.schema = ptrcls.set_field_value( ctx.env.schema, 'owned', True, ) if ptr_cardinality is not None: ctx.env.schema = ptrcls.set_field_value( ctx.env.schema, 'cardinality', ptr_cardinality) else: if qlexpr is None and ptrcls is not base_ptrcls: ctx.env.pointer_derivation_map[base_ptrcls].append(ptrcls) base_cardinality = None base_required = False if base_ptrcls is not None and not base_ptrcls_is_alias: base_cardinality = _get_base_ptr_cardinality(base_ptrcls, ctx=ctx) base_required = base_ptrcls.get_required(ctx.env.schema) if base_cardinality is None or not base_cardinality.is_known(): specified_cardinality = shape_el.cardinality specified_required = shape_el.required else: specified_cardinality = base_cardinality specified_required = base_required if (shape_el.cardinality is not None and base_ptrcls is not None and shape_el.cardinality != base_cardinality): base_src = base_ptrcls.get_source(ctx.env.schema) assert base_src is not None base_src_name = base_src.get_verbosename(ctx.env.schema) raise errors.SchemaError( f'cannot redefine the cardinality of ' f'{ptrcls.get_verbosename(ctx.env.schema)}: ' f'it is defined as {base_cardinality.as_ptr_qual()!r} ' f'in the base {base_src_name}', context=compexpr and compexpr.context, ) # The required flag may be inherited from the base specified_required = shape_el.required or base_required ctx.env.pointer_specified_info[ptrcls] = ( specified_cardinality, specified_required, shape_el.context) ctx.env.schema = ptrcls.set_field_value( ctx.env.schema, 'cardinality', qltypes.SchemaCardinality.Unknown) if ( ptrcls.is_protected_pointer(ctx.env.schema) and qlexpr is not None and not from_default and not ctx.env.options.allow_writing_protected_pointers ): ptrcls_sn = ptrcls.get_shortname(ctx.env.schema) if is_polymorphic: msg = (f'cannot access {ptrcls_sn.name} on a polymorphic ' f'shape element') else: msg = f'cannot assign to {ptrcls_sn.name}' raise errors.QueryError(msg, context=shape_el.context) if is_update and ptrcls.get_readonly(ctx.env.schema): raise errors.QueryError( f'cannot update {ptrcls.get_verbosename(ctx.env.schema)}: ' f'it is declared as read-only', context=compexpr and compexpr.context, ) return ptrcls
def get_path_var(rel: pgast.Query, path_id: irast.PathId, *, aspect: str, env: context.Environment) -> pgast.BaseExpr: """Return a value expression for a given *path_id* in a given *rel*.""" if isinstance(rel, pgast.CommonTableExpr): rel = rel.query # Check if we already have a var, before remapping the path_id. # This is useful for serialized aspect disambiguation in tuples, # since process_set_as_tuple() records serialized vars with # original path_id. if (path_id, aspect) in rel.path_namespace: return rel.path_namespace[path_id, aspect] if rel.view_path_id_map: path_id = map_path_id(path_id, rel.view_path_id_map) if (path_id, aspect) in rel.path_namespace: return rel.path_namespace[path_id, aspect] ptrref = path_id.rptr() is_type_indirection = path_id.is_type_indirection_path() if ptrref is not None and not is_type_indirection: ptr_info = pg_types.get_ptrref_storage_info(ptrref, resolve_type=False, link_bias=False) ptr_dir = path_id.rptr_dir() is_inbound = ptr_dir == s_pointers.PointerDirection.Inbound if is_inbound: src_path_id = path_id else: src_path_id = path_id.src_path() src_rptr = src_path_id.rptr() if (irtyputils.is_id_ptrref(ptrref) and (src_rptr is None or not irtyputils.is_inbound_ptrref(src_rptr))): # When there is a reference to the id property of # an object which is linked to by a link stored # inline, we want to route the reference to the # inline attribute. For example, # Foo.__type__.id gets resolved to the Foo.__type__ # column. This can only be done if Foo is visible # in scope, and Foo.__type__ is not a computable. pid = src_path_id while pid.is_type_indirection_path(): # Skip type indirection step(s). src_pid = pid.src_path() if src_pid is not None: src_rptr = src_pid.rptr() pid = src_pid else: break src_src_is_visible = env.ptrref_source_visibility.get(src_rptr) if (src_rptr is not None and not irtyputils.is_computable_ptrref(src_rptr) and src_src_is_visible): src_ptr_info = pg_types.get_ptrref_storage_info( src_rptr, resolve_type=False, link_bias=False) if src_ptr_info.table_type == 'ObjectType': src_path_id = src_path_id.src_path() ptr_info = src_ptr_info else: ptr_info = None src_path_id = None ptr_dir = None var: typing.Optional[pgast.BaseExpr] if astutils.is_set_op_query(rel): cb = functools.partial(get_path_output_or_null, env=env, path_id=path_id, aspect=aspect) outputs = astutils.for_each_query_in_set(rel, cb) first = None optional = False all_null = True nullable = False for colref, is_null in outputs: if colref.nullable: nullable = True if first is None: first = colref if is_null: optional = True else: all_null = False if all_null: raise LookupError(f'cannot find refs for ' f'path {path_id} {aspect} in {rel}') # Path vars produced by UNION expressions can be "optional", # i.e the record is accepted as-is when such var is NULL. # This is necessary to correctly join heterogeneous UNIONs. var = astutils.strip_output_var(first, optional=optional, nullable=optional or nullable) put_path_var(rel, path_id, var, aspect=aspect, env=env) return var if ptrref is None: if len(path_id) == 1: # This is an scalar set derived from an expression. src_path_id = path_id elif ptrref.parent_ptr is not None: if ptr_info.table_type != 'link' and not is_inbound: # This is a link prop that is stored in source rel, # step back to link source rvar. src_path_id = path_id.src_path().src_path() elif (is_type_indirection or (ptr_info.table_type != 'ObjectType' and not is_inbound)): # Ref is in the mapping rvar. src_path_id = path_id.ptr_path() rel_rvar = maybe_get_path_rvar(rel, path_id, aspect=aspect, env=env) if rel_rvar is None: alt_aspect = get_less_specific_aspect(path_id, aspect) if alt_aspect is not None: rel_rvar = maybe_get_path_rvar(rel, path_id, aspect=alt_aspect, env=env) else: alt_aspect = None if rel_rvar is None: if src_path_id.is_objtype_path(): src_aspect = 'source' else: src_aspect = aspect if src_path_id.is_tuple_path(): rel_rvar = maybe_get_path_rvar(rel, src_path_id, aspect=src_aspect, env=env) if rel_rvar is None: rel_rvar = maybe_get_path_rvar(rel, src_path_id.src_path(), aspect=src_aspect, env=env) else: rel_rvar = maybe_get_path_rvar(rel, src_path_id, aspect=src_aspect, env=env) if (rel_rvar is None and src_aspect != 'source' and path_id != src_path_id): rel_rvar = maybe_get_path_rvar(rel, src_path_id, aspect='source', env=env) if rel_rvar is None and alt_aspect is not None: # There is no source range var for the requested aspect, # check if there is a cached var with less specificity. var = rel.path_namespace.get((path_id, alt_aspect)) if var is not None: put_path_var(rel, path_id, var, aspect=aspect, env=env) return var if rel_rvar is None: raise LookupError(f'there is no range var for ' f'{src_path_id} {src_aspect} in {rel}') source_rel = rel_rvar.query drilldown_path_id = map_path_id(path_id, rel.view_path_id_map) if source_rel in env.root_rels and len(source_rel.path_scope) == 1: if not drilldown_path_id.is_objtype_path() and ptrref is not None: outer_path_id = drilldown_path_id.src_path() else: outer_path_id = drilldown_path_id path_id_map = {outer_path_id: next(iter(source_rel.path_scope))} drilldown_path_id = map_path_id(drilldown_path_id, path_id_map) outvar = get_path_output(source_rel, drilldown_path_id, ptr_info=ptr_info, aspect=aspect, env=env) var = astutils.get_rvar_var(rel_rvar, outvar) put_path_var(rel, path_id, var, aspect=aspect, env=env) if isinstance(var, pgast.TupleVar): for element in var.elements: put_path_var_if_not_exists(rel, element.path_id, element.val, aspect=aspect, env=env) return var
def _get_path_output(rel: pgast.BaseRelation, path_id: irast.PathId, *, aspect: str, allow_nullable: bool = True, ptr_info: typing.Optional[ pg_types.PointerStorageInfo] = None, env: context.Environment) -> pgast.OutputVar: result = rel.path_outputs.get((path_id, aspect)) if result is not None: return result ref: pgast.BaseExpr alias = None rptr = path_id.rptr() if rptr is not None and irtyputils.is_id_ptrref(rptr): # A value reference to Object.id is the same as a value # reference to the Object itself. src_path_id = path_id.src_path() id_output = rel.path_outputs.get((src_path_id, 'value')) if id_output is not None: _put_path_output_var(rel, path_id, aspect, id_output, env=env) return id_output if is_terminal_relation(rel): return _get_rel_path_output(rel, path_id, aspect=aspect, ptr_info=ptr_info, env=env) assert isinstance(rel, pgast.Query) if is_values_relation(rel): # The VALUES() construct seems to always expose its # value as "column1". alias = 'column1' ref = pgast.ColumnRef(name=[alias]) else: ref = get_path_var(rel, path_id, aspect=aspect, env=env) other_output = find_path_output(rel, path_id, ref, env=env) if other_output is not None: _put_path_output_var(rel, path_id, aspect, other_output, env=env) return other_output if isinstance(ref, pgast.TupleVarBase): elements = [] for el in ref.elements: el_path_id = reverse_map_path_id(el.path_id, rel.view_path_id_map) try: # Similarly to get_path_var(), check for outer path_id # first for tuple serialized var disambiguation. element = _get_path_output(rel, el_path_id, aspect=aspect, allow_nullable=False, env=env) except LookupError: element = get_path_output(rel, el_path_id, aspect=aspect, allow_nullable=False, env=env) elements.append( pgast.TupleElementBase(path_id=el_path_id, name=element)) result = pgast.TupleVarBase(elements=elements, named=ref.named) else: if astutils.is_set_op_query(rel): assert isinstance(ref, pgast.OutputVar) result = astutils.strip_output_var(ref) else: assert isinstance(rel, pgast.ReturningQuery), \ "expected ReturningQuery" if alias is None: alias = get_path_output_alias(path_id, aspect, env=env) restarget = pgast.ResTarget(name=alias, val=ref, ser_safe=getattr( ref, 'ser_safe', False)) rel.target_list.append(restarget) nullable = is_nullable(ref, env=env) optional = None if isinstance(ref, pgast.ColumnRef): optional = ref.optional if nullable and not allow_nullable: var = get_path_var(rel, path_id, aspect=aspect, env=env) rel.where_clause = astutils.extend_binop( rel.where_clause, pgast.NullTest(arg=var, negated=True)) nullable = False result = pgast.ColumnRef(name=[alias], nullable=nullable, optional=optional) _put_path_output_var(rel, path_id, aspect, result, env=env) if (path_id.is_objtype_path() and not isinstance(result, pgast.TupleVarBase)): equiv_aspect = None if aspect == 'identity': equiv_aspect = 'value' elif aspect == 'value': equiv_aspect = 'identity' if (equiv_aspect is not None and (path_id, equiv_aspect) not in rel.path_outputs): _put_path_output_var(rel, path_id, equiv_aspect, result, env=env) return result
def _get_rel_path_output(rel: pgast.BaseRelation, path_id: irast.PathId, *, aspect: str, ptr_info: typing.Optional[ pg_types.PointerStorageInfo] = None, env: context.Environment) -> pgast.OutputVar: if path_id.is_objtype_path(): if aspect == 'identity': aspect = 'value' if aspect != 'value': raise LookupError( f'invalid request for non-scalar path {path_id} {aspect}') if (path_id == rel.path_id or (rel.path_id.is_type_indirection_path() and path_id == rel.path_id.src_path())): return _get_rel_object_id_output(rel, path_id, aspect=aspect, env=env) else: if aspect == 'identity': raise LookupError( f'invalid request for scalar path {path_id} {aspect}') elif aspect == 'serialized': aspect = 'value' var = rel.path_outputs.get((path_id, aspect)) if var is not None: return var ptrref = path_id.rptr() rptr_dir = path_id.rptr_dir() if (rptr_dir is not None and rptr_dir != s_pointers.PointerDirection.Outbound): raise LookupError( f'{path_id} is an inbound pointer and cannot be resolved ' f'on a base relation') if isinstance(rel, pgast.NullRelation): if ptrref is not None: target = ptrref.out_target else: target = path_id.target pg_type = pg_types.pg_type_from_ir_typeref(target) if ptr_info is not None: name = env.aliases.get(ptr_info.column_name) else: name = env.aliases.get('v') val = pgast.TypeCast(arg=pgast.NullConstant(), type_name=pgast.TypeName(name=pg_type, )) rel.target_list.append(pgast.ResTarget(name=name, val=val)) result = pgast.ColumnRef(name=[name], nullable=True) else: if ptrref is None: raise ValueError( f'could not resolve trailing pointer class for {path_id}') if ptr_info is None: ptr_info = pg_types.get_ptrref_storage_info(ptrref, resolve_type=False, link_bias=False) result = pgast.ColumnRef(name=[ptr_info.column_name], nullable=not ptrref.required) _put_path_output_var(rel, path_id, aspect, result, env=env) return result
def _normalize_view_ptr_expr(shape_el: qlast.ShapeElement, view_scls: s_types.Type, *, path_id: irast.PathId, path_id_namespace: Optional[ irast.WeakNamespace] = None, is_insert: bool = False, is_update: bool = False, view_rptr: Optional[context.ViewRPtr] = None, ctx: context.ContextLevel) -> s_pointers.Pointer: steps = shape_el.expr.steps is_linkprop = False is_polymorphic = False is_mutation = is_insert or is_update # Pointers may be qualified by the explicit source # class, which is equivalent to Expr[IS Type]. plen = len(steps) ptrsource = view_scls qlexpr = None target_typexpr = None source: qlast.Base if plen >= 2 and isinstance(steps[-1], qlast.TypeIndirection): # Target type indirection: foo: Type target_typexpr = steps[-1].type plen -= 1 steps = steps[:-1] if plen == 1: # regular shape lexpr = steps[0] assert isinstance(lexpr, qlast.Ptr) is_linkprop = lexpr.type == 'property' if is_linkprop: if view_rptr is None: raise errors.QueryError( 'invalid reference to link property ' 'in top level shape', context=lexpr.context) ptrsource = view_rptr.ptrcls source = qlast.Source() elif plen == 2 and isinstance(steps[0], qlast.TypeIndirection): # Source type indirection: [IS Type].foo source = qlast.Path(steps=[ qlast.Source(), steps[0], ]) lexpr = steps[1] ptype = steps[0].type if not isinstance(ptype, qlast.TypeName): raise errors.QueryError( 'complex type expressions are not supported here', context=ptype.context, ) ptrsource = schemactx.get_schema_type(ptype.maintype, ctx=ctx) is_polymorphic = True else: # pragma: no cover raise RuntimeError( f'unexpected path length in view shape: {len(steps)}') assert isinstance(lexpr, qlast.Ptr) ptrname = lexpr.ptr.name compexpr = shape_el.compexpr if compexpr is None and is_insert and shape_el.elements: # Short shape form in INSERT, e.g # INSERT Foo { bar: Spam { name := 'name' }} # is prohibited. raise errors.EdgeQLSyntaxError("unexpected ':'", context=steps[-1].context) if compexpr is None: ptrcls = setgen.resolve_ptr(ptrsource, ptrname, ctx=ctx) if is_polymorphic: ptrcls = schemactx.derive_ptr(ptrcls, view_scls, is_insert=is_insert, is_update=is_update, ctx=ctx) base_ptrcls = ptrcls.get_bases(ctx.env.schema).first(ctx.env.schema) base_ptr_is_computable = base_ptrcls in ctx.source_map ptr_name = sn.Name( module='__', name=ptrcls.get_shortname(ctx.env.schema).name, ) if (shape_el.where or shape_el.orderby or shape_el.offset or shape_el.limit or base_ptr_is_computable or is_polymorphic or target_typexpr is not None): if target_typexpr is None: qlexpr = qlast.Path(steps=[source, lexpr]) else: qlexpr = qlast.Path(steps=[ source, lexpr, qlast.TypeIndirection(type=target_typexpr), ]) qlexpr = astutils.ensure_qlstmt(qlexpr) qlexpr.where = shape_el.where qlexpr.orderby = shape_el.orderby if shape_el.offset or shape_el.limit: qlexpr = qlast.SelectQuery(result=qlexpr, implicit=True) qlexpr.offset = shape_el.offset qlexpr.limit = shape_el.limit if target_typexpr is not None: ptr_target = schemactx.get_schema_type(target_typexpr.maintype, ctx=ctx) else: ptr_target = ptrcls.get_target(ctx.env.schema) if base_ptrcls in ctx.pending_cardinality: # We do not know the parent's pointer cardinality yet. ptr_cardinality = None ctx.pointer_derivation_map[base_ptrcls].append(ptrcls) stmtctx.pend_pointer_cardinality_inference( ptrcls=ptrcls, specified_card=shape_el.cardinality, source_ctx=shape_el.context, ctx=ctx) else: ptr_cardinality = base_ptrcls.get_cardinality(ctx.env.schema) implicit_tid = has_implicit_tid( ptr_target, is_mutation=is_mutation, ctx=ctx, ) if shape_el.elements or implicit_tid: sub_view_rptr = context.ViewRPtr( ptrsource if is_linkprop else view_scls, ptrcls=ptrcls, is_insert=is_insert, is_update=is_update) sub_path_id = pathctx.extend_path_id(path_id, ptrcls=base_ptrcls, target=ptrcls.get_target( ctx.env.schema), ns=ctx.path_id_namespace, ctx=ctx) ctx.path_scope.attach_path(sub_path_id) if is_update: for subel in shape_el.elements or []: is_prop = (isinstance(subel.expr.steps[0], qlast.Ptr) and subel.expr.steps[0].type == 'property') if not is_prop: raise errors.QueryError( 'only references to link properties are allowed ' 'in nested UPDATE shapes', context=subel.context) ptr_target = _process_view(stype=ptr_target, path_id=sub_path_id, path_id_namespace=path_id_namespace, view_rptr=sub_view_rptr, elements=shape_el.elements, is_update=True, ctx=ctx) else: ptr_target = _process_view(stype=ptr_target, path_id=sub_path_id, path_id_namespace=path_id_namespace, view_rptr=sub_view_rptr, elements=shape_el.elements, ctx=ctx) else: base_ptrcls = ptrcls = None if (is_mutation and ptrname not in ctx.special_computables_in_mutation_shape): # If this is a mutation, the pointer must exist. ptrcls = setgen.resolve_ptr(ptrsource, ptrname, ctx=ctx) base_ptrcls = ptrcls.get_bases(ctx.env.schema).first( ctx.env.schema) ptr_name = sn.Name( module='__', name=ptrcls.get_shortname(ctx.env.schema).name, ) else: # Otherwise, assume no pointer inheritance. # Every computable is a new pointer derived from # std::link or std::property. There is one exception: # pointer aliases (Foo {some := Foo.other}), where `foo` # gets derived from `Foo.other`. This logic is applied # in compile_query_subject() by populating the base_ptrcls. ptr_name = sn.Name( module='__', name=ptrname, ) qlexpr = astutils.ensure_qlstmt(compexpr) with ctx.newscope(fenced=True) as shape_expr_ctx: # Put current pointer class in context, so # that references to link properties in sub-SELECT # can be resolved. This is necessary for proper # evaluation of link properties on computable links, # most importantly, in INSERT/UPDATE context. shape_expr_ctx.view_rptr = context.ViewRPtr( ptrsource if is_linkprop else view_scls, ptrcls=ptrcls, ptrcls_name=ptr_name, ptrcls_is_linkprop=is_linkprop, is_insert=is_insert, is_update=is_update) shape_expr_ctx.defining_view = True shape_expr_ctx.path_scope.unnest_fence = True shape_expr_ctx.partial_path_prefix = setgen.class_set( view_scls, path_id=path_id, ctx=shape_expr_ctx) prefix_rptrref = path_id.rptr() if prefix_rptrref is not None: # Source path seems to contain multiple steps, # so set up a rptr for abbreviated link property # paths. src_path_id = path_id.src_path() prefix_rptr = irast.Pointer( source=setgen.class_set( irtyputils.ir_typeref_to_type( shape_expr_ctx.env.schema, src_path_id.target, ), path_id=src_path_id, ctx=shape_expr_ctx, ), target=shape_expr_ctx.partial_path_prefix, ptrref=prefix_rptrref, direction=s_pointers.PointerDirection.Outbound, ) shape_expr_ctx.partial_path_prefix.rptr = prefix_rptr if is_mutation and ptrcls is not None: shape_expr_ctx.expr_exposed = True shape_expr_ctx.empty_result_type_hint = \ ptrcls.get_target(ctx.env.schema) irexpr = dispatch.compile(qlexpr, ctx=shape_expr_ctx) irexpr.context = compexpr.context if base_ptrcls is None: base_ptrcls = shape_expr_ctx.view_rptr.base_ptrcls ptr_cardinality = None ptr_target = inference.infer_type(irexpr, ctx.env) anytype = ptr_target.find_any(ctx.env.schema) if anytype is not None: raise errors.QueryError( 'expression returns value of indeterminate type', context=ctx.env.type_origins.get(anytype), ) # Validate that the insert/update expression is # of the correct class. if is_mutation and ptrcls is not None: base_target = ptrcls.get_target(ctx.env.schema) assert base_target is not None if ptr_target.assignment_castable_to(base_target, schema=ctx.env.schema): # Force assignment casts if the target type is not a # subclass of the base type and the cast is not to an # object type. if not (base_target.is_object_type() or ptr_target.issubclass(ctx.env.schema, base_target)): qlexpr = astutils.ensure_qlstmt( qlast.TypeCast( type=astutils.type_to_ql_typeref( base_target, schema=ctx.env.schema), expr=compexpr, )) ptr_target = base_target else: expected = [ repr(str(base_target.get_displayname(ctx.env.schema))) ] ercls: Type[errors.EdgeDBError] if ptrcls.is_property(ctx.env.schema): ercls = errors.InvalidPropertyTargetError else: ercls = errors.InvalidLinkTargetError ptr_vn = ptrcls.get_verbosename(ctx.env.schema, with_parent=True) raise ercls( f'invalid target for {ptr_vn}: ' f'{str(ptr_target.get_displayname(ctx.env.schema))!r} ' f'(expecting {" or ".join(expected)})') if qlexpr is not None or ptrcls is None: if is_linkprop: # Proper checking was done when is_linkprop is defined. assert view_rptr is not None src_scls = view_rptr.ptrcls else: src_scls = view_scls if ptr_target.is_object_type(): base = ctx.env.get_track_schema_object('std::link') else: base = ctx.env.get_track_schema_object('std::property') if base_ptrcls is not None: derive_from = base_ptrcls else: derive_from = base derived_name = schemactx.derive_view_name( base_ptrcls, derived_name_base=ptr_name, derived_name_quals=[src_scls.get_name(ctx.env.schema)], ctx=ctx) existing = ctx.env.schema.get(derived_name, None) if existing is not None: existing_target = existing.get_target(ctx.env.schema) if ptr_target == existing_target: ptrcls = existing elif ptr_target.implicitly_castable_to(existing_target, ctx.env.schema): ctx.env.schema = existing.set_target(ctx.env.schema, ptr_target) ptrcls = existing else: target_rptr_set = (ptr_target.get_rptr(ctx.env.schema) is not None) if target_rptr_set: ctx.env.schema = ptr_target.set_field_value( ctx.env.schema, 'rptr', None, ) ctx.env.schema = existing.delete(ctx.env.schema) ptrcls = schemactx.derive_ptr(derive_from, src_scls, ptr_target, is_insert=is_insert, is_update=is_update, derived_name=derived_name, inheritance_merge=False, ctx=ctx) if target_rptr_set: ctx.env.schema = ptr_target.set_field_value( ctx.env.schema, 'rptr', ptrcls, ) else: ptrcls = schemactx.derive_ptr(derive_from, src_scls, ptr_target, is_insert=is_insert, is_update=is_update, derived_name=derived_name, ctx=ctx) elif ptrcls.get_target(ctx.env.schema) != ptr_target: ctx.env.schema = ptrcls.set_target(ctx.env.schema, ptr_target) assert ptrcls is not None if qlexpr is None: # This is not a computable, just a pointer # to a nested shape. Have it reuse the original # pointer name so that in `Foo.ptr.name` and # `Foo { ptr: {name}}` are the same path. path_id_name = base_ptrcls.get_name(ctx.env.schema) ctx.env.schema = ptrcls.set_field_value(ctx.env.schema, 'path_id_name', path_id_name) if qlexpr is not None: ctx.source_map[ptrcls] = (qlexpr, ctx, path_id, path_id_namespace) if not is_mutation: if ptr_cardinality is None: if qlexpr is None and ptrcls is not base_ptrcls: ctx.pointer_derivation_map[base_ptrcls].append(ptrcls) stmtctx.pend_pointer_cardinality_inference( ptrcls=ptrcls, specified_card=shape_el.cardinality, source_ctx=shape_el.context, ctx=ctx) ctx.env.schema = ptrcls.set_field_value(ctx.env.schema, 'cardinality', None) else: ctx.env.schema = ptrcls.set_field_value(ctx.env.schema, 'cardinality', ptr_cardinality) if ptrcls.is_protected_pointer(ctx.env.schema) and qlexpr is not None: ptrcls_sn = ptrcls.get_shortname(ctx.env.schema) if is_polymorphic: msg = (f'cannot access {ptrcls_sn.name} on a polymorphic ' f'shape element') else: msg = f'cannot assign to {ptrcls_sn.name}' raise errors.QueryError(msg, context=shape_el.context) return ptrcls
def get_path_var( rel: pgast.Query, path_id: irast.PathId, *, aspect: str, env: context.Environment) -> pgast.BaseExpr: """Return a value expression for a given *path_id* in a given *rel*.""" if isinstance(rel, pgast.CommonTableExpr): rel = rel.query # Check if we already have a var, before remapping the path_id. # This is useful for serialized aspect disambiguation in tuples, # since process_set_as_tuple() records serialized vars with # original path_id. if (path_id, aspect) in rel.path_namespace: return rel.path_namespace[path_id, aspect] if rel.view_path_id_map: path_id = map_path_id(path_id, rel.view_path_id_map) if (path_id, aspect) in rel.path_namespace: return rel.path_namespace[path_id, aspect] if astutils.is_set_op_query(rel): return _get_path_var_in_setop(rel, path_id, aspect=aspect, env=env) ptrref = path_id.rptr() is_type_intersection = path_id.is_type_intersection_path() src_path_id: Optional[irast.PathId] = None if ptrref is not None and not is_type_intersection: ptr_info = pg_types.get_ptrref_storage_info( ptrref, resolve_type=False, link_bias=False, allow_missing=True) ptr_dir = path_id.rptr_dir() is_inbound = ptr_dir == s_pointers.PointerDirection.Inbound if is_inbound: src_path_id = path_id else: src_path_id = path_id.src_path() assert src_path_id is not None src_rptr = src_path_id.rptr() if (irtyputils.is_id_ptrref(ptrref) and (src_rptr is None or not irtyputils.is_inbound_ptrref(src_rptr))): # When there is a reference to the id property of # an object which is linked to by a link stored # inline, we want to route the reference to the # inline attribute. For example, # Foo.__type__.id gets resolved to the Foo.__type__ # column. This can only be done if Foo is visible # in scope, and Foo.__type__ is not a computable. pid = src_path_id while pid.is_type_intersection_path(): # Skip type intersection step(s). src_pid = pid.src_path() if src_pid is not None: src_rptr = src_pid.rptr() pid = src_pid else: break if (src_rptr is not None and not irtyputils.is_computable_ptrref(src_rptr) and env.ptrref_source_visibility.get(src_rptr)): src_ptr_info = pg_types.get_ptrref_storage_info( src_rptr, resolve_type=False, link_bias=False, allow_missing=True) if (src_ptr_info and src_ptr_info.table_type == 'ObjectType'): src_path_id = src_path_id.src_path() ptr_info = src_ptr_info else: ptr_info = None ptr_dir = None var: Optional[pgast.BaseExpr] if ptrref is None: if len(path_id) == 1: # This is an scalar set derived from an expression. src_path_id = path_id elif ptrref.source_ptr is not None: if ptr_info and ptr_info.table_type != 'link' and not is_inbound: # This is a link prop that is stored in source rel, # step back to link source rvar. _prefix_pid = path_id.src_path() assert _prefix_pid is not None src_path_id = _prefix_pid.src_path() elif is_type_intersection: src_path_id = path_id assert src_path_id is not None # Find which rvar will have path_id as an output src_aspect, rel_rvar, found_path_var = _find_rel_rvar( rel, path_id, src_path_id, aspect=aspect, env=env) if found_path_var: return found_path_var if rel_rvar is None: raise LookupError( f'there is no range var for ' f'{src_path_id} {src_aspect} in {rel}') if isinstance(rel_rvar, pgast.IntersectionRangeVar): if ( (path_id.is_objtype_path() and src_path_id == path_id) or (ptrref is not None and irtyputils.is_id_ptrref(ptrref)) ): rel_rvar = rel_rvar.component_rvars[-1] else: # Intersection rvars are basically JOINs of the relevant # parts of the type intersection, and so we need to make # sure we pick the correct component relation of that JOIN. rel_rvar = _find_rvar_in_intersection_by_typeref( path_id, rel_rvar.component_rvars, ) source_rel = rel_rvar.query if isinstance(ptrref, irast.PointerRef) and rel_rvar.typeref is not None: actual_ptrref = irtyputils.maybe_find_actual_ptrref( rel_rvar.typeref, ptrref) if actual_ptrref is not None: ptr_info = pg_types.get_ptrref_storage_info( actual_ptrref, resolve_type=False, link_bias=False) outvar = get_path_output( source_rel, path_id, ptr_info=ptr_info, aspect=aspect, env=env) var = astutils.get_rvar_var(rel_rvar, outvar) put_path_var(rel, path_id, var, aspect=aspect, env=env) if isinstance(var, pgast.TupleVar): for element in var.elements: put_path_var_if_not_exists(rel, element.path_id, element.val, aspect=aspect, env=env) return var
def _compile_qlexpr( qlexpr: qlast.Base, view_scls: s_objtypes.ObjectType, *, ptrcls: Optional[s_pointers.Pointer], ptrsource: s_sources.Source, path_id: irast.PathId, ptr_name: sn.QualName, is_insert: bool, is_update: bool, is_linkprop: bool, ctx: context.ContextLevel, ) -> Tuple[irast.Set, context.ViewRPtr]: is_mutation = is_insert or is_update with ctx.newscope(fenced=True) as shape_expr_ctx: # Put current pointer class in context, so # that references to link properties in sub-SELECT # can be resolved. This is necessary for proper # evaluation of link properties on computable links, # most importantly, in INSERT/UPDATE context. shape_expr_ctx.view_rptr = context.ViewRPtr( ptrsource if is_linkprop else view_scls, ptrcls=ptrcls, ptrcls_name=ptr_name, ptrcls_is_linkprop=is_linkprop, is_insert=is_insert, is_update=is_update, ) shape_expr_ctx.defining_view = view_scls shape_expr_ctx.path_scope.unnest_fence = True shape_expr_ctx.partial_path_prefix = setgen.class_set( view_scls.get_bases(ctx.env.schema).first(ctx.env.schema), path_id=path_id, ctx=shape_expr_ctx) prefix_rptrref = path_id.rptr() if prefix_rptrref is not None: # Source path seems to contain multiple steps, # so set up a rptr for abbreviated link property # paths. src_path_id = path_id.src_path() assert src_path_id is not None ctx.env.schema, src_t = irtyputils.ir_typeref_to_type( shape_expr_ctx.env.schema, src_path_id.target, ) prefix_rptr = irast.Pointer( source=setgen.class_set( src_t, path_id=src_path_id, ctx=shape_expr_ctx, ), target=shape_expr_ctx.partial_path_prefix, ptrref=prefix_rptrref, direction=s_pointers.PointerDirection.Outbound, ) shape_expr_ctx.partial_path_prefix.rptr = prefix_rptr if is_mutation and ptrcls is not None: shape_expr_ctx.expr_exposed = True shape_expr_ctx.empty_result_type_hint = \ ptrcls.get_target(ctx.env.schema) irexpr = dispatch.compile(qlexpr, ctx=shape_expr_ctx) return irexpr, shape_expr_ctx.view_rptr
def get_path_var( rel: pgast.Query, path_id: irast.PathId, *, aspect: str, env: context.Environment) -> pgast.BaseExpr: """Return a value expression for a given *path_id* in a given *rel*.""" if isinstance(rel, pgast.CommonTableExpr): rel = rel.query # Check if we already have a var, before remapping the path_id. # This is useful for serialized aspect disambiguation in tuples, # since process_set_as_tuple() records serialized vars with # original path_id. if (path_id, aspect) in rel.path_namespace: return rel.path_namespace[path_id, aspect] if rel.view_path_id_map: path_id = map_path_id(path_id, rel.view_path_id_map) if (path_id, aspect) in rel.path_namespace: return rel.path_namespace[path_id, aspect] ptrref = path_id.rptr() is_type_intersection = path_id.is_type_intersection_path() src_path_id: Optional[irast.PathId] = None if ptrref is not None and not is_type_intersection: ptr_info = pg_types.get_ptrref_storage_info( ptrref, resolve_type=False, link_bias=False, allow_missing=True) ptr_dir = path_id.rptr_dir() is_inbound = ptr_dir == s_pointers.PointerDirection.Inbound if is_inbound: src_path_id = path_id else: src_path_id = path_id.src_path() assert src_path_id is not None src_rptr = src_path_id.rptr() if (irtyputils.is_id_ptrref(ptrref) and (src_rptr is None or not irtyputils.is_inbound_ptrref(src_rptr))): # When there is a reference to the id property of # an object which is linked to by a link stored # inline, we want to route the reference to the # inline attribute. For example, # Foo.__type__.id gets resolved to the Foo.__type__ # column. This can only be done if Foo is visible # in scope, and Foo.__type__ is not a computable. pid = src_path_id while pid.is_type_intersection_path(): # Skip type intersection step(s). src_pid = pid.src_path() if src_pid is not None: src_rptr = src_pid.rptr() pid = src_pid else: break if (src_rptr is not None and not irtyputils.is_computable_ptrref(src_rptr) and env.ptrref_source_visibility.get(src_rptr)): src_ptr_info = pg_types.get_ptrref_storage_info( src_rptr, resolve_type=False, link_bias=False, allow_missing=True) if (src_ptr_info and src_ptr_info.table_type == 'ObjectType'): src_path_id = src_path_id.src_path() ptr_info = src_ptr_info else: ptr_info = None ptr_dir = None var: Optional[pgast.BaseExpr] if astutils.is_set_op_query(rel): # We disable the find_path_output optimizaiton when doing # UNIONs to avoid situations where they have different numbers # of columns. cb = functools.partial( get_path_output_or_null, env=env, disable_output_fusion=True, path_id=path_id, aspect=aspect) outputs = astutils.for_each_query_in_set(rel, cb) first: Optional[pgast.OutputVar] = None optional = False all_null = True nullable = False for colref, is_null in outputs: if colref.nullable: nullable = True if first is None: first = colref if is_null: optional = True else: all_null = False if all_null: raise LookupError( f'cannot find refs for ' f'path {path_id} {aspect} in {rel}') if first is None: raise AssertionError( f'union did not produce any outputs') # Path vars produced by UNION expressions can be "optional", # i.e the record is accepted as-is when such var is NULL. # This is necessary to correctly join heterogeneous UNIONs. var = astutils.strip_output_var( first, optional=optional, nullable=optional or nullable) put_path_var(rel, path_id, var, aspect=aspect, env=env) return var if ptrref is None: if len(path_id) == 1: # This is an scalar set derived from an expression. src_path_id = path_id elif ptrref.source_ptr is not None: if ptr_info and ptr_info.table_type != 'link' and not is_inbound: # This is a link prop that is stored in source rel, # step back to link source rvar. _prefix_pid = path_id.src_path() assert _prefix_pid is not None src_path_id = _prefix_pid.src_path() elif is_type_intersection: src_path_id = path_id rel_rvar = maybe_get_path_rvar(rel, path_id, aspect=aspect, env=env) if rel_rvar is None: alt_aspect = get_less_specific_aspect(path_id, aspect) if alt_aspect is not None: rel_rvar = maybe_get_path_rvar( rel, path_id, aspect=alt_aspect, env=env) else: alt_aspect = None assert src_path_id is not None if rel_rvar is None: if src_path_id.is_objtype_path(): src_aspect = 'source' else: src_aspect = aspect if src_path_id.is_tuple_path(): rel_rvar = maybe_get_path_rvar( rel, src_path_id, aspect=src_aspect, env=env) if rel_rvar is None: _src_path_id_prefix = src_path_id.src_path() if _src_path_id_prefix is not None: rel_rvar = maybe_get_path_rvar( rel, _src_path_id_prefix, aspect=src_aspect, env=env) else: rel_rvar = maybe_get_path_rvar( rel, src_path_id, aspect=src_aspect, env=env) if (rel_rvar is None and src_aspect != 'source' and path_id != src_path_id): rel_rvar = maybe_get_path_rvar( rel, src_path_id, aspect='source', env=env) if rel_rvar is None and alt_aspect is not None: # There is no source range var for the requested aspect, # check if there is a cached var with less specificity. var = rel.path_namespace.get((path_id, alt_aspect)) if var is not None: put_path_var(rel, path_id, var, aspect=aspect, env=env) return var if rel_rvar is None: raise LookupError( f'there is no range var for ' f'{src_path_id} {src_aspect} in {rel}') if isinstance(rel_rvar, pgast.IntersectionRangeVar): if ( (path_id.is_objtype_path() and src_path_id == path_id) or (ptrref is not None and irtyputils.is_id_ptrref(ptrref)) ): rel_rvar = rel_rvar.component_rvars[-1] else: # Intersection rvars are basically JOINs of the relevant # parts of the type intersection, and so we need to make # sure we pick the correct component relation of that JOIN. rel_rvar = _find_rvar_in_intersection_by_typeref( path_id, rel_rvar.component_rvars, ) source_rel = rel_rvar.query if isinstance(ptrref, irast.PointerRef) and rel_rvar.typeref is not None: actual_ptrref = irtyputils.maybe_find_actual_ptrref( rel_rvar.typeref, ptrref) if actual_ptrref is not None: ptr_info = pg_types.get_ptrref_storage_info( actual_ptrref, resolve_type=False, link_bias=False) outvar = get_path_output( source_rel, path_id, ptr_info=ptr_info, aspect=aspect, env=env) var = astutils.get_rvar_var(rel_rvar, outvar) put_path_var(rel, path_id, var, aspect=aspect, env=env) if isinstance(var, pgast.TupleVar): for element in var.elements: put_path_var_if_not_exists(rel, element.path_id, element.val, aspect=aspect, env=env) return var