def compile_Coalesce(expr: qlast.Base, *, ctx: context.ContextLevel) -> irast.Base: if all(isinstance(a, qlast.Set) and not a.elements for a in expr.args): return irutils.new_empty_set(ctx.schema, alias=ctx.aliases.get('e')) with ctx.newscope() as newctx: leftmost_arg = larg = setgen.ensure_set(dispatch.compile(expr.args[0], ctx=newctx), ctx=newctx) for rarg_ql in expr.args[1:]: with newctx.new() as nestedscopectx: with nestedscopectx.newscope(fenced=True) as fencectx: rarg = setgen.scoped_set(dispatch.compile(rarg_ql, ctx=fencectx), ctx=fencectx) coalesce = irast.Coalesce(left=larg, right=rarg) larg = setgen.generated_set(coalesce, ctx=nestedscopectx) # Make sure any empty set types are properly resolved # before entering them into the scope tree. irutils.infer_type(larg, schema=ctx.schema) pathctx.register_set_in_scope(leftmost_arg, ctx=ctx) pathctx.mark_path_as_optional(leftmost_arg.path_id, ctx=ctx) return larg
def compile_Set( expr: qlast.Base, *, ctx: context.ContextLevel) -> irast.Base: if expr.elements: if len(expr.elements) == 1: # From the scope perspective, single-element set # literals are equivalent to a binary UNION with # an empty set, not to the element. with ctx.newscope(fenced=True) as scopectx: ir_set = dispatch.compile(expr.elements[0], ctx=scopectx) return setgen.scoped_set(ir_set, ctx=scopectx) else: elements = flatten_set(expr) # a set literal is just sugar for a UNION op = qlast.UNION bigunion = qlast.BinOp( left=elements[0], right=elements[1], op=op ) for el in elements[2:]: bigunion = qlast.BinOp( left=bigunion, right=el, op=op ) return dispatch.compile(bigunion, ctx=ctx) else: return irutils.new_empty_set(ctx.schema, alias=ctx.aliases.get('e'))
def _cast_expr(ql_type: qlast.TypeName, ir_expr: irast.Base, *, source_context: parsing.ParserContext, ctx: context.ContextLevel) -> irast.Base: try: orig_type = irutils.infer_type(ir_expr, ctx.schema) except errors.EdgeQLError: # It is possible that the source expression is unresolved # if the expr is an empty set (or a coalesce of empty sets). orig_type = None if isinstance(orig_type, s_types.Tuple): # For tuple-to-tuple casts we generate a new tuple # to simplify things on sqlgen side. new_type = typegen.ql_typeref_to_type(ql_type, ctx=ctx) if not isinstance(new_type, s_types.Tuple): raise errors.EdgeQLError(f'cannot cast tuple to {new_type.name}', context=source_context) if len(orig_type.element_types) != len(new_type.element_types): raise errors.EdgeQLError( f'cannot cast to {new_type.name}: ' f'number of elements is not the same', context=source_context) new_names = list(new_type.element_types) elements = [] for i, n in enumerate(orig_type.element_types): val = setgen.generated_set(irast.TupleIndirection(expr=ir_expr, name=n), ctx=ctx) val.path_id = irutils.tuple_indirection_path_id( ir_expr.path_id, n, orig_type.element_types[n]) val_type = irutils.infer_type(val, ctx.schema) new_el_name = new_names[i] if val_type != new_type.element_types[new_el_name]: # Element cast val = _cast_expr(ql_type.subtypes[i], val, ctx=ctx, source_context=source_context) elements.append(irast.TupleElement(name=new_el_name, val=val)) return irast.Tuple(named=new_type.named, elements=elements) elif isinstance(ir_expr, irast.EmptySet): # For the common case of casting an empty set, we simply # generate a new EmptySet node of the requested type. scls = typegen.ql_typeref_to_type(ql_type, ctx=ctx) return irutils.new_empty_set(ctx.schema, scls=scls, alias=ir_expr.path_id.target.name.name) else: typ = typegen.ql_typeref_to_ir_typeref(ql_type, ctx=ctx) return setgen.ensure_set(irast.TypeCast(expr=ir_expr, type=typ), ctx=ctx)
def compile_Coalesce(expr: qlast.Base, *, ctx: context.ContextLevel) -> irast.Base: if all(isinstance(a, qlast.Set) and not a.elements for a in expr.args): return irutils.new_empty_set(ctx.env.schema, alias=ctx.aliases.get('e')) # Due to the construction of relgen, the (unfenced) subscope # below is necessary to shield LHS paths from the outer query # to prevent path binding which may break OPTIONAL. with ctx.newscope() as newctx: leftmost_arg = larg = setgen.ensure_set(dispatch.compile(expr.args[0], ctx=newctx), ctx=newctx) for rarg_ql in expr.args[1:]: with newctx.new() as nestedscopectx: with nestedscopectx.newscope(fenced=True) as fencectx: rarg = setgen.scoped_set(dispatch.compile(rarg_ql, ctx=fencectx), force_reassign=True, ctx=fencectx) coalesce = irast.Coalesce(left=larg, right=rarg) larg = setgen.generated_set(coalesce, ctx=nestedscopectx) stmtctx.get_expr_cardinality_later(target=coalesce, field='right_card', irexpr=rarg, ctx=ctx) # Make sure any empty set types are properly resolved # before entering them into the scope tree. inference.infer_type(larg, env=ctx.env) pathctx.register_set_in_scope(leftmost_arg, ctx=ctx) pathctx.mark_path_as_optional(leftmost_arg.path_id, ctx=ctx) pathctx.assign_set_scope(leftmost_arg, newctx.path_scope, ctx=ctx) return larg
def compile_Coalesce(expr: qlast.Base, *, ctx: context.ContextLevel) -> irast.Base: if all(isinstance(a, qlast.Set) and not a.elements for a in expr.args): return irutils.new_empty_set(ctx.schema, alias=ctx.aliases.get('e')) with ctx.new() as newctx: larg = setgen.ensure_set(dispatch.compile(expr.args[0], ctx=newctx), ctx=newctx) lcard = irinference.infer_cardinality(larg, singletons=newctx.singletons, schema=newctx.schema) pathctx.register_set_in_scope(larg, ctx=ctx) pathctx.mark_path_as_optional(larg.path_id, ctx=ctx) for rarg_ql in expr.args[1:]: with newctx.new() as nestedscopectx: with nestedscopectx.newscope(fenced=True) as fencectx: rarg = setgen.scoped_set(dispatch.compile(rarg_ql, ctx=fencectx), ctx=fencectx) rcard = irinference.infer_cardinality( rarg, singletons=fencectx.singletons, schema=fencectx.schema) coalesce = irast.Coalesce(left=larg, lcardinality=lcard, right=rarg, rcardinality=rcard) larg = setgen.generated_set(coalesce, ctx=nestedscopectx) lcard = irinference.infer_cardinality( larg, singletons=nestedscopectx.singletons, schema=nestedscopectx.schema) return larg
def try_bind_call_args(args: typing.List[typing.Tuple[s_types.Type, irast.Base]], kwargs: typing.Dict[str, typing.Tuple[s_types.Type, irast.Base]], func: s_func.CallableObject, *, ctx: context.ContextLevel) -> BoundCall: def _get_cast_distance(arg, arg_type, param_type) -> int: nonlocal resolved_poly_base_type if in_polymorphic_func: # Compiling a body of a polymorphic function. if arg_type.is_polymorphic(schema): if param_type.is_polymorphic(schema): if arg_type.test_polymorphic(schema, param_type): return 0 else: return -1 else: if arg_type.resolve_polymorphic(schema, param_type): return 0 else: return -1 else: if arg_type.is_polymorphic(schema): raise errors.QueryError( f'a polymorphic argument in a non-polymorphic function', context=arg.context) if param_type.is_polymorphic(schema): if not arg_type.test_polymorphic(schema, param_type): return -1 resolved = param_type.resolve_polymorphic(schema, arg_type) if resolved is None: return -1 if resolved_poly_base_type is None: resolved_poly_base_type = resolved if resolved_poly_base_type == resolved: return 0 ct = resolved_poly_base_type.find_common_implicitly_castable_type( resolved, ctx.env.schema) return 0 if ct is not None else -1 if arg_type.issubclass(schema, param_type): return 0 return arg_type.get_implicit_cast_distance(param_type, schema) schema = ctx.env.schema in_polymorphic_func = (ctx.func is not None and ctx.func.get_params(schema).has_polymorphic(schema)) has_empty_variadic = False resolved_poly_base_type = None no_args_call = not args and not kwargs has_inlined_defaults = func.has_inlined_defaults(schema) func_params = func.get_params(schema) if not func_params: if no_args_call: # Match: `func` is a function without parameters # being called with no arguments. args = [] if has_inlined_defaults: bytes_t = schema.get('std::bytes') argval = setgen.ensure_set(irast.BytesConstant(value='\x00', stype=bytes_t), typehint=bytes_t, ctx=ctx) args = [BoundArg(None, argval, bytes_t, 0)] return BoundCall(func, args, set(), func.get_return_type(schema), False) else: # No match: `func` is a function without parameters # being called with some arguments. return _NO_MATCH pg_params = s_func.PgParams.from_params(schema, func_params) named_only = func_params.find_named_only(schema) if no_args_call and pg_params.has_param_wo_default: # A call without arguments and there is at least # one parameter without default. return _NO_MATCH bound_param_args = [] params = pg_params.params nparams = len(params) nargs = len(args) has_missing_args = False ai = 0 pi = 0 matched_kwargs = 0 # Bind NAMED ONLY arguments (they are compiled as first set of arguments). while True: if pi >= nparams: break param = params[pi] if param.get_kind(schema) is not _NAMED_ONLY: break pi += 1 param_shortname = param.get_shortname(schema) if param_shortname in kwargs: matched_kwargs += 1 arg_type, arg_val = kwargs[param_shortname] cd = _get_cast_distance(arg_val, arg_type, param.get_type(schema)) if cd < 0: return _NO_MATCH bound_param_args.append(BoundArg(param, arg_val, arg_type, cd)) else: if param.get_default(schema) is None: # required named parameter without default and # without a matching argument return _NO_MATCH has_missing_args = True bound_param_args.append( BoundArg(param, None, param.get_type(schema), 0)) if matched_kwargs != len(kwargs): # extra kwargs? return _NO_MATCH # Bind POSITIONAL arguments (compiled to go after NAMED ONLY arguments). while True: if ai < nargs: arg_type, arg_val = args[ai] ai += 1 if pi >= nparams: # too many positional arguments return _NO_MATCH param = params[pi] param_kind = param.get_kind(schema) pi += 1 if param_kind is _NAMED_ONLY: # impossible condition raise RuntimeError('unprocessed NAMED ONLY parameter') if param_kind is _VARIADIC: var_type = param.get_type(schema).get_subtypes()[0] cd = _get_cast_distance(arg_val, arg_type, var_type) if cd < 0: return _NO_MATCH bound_param_args.append(BoundArg(param, arg_val, arg_type, cd)) for arg_type, arg_val in args[ai:]: cd = _get_cast_distance(arg_val, arg_type, var_type) if cd < 0: return _NO_MATCH bound_param_args.append( BoundArg(param, arg_val, arg_type, cd)) break cd = _get_cast_distance(arg_val, arg_type, param.get_type(schema)) if cd < 0: return _NO_MATCH bound_param_args.append(BoundArg(param, arg_val, arg_type, cd)) else: break # Handle yet unprocessed POSITIONAL & VARIADIC arguments. for pi in range(pi, nparams): param = params[pi] param_kind = param.get_kind(schema) if param_kind is _POSITIONAL: if param.get_default(schema) is None: # required positional parameter that we don't have a # positional argument for. return _NO_MATCH has_missing_args = True param_type = param.get_type(schema) bound_param_args.append(BoundArg(param, None, param_type, 0)) elif param_kind is _VARIADIC: has_empty_variadic = True elif param_kind is _NAMED_ONLY: # impossible condition raise RuntimeError('unprocessed NAMED ONLY parameter') # Populate defaults. defaults_mask = 0 null_args = set() if has_missing_args: if has_inlined_defaults or named_only: for i in range(len(bound_param_args)): barg = bound_param_args[i] if barg.val is not None: continue param = barg.param param_shortname = param.get_shortname(schema) null_args.add(param_shortname) defaults_mask |= 1 << i if not has_inlined_defaults: ql_default = param.get_ql_default(schema) default = dispatch.compile(ql_default, ctx=ctx) empty_default = (has_inlined_defaults or irutils.is_empty(default)) param_type = param.get_type(schema) if empty_default: default_type = None if param_type.is_any(): if resolved_poly_base_type is None: raise errors.QueryError( f'could not resolve "anytype" type for the ' f'${param_shortname} parameter') else: default_type = resolved_poly_base_type else: default_type = param_type else: default_type = param_type if has_inlined_defaults: default = irutils.new_empty_set(schema, stype=default_type, alias=param_shortname) default = setgen.ensure_set(default, typehint=default_type, ctx=ctx) bound_param_args[i] = BoundArg( param, default, barg.valtype, barg.cast_distance, ) else: bound_param_args = [ barg for barg in bound_param_args if barg.val is not None ] if has_inlined_defaults: # If we are compiling an EdgeQL function, inject the defaults # bit-mask as a first argument. bytes_t = schema.get('std::bytes') bm = defaults_mask.to_bytes(nparams // 8 + 1, 'little') bm_set = setgen.ensure_set(irast.BytesConstant( value=bm.decode('ascii'), stype=bytes_t), typehint=bytes_t, ctx=ctx) bound_param_args.insert(0, BoundArg(None, bm_set, bytes_t, 0)) return_type = func.get_return_type(schema) if return_type.is_polymorphic(schema): if resolved_poly_base_type is not None: return_type = return_type.to_nonpolymorphic( schema, resolved_poly_base_type) elif not in_polymorphic_func: return _NO_MATCH return BoundCall(func, bound_param_args, null_args, return_type, has_empty_variadic)
def compile_result_clause( result: qlast.Base, *, view_scls: typing.Optional[s_types.Type]=None, view_rptr: typing.Optional[context.ViewRPtr]=None, view_name: typing.Optional[s_name.SchemaName]=None, result_alias: typing.Optional[str]=None, forward_rptr: bool=False, ctx: context.ContextLevel) -> irast.Set: with ctx.new() as sctx: sctx.clause = 'result' if sctx.stmt is ctx.toplevel_stmt: sctx.toplevel_clause = sctx.clause sctx.expr_exposed = True if forward_rptr: sctx.view_rptr = view_rptr # sctx.view_scls = view_scls if isinstance(result, qlast.Shape): result_expr = result.expr shape = result.elements else: result_expr = result shape = None if result_alias: # `SELECT foo := expr` is largely equivalent to # `WITH foo := expr SELECT foo` with one important exception: # the scope namespace does not get added to the current query # path scope. This is needed to handle FOR queries correctly. with sctx.newscope(temporary=True, fenced=True) as scopectx: stmtctx.declare_view( result_expr, alias=result_alias, temporary_scope=False, ctx=scopectx) result_expr = qlast.Path( steps=[qlast.ObjectRef(name=result_alias)] ) if (view_rptr is not None and (view_rptr.is_insert or view_rptr.is_update) and view_rptr.ptrcls is not None) and False: # If we have an empty set assigned to a pointer in an INSERT # or UPDATE, there's no need to explicitly specify the # empty set type and it can be assumed to match the pointer # target type. target_t = view_rptr.ptrcls.target if astutils.is_ql_empty_set(result_expr): expr = irutils.new_empty_set( sctx.schema, scls=target_t, alias=ctx.aliases.get('e')) else: with sctx.new() as exprctx: exprctx.empty_result_type_hint = target_t expr = setgen.ensure_set( dispatch.compile(result_expr, ctx=exprctx), ctx=exprctx) else: if astutils.is_ql_empty_set(result_expr): expr = irutils.new_empty_set( sctx.schema, scls=sctx.empty_result_type_hint, alias=ctx.aliases.get('e')) else: expr = setgen.ensure_set( dispatch.compile(result_expr, ctx=sctx), ctx=sctx) result = compile_query_subject( expr, shape=shape, view_rptr=view_rptr, view_name=view_name, result_alias=result_alias, view_scls=view_scls, compile_views=ctx.stmt is ctx.toplevel_stmt, ctx=sctx) ctx.partial_path_prefix = result return result
def compile_cast(ir_expr: irast.Base, new_stype: s_types.Type, *, srcctx: parsing.ParserContext, ctx: context.ContextLevel) -> irast.OperatorCall: if isinstance(ir_expr, irast.EmptySet): # For the common case of casting an empty set, we simply # generate a new EmptySet node of the requested type. return irutils.new_empty_set(ctx.env.schema, stype=new_stype, alias=ir_expr.path_id.target_name.name) elif irutils.is_untyped_empty_array_expr(ir_expr): # Ditto for empty arrays. return setgen.generated_set(irast.Array(elements=[], stype=new_stype), ctx=ctx) ir_set = setgen.ensure_set(ir_expr, ctx=ctx) orig_stype = ir_set.stype if orig_stype == new_stype: return ir_set elif orig_stype.is_object_type() and new_stype.is_object_type(): # Object types cannot be cast between themselves, # as cast is a _constructor_ operation, and the only # valid way to construct an object is to INSERT it. raise errors.QueryError( f'cannot cast object type ' f'{orig_stype.get_displayname(ctx.env.schema)!r} ' f'to {new_stype.get_displayname(ctx.env.schema)!r}, use ' f'`...[IS {new_stype.get_displayname(ctx.env.schema)}]` instead', context=srcctx) if isinstance(ir_set.expr, irast.Array): return _cast_array_literal(ir_set, orig_stype, new_stype, srcctx=srcctx, ctx=ctx) elif orig_stype.is_tuple(): return _cast_tuple(ir_set, orig_stype, new_stype, srcctx=srcctx, ctx=ctx) elif orig_stype.issubclass(ctx.env.schema, new_stype): # The new type is a supertype of the old type, # and is always a wider domain, so we simply reassign # the stype. return _inheritance_cast_to_ir(ir_set, orig_stype, new_stype, ctx=ctx) elif new_stype.issubclass(ctx.env.schema, orig_stype): # The new type is a subtype, so may potentially have # a more restrictive domain, generate a cast call. return _inheritance_cast_to_ir(ir_set, orig_stype, new_stype, ctx=ctx) elif orig_stype.is_array(): return _cast_array(ir_set, orig_stype, new_stype, srcctx=srcctx, ctx=ctx) else: json_t = ctx.env.schema.get('std::json') if (new_stype.issubclass(ctx.env.schema, json_t) and ir_set.path_id.is_objtype_path()): # JSON casts of objects are special: we want the full shape # and not just an identity. viewgen.compile_view_shapes(ir_set, ctx=ctx) return _compile_cast(ir_expr, orig_stype, new_stype, srcctx=srcctx, ctx=ctx)