结构语义

PackStream 定义了结构体的外观,但未定义其含义。结构体的语义与 Bolt 协议版本绑定。

下表列出了 PackStream 指定的结构体,以及它们在所有当前存在的 Bolt 协议版本中的代码和标签字节。

结构体

结构体名称 代码 标签字节

Node

N

4E

Relationship

R

52

UnboundRelationship

r

72

Path

P

50

Date

D

44

Time

T

54

LocalTime

t

74

DateTime

I

49

DateTimeZoneId

i

69

LocalDateTime

d

64

Duration

E

45

Point2D

X

58

Point3D

Y

59

旧版结构体

旧版日期时间

F

46

旧版日期时间(时区ID)

f

66

Node

图数据库中节点的快照。

`element_id` 字段是在 **5.0** 版本中添加的,在早期版本中不存在。

**标签字节:** `4E`

**字段数量:** 4 个 *(在 **5.0** 版本之前为 3 个)*

Node::Structure(
    id::Integer,
    labels::List<String>,
    properties::Dictionary,
    element_id::String,
)
节点结构体示例
Node(
  id = 3,
  labels = ["Example", "Node"],
  properties = {"name": "example"},
  element_id = "abc123",
)
B4 4E
...

Relationship

图数据库中关系的快照。

`element_id`、`start_node_element_id` 和 `end_node_element_id` 字段是在 **5.0** 版本中添加的,在早期版本中不存在。

**标签字节:** `52`

**字段数量:** 8 个 *(在 **5.0** 版本之前为 5 个)*

Relationship::Structure(
    id::Integer,
    startNodeId::Integer,
    endNodeId::Integer,
    type::String,
    properties::Dictionary,
    element_id::String,
    start_node_element_id::String,
    end_node_element_id::String,
)
关系结构体示例
Relationship(
    id = 11,
    startNodeId = 2,
    endNodeId = 3,
    type = "KNOWS",
    properties = {"name": "example"},
    element_id = "abc123",
    start_node_element_id = "def456",
    end_node_element_id = "ghi789",
)
B8 52
...

UnboundRelationship

不带起始或结束节点 ID 的关系。它在内部用于路径序列化。

`element_id` 字段是在 **5.0** 版本中添加的,在早期版本中不存在。

**标签字节:** `72`

**字段数量:** 4 个 *(在 **5.0** 版本之前为 3 个)*

UnboundRelationship::Structure(
    id::Integer,
    type::String,
    properties::Dictionary,
    element_id::String,
)
未绑定关系结构体示例
UnboundRelationship(
    id = 17,
    type = "KNOWS",
    properties = {"name": "example"},
    element_id = "foo"
)
B4 72
...

Path

节点和关系的交替序列。

**标签字节:** `50`

字段数量 3

Path::Structure(
    nodes::List<Node>,
    rels::List<UnboundRelationship>,
    indices::List<Integer>,
)

其中 `nodes` 字段包含节点列表,`rels` 字段是未绑定关系的列表。`indices` 是一个整数列表,描述了如何从 `nodes` 和 `rels` 构建路径。`nodes` 中的第一个节点始终是路径中的第一个节点,并且在 `indices` 中不被引用。`indices` 始终具有偶数个条目。`indices` 中第 1、3、……个条目引用 `rels` 中的一个条目(1-索引),例如,`3` 将引用 `rels` 的第 3 个元素。该数字也可以是负数,应将其视为正数,但表示关系方向相反。该数字永远不是 `0`。`indices` 中第 2、4、……个条目引用 `nodes` 中的一个条目(0-索引),例如,`3` 将引用 `nodes` 的第 4 个元素。该数字始终 `≥ 0`。

