Skip to content

Commit a948ef5

Browse files
authored
ADR 033: UnsupportedType (#1241)
Bolt 6 introduces a new data type representing a value of a type that the server cannot transmit to the client due to the negotiation of an incompatible bolt protocol version.
1 parent 187b4be commit a948ef5

File tree

13 files changed

+368
-2
lines changed

13 files changed

+368
-2
lines changed

docs/source/index.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ Topics
3939

4040
+ :ref:`vector-data-types`
4141

42+
+ :ref:`other-data-types`
43+
4244
+ :ref:`breaking-changes`
4345

4446

@@ -51,6 +53,7 @@ Topics
5153
types/spatial.rst
5254
types/temporal.rst
5355
types/vector.rst
56+
types/other.rst
5457
breaking_changes.rst
5558

5659

docs/source/types/other.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.. _other-data-types:
2+
3+
***********************
4+
Other Driver Data Types
5+
***********************
6+
7+
================
8+
Unsupported Type
9+
================
10+
11+
.. autoclass:: neo4j.types.UnsupportedType
12+
:members:

src/neo4j/_codec/hydration/v3/hydration_handler.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@
4545
)
4646
from ..v1.hydration_handler import _GraphHydrator
4747
from ..v2 import temporal as temporal_v2
48-
from . import vector
48+
from . import (
49+
unsupported,
50+
vector,
51+
)
4952

5053

