def range_for_material_objtype(typeref: irast.TypeRef, path_id: irast.PathId, *, include_overlays: bool = True, env: context.Environment) -> pgast.BaseRangeVar: from . import pathctx # XXX: fix cycle if typeref.material_type is not None: typeref = typeref.material_type table_schema_name, table_name = common.get_objtype_backend_name( typeref.id, typeref.module_id, catenate=False) if typeref.name_hint.module in {'schema', 'cfg', 'sys'}: # Redirect all queries to schema tables to edgedbss table_schema_name = 'edgedbss' relation = pgast.Relation( schemaname=table_schema_name, name=table_name, path_id=path_id, ) rvar = pgast.RangeVar( relation=relation, alias=pgast.Alias(aliasname=env.aliases.get(typeref.name_hint.name))) overlays = env.rel_overlays.get(str(typeref.id)) if overlays and include_overlays: set_ops = [] qry = pgast.SelectStmt() qry.from_clause.append(rvar) pathctx.put_path_value_rvar(qry, path_id, rvar, env=env) pathctx.put_path_bond(qry, path_id) set_ops.append(('union', qry)) for op, cte in overlays: rvar = pgast.RangeVar( relation=cte, alias=pgast.Alias(aliasname=env.aliases.get(hint=cte.name))) qry = pgast.SelectStmt(from_clause=[rvar], ) pathctx.put_path_value_rvar(qry, path_id, rvar, env=env) pathctx.put_path_bond(qry, path_id) if op == 'replace': op = 'union' set_ops = [] set_ops.append((op, qry)) rvar = range_from_queryset(set_ops, typeref.name_hint, env=env) return rvar
def process_linkprop_update(ir_stmt: irast.MutatingStmt, ir_expr: irast.Base, wrapper: pgast.Query, dml_cte: pgast.CommonTableExpr, *, ctx: context.CompilerContextLevel) -> None: """Perform link property updates to a link relation. :param ir_stmt: IR of the statement. :param ir_expr: IR of the UPDATE body element. :param wrapper: Top-level SQL query. :param dml_cte: CTE representing the SQL UPDATE to the main relation of the Object. """ toplevel = ctx.toplevel_stmt rptr = ir_expr.rptr ptrref = rptr.ptrref if ptrref.material_ptr: ptrref = ptrref.material_ptr target_tab = dbobj.range_for_ptrref(ptrref, include_overlays=False, env=ctx.env) dml_cte_rvar = pgast.RangeVar( relation=dml_cte, alias=pgast.Alias(aliasname=ctx.env.aliases.get('m'))) cond = astutils.new_binop( pathctx.get_rvar_path_identity_var(dml_cte_rvar, ir_stmt.subject.path_id, env=ctx.env), dbobj.get_column(target_tab, 'source', nullable=False), op='=', ) targets = [] for prop_el in ir_expr.shape: ptrname = prop_el.rptr.ptrref.shortname with ctx.new() as input_rel_ctx: input_rel_ctx.expr_exposed = False input_rel = dispatch.compile(prop_el.expr, ctx=input_rel_ctx) targets.append(pgast.UpdateTarget(name=ptrname.name, val=input_rel)) updstmt = pgast.UpdateStmt(relation=target_tab, where_clause=cond, targets=targets, from_clause=[dml_cte_rvar]) updcte = pgast.CommonTableExpr(query=updstmt, name=ctx.env.aliases.get( ptrref.shortname.name)) toplevel.ctes.append(updcte)
def range_for_ptrref(ptrref: irast.BasePointerRef, *, include_overlays: bool = True, only_self: bool = False, env: context.Environment) -> pgast.BaseRangeVar: """"Return a Range subclass corresponding to a given ptr step. The return value may potentially be a UNION of all tables corresponding to a set of specialized links computed from the given `ptrref` taking source inheritance into account. """ tgt_col = pgtypes.get_ptrref_storage_info(ptrref, resolve_type=False, link_bias=True).column_name cols = ['source', tgt_col] set_ops = [] if only_self: ptrrefs = {ptrref} else: ptrrefs = {ptrref} | ptrref.descendants for src_ptrref in ptrrefs: table = table_from_ptrref(src_ptrref, env=env) qry = pgast.SelectStmt() qry.from_clause.append(table) qry.rptr_rvar = table # Make sure all property references are pulled up properly for colname in cols: selexpr = pgast.ColumnRef(name=[table.alias.aliasname, colname]) qry.target_list.append(pgast.ResTarget(val=selexpr, name=colname)) set_ops.append(('union', qry)) overlays = env.rel_overlays.get(src_ptrref.shortname) if overlays and include_overlays: for op, cte in overlays: rvar = pgast.RangeVar( relation=cte, alias=pgast.Alias(aliasname=env.aliases.get(cte.name))) qry = pgast.SelectStmt( target_list=[ pgast.ResTarget(val=pgast.ColumnRef(name=[col])) for col in cols ], from_clause=[rvar], ) set_ops.append((op, qry)) rvar = range_from_queryset(set_ops, ptrref.shortname, env=env) return rvar
def table_from_ptrref(ptrref: irast.PointerRef, *, env: context.Environment) -> pgast.RangeVar: """Return a Table corresponding to a given Link.""" table_schema_name, table_name = common.get_pointer_backend_name( ptrref.id, ptrref.module_id, catenate=False) if ptrref.shortname.module in {'schema', 'cfg', 'sys'}: # Redirect all queries to schema tables to edgedbss table_schema_name = 'edgedbss' relation = pgast.Relation(schemaname=table_schema_name, name=table_name) rvar = pgast.RangeVar( relation=relation, alias=pgast.Alias(aliasname=env.aliases.get(ptrref.shortname.name))) return rvar
def rvar_for_rel( rel: pgast.BaseRelation, *, lateral: bool=False, colnames: typing.List[str]=[], env: context.Environment) -> pgast.BaseRangeVar: if isinstance(rel, pgast.Query): alias = env.aliases.get(rel.name or 'q') rvar = pgast.RangeSubselect( subquery=rel, alias=pgast.Alias(aliasname=alias, colnames=colnames), lateral=lateral, ) else: alias = env.aliases.get(rel.name) rvar = pgast.RangeVar( relation=rel, alias=pgast.Alias(aliasname=alias, colnames=colnames) ) return rvar
def compile_ConfigSet( op: irast.ConfigSet, *, ctx: context.CompilerContextLevel) -> pgast.Query: with ctx.new() as subctx: val = dispatch.compile(op.expr, ctx=subctx) pathctx.get_path_serialized_output( val, op.expr.path_id, env=ctx.env) if op.cardinality is qltypes.Cardinality.MANY: val = output.aggregate_json_output(val, op.expr, env=ctx.env) result_row = pgast.RowExpr( args=[ pgast.StringConstant(val='SET'), pgast.StringConstant(val='SYSTEM' if op.system else 'SESSION'), pgast.StringConstant(val=op.name), val, ] ) result = pgast.FuncCall( name=('jsonb_build_array',), args=result_row.args, null_safe=True, ser_safe=True, ) if not op.system: stmt = pgast.InsertStmt( relation=pgast.RangeVar( relation=pgast.Relation( name='_edgecon_state', ), ), select_stmt=pgast.SelectStmt( values=[ pgast.ImplicitRowExpr( args=[ pgast.StringConstant( val=op.name, ), val, pgast.StringConstant( val='C', ), ] ) ] ), cols=[ pgast.ColumnRef(name=['name']), pgast.ColumnRef(name=['value']), pgast.ColumnRef(name=['type']), ], on_conflict=pgast.OnConflictClause( action='update', infer=pgast.InferClause( index_elems=[ pgast.ColumnRef(name=['name']), pgast.ColumnRef(name=['type']), ], ), target_list=[ pgast.MultiAssignRef( columns=[pgast.ColumnRef(name=['value'])], source=pgast.RowExpr( args=[ val, ], ), ), ], ), ) else: stmt = pgast.SelectStmt( target_list=[ pgast.ResTarget( val=result, ), ], ) return stmt
def compile_ConfigReset( op: irast.ConfigReset, *, ctx: context.CompilerContextLevel) -> pgast.Query: if op.selector is None: # Scalar reset result_row = pgast.RowExpr( args=[ pgast.StringConstant(val='RESET'), pgast.StringConstant(val='SYSTEM' if op.system else 'SESSION'), pgast.StringConstant(val=op.name), pgast.NullConstant(), ] ) rvar = None else: selector = dispatch.compile(op.selector, ctx=ctx) target = selector.target_list[0] if not target.name: target = selector.target_list[0] = pgast.ResTarget( name=ctx.env.aliases.get('res'), val=target.val, ) rvar = dbobj.rvar_for_rel(selector, env=ctx.env) result_row = pgast.RowExpr( args=[ pgast.StringConstant(val='REM'), pgast.StringConstant(val='SYSTEM' if op.system else 'SESSION'), pgast.StringConstant(val=op.name), dbobj.get_column(rvar, target.name), ] ) result = pgast.FuncCall( name=('jsonb_build_array',), args=result_row.args, null_safe=True, ser_safe=True, ) if not op.system: stmt = pgast.DeleteStmt( relation=pgast.RangeVar( relation=pgast.Relation( name='_edgecon_state', ), ), where_clause=astutils.new_binop( lexpr=astutils.new_binop( lexpr=pgast.ColumnRef(name=['name']), rexpr=pgast.StringConstant(val=op.name), op='=', ), rexpr=astutils.new_binop( lexpr=pgast.ColumnRef(name=['type']), rexpr=pgast.StringConstant(val='C'), op='=', ), op='AND', ) ) else: stmt = pgast.SelectStmt( target_list=[ pgast.ResTarget( val=result, ), ], ) if rvar is not None: stmt.from_clause = [rvar] return stmt
def process_link_update( *, ir_stmt: irast.MutatingStmt, ir_set: irast.Set, props_only: bool, is_insert: bool, wrapper: pgast.Query, dml_cte: pgast.CommonTableExpr, iterator_cte: pgast.CommonTableExpr, ctx: context.CompilerContextLevel) -> typing.Optional[pgast.Query]: """Perform updates to a link relation as part of a DML statement. :param ir_stmt: IR of the statement. :param ir_set: IR of the INSERT/UPDATE body element. :param props_only: Whether this link update only touches link properties. :param wrapper: Top-level SQL query. :param dml_cte: CTE representing the SQL INSERT or UPDATE to the main relation of the Object. :param iterator_cte: CTE representing the iterator range in the FOR clause of the EdgeQL DML statement. """ toplevel = ctx.toplevel_stmt rptr = ir_set.rptr ptrref = rptr.ptrref target_is_scalar = irtyputils.is_scalar(ptrref.dir_target) path_id = ir_set.path_id # The links in the dml class shape have been derived, # but we must use the correct specialized link class for the # base material type. if ptrref.material_ptr is not None: mptrref = ptrref.material_ptr else: mptrref = ptrref target_rvar = dbobj.range_for_ptrref(mptrref, include_overlays=False, only_self=True, env=ctx.env) target_alias = target_rvar.alias.aliasname target_tab_name = (target_rvar.relation.schemaname, target_rvar.relation.name) dml_cte_rvar = pgast.RangeVar( relation=dml_cte, alias=pgast.Alias(aliasname=ctx.env.aliases.get('m'))) col_data = { 'ptr_item_id': pgast.TypeCast(arg=pgast.StringConstant(val=str(mptrref.id)), type_name=pgast.TypeName(name=('uuid', ))), 'source': pathctx.get_rvar_path_identity_var(dml_cte_rvar, ir_stmt.subject.path_id, env=ctx.env) } if not is_insert: # Drop all previous link records for this source. delcte = pgast.CommonTableExpr(query=pgast.DeleteStmt( relation=target_rvar, where_clause=astutils.new_binop( lexpr=col_data['source'], op='=', rexpr=pgast.ColumnRef(name=[target_alias, 'source'])), using_clause=[dml_cte_rvar], returning_list=[ pgast.ResTarget(val=pgast.ColumnRef( name=[target_alias, pgast.Star()])) ]), name=ctx.env.aliases.get(hint='d')) pathctx.put_path_value_rvar(delcte.query, path_id.ptr_path(), target_rvar, env=ctx.env) # Record the effect of this removal in the relation overlay # context to ensure that the RETURNING clause potentially # referencing this link yields the expected results. overlays = ctx.env.rel_overlays[ptrref.shortname] overlays.append(('except', delcte)) toplevel.ctes.append(delcte) # Turn the IR of the expression on the right side of := # into a subquery returning records for the link table. data_cte, specified_cols = process_link_values(ir_stmt, ir_set, target_tab_name, col_data, dml_cte_rvar, [], props_only, target_is_scalar, iterator_cte, ctx=ctx) toplevel.ctes.append(data_cte) data_select = pgast.SelectStmt( target_list=[ pgast.ResTarget(val=pgast.ColumnRef( name=[data_cte.name, pgast.Star()])) ], from_clause=[pgast.RangeVar(relation=data_cte)]) cols = [pgast.ColumnRef(name=[col]) for col in specified_cols] if is_insert: conflict_clause = None else: # Inserting rows into the link table may produce cardinality # constraint violations, since the INSERT into the link table # is executed in the snapshot where the above DELETE from # the link table is not visible. Hence, we need to use # the ON CONFLICT clause to resolve this. conflict_cols = ['source', 'target', 'ptr_item_id'] conflict_inference = [] conflict_exc_row = [] for col in conflict_cols: conflict_inference.append(pgast.ColumnRef(name=[col])) conflict_exc_row.append(pgast.ColumnRef(name=['excluded', col])) conflict_data = pgast.SelectStmt( target_list=[ pgast.ResTarget(val=pgast.ColumnRef( name=[data_cte.name, pgast.Star()])) ], from_clause=[pgast.RangeVar(relation=data_cte)], where_clause=astutils.new_binop( lexpr=pgast.ImplicitRowExpr(args=conflict_inference), rexpr=pgast.ImplicitRowExpr(args=conflict_exc_row), op='=')) conflict_clause = pgast.OnConflictClause( action='update', infer=pgast.InferClause(index_elems=conflict_inference), target_list=[ pgast.MultiAssignRef(columns=cols, source=conflict_data) ]) updcte = pgast.CommonTableExpr( name=ctx.env.aliases.get(hint='i'), query=pgast.InsertStmt( relation=target_rvar, select_stmt=data_select, cols=cols, on_conflict=conflict_clause, returning_list=[ pgast.ResTarget(val=pgast.ColumnRef(name=[pgast.Star()])) ])) pathctx.put_path_value_rvar(updcte.query, path_id.ptr_path(), target_rvar, env=ctx.env) # Record the effect of this insertion in the relation overlay # context to ensure that the RETURNING clause potentially # referencing this link yields the expected results. overlays = ctx.env.rel_overlays[ptrref.shortname] overlays.append(('union', updcte)) toplevel.ctes.append(updcte) return data_cte
def init_dml_stmt( ir_stmt: irast.MutatingStmt, dml_stmt: pgast.DML, *, ctx: context.CompilerContextLevel, parent_ctx: context.CompilerContextLevel) \ -> typing.Tuple[pgast.Query, pgast.CommonTableExpr, pgast.CommonTableExpr]: """Prepare the common structure of the query representing a DML stmt. :param ir_stmt: IR of the statement. :param dml_stmt: SQL DML node instance. :return: A (*wrapper*, *dml_cte*, *range_cte*) tuple, where *wrapper* the the wrapping SQL statement, *dml_cte* is the CTE representing the SQL DML operation in the main relation of the Object, and *range_cte* is the CTE for the subset affected by the statement. *range_cte* is None for INSERT statmenets. """ wrapper = ctx.rel clauses.init_stmt(ir_stmt, ctx, parent_ctx) target_ir_set = ir_stmt.subject dml_stmt.relation = dbobj.range_for_set(ir_stmt.subject, include_overlays=False, common_parent=True, env=ctx.env) pathctx.put_path_value_rvar(dml_stmt, target_ir_set.path_id, dml_stmt.relation, env=ctx.env) pathctx.put_path_source_rvar(dml_stmt, target_ir_set.path_id, dml_stmt.relation, env=ctx.env) pathctx.put_path_bond(dml_stmt, target_ir_set.path_id) dml_cte = pgast.CommonTableExpr(query=dml_stmt, name=ctx.env.aliases.get(hint='m')) if isinstance(ir_stmt, (irast.UpdateStmt, irast.DeleteStmt)): # UPDATE and DELETE operate over a range, so generate # the corresponding CTE and connect it to the DML query. range_cte = get_dml_range(ir_stmt, dml_stmt, ctx=ctx) range_rvar = pgast.RangeVar( relation=range_cte, alias=pgast.Alias(aliasname=ctx.env.aliases.get(hint='range'))) relctx.pull_path_namespace(target=dml_stmt, source=range_rvar, ctx=ctx) # Auxiliary relations are always joined via the WHERE # clause due to the structure of the UPDATE/DELETE SQL statements. dml_stmt.where_clause = astutils.new_binop( lexpr=pgast.ColumnRef( name=[dml_stmt.relation.alias.aliasname, 'id']), op='=', rexpr=pathctx.get_rvar_path_identity_var(range_rvar, target_ir_set.path_id, env=ctx.env)) # UPDATE has "FROM", while DELETE has "USING". if hasattr(dml_stmt, 'from_clause'): dml_stmt.from_clause.append(range_rvar) else: dml_stmt.using_clause.append(range_rvar) else: range_cte = None # Due to the fact that DML statements are structured # as a flat list of CTEs instead of nested range vars, # the top level path scope must be empty. The necessary # range vars will be injected explicitly in all rels that # need them. ctx.path_scope.clear() pathctx.put_path_value_rvar(dml_stmt, ir_stmt.subject.path_id, dml_stmt.relation, env=ctx.env) pathctx.put_path_source_rvar(dml_stmt, ir_stmt.subject.path_id, dml_stmt.relation, env=ctx.env) dml_rvar = pgast.RangeVar( relation=dml_cte, alias=pgast.Alias(aliasname=parent_ctx.env.aliases.get('d'))) relctx.include_rvar(wrapper, dml_rvar, ir_stmt.subject.path_id, ctx=ctx) pathctx.put_path_bond(wrapper, ir_stmt.subject.path_id) return wrapper, dml_cte, dml_rvar, range_cte