示例(`<Node>` 和 `<UnboundRelationship>` 的简化表示法)
Path::Structure(
    nodes: [Node::Structure(42, ...), Node::Structure(69, ...), Node::Structure(1, ...)],
    rels: [UnboundRelationship::Structure(1000, ...), UnboundRelationship::Structure(1001, ...)],
    indices: [1, 1, 1, 0, -2, 2],

这表示路径 `(42)-[1000]→(69)-[1000]→(42)←[1001]-(1)`,其中 `(n)` 表示 ID 为 `n` 的节点,`[n]` 表示 ID 为 `n` 的关系(`→` 或 `←` 表示每个关系的方向)。

Date

不带时区的 ISO-8601 日历系统中的日期,例如 `2007-12-03`。

**标签字节:** `44`

字段数量 1

Date::Structure(
    days::Integer,
)

其中 `days` 是自 Unix 纪元以来的天数。例如,`0` 表示 1970-01-01,而 `1` 表示 1970-01-02。

Time

捕获一天中的时间以及时区,但不包括日期的即时点。

**标签字节:** `54`

字段数量 2

Time::Structure(
    nanoseconds::Integer,
    tz_offset_seconds::Integer,
)

其中 `nanoseconds` 是自午夜以来的纳秒数(此时间*非* UTC),`tz_offset_seconds` 是与 UTC 的秒数偏移量。

LocalTime

捕获一天中的时间,但不包括日期或时区的即时点。

**标签字节:** `74`

字段数量 1

LocalTime::Structure(
    nanoseconds::Integer,
)

其中 `nanoseconds` 是自午夜以来的纳秒数。

DateTime

捕获日期、时间和时区的即时点。时区信息通过区域偏移量指定。

*此结构体是 **5.0** 版本中新增的。* 它取代了`旧版日期时间`并修复了某些边缘情况下的错误。如果服务器和客户端协商其使用(参见`HELLO` 消息),**4.4** 版本也允许使用此固定结构体。

**标签字节:** `49`

字段数量 3

DateTime::Structure(
    seconds::Integer,
    nanoseconds::Integer,
    tz_offset_seconds::Integer,
)
  • `seconds` 和 `nanoseconds` 是自 Unix 纪元以来的时间,通常被称为 Unix 时间戳。

  • `nanoseconds` 的数量范围从 `0` 到 `999_999_999`(为了清晰起见,此处及后续添加了 `_` 分隔符)。

  • `tz_offset_seconds` 指定了与 UTC 的秒数偏移量。

例如,表示为 `1970-01-01T02:15:00.000000042+01:00` 的时间点的序列化可以按如下方式实现

  • 计算 UTC 时间,即 `1970-01-01T01:15:00.000000042Z`(`Z` 表示 UTC)。

  • 计算该 UTC 时间与 Unix 纪元之间的差值,即 1 小时 15 分钟,也就是 `4_500` 秒。

因此,生成的 `DateTime` 实例如下

{
  seconds: 4500
  nanoseconds: 42,
  tz_offset_seconds: 3600
}

这种 `DateTime` 结构体的反序列化预期以相反的方式进行

  • 根据该 Unix 时间戳(`4500` 秒和 `42` 纳秒)实例化 `DateTime` 的习惯性等效对象,得到 `1970-01-01T01:15:00.000000042Z`

  • 将生成的 UTC `DateTime` 本地化到指定偏移量的时区,得到 `1970-01-01T02:15:00.000000042+0100`

DateTimeZoneId

捕获日期、时间和时区的即时点。时区信息通过区域标识符指定。

*此结构体是 **5.0** 版本中新增的。* 它取代了`旧版日期时间(时区ID)`并修复了某些边缘情况下的错误。如果服务器和客户端协商其使用(参见`HELLO` 消息),**4.4** 版本也允许使用此固定结构体。

**标签字节:** `69`

字段数量 3

DateTimeZoneId::Structure(
    seconds::Integer,
    nanoseconds::Integer,
    tz_id::String,
)
  • `seconds` 和 `nanoseconds` 是自 Unix 纪元以来的时间,通常被称为 Unix 时间戳。

  • `nanoseconds` 的数量范围从 `0` 到 `999_999_999`(为了清晰起见,此处及后续添加了 `_` 分隔符)。

  • `tz_id` 指定了 时区数据库所理解的时区名称。

例如,表示为 `1970-01-01T02:15:00.000000042+0100[Europe/Paris]` 的时间点的序列化可以按如下方式实现

  • 检索该时间点命名时区的偏移量,此处为 +1 小时,即 `3_600` 秒。

  • 计算 UTC 时间,即 `1970-01-01T01:15:00.000000042Z`(`Z` 表示 UTC)。

  • 计算该 UTC 时间与 Unix 纪元之间的差值,即 1 小时 15 分钟,也就是 `4_500` 秒。

因此,生成的 `DateTime` 实例如下

{
  seconds: 4500
  nanoseconds: 42,
  tz_id: "Europe/Paris"
}

这种 `DateTime` 结构体的反序列化以相反的方式进行

  • 根据该 Unix 时间戳(`4500` 秒和 `42` 纳秒)实例化 `DateTime` 的习惯性等效对象,得到 `1970-01-01T01:15:00.000000042Z`

  • 将生成的 UTC `DateTime` 本地化到 `tz_id` 指定的时区,得到 `1970-01-01T02:15:00.000000042+0100[Europe/Paris]`

已知限制

准确性

给定时间点和时区名称的偏移量解析受底层时区数据库准确性的限制。特别是,1970 年之前的时区没有得到很好的规定。此外,偏移量解析可能同时发生在 Bolt 客户端和 Bolt 服务器端。它们各自依赖不同的时区数据库。如果这些副本不同步,可能会导致不必要的差异。在这种情况下,服务器或客户端可能会

  • 拒绝对方认为有效的时区名称。

  • 为相同的时区和 `DateTimeZoneId` 解析出不同的偏移量。

LocalDateTime

捕获日期和时间,但不包括时区的即时点。

**标签字节:** `64`

字段数量 2

LocalDateTime::Structure(
    seconds::Integer,
    nanoseconds::Integer,
)

其中 `seconds` 是自 Unix 纪元以来的秒数。

Duration

一个时间量。它捕获两个即时点之间的时间差。它只捕获两个即时点之间的时间量,不捕获开始时间和结束时间。捕获开始时间和结束时间的单位将是 `时间间隔 (Time Interval)`,这超出了本提案的范围。

持续时间可以是负数。

**标签字节:** `45`

字段数量 4

Duration::Structure(
    months::Integer,
    days::Integer,
    seconds::Integer,
    nanoseconds::Integer,
)

Point2D

二维空间中单个位置的表示。

**标签字节:** `58`

字段数量 3

Point2D::Structure(
    srid::Integer,
    x::Float,
    y::Float,
)

其中 `srid` 是一个 *空间参考系统标识符*。

Point3D

三维空间中单个位置的表示。

**标签字节:** `59`

字段数量 4

Point3D::Structure(
    srid::Integer,
    x::Float,
    y::Float,
    z::Float,
)

其中 `srid` 是一个 *空间参考系统标识符*。

旧版结构体

旧版日期时间

捕获日期、时间和时区的即时点。时区信息通过区域偏移量指定。

此结构体在 **5.0** 版本中已移除,取而代之的是`DateTime`

**标签字节:** `46`

字段数量 3

DateTime::Structure(
    seconds::Integer,
    nanoseconds::Integer,
    tz_offset_seconds::Integer,
)
  • `tz_offset_seconds` 指定了与 UTC 的秒数偏移量。

  • `seconds` 是自 Unix 纪元以来经过的秒数,通常称为 Unix 时间戳,**加上**上述偏移量。

  • `nanoseconds` 是 `DateTime` 的最后一秒之后剩余的部分。`nanoseconds` 的数量范围从 `0` 到 `999_999_999`(为了清晰起见,此处添加了 `_` 分隔符)。

例如,表示为 `1970-01-01T02:15:00+01:00`(以及 `42` 纳秒)的时间点的序列化可以按如下方式实现

  • 计算 UTC 时间,即 `1970-01-01T01:15:00Z`(`Z` 表示 UTC)。

  • 计算该 UTC 时间与 Unix 纪元之间的差值,即 1 小时 15 分钟,也就是 `4500` 秒。

  • 将 +1 小时的偏移量(即 `3600` 秒)加到上述差值上,得到 `8100`(`4500` + `3600`)。

因此,生成的 `DateTime` 实例如下

{
  seconds: 8100
  nanoseconds: 42,
  tz_offset_seconds: 3600
}

这种 `DateTime` 结构体的反序列化预期以相反的方式进行

  • 从 `seconds` 字段中移除偏移量,此处得到 `8100`

  • 根据该 Unix 时间戳实例化 `DateTime` 的习惯性等效对象,得到 `1970-01-01T01:15:00Z`

  • 将生成的 UTC `DateTime` 本地化到指定偏移量的时区,得到 `1970-01-01T02:15:00+0100`

旧版日期时间(时区ID)

捕获日期、时间和时区的即时点。时区信息通过区域标识符指定。

此结构体在 **5.0** 版本中已移除,取而代之的是`DateTimeZoneId`

**标签字节:** `66`

字段数量 3

DateTimeZoneId::Structure(
    seconds::Integer,
    nanoseconds::Integer,
    tz_id::String,
)
  • `tz_id` 指定了 时区数据库所理解的时区名称。

  • `seconds` 是自 Unix 纪元以来经过的秒数,通常称为 Unix 时间戳,**加上**从命名时区和指定时间点导出的偏移量。

  • `nanoseconds` 是 `DateTime` 的最后一秒之后剩余的部分。纳秒的数量范围从 `0` 到 `999_999_999`(为了清晰起见,此处及后续添加了 `_` 分隔符)。

例如,表示为 `1970-01-01T02:15:00+0100[Europe/Paris]`(以及 `42` 纳秒)的时间点的序列化可以按如下方式实现

  • 检索该时间点命名时区的偏移量,此处为 +1 小时,即 `3600` 秒。

  • 计算 UTC 时间,即 `1970-01-01T01:15:00Z`(`Z` 表示 UTC)。

  • 计算该 UTC 时间与 Unix 纪元之间的差值,即 1 小时 15 分钟,也就是 `4500` 秒。

  • 将解析出的 +1 小时偏移量(即 `3600` 秒)加到上述差值上,得到 `8100`(`4500` + `3600`)。

因此,生成的 `DateTime` 实例如下

{
  seconds: 8100
  nanoseconds: 42,
  tz_id: "Europe/Paris"
}

这种 `DateTime` 结构体的反序列化过程如下

  • 假设秒数表示 Unix 时间戳,实例化 `DateTime` 的习惯性等效对象,得到 `1970-01-01T02:15:00Z`。

  • 设置生成实例的时区,但不改变日期/时间组件,得到 `1970-01-01T02:15:00+0100[Europe/Paris]`(这可能导致歧义,更多信息请参阅下方的`已知限制`部分)。

已知限制

准确性

给定时间点和时区名称的偏移量解析受底层时区数据库准确性的限制。特别是,1970 年之前的时区没有得到很好的规定。此外,偏移量解析可能同时发生在 Bolt 客户端和 Bolt 服务器端。它们各自依赖不同的时区数据库。如果这些副本不同步,可能会导致不必要的差异。在这种情况下,服务器或客户端可能会

  • 拒绝对方认为有效的时区名称。

  • 为相同的时区和 `DateTimeZoneId` 解析出不同的偏移量。

时间偏移

注意:这些问题已在 **5.0** 版本中引入`DateTimeZoneId`后解决。

并非所有 `DateTimeZoneId` 实例都映射到单个有效时间点。

  1. 在给定日期和时区内的时间偏移期间,例如从凌晨 2 点到凌晨 3 点,凌晨 2:30 不会发生。

  2. 类似地,在给定日期和时区内,当从凌晨 3 点回到凌晨 2 点时,凌晨 2:30 会出现两次。

在第一种情况下,指定凌晨 2 点到凌晨 3 点之间时间的 `DateTimeZoneId` 不对应于该时区的任何实际时间点,因此是无效的。

在第二种情况下,凌晨 2 点到凌晨 3 点之间的所有时间点都存在两次,但偏移量不同。因此,时区名称不足以解决歧义,还需要时区偏移量。由于 `DateTimeZoneId` 不包括时区偏移量,这些特定日期时间的解析是未定义行为。

各版本变更摘要

以下部分列出了结构体语义发生变化的版本中的变更。另请查看Bolt 消息中的变更。

版本 5.0

© . All rights reserved.