diff --git a/ravendb/__init__.py b/ravendb/__init__.py index 4cb7642c..a7f3ef5e 100644 --- a/ravendb/__init__.py +++ b/ravendb/__init__.py @@ -26,7 +26,10 @@ AutoIndexDefinition, AutoIndexFieldOptions, ) -from ravendb.documents.indexes.index_creation import AbstractIndexDefinitionBuilder, AbstractIndexCreationTask +from ravendb.documents.indexes.abstract_index_creation_tasks import ( + AbstractIndexDefinitionBuilder, + AbstractIndexCreationTask, +) from ravendb.documents.indexes.spatial.configuration import AutoSpatialOptions from ravendb.documents.operations.attachments import ( DeleteAttachmentOperation, diff --git a/ravendb/documents/commands/subscriptions.py b/ravendb/documents/commands/subscriptions.py index 7089f3aa..586ea9a0 100644 --- a/ravendb/documents/commands/subscriptions.py +++ b/ravendb/documents/commands/subscriptions.py @@ -75,7 +75,7 @@ def __init__( @classmethod def from_json(cls, json_dict: Dict) -> TcpConnectionInfo: return TcpConnectionInfo( - json_dict.get("Port", None), + json_dict.get("Port", None) or 0, json_dict.get("Url", None), json_dict.get("Certificate", None), json_dict.get("Urls", None), diff --git a/ravendb/documents/indexes/abstract_index_creation_tasks.py b/ravendb/documents/indexes/abstract_index_creation_tasks.py new file mode 100644 index 00000000..e96bbee8 --- /dev/null +++ b/ravendb/documents/indexes/abstract_index_creation_tasks.py @@ -0,0 +1,434 @@ +from abc import abstractmethod, ABC +from typing import Generic, TypeVar, Union, Dict, Set, Callable, Optional, List, TYPE_CHECKING + +from ravendb.documents.conventions import DocumentConventions +from ravendb.documents.indexes.definitions import ( + IndexDefinition, + AbstractCommonApiForIndexes, + IndexPriority, + IndexLockMode, + IndexDeploymentMode, + IndexState, + FieldIndexing, + FieldStorage, + FieldTermVector, + AdditionalAssembly, + SpatialOptions, + IndexFieldOptions, + IndexType, +) +from ravendb.documents.indexes.spatial.configuration import SpatialOptionsFactory +from ravendb.documents.operations.indexes import PutIndexesOperation +from ravendb.documents.store.definition import DocumentStoreBase +from ravendb.primitives import constants + +_T_IndexDefinition = TypeVar("_T_IndexDefinition", bound=IndexDefinition) + +if TYPE_CHECKING: + from ravendb.documents.store.definition import DocumentStore + + +class AbstractIndexCreationTaskBase(AbstractCommonApiForIndexes, Generic[_T_IndexDefinition]): + @abstractmethod + def create_index_definition(self) -> Union[IndexDefinition, _T_IndexDefinition]: + pass + + def __init__( + self, + conventions: DocumentConventions = None, + priority: IndexPriority = None, + lock_mode: IndexLockMode = None, + deployment_mode: IndexDeploymentMode = None, + state: IndexState = None, + ): + super().__init__() + self.conventions = conventions + self.priority = priority + self.lock_mode = lock_mode + self.deployment_mode = deployment_mode + self.state = state + + def execute(self, store: "DocumentStore", conventions: DocumentConventions = None, database: str = None): + old_conventions = self.conventions + database = DocumentStoreBase.get_effective_database(store, database) + try: + self.conventions = conventions or self.conventions or store.get_request_executor(database).conventions + index_definition = self.create_index_definition() + index_definition.name = self.index_name + + if self.lock_mode is not None: + index_definition.lock_mode = self.lock_mode + + if self.priority is not None: + index_definition.priority = self.priority + + if self.state is not None: + index_definition.state = self.state + + if self.deployment_mode is not None: + index_definition.deployment_mode = self.deployment_mode + + store.maintenance.for_database(database).send(PutIndexesOperation(index_definition)) + + finally: + self.conventions = old_conventions + + +class AbstractGenericIndexCreationTask( + Generic[_T_IndexDefinition], AbstractIndexCreationTaskBase[_T_IndexDefinition], ABC +): + def __init__(self): + super().__init__() + + self._reduce: Union[None, str] = None + + self._stores_strings: Dict[str, FieldStorage] = {} + self._indexes_strings: Dict[str, FieldIndexing] = {} + self._analyzers_strings: Dict[str, str] = {} + self._index_suggestions: Set[str] = set() + self._term_vectors_strings: Dict[str, FieldTermVector] = {} + self._spatial_options_strings: Dict[str, SpatialOptions] = {} + + self._output_reduce_to_collection: Union[None, str] = None + self._pattern_for_output_reduce_to_collection_references: Union[None, str] = None + self._pattern_references_collection_name: Union[None, str] = None + + @property + def reduce(self) -> str: + return self._reduce + + @reduce.setter + def reduce(self, value: str): + self._reduce = value + + @property + def is_map_reduce(self) -> bool: + return self._reduce is not None + + def _index(self, field: str, indexing: FieldIndexing) -> None: + self._indexes_strings[field] = indexing + + def _spatial(self, field: str, indexing: Callable[[SpatialOptionsFactory], SpatialOptions]) -> None: + self._spatial_options_strings[field] = indexing(SpatialOptionsFactory()) + + def _store_all_fields(self, storage: FieldStorage) -> None: + self._stores_strings[constants.Documents.Indexing.Fields.ALL_FIELDS] = storage + + def _store(self, field: str, storage: FieldStorage) -> None: + """ + Register a field to be stored + @param field: Field name + @param storage: Field storage value to use + """ + self._stores_strings[field] = storage + + def _analyze(self, field: str, analyzer: str) -> None: + """ + Register a field to be analyzed + @param field: Field name + @param analyzer: analyzer to use + """ + self._analyzers_strings[field] = analyzer + + def _term_vector(self, field: str, term_vector: FieldTermVector) -> None: + """ + Register a field to have term vectors + @param field: Field name + @param term_vector: TermVector type + """ + self._term_vectors_strings[field] = term_vector + + def _suggestion(self, field: str) -> None: + self._index_suggestions.add(field) + + def _add_assembly(self, assembly: AdditionalAssembly) -> None: + if assembly is None: + raise ValueError("Assembly cannot be None") + + if self.additional_assemblies is None: + self.additional_assemblies = set() + + self.additional_assemblies.add(assembly) + + +class AbstractIndexDefinitionBuilder(Generic[_T_IndexDefinition]): + def __init__(self, index_name: str): + self._index_name = index_name or self.__class__.__name__ + if len(self._index_name) > 256: + raise ValueError(f"The index name is limited to 256 characters, but was: {self._index_name}") + + self.reduce: Optional[str] = None + + self.stores_strings: Dict[str, FieldStorage] = {} + self.indexes_strings: Dict[str, FieldIndexing] = {} + self.analyzers_strings: Dict[str, str] = {} + self.suggestions_options: Set[str] = set() + self.term_vectors_strings: Dict[str, FieldTermVector] = {} + self.spatial_indexes_strings: Dict[str, SpatialOptions] = {} + + self.lock_mode: Optional[IndexLockMode] = None + self.priority: Optional[IndexLockMode] = None + self.state: Optional[IndexState] = None + self.deployment_mode: Optional[IndexDeploymentMode] = None + + self.output_reduce_to_collection: Optional[str] = None + self.pattern_for_output_reduce_to_collection_references: Optional[str] = None + self.pattern_references_collection_name: Optional[str] = None + + self.additional_sources: Optional[Dict[str, str]] = None + self.additional_assemblies: Optional[Set[AdditionalAssembly]] = None + self.configuration: Dict[str, str] = {} + + @abstractmethod + def _new_index_definition(self) -> Union[IndexDefinition, _T_IndexDefinition]: + pass + + @abstractmethod + def _to_index_definition(self, index_definition: _T_IndexDefinition, conventions: DocumentConventions) -> None: + pass + + def __apply_values( + self, + index_definition: IndexDefinition, + values: Dict[str, object], + action: Callable[[IndexFieldOptions, object], None], + ) -> None: + for key, value in values.items(): + field = index_definition.fields.get(key, IndexFieldOptions()) + action(field, value) + index_definition.fields[key] = field # if the field wasn't indexed yet, we need to set it. + + def to_index_definition(self, conventions: DocumentConventions, validate_map: bool = True) -> _T_IndexDefinition: + try: + index_definition = self._new_index_definition() + index_definition.name = self._index_name + index_definition.reduce = self.reduce + index_definition.lock_mode = self.lock_mode + index_definition.priority = self.priority + index_definition.state = self.state + index_definition.output_reduce_to_collection = self.output_reduce_to_collection + index_definition.pattern_for_output_reduce_to_collection_references = ( + self.pattern_for_output_reduce_to_collection_references + ) + index_definition.pattern_references_collection_name = self.pattern_references_collection_name + + suggestions = {} + for suggestions_option in self.suggestions_options: + suggestions[suggestions_option] = True + + def __set_indexing(options, value): + options.indexing = value + + def __set_storage(options, value): + options.storage = value + + def __set_analyzer(options, value): + options.analyzer = value + + def __set_term_vector(options, value): + options.term_vector = value + + def __set_spatial(options, value): + options.spatial = value + + def __set_suggestions(options, value): + options.suggestions = value + + self.__apply_values(index_definition, self.indexes_strings, __set_indexing) + self.__apply_values(index_definition, self.stores_strings, __set_storage) + self.__apply_values(index_definition, self.analyzers_strings, __set_analyzer) + self.__apply_values(index_definition, self.term_vectors_strings, __set_term_vector) + self.__apply_values(index_definition, self.spatial_indexes_strings, __set_spatial) + self.__apply_values(index_definition, suggestions, __set_suggestions) + + index_definition.additional_sources = self.additional_sources + index_definition.additional_assemblies = self.additional_assemblies + index_definition.configuration = self.configuration + + self._to_index_definition(index_definition, conventions) + + return index_definition + + except Exception as e: + raise RuntimeError(f"Failed to create index {self._index_name}", e) # todo: IndexCompilationException + + +class IndexDefinitionBuilder(AbstractIndexDefinitionBuilder[IndexDefinition]): + def __init__(self, index_name: Optional[str] = None): + super().__init__(index_name) + self.map: Union[None, str] = None + + def _new_index_definition(self) -> IndexDefinition: + return IndexDefinition() + + def to_index_definition(self, conventions: DocumentConventions, validate_map: bool = True) -> IndexDefinition: + if self.map is None and validate_map: + raise ValueError( + f"Map is required to generate an index, " + f"you cannot create an index without a valid map property (in index {self._index_name})." + ) + return super().to_index_definition(conventions, validate_map) + + def _to_index_definition(self, index_definition: IndexDefinition, conventions: DocumentConventions) -> None: + if self.map is None: + return + + index_definition.maps.add(self.map) + + +class AbstractIndexCreationTask(AbstractGenericIndexCreationTask[IndexDefinition], ABC): + def __init__(self): + super().__init__() + self._map: Union[None, str] = None + + @property + def map(self) -> str: + return self._map + + @map.setter + def map(self, value: str): + self._map = value + + def create_index_definition(self) -> IndexDefinition: + if self.conventions is None: + self.conventions = DocumentConventions() + + index_definition_builder = IndexDefinitionBuilder(self.index_name) + index_definition_builder.indexes_strings = self._indexes_strings + index_definition_builder.analyzers_strings = self._analyzers_strings + index_definition_builder.map = self._map + index_definition_builder.reduce = self._reduce + index_definition_builder.stores_strings = self._stores_strings + index_definition_builder.suggestions_options = self._index_suggestions + index_definition_builder.term_vectors_strings = self._term_vectors_strings + index_definition_builder.spatial_indexes_strings = self._spatial_options_strings + index_definition_builder.output_reduce_to_collection = self._output_reduce_to_collection + index_definition_builder.pattern_for_output_reduce_to_collection_references = ( + self._pattern_for_output_reduce_to_collection_references + ) + index_definition_builder.pattern_references_collection_name = self._pattern_references_collection_name + index_definition_builder.additional_sources = self.additional_sources + index_definition_builder.additional_assemblies = self.additional_assemblies + index_definition_builder.configuration = self.configuration + index_definition_builder.lock_mode = self.lock_mode + index_definition_builder.priority = self.priority + index_definition_builder.state = self.state + index_definition_builder.deployment_mode = self.deployment_mode + + return index_definition_builder.to_index_definition(self.conventions) + + +class AbstractJavaScriptIndexCreationTask(AbstractIndexCreationTaskBase[IndexDefinition]): + def __init__(self): + super().__init__() + self._definition = IndexDefinition() + + @property + def maps(self) -> List[str]: + return self._definition.maps + + @maps.setter + def maps(self, value: List[str]): + self._definition.maps = value + + @property + def fields(self) -> Dict[str, IndexFieldOptions]: + return self._definition.fields + + @fields.setter + def fields(self, value: Dict[str, IndexFieldOptions]): + self._definition.fields = value + + @property + def reduce(self) -> str: + return self._definition.reduce + + @reduce.setter + def reduce(self, value: str): + self._definition.reduce = value + + @property + def output_reduce_to_collection(self) -> str: + return self._definition.output_reduce_to_collection + + @output_reduce_to_collection.setter + def output_reduce_to_collection(self, value: str): + self._definition.output_reduce_to_collection = value + + @property + def pattern_references_collection_name(self) -> str: + return self._definition.pattern_references_collection_name + + @pattern_references_collection_name.setter + def pattern_references_collection_name(self, value: str): + self._definition.pattern_references_collection_name = value + + @property + def pattern_for_output_reduce_to_collection_references(self) -> str: + return self._definition.pattern_for_output_reduce_to_collection_references + + @pattern_for_output_reduce_to_collection_references.setter + def pattern_for_output_reduce_to_collection_references(self, value: str): + self._definition.pattern_for_output_reduce_to_collection_references = value + + def create_index_definition(self) -> Union[IndexDefinition, _T_IndexDefinition]: + self._definition.name = self.index_name + self._definition.type = IndexType.JAVA_SCRIPT_MAP_REDUCE if self.is_map_reduce else IndexType.JAVA_SCRIPT_MAP + if self.additional_sources: + self._definition.additional_sources = self.additional_sources + else: + self._definition.additional_sources = {} + + if self.additional_assemblies: + self._definition.additional_assemblies = self.additional_assemblies + else: + self._definition.additional_assemblies = {} + + self._definition.configuration = self.configuration + self._definition.lock_mode = self.lock_mode + self._definition.priority = self.priority + self._definition.state = self.state + self._definition.deployment_mode = self.deployment_mode + return self._definition + + +class AbstractMultiMapIndexCreationTask(AbstractGenericIndexCreationTask[IndexDefinition]): + def __init__(self): + super().__init__() + self.__maps: List[str] = [] + + def _add_map(self, map: str) -> None: + if map is None: + raise ValueError("Map cannot be None") + + self.__maps.append(map) + + def create_index_definition(self): + if self.conventions is None: + self.conventions = DocumentConventions() + + index_definition_builder = IndexDefinitionBuilder(self.index_name) + index_definition_builder.indexes_strings = self._indexes_strings + index_definition_builder.analyzers_strings = self._analyzers_strings + index_definition_builder.reduce = self._reduce + index_definition_builder.stores_strings = self._stores_strings + index_definition_builder.suggestions_options = self._index_suggestions + index_definition_builder.term_vectors_strings = self._term_vectors_strings + index_definition_builder.spatial_indexes_strings = self._spatial_options_strings + index_definition_builder.output_reduce_to_collection = self._output_reduce_to_collection + index_definition_builder.pattern_for_output_reduce_to_collection_references = ( + self._pattern_for_output_reduce_to_collection_references + ) + index_definition_builder.pattern_references_collection_name = self._pattern_references_collection_name + index_definition_builder.additional_sources = self.additional_sources + index_definition_builder.additional_assemblies = self.additional_assemblies + index_definition_builder.configuration = self.configuration + index_definition_builder.lock_mode = self.lock_mode + index_definition_builder.priority = self.priority + index_definition_builder.state = self.state + index_definition_builder.deployment_mode = self.deployment_mode + + index_definition = index_definition_builder.to_index_definition(self.conventions, False) + index_definition.maps = set(self.__maps) + + return index_definition diff --git a/ravendb/documents/indexes/counters.py b/ravendb/documents/indexes/counters.py index 99cb4cc6..8b17e187 100644 --- a/ravendb/documents/indexes/counters.py +++ b/ravendb/documents/indexes/counters.py @@ -11,7 +11,10 @@ IndexFieldOptions, IndexType, ) -from ravendb.documents.indexes.index_creation import AbstractIndexCreationTaskBase, AbstractIndexDefinitionBuilder +from ravendb.documents.indexes.abstract_index_creation_tasks import ( + AbstractIndexCreationTaskBase, + AbstractIndexDefinitionBuilder, +) from ravendb.documents.indexes.spatial.configuration import SpatialOptions, SpatialOptionsFactory diff --git a/ravendb/documents/indexes/index_creation.py b/ravendb/documents/indexes/index_creation.py index a2a13cf3..6904abea 100644 --- a/ravendb/documents/indexes/index_creation.py +++ b/ravendb/documents/indexes/index_creation.py @@ -1,453 +1,28 @@ import logging -from abc import abstractmethod, ABC -from typing import Generic, TypeVar, Union, Dict, Set, Callable, Optional, List, Collection +from typing import Collection, TYPE_CHECKING, Optional, List, TypeVar -from ravendb.primitives import constants -from ravendb.documents.conventions import DocumentConventions -from ravendb.documents.indexes.spatial.configuration import SpatialOptionsFactory -from ravendb.documents.store.definition import DocumentStore, DocumentStoreBase -from ravendb.documents.indexes.definitions import ( - IndexDefinition, - AbstractCommonApiForIndexes, - IndexPriority, - IndexLockMode, - IndexDeploymentMode, - IndexState, - FieldIndexing, - FieldStorage, - FieldTermVector, - AdditionalAssembly, - SpatialOptions, - IndexFieldOptions, - IndexType, -) +from ravendb.documents.indexes.definitions import IndexDefinition, IndexPriority, IndexState from ravendb.documents.operations.indexes import PutIndexesOperation -_T_IndexDefinition = TypeVar("_T_IndexDefinition", bound=IndexDefinition) - - -class AbstractIndexCreationTaskBase(AbstractCommonApiForIndexes, Generic[_T_IndexDefinition]): - @abstractmethod - def create_index_definition(self) -> Union[IndexDefinition, _T_IndexDefinition]: - pass - - def __init__( - self, - conventions: DocumentConventions = None, - priority: IndexPriority = None, - lock_mode: IndexLockMode = None, - deployment_mode: IndexDeploymentMode = None, - state: IndexState = None, - ): - super().__init__() - self.conventions = conventions - self.priority = priority - self.lock_mode = lock_mode - self.deployment_mode = deployment_mode - self.state = state - - def execute(self, store: DocumentStore, conventions: DocumentConventions = None, database: str = None): - old_conventions = self.conventions - database = DocumentStoreBase.get_effective_database(store, database) - try: - self.conventions = conventions or self.conventions or store.get_request_executor(database).conventions - index_definition = self.create_index_definition() - index_definition.name = self.index_name - - if self.lock_mode is not None: - index_definition.lock_mode = self.lock_mode - - if self.priority is not None: - index_definition.priority = self.priority - - if self.state is not None: - index_definition.state = self.state - - if self.deployment_mode is not None: - index_definition.deployment_mode = self.deployment_mode - - store.maintenance.for_database(database).send(PutIndexesOperation(index_definition)) - - finally: - self.conventions = old_conventions - - -class AbstractGenericIndexCreationTask( - Generic[_T_IndexDefinition], AbstractIndexCreationTaskBase[_T_IndexDefinition], ABC -): - def __init__(self): - super().__init__() - - self._reduce: Union[None, str] = None - - self._stores_strings: Dict[str, FieldStorage] = {} - self._indexes_strings: Dict[str, FieldIndexing] = {} - self._analyzers_strings: Dict[str, str] = {} - self._index_suggestions: Set[str] = set() - self._term_vectors_strings: Dict[str, FieldTermVector] = {} - self._spatial_options_strings: Dict[str, SpatialOptions] = {} - - self._output_reduce_to_collection: Union[None, str] = None - self._pattern_for_output_reduce_to_collection_references: Union[None, str] = None - self._pattern_references_collection_name: Union[None, str] = None - - @property - def reduce(self) -> str: - return self._reduce - - @reduce.setter - def reduce(self, value: str): - self._reduce = value - - @property - def is_map_reduce(self) -> bool: - return self._reduce is not None - - def _index(self, field: str, indexing: FieldIndexing) -> None: - self._indexes_strings[field] = indexing - - def _spatial(self, field: str, indexing: Callable[[SpatialOptionsFactory], SpatialOptions]) -> None: - self._spatial_options_strings[field] = indexing(SpatialOptionsFactory()) - - def _store_all_fields(self, storage: FieldStorage) -> None: - self._stores_strings[constants.Documents.Indexing.Fields.ALL_FIELDS] = storage - - def _store(self, field: str, storage: FieldStorage) -> None: - """ - Register a field to be stored - @param field: Field name - @param storage: Field storage value to use - """ - self._stores_strings[field] = storage - - def _analyze(self, field: str, analyzer: str) -> None: - """ - Register a field to be analyzed - @param field: Field name - @param analyzer: analyzer to use - """ - self._analyzers_strings[field] = analyzer - - def _term_vector(self, field: str, term_vector: FieldTermVector) -> None: - """ - Register a field to have term vectors - @param field: Field name - @param term_vector: TermVector type - """ - self._term_vectors_strings[field] = term_vector - - def _suggestion(self, field: str) -> None: - self._index_suggestions.add(field) - - def _add_assembly(self, assembly: AdditionalAssembly) -> None: - if assembly is None: - raise ValueError("Assembly cannot be None") - - if self.additional_assemblies is None: - self.additional_assemblies = set() - - self.additional_assemblies.add(assembly) - - -class AbstractIndexDefinitionBuilder(Generic[_T_IndexDefinition]): - def __init__(self, index_name: str): - self._index_name = index_name or self.__class__.__name__ - if len(self._index_name) > 256: - raise ValueError(f"The index name is limited to 256 characters, but was: {self._index_name}") - - self.reduce: Optional[str] = None - - self.stores_strings: Dict[str, FieldStorage] = {} - self.indexes_strings: Dict[str, FieldIndexing] = {} - self.analyzers_strings: Dict[str, str] = {} - self.suggestions_options: Set[str] = set() - self.term_vectors_strings: Dict[str, FieldTermVector] = {} - self.spatial_indexes_strings: Dict[str, SpatialOptions] = {} - - self.lock_mode: Optional[IndexLockMode] = None - self.priority: Optional[IndexLockMode] = None - self.state: Optional[IndexState] = None - self.deployment_mode: Optional[IndexDeploymentMode] = None - - self.output_reduce_to_collection: Optional[str] = None - self.pattern_for_output_reduce_to_collection_references: Optional[str] = None - self.pattern_references_collection_name: Optional[str] = None - - self.additional_sources: Optional[Dict[str, str]] = None - self.additional_assemblies: Optional[Set[AdditionalAssembly]] = None - self.configuration: Dict[str, str] = {} - - @abstractmethod - def _new_index_definition(self) -> Union[IndexDefinition, _T_IndexDefinition]: - pass - - @abstractmethod - def _to_index_definition(self, index_definition: _T_IndexDefinition, conventions: DocumentConventions) -> None: - pass - - def __apply_values( - self, - index_definition: IndexDefinition, - values: Dict[str, object], - action: Callable[[IndexFieldOptions, object], None], - ) -> None: - for key, value in values.items(): - field = index_definition.fields.get(key, IndexFieldOptions()) - action(field, value) - index_definition.fields[key] = field # if the field wasn't indexed yet, we need to set it. - - def to_index_definition(self, conventions: DocumentConventions, validate_map: bool = True) -> _T_IndexDefinition: - try: - index_definition = self._new_index_definition() - index_definition.name = self._index_name - index_definition.reduce = self.reduce - index_definition.lock_mode = self.lock_mode - index_definition.priority = self.priority - index_definition.state = self.state - index_definition.output_reduce_to_collection = self.output_reduce_to_collection - index_definition.pattern_for_output_reduce_to_collection_references = ( - self.pattern_for_output_reduce_to_collection_references - ) - index_definition.pattern_references_collection_name = self.pattern_references_collection_name - - suggestions = {} - for suggestions_option in self.suggestions_options: - suggestions[suggestions_option] = True - - def __set_indexing(options, value): - options.indexing = value - - def __set_storage(options, value): - options.storage = value - - def __set_analyzer(options, value): - options.analyzer = value - - def __set_term_vector(options, value): - options.term_vector = value - - def __set_spatial(options, value): - options.spatial = value - - def __set_suggestions(options, value): - options.suggestions = value - - self.__apply_values(index_definition, self.indexes_strings, __set_indexing) - self.__apply_values(index_definition, self.stores_strings, __set_storage) - self.__apply_values(index_definition, self.analyzers_strings, __set_analyzer) - self.__apply_values(index_definition, self.term_vectors_strings, __set_term_vector) - self.__apply_values(index_definition, self.spatial_indexes_strings, __set_spatial) - self.__apply_values(index_definition, suggestions, __set_suggestions) - - index_definition.additional_sources = self.additional_sources - index_definition.additional_assemblies = self.additional_assemblies - index_definition.configuration = self.configuration - - self._to_index_definition(index_definition, conventions) - - return index_definition - - except Exception as e: - raise RuntimeError(f"Failed to create index {self._index_name}", e) # todo: IndexCompilationException - - -class IndexDefinitionBuilder(AbstractIndexDefinitionBuilder[IndexDefinition]): - def __init__(self, index_name: Optional[str] = None): - super().__init__(index_name) - self.map: Union[None, str] = None - - def _new_index_definition(self) -> IndexDefinition: - return IndexDefinition() - - def to_index_definition(self, conventions: DocumentConventions, validate_map: bool = True) -> IndexDefinition: - if self.map is None and validate_map: - raise ValueError( - f"Map is required to generate an index, " - f"you cannot create an index without a valid map property (in index {self._index_name})." - ) - return super().to_index_definition(conventions, validate_map) - - def _to_index_definition(self, index_definition: IndexDefinition, conventions: DocumentConventions) -> None: - if self.map is None: - return - - index_definition.maps.add(self.map) - - -class AbstractIndexCreationTask(AbstractGenericIndexCreationTask[IndexDefinition], ABC): - def __init__(self): - super().__init__() - self._map: Union[None, str] = None - - @property - def map(self) -> str: - return self._map - - @map.setter - def map(self, value: str): - self._map = value - - def create_index_definition(self) -> IndexDefinition: - if self.conventions is None: - self.conventions = DocumentConventions() - - index_definition_builder = IndexDefinitionBuilder(self.index_name) - index_definition_builder.indexes_strings = self._indexes_strings - index_definition_builder.analyzers_strings = self._analyzers_strings - index_definition_builder.map = self._map - index_definition_builder.reduce = self._reduce - index_definition_builder.stores_strings = self._stores_strings - index_definition_builder.suggestions_options = self._index_suggestions - index_definition_builder.term_vectors_strings = self._term_vectors_strings - index_definition_builder.spatial_indexes_strings = self._spatial_options_strings - index_definition_builder.output_reduce_to_collection = self._output_reduce_to_collection - index_definition_builder.pattern_for_output_reduce_to_collection_references = ( - self._pattern_for_output_reduce_to_collection_references - ) - index_definition_builder.pattern_references_collection_name = self._pattern_references_collection_name - index_definition_builder.additional_sources = self.additional_sources - index_definition_builder.additional_assemblies = self.additional_assemblies - index_definition_builder.configuration = self.configuration - index_definition_builder.lock_mode = self.lock_mode - index_definition_builder.priority = self.priority - index_definition_builder.state = self.state - index_definition_builder.deployment_mode = self.deployment_mode - - return index_definition_builder.to_index_definition(self.conventions) - - -_T_AbstractIndexCreationTask = TypeVar("_T_AbstractIndexCreationTask", bound=AbstractIndexCreationTask) - - -class AbstractJavaScriptIndexCreationTask(AbstractIndexCreationTaskBase[IndexDefinition]): - def __init__(self): - super().__init__() - self._definition = IndexDefinition() - - @property - def maps(self) -> List[str]: - return self._definition.maps - - @maps.setter - def maps(self, value: List[str]): - self._definition.maps = value - - @property - def fields(self) -> Dict[str, IndexFieldOptions]: - return self._definition.fields - - @fields.setter - def fields(self, value: Dict[str, IndexFieldOptions]): - self._definition.fields = value - - @property - def reduce(self) -> str: - return self._definition.reduce - - @reduce.setter - def reduce(self, value: str): - self._definition.reduce = value - - @property - def output_reduce_to_collection(self) -> str: - return self._definition.output_reduce_to_collection - - @output_reduce_to_collection.setter - def output_reduce_to_collection(self, value: str): - self._definition.output_reduce_to_collection = value - - @property - def pattern_references_collection_name(self) -> str: - return self._definition.pattern_references_collection_name - - @pattern_references_collection_name.setter - def pattern_references_collection_name(self, value: str): - self._definition.pattern_references_collection_name = value - - @property - def pattern_for_output_reduce_to_collection_references(self) -> str: - return self._definition.pattern_for_output_reduce_to_collection_references - - @pattern_for_output_reduce_to_collection_references.setter - def pattern_for_output_reduce_to_collection_references(self, value: str): - self._definition.pattern_for_output_reduce_to_collection_references = value - - def create_index_definition(self) -> Union[IndexDefinition, _T_IndexDefinition]: - self._definition.name = self.index_name - self._definition.type = IndexType.JAVA_SCRIPT_MAP_REDUCE if self.is_map_reduce else IndexType.JAVA_SCRIPT_MAP - if self.additional_sources: - self._definition.additional_sources = self.additional_sources - else: - self._definition.additional_sources = {} - - if self.additional_assemblies: - self._definition.additional_assemblies = self.additional_assemblies - else: - self._definition.additional_assemblies = {} - - self._definition.configuration = self.configuration - self._definition.lock_mode = self.lock_mode - self._definition.priority = self.priority - self._definition.state = self.state - self._definition.deployment_mode = self.deployment_mode - return self._definition - - -class AbstractMultiMapIndexCreationTask(AbstractGenericIndexCreationTask[IndexDefinition]): - def __init__(self): - super().__init__() - self.__maps: List[str] = [] - - def _add_map(self, map: str) -> None: - if map is None: - raise ValueError("Map cannot be None") - - self.__maps.append(map) - - def create_index_definition(self): - if self.conventions is None: - self.conventions = DocumentConventions() - - index_definition_builder = IndexDefinitionBuilder(self.index_name) - index_definition_builder.indexes_strings = self._indexes_strings - index_definition_builder.analyzers_strings = self._analyzers_strings - index_definition_builder.reduce = self._reduce - index_definition_builder.stores_strings = self._stores_strings - index_definition_builder.suggestions_options = self._index_suggestions - index_definition_builder.term_vectors_strings = self._term_vectors_strings - index_definition_builder.spatial_indexes_strings = self._spatial_options_strings - index_definition_builder.output_reduce_to_collection = self._output_reduce_to_collection - index_definition_builder.pattern_for_output_reduce_to_collection_references = ( - self._pattern_for_output_reduce_to_collection_references - ) - index_definition_builder.pattern_references_collection_name = self._pattern_references_collection_name - index_definition_builder.additional_sources = self.additional_sources - index_definition_builder.additional_assemblies = self.additional_assemblies - index_definition_builder.configuration = self.configuration - index_definition_builder.lock_mode = self.lock_mode - index_definition_builder.priority = self.priority - index_definition_builder.state = self.state - index_definition_builder.deployment_mode = self.deployment_mode - - index_definition = index_definition_builder.to_index_definition(self.conventions, False) - index_definition.maps = set(self.__maps) - - return index_definition +if TYPE_CHECKING: + from ravendb.documents.store.definition import DocumentStore + from ravendb.documents.conventions import DocumentConventions + from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractIndexCreationTask class IndexCreation: @staticmethod def create_indexes( - indexes: Collection[_T_AbstractIndexCreationTask], - store: DocumentStore, - conventions: Optional[DocumentConventions] = None, + indexes: Collection["AbstractIndexCreationTask"], + store: "DocumentStore", + conventions: Optional["DocumentConventions"] = None, ) -> None: if conventions is None: conventions = store.conventions try: indexes_to_add = IndexCreation.create_indexes_to_add(indexes, conventions) - store.maintenance.send(PutIndexesOperation(indexes_to_add)) + store.maintenance.send(PutIndexesOperation(*indexes_to_add)) except Exception as e: logging.info("Could not create indexes in one shot (maybe using older version of RavenDB ?)", exc_info=e) for index in indexes: @@ -455,9 +30,9 @@ def create_indexes( @staticmethod def create_indexes_to_add( - index_creation_tasks: Collection[_T_AbstractIndexCreationTask], conventions: DocumentConventions + index_creation_tasks: "AbstractIndexCreationTask", conventions: "DocumentConventions" ) -> List[IndexDefinition]: - def __map(x: _T_AbstractIndexCreationTask): + def __map(x: "AbstractIndexCreationTask"): old_conventions = x.conventions try: x.conventions = conventions diff --git a/ravendb/documents/indexes/time_series.py b/ravendb/documents/indexes/time_series.py index 97e7dc81..f3ed65fb 100644 --- a/ravendb/documents/indexes/time_series.py +++ b/ravendb/documents/indexes/time_series.py @@ -14,7 +14,10 @@ IndexFieldOptions, IndexType, ) -from ravendb.documents.indexes.index_creation import AbstractIndexCreationTaskBase, AbstractIndexDefinitionBuilder +from ravendb.documents.indexes.abstract_index_creation_tasks import ( + AbstractIndexCreationTaskBase, + AbstractIndexDefinitionBuilder, +) from ravendb.documents.indexes.spatial.configuration import SpatialOptions, SpatialOptionsFactory from ravendb.primitives import constants diff --git a/ravendb/documents/operations/executor.py b/ravendb/documents/operations/executor.py index 6065bb86..ca5b5408 100644 --- a/ravendb/documents/operations/executor.py +++ b/ravendb/documents/operations/executor.py @@ -1,7 +1,10 @@ from __future__ import annotations +import http from typing import Union, Optional, TYPE_CHECKING, TypeVar +from ravendb.documents.session.entity_to_json import EntityToJson +from ravendb.documents.operations.patch import PatchOperation, PatchStatus from ravendb.documents.operations.definitions import ( IOperation, OperationIdResult, @@ -26,50 +29,79 @@ class OperationExecutor: - def __init__(self, store: DocumentStore, database_name: str = None): - self.__store = store - self.__database_name = database_name if database_name else store.database - if not self.__database_name.isspace(): - self.__request_executor = store.get_request_executor(self.__database_name) - else: + def __init__(self, store: "DocumentStore", database_name: str = None): + self._store = store + self._database_name = database_name if database_name else store.database + if self._database_name.isspace(): raise ValueError("Cannot use operations without a database defined, did you forget to call 'for_database'?") + self._request_executor = store.get_request_executor(self._database_name) def for_database(self, database_name: str) -> OperationExecutor: - if self.__database_name.lower() == database_name.lower(): + if self._database_name.lower() == database_name.lower(): return self - return OperationExecutor(self.__store, database_name) + return OperationExecutor(self._store, database_name) def send(self, operation: IOperation[_Operation_T], session_info: SessionInfo = None) -> _Operation_T: - command = operation.get_command( - self.__store, self.__request_executor.conventions, self.__request_executor.cache - ) - self.__request_executor.execute_command(command, session_info) + command = operation.get_command(self._store, self._request_executor.conventions, self._request_executor.cache) + self._request_executor.execute_command(command, session_info) return None if isinstance(operation, VoidOperation) else command.result def send_async(self, operation: IOperation[OperationIdResult]) -> Operation: - command = operation.get_command( - self.__store, self.__request_executor.conventions, self.__request_executor.cache - ) - self.__request_executor.execute_command(command) + command = operation.get_command(self._store, self._request_executor.conventions, self._request_executor.cache) + self._request_executor.execute_command(command) node = command.selected_node_tag if command.selected_node_tag else command.result.operation_node_tag return Operation( - self.__request_executor, + self._request_executor, lambda: None, - self.__request_executor.conventions, + self._request_executor.conventions, command.result.operation_id, node, ) - # todo: send patch operations - create send_patch method - # or - # refactor 'send' methods above to act different while taking different sets of args - # (see jvmravendb OperationExecutor.java line 83-EOF) + def send_patch_operation(self, operation: PatchOperation, session_info: SessionInfo) -> PatchStatus: + command = operation.get_command(self._store, self._request_executor.conventions, self._request_executor.cache) + + self._request_executor.execute_command(command, session_info) + + if command.status_code == http.HTTPStatus.NOT_MODIFIED: + return PatchStatus.NOT_MODIFIED + + if command.status_code == http.HTTPStatus.NOT_FOUND: + return PatchStatus.DOCUMENT_DOES_NOT_EXIST + + return command.result.status + + def send_patch_operation_with_entity_class( + self, entity_class: _T, operation: PatchOperation, session_info: Optional[SessionInfo] = None + ) -> PatchOperation.Result[_T]: + command = operation.get_command(self._store, self._request_executor.conventions, self._request_executor.cache) + + self._request_executor.execute_command(command, session_info) + + result = PatchOperation.Result() + + if command.status_code == http.HTTPStatus.NOT_MODIFIED: + result.status = PatchStatus.NOT_MODIFIED + return result + + if command.status_code == http.HTTPStatus.NOT_FOUND: + result.status = PatchStatus.DOCUMENT_DOES_NOT_EXIST + return result + + try: + result.status = command.result.status + result.document = EntityToJson.convert_to_entity_static( + command.result.modified_document, entity_class, self._request_executor.conventions + ) + return result + except Exception as e: + raise RuntimeError(f"Unable to read patch result: {e.args[0]}", e) class SessionOperationExecutor(OperationExecutor): def __init__(self, session: InMemoryDocumentSessionOperations): super().__init__(session._document_store, session.database_name) - self.__session = session + self._session = session def for_database(self, database_name: str) -> OperationExecutor: raise RuntimeError("The method is not supported") @@ -77,59 +109,58 @@ def for_database(self, database_name: str) -> OperationExecutor: class MaintenanceOperationExecutor: def __init__(self, store: DocumentStore, database_name: Optional[str] = None): - self.__store = store - self.__database_name = database_name or store.database - self.__request_executor: Union[None, RequestExecutor] = None - self.__server_operation_executor: Union[ServerOperationExecutor, None] = None + self._store = store + self._database_name = database_name or store.database + self._request_executor: Union[None, RequestExecutor] = None + self._server_operation_executor: Union[ServerOperationExecutor, None] = None - def __get_request_executor(self) -> RequestExecutor: - if self.__request_executor is not None: - return self.__request_executor + @property + def request_executor(self) -> RequestExecutor: + if self._request_executor is not None: + return self._request_executor - self.__request_executor = ( - self.__store.get_request_executor(self.__database_name) if self.__database_name else None - ) - return self.__request_executor + self._request_executor = self._store.get_request_executor(self._database_name) if self._database_name else None + return self._request_executor @property def server(self) -> ServerOperationExecutor: - if self.__server_operation_executor is not None: - return self.__server_operation_executor - self.__server_operation_executor = ServerOperationExecutor(self.__store) - return self.__server_operation_executor + if self._server_operation_executor is not None: + return self._server_operation_executor + self._server_operation_executor = ServerOperationExecutor(self._store) + return self._server_operation_executor def for_database(self, database_name: str) -> MaintenanceOperationExecutor: - if database_name is not None and self.__database_name.lower() == database_name.lower(): + if database_name is not None and self._database_name.lower() == database_name.lower(): return self - return MaintenanceOperationExecutor(self.__store, database_name) + return MaintenanceOperationExecutor(self._store, database_name) def send( self, operation: Union[VoidMaintenanceOperation, MaintenanceOperation[_Operation_T]] ) -> Optional[_Operation_T]: - self.__assert_database_name_set() - command = operation.get_command(self.__get_request_executor().conventions) - self.__get_request_executor().execute_command(command) + self._assert_database_name_set() + command = operation.get_command(self.request_executor.conventions) + self.request_executor.execute_command(command) return None if isinstance(operation, VoidMaintenanceOperation) else command.result def send_async(self, operation: MaintenanceOperation[OperationIdResult]) -> Operation: - self.__assert_database_name_set() - command = operation.get_command(self.__get_request_executor().conventions) + self._assert_database_name_set() + command = operation.get_command(self.request_executor.conventions) - self.__get_request_executor().execute_command(command) + self.request_executor.execute_command(command) node = command.selected_node_tag if command.selected_node_tag else command.result.operation_node_tag return Operation( - self.__get_request_executor(), + self.request_executor, # todo : changes # lambda: self.__store.changes(self.__database_name, node), lambda: None, - self.__get_request_executor().conventions, + self.request_executor.conventions, command.result.operation_id, node, ) - def __assert_database_name_set(self) -> None: - if self.__database_name is None: + def _assert_database_name_set(self) -> None: + if self._database_name is None: raise ValueError( "Cannot use maintenance without a database defined, did you forget to call 'for_database'?" ) diff --git a/ravendb/documents/operations/expiration/configuration.py b/ravendb/documents/operations/expiration/configuration.py index 10f6af37..ee22e36b 100644 --- a/ravendb/documents/operations/expiration/configuration.py +++ b/ravendb/documents/operations/expiration/configuration.py @@ -1,7 +1,15 @@ -from typing import Optional +from __future__ import annotations +from typing import Optional, Dict, Any class ExpirationConfiguration: def __init__(self, disabled: Optional[bool] = None, delete_frequency_in_sec: Optional[int] = None): self.disabled = disabled self.delete_frequency_in_sec = delete_frequency_in_sec + + def to_json(self) -> Dict[str, Any]: + return {"Disabled": self.disabled, "DeleteFrequencyInSec": self.delete_frequency_in_sec} + + @classmethod + def from_json(cls, json_dict) -> ExpirationConfiguration: + return cls(json_dict["Disabled"], json_dict["DeleteFrequencyInSec"]) diff --git a/ravendb/documents/operations/expiration/operations.py b/ravendb/documents/operations/expiration/operations.py new file mode 100644 index 00000000..912ff8bc --- /dev/null +++ b/ravendb/documents/operations/expiration/operations.py @@ -0,0 +1,52 @@ +from __future__ import annotations +import json +from typing import Optional, Dict, Any + +import requests + +from ravendb import ServerNode +from ravendb.http.raven_command import RavenCommand +from ravendb.documents.conventions import DocumentConventions +from ravendb.documents.operations.expiration.configuration import ExpirationConfiguration +from ravendb.documents.operations.definitions import MaintenanceOperation + + +class ConfigureExpirationOperationResult: + def __init__(self, raft_command_index: int = None): + self.raft_command_index = raft_command_index + + @classmethod + def from_json(cls, json_dict: Dict[str, Any]) -> ConfigureExpirationOperationResult: + return cls(json_dict["RaftCommandIndex"]) + + +class ConfigureExpirationOperation(MaintenanceOperation[ConfigureExpirationOperationResult]): + def __init__(self, configuration: ExpirationConfiguration): + self._configuration = configuration + + def get_command(self, conventions: DocumentConventions) -> RavenCommand: + return self.ConfigureExpirationCommand(self._configuration) + + class ConfigureExpirationCommand(RavenCommand[ConfigureExpirationOperationResult]): + def __init__(self, configuration: ExpirationConfiguration): + super().__init__(ConfigureExpirationOperationResult) + if configuration is None: + raise ValueError("Configuration cannot be None") + self._configuration = configuration + + def is_read_request(self) -> bool: + return False + + def create_request(self, node: ServerNode) -> requests.Request: + url = f"{node.url}/databases/{node.database}/admin/expiration/config" + + request = requests.Request("POST", url) + request.data = self._configuration.to_json() + + return request + + def set_response(self, response: Optional[str], from_cache: bool) -> None: + if response is None: + self._throw_invalid_response() + + self.result = ConfigureExpirationOperationResult.from_json(json.loads(response)) diff --git a/ravendb/documents/operations/identities.py b/ravendb/documents/operations/identities.py new file mode 100644 index 00000000..a8a8826e --- /dev/null +++ b/ravendb/documents/operations/identities.py @@ -0,0 +1,118 @@ +import json +from typing import Optional, Union, TypeVar + +import requests + +from ravendb.documents.conventions import DocumentConventions +from ravendb.http.topology import RaftCommand +from ravendb.http.server_node import ServerNode +from ravendb.http.misc import Broadcast +from ravendb.http.raven_command import RavenCommand +from ravendb.documents.operations.definitions import MaintenanceOperation +from ravendb.util.util import RaftIdGenerator + +_T_Result = TypeVar("_T_Result") + + +class NextIdentityForCommand(RavenCommand, RaftCommand, Broadcast): + def __init__(self, _id: str): + super().__init__(int) + if _id is None: + raise ValueError("Id cannot be None") + + self._id = _id + self._raft_unique_request_id = RaftIdGenerator.new_id() + + @classmethod + def from_copy(cls, copy: RavenCommand[_T_Result]) -> Union[Broadcast, RavenCommand[_T_Result]]: + copied = super().from_copy(copy) + + copied._raft_unique_request_id = copy._raft_unique_request_id + copied._id = copy._id + return copied + + def is_read_request(self) -> bool: + return False + + def create_request(self, node: ServerNode) -> requests.Request: + self.ensure_is_not_null_or_string(self._id, "id") + + return requests.Request("POST", f"{node.url}/databases/{node.database}/identity/next?name={self._id}") + + def set_response(self, response: Optional[str], from_cache: bool) -> None: + if response is None: + self._throw_invalid_response() + + json_node = json.loads(response) + if "NewIdentityValue" not in json_node: + self._throw_invalid_response() + + self.result = json_node["NewIdentityValue"] + + def get_raft_unique_request_id(self) -> str: + return self._raft_unique_request_id + + def prepare_to_broadcast(self, conventions: DocumentConventions) -> Broadcast: + return NextIdentityForCommand.from_copy(self) + + +class NextIdentityForOperation(MaintenanceOperation[int]): + def __init__(self, name: str): + if not name or name.isspace(): + raise ValueError("The field name cannot be None or whitespace.") + self._identity_name = name + + def get_command(self, conventions: "DocumentConventions") -> "RavenCommand[int]": + return NextIdentityForCommand(self._identity_name) + + +class SeedIdentityForCommand(RavenCommand[int], RaftCommand): + def __init__(self, id_: str, value: int = None, forced: bool = False): + super().__init__(int) + if id_ is None: + raise ValueError("Id cannot be None") + + self._id = id_ + self._value = value + self._forced = forced + + def is_read_request(self) -> bool: + return False + + def create_request(self, node: ServerNode) -> requests.Request: + self.ensure_is_not_null_or_string(self._id, "id") + + url = f"{node.url}/databases/{node.database}/identity/seed?name={self._id}&value={self._value}" + + if self._forced: + url += "&force=true" + + return requests.Request("POST", url) + + def set_response(self, response: Optional[str], from_cache: bool) -> None: + if response is None: + self._throw_invalid_response() + + json_node = json.loads(response) + + if "NewSeedValue" not in json_node: + self._throw_invalid_response() + + self.result = json_node["NewSeedValue"] + + def get_raft_unique_request_id(self) -> str: + return RaftIdGenerator.new_id() + + +class SeedIdentityForOperation(MaintenanceOperation[int]): + def __init__(self, name: str, value: int, force_update: bool = False): + super().__init__() + if not name or name.isspace(): + raise ValueError("The field name cannot be None or whitespace") + + self._identity_name = name + self._identity_value = value + self._force_update = force_update + + def get_command(self, conventions: "DocumentConventions") -> "RavenCommand[_T]": + return SeedIdentityForCommand(self._identity_name, self._identity_value, self._force_update) diff --git a/ravendb/documents/operations/indexes.py b/ravendb/documents/operations/indexes.py index 4392f27c..1749f43e 100644 --- a/ravendb/documents/operations/indexes.py +++ b/ravendb/documents/operations/indexes.py @@ -1,7 +1,7 @@ from __future__ import annotations import enum import json -from typing import List, TYPE_CHECKING, Optional +from typing import List, TYPE_CHECKING, Optional, Tuple import requests @@ -20,7 +20,7 @@ class PutIndexesOperation(MaintenanceOperation): - def __init__(self, *indexes_to_add): + def __init__(self, *indexes_to_add: IndexDefinition): if len(indexes_to_add) == 0: raise ValueError("Invalid indexes_to_add") @@ -29,10 +29,10 @@ def __init__(self, *indexes_to_add): self.__all_java_script_indexes = True # todo: set it in the command def get_command(self, conventions: "DocumentConventions") -> RavenCommand[List]: - return self.__PutIndexesCommand(conventions, *self._indexes_to_add) + return self.__PutIndexesCommand(conventions, self._indexes_to_add) class __PutIndexesCommand(RavenCommand[List], RaftCommand): - def __init__(self, conventions: "DocumentConventions", *index_to_add): + def __init__(self, conventions: "DocumentConventions", index_to_add: Tuple[IndexDefinition]): super().__init__(list) if conventions is None: raise ValueError("Conventions cannot be None") @@ -635,3 +635,25 @@ def set_response(self, response: str, from_cache: bool) -> None: raise ValueError("Response is invalid") self.result = json.loads(response)["Changed"] + + +class ResetIndexOperation(VoidMaintenanceOperation): + def __init__(self, index_name: str): + if index_name is None: + raise ValueError("Index name cannot be None") + + self._index_name = index_name + + def get_command(self, conventions: "DocumentConventions") -> "VoidRavenCommand": + return ResetIndexOperation.ResetIndexCommand(self._index_name) + + class ResetIndexCommand(VoidRavenCommand): + def __init__(self, index_name: str): + super().__init__() + if index_name is None: + raise ValueError("Index name cannot be None") + + self._index_name = index_name + + def create_request(self, node: ServerNode) -> requests.Request: + return requests.Request("RESET", f"{node.url}/databases/{node.database}/indexes?name={self._index_name}") diff --git a/ravendb/documents/operations/patch.py b/ravendb/documents/operations/patch.py index 54a1171e..4827ed46 100644 --- a/ravendb/documents/operations/patch.py +++ b/ravendb/documents/operations/patch.py @@ -71,28 +71,28 @@ def from_json(cls, json_dict: dict) -> PatchResult: json_dict.get("ModifiedDocument", None), json_dict.get("OriginalDocument", None), json_dict.get("Debug", None), - Utils.string_to_datetime(json_dict["LastModified"]), - json_dict["ChangeVector"], - json_dict["Collection"], + Utils.string_to_datetime(json_dict["LastModified"]) if "LastModified" in json_dict else None, + json_dict.get("ChangeVector", None), + json_dict.get("Collection", None), ) class PatchOperation(IOperation[PatchResult]): class Payload: def __init__(self, patch: PatchRequest, patch_if_missing: PatchResult): - self.__patch = patch - self.__patch_if_missing = patch_if_missing + self._patch = patch + self._patch_if_missing = patch_if_missing @property def patch(self): - return self.__patch + return self._patch @property def patch_if_missing(self): - return self.__patch_if_missing + return self._patch_if_missing class Result(Generic[_Operation_T]): - def __init__(self, status: PatchStatus, document: _Operation_T): + def __init__(self, status: PatchStatus = None, document: _Operation_T = None): self.status = status self.document = document @@ -113,11 +113,11 @@ def __init__( if patch_if_missing and patch_if_missing.script.isspace(): raise ValueError("PatchIfMissing script cannot be None or whitespace") - self.__key = key - self.__change_vector = change_vector - self.__patch = patch - self.__patch_if_missing = patch_if_missing - self.__skip_patch_if_change_vector_mismatch = skip_patch_if_change_vector_mismatch + self._key = key + self._change_vector = change_vector + self._patch = patch + self._patch_if_missing = patch_if_missing + self._skip_patch_if_change_vector_mismatch = skip_patch_if_change_vector_mismatch def get_command( self, @@ -129,11 +129,11 @@ def get_command( ) -> RavenCommand[_Operation_T]: return self.PatchCommand( conventions, - self.__key, - self.__change_vector, - self.__patch, - self.__patch_if_missing, - self.__skip_patch_if_change_vector_mismatch, + self._key, + self._change_vector, + self._patch, + self._patch_if_missing, + self._skip_patch_if_change_vector_mismatch, return_debug_information, test, ) @@ -164,31 +164,31 @@ def __init__( raise ValueError("Document_id canoot be None") super().__init__(PatchResult) - self.__conventions = conventions - self.__document_id = document_id - self.__change_vector = change_vector - self.__patch = patch - self.__patch_if_missing = patch_if_missing - self.__skip_patch_if_change_vector_mismatch = skip_patch_if_change_vector_mismatch - self.__return_debug_information = return_debug_information - self.__test = test + self._conventions = conventions + self._document_id = document_id + self._change_vector = change_vector + self._patch = patch + self._patch_if_missing = patch_if_missing + self._skip_patch_if_change_vector_mismatch = skip_patch_if_change_vector_mismatch + self._return_debug_information = return_debug_information + self._test = test def create_request(self, server_node: ServerNode) -> requests.Request: - path = f"docs?id={Utils.quote_key(self.__document_id)}" - if self.__skip_patch_if_change_vector_mismatch: + path = f"docs?id={Utils.quote_key(self._document_id)}" + if self._skip_patch_if_change_vector_mismatch: path += "&skipPatchIfChangeVectorMismatch=true" - if self.__return_debug_information: + if self._return_debug_information: path += "&debug=true" - if self.__test: + if self._test: path += "&test=true" url = f"{server_node.url}/databases/{server_node.database}/{path}" request = requests.Request("PATCH", url) - if self.__change_vector is not None: - request.headers = {"If-Match": f'"{self.__change_vector}"'} + if self._change_vector is not None: + request.headers = {"If-Match": f'"{self._change_vector}"'} request.data = { - "Patch": self.__patch.serialize(), - "PatchIfMissing": self.__patch_if_missing.serialize() if self.__patch_if_missing else None, + "Patch": self._patch.serialize(), + "PatchIfMissing": self._patch_if_missing.serialize() if self._patch_if_missing else None, } return request @@ -210,39 +210,39 @@ def __init__(self, query_to_update: Union[IndexQuery, str], options: Optional[Qu super().__init__() if isinstance(query_to_update, str): query_to_update = IndexQuery(query=query_to_update) - self.__query_to_update = query_to_update + self._query_to_update = query_to_update if options is None: options = QueryOperationOptions() - self.__options = options + self._options = options def get_command( self, store: "DocumentStore", conventions: "DocumentConventions", cache: Optional[HttpCache] = None ) -> RavenCommand[OperationIdResult]: - return self.__PatchByQueryCommand(conventions, self.__query_to_update, self.__options) + return self.__PatchByQueryCommand(conventions, self._query_to_update, self._options) class __PatchByQueryCommand(RavenCommand[OperationIdResult]): def __init__( self, conventions: "DocumentConventions", query_to_update: IndexQuery, options: QueryOperationOptions ): super().__init__(OperationIdResult) - self.__conventions = conventions - self.__query_to_update = query_to_update - self.__options = options + self._conventions = conventions + self._query_to_update = query_to_update + self._options = options def create_request(self, server_node) -> requests.Request: - if not isinstance(self.__query_to_update, IndexQuery): + if not isinstance(self._query_to_update, IndexQuery): raise ValueError("query must be IndexQuery Type") url = server_node.url + "/databases/" + server_node.database + "/queries" path = ( - f"?allowStale={self.__options.allow_stale}&maxOpsPerSec={self.__options.max_ops_per_sec or ''}" - f"&details={self.__options.retrieve_details}" + f"?allowStale={self._options.allow_stale}&maxOpsPerSec={self._options.max_ops_per_sec or ''}" + f"&details={self._options.retrieve_details}" ) - if self.__options.stale_timeout is not None: - path += "&staleTimeout=" + str(self.__options.stale_timeout) + if self._options.stale_timeout is not None: + path += "&staleTimeout=" + str(self._options.stale_timeout) url += path - data = {"Query": self.__query_to_update.to_json()} + data = {"Query": self._query_to_update.to_json()} return requests.Request("PATCH", url, data=data) def set_response(self, response: str, from_cache: bool) -> None: diff --git a/ravendb/documents/operations/statistics.py b/ravendb/documents/operations/statistics.py index c4824b71..baa219a5 100644 --- a/ravendb/documents/operations/statistics.py +++ b/ravendb/documents/operations/statistics.py @@ -6,7 +6,7 @@ import requests -from ravendb.documents.indexes.definitions import IndexPriority, IndexLockMode, IndexType, IndexSourceType +from ravendb.documents.indexes.definitions import IndexPriority, IndexLockMode, IndexType, IndexSourceType, IndexState from ravendb.documents.operations.definitions import MaintenanceOperation from ravendb.http.raven_command import RavenCommand from ravendb.http.server_node import ServerNode @@ -16,16 +16,96 @@ from ravendb.documents.conventions import DocumentConventions -class GetStatisticsOperation(MaintenanceOperation[dict]): +class DatabaseStatistics: + def __init__( + self, + last_doc_etag: int = None, + last_database_etag: int = None, + count_of_indexes: int = None, + count_of_documents: int = None, + count_of_revision_documents: int = None, + count_of_documents_conflicts: int = None, + count_of_tombstones: int = None, + count_of_conflicts: int = None, + count_of_attachments: int = None, + count_of_unique_attachments: int = None, + count_of_counter_entries: int = None, + count_of_time_series_segments: int = None, + indexes: List[IndexInformation] = None, + database_change_vector: str = None, + database_id: str = None, + is_64_bit: bool = None, + pager: str = None, + last_indexing_time: datetime.datetime = None, + size_on_disk: Size = None, + temp_buffers_size_on_disk: Size = None, + number_of_transaction_merger_queue_operations: int = None, + ): + self.last_doc_etag = last_doc_etag + self.last_database_etag = last_database_etag + self.count_of_indexes = count_of_indexes + self.count_of_documents = count_of_documents + self.count_of_revision_documents = count_of_revision_documents + self.count_of_documents_conflicts = count_of_documents_conflicts + self.count_of_tombstones = count_of_tombstones + self.count_of_conflicts = count_of_conflicts + self.count_of_attachments = count_of_attachments + self.count_of_unique_attachments = count_of_unique_attachments + self.count_of_counter_entries = count_of_counter_entries + self.count_of_time_series_segments = count_of_time_series_segments + + self.indexes = indexes + + self.database_change_vector = database_change_vector + self.database_id = database_id + self.is_64_bit = is_64_bit + self.pager = pager + self.last_indexing_time = last_indexing_time + self.size_on_disk = size_on_disk + self.temp_buffers_size_on_disk = temp_buffers_size_on_disk + self.number_of_transaction_merger_queue_operations = number_of_transaction_merger_queue_operations + + @property + def stale_indexes(self) -> List[IndexInformation]: + return list(filter(lambda x: x.stale, self.indexes)) + + @classmethod + def from_json(cls, json_dict) -> DatabaseStatistics: + return cls( + json_dict.get("LastDocEtag", None), + json_dict.get("LastDatabaseEtag", None), + json_dict.get("CountOfIndexes", None), + json_dict.get("CountOfDocuments", None), + json_dict.get("CountOfRevisionDocuments", None), + json_dict.get("CountOfDocumentsConflicts", None), + json_dict.get("CountOfTombstones", None), + json_dict.get("CountOfConflicts", None), + json_dict.get("CountOfAttachments", None), + json_dict.get("CountOfUniqueAttachments", None), + json_dict.get("CountOfCounterEntries", None), + json_dict.get("CountOfTimeSeriesSegments", None), + [IndexInformation.from_json(x) for x in json_dict["Indexes"]] if "Indexes" in json_dict else None, + json_dict.get("DatabaseChangeVector", None), + json_dict.get("DatabaseId", None), + json_dict.get("Is64Bit", None), + json_dict.get("Pager", None), + Utils.string_to_datetime(json_dict["LastIndexingTime"]) if json_dict.get("LastIndexingTime") else None, + Size.from_json(json_dict.get("SizeOnDisk", None)), + Size.from_json(json_dict.get("TempBuffersSizeOnDisk", None)), + json_dict.get("NumberOfTransactionMergerQueueOperations", None), + ) + + +class GetStatisticsOperation(MaintenanceOperation[DatabaseStatistics]): def __init__(self, debug_tag: str = None, node_tag: str = None): self.debug_tag = debug_tag self.node_tag = node_tag - def get_command(self, conventions: "DocumentConventions") -> RavenCommand[dict]: - return self.__GetStatisticsCommand(self.debug_tag, self.node_tag) + def get_command(self, conventions: "DocumentConventions") -> _GetStatisticsCommand[DatabaseStatistics]: + return self._GetStatisticsCommand(self.debug_tag, self.node_tag) - class __GetStatisticsCommand(RavenCommand[dict]): - def __init__(self, debug_tag: str, node_tag: str): + class _GetStatisticsCommand(RavenCommand[DatabaseStatistics]): + def __init__(self, debug_tag: Optional[str] = None, node_tag: Optional[str] = None): super().__init__(dict) self.debug_tag = debug_tag self.node_tag = node_tag @@ -38,7 +118,7 @@ def create_request(self, node: ServerNode) -> requests.Request: ) def set_response(self, response: str, from_cache: bool) -> None: - self.result = json.loads(response) + self.result = DatabaseStatistics.from_json(json.loads(response)) def is_read_request(self) -> bool: return True @@ -89,6 +169,8 @@ def __init__( index_type: IndexType = None, last_indexing_time: datetime.datetime = None, source_type: IndexSourceType = None, + index_state: IndexState = None, + name: str = None, ): self.stale = stale self.lock_mode = lock_mode @@ -96,96 +178,20 @@ def __init__( self.type = index_type self.last_indexing_time: datetime.datetime = last_indexing_time self.source_type = source_type + self.state = index_state + self.name = name @classmethod def from_json(cls, json_dict) -> IndexInformation: return cls( - json_dict["Stale"], + json_dict["IsStale"] if "IsStale" in json_dict else None, IndexLockMode(json_dict["LockMode"]), - IndexPriority(json_dict["IndexPriority"]), - IndexType(json_dict["IndexType"]), - Utils.string_to_datetime(json_dict["LastIndexingTime"]), - IndexSourceType(json_dict["SourceType"]), - ) - - -class DatabaseStatistics: - def __init__( - self, - last_doc_etag: int = None, - last_database_etag: int = None, - count_of_indexes: int = None, - count_of_documents: int = None, - count_of_revision_documents: int = None, - count_of_documents_conflicts: int = None, - count_of_tombstones: int = None, - count_of_conflicts: int = None, - count_of_attachments: int = None, - count_of_unique_attachments: int = None, - count_of_counter_entries: int = None, - count_of_time_series_segments: int = None, - indexes: List[IndexInformation] = None, - database_change_vector: str = None, - database_id: str = None, - is_64_bit: bool = None, - pager: str = None, - last_indexing_time: datetime.datetime = None, - size_on_disk: Size = None, - temp_buffers_size_on_disk: Size = None, - number_of_transaction_merger_queue_operations: int = None, - ): - self.last_doc_etag = last_doc_etag - self.last_database_etag = last_database_etag - self.count_of_indexes = count_of_indexes - self.count_of_documents = count_of_documents - self.count_of_revision_documents = count_of_revision_documents - self.count_of_documents_conflicts = count_of_documents_conflicts - self.count_of_tombstones = count_of_tombstones - self.count_of_conflicts = count_of_conflicts - self.count_of_attachments = count_of_attachments - self.count_of_unique_attachments = count_of_unique_attachments - self.count_of_counter_entries = count_of_counter_entries - self.count_of_time_series_segments = count_of_time_series_segments - - self.indexes = indexes - - self.database_change_vector = database_change_vector - self.database_id = database_id - self.is_64_bit = is_64_bit - self.pager = pager - self.last_indexing_time = last_indexing_time - self.size_on_disk = size_on_disk - self.temp_buffers_size_on_disk = temp_buffers_size_on_disk - self.number_of_transaction_merger_queue_operations = number_of_transaction_merger_queue_operations - - @property - def stale_indexes(self) -> List[IndexInformation]: - return list(filter(lambda x: x.stale, self.indexes)) - - @classmethod - def from_json(cls, json_dict) -> DatabaseStatistics: - return cls( - json_dict.get("LastDocEtag", None), - json_dict.get("LastDatabaseEtag", None), - json_dict.get("CountOfIndexes", None), - json_dict.get("CountOfDocuments", None), - json_dict.get("CountOfRevisionDocuments", None), - json_dict.get("CountOfDocumentsConflicts", None), - json_dict.get("CountOfTombstones", None), - json_dict.get("CountOfConflicts", None), - json_dict.get("CountOfAttachments", None), - json_dict.get("CountOfUniqueAttachments", None), - json_dict.get("CountOfCounterEntries", None), - json_dict.get("CountOfTimeSeriesSegments", None), - list(map(lambda x: IndexInformation.from_json(x), json_dict.get("Indexes", None))), - json_dict.get("DatabaseChangeVector", None), - json_dict.get("DatabaseId", None), - json_dict.get("Is64Bit", None), - json_dict.get("Pager", None), - Utils.string_to_datetime(json_dict["LastIndexingTime"]) if json_dict.get("LastIndexingTime") else None, - Size.from_json(json_dict.get("SizeOnDisk", None)), - Size.from_json(json_dict.get("TempBuffersSizeOnDisk", None)), - json_dict.get("NumberOfTransactionMergerQueueOperations", None), + IndexPriority(json_dict["Priority"]) if "Priority" in json_dict else None, + IndexType(json_dict["Type"]) if "Type" in json_dict else None, + Utils.string_to_datetime(json_dict["LastIndexingTime"]) if "LastIndexingTime" in json_dict else None, + IndexSourceType(json_dict["SourceType"]) if "SourceType" in json_dict else None, + IndexState(json_dict["State"]) if "State" in json_dict else None, + json_dict["Name"] if "Name" in json_dict else None, ) diff --git a/ravendb/documents/session/document_session.py b/ravendb/documents/session/document_session.py index db80dd5a..b63bc10a 100644 --- a/ravendb/documents/session/document_session.py +++ b/ravendb/documents/session/document_session.py @@ -588,6 +588,14 @@ def transaction_mode(self) -> TransactionMode: def transaction_mode(self, value: TransactionMode): self._session.transaction_mode = value + @property + def use_optimistic_concurrency(self) -> bool: + return self._session._use_optimistic_concurrency + + @use_optimistic_concurrency.setter + def use_optimistic_concurrency(self, value: bool): + self._session._use_optimistic_concurrency = value + def is_loaded(self, key: str) -> bool: return self._session.is_loaded_or_deleted(key) @@ -919,6 +927,8 @@ def patch_object( script_map = JavaScriptMap(self.__custom_count, path_to_object) self.__custom_count += 1 + dictionary_adder(script_map) + patch_request = PatchRequest() patch_request.script = script_map.script patch_request.values = script_map.parameters diff --git a/ravendb/documents/session/document_session_operations/in_memory_document_session_operations.py b/ravendb/documents/session/document_session_operations/in_memory_document_session_operations.py index 1ed59127..90bbaa63 100644 --- a/ravendb/documents/session/document_session_operations/in_memory_document_session_operations.py +++ b/ravendb/documents/session/document_session_operations/in_memory_document_session_operations.py @@ -423,32 +423,30 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.close() def __init__(self, store: "DocumentStore", key: uuid.UUID, options: SessionOptions): - self.__id = key - self.__database_name = options.database if options.database else store.database if store.database else None - if not self.__database_name: + self._id = key + self._database_name = options.database if options.database else store.database if store.database else None + if not self._database_name: InMemoryDocumentSessionOperations.raise_no_database() self._request_executor = ( - options.request_executor if options.request_executor else store.get_request_executor(self.__database_name) + options.request_executor if options.request_executor else store.get_request_executor(self._database_name) ) self._operation_executor: Union[None, OperationExecutor] = None self._pending_lazy_operations: List[LazyOperation] = [] self._on_evaluate_lazy = {} - self.__no_tracking = options.no_tracking + self._no_tracking = options.no_tracking - self.__use_optimistic_concurrency = self._request_executor.conventions.use_optimistic_concurrency - self.__max_number_of_requests_per_session = ( - self._request_executor.conventions.max_number_of_requests_per_session - ) - self.__generate_entity_id_on_client = GenerateEntityIdOnTheClient( + self._use_optimistic_concurrency = self._request_executor.conventions.use_optimistic_concurrency + self._max_number_of_requests_per_session = self._request_executor.conventions.max_number_of_requests_per_session + self._generate_entity_id_on_client = GenerateEntityIdOnTheClient( self._request_executor.conventions, self._generate_id ) self.entity_to_json = EntityToJson(self) self._document_store: DocumentStore = store self.session_info = SessionInfo(self, options, self._document_store) - self.__save_changes_options = BatchOptions() + self._save_changes_options = BatchOptions() self.transaction_mode = options.transaction_mode self.disable_atomic_document_writes_in_cluster_wide_transaction = ( @@ -472,124 +470,124 @@ def __init__(self, store: "DocumentStore", key: uuid.UUID, options: SessionOptio # todo: pendingLazyOperations, onEvaluateLazy self._generate_document_keys_on_store: bool = True self._save_changes_options: Union[None, BatchOptions] = None - self.__hash: int = self.__instances_counter.__add__(1) - self.__is_disposed: Union[None, bool] = None + self._hash: int = self.__instances_counter.__add__(1) + self._is_disposed: Union[None, bool] = None - self.__number_of_requests: int = 0 - self.__max_number_of_requests_per_session: int = ( + self._number_of_requests: int = 0 + self._max_number_of_requests_per_session: int = ( self._request_executor.conventions.max_number_of_requests_per_session ) # --- EVENTS --- - self.__before_store: List[Callable[[BeforeStoreEventArgs], None]] = [] - self.__after_save_changes: List[Callable[[AfterSaveChangesEventArgs], None]] = [] - self.__before_delete: List[Callable[[BeforeDeleteEventArgs], None]] = [] - self.__before_query: List[Callable[[BeforeQueryEventArgs], None]] = [] - self.__before_conversion_to_document: List[Callable[[BeforeConversionToDocumentEventArgs], None]] = [] - self.__after_conversion_to_document: List[Callable[[AfterConversionToDocumentEventArgs], None]] = [] + self._before_store: List[Callable[[BeforeStoreEventArgs], None]] = [] + self._after_save_changes: List[Callable[[AfterSaveChangesEventArgs], None]] = [] + self._before_delete: List[Callable[[BeforeDeleteEventArgs], None]] = [] + self._before_query: List[Callable[[BeforeQueryEventArgs], None]] = [] + self._before_conversion_to_document: List[Callable[[BeforeConversionToDocumentEventArgs], None]] = [] + self._after_conversion_to_document: List[Callable[[AfterConversionToDocumentEventArgs], None]] = [] - self.__before_conversion_to_entity: List[Callable[[BeforeConversionToEntityEventArgs], None]] = [] + self._before_conversion_to_entity: List[Callable[[BeforeConversionToEntityEventArgs], None]] = [] - self.__after_conversion_to_entity: List[Callable[[AfterConversionToEntityEventArgs], None]] = [] + self._after_conversion_to_entity: List[Callable[[AfterConversionToEntityEventArgs], None]] = [] - self.__session_closing: List[Callable[[SessionClosingEventArgs], None]] = [] + self._session_closing: List[Callable[[SessionClosingEventArgs], None]] = [] def add_before_store(self, event: Callable[[BeforeStoreEventArgs], None]): - self.__before_store.append(event) + self._before_store.append(event) def remove_before_store(self, event: Callable[[BeforeStoreEventArgs], None]): - self.__before_store.remove(event) + self._before_store.remove(event) def add_session_closing(self, event: Callable[[SessionClosingEventArgs], None]): - self.__session_closing.append(event) + self._session_closing.append(event) def remove_session_closing(self, event: Callable[[SessionClosingEventArgs], None]): - self.__session_closing.remove(event) + self._session_closing.remove(event) def add_before_conversion_to_document(self, event: Callable[[BeforeConversionToDocumentEventArgs], None]): - self.__before_conversion_to_document.append(event) + self._before_conversion_to_document.append(event) def remove_before_conversion_to_document(self, event: Callable[[BeforeConversionToDocumentEventArgs], None]): - self.__before_conversion_to_document.remove(event) + self._before_conversion_to_document.remove(event) def add_after_conversion_to_document(self, event: Callable[[AfterConversionToDocumentEventArgs], None]): - self.__after_conversion_to_document.append(event) + self._after_conversion_to_document.append(event) def remove_after_conversion_to_document(self, event: Callable[[AfterConversionToDocumentEventArgs], None]): - self.__after_conversion_to_document.remove(event) + self._after_conversion_to_document.remove(event) def add_before_conversion_to_entity(self, event: Callable[[BeforeConversionToEntityEventArgs], None]): - self.__before_conversion_to_entity.append(event) + self._before_conversion_to_entity.append(event) def remove_before_conversion_to_entity(self, event: Callable[[BeforeConversionToEntityEventArgs], None]): - self.__before_conversion_to_entity.remove(event) + self._before_conversion_to_entity.remove(event) def add_after_conversion_to_entity(self, event: Callable[[AfterConversionToEntityEventArgs], None]): - self.__after_conversion_to_entity.append(event) + self._after_conversion_to_entity.append(event) def remove_after_conversion_to_entity(self, event: Callable[[AfterConversionToEntityEventArgs], None]): - self.__after_conversion_to_entity.remove(event) + self._after_conversion_to_entity.remove(event) def add_after_save_changes(self, event: Callable[[AfterSaveChangesEventArgs], None]): - self.__after_save_changes.append(event) + self._after_save_changes.append(event) def remove_after_save_changes(self, event: Callable[[AfterSaveChangesEventArgs], None]): - self.__after_save_changes.remove(event) + self._after_save_changes.remove(event) def add_before_delete(self, event: Callable[[BeforeDeleteEventArgs], None]): - self.__before_delete.append(event) + self._before_delete.append(event) def remove_before_delete_entity(self, event: Callable[[BeforeDeleteEventArgs], None]): - self.__before_delete.remove(event) + self._before_delete.remove(event) def add_before_query(self, event: Callable[[BeforeQueryEventArgs], None]): - self.__before_query.append(event) + self._before_query.append(event) def remove_before_query(self, event: Callable[[BeforeQueryEventArgs], None]): - self.__before_query.append(event) + self._before_query.append(event) def before_store_invoke(self, before_store_event_args: BeforeStoreEventArgs): - for event in self.__before_store: + for event in self._before_store: event(before_store_event_args) def session_closing_invoke(self, session_closing_event_args: SessionClosingEventArgs): - for event in self.__session_closing: + for event in self._session_closing: event(session_closing_event_args) def before_conversion_to_document_invoke( self, before_conversion_to_document_event_args: BeforeConversionToDocumentEventArgs ): - for event in self.__before_conversion_to_document: + for event in self._before_conversion_to_document: event(before_conversion_to_document_event_args) def after_conversion_to_document_invoke( self, after_conversion_to_document_event_args: AfterConversionToDocumentEventArgs ): - for event in self.__after_conversion_to_document: + for event in self._after_conversion_to_document: event(after_conversion_to_document_event_args) def before_conversion_to_entity_invoke( self, before_conversion_to_entity_event_args: BeforeConversionToEntityEventArgs ): - for event in self.__before_conversion_to_entity: + for event in self._before_conversion_to_entity: event(before_conversion_to_entity_event_args) def after_conversion_to_entity_invoke( self, after_conversion_to_entity_event_args: AfterConversionToEntityEventArgs ): - for event in self.__after_conversion_to_entity: + for event in self._after_conversion_to_entity: event(after_conversion_to_entity_event_args) def after_save_changes_invoke(self, after_save_changes_event_args: AfterSaveChangesEventArgs): - for event in self.__after_save_changes: + for event in self._after_save_changes: event(after_save_changes_event_args) def before_delete_invoke(self, before_delete_event_args: BeforeDeleteEventArgs): - for event in self.__before_delete: + for event in self._before_delete: event(before_delete_event_args) def before_query_invoke(self, before_query_event_args: BeforeQueryEventArgs): - for event in self.__before_query: + for event in self._before_query: event(before_query_event_args) @property @@ -622,11 +620,11 @@ def _store_identifier(self): @property def database_name(self): - return self.__database_name + return self._database_name @property def generate_entity_id_on_the_client(self) -> GenerateEntityIdOnTheClient: - return self.__generate_entity_id_on_client + return self._generate_entity_id_on_client @property def counters_by_doc_id(self): @@ -640,15 +638,15 @@ def time_series_by_doc_id(self) -> Dict[str, Dict[str, List[TimeSeriesRangeResul @property def number_of_requests(self) -> int: - return self.__number_of_requests + return self._number_of_requests @property def no_tracking(self) -> bool: - return self.__no_tracking + return self._no_tracking @no_tracking.setter def no_tracking(self, value: bool): - self.__no_tracking = value + self._no_tracking = value # def counters_for(self, entity_or_document_id): # """ @@ -673,7 +671,7 @@ def _get_document_info(self, entity: object) -> DocumentInfo: if document_info: return document_info - found, value = self.__generate_entity_id_on_client.try_get_id_from_instance(entity) + found, value = self._generate_entity_id_on_client.try_get_id_from_instance(entity) if not found: raise ValueError(f"Could not find the document id for {entity}") self._assert_no_non_unique_instance(entity, value) @@ -695,10 +693,10 @@ def is_deleted(self, key: str) -> bool: return key in self._known_missing_ids def increment_requests_count(self) -> None: - self.__number_of_requests += 1 - if self.__number_of_requests > self.__max_number_of_requests_per_session: + self._number_of_requests += 1 + if self._number_of_requests > self._max_number_of_requests_per_session: raise ValueError( - f"The maximum number of requests {self.__max_number_of_requests_per_session} allowed for this session" + f"The maximum number of requests {self._max_number_of_requests_per_session} allowed for this session" "has been reached. " "Raven limits the number of remote calls that a session is allowed to make as an early warning system. " "Sessions are expected to be short lived, and Raven provides facilities like load(String[] keys) " @@ -819,7 +817,7 @@ def delete(self, key_or_entity: Union[str, object], expected_change_vector: Opti change_vector = document_info.change_vector self._known_missing_ids.add(key) - change_vector = change_vector if self.__use_optimistic_concurrency else None + change_vector = change_vector if self._use_optimistic_concurrency else None if self._counters_by_doc_id: self._counters_by_doc_id.pop(key, None) self.defer( @@ -849,7 +847,7 @@ def delete(self, key_or_entity: Union[str, object], expected_change_vector: Opti def store(self, entity: object, key: Optional[str] = None, change_vector: Optional[str] = None) -> None: if all([entity, not key, not change_vector]): - has_id = self.__generate_entity_id_on_client.try_get_id_from_instance(entity)[0] + has_id = self._generate_entity_id_on_client.try_get_id_from_instance(entity)[0] checkmode = ConcurrencyCheckMode.FORCED if not has_id else ConcurrencyCheckMode.AUTO elif all([entity, key, not change_vector]): checkmode = ConcurrencyCheckMode.AUTO @@ -875,11 +873,11 @@ def __store_internal( if key is None: if self._generate_document_keys_on_store: - key = self.__generate_entity_id_on_client.generate_document_key_for_storage(entity) + key = self._generate_entity_id_on_client.generate_document_key_for_storage(entity) else: self._remember_entity_for_document_id_generation(entity) else: - self.__generate_entity_id_on_client.try_set_identity(entity, key) + self._generate_entity_id_on_client.try_set_identity(entity, key) if IdTypeAndName.create(key, CommandType.CLIENT_ANY_COMMAND, None) in self._deferred_commands_map.keys(): raise InvalidOperationException( @@ -959,7 +957,7 @@ def validate_cluster_transaction(self, result: SaveChangesData) -> None: if self.transaction_mode != TransactionMode.CLUSTER_WIDE: return - if self.__use_optimistic_concurrency: + if self._use_optimistic_concurrency: raise RuntimeError( f"useOptimisticConcurrency is not supported with TransactionMode set to {TransactionMode.CLUSTER_WIDE}" ) @@ -1042,7 +1040,7 @@ def __prepare_for_entities_deletion( result.on_success.remove_document_by_id(document_info.key) - change_vector = change_vector if self.__use_optimistic_concurrency else None + change_vector = change_vector if self._use_optimistic_concurrency else None self.before_delete_invoke(BeforeDeleteEventArgs(self, document_info.key, document_info.entity)) result.session_commands.append( DeleteCommandData(document_info.key, change_vector, document_info.change_vector) @@ -1078,7 +1076,7 @@ def __prepare_for_entities_puts(self, result: SaveChangesData) -> None: if command: self.__throw_invalid_modified_document_with_deferred_command(command) - if self.__before_store and entity.execute_on_before_store: + if self._before_store and entity.execute_on_before_store: before_store_event_args = BeforeStoreEventArgs(self, entity.value.key, entity.key) self.before_store_invoke(before_store_event_args) @@ -1097,7 +1095,7 @@ def __prepare_for_entities_puts(self, result: SaveChangesData) -> None: change_vector = ( (entity.value.change_vector if entity.value.change_vector else "") - if self.__use_optimistic_concurrency + if self._use_optimistic_concurrency else ( entity.value.change_vector if entity.value.concurrency_check_mode == ConcurrencyCheckMode.FORCED @@ -1202,11 +1200,11 @@ def __add_command(self, command: CommandData, key: str, command_type: CommandTyp ) def __close(self, is_disposing: bool) -> None: - if self.__is_disposed: + if self._is_disposed: return self.session_closing_invoke(SessionClosingEventArgs(self)) - self.__is_disposed = True + self._is_disposed = True def close(self) -> None: self.__close(True) @@ -1693,7 +1691,7 @@ def __update_existing_range(local_range: TimeSeriesRangeResult, new_range: TimeS local_range.entries = new_values def hash_code(self) -> int: - return self.__hash + return self._hash def __deserialize_from_transformer( self, object_type: Type[_T], key: Union[None, str], document: dict, track_entity: bool diff --git a/ravendb/documents/session/misc.py b/ravendb/documents/session/misc.py index f4c9fef7..60ab6f27 100644 --- a/ravendb/documents/session/misc.py +++ b/ravendb/documents/session/misc.py @@ -4,7 +4,7 @@ import threading from abc import ABC from enum import Enum -from typing import Union, Optional, TYPE_CHECKING, List, Dict +from typing import Union, Optional, TYPE_CHECKING, List, Dict, Generic, TypeVar from ravendb.http.misc import LoadBalanceBehavior @@ -18,6 +18,10 @@ from ravendb.documents import DocumentStore +_T_Key = TypeVar("_T_Key") +_T_Value = TypeVar("_T_Value") + + class TransactionMode(Enum): SINGLE_NODE = "single_node" CLUSTER_WIDE = "cluster_wide" @@ -210,33 +214,36 @@ def remove_at(self, index: int) -> JavaScriptArray: return self -class JavaScriptMap: +class JavaScriptMap(Generic[_T_Key, _T_Value]): def __init__(self, suffix: int, path_to_map: str): - self.__suffix = suffix - self.__path_to_map = path_to_map - self.__arg_counter = 0 - self.__script_lines = [] - self.__parameters: Dict[str, object] = {} + self._suffix = suffix + self._path_to_map = path_to_map + self._arg_counter = 0 + self._script_lines = [] + self._parameters: Dict[str, object] = {} @property def script(self) -> str: - return "\r".join(self.__script_lines) + return "\r".join(self._script_lines) @property def parameters(self) -> Dict[str, object]: - return self.__parameters + return self._parameters - def __get_next_argument_name(self) -> str: - self.__arg_counter += 1 - return f"val_{self.__arg_counter - 1}_{self.__suffix}" + def _get_next_argument_name(self) -> str: + self._arg_counter += 1 + return f"val_{self._arg_counter - 1}_{self._suffix}" - def put(self, key, value) -> JavaScriptMap: - argument_name = self.__get_next_argument_name() + def put(self, key: _T_Key, value: _T_Value) -> JavaScriptMap[_T_Key, _T_Value]: + argument_name = self._get_next_argument_name() - self.__script_lines.append(f"this.{self.__path_to_map}.{key} = args.{argument_name};") + self._script_lines.append(f"this.{self._path_to_map}.{key} = args.{argument_name};") self.parameters[argument_name] = value return self + def remove(self, key: _T_Key) -> JavaScriptMap[_T_Key, _T_Value]: + self._script_lines.append(f"delete this.{self._path_to_map}.{key};") + class MethodCall(ABC): def __init__(self, args: List[object] = None, access_path: str = None): diff --git a/ravendb/documents/smuggler/__init__.py b/ravendb/documents/smuggler/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ravendb/documents/smuggler/common.py b/ravendb/documents/smuggler/common.py new file mode 100644 index 00000000..aa9cf44d --- /dev/null +++ b/ravendb/documents/smuggler/common.py @@ -0,0 +1,24 @@ +import enum + + +class DatabaseItemType(enum.Enum): + NONE = "None" + DOCUMENTS = "Documents" + REVISION_DOCUMENTS = "RevisionDocuments" + INDEXES = "Indexes" + IDENTITIES = "Identities" + TOMBSTONES = "Tombstones" + LEGACY_ATTACHMENTS = "LegacyAttachments" + CONFLICTS = "Conflicts" + COMPARE_EXCHANGE = "CompareExchange" + LEGACY_DOCUMENT_DELETIONS = "LegacyDocumentDeletions" + LEGACY_ATTACHMENT_DELETIONS = "LegacyAttachmentDeletions" + DATABASE_RECORD = "DatabaseRecord" + UNKNOWN = "Unknown" + COUNTERS = "Counters" + ATTACHMENTS = "Attachments" + COUNTER_GROUPS = "CounterGroups" + SUBSCRIPTIONS = "Subscriptions" + COMPARE_EXCHANGE_TOMBSTONES = "CompareExchangeTombstones" + TIME_SERIES = "TimeSeries" + REPLICATION_HUB_CERTIFICATES = "ReplicationHubCertificates" diff --git a/ravendb/documents/store/definition.py b/ravendb/documents/store/definition.py index d434d80e..8bb5e8d5 100644 --- a/ravendb/documents/store/definition.py +++ b/ravendb/documents/store/definition.py @@ -8,6 +8,7 @@ from ravendb.changes.database_changes import DatabaseChanges from ravendb.documents.bulk_insert_operation import BulkInsertOperation, BulkInsertOptions +from ravendb.documents.indexes.index_creation import IndexCreation from ravendb.documents.operations.executor import MaintenanceOperationExecutor, OperationExecutor from ravendb.documents.operations.indexes import PutIndexesOperation from ravendb.documents.session.event_args import ( @@ -42,7 +43,7 @@ T = TypeVar("T") if TYPE_CHECKING: - from ravendb.documents.indexes.index_creation import AbstractIndexCreationTask, IndexCreation + from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractIndexCreationTask class DocumentStoreBase: @@ -475,7 +476,7 @@ def execute_indexes(self, tasks: "List[AbstractIndexCreationTask]", database: Op self.assert_initialized() indexes_to_add = IndexCreation.create_indexes_to_add(tasks, self.conventions) - self.maintenance.for_database(self.get_effective_database(database)).send(PutIndexesOperation(indexes_to_add)) + self.maintenance.for_database(self.get_effective_database(database)).send(PutIndexesOperation(*indexes_to_add)) def changes(self, database=None, on_error=None, executor=None) -> DatabaseChanges: # todo: sync with java self.assert_initialized() diff --git a/ravendb/http/raven_command.py b/ravendb/http/raven_command.py index 3aa5b645..baa9f3a2 100644 --- a/ravendb/http/raven_command.py +++ b/ravendb/http/raven_command.py @@ -30,12 +30,13 @@ def __str__(self): class RavenCommand(Generic[_T_Result]): @classmethod - def from_copy(cls, copy: RavenCommand[_T_Result]): + def from_copy(cls, copy: RavenCommand[_T_Result]) -> RavenCommand[_T_Result]: command = cls(copy._result_class) command._response_type = copy.response_type command._can_cache = copy.can_cache command._can_cache_aggressively = copy.can_cache_aggressively command._selected_node_tag = copy.selected_node_tag + return command def __init__(self, result_class: Type[_T_Result] = None): self._result_class = result_class diff --git a/ravendb/http/request_executor.py b/ravendb/http/request_executor.py index 0b4cad3e..458b2e5a 100644 --- a/ravendb/http/request_executor.py +++ b/ravendb/http/request_executor.py @@ -1061,7 +1061,11 @@ def __handle_unsuccessful_response( ) elif response.status_code == HTTPStatus.CONFLICT: - raise NotImplementedError("Handle conflict") # todo: handle conflict (exception dispatcher involved) + data = json.loads(response.text) + message = data.get("Message", None) + err_type = data.get("Type", None) + + raise RuntimeError(f"{err_type}: {message}") # todo: handle conflict (exception dispatcher involved) elif response.status_code == 425: # too early if not should_retry: diff --git a/ravendb/infrastructure/operations.py b/ravendb/infrastructure/operations.py index 08ee3cba..e4f0d66d 100644 --- a/ravendb/infrastructure/operations.py +++ b/ravendb/infrastructure/operations.py @@ -1,11 +1,15 @@ import json -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Set import requests -from ravendb.http.raven_command import RavenCommand +from ravendb.http.topology import RaftCommand +from ravendb.documents.operations.definitions import VoidMaintenanceOperation +from ravendb.documents.smuggler.common import DatabaseItemType +from ravendb.http.raven_command import RavenCommand, VoidRavenCommand from ravendb.http.server_node import ServerNode from ravendb.serverwide.operations.common import ServerOperation +from ravendb.util.util import RaftIdGenerator if TYPE_CHECKING: from ravendb.documents.conventions import DocumentConventions @@ -33,3 +37,29 @@ def is_read_request(self) -> bool: def set_response(self, response: str, from_cache: bool) -> None: self.result = json.loads(response) + + +class CreateSampleDataOperation(VoidMaintenanceOperation): + def __init__(self, operate_on_types=None): + if operate_on_types is None: + operate_on_types = {DatabaseItemType.DOCUMENTS} + self._operate_on_types = operate_on_types + + def get_command(self, conventions: "DocumentConventions") -> "VoidRavenCommand": + return self.CreateSampleDataCommand(self._operate_on_types) + + class CreateSampleDataCommand(VoidRavenCommand, RaftCommand): + def __init__(self, operate_on_types: Set[DatabaseItemType]): + super().__init__() + self._operate_on_types = operate_on_types + + def is_read_request(self) -> bool: + return False + + def create_request(self, node: ServerNode) -> requests.Request: + url = f"{node.url}/databases/{node.database}/studio/sample-data" + url += f"?{'&'.join([f'operateOnTypes={x.value}' for x in self._operate_on_types])}" + return requests.Request("POST", url) + + def get_raft_unique_request_id(self) -> str: + return RaftIdGenerator.new_id() diff --git a/ravendb/serverwide/operations/common.py b/ravendb/serverwide/operations/common.py index 82a989e2..2679b325 100644 --- a/ravendb/serverwide/operations/common.py +++ b/ravendb/serverwide/operations/common.py @@ -4,7 +4,7 @@ import enum import json from abc import abstractmethod -from typing import Generic, TypeVar, TYPE_CHECKING, Optional, List, Dict +from typing import Generic, TypeVar, TYPE_CHECKING, Optional, List, Dict, Any import requests from ravendb.primitives import constants @@ -12,13 +12,13 @@ from ravendb.serverwide.database_record import DatabaseRecordWithEtag, DatabaseRecord from ravendb.serverwide.misc import DatabaseTopology from ravendb.tools.utils import Utils -from ravendb.http.raven_command import RavenCommand +from ravendb.http.raven_command import RavenCommand, VoidRavenCommand from ravendb.util.util import RaftIdGenerator from ravendb.http.topology import RaftCommand + if TYPE_CHECKING: from ravendb.http.server_node import ServerNode - from ravendb.http.raven_command import VoidRavenCommand from ravendb.http.request_executor import RequestExecutor from ravendb.documents.conventions import DocumentConventions @@ -109,8 +109,8 @@ def from_json(cls, json_dict: dict) -> DatabasePutResult: return cls( json_dict["RaftCommandIndex"], json_dict["Name"], - DatabaseTopology.from_json(json_dict["Topology"]), - json_dict["NodesAddedTo"], + DatabaseTopology.from_json(json_dict["Topology"]) if "Topology" in json_dict else None, + json_dict["NodesAddedTo"] if "NodesAddedTo" in json_dict else None, ) @@ -369,3 +369,82 @@ def from_json(cls, json_dict: Dict) -> ModifyOngoingTaskResult: class DatabaseSettings: def __init__(self, settings: Dict[str, str] = None): self.settings = settings + + +class ReorderDatabaseMembersOperation(VoidServerOperation): + class Parameters: + def __init__(self, members_order: List[str] = None, fixed: bool = None): + self.members_order = members_order + self.fixed = fixed + + def to_json(self) -> Dict[str, Any]: + return {"MembersOrder": self.members_order, "Fixed": self.fixed} + + def __init__(self, database: str = None, order: List[str] = None, fixed: bool = False): + if not order: + raise ValueError("Order list must contain values") + + self._database = database + self._parameters = self.Parameters(order, fixed) + + def get_command(self, conventions: "DocumentConventions") -> "VoidRavenCommand": + return self.ReorderDatabaseMemberCommand(self._database, self._parameters) + + class ReorderDatabaseMemberCommand(VoidRavenCommand, RaftCommand): + def __init__(self, database_name: str, parameters: Optional[ReorderDatabaseMembersOperation.Parameters] = None): + super().__init__() + if not database_name: + raise ValueError("Database cannot be empty") + + self._database_name = database_name + self._parameters = parameters + + def create_request(self, node: ServerNode) -> requests.Request: + url = f"{node.url}/admin/databases/reorder?name={self._database_name}" + request = requests.Request("POST", url) + request.data = self._parameters.to_json() + return request + + def is_read_request(self) -> bool: + return False + + def get_raft_unique_request_id(self) -> str: + return RaftIdGenerator.new_id() + + +class PromoteDatabaseNodeOperation(ServerOperation[DatabasePutResult]): + def __init__(self, database_name: str = None, node: str = None): + self._database_name = database_name + self._node = node + + def get_command(self, conventions: "DocumentConventions") -> RavenCommand[DatabasePutResult]: + return self._PromoteDatabaseNodeCommand(self._database_name, self._node) + + class _PromoteDatabaseNodeCommand(RavenCommand[DatabasePutResult], RaftCommand): + def __init__(self, database_name: str = None, node: str = None): + super().__init__(DatabasePutResult) + if not database_name: + raise ValueError("Database name cannot be None") + + if not node: + raise ValueError("Node cannot be None") + + self._database_name = database_name + self._node = node + + def create_request(self, node: ServerNode) -> requests.Request: + return requests.Request( + "POST", f"{node.url}/admin/databases/promote?name={self._database_name}&node={self._node}" + ) + + def set_response(self, response: Optional[str], from_cache: bool) -> None: + if response is None: + self._throw_invalid_response() + + self.result = DatabasePutResult.from_json(json.loads(response)) + + def is_read_request(self) -> bool: + return False + + def get_raft_unique_request_id(self) -> str: + return RaftIdGenerator.new_id() diff --git a/ravendb/tests/jvm_migrated_tests/bugs_tests/test_simple_multi_map.py b/ravendb/tests/jvm_migrated_tests/bugs_tests/test_simple_multi_map.py index cb8513bb..ab59e23e 100644 --- a/ravendb/tests/jvm_migrated_tests/bugs_tests/test_simple_multi_map.py +++ b/ravendb/tests/jvm_migrated_tests/bugs_tests/test_simple_multi_map.py @@ -2,7 +2,8 @@ from abc import ABC from typing import Optional -from ravendb.documents.indexes.index_creation import AbstractMultiMapIndexCreationTask +from ravendb import GetIndexOperation +from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractMultiMapIndexCreationTask from ravendb.tests.test_base import TestBase @@ -56,3 +57,8 @@ def test_can_query_using_multi_map(self): self.assertTrue(isinstance(have_names[0], Dog)) self.assertTrue(isinstance(have_names[1], Cat)) + + def test_can_create_multi_map_index(self): + CatsAndDogs().execute(self.store) + index_definition = self.store.maintenance.send(GetIndexOperation("CatsAndDogs")) + self.assertEqual(2, len(index_definition.maps)) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/commands_tests/test_delete_document_command.py b/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/commands_tests/test_delete_document_command.py index b749b062..b3ac9796 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/commands_tests/test_delete_document_command.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/commands_tests/test_delete_document_command.py @@ -10,20 +10,15 @@ class TestDeleteDocumentCommand(TestBase): def setUp(self): super(TestDeleteDocumentCommand, self).setUp() - @unittest.skip("exception dispatcher - handle conflict") def test_can_delete_document(self): with self.store.open_session() as session: user = User(name="Marcin") session.store(user, "users/1") session.save_changes() - change_vector = session.advanced.get_change_vector_for(user) + command = DeleteDocumentCommand("users/1") + self.store.get_request_executor().execute_command(command) with self.store.open_session() as session: loaded_user = session.load("users/1", User) - loaded_user.age = 5 - session.save_changes() - - command = DeleteDocumentCommand("users/1", change_vector) - with self.assertRaises(ConcurrencyException): - self.store.get_request_executor().execute_command(command) + self.assertIsNone(loaded_user) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/commands_tests/test_get_next_operation_id_command.py b/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/commands_tests/test_get_next_operation_id_command.py new file mode 100644 index 00000000..9ba3a931 --- /dev/null +++ b/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/commands_tests/test_get_next_operation_id_command.py @@ -0,0 +1,12 @@ +from ravendb.documents.commands.bulkinsert import GetNextOperationIdCommand +from ravendb.tests.test_base import TestBase + + +class TestGetNextOperationIdCommand(TestBase): + def setUp(self): + super().setUp() + + def test_can_get_next_operation_id(self): + command = GetNextOperationIdCommand() + self.store.get_request_executor().execute_command(command) + self.assertIsNotNone(command.result) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/commands_tests/test_get_statistics_command.py b/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/commands_tests/test_get_statistics_command.py new file mode 100644 index 00000000..c9b55c6b --- /dev/null +++ b/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/commands_tests/test_get_statistics_command.py @@ -0,0 +1,76 @@ +from ravendb import GetStatisticsOperation +from ravendb.documents.smuggler.common import DatabaseItemType +from ravendb.infrastructure.operations import CreateSampleDataOperation +from ravendb.tests.test_base import TestBase + + +class TestGetStatisticsCommand(TestBase): + def setUp(self): + super().setUp() + + def test_can_get_stats(self): + executor = self.store.get_request_executor() + + sample_data = CreateSampleDataOperation( + { + DatabaseItemType.DOCUMENTS, + DatabaseItemType.INDEXES, + DatabaseItemType.ATTACHMENTS, + DatabaseItemType.REVISION_DOCUMENTS, + } + ) + + self.store.maintenance.send(sample_data) + + self.wait_for_indexing(self.store) + + command = GetStatisticsOperation._GetStatisticsCommand() + executor.execute_command(command) + + stats = command.result + self.assertIsNotNone(stats) + + self.assertIsNotNone(stats.last_doc_etag) + self.assertGreater(stats.last_doc_etag, 0) + + self.assertGreaterEqual(stats.count_of_indexes, 3) + self.assertGreaterEqual(1059, stats.count_of_documents) + + self.assertGreater(stats.count_of_revision_documents, 0) + + self.assertGreaterEqual(0, stats.count_of_documents_conflicts) + + self.assertGreaterEqual(0, stats.count_of_conflicts) + + self.assertGreaterEqual(17, stats.count_of_unique_attachments) + + self.assertEqual(17, stats.count_of_unique_attachments) + + self.assertIsNotNone(stats.database_change_vector) + + self.assertIsNotNone(stats.database_id) + + self.assertIsNotNone(stats.pager) + + self.assertIsNotNone(stats.last_indexing_time) + + self.assertIsNotNone(stats.indexes) + + self.assertIsNotNone(stats.size_on_disk.human_size) + + self.assertIsNotNone(stats.size_on_disk.size_in_bytes) + + for index_information in stats.indexes: + self.assertIsNotNone(index_information.name) + + self.assertFalse(index_information.stale) + + self.assertIsNotNone(index_information.state) + + self.assertIsNotNone(index_information.lock_mode) + + self.assertIsNotNone(index_information.priority) + + self.assertIsNotNone(index_information.type) + + self.assertIsNotNone(index_information.last_indexing_time) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/commands_tests/test_put_document_command.py b/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/commands_tests/test_put_document_command.py new file mode 100644 index 00000000..fd7f04b3 --- /dev/null +++ b/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/commands_tests/test_put_document_command.py @@ -0,0 +1,47 @@ +import unittest + +from ravendb.documents.commands.crud import PutDocumentCommand +from ravendb.infrastructure.entities import User +from ravendb.tests.test_base import TestBase +from ravendb.tools.utils import Utils + + +class TestPutDocumentCommand(TestBase): + def setUp(self): + super().setUp() + + def test_can_put_document_using_command(self): + with self.store.open_session() as session: + user = User(name="Gracjan", age=30) + node = Utils.entity_to_dict(user, self.store.conventions.json_default_method) + command = PutDocumentCommand("users/1", None, node) + self.store.get_request_executor().execute_command(command) + + result = command.result + + self.assertEqual("users/1", result.key) + + self.assertIsNotNone(result.change_vector) + + with self.store.open_session() as session: + loaded_user = session.load("users/1", User) + self.assertEqual(loaded_user.name, "Gracjan") + + @unittest.skip("todo: Not passing on CI/CD") + def test_can_put_document_using_command_with_surrogate_pairs(self): + name_with_emojis = "Gracjan \uD83D\uDE21\uD83D\uDE21\uD83E\uDD2C\uD83D\uDE00😡😡🤬😀" + + user = User(name=name_with_emojis, age=31) + node = Utils.entity_to_dict(user, self.store.conventions.json_default_method) + command = PutDocumentCommand("users/2", None, node) + self.store.get_request_executor().execute_command(command) + + result = command.result + + self.assertEqual("users/2", result.key) + + self.assertIsNotNone(result.change_vector) + + with self.store.open_session() as session: + loaded_user = session.load("users/2", User) + self.assertEqual(loaded_user.name, name_with_emojis) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/commands_tests/test_reorder_database_members.py b/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/commands_tests/test_reorder_database_members.py new file mode 100644 index 00000000..d2411a83 --- /dev/null +++ b/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/commands_tests/test_reorder_database_members.py @@ -0,0 +1,10 @@ +from ravendb.serverwide.operations.common import ReorderDatabaseMembersOperation +from ravendb.tests.test_base import TestBase + + +class TestReorderDatabaseMembers(TestBase): + def setUp(self): + super().setUp() + + def test_can_send_reorder_command(self): + self.store.maintenance.server.send(ReorderDatabaseMembersOperation(self.store.database, ["A"])) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/operations_tests/configuration/test_client_configuration.py b/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/operations_tests/configuration/test_client_configuration.py index e931d22f..7779a945 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/operations_tests/configuration/test_client_configuration.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/documents_tests/operations_tests/configuration/test_client_configuration.py @@ -3,6 +3,7 @@ PutServerWideClientConfigurationOperation, GetServerWideClientConfigurationOperation, GetClientConfigurationOperation, + PutClientConfigurationOperation, ) from ravendb.http.misc import ReadBalanceBehavior, LoadBalanceBehavior from ravendb.tests.test_base import TestBase @@ -39,3 +40,23 @@ def test_can_handle_no_configuration(self): result = self.store.maintenance.send(operation) self.assertIsNotNone(result.etag) + + def test_can_save_and_read_client_configuration(self): + configuration_to_save = ClientConfiguration() + configuration_to_save.etag = 123 + configuration_to_save.max_number_of_requests_per_session = 80 + configuration_to_save.read_balance_behavior = ReadBalanceBehavior.FASTEST_NODE + configuration_to_save.disabled = True + save_operation = PutClientConfigurationOperation(configuration_to_save) + self.store.maintenance.send(save_operation) + + operation = GetClientConfigurationOperation() + result = self.store.maintenance.send(operation) + + self.assertIsNotNone(result.etag) + new_configuration: ClientConfiguration = result.configuration + + self.assertIsNotNone(new_configuration) + self.assertTrue(new_configuration.disabled) + self.assertEqual(80, new_configuration.max_number_of_requests_per_session) + self.assertEqual(ReadBalanceBehavior.FASTEST_NODE, new_configuration.read_balance_behavior) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/indexing_tests/test_indexes_from_client.py b/ravendb/tests/jvm_migrated_tests/client_tests/indexing_tests/test_indexes_from_client.py index 8ff36212..e159d7fc 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/indexing_tests/test_indexes_from_client.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/indexing_tests/test_indexes_from_client.py @@ -1,6 +1,11 @@ -from ravendb import MoreLikeThisOptions +import time + +from ravendb.documents.operations.statistics import GetStatisticsOperation +from ravendb.documents.indexes.index_creation import IndexCreation +from ravendb.documents.queries.more_like_this import MoreLikeThisOptions +from ravendb.documents.operations.indexes import GetIndexNamesOperation, DeleteIndexOperation, ResetIndexOperation from ravendb.documents.indexes.definitions import FieldIndexing, FieldStorage -from ravendb.documents.indexes.index_creation import IndexCreation, AbstractIndexCreationTask +from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractIndexCreationTask from ravendb.infrastructure.entities import User, Post from ravendb.tests.test_base import TestBase @@ -29,6 +34,12 @@ def __init__(self): self._analyze("desc", "Lucene.Net.Analysis.SimpleAnalyzer") +class UsersIndex(AbstractIndexCreationTask): + def __init__(self): + super().__init__() + self.map = "from user in docs.users select new { user.name }" + + class TestIndexesFromClient(TestBase): def setUp(self): super(TestIndexesFromClient, self).setUp() @@ -80,3 +91,54 @@ def test_more_like_this(self): self.assertEqual("love programming", results[1].desc) self.assertEqual("We do", results[2].title) self.assertEqual("prototype", results[2].desc) + + def test_can_execute_many_indexes(self): + self.store.execute_indexes([UsersIndex()]) + index_names_operation = GetIndexNamesOperation(0, 10) + index_names = self.store.maintenance.send(index_names_operation) + self.assertEqual(1, len(index_names)) + + def test_can_delete(self): + self.store.execute_index(UsersIndex()) + self.store.maintenance.send(DeleteIndexOperation(UsersIndex().index_name)) + + command = GetStatisticsOperation._GetStatisticsCommand() + self.store.get_request_executor().execute_command(command) + + statistics = command.result + + self.assertEqual(0, len(statistics.indexes)) + + def test_can_reset(self): + with self.store.open_session() as session: + user1 = User() + user1.name = "Marcin" + session.store(user1, "users/1") + session.save_changes() + + self.store.execute_index(UsersIndex()) + self.wait_for_indexing(self.store) + + command = GetStatisticsOperation._GetStatisticsCommand() + self.store.get_request_executor().execute_command(command) + + statistics = command.result + + first_indexing_time = statistics.indexes[0].last_indexing_time + + index_name = UsersIndex().index_name + + # now reset index + + time.sleep(0.02) # avoid the same millisecond + + self.store.maintenance.send(ResetIndexOperation(index_name)) + self.wait_for_indexing(self.store) + + command = GetStatisticsOperation._GetStatisticsCommand() + self.store.get_request_executor().execute_command(command) + + statistics = command.result + + second_indexing_time = statistics.last_indexing_time + self.assertLess(first_indexing_time, second_indexing_time) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/indexing_tests/test_java_script_index.py b/ravendb/tests/jvm_migrated_tests/client_tests/indexing_tests/test_java_script_index.py index 76f210c3..fab4a970 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/indexing_tests/test_java_script_index.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/indexing_tests/test_java_script_index.py @@ -3,7 +3,7 @@ from ravendb import IndexFieldOptions from ravendb.primitives import constants from ravendb.documents.indexes.definitions import FieldIndexing -from ravendb.documents.indexes.index_creation import AbstractJavaScriptIndexCreationTask +from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractJavaScriptIndexCreationTask from ravendb.infrastructure.entities import User from ravendb.tests.test_base import TestBase diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/indexing_tests/time_series_tests/test_basic_time_series_indexes_java_script.py b/ravendb/tests/jvm_migrated_tests/client_tests/indexing_tests/time_series_tests/test_basic_time_series_indexes_java_script.py index 0fa89e0c..40b9f945 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/indexing_tests/time_series_tests/test_basic_time_series_indexes_java_script.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/indexing_tests/time_series_tests/test_basic_time_series_indexes_java_script.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta from ravendb import GetTermsOperation -from ravendb.documents.indexes.index_creation import AbstractJavaScriptIndexCreationTask +from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractJavaScriptIndexCreationTask from ravendb.documents.indexes.time_series import AbstractJavaScriptTimeSeriesIndexCreationTask from ravendb.infrastructure.entities import User from ravendb.infrastructure.orders import Company, Employee, Address diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/test_next_and_seed_identities.py b/ravendb/tests/jvm_migrated_tests/client_tests/test_next_and_seed_identities.py new file mode 100644 index 00000000..99da8e12 --- /dev/null +++ b/ravendb/tests/jvm_migrated_tests/client_tests/test_next_and_seed_identities.py @@ -0,0 +1,50 @@ +from ravendb.documents.operations.identities import NextIdentityForOperation, SeedIdentityForOperation +from ravendb.infrastructure.entities import User +from ravendb.tests.test_base import TestBase + + +class TestNextAndSeedIdentities(TestBase): + def setUp(self): + super().setUp() + + def test_next_identity_for_operation_should_create_a_new_identify_if_there_is_none(self): + with self.store.open_session() as session: + result = self.store.maintenance.send(NextIdentityForOperation("person|")) + + self.assertEqual(1, result) + + def test_seed_identity_for(self): + with self.store.open_session() as session: + user = User(last_name="Adi") + session.store(user, "users|") + session.save_changes() + + result1 = self.store.maintenance.send(SeedIdentityForOperation("users", 1990)) + self.assertEqual(1990, result1) + + with self.store.open_session() as session: + user = User(last_name="Avivi") + session.store(user, "users|") + session.save_changes() + + with self.store.open_session() as session: + entity_with_id_1 = session.load("users/1", User) + entity_with_id_2 = session.load("users/2", User) + entity_with_id_1990 = session.load("users/1990", User) + entity_with_id_1991 = session.load("users/1991", User) + entity_with_id_1992 = session.load("users/1992", User) + + self.assertIsNotNone(entity_with_id_1) + self.assertIsNotNone(entity_with_id_1991) + self.assertIsNone(entity_with_id_2) + self.assertIsNone(entity_with_id_1990) + self.assertIsNone(entity_with_id_1992) + + self.assertEqual("Adi", entity_with_id_1.last_name) + self.assertEqual("Avivi", entity_with_id_1991.last_name) + + result2 = self.store.maintenance.send(SeedIdentityForOperation("users", 1975)) + self.assertEqual(1991, result2) + + result3 = self.store.maintenance.send(SeedIdentityForOperation("users", 1975, True)) + self.assertEqual(1975, result3) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/test_patch.py b/ravendb/tests/jvm_migrated_tests/client_tests/test_patch.py index ec3e7cf6..5afe00a5 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/test_patch.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/test_patch.py @@ -1,4 +1,4 @@ -from ravendb import PatchByQueryOperation +from ravendb import PatchByQueryOperation, PatchOperation, PatchRequest, PatchStatus from ravendb.infrastructure.entities import User from ravendb.tests.test_base import TestBase @@ -23,3 +23,17 @@ def test_can_patch_many_documents(self): with self.store.open_session() as session: loaded_user = session.load("users/1", User) self.assertEqual("Patched", loaded_user.name) + + def test_can_patch_single_document(self): + with self.store.open_session() as session: + user = User(name="RavenDB") + session.store(user, "users/1") + session.save_changes() + + patch_operation = PatchOperation("users/1", None, PatchRequest.for_script('this.name = "Patched"')) + status = self.store.operations.send(patch_operation).status + self.assertEqual(PatchStatus.PATCHED, status) + + with self.store.open_session() as session: + loaded_user = session.load("users/1", User) + self.assertEqual("Patched", loaded_user.name) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/test_query.py b/ravendb/tests/jvm_migrated_tests/client_tests/test_query.py index 3ec0f821..58c4fdc4 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/test_query.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/test_query.py @@ -1,6 +1,6 @@ from typing import Optional -from ravendb.documents.indexes.index_creation import AbstractIndexCreationTask +from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractIndexCreationTask from ravendb.documents.session.query_group_by import GroupByField from ravendb.infrastructure.entities import User from ravendb.tests.test_base import TestBase diff --git a/ravendb/tests/jvm_migrated_tests/crud_tests/test_store.py b/ravendb/tests/jvm_migrated_tests/crud_tests/test_store.py index ae5ed5f4..0138f656 100644 --- a/ravendb/tests/jvm_migrated_tests/crud_tests/test_store.py +++ b/ravendb/tests/jvm_migrated_tests/crud_tests/test_store.py @@ -5,6 +5,21 @@ class TestStore(TestBase): def setUp(self): super(TestStore, self).setUp() + def test_refresh(self): + with self.store.open_session() as session: + user = User("RavenDB") + session.store(user, "users/1") + session.save_changes() + + with self.store.open_session() as inner_session: + inner_user = inner_session.load("users/1", User) + inner_user.name = "RavenDB 4.0" + inner_session.save_changes() + + user = session.advanced.refresh(user) + + self.assertEqual("RavenDB 4.0", user.name) + def test_store_document(self): with self.store.open_session() as session: user = User("Jacex", 10) diff --git a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_11058.py b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_11058.py index f0f1cfbd..eb98efc5 100644 --- a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_11058.py +++ b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_11058.py @@ -21,8 +21,8 @@ def test_can_copy_attachment(self): session.save_changes() stats = self.store.maintenance.send(GetStatisticsOperation()) - self.assertEqual(2, stats.get("CountOfAttachments")) - self.assertEqual(2, stats.get("CountOfUniqueAttachments")) + self.assertEqual(2, stats.count_of_attachments) + self.assertEqual(2, stats.count_of_unique_attachments) with self.store.open_session() as session: new_company = Company(name="CF") @@ -33,18 +33,8 @@ def test_can_copy_attachment(self): session.save_changes() stats = self.store.maintenance.send(GetStatisticsOperation()) - self.assertEqual( - 3, - stats.get( - "CountOfAttachments", - ), - ) - self.assertEqual( - 2, - stats.get( - "CountOfUniqueAttachments", - ), - ) + self.assertEqual(3, stats.count_of_attachments) + self.assertEqual(2, stats.count_of_unique_attachments) with self.store.open_session() as session: self.assertTrue(session.advanced.attachments.exists("companies/1", "file1")) @@ -69,14 +59,8 @@ def test_can_rename_attachment(self): session.save_changes() stats = self.store.maintenance.send(GetStatisticsOperation()) - self.assertEqual( - 2, - stats.get("CountOfAttachments"), - ) - self.assertEqual( - 2, - stats.get("CountOfUniqueAttachments"), - ) + self.assertEqual(2, stats.count_of_attachments) + self.assertEqual(2, stats.count_of_unique_attachments) with self.store.open_session() as session: company = session.load("companies/1-A", Company) @@ -84,14 +68,8 @@ def test_can_rename_attachment(self): session.save_changes() stats = self.store.maintenance.send(GetStatisticsOperation()) - self.assertEqual( - 2, - stats.get("CountOfAttachments"), - ) - self.assertEqual( - 2, - stats.get("CountOfUniqueAttachments"), - ) + self.assertEqual(2, stats.count_of_attachments) + self.assertEqual(2, stats.count_of_unique_attachments) with self.store.open_session() as session: self.assertFalse(session.advanced.attachments.exists("companies/1-A", "file1")) @@ -103,14 +81,8 @@ def test_can_rename_attachment(self): session.save_changes() stats = self.store.maintenance.send(GetStatisticsOperation()) - self.assertEqual( - 2, - stats.get("CountOfAttachments"), - ) - self.assertEqual( - 2, - stats.get("CountOfUniqueAttachments"), - ) + self.assertEqual(2, stats.count_of_attachments) + self.assertEqual(2, stats.count_of_unique_attachments) with self.store.open_session() as session: self.assertFalse(session.advanced.attachments.exists("companies/1-A", "file2")) @@ -124,14 +96,8 @@ def test_can_rename_attachment(self): session.save_changes() stats = self.store.maintenance.send(GetStatisticsOperation()) - self.assertEqual( - 2, - stats.get("CountOfAttachments"), - ) - self.assertEqual( - 2, - stats.get("CountOfUniqueAttachments"), - ) + self.assertEqual(2, stats.count_of_attachments) + self.assertEqual(2, stats.count_of_unique_attachments) def test_can_move_attachment(self): with self.store.open_session() as session: @@ -148,14 +114,8 @@ def test_can_move_attachment(self): session.save_changes() stats = self.store.maintenance.send(GetStatisticsOperation()) - self.assertEqual( - 3, - stats.get("CountOfAttachments"), - ) - self.assertEqual( - 3, - stats.get("CountOfUniqueAttachments"), - ) + self.assertEqual(3, stats.count_of_attachments) + self.assertEqual(3, stats.count_of_unique_attachments) with self.store.open_session() as session: new_company = Company(name="CF") @@ -166,14 +126,8 @@ def test_can_move_attachment(self): session.advanced.attachments.move(old_company, "file1", new_company, "file2") session.save_changes() - self.assertEqual( - 3, - stats.get("CountOfAttachments"), - ) - self.assertEqual( - 3, - stats.get("CountOfUniqueAttachments"), - ) + self.assertEqual(3, stats.count_of_attachments) + self.assertEqual(3, stats.count_of_unique_attachments) with self.store.open_session() as session: self.assertFalse(session.advanced.attachments.exists("companies/1", "file1")) @@ -190,15 +144,8 @@ def test_can_move_attachment(self): session.advanced.attachments.move("companies/1", "file10", "companies/2", "file3") session.save_changes() - stats = self.store.maintenance.send(GetStatisticsOperation()) - self.assertEqual( - 3, - stats.get("CountOfAttachments"), - ) - self.assertEqual( - 3, - stats.get("CountOfUniqueAttachments"), - ) + self.assertEqual(3, stats.count_of_attachments) + self.assertEqual(3, stats.count_of_unique_attachments) with self.store.open_session() as session: self.assertFalse(session.advanced.attachments.exists("companies/1", "file1")) diff --git a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_12902.py b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_12902.py index c8beffa7..69999149 100644 --- a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_12902.py +++ b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_12902.py @@ -1,6 +1,6 @@ from typing import Optional -from ravendb.documents.indexes.index_creation import AbstractIndexCreationTask +from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractIndexCreationTask from ravendb.documents.queries.query import QueryResult from ravendb.documents.session.query import QueryStatistics from ravendb.infrastructure.entities import User diff --git a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_13034.py b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_13034.py new file mode 100644 index 00000000..a51564f9 --- /dev/null +++ b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_13034.py @@ -0,0 +1,45 @@ +from ravendb.exceptions.raven_exceptions import ConcurrencyException +from ravendb.infrastructure.entities import User +from ravendb.tests.test_base import TestBase + + +class TestRavenDB13034(TestBase): + def setUp(self): + super().setUp() + + def test_exploring_concurrency_behavior(self): + with self.store.open_session() as s1: + user = User(name="Nick", age=99) + s1.store(user, "users/1-A") + s1.save_changes() + + with self.store.open_session() as s2: + s2.advanced.use_optimistic_concurrency = True + u2 = s2.load("users/1-A", User) + with self.store.open_session() as s3: + u3 = s3.load("users/1-A", User) + self.assertNotEqual(u2, u3) + u3.age -= 1 + s3.save_changes() + + u2.age += 1 + + u2_2 = s2.load("users/1-A", User) + self.assertEqual(u2, u2_2) + self.assertEqual(1, s2.advanced.number_of_requests) + + with self.assertRaises(RuntimeError): + s2.save_changes() + + self.assertEqual(2, s2.advanced.number_of_requests) + + u2_3 = s2.load("users/1-A", User) + self.assertEqual(u2_3, u2) + self.assertEqual(2, s2.advanced.number_of_requests) + + with self.assertRaises(RuntimeError): + s2.save_changes() + + with self.store.open_session() as s4: + u4 = s4.load("users/1-A", User) + self.assertEqual(98, u4.age) diff --git a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_13452.py b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_13452.py new file mode 100644 index 00000000..1d7b42ff --- /dev/null +++ b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_13452.py @@ -0,0 +1,59 @@ +from typing import Dict + +from ravendb.tests.test_base import TestBase + + +class Item: + def __init__(self, values: Dict[str, str] = None): + self.values = values + + +class TestRavenDB13452(TestBase): + def setUp(self): + super().setUp() + + def test_can_modify_dictionary_with_patch_add(self): + with self.store.open_session() as session: + item = Item({"Key1": "Value1", "Key2": "Value2"}) + session.store(item, "items/1") + session.save_changes() + + with self.store.open_session() as session: + item = session.load("items/1", Item) + session.advanced.patch_object(item, "values", lambda dict_: dict_.put("Key3", "Value3")) + session.save_changes() + + with self.store.open_session() as session: + item = session.load("items/1", dict) + values = item.get("values", None) + self.assertIsNotNone(values) + self.assertEqual(3, len(values)) + self.assertEqual("Value1", values.get("Key1")) + self.assertEqual("Value2", values.get("Key2")) + self.assertEqual("Value3", values.get("Key3")) + + def test_can_modify_dictionary_with_patch_remove(self): + with self.store.open_session() as session: + item = Item() + values = {} + item.values = values + values["Key1"] = "Value1" + values["Key2"] = "Value2" + values["Key3"] = "Value3" + + session.store(item, "items/1") + session.save_changes() + + with self.store.open_session() as session: + item = session.load("items/1", Item) + session.advanced.patch_object(item, "values", lambda dict_: dict_.remove("Key2")) + session.save_changes() + + with self.store.open_session() as session: + item = session.load("items/1", dict) + values = item.get("values", None) + self.assertIsNotNone(values) + self.assertEqual(2, len(values)) + + self.assertEqual("Value1", values.get("Key1")) + self.assertEqual("Value3", values.get("Key3")) diff --git a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_13682.py b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_13682.py index d3cfff3a..80a791ef 100644 --- a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_13682.py +++ b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_13682.py @@ -1,4 +1,4 @@ -from ravendb.documents.indexes.index_creation import AbstractIndexCreationTask +from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractIndexCreationTask from ravendb.documents.queries.spatial import PointField from ravendb.tests.test_base import TestBase diff --git a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_14006.py b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_14006.py index 8a7ae47c..f52604ac 100644 --- a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_14006.py +++ b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_14006.py @@ -2,7 +2,7 @@ import unittest from typing import Optional -from ravendb.documents.indexes.index_creation import AbstractIndexCreationTask +from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractIndexCreationTask from ravendb.documents.session.loaders.include import QueryIncludeBuilder from ravendb.documents.session.misc import TransactionMode, SessionOptions from ravendb.documents.session.query import QueryStatistics diff --git a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_15825.py b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_15825.py index 215638b5..b08c8301 100644 --- a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_15825.py +++ b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_15825.py @@ -1,7 +1,7 @@ import random from typing import Optional, List, Dict, Callable -from ravendb.documents.indexes.index_creation import AbstractIndexCreationTask +from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractIndexCreationTask from ravendb.documents.queries.facets.misc import FacetResult, FacetOptions from ravendb.documents.queries.index_query import Parameters from ravendb.documents.queries.utils import HashCalculator diff --git a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_5669.py b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_5669.py index d3e134fa..3177458a 100644 --- a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_5669.py +++ b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_5669.py @@ -1,5 +1,5 @@ from ravendb.documents.indexes.definitions import FieldIndexing -from ravendb.documents.indexes.index_creation import AbstractIndexCreationTask +from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractIndexCreationTask from ravendb.tests.test_base import TestBase diff --git a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_6558.py b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_6558.py index b6d5153e..dd5288c3 100644 --- a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_6558.py +++ b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_6558.py @@ -2,7 +2,7 @@ from ravendb import HighlightingOptions from ravendb.documents.indexes.definitions import FieldIndexing, FieldStorage, FieldTermVector -from ravendb.documents.indexes.index_creation import AbstractMultiMapIndexCreationTask +from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractMultiMapIndexCreationTask from ravendb.documents.queries.highlighting import Highlightings from ravendb.tests.test_base import TestBase diff --git a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_903.py b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_903.py index 82a191db..06df6cbb 100644 --- a/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_903.py +++ b/ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_903.py @@ -1,7 +1,7 @@ from typing import Callable from ravendb.documents.indexes.definitions import FieldIndexing -from ravendb.documents.indexes.index_creation import AbstractIndexCreationTask +from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractIndexCreationTask from ravendb.documents.session.document_session import DocumentSession from ravendb.documents.session.query import DocumentQuery from ravendb.tests.test_base import TestBase diff --git a/ravendb/tests/jvm_migrated_tests/mailing_list_tests/test_lazy_aggregation_embedded.py b/ravendb/tests/jvm_migrated_tests/mailing_list_tests/test_lazy_aggregation_embedded.py index 3b5688db..18a3396d 100644 --- a/ravendb/tests/jvm_migrated_tests/mailing_list_tests/test_lazy_aggregation_embedded.py +++ b/ravendb/tests/jvm_migrated_tests/mailing_list_tests/test_lazy_aggregation_embedded.py @@ -1,4 +1,4 @@ -from ravendb.documents.indexes.index_creation import AbstractIndexCreationTask +from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractIndexCreationTask from ravendb.tests.test_base import TestBase diff --git a/ravendb/tests/jvm_migrated_tests/server_tests/patching/__init__.py b/ravendb/tests/jvm_migrated_tests/server_tests/patching/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ravendb/tests/jvm_migrated_tests/server_tests/patching/test_advanced_patching.py b/ravendb/tests/jvm_migrated_tests/server_tests/patching/test_advanced_patching.py new file mode 100644 index 00000000..df48824e --- /dev/null +++ b/ravendb/tests/jvm_migrated_tests/server_tests/patching/test_advanced_patching.py @@ -0,0 +1,107 @@ +from __future__ import annotations +from datetime import datetime +from typing import List, Any, Dict + +from ravendb import PatchRequest, PatchOperation, PatchStatus +from ravendb.tests.test_base import TestBase +from ravendb.tools.utils import Utils + +_SAMPLE_SCRIPT = ( + "this.comments.splice(2, 1);\n" + " this.owner = 'Something new';\n" + " this.value++;\n" + ' this.newValue = "err!!";\n' + " this.comments = this.comments.map(function(comment) {\n" + ' return (comment == "one") ? comment + " test" : comment;\n' + " });" +) + + +class CustomType: + def __init__( + self, Id: str = None, owner: str = None, value: int = None, comments: List[str] = None, date: datetime = None + ): + self.Id = Id + self.owner = owner + self.value = value + self.comments = comments + self.date = date + + def to_json(self) -> Dict[str, Any]: + return { + "Id": self.Id, + "owner": self.owner, + "value": self.value, + "comments": self.comments, + "date": Utils.datetime_to_string(self.date), + } + + @classmethod + def from_json(cls, json_dict: Dict[str, Any]) -> CustomType: + return cls( + json_dict["Id"], + json_dict["owner"], + json_dict["value"], + json_dict["comments"], + Utils.string_to_datetime(json_dict["date"]), + ) + + +class TestAdvancedPatching(TestBase): + def setUp(self): + super().setUp() + + def test_with_variables(self): + with self.store.open_session() as session: + custom_type = CustomType() + custom_type.owner = "me" + session.store(custom_type, "customTypes/1-A") + session.save_changes() + + patch_request = PatchRequest() + patch_request.script = "this.owner = args.v1" + patch_request.values = {"v1": "not-me"} + + patch_operation = PatchOperation("customTypes/1-A", None, patch_request) + self.store.operations.send(patch_operation) + + with self.store.open_session() as session: + loaded = session.load("customTypes/1-A", CustomType) + self.assertEqual("not-me", loaded.owner) + + def test_can_apply_basic_script_as_patch(self): + with self.store.open_session() as session: + test = CustomType("someId", "bob", 12143, ["one", "two", "seven"]) + session.store(test) + session.save_changes() + + self.store.operations.send(PatchOperation("someId", None, PatchRequest.for_script(_SAMPLE_SCRIPT))) + + with self.store.open_session() as session: + result = session.load("someId", CustomType) + + self.assertEqual("Something new", result.owner) + self.assertEqual(2, len(result.comments)) + self.assertEqual("one test", result.comments[0]) + self.assertEqual("two", result.comments[1]) + self.assertEqual(12144, result.value) + + def test_can_deserialize_modified_document(self): + custom_type = CustomType(owner="somebody@somewhere.com") + with self.store.open_session() as session: + session.store(custom_type, "doc") + session.save_changes() + + patch1 = PatchOperation("doc", None, PatchRequest.for_script("this.owner = '123';")) + + result = self.store.operations.send_patch_operation_with_entity_class(CustomType, patch1) + + self.assertEqual(PatchStatus.PATCHED, result.status) + self.assertEqual("123", result.document.owner) + + patch2 = PatchOperation("doc", None, PatchRequest.for_script("this.owner = '123';")) + + result = self.store.operations.send_patch_operation_with_entity_class(CustomType, patch2) + + self.assertEqual(PatchStatus.NOT_MODIFIED, result.status) + self.assertEqual("123", result.document.owner) diff --git a/ravendb/tests/jvm_migrated_tests/server_tests/test_expiration_configuration.py b/ravendb/tests/jvm_migrated_tests/server_tests/test_expiration_configuration.py new file mode 100644 index 00000000..0d8114c6 --- /dev/null +++ b/ravendb/tests/jvm_migrated_tests/server_tests/test_expiration_configuration.py @@ -0,0 +1,19 @@ +import unittest + +from ravendb import ExpirationConfiguration +from ravendb.documents.operations.expiration.operations import ConfigureExpirationOperation +from ravendb.tests.test_base import TestBase + + +class TestExpirationConfiguration(TestBase): + def setUp(self): + super().setUp() + + @unittest.skip("License on ci/cd") + def test_can_setup_expiration(self): + expiration_configuration = ExpirationConfiguration(False, 5) + configure_operation = ConfigureExpirationOperation(expiration_configuration) + + expiration_operation_result = self.store.maintenance.send(configure_operation) + + self.assertIsNotNone(expiration_operation_result.raft_command_index) diff --git a/ravendb/tests/jvm_migrated_tests/server_tests/test_promote_database.py b/ravendb/tests/jvm_migrated_tests/server_tests/test_promote_database.py new file mode 100644 index 00000000..f682e3ff --- /dev/null +++ b/ravendb/tests/jvm_migrated_tests/server_tests/test_promote_database.py @@ -0,0 +1,13 @@ +from ravendb.serverwide.operations.common import PromoteDatabaseNodeOperation +from ravendb.tests.test_base import TestBase + + +class TestPromoteDatabase(TestBase): + def setUp(self): + super().setUp() + + def test_can_send_promote_database_command(self): + operation = PromoteDatabaseNodeOperation(self.store.database, "A") + self.store.maintenance.server.send(operation) + + # since we are running single node cluster we cannot assert much diff --git a/ravendb/tests/jvm_migrated_tests/serverwide_tests/__init__.py b/ravendb/tests/jvm_migrated_tests/serverwide_tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ravendb/tests/jvm_migrated_tests/serverwide_tests/test_commands.py b/ravendb/tests/jvm_migrated_tests/serverwide_tests/test_commands.py new file mode 100644 index 00000000..52480726 --- /dev/null +++ b/ravendb/tests/jvm_migrated_tests/serverwide_tests/test_commands.py @@ -0,0 +1,17 @@ +from ravendb.serverwide.commands import GetTcpInfoCommand +from ravendb.tests.test_base import TestBase + + +class TestGetTcpInfo(TestBase): + def setUp(self): + super().setUp() + + def test_can_get_tcp_info(self): + command = GetTcpInfoCommand("test") + self.store.get_request_executor().execute_command(command) + result = command.result + + self.assertIsNotNone(result) + self.assertIsNone(result.certificate) + self.assertIsNotNone(result.port) + self.assertIsNotNone(result.url) diff --git a/ravendb/tests/session_tests/test_full_text_search.py b/ravendb/tests/session_tests/test_full_text_search.py index cc4d5e1c..4ae9f963 100644 --- a/ravendb/tests/session_tests/test_full_text_search.py +++ b/ravendb/tests/session_tests/test_full_text_search.py @@ -1,5 +1,5 @@ from ravendb.documents.indexes.definitions import FieldIndexing -from ravendb.documents.indexes.index_creation import AbstractIndexCreationTask +from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractIndexCreationTask from ravendb.documents.queries.misc import SearchOperator from ravendb.tests.test_base import TestBase from datetime import datetime diff --git a/ravendb/tests/test_base.py b/ravendb/tests/test_base.py index ad52df1b..9162daba 100644 --- a/ravendb/tests/test_base.py +++ b/ravendb/tests/test_base.py @@ -357,18 +357,16 @@ def wait_for_indexing( timestamp = datetime.datetime.now() while datetime.datetime.now() - timestamp < timeout: database_statistics = admin.send(GetStatisticsOperation("wait-for-indexing", node_tag)) - indexes = list( - filter(lambda index: index["State"] != str(IndexState.DISABLED), database_statistics["Indexes"]) - ) + indexes = list(filter(lambda index: index.state != str(IndexState.DISABLED), database_statistics.indexes)) if all( [ - not index["IsStale"] - and not index["Name"].startswith(constants.Documents.Indexing.SIDE_BY_SIDE_INDEX_NAME_PREFIX) + not index.stale + and not index.name.startswith(constants.Documents.Indexing.SIDE_BY_SIDE_INDEX_NAME_PREFIX) for index in indexes ] ): return - if any([IndexState.ERROR == index["State"] for index in indexes]): + if any([IndexState.ERROR == index.state for index in indexes]): break try: time.sleep(0.1) diff --git a/ravendb/tools/utils.py b/ravendb/tools/utils.py index bcf5cba0..168d9d25 100644 --- a/ravendb/tools/utils.py +++ b/ravendb/tools/utils.py @@ -651,9 +651,15 @@ def dict_to_string(dictionary): return ",".join(item for item in builder) @staticmethod - def datetime_to_string(datetime_obj: datetime): + def datetime_to_string(datetime_obj: datetime, return_none_if_none: bool = True): add_suffix = "0" if datetime_obj != datetime.max else "9" - return datetime_obj.strftime(f"%Y-%m-%dT%H:%M:%S.%f{add_suffix}") if datetime_obj else "" + return ( + datetime_obj.strftime(f"%Y-%m-%dT%H:%M:%S.%f{add_suffix}") + if datetime_obj + else None + if return_none_if_none + else "" + ) @staticmethod def start_a_timer(interval, function, args=None, name=None, daemon=False):