def join_condition(lref: pgast.ColumnRef, rref: pgast.ColumnRef) -> pgast.Base: path_cond = new_binop(lref, rref, op='=') if lref.optional: opt_cond = pgast.NullTest(arg=lref) path_cond = extend_binop(path_cond, opt_cond, op='OR') if rref.optional: opt_cond = pgast.NullTest(arg=rref) path_cond = extend_binop(path_cond, opt_cond, op='OR') return path_cond
def compile_SelectStmt( stmt: irast.SelectStmt, *, ctx: context.CompilerContextLevel) -> pgast.BaseExpr: if ctx.singleton_mode: return dispatch.compile(stmt.result, ctx=ctx) parent_ctx = ctx with parent_ctx.substmt() as ctx: # Common setup. clauses.init_stmt(stmt, ctx=ctx, parent_ctx=parent_ctx) query = ctx.stmt if not isinstance(stmt.result.expr, irast.MutatingStmt): iterators = irutils.get_iterator_sets(stmt) for iterator_set in iterators: # Process FOR clause. clauses.compile_iterator_expr(query, iterator_set, ctx=ctx) # Process the result expression; outvar = clauses.compile_output(stmt.result, ctx=ctx) # The FILTER clause. if stmt.where is not None: query.where_clause = astutils.extend_binop( query.where_clause, clauses.compile_filter_clause( stmt.where, stmt.where_card, ctx=ctx)) if outvar.nullable and query is ctx.toplevel_stmt: # A nullable var has bubbled up to the top, # filter out NULLs. valvar: pgast.BaseExpr = pathctx.get_path_value_var( query, stmt.result.path_id, env=ctx.env) if isinstance(valvar, pgast.TupleVar): valvar = pgast.ImplicitRowExpr( args=[e.val for e in valvar.elements]) query.where_clause = astutils.extend_binop( query.where_clause, pgast.NullTest(arg=valvar, negated=True) ) # The ORDER BY clause query.sort_clause = clauses.compile_orderby_clause( stmt.orderby, ctx=ctx) # The OFFSET clause query.limit_offset = clauses.compile_limit_offset_clause( stmt.offset, ctx=ctx) # The LIMIT clause query.limit_count = clauses.compile_limit_offset_clause( stmt.limit, ctx=ctx) clauses.fini_stmt(query, ctx, parent_ctx) return query
def apply_volatility_ref(stmt: pgast.SelectStmt, *, ctx: context.CompilerContextLevel) -> None: for ref in ctx.volatility_ref: # Apply the volatility reference. # See the comment in process_set_as_subquery(). stmt.where_clause = astutils.extend_binop( stmt.where_clause, pgast.NullTest( arg=ref(), negated=True, ))
def wrap_script_stmt( stmt: pgast.SelectStmt, *, suppress_all_output: bool = False, env: context.Environment, ) -> pgast.SelectStmt: subrvar = pgast.RangeSubselect( subquery=stmt, alias=pgast.Alias(aliasname=env.aliases.get('aggw'))) stmt_res = stmt.target_list[0] if stmt_res.name is None: stmt_res = stmt.target_list[0] = pgast.ResTarget( name=env.aliases.get('v'), val=stmt_res.val, ) assert stmt_res.name is not None count_val = pgast.FuncCall(name=('count', ), args=[pgast.ColumnRef(name=[stmt_res.name])]) result = pgast.SelectStmt(target_list=[ pgast.ResTarget( val=count_val, name=stmt_res.name, ), ], from_clause=[ subrvar, ]) if suppress_all_output: subrvar = pgast.RangeSubselect( subquery=result, alias=pgast.Alias(aliasname=env.aliases.get('q'))) result = pgast.SelectStmt( target_list=[], from_clause=[ subrvar, ], where_clause=pgast.NullTest(arg=pgast.ColumnRef( name=[subrvar.alias.aliasname, stmt_res.name], ), ), ) result.ctes = stmt.ctes result.argnames = stmt.argnames stmt.ctes = [] return result
def compile_iterator_expr( query: pgast.SelectStmt, iterator_expr: irast.Set, *, ctx: context.CompilerContextLevel) \ -> pgast.PathRangeVar: assert isinstance(iterator_expr.expr, (irast.GroupStmt, irast.SelectStmt)) with ctx.new() as subctx: subctx.expr_exposed = False subctx.rel = query already_existed = bool( relctx.maybe_get_path_rvar(query, iterator_expr.path_id, aspect='value', ctx=ctx)) dispatch.visit(iterator_expr, ctx=subctx) iterator_rvar = relctx.get_path_rvar(query, iterator_expr.path_id, aspect='value', ctx=ctx) iterator_query = iterator_rvar.query # If the iterator value is nullable, add a null test. This # makes sure that we don't spuriously produce output when # iterating over options pointers. assert isinstance(iterator_query, pgast.SelectStmt) iterator_var = pathctx.get_path_value_var( iterator_query, path_id=iterator_expr.path_id, env=ctx.env) if iterator_var.nullable: iterator_query.where_clause = astutils.extend_binop( iterator_query.where_clause, pgast.NullTest(arg=iterator_var, negated=True)) # Regardless of result type, we use transient identity, # for path identity of the iterator expression. This is # necessary to maintain correct correlation for the state # of iteration in DML statements. # The already_existed check is to avoid adding in bogus volatility refs # when we reprocess an iterator that was hoisted. if not already_existed: relctx.ensure_bond_for_expr(iterator_expr.expr.result, iterator_query, ctx=subctx) return iterator_rvar
def ensure_transient_identity_for_set(ir_set: irast.Set, stmt: pgast.BaseRelation, *, ctx: context.CompilerContextLevel, type: str = 'int') -> None: if type == 'uuid': id_expr = pgast.FuncCall( name=( 'edgedbext', 'uuid_generate_v1mc', ), args=[], ) else: id_expr = pgast.FuncCall(name=('row_number', ), args=[], over=pgast.WindowDef()) pathctx.put_path_identity_var(stmt, ir_set.path_id, id_expr, force=True, env=ctx.env) pathctx.put_path_bond(stmt, ir_set.path_id) if (ctx.volatility_ref is not None and ctx.volatility_ref is not context.NO_VOLATILITY and isinstance(stmt, pgast.SelectStmt)): # Apply the volatility reference. # See the comment in process_set_as_subquery(). stmt.where_clause = astutils.extend_binop( stmt.where_clause, pgast.NullTest( arg=ctx.volatility_ref, negated=True, ))
def compile_operator(expr: irast.OperatorCall, args: Sequence[pgast.BaseExpr], *, ctx: context.CompilerContextLevel) -> pgast.BaseExpr: lexpr = rexpr = None result: Optional[pgast.BaseExpr] = None if expr.operator_kind is ql_ft.OperatorKind.Infix: lexpr, rexpr = args elif expr.operator_kind is ql_ft.OperatorKind.Prefix: rexpr = args[0] elif expr.operator_kind is ql_ft.OperatorKind.Postfix: lexpr = args[0] else: raise RuntimeError(f'unexpected operator kind: {expr.operator_kind!r}') str_func_name = str(expr.func_shortname) if ((str_func_name in {'std::=', 'std::!='} or str(expr.origin_name) in {'std::=', 'std::!='}) and expr.args[0].expr.typeref is not None and irtyputils.is_object(expr.args[0].expr.typeref) and expr.args[1].expr.typeref is not None and irtyputils.is_object(expr.args[1].expr.typeref)): if str_func_name == 'std::=' or str(expr.origin_name) == 'std::=': sql_oper = '=' else: sql_oper = '!=' elif str_func_name == 'std::EXISTS': assert rexpr result = pgast.NullTest(arg=rexpr, negated=True) elif expr.sql_operator: sql_oper = expr.sql_operator[0] if len(expr.sql_operator) > 1: # Explicit operand types given in FROM SQL OPERATOR lexpr, rexpr = _cast_operands(lexpr, rexpr, expr.sql_operator[1:]) elif expr.sql_function: sql_func = expr.sql_function[0] func_name = tuple(sql_func.split('.', 1)) if len(expr.sql_function) > 1: # Explicit operand types given in FROM SQL FUNCTION lexpr, rexpr = _cast_operands(lexpr, rexpr, expr.sql_function[1:]) args = [] if lexpr is not None: args.append(lexpr) if rexpr is not None: args.append(rexpr) result = pgast.FuncCall(name=func_name, args=args) elif expr.origin_name is not None: sql_oper = common.get_operator_backend_name(expr.origin_name)[1] else: sql_oper = common.get_operator_backend_name(expr.func_shortname)[1] # If result was not already computed, it's going to be a generic Expr. if result is None: result = pgast.Expr( kind=pgast.ExprKind.OP, name=sql_oper, lexpr=lexpr, rexpr=rexpr, ) if expr.force_return_cast: # The underlying operator has a return value type # different from that of the EdgeQL operator declaration, # so we need to make an explicit cast here. result = pgast.TypeCast( arg=result, type_name=pgast.TypeName( name=pg_types.pg_type_from_ir_typeref(expr.typeref))) return result
def compile_SelectStmt(stmt: irast.SelectStmt, *, ctx: context.CompilerContextLevel) -> pgast.BaseExpr: if ctx.singleton_mode: return dispatch.compile(stmt.result, ctx=ctx) parent_ctx = ctx with parent_ctx.substmt() as ctx: # Common setup. clauses.init_stmt(stmt, ctx=ctx, parent_ctx=parent_ctx) for binding in (stmt.bindings or ()): # If something we are WITH binding contains DML, we want to # compile it *now*, in the context of its initial appearance # and not where the variable is used. This will populate # dml_stmts with the CTEs, which will be picked up when the # variable is referenced. if irutils.contains_dml(binding): with ctx.substmt() as bctx: dispatch.compile(binding, ctx=bctx) query = ctx.stmt iterator_set = stmt.iterator_stmt last_iterator: Optional[irast.Set] = None if iterator_set and irutils.contains_dml(stmt): # If we have iterators and we contain nested DML # statements, we need to hoist the iterators into CTEs and # then explicitly join them back into the query. iterator = dml.compile_iterator_cte(iterator_set, ctx=ctx) ctx.path_scope = ctx.path_scope.new_child() dml.merge_iterator(iterator, ctx.rel, ctx=ctx) ctx.enclosing_cte_iterator = iterator last_iterator = stmt.iterator_stmt else: if iterator_set: # Process FOR clause. with ctx.new() as ictx: clauses.setup_iterator_volatility(last_iterator, ctx=ictx) iterator_rvar = clauses.compile_iterator_expr(query, iterator_set, ctx=ictx) for aspect in {'identity', 'value'}: pathctx.put_path_rvar( query, path_id=iterator_set.path_id, rvar=iterator_rvar, aspect=aspect, env=ctx.env, ) last_iterator = iterator_set # Process materialized sets clauses.compile_materialized_exprs(query, stmt, ctx=ctx) # Process the result expression. with ctx.new() as ictx: clauses.setup_iterator_volatility(last_iterator, ctx=ictx) outvar = clauses.compile_output(stmt.result, ctx=ictx) with ctx.new() as ictx: # FILTER and ORDER BY need to have the base result as a # volatility ref. clauses.setup_iterator_volatility(stmt.result, ctx=ictx) # The FILTER clause. if stmt.where is not None: query.where_clause = astutils.extend_binop( query.where_clause, clauses.compile_filter_clause(stmt.where, stmt.where_card, ctx=ctx)) # The ORDER BY clause if stmt.orderby is not None: with ctx.new() as ictx: query.sort_clause = clauses.compile_orderby_clause( stmt.orderby, ctx=ictx) if outvar.nullable and query is ctx.toplevel_stmt: # A nullable var has bubbled up to the top, # filter out NULLs. valvar: pgast.BaseExpr = pathctx.get_path_value_var( query, stmt.result.path_id, env=ctx.env) if isinstance(valvar, pgast.TupleVar): valvar = pgast.ImplicitRowExpr( args=[e.val for e in valvar.elements]) query.where_clause = astutils.extend_binop( query.where_clause, pgast.NullTest(arg=valvar, negated=True)) # The OFFSET clause query.limit_offset = clauses.compile_limit_offset_clause(stmt.offset, ctx=ctx) # The LIMIT clause query.limit_count = clauses.compile_limit_offset_clause(stmt.limit, ctx=ctx) clauses.fini_stmt(query, ctx, parent_ctx) return query
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 compile_SelectStmt(stmt: irast.SelectStmt, *, ctx: context.CompilerContextLevel) -> pgast.BaseExpr: if ctx.singleton_mode: return dispatch.compile(stmt.result, ctx=ctx) parent_ctx = ctx with parent_ctx.substmt() as ctx: # Common setup. clauses.init_stmt(stmt, ctx=ctx, parent_ctx=parent_ctx) query = ctx.stmt iterators = irutils.get_iterator_sets(stmt) if iterators and irutils.contains_dml(stmt): # If we have iterators and we contain nested DML # statements, we need to hoist the iterators into CTEs and # then explicitly join them back into the query. iterator = dml.compile_iterator_ctes(iterators, ctx=ctx) ctx.path_scope = ctx.path_scope.new_child() dml.merge_iterator(iterator, ctx.rel, ctx=ctx) ctx.enclosing_cte_iterator = iterator else: iterator = None for iterator_set in iterators: # Process FOR clause. iterator_rvar = clauses.compile_iterator_expr(query, iterator_set, ctx=ctx) for aspect in {'identity', 'value'}: pathctx.put_path_rvar( query, path_id=iterator_set.path_id, rvar=iterator_rvar, aspect=aspect, env=ctx.env, ) # Process the result expression; outvar = clauses.compile_output(stmt.result, ctx=ctx) # The FILTER clause. if stmt.where is not None: query.where_clause = astutils.extend_binop( query.where_clause, clauses.compile_filter_clause(stmt.where, stmt.where_card, ctx=ctx)) if outvar.nullable and query is ctx.toplevel_stmt: # A nullable var has bubbled up to the top, # filter out NULLs. valvar: pgast.BaseExpr = pathctx.get_path_value_var( query, stmt.result.path_id, env=ctx.env) if isinstance(valvar, pgast.TupleVar): valvar = pgast.ImplicitRowExpr( args=[e.val for e in valvar.elements]) query.where_clause = astutils.extend_binop( query.where_clause, pgast.NullTest(arg=valvar, negated=True)) # The ORDER BY clause query.sort_clause = clauses.compile_orderby_clause(stmt.orderby, ctx=ctx) # The OFFSET clause query.limit_offset = clauses.compile_limit_offset_clause(stmt.offset, ctx=ctx) # The LIMIT clause query.limit_count = clauses.compile_limit_offset_clause(stmt.limit, ctx=ctx) clauses.fini_stmt(query, ctx, parent_ctx) return query
def compile_operator(expr: irast.OperatorCall, args: Sequence[pgast.BaseExpr], *, ctx: context.CompilerContextLevel) -> pgast.BaseExpr: lexpr = rexpr = None result: Optional[pgast.BaseExpr] = None if expr.operator_kind is ql_ft.OperatorKind.Infix: lexpr, rexpr = args elif expr.operator_kind is ql_ft.OperatorKind.Prefix: rexpr = args[0] elif expr.operator_kind is ql_ft.OperatorKind.Postfix: lexpr = args[0] else: raise RuntimeError(f'unexpected operator kind: {expr.operator_kind!r}') str_func_name = str(expr.func_shortname) if ((str_func_name in {'std::=', 'std::!='} or str(expr.origin_name) in {'std::=', 'std::!='}) and expr.args[0].expr.typeref is not None and irtyputils.is_object(expr.args[0].expr.typeref) and expr.args[1].expr.typeref is not None and irtyputils.is_object(expr.args[1].expr.typeref)): if str_func_name == 'std::=' or str(expr.origin_name) == 'std::=': sql_oper = '=' else: sql_oper = '!=' elif str_func_name == 'std::EXISTS': result = pgast.NullTest(arg=rexpr, negated=True) elif expr.sql_operator: sql_oper = expr.sql_operator[0] if len(expr.sql_operator) > 1: # Explicit operand types given in FROM SQL OPERATOR if lexpr is not None: lexpr = pgast.TypeCast( arg=lexpr, type_name=pgast.TypeName(name=(expr.sql_operator[1], ))) if rexpr is not None: rexpr_qry = None if (isinstance(rexpr, pgast.SubLink) and isinstance(rexpr.expr, pgast.SelectStmt)): rexpr_qry = rexpr.expr elif isinstance(rexpr, pgast.SelectStmt): rexpr_qry = rexpr if rexpr_qry is not None: # Handle cases like foo <op> ANY (SELECT) and # foo <OP> (SELECT). rexpr_qry.target_list[0] = pgast.ResTarget( name=rexpr_qry.target_list[0].name, val=pgast.TypeCast(arg=rexpr_qry.target_list[0].val, type_name=pgast.TypeName( name=(expr.sql_operator[2], )))) else: rexpr = pgast.TypeCast(arg=rexpr, type_name=pgast.TypeName( name=(expr.sql_operator[2], ))) elif expr.origin_name is not None: sql_oper = common.get_operator_backend_name(expr.origin_name)[1] else: sql_oper = common.get_operator_backend_name(expr.func_shortname)[1] # If result was not already computed, it's going to be a generic Expr. if result is None: result = pgast.Expr( kind=pgast.ExprKind.OP, name=sql_oper, lexpr=lexpr, rexpr=rexpr, ) if expr.force_return_cast: # The underlying operator has a return value type # different from that of the EdgeQL operator declaration, # so we need to make an explicit cast here. result = pgast.TypeCast( arg=result, type_name=pgast.TypeName( name=pg_types.pg_type_from_ir_typeref(expr.typeref))) return result
def compile_shape(ir_set: irast.Set, shape: Sequence[Tuple[irast.Set, qlast.ShapeOp]], *, ctx: context.CompilerContextLevel) -> pgast.TupleVar: elements = [] # If the object identity is potentially nullable, filter it out # to prevent shapes with bogusly null insides. var = pathctx.get_path_value_var(ctx.rel, path_id=ir_set.path_id, env=ctx.env) if var.nullable: ctx.rel.where_clause = astutils.extend_binop( ctx.rel.where_clause, pgast.NullTest(arg=var, negated=True)) with ctx.newscope() as shapectx: shapectx.disable_semi_join |= {ir_set.path_id} if isinstance(ir_set.expr, irast.Stmt): # The source set for this shape is a FOR statement, # which is special in that besides set path_id it # should also expose the path_id of the FOR iterator # so that shape element expressions that might contain # an iterator reference find it properly. # # So, for: # SELECT Bar { # foo := (FOR x := ... UNION Foo { spam := x }) # } # # the path scope when processing the shape of Bar.foo # should be {'Bar.foo', 'x'}. iterator = ir_set.expr.iterator_stmt if iterator: shapectx.path_scope[iterator.path_id] = ctx.rel for el, op in shape: if op == qlast.ShapeOp.MATERIALIZE and not ctx.materializing: continue rptr = el.rptr assert rptr is not None ptrref = rptr.ptrref # As an implementation expedient, we currently represent # AT_MOST_ONE materialized values with arrays card = rptr.dir_cardinality is_singleton = (card.is_single() and (not ctx.materializing or not card.can_be_zero())) value: pgast.BaseExpr if (irutils.is_subquery_set(el) or el.path_id.is_objtype_path() or not is_singleton or not ptrref.required): wrapper = relgen.set_as_subquery(el, as_value=True, ctx=shapectx) if not is_singleton: value = relctx.set_to_array(path_id=el.path_id, query=wrapper, ctx=shapectx) else: value = wrapper else: value = dispatch.compile(el, ctx=shapectx) tuple_el = astutils.tuple_element_for_shape_el(el, value, ctx=shapectx) assert isinstance(tuple_el, pgast.TupleElement) elements.append(tuple_el) return pgast.TupleVar(elements=elements, named=True)
def named_tuple_as_json_object( expr: pgast.BaseExpr, *, styperef: irast.TypeRef, env: context.Environment, ) -> pgast.BaseExpr: keyvals: List[pgast.BaseExpr] = [] if irtyputils.is_persistent_tuple(styperef): for el_type in styperef.subtypes: assert el_type.element_name keyvals.append(pgast.StringConstant(val=el_type.element_name)) val: pgast.BaseExpr = pgast.Indirection( arg=expr, indirection=[pgast.ColumnRef(name=[el_type.element_name])]) val = serialize_expr_to_json(val, styperef=el_type, nested=True, env=env) keyvals.append(val) obj = _build_json( 'build_object', args=keyvals, null_safe=True, ser_safe=True, nullable=expr.nullable, env=env, ) else: coldeflist = [] for el_type in styperef.subtypes: assert el_type.element_name keyvals.append(pgast.StringConstant(val=el_type.element_name)) coldeflist.append( pgast.ColumnDef( name=el_type.element_name, typename=pgast.TypeName( name=pgtypes.pg_type_from_ir_typeref(el_type), ), )) val = pgast.ColumnRef(name=[el_type.element_name]) val = serialize_expr_to_json(val, styperef=el_type, nested=True, env=env) keyvals.append(val) obj = _build_json( 'build_object', args=keyvals, null_safe=True, ser_safe=True, nullable=expr.nullable, env=env, ) obj = pgast.SelectStmt( target_list=[ pgast.ResTarget(val=obj, ), ], from_clause=[ pgast.RangeFunction(functions=[ pgast.FuncCall( name=('unnest', ), args=[pgast.ArrayExpr(elements=[expr], )], coldeflist=coldeflist, ) ]) ] if styperef.subtypes else []) if expr.nullable: obj = pgast.SelectStmt(target_list=[pgast.ResTarget(val=obj)], where_clause=pgast.NullTest(arg=expr, negated=True)) return obj