5154
class HydrationHandler(HydrationHandlerABC): # type: ignore[no-redef]
@@ -64,6 +67,7 @@ def __init__(self):
6467
b"d": temporal_v2.hydrate_datetime, # no time zone
6568
b"E": temporal_v1.hydrate_duration,
6669
b"V": vector.hydrate_vector,
70+
b"?": unsupported.hydrate_unsupported,
6771
}
6872
self.dehydration_hooks.update(
6973
exact_types={
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright (c) "Neo4j"
2+
# Neo4j Sweden AB [https://neo4j.com]
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
17+
from .... import _typing as t
18+
from ....types import UnsupportedType
19+
20+
21+
def hydrate_unsupported(
22+
name: str,
23+
min_bolt_major: int,
24+
min_bolt_minor: int,
25+
extra: dict[str, t.Any],
26+
) -> UnsupportedType:
27+
"""
28+
Hydrator for `UnsupportedType` values.
29+
30+
:param name: name of the type
31+
:param min_bolt_major: minimum major version of the Bolt protocol
32+
supporting this type
33+
:param min_bolt_minor: minimum minor version of the Bolt protocol
34+
supporting this type
35+
:param extra: dict containing optional "message" key
36+
:returns: UnsupportedType instance
37+
"""
38+
return UnsupportedType._new(
39+
name,
40+
(min_bolt_major, min_bolt_minor),
41+
extra.get("message"),
42+
)

src/neo4j/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ def protocol_version(self) -> tuple[int, int]:
296296
"""
297297
Bolt protocol version with which the remote server communicates.
298298
299-
This is returned as a 2-tuple:class:`tuple` of ``(major, minor)``
299+
This is returned as a 2-:class:`tuple` of ``(major, minor)``
300300
integers.
301301
"""
302302
return self._protocol_version

src/neo4j/types/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright (c) "Neo4j"
2+
# Neo4j Sweden AB [https://neo4j.com]
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from ._unsupported import UnsupportedType
17+
18+
19+
__all__ = [
20+
"UnsupportedType",
21+
]

src/neo4j/types/_unsupported.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Copyright (c) "Neo4j"
2+
# Neo4j Sweden AB [https://neo4j.com]
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
17+
from __future__ import annotations
18+
19+
from .. import _typing as t # noqa: TC001
20+
21+
22+
class UnsupportedType:
23+
"""
24+
Represents a type unknown to the driver, received from the server.
25+
26+
This type is used for instance when a newer DBMS produces a result
27+
containing a type that the current version of the driver does not yet
28+
understand.
29+
30+
Note that this type may only be received from the server, but cannot be
31+
sent to the server (e.g., as a query parameter).
32+
33+
The attributes exposed by this type are meant for displaying and debugging
34+
purposes.
35+
They may change in future versions of the server, and should not be relied
36+
upon for any logic in your application.
37+
If your application requires handling this type, you must upgrade your
38+
driver to a version that supports it.
39+
"""
40+
41+
_name: str
42+
_minimum_protocol_version: tuple[int, int]
43+
_message: str | None
44+
45+
@classmethod
46+
def _new(
47+
cls,
48+
name: str,
49+
minimum_protocol_version: tuple[int, int],
50+
message: str | None,
51+
) -> t.Self:
52+
obj = cls.__new__(cls)
53+
obj._name = name
54+
obj._minimum_protocol_version = minimum_protocol_version
55+
obj._message = message
56+
return obj
57+
58+
@property
59+
def name(self) -> str:
60+
"""The name of the type."""
61+
return self._name
62+
63+
@property
64+
def minimum_protocol_version(self) -> tuple[int, int]:
65+
"""
66+
The minimum required Bolt protocol version that supports this type.
67+
68+
This is a 2-:class:`tuple` of ``(major, minor)`` integers.
69+
70+
To understand which driver version this corresponds to, refer to the
71+
driver's release notes or documentation.
72+
73+
74+
.. seealso::
75+
<
76+
link to evolving doc listing which version of the driver
77+
supports which Bolt version
78+
>
79+
"""
80+
# TODO fix link above
81+
return self._minimum_protocol_version
82+
83+
@property
84+
def message(self) -> str | None:
85+
"""
86+
Optional, further details about this type.
87+
88+
Any additional information provided by the server about this type.
89+
"""
90+
return self._message
91+
92+
def __str__(self) -> str:
93+
return f"{self.__class__.__name__}<{self._name}>"
94+
95+
def __repr__(self) -> str:
96+
args = [
97+
f" name={self._name!r}",
98+
f" minimum_protocol_version={self._minimum_protocol_version!r}",
99+
]
100+
if self._message is not None:
101+
args.append(f" message={self._message!r}")
102+
return f"<{self.__class__.__name__}{''.join(args)}>"

testkitbackend/test_config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"Feature:API:Summary:GqlStatusObjects": true,
4040
"Feature:API:Type.Spatial": true,
4141
"Feature:API:Type.Temporal": true,
42+
"Feature:API:Type.UnsupportedType": true,
4243
"Feature:API:Type.Vector": true,
4344
"Feature:Auth:Bearer": true,
4445
"Feature:Auth:Custom": true,

testkitbackend/totestkit.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
Duration,
4040
Time,
4141
)
42+
from neo4j.types import UnsupportedType
4243
from neo4j.vector import Vector
4344

4445
from ._warning_check import warning_check
@@ -310,6 +311,20 @@ def to(name, val):
310311
"data": " ".join(f"{byte:02x}" for byte in v.raw()),
311312
},
312313
}
314+
if isinstance(v, UnsupportedType):
315+
data = {
316+
"name": v.name,
317+
"minimumProtocol": (
318+
f"{v.minimum_protocol_version[0]}"
319+
f".{v.minimum_protocol_version[1]}"
320+
),
321+
}
322+
if v.message is not None:
323+
data["message"] = v.message
324+
return {
325+
"name": "CypherUnsupportedType",
326+
"data": data,
327+
}
313328

314329
raise ValueError("Unhandled type:" + str(type(v)))
315330

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright (c) "Neo4j"
2+
# Neo4j Sweden AB [https://neo4j.com]
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
17+
import pytest
18+
19+
from neo4j._codec.hydration.v3 import HydrationHandler
20+
from neo4j.types import UnsupportedType
21+
22+
from .._base import HydrationHandlerTestBase
23+
24+
25+
class TestUnsupportedTypeDehydration(HydrationHandlerTestBase):
26+
@pytest.fixture
27+
def hydration_handler(self):
28+
return HydrationHandler()
29+
30+
def test_has_no_transformer(self, hydration_scope):
31+
value = UnsupportedType._new("UUID", (255, 255), None)
32+
33+
transformer = hydration_scope.dehydration_hooks.get_transformer(value)
34+
35+
assert transformer is None

0 commit comments

Comments
 (0)