From a92dea0b54c7cbf4d7f5d568e7a111e8235e04d8 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz <25389585+poissoncorp@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:52:53 +0200 Subject: [PATCH 1/2] RDBC-1156 Add IndexCompilationException and raise it for index compilation errors --- ravendb/__init__.py | 2 + .../indexes/abstract_index_creation_tasks.py | 3 +- ravendb/exceptions/compilation.py | 5 + ravendb/exceptions/documents/compilation.py | 12 ++ ravendb/exceptions/exception_dispatcher.py | 8 + .../test_ravenDB_22083.py | 196 ++++++++++++++++++ 6 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 ravendb/exceptions/compilation.py create mode 100644 ravendb/exceptions/documents/compilation.py create mode 100644 ravendb/tests/dotnet_migrated_tests/test_ravenDB_22083.py diff --git a/ravendb/__init__.py b/ravendb/__init__.py index b4e6c79b..484aff97 100644 --- a/ravendb/__init__.py +++ b/ravendb/__init__.py @@ -669,6 +669,8 @@ TimeSeriesRangeResult, TimeSeriesStatistics, ) +from ravendb.exceptions.compilation import CompilationException +from ravendb.exceptions.documents.compilation import IndexCompilationException from ravendb.exceptions.raven_exceptions import ( AiException, BadResponseException, diff --git a/ravendb/documents/indexes/abstract_index_creation_tasks.py b/ravendb/documents/indexes/abstract_index_creation_tasks.py index 66acf7f6..0a206d84 100644 --- a/ravendb/documents/indexes/abstract_index_creation_tasks.py +++ b/ravendb/documents/indexes/abstract_index_creation_tasks.py @@ -22,6 +22,7 @@ from ravendb.documents.indexes.vector.options import VectorOptions from ravendb.documents.operations.indexes import PutIndexesOperation from ravendb.documents.store.definition import DocumentStoreBase +from ravendb.exceptions.documents.compilation import IndexCompilationException from ravendb.primitives import constants _T_IndexDefinition = TypeVar("_T_IndexDefinition", bound=IndexDefinition) @@ -268,7 +269,7 @@ def __set_suggestions(options: IndexFieldOptions, value: bool): return index_definition except Exception as e: - raise RuntimeError(f"Failed to create index {self._index_name}", e) # todo: IndexCompilationException + raise IndexCompilationException(f"Failed to create index {self._index_name}", e) class IndexDefinitionBuilder(AbstractIndexDefinitionBuilder[IndexDefinition]): diff --git a/ravendb/exceptions/compilation.py b/ravendb/exceptions/compilation.py new file mode 100644 index 00000000..f0691364 --- /dev/null +++ b/ravendb/exceptions/compilation.py @@ -0,0 +1,5 @@ +from ravendb.exceptions.raven_exceptions import RavenException + + +class CompilationException(RavenException): + pass diff --git a/ravendb/exceptions/documents/compilation.py b/ravendb/exceptions/documents/compilation.py new file mode 100644 index 00000000..d2e563dc --- /dev/null +++ b/ravendb/exceptions/documents/compilation.py @@ -0,0 +1,12 @@ +from typing import Optional + +from ravendb.exceptions.compilation import CompilationException + + +class IndexCompilationException(CompilationException): + def __init__(self, message: str = None, cause: BaseException = None): + super().__init__(message, cause) + # Index definition property that caused the error (Maps, Reduce) + self.index_definition_property: Optional[str] = None + # Value of the problematic property + self.problematic_text: Optional[str] = None diff --git a/ravendb/exceptions/exception_dispatcher.py b/ravendb/exceptions/exception_dispatcher.py index 6fbf1a39..b940df8f 100644 --- a/ravendb/exceptions/exception_dispatcher.py +++ b/ravendb/exceptions/exception_dispatcher.py @@ -7,6 +7,8 @@ from ravendb.exceptions.cluster import NodeIsPassiveException, NoLeaderException from ravendb.exceptions.commercial import LicenseLimitException from ravendb.exceptions.documents import DocumentConflictException, DocumentDoesNotExistException +from ravendb.exceptions.compilation import CompilationException +from ravendb.exceptions.documents.compilation import IndexCompilationException from ravendb.exceptions.documents.bulkinsert import BulkInsertAbortedException, BulkInsertProtocolViolationException from ravendb.exceptions.documents.indexes import IndexDoesNotExistException from ravendb.exceptions.raven_exceptions import ( @@ -56,6 +58,9 @@ "IndexDoesNotExistException": IndexDoesNotExistException, "BulkInsertAbortedException": BulkInsertAbortedException, "BulkInsertProtocolViolationException": BulkInsertProtocolViolationException, + # compilation + "CompilationException": CompilationException, + "IndexCompilationException": IndexCompilationException, # schema validation "SchemaValidationException": SchemaValidationException, # replication @@ -131,6 +136,9 @@ def __fill_exception(exception: RavenException, data: dict) -> None: exception.status_code = int(status_code) except Exception: pass + elif isinstance(exception, IndexCompilationException): + exception.index_definition_property = data.get("IndexDefinitionProperty") + exception.problematic_text = data.get("ProblematicText") elif isinstance(exception, RefusedToAnswerException): exception.refusal = data.get("Refusal") exception.finish_reason = data.get("FinishReason") diff --git a/ravendb/tests/dotnet_migrated_tests/test_ravenDB_22083.py b/ravendb/tests/dotnet_migrated_tests/test_ravenDB_22083.py new file mode 100644 index 00000000..87a6d3ed --- /dev/null +++ b/ravendb/tests/dotnet_migrated_tests/test_ravenDB_22083.py @@ -0,0 +1,196 @@ +import http +import unittest + +from ravendb import AbstractIndexCreationTask, IndexDefinition, PutIndexesOperation +from ravendb.exceptions.compilation import CompilationException +from ravendb.exceptions.documents.compilation import IndexCompilationException +from ravendb.exceptions.exception_dispatcher import ExceptionDispatcher +from ravendb.exceptions.raven_exceptions import RavenException +from ravendb.tests.test_base import TestBase + + +class Candle: + def __init__(self, Id: str = None, volume: float = None, time: int = None): + self.Id = Id + self.volume = volume + self.time = time + + +class AggregateCandle: + def __init__(self, ref: str = None, volume: float = None, timeframe: str = None): + self.ref = ref + self.volume = volume + self.timeframe = timeframe + + +class IndexWithConstantArrayAsInnerSource(AbstractIndexCreationTask): + def __init__(self): + super().__init__() + self.map = ( + "from candle in docs.Candles " + 'from x in new[] { new { Interval = 60000L, Timeframe = "1m" }, ' + 'new { Interval = 300000L, Timeframe = "5m" } } ' + 'select new { ref = $"{candle.time / x.Interval}", volume = candle.volume, timeframe = x.Timeframe }' + ) + self.reduce = ( + "from result in results " + "group result by new { result.timeframe, result.ref } into g " + "select new { ref = g.Key.ref, volume = g.Sum(x => x.volume), timeframe = g.Key.timeframe }" + ) + + +class IndexWithConstantArrayAsOuterSource(AbstractIndexCreationTask): + def __init__(self): + super().__init__() + self.map = ( + 'from x in new[] { new { Interval = 60000L, Timeframe = "1m" }, ' + 'new { Interval = 300000L, Timeframe = "5m" } } ' + "from candle in docs.Candles " + 'select new { ref = $"{candle.time / x.Interval}", volume = candle.volume, timeframe = x.Timeframe }' + ) + + +class IndexWithOrderedReduce(AbstractIndexCreationTask): + def __init__(self): + super().__init__() + self.map = ( + "from candle in docs.Candles " + 'select new { ref = $"{candle.time / 60000}", volume = candle.volume, timeframe = "1m" }' + ) + self.reduce = ( + "results.GroupBy(x => new { x.timeframe, x.ref })" + ".Select(g => new { ref = g.Key.ref, volume = g.Sum(x => x.volume), timeframe = g.Key.timeframe })" + ".OrderBy(x => x.ref)" + ) + + +class TestIndexCompilationExceptionDispatch(unittest.TestCase): + def _dispatch(self, type_as_string: str, json_body: dict = None) -> RavenException: + schema = ExceptionDispatcher.ExceptionSchema( + url="http://localhost:8080", + object_type=type_as_string, + message="Failed to compile index 'Index1'", + error="Failed to compile index 'Index1'", + ) + return ExceptionDispatcher.get(schema, http.HTTPStatus.INTERNAL_SERVER_ERROR, json_body=json_body) + + def test_index_compilation_exception_is_typed_and_filled(self): + exception = self._dispatch( + "Raven.Client.Exceptions.Documents.Compilation.IndexCompilationException", + {"IndexDefinitionProperty": "Maps", "ProblematicText": "from x in items select x"}, + ) + + self.assertIsInstance(exception, IndexCompilationException) + self.assertIsInstance(exception, CompilationException) + self.assertEqual("Maps", exception.index_definition_property) + self.assertEqual("from x in items select x", exception.problematic_text) + self.assertIn("Failed to compile index 'Index1'", str(exception)) + + def test_index_compilation_exception_without_body(self): + exception = self._dispatch("Raven.Client.Exceptions.Documents.Compilation.IndexCompilationException") + + self.assertIsInstance(exception, IndexCompilationException) + self.assertIsNone(exception.index_definition_property) + self.assertIsNone(exception.problematic_text) + + def test_compilation_exception_is_typed(self): + exception = self._dispatch("Raven.Client.Exceptions.Compilation.CompilationException") + + self.assertIs(CompilationException, type(exception)) + + +class TestRavenDB22083(TestBase): + + def _store_candles(self): + with self.store.open_session() as session: + session.store(Candle(volume=1, time=60000)) + session.store(Candle(volume=2, time=60000)) + session.save_changes() + + def test_index_with_constant_array_as_inner_source_deploys_and_indexes(self): + self.store.execute_index(IndexWithConstantArrayAsInnerSource()) + self._store_candles() + self.wait_for_indexing(self.store) + + with self.store.open_session() as session: + results = list( + session.query_index_type(IndexWithConstantArrayAsInnerSource, AggregateCandle).where_equals( + "timeframe", "1m" + ) + ) + + self.assertEqual(1, len(results)) + self.assertEqual(3, results[0].volume) + + def test_index_with_ordered_reduce_deploys_and_indexes(self): + self.store.execute_index(IndexWithOrderedReduce()) + self._store_candles() + self.wait_for_indexing(self.store) + + with self.store.open_session() as session: + results = list(session.query_index_type(IndexWithOrderedReduce, AggregateCandle)) + + self.assertEqual(1, len(results)) + self.assertEqual(3, results[0].volume) + + def test_index_with_constant_array_as_outer_source_is_rejected_at_compilation(self): + with self.assertRaises(IndexCompilationException) as context: + self.store.execute_index(IndexWithConstantArrayAsOuterSource()) + + self.assertEqual("Maps", context.exception.index_definition_property) + self.assertIn("a C# map must start its enumeration from one of these sources", str(context.exception)) + + def test_index_definition_sent_as_text_with_constant_array_as_outer_source_throws(self): + index_definition = IndexDefinition() + index_definition.name = "CandlesByTimeframeFromText" + index_definition.maps = { + 'from x in new[] { new { Timeframe = "1m", Interval = 60000L }, ' + 'new { Timeframe = "5m", Interval = 300000L } } ' + "from candle in docs.Candles " + 'select new { Ref = $"{candle.Time / x.Interval}", Volume = candle.Volume, Timeframe = x.Timeframe }' + } + + with self.assertRaises(IndexCompilationException) as context: + self.store.maintenance.send(PutIndexesOperation(index_definition)) + + self.assertIn("Failed to compile index 'CandlesByTimeframeFromText'", str(context.exception)) + self.assertIn("must start its enumeration from 'docs'", str(context.exception)) + self.assertEqual("Maps", context.exception.index_definition_property) + + def test_index_definition_sent_as_text_not_rooted_in_documents_source_throws(self): + index_definition = IndexDefinition() + index_definition.name = "CandlesFromMethodSyntaxText" + index_definition.maps = { + "new[] { 60000L, 300000L }.SelectMany(t => docs.Candles, " + "(t, candle) => new { Ref = candle.Time / t, Volume = candle.Volume })" + } + + with self.assertRaises(IndexCompilationException) as context: + self.store.maintenance.send(PutIndexesOperation(index_definition)) + + self.assertEqual("Maps", context.exception.index_definition_property) + self.assertIn("compiled as JavaScript", str(context.exception)) + self.assertIn("a C# map must start its enumeration from one of these sources", str(context.exception)) + + def test_javascript_index_with_invalid_map_throws(self): + index_definition = IndexDefinition() + index_definition.name = "BrokenJsMap" + index_definition.maps = {"map('Candles', function (c) { return { Volume: c.Volume }; )"} + + with self.assertRaises(IndexCompilationException) as context: + self.store.maintenance.send(PutIndexesOperation(index_definition)) + + self.assertEqual("Maps", context.exception.index_definition_property) + + def test_javascript_index_with_invalid_reduce_throws(self): + index_definition = IndexDefinition() + index_definition.name = "BrokenJsReduce" + index_definition.maps = {"map('Candles', function (c) { return { Volume: c.Volume }; })"} + index_definition.reduce = ( + "groupBy(x => x.Volume).aggregate(g => { return { Volume: g.values.reduce((a, b) => a + b.Volume, 0) }; )" + ) + + with self.assertRaises(IndexCompilationException) as context: + self.store.maintenance.send(PutIndexesOperation(index_definition)) + + self.assertEqual("Reduce", context.exception.index_definition_property) From d43a14eff5689f0ae9bbb32209b961026b1e5135 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz <25389585+poissoncorp@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:21:43 +0200 Subject: [PATCH 2/2] RDBC-1156 Fix RavenDB_22083 tests: avoid the C# keyword ref as a field name, expect the server message for a query-syntax map --- .../test_ravenDB_22083.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ravendb/tests/dotnet_migrated_tests/test_ravenDB_22083.py b/ravendb/tests/dotnet_migrated_tests/test_ravenDB_22083.py index 87a6d3ed..394fc8e7 100644 --- a/ravendb/tests/dotnet_migrated_tests/test_ravenDB_22083.py +++ b/ravendb/tests/dotnet_migrated_tests/test_ravenDB_22083.py @@ -17,8 +17,8 @@ def __init__(self, Id: str = None, volume: float = None, time: int = None): class AggregateCandle: - def __init__(self, ref: str = None, volume: float = None, timeframe: str = None): - self.ref = ref + def __init__(self, reference: str = None, volume: float = None, timeframe: str = None): + self.reference = reference self.volume = volume self.timeframe = timeframe @@ -30,12 +30,12 @@ def __init__(self): "from candle in docs.Candles " 'from x in new[] { new { Interval = 60000L, Timeframe = "1m" }, ' 'new { Interval = 300000L, Timeframe = "5m" } } ' - 'select new { ref = $"{candle.time / x.Interval}", volume = candle.volume, timeframe = x.Timeframe }' + 'select new { reference = $"{candle.time / x.Interval}", volume = candle.volume, timeframe = x.Timeframe }' ) self.reduce = ( "from result in results " - "group result by new { result.timeframe, result.ref } into g " - "select new { ref = g.Key.ref, volume = g.Sum(x => x.volume), timeframe = g.Key.timeframe }" + "group result by new { result.timeframe, result.reference } into g " + "select new { reference = g.Key.reference, volume = g.Sum(x => x.volume), timeframe = g.Key.timeframe }" ) @@ -46,7 +46,7 @@ def __init__(self): 'from x in new[] { new { Interval = 60000L, Timeframe = "1m" }, ' 'new { Interval = 300000L, Timeframe = "5m" } } ' "from candle in docs.Candles " - 'select new { ref = $"{candle.time / x.Interval}", volume = candle.volume, timeframe = x.Timeframe }' + 'select new { reference = $"{candle.time / x.Interval}", volume = candle.volume, timeframe = x.Timeframe }' ) @@ -55,12 +55,12 @@ def __init__(self): super().__init__() self.map = ( "from candle in docs.Candles " - 'select new { ref = $"{candle.time / 60000}", volume = candle.volume, timeframe = "1m" }' + 'select new { reference = $"{candle.time / 60000}", volume = candle.volume, timeframe = "1m" }' ) self.reduce = ( - "results.GroupBy(x => new { x.timeframe, x.ref })" - ".Select(g => new { ref = g.Key.ref, volume = g.Sum(x => x.volume), timeframe = g.Key.timeframe })" - ".OrderBy(x => x.ref)" + "results.GroupBy(x => new { x.timeframe, x.reference })" + ".Select(g => new { reference = g.Key.reference, volume = g.Sum(x => x.volume), timeframe = g.Key.timeframe })" + ".OrderBy(x => x.reference)" ) @@ -138,7 +138,7 @@ def test_index_with_constant_array_as_outer_source_is_rejected_at_compilation(se self.store.execute_index(IndexWithConstantArrayAsOuterSource()) self.assertEqual("Maps", context.exception.index_definition_property) - self.assertIn("a C# map must start its enumeration from one of these sources", str(context.exception)) + self.assertIn("must start its enumeration from 'docs'", str(context.exception)) def test_index_definition_sent_as_text_with_constant_array_as_outer_source_throws(self): index_definition = IndexDefinition()