class DatabaseCommand(sd.ObjectCommand, schema_metaclass=Database, context_class=DatabaseCommandContext): classname = struct.Field(str) @classmethod def _classname_from_ast(cls, schema, astnode, context): return astnode.name.name
class ModuleCommand(sd.ObjectCommand, schema_metaclass=Module, context_class=ModuleCommandContext): classname = struct.Field(str) @classmethod def _classname_from_ast(cls, schema, astnode, context): if astnode.name.module: classname = sn.Name(module=astnode.name.module, name=astnode.name.name) else: classname = astnode.name.name return classname
class AlterObjectProperty(Command): property = struct.Field(str) old_value = struct.Field(object, None) new_value = struct.Field(object, None) source = struct.Field(str, None) @classmethod def _cmd_tree_from_ast(cls, astnode, context, schema): propname = astnode.name.name if astnode.name.module: propname = astnode.name.module + '::' + propname assert '::' not in propname if isinstance(astnode, qlast.DropAttributeValue): parent_ctx = context.get(CommandContextToken) parent_cls = parent_ctx.op.get_schema_metaclass() field = parent_cls._fields.get(propname) if (field is not None and issubclass(field.type[0], typed.AbstractTypedCollection)): value = field.type[0]() else: value = None return cls(property=propname, new_value=value) if astnode.as_expr: new_value = s_expr.ExpressionText( edgeql.generate_source(astnode.value, pretty=False)) else: if isinstance(astnode.value, qlast.Constant): new_value = astnode.value.value elif isinstance(astnode.value, qlast.Tuple): new_value = tuple(el.value for el in astnode.value.elements) else: raise ValueError( f'unexpected value in attribute: {astnode.value!r}') return cls(property=propname, new_value=new_value) def _get_ast(self, context): value = self.new_value new_value_empty = \ (value is None or (isinstance(value, collections.Container) and not value)) old_value_empty = \ (self.old_value is None or (isinstance(self.old_value, collections.Container) and not self.old_value)) if new_value_empty and not old_value_empty: op = qlast.DropAttributeValue( name=qlast.ObjectRef(module='', name=self.property)) return op if new_value_empty and old_value_empty: return if isinstance(value, s_expr.ExpressionText): value = edgeql.parse(str(value)) elif utils.is_nontrivial_container(value): value = qlast.Tuple(elements=[ qlast.Constant(value=el) for el in value ]) elif isinstance(value, nlang.WordCombination): forms = value.as_dict() if len(forms) > 1: items = [] for k, v in forms.items(): items.append(( qlast.Constant(value=k), qlast.Constant(value=v) )) value = qlast.Array(elements=[ qlast.Tuple(elements=[k, v]) for k, v in items ]) else: value = qlast.Constant(value=str(value)) else: value = qlast.Constant(value=value) as_expr = isinstance(value, qlast.ExpressionText) op = qlast.CreateAttributeValue( name=qlast.ObjectRef(module='', name=self.property), value=value, as_expr=as_expr) return op def __repr__(self): return '<%s.%s "%s":"%s"->"%s">' % ( self.__class__.__module__, self.__class__.__name__, self.property, self.old_value, self.new_value)
class Command(struct.MixedStruct, metaclass=CommandMeta): """Abstract base class for all delta commands.""" source_context = struct.Field(object, default=None) _context_class = None def __init__(self, **kwargs): super().__init__(**kwargs) self.ops = ordered.OrderedSet() self.before_ops = ordered.OrderedSet() def copy(self): result = super().copy() result.ops = ordered.OrderedSet( op.copy() for op in self.ops) result.before_ops = ordered.OrderedSet( op.copy() for op in self.before_ops) return result @classmethod def adapt(cls, obj): result = obj.copy_with_class(cls) for op in obj: result.ops.add(type(cls).adapt(op)) return result def _resolve_type_ref(self, ref, schema): return utils.resolve_typeref(ref, schema) def _resolve_attr_value(self, value, fname, field, schema): ftype = field.type[0] if isinstance(ftype, so.ObjectMeta): value = self._resolve_type_ref(value, schema) elif issubclass(ftype, typed.AbstractTypedMapping): if issubclass(ftype.valuetype, so.Object): vals = {} for k, val in value.items(): vals[k] = self._resolve_type_ref(val, schema) value = ftype(vals) elif issubclass(ftype, (typed.AbstractTypedSequence, typed.AbstractTypedSet)): if issubclass(ftype.type, so.Object): vals = [] for val in value: vals.append(self._resolve_type_ref(val, schema)) value = ftype(vals) else: value = self.adapt_value(field, value) return value def get_struct_properties(self, schema): result = {} metaclass = self.get_schema_metaclass() for op in self.get_subcommands(type=AlterObjectProperty): try: field = metaclass.get_field(op.property) except KeyError: continue result[op.property] = self._resolve_attr_value( op.new_value, op.property, field, schema) return result def get_attribute_value(self, attr_name): for op in self.get_subcommands(type=AlterObjectProperty): if op.property == attr_name: return op.new_value else: return None def discard_attribute(self, attr_name): for op in self.get_subcommands(type=AlterObjectProperty): if op.property == attr_name: self.discard(op) return def adapt_value(self, field, value): if value is not None and not isinstance(value, field.type): value = field.adapt(value) return value def __iter__(self): for op in self.ops: yield from op.before_ops yield op def get_subcommands(self, *, type=None): if type is not None: return filter(lambda i: isinstance(i, type), self) else: return list(self) def has_subcommands(self): return bool(self.ops) def after(self, command): if isinstance(command, CommandGroup): for op in command: self.before_ops.add(command) else: self.before_ops.add(command) def prepend(self, command): if isinstance(command, CommandGroup): for op in reversed(command): self.ops.add(op, last=False) else: self.ops.add(command, last=False) def add(self, command): if isinstance(command, CommandGroup): self.update(command) else: self.ops.add(command) def update(self, commands): for command in commands: self.add(command) def discard(self, command): self.ops.discard(command) def sort_subcommands_by_type(self): def _key(c): if isinstance(c, CreateObject): return 0 else: return 2 self.ops = ordered.OrderedSet(sorted(self.ops, key=_key)) def apply(self, schema, context): pass def upgrade(self, context, format_ver, schema): if context is None: context = CommandContext() with self.new_context(context): for op in self.ops: op.upgrade(context, format_ver, schema) def get_ast(self, context=None): if context is None: context = CommandContext() with self.new_context(context): return self._get_ast(context) @classmethod def command_for_ast_node(cls, astnode, schema, context): cmdcls = type(cls)._astnode_map.get(type(astnode)) if hasattr(cmdcls, '_command_for_ast_node'): # Delegate the choice of command class to the specific command. cmdcls = cmdcls._command_for_ast_node(astnode, schema, context) return cmdcls @classmethod def from_ast(cls, astnode, *, context=None, schema): if context is None: context = CommandContext() cmdcls = cls.command_for_ast_node( astnode, schema=schema, context=context) if cmdcls is None: msg = 'cannot find command for ast node {!r}'.format(astnode) raise TypeError(msg) return cmdcls._cmd_tree_from_ast( astnode, context=context, schema=schema) @classmethod def _cmd_tree_from_ast(cls, astnode, context, schema): cmd = cls._cmd_from_ast(astnode, context, schema) cmd.source_context = astnode.context if getattr(astnode, 'commands', None): context_class = cls.get_context_class() if context_class is not None: with context(context_class(cmd)): for subastnode in astnode.commands: subcmd = Command.from_ast( subastnode, context=context, schema=schema) if subcmd is not None: cmd.add(subcmd) else: for subastnode in astnode.commands: subcmd = Command.from_ast( subastnode, context=context, schema=schema) if subcmd is not None: cmd.add(subcmd) return cmd @classmethod def _cmd_from_ast(cls, astnode, context, schema): return cls() @classmethod def as_markup(cls, self, *, ctx): node = markup.elements.lang.TreeNode(name=str(self)) for dd in self: if isinstance(dd, AlterObjectProperty): diff = markup.elements.doc.ValueDiff( before=repr(dd.old_value), after=repr(dd.new_value)) node.add_child(label=dd.property, node=diff) else: node.add_child(node=markup.serialize(dd, ctx=ctx)) return node @classmethod def get_context_class(cls): return cls._context_class def new_context(self, context, scls=_void): if context is None: context = CommandContext() if scls is _void: scls = getattr(self, 'scls', None) context_class = self.get_context_class() return context(context_class(self, scls)) def __str__(self): return struct.MixedStruct.__str__(self) def __repr__(self): flds = struct.MixedStruct.__repr__(self) return '<{}.{}{}>'.format(self.__class__.__module__, self.__class__.__name__, (' ' + flds) if flds else '')
class Delta(struct.MixedStruct, metaclass=DeltaMeta): CURRENT_FORMAT_VERSION = 14 id = struct.Field(int) parent_id = struct.Field(int, None) comment = struct.Field(str, None) checksum = struct.Field(int) checksum_details = struct.Field(list, list) deltas = struct.Field(list, None) script = struct.Field(str, None) snapshot = struct.Field(object, None) formatver = struct.Field(int, None) preprocess = struct.Field(str, None) postprocess = struct.Field(str, None) def __init__(self, **kwargs): hash_items = (kwargs['parent_id'], kwargs['checksum'], kwargs['comment']) kwargs['id'] = persistent_hash('%s%s%s' % hash_items) super().__init__(**kwargs) def apply(self, schema): if self.snapshot is not None: try: self.snapshot.apply(schema) except Exception as e: msg = 'failed to apply delta {:032x}'.format(self.id) raise DeltaError(msg, delta=self) from e elif self.deltas: for d in self.deltas: try: d.apply(schema) except Exception as e: msg = 'failed to apply delta {:032x}'.format(self.id) raise DeltaError(msg, delta=self) from e def call_hook(self, session, stage, hook): stage_code = getattr(self, stage, None) if stage_code is not None: with session.transaction(): def _call_hook(): locals = {} exec(stage_code, {}, locals) try: hook_func = locals[hook] except KeyError as e: msg = f'{stage} code does not define {hook}() callable' raise DeltaHookNotFoundError(msg) from e else: hook_func(session) _call_hook() def upgrade(self, context, schema): for d in self.deltas: d.upgrade(context, self.formatver, schema) if self.formatver < 14: if self.deltas[0].preprocess: self.preprocess = \ s_expr.ExpressionText(self.deltas[0].preprocess) if self.deltas[0].postprocess: self.postprocess = \ s_expr.ExpressionText(self.deltas[0].postprocess) self.formatver = context.new_format_ver
class RebaseInheritingObject(sd.ObjectCommand): _delta_action = 'rebase' new_base = struct.Field(tuple, default=tuple()) removed_bases = struct.Field(tuple) added_bases = struct.Field(tuple) def __repr__(self): return '<%s.%s "%s">' % (self.__class__.__module__, self.__class__.__name__, self.classname) def apply(self, schema, context): metaclass = self.get_schema_metaclass() scls = schema.get(self.classname, type=metaclass) self.scls = scls objects = [scls] + list(scls.descendants(schema)) for obj in objects: for refdict in scls.__class__.get_refdicts(): attr = refdict.attr local_attr = refdict.local_attr backref = refdict.backref_attr coll = obj.get_field_value(schema, attr) local_coll = obj.get_field_value(schema, local_attr) for ref_name in tuple(coll.keys(schema)): if not local_coll.has(schema, ref_name): try: obj.get_classref_origin( schema, ref_name, attr, local_attr, backref) except KeyError: del coll[ref_name] for op in self.get_subcommands(type=sd.ObjectCommand): schema, _ = op.apply(schema, context) bases = list(scls.get_bases(schema).objects(schema)) removed_bases = {b.get_name(schema) for b in self.removed_bases} existing_bases = set() for b in bases: if b.get_name(schema) in removed_bases: bases.remove(b) else: existing_bases.add(b.get_name(schema)) index = {b.get_name(schema): i for i, b in enumerate(bases)} for new_bases, pos in self.added_bases: if isinstance(pos, tuple): pos, ref = pos if pos == 'LAST': idx = len(bases) elif pos == 'FIRST': idx = 0 else: idx = index[ref] bases[idx:idx] = [schema.get(b.get_name(schema)) for b in new_bases if b.get_name(schema) not in existing_bases] index = {b.get_name(schema): i for i, b in enumerate(bases)} if not bases: bases = [schema.get(metaclass.get_default_base_name())] schema = scls.set_field_value(schema, 'bases', bases) new_mro = compute_mro(schema, scls)[1:] schema = scls.set_field_value(schema, 'mro', new_mro) if not self.has_attribute_value('mro'): self.set_attribute_value('mro', new_mro) alter_cmd = sd.ObjectCommandMeta.get_command_class( sd.AlterObject, metaclass) descendants = list(scls.descendants(schema)) if descendants and not list(self.get_subcommands(type=alter_cmd)): for descendant in descendants: new_mro = compute_mro(schema, descendant)[1:] schema = descendant.set_field_value(schema, 'mro', new_mro) alter = alter_cmd(classname=descendant.get_name(schema)) alter.add(sd.AlterObjectProperty( property='mro', new_value=new_mro, )) self.add(alter) return schema, scls
class RenameObject(ObjectCommand): _delta_action = 'rename' astnode = qlast.Rename new_name = struct.Field(sn.Name) def __repr__(self): return '<%s.%s "%s" to "%s">' % (self.__class__.__module__, self.__class__.__name__, self.classname, self.new_name) def _rename_begin(self, schema, context, scls): self._validate_legal_command(schema, context) self.old_name = self.classname schema = scls.set_field_value(schema, 'name', self.new_name) parent_ctx = context.get(CommandContextToken) for subop in parent_ctx.op.get_subcommands(type=ObjectCommand): if subop is not self and subop.classname == self.old_name: subop.classname = self.new_name return schema def _rename_innards(self, schema, context, scls): return schema def _rename_finalize(self, schema, context, scls): return schema def apply(self, schema, context): metaclass = self.get_schema_metaclass() scls = schema.get(self.classname, type=metaclass) self.scls = scls schema = self._rename_begin(schema, context, scls) schema = self._rename_innards(schema, context, scls) schema = self._rename_finalize(schema, context, scls) return schema, scls def _get_ast(self, schema, context): astnode = self._get_ast_node(context) new_name = sn.shortname_from_fullname(self.new_name) if new_name != self.new_name: # Derived name name_b32 = base64.b32encode(self.new_name.name.encode()).decode() new_nname = '__b32_' + name_b32.replace('=', '_') new_name = sn.Name(module=self.new_name.module, name=new_nname) else: new_name = self.new_name ref = qlast.ObjectRef( name=new_name.name, module=new_name.module) return astnode(new_name=ref) @classmethod def _cmd_from_ast(cls, schema, astnode, context): parent_ctx = context.get(CommandContextToken) parent_class = parent_ctx.op.get_schema_metaclass() rename_class = ObjectCommandMeta.get_command_class( RenameObject, parent_class) return rename_class._rename_cmd_from_ast(schema, astnode, context) @classmethod def _rename_cmd_from_ast(cls, schema, astnode, context): parent_ctx = context.get(CommandContextToken) parent_class = parent_ctx.op.get_schema_metaclass() rename_class = ObjectCommandMeta.get_command_class( RenameObject, parent_class) new_name = astnode.new_name if new_name.name.startswith('__b32_'): name_b32 = new_name.name[6:].replace('_', '=') new_nname = base64.b32decode(name_b32).decode() new_name = sn.Name(module=new_name.module, name=new_nname) return rename_class( metaclass=parent_class, classname=parent_ctx.op.classname, new_name=sn.Name( module=new_name.module, name=new_name.name ) )
class ObjectCommand(Command, metaclass=ObjectCommandMeta): """Base class for all Object-related commands.""" classname = struct.Field(sn.Name) @classmethod def _get_ast_name(cls, schema, astnode, context): return astnode.name.name @classmethod def _classname_from_ast(cls, schema, astnode, context): nqname = cls._get_ast_name(schema, astnode, context) module = context.modaliases.get(astnode.name.module, astnode.name.module) if module is None: raise errors.SchemaDefinitionError( f'unqualified name and no default module set', context=astnode.name.context ) return sn.Name(module=module, name=nqname) @classmethod def _cmd_from_ast(cls, schema, astnode, context): classname = cls._classname_from_ast(schema, astnode, context) return cls(classname=classname) def _append_subcmd_ast(cls, schema, node, subcmd, context): subnode = subcmd.get_ast(schema, context) if subnode is not None: node.commands.append(subnode) def _get_ast_node(self, context): return self.__class__.astnode def _get_ast(self, schema, context): astnode = self._get_ast_node(context) if isinstance(self.classname, sn.Name): nname = sn.shortname_from_fullname(self.classname) name = qlast.ObjectRef(module=nname.module, name=nname.name) else: name = qlast.ObjectRef(module='', name=self.classname) if astnode.get_field('name'): op = astnode(name=name) else: op = astnode() self._apply_fields_ast(schema, context, op) return op def _apply_fields_ast(self, schema, context, node): for op in self.get_subcommands(type=RenameObject): self._append_subcmd_ast(schema, node, op, context) for op in self.get_subcommands(type=AlterObjectProperty): self._apply_field_ast(schema, context, node, op) mcls = self.get_schema_metaclass() for refdict in mcls.get_refdicts(): self._apply_refs_fields_ast(schema, context, node, refdict) def _apply_refs_fields_ast(self, schema, context, node, refdict): for op in self.get_subcommands(metaclass=refdict.ref_cls): self._append_subcmd_ast(schema, node, op, context) def _apply_field_ast(self, schema, context, node, op): if op.property == 'name': pass else: subnode = op._get_ast(schema, context) if subnode is not None: node.commands.append(subnode) @classmethod def get_schema_metaclass(cls): if cls._schema_metaclass is None: raise TypeError(f'schema metaclass not set for {cls}') return cls._schema_metaclass def get_subcommands(self, *, type=None, metaclass=None): if metaclass is not None: return filter( lambda i: (isinstance(i, ObjectCommand) and issubclass(i.get_schema_metaclass(), metaclass)), self) else: return super().get_subcommands(type=type) def _validate_legal_command(self, schema, context): from . import functions as s_functions if (not context.stdmode and not context.testmode and not isinstance(self, s_functions.ParameterCommand)): if isinstance(self.classname, sn.Name): shortname = sn.shortname_from_fullname(self.classname) modname = self.classname.module else: # modules have classname as simple strings shortname = modname = self.classname if modname in s_schema.STD_MODULES: raise errors.SchemaDefinitionError( f'cannot {self._delta_action} `{shortname}`: ' f'module {modname} is read-only', context=self.source_context)
class AlterObjectProperty(Command): astnode = (qlast.SetField, qlast.SetInternalField) property = struct.Field(str) old_value = struct.Field(object, None) new_value = struct.Field(object, None) source = struct.Field(str, None) @classmethod def _cmd_tree_from_ast(cls, schema, astnode, context): from edb.lang.edgeql import compiler as qlcompiler propname = astnode.name.name parent_ctx = context.get(CommandContextToken) parent_op = parent_ctx.op field = parent_op.get_schema_metaclass().get_field(propname) if field is None: raise errors.SchemaDefinitionError( f'{propname!r} is not a valid field', context=astnode.context) if not (isinstance(astnode, qlast.SetInternalField) or field.allow_ddl_set or context.stdmode or context.testmode): raise errors.SchemaDefinitionError( f'{propname!r} is not a valid field', context=astnode.context) if astnode.as_expr: new_value = s_expr.ExpressionText( edgeql.generate_source(astnode.value, pretty=False)) else: if isinstance(astnode.value, qlast.BaseConstant): new_value = qlcompiler.evaluate_ast_to_python_val( astnode.value, schema=schema) elif isinstance(astnode.value, qlast.Tuple): new_value = tuple( qlcompiler.evaluate_ast_to_python_val( el.value, schema=schema) for el in astnode.value.elements ) elif isinstance(astnode.value, qlast.ObjectRef): new_value = utils.ast_objref_to_objref( astnode.value, modaliases=context.modaliases, schema=schema) else: raise ValueError( f'unexpected value in attribute: {astnode.value!r}') return cls(property=propname, new_value=new_value) def _get_ast(self, schema, context): value = self.new_value new_value_empty = \ (value is None or (isinstance(value, collections.abc.Container) and not value)) old_value_empty = \ (self.old_value is None or (isinstance(self.old_value, collections.abc.Container) and not self.old_value)) if new_value_empty and not old_value_empty: op = qlast.DropAttributeValue( name=qlast.ObjectRef(module='', name=self.property)) return op if new_value_empty and old_value_empty: return if isinstance(value, s_expr.ExpressionText): value = edgeql.parse(str(value)) elif utils.is_nontrivial_container(value): value = qlast.Tuple(elements=[ qlast.BaseConstant.from_python(el) for el in value ]) else: value = qlast.BaseConstant.from_python(value) as_expr = isinstance(value, qlast.ExpressionText) op = qlast.SetField( name=qlast.ObjectRef(module='', name=self.property), value=value, as_expr=as_expr) return op def __repr__(self): return '<%s.%s "%s":"%s"->"%s">' % ( self.__class__.__module__, self.__class__.__name__, self.property, self.old_value, self.new_value)
class Code(BaseCode): tokens = struct.Field(TokenList, default=None, coerce=True)
class Token(BaseCode): val = struct.Field(str)