def change_type_key(self, updater: DocumentUpdater, diff: Diff): """ Change type of field. Try to convert value in db :param updater: :param diff: :return: """ self._check_diff(updater, diff, False, str) if not diff.old or not diff.new: raise SchemaError( f"Old or new {updater.document_type}{updater.field_name}.type_key " f"values are not set") field_classes = [] for val in (diff.old, diff.new): if val not in type_key_registry: raise SchemaError( f'Unknown type_key {updater.document_type}{updater.field_name}: ' f'{val!r}') field_classes.append(type_key_registry[val].field_cls) new_handler_cls = type_key_registry[diff.new].field_handler_cls new_handler = new_handler_cls(self.db, self.document_type, self.left_schema, self.left_field_schema, self.right_field_schema, self.migration_policy) new_handler.convert_type(updater, *field_classes)
def _prepare(self, db: Database, left_schema: Schema, migration_policy: MigrationPolicy, ensure_existence: bool): super()._prepare(db, left_schema, migration_policy, True) if ensure_existence and self.field_name not in left_schema[ self.document_type]: raise SchemaError(f'Field {self.document_type}.{self.field_name} ' f'does not exist in schema') elif not ensure_existence and self.field_name in left_schema[ self.document_type]: raise SchemaError(f'Field {self.document_type}.{self.field_name} ' f'already exists in schema')
def to_schema_patch(self, left_schema: Schema): if self.document_type not in left_schema: raise SchemaError(f'Document {self.document_type!r} is not in schema') if self.field_name not in left_schema[self.document_type]: raise SchemaError(f'Field {self.document_type}.{self.field_name} is not in schema') left_field_schema = left_schema[self.document_type][self.field_name] return [ ('remove', f'{self.document_type}', [(self.field_name, left_field_schema)]), ('add', f'{self.document_type}', [(self.new_name, left_field_schema)]) ]
def _check_diff(self, diff: Diff, can_be_none=True, check_type=None): if diff.new == diff.old: raise SchemaError( f'Parameter {diff.key} does not changed from previous Action') if check_type is not None: if diff.old not in (UNSET, None) and not isinstance(diff.old, check_type) \ or diff.new not in (UNSET, None) and not isinstance(diff.new, check_type): raise SchemaError(f'{diff.key} must have type {check_type!r}') if not can_be_none: if diff.old is None or diff.new is None: raise SchemaError(f'{diff.key} could not be None')
def _prepare(self, db: Database, left_schema: Schema, migration_policy: MigrationPolicy, ensure_existence: bool): super()._prepare(db, left_schema, migration_policy, True) if ensure_existence and self.index_name not in left_schema[ self.document_type].indexes: raise SchemaError( f'Index {self.index_name} does not exist in schema of {self.document_type}' ) elif not ensure_existence and self.index_name in left_schema[ self.document_type].indexes: raise SchemaError( f'Index {self.index_name} already exists in schema of {self.document_type}' )
def _run_migration(self, self_schema: Schema.Document, parameters: Mapping[str, Any], swap: bool = False): # Try to process all parameters on same order to avoid # potential problems on repeated launches if some query on # previous launch was failed for name in sorted(parameters.keys() | self_schema.parameters.keys()): left_value = self_schema.parameters.get(name, UNSET) right_value = parameters.get(name, UNSET) if left_value == right_value: continue diff = Diff(old=right_value if swap else left_value, new=left_value if swap else right_value, key=name) log.debug(">> Change %s: %s => %s", repr(name), repr(diff.old), repr(diff.new)) try: method = getattr(self, f'change_{name}') except AttributeError as e: raise SchemaError(f'Unknown document parameter: {name}') from e inherit = self._run_ctx['left_schema'][ self.document_type].parameters.get('inherit') document_cls = document_type_to_class_name( self.document_type) if inherit else None updater = DocumentUpdater(self._run_ctx['db'], self.document_type, self._run_ctx['left_schema'], '', self._run_ctx['migration_policy'], document_cls) method(updater, diff)
def change_db_field(self, updater: DocumentUpdater, diff: Diff): """ Change db field name of a field. Simply rename this field :param updater: :param diff: :return: """ def by_path(ctx: ByPathContext): path = ctx.filter_dotpath.split('.')[:-1] ctx.collection.update_many( { ctx.filter_dotpath: { '$exists': True }, **ctx.extra_filter }, {'$rename': { ctx.filter_dotpath: '.'.join(path + [diff.new]) }}) def by_doc(ctx: ByDocContext): doc = ctx.document if diff.old in doc: doc[diff.new] = doc.pop(diff.old) self._check_diff(updater, diff, False, str) if not diff.new or not diff.old: raise SchemaError( f"{updater.document_type}{updater.field_name}.db_field " f"must be a non-empty string") updater.update_combined(by_path, by_doc, False)
def get_field_handler_cls(self, type_key: str): """Concrete FieldHandler class for a given type key""" if type_key not in type_key_registry: raise SchemaError( f'Unknown type_key in {self.document_type}.{self.field_name}: ' f'{type_key}') return type_key_registry[type_key].field_handler_cls
def _check_diff(updater: DocumentUpdater, diff: Diff, can_be_none=True, check_type=None): if diff.new == diff.old: raise SchemaError( f'{updater.document_type}.{updater.field_name}.{diff.key} ' f'does not changed from previous Action') if check_type is not None: if diff.old not in (UNSET, None) and not isinstance(diff.old, check_type) \ or diff.new not in (UNSET, None) and not isinstance(diff.new, check_type): raise SchemaError( f'{updater.document_type}.{updater.field_name}.{diff.key} ' f'must have type {check_type!r}') if not can_be_none: if diff.old is None or diff.new is None: raise SchemaError( f'{updater.document_type}.{updater.field_name}.{diff.key} ' f'could not be None')
def _prepare(self, db: Database, left_schema: Schema, migration_policy: MigrationPolicy, ensure_existence: bool): if ensure_existence and self.document_type not in left_schema: raise SchemaError( f'Document {self.document_type} does not exist in schema') elif not ensure_existence and self.document_type in left_schema: raise SchemaError( f'Document {self.document_type} already exists in schema') collection_name = self.parameters.get('collection') if not collection_name: docschema = left_schema.get(self.document_type) if docschema: collection_name = docschema.parameters.get('collection') collection = db[collection_name] if collection_name else db[ 'COLLECTION_PLACEHOLDER'] self._run_ctx = { 'left_schema': left_schema, 'db': db, 'collection': collection, 'migration_policy': migration_policy }
def to_schema_patch(self, left_schema: Schema): if self.document_type not in left_schema: raise SchemaError(f'Document {self.document_type!r} is not in schema') if self.field_name not in left_schema[self.document_type]: raise SchemaError(f'Field {self.document_type}.{self.field_name} is not in schema') left_field_schema = left_schema[self.document_type][self.field_name] # Get schema skeleton for field type field_handler_cls = self.get_field_handler_cls( self.parameters.get('type_key', left_field_schema['type_key']) ) right_schema_skel = field_handler_cls.schema_skel() extra_keys = self.parameters.keys() - right_schema_skel.keys() if extra_keys: raise SchemaError(f'Unknown keys in schema of field ' f'{self.document_type}.{self.field_name}: {extra_keys}') # Shortcuts left = left_field_schema params = self.parameters # Remove params d = [('remove', f'{self.document_type}.{self.field_name}', [(key, ())]) for key in left.keys() - right_schema_skel.keys()] # Add new params d += [('add', f'{self.document_type}.{self.field_name}', [(key, params[key])]) for key in params.keys() - left.keys()] # Change params if they are requested to be changed d += [('change', f'{self.document_type}.{self.field_name}.{key}', (left[key], params[key])) for key in params.keys() & left.keys() if left[key] != params[key]] return d
def to_schema_patch(self, left_schema: Schema): keys_to_check = {'type_key', 'db_field'} missed_keys = keys_to_check - self.parameters.keys() if missed_keys: raise SchemaError(f"Missed required parameters in " f"CreateField({self.document_type}...): {missed_keys}") # Get schema skeleton for field type field_handler_cls = self.get_field_handler_cls(self.parameters['type_key']) right_field_schema_skel = field_handler_cls.schema_skel() extra_keys = self.parameters.keys() - right_field_schema_skel.keys() if extra_keys: raise SchemaError(f'Unknown CreateField({self.document_type}...) parameters: ' f'{extra_keys}') field_params = { **right_field_schema_skel, **self.parameters } return [( 'add', self.document_type, [(self.field_name, field_params)] )]
def change_separator(self, updater: DocumentUpdater, diff: Diff): """Change separator in datetime strings""" def by_doc(ctx: ByDocContext): doc = ctx.document if updater.field_name in doc: doc[updater.field_name] = doc[updater.field_name].replace( diff.old, diff.new) self._check_diff(updater, diff, False, str) if not diff.new or not diff.old: raise SchemaError( f'{updater.document_type}{updater.field_name}.separator ' f'must not be empty') if diff.new == UNSET: return updater.update_by_document(by_doc)
def change_required(self, updater: DocumentUpdater, diff: Diff): """ Make field required, which means to add this field to all documents. Reverting of this doesn't require smth to do :param updater: :param diff: :return: """ self._check_diff(updater, diff, False, bool) if diff.old is not True and diff.new is True: default = self.right_field_schema.get('default') # None and UNSET default has the same meaning here if default is None: raise SchemaError( f'{updater.document_type}{updater.field_name}.default is not ' f'set for required field') self._set_default_value(updater, default)
def change_primary_key(self, updater: DocumentUpdater, diff: Diff): """ Setting field as primary key means to set it required and unique :param updater: :param diff: :return: """ def by_path(ctx: ByPathContext): fltr = {ctx.filter_dotpath: {'$exists': False}, **ctx.extra_filter} check_empty_result(ctx.collection, ctx.filter_dotpath, fltr) self._check_diff(updater, diff, False, bool) if updater.is_embedded: raise SchemaError( f'Embedded document {updater.document_type} cannot have primary key' ) if self.migration_policy.name == 'strict': updater.update_by_path(by_path)
def _fix_field_params(cls, document_type: str, field_name: str, field_params: Mapping[str, Any], old_schema: Schema, new_schema: Schema) -> Mapping[str, Any]: """ Search for potential problems which could be happened during migration and return fixed field schema. If such problem could not be resolved only by changing parameters then raise an SchemaError :param document_type: :param field_name: :param field_params: :param old_schema: :param new_schema: :raises SchemaError: when some problem found :return: """ # TODO: Check all defaults in diffs against choices, required, etc. # TODO: check nones for type_key, etc. new_changes = {k: v.new for k, v in field_params.items()} # Field becomes required or left as required without changes become_required = new_changes.get('required', new_schema.get(field_name, {}).get('required')) # 'default' diff object has default parameter or field become # field with default or field has default value already default = (field_params.get('required') and field_params['required'].default) \ or new_changes.get('default') \ or new_schema.get(field_name, {}).get('default') if become_required and default is None: # TODO: replace following error on interactive mode raise SchemaError(f'Field {document_type}.{field_name} could not be ' f'created since it defined as required but has not a default value') return field_params
def _is_my_index_used_by_other_documents(self) -> bool: """ Return True if current index is declared in another document for the same collection :return: """ my_document = self._run_ctx['left_schema'].get( self.document_type) # type: Schema.Document my_collection = my_document.parameters.get('collection') if my_collection is None: raise SchemaError( f'No collection name in {self.document_type} schema parameters. Schema is corrupted' ) for name, schema in self._run_ctx['left_schema'].items(): if name == self.document_type or name.startswith( flags.EMBEDDED_DOCUMENT_NAME_PREFIX): continue col = schema.parameters.get('collection') if col == my_collection and self.index_name in schema.indexes: return True return False
def popitem(self): try: return super().popitem() except KeyError as e: raise SchemaError(f'Schema is empty') from e
def __access(self, method, item): try: return method(item) except KeyError as e: raise SchemaError(f'Unknown key {item!r}') from e