Example #1
0
    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)
Example #2
0
    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')
Example #3
0
    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)])
        ]
Example #4
0
    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')
Example #5
0
    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}'
            )
Example #6
0
    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)
Example #7
0
    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)
Example #8
0
    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
Example #9
0
    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')
Example #10
0
    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
        }
Example #11
0
    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
Example #12
0
    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)]
        )]
Example #13
0
    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)
Example #14
0
    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)
Example #15
0
    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)
Example #16
0
    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
Example #17
0
    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
Example #18
0
 def popitem(self):
     try:
         return super().popitem()
     except KeyError as e:
         raise SchemaError(f'Schema is empty') from e
Example #19
0
 def __access(self, method, item):
     try:
         return method(item)
     except KeyError as e:
         raise SchemaError(f'Unknown key {item!r}') from e