neo4j_bolt 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +9 -3
- data/lib/neo4j_bolt/version.rb +1 -1
- data/lib/neo4j_bolt.rb +77 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 865849878d27a8936cb83b5d6567b2ef5d4256f422dc716cc0d496fcdfa31657
|
|
4
|
+
data.tar.gz: e3d4d1b1a57214d0ef7f99c75cd6a76c9a0c2fbb187fb02bb815b821f5dbc4b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 36b289414eaed0af1eb8b628863e7d2f8cd88155377c0104bff8dc4edfda3e12bdf6157ed61e832be04856bb7cffe37d8be96a5d7c6ae5dac01d50a4fb6a75c5
|
|
7
|
+
data.tar.gz: 2fe7a4858a2c03f1b96e9260d93d85e77bdfef60f05fe0c1a2793b7c125e3e0d3db35902ec07ba6fc9cec4ce15cc02d2b8bb7cfad75c7d73503bf612a890ed38
|
data/README.md
CHANGED
|
@@ -4,14 +4,14 @@ Neo4jBolt 0.4 is a small compatibility and convenience layer for Ruby applicatio
|
|
|
4
4
|
|
|
5
5
|
Neo4jBolt no longer implements the Bolt wire protocol itself.
|
|
6
6
|
|
|
7
|
-
`0.5.
|
|
7
|
+
`0.5.1` requires Ruby 3.4 or newer and pins `neo4j-ruby-driver` to `6.2.1.beta.4`. Applications on older Rubies can remain on the Neo4jBolt 0.3.x line; this prerelease is intentionally not an automatic upgrade for them.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
11
|
For this prerelease, specify the version explicitly:
|
|
12
12
|
|
|
13
13
|
```ruby
|
|
14
|
-
gem "neo4j_bolt", "0.5.
|
|
14
|
+
gem "neo4j_bolt", "0.5.1"
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Then run `bundle install`. A running Neo4j database is required.
|
|
@@ -218,6 +218,8 @@ neo4j_bolt dump --data-only -o data.dump
|
|
|
218
218
|
neo4j_bolt load --data-only database.dump
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
+
Property values that JSON cannot express keep their type: dates, datetimes, local datetimes, local times, times, durations and points are written as maps tagged with `"$neo4j"`, for example `{"$neo4j":"date","value":"1815-12-10"}`, carrying the driver's own fields so that loading restores the exact value, and a readable `value` where there is one. Neo4j properties are never maps, so a tagged map cannot clash with an ordinary value; older dumps, which wrote such values as strings, still load as before. A datetime with a named zone (`[Europe/Berlin]`) comes back with its offset only, as the driver reads it that way.
|
|
222
|
+
|
|
221
223
|
The `0, 1, 2, ...` IDs are synthetic dump-local IDs. Database-internal IDs and element IDs are never written to the persistent format. Dumping keeps the count, node, and relationship reads in one transaction, so the driver-provided entity identity used to connect the two streamed result sets stays within Neo4j's transaction-scoped identity guarantee. To keep dump numbering deterministic, ordering uses `elementId()` on modern Neo4j and an internal `id()` fallback only on Neo4j 4.4, where `elementId()` does not exist. Old Neo4jBolt dumps containing only `n` and `r` records remain loadable.
|
|
222
224
|
|
|
223
225
|
For relational loads, the adapter assigns a random temporary label and the fixed reserved property `__neo4j_bolt_load_id` to imported nodes. After the nodes are committed it builds a temporary `neo4j_bolt_` index on that property, uses indexed lookups while creating relationships, drops the index, and removes the temporary metadata in batches. The fixed property key is reused by every load instead of registering a new random property-key token each time. Neo4j can keep the property key registered after all values have been removed, so it may remain visible in Browser's property-key list; no imported node retains the property after successful cleanup. Relational loads refuse to run if the dump itself, or existing data during `force_append`, currently uses the reserved property. No database-internal identity is carried from one load transaction to another. Loading a relational dump therefore requires permission to create and drop an index.
|
|
@@ -257,7 +259,7 @@ The complete integration suite is run against both Community and Enterprise for
|
|
|
257
259
|
- `neo4j:2026.06.0-community`
|
|
258
260
|
- `neo4j:2026.06.0-enterprise`
|
|
259
261
|
|
|
260
|
-
No compatibility beyond this matrix is claimed for `0.5.
|
|
262
|
+
No compatibility beyond this matrix is claimed for `0.5.1`.
|
|
261
263
|
|
|
262
264
|
Enterprise test containers are started with `NEO4J_ACCEPT_LICENSE_AGREEMENT=yes`. The Community targets verify that Community-compatible features remain portable; the Enterprise targets additionally exercise successful node-key dump/load instead of merely observing that Community rejects node keys.
|
|
263
265
|
|
|
@@ -297,6 +299,10 @@ To point RSpec itself at an already disposable database, set `NEO4J_BOLT_TEST_HO
|
|
|
297
299
|
| CLI commands | Preserved |
|
|
298
300
|
| `BoltSocket`, `BoltBuffer`, protocol markers/state/parser/packer | Intentionally removed private implementation details |
|
|
299
301
|
|
|
302
|
+
## 0.5.1
|
|
303
|
+
|
|
304
|
+
- Dumps keep dates, datetimes, local datetimes, local times, times, durations and points; before, they were written as text and came back as strings. Older dumps load as before.
|
|
305
|
+
|
|
300
306
|
## 0.5.0 migration notes
|
|
301
307
|
|
|
302
308
|
- Ruby 3.4 or newer is required; Ruby 2.x/3.0–3.3 applications should stay on 0.3.x until upgraded.
|
data/lib/neo4j_bolt/version.rb
CHANGED
data/lib/neo4j_bolt.rb
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "date"
|
|
3
4
|
require "json"
|
|
4
5
|
require "securerandom"
|
|
5
6
|
require "set"
|
|
6
7
|
require "thread"
|
|
8
|
+
require "time"
|
|
7
9
|
require "uri"
|
|
8
10
|
require "neo4j/driver"
|
|
9
11
|
require "neo4j_bolt/version"
|
|
@@ -20,6 +22,10 @@ module Neo4jBolt
|
|
|
20
22
|
LOAD_PROGRESS_STEP = 5_000
|
|
21
23
|
DUMP_PROGRESS_STEP = 1_000
|
|
22
24
|
LOAD_ID_PROPERTY = "__neo4j_bolt_load_id"
|
|
25
|
+
# Dumps write temporal and spatial property values as maps tagged with
|
|
26
|
+
# this key. Neo4j properties are never maps, so a tagged map cannot be an
|
|
27
|
+
# ordinary value, and dumps without such values read as before.
|
|
28
|
+
DUMP_TYPE_KEY = "$neo4j"
|
|
23
29
|
private_constant :LOAD_MEMORY_ERROR_CODES, :LOAD_MEMORY_GQL_STATUSES,
|
|
24
30
|
:LOAD_PROGRESS_STEP, :DUMP_PROGRESS_STEP, :LOAD_ID_PROPERTY
|
|
25
31
|
|
|
@@ -618,7 +624,7 @@ module Neo4jBolt
|
|
|
618
624
|
"ORDER BY #{identity_function}(n)"
|
|
619
625
|
) do |row|
|
|
620
626
|
dump_ids[row["identity"].to_s] = dumped_nodes if dump_ids
|
|
621
|
-
io.puts "n #{JSON.generate(id: dumped_nodes, labels: row["labels"], properties: row["properties"])}"
|
|
627
|
+
io.puts "n #{JSON.generate(id: dumped_nodes, labels: row["labels"], properties: encode_dump_properties(row["properties"]))}"
|
|
622
628
|
dumped_nodes += 1
|
|
623
629
|
if (dumped_nodes % DUMP_PROGRESS_STEP).zero?
|
|
624
630
|
progress.update(progress_message(
|
|
@@ -638,7 +644,7 @@ module Neo4jBolt
|
|
|
638
644
|
from: dump_ids.fetch(row["from_identity"].to_s),
|
|
639
645
|
to: dump_ids.fetch(row["to_identity"].to_s),
|
|
640
646
|
type: row["type"],
|
|
641
|
-
properties: row["properties"]
|
|
647
|
+
properties: encode_dump_properties(row["properties"])
|
|
642
648
|
)}"
|
|
643
649
|
dumped_relationships += 1
|
|
644
650
|
if (dumped_relationships % DUMP_PROGRESS_STEP).zero?
|
|
@@ -1098,6 +1104,73 @@ module Neo4jBolt
|
|
|
1098
1104
|
version.to_s.split(".").first.to_i >= 5 ? "elementId" : "id"
|
|
1099
1105
|
end
|
|
1100
1106
|
|
|
1107
|
+
# Property values for a dump: JSON values as they are; dates, times,
|
|
1108
|
+
# durations and points as maps tagged with DUMP_TYPE_KEY that carry the
|
|
1109
|
+
# driver's own fields (so that loading restores the exact value) and, for
|
|
1110
|
+
# people reading the dump, the value as text.
|
|
1111
|
+
def encode_dump_properties(properties)
|
|
1112
|
+
return properties unless properties.is_a?(Hash)
|
|
1113
|
+
|
|
1114
|
+
properties.to_h { |key, value| [key, encode_dump_value(value)] }
|
|
1115
|
+
end
|
|
1116
|
+
|
|
1117
|
+
def encode_dump_value(value)
|
|
1118
|
+
tagged = ->(type, fields) { { DUMP_TYPE_KEY => type }.merge(fields) }
|
|
1119
|
+
case value
|
|
1120
|
+
when Array
|
|
1121
|
+
value.map { |item| encode_dump_value(item) }
|
|
1122
|
+
when Time
|
|
1123
|
+
tagged.call("datetime", "value" => value.iso8601(9))
|
|
1124
|
+
when DateTime
|
|
1125
|
+
tagged.call("datetime", "value" => value.to_time.iso8601(9))
|
|
1126
|
+
when Date
|
|
1127
|
+
tagged.call("date", "value" => value.iso8601)
|
|
1128
|
+
when Neo4j::Driver::Types::LocalDateTime
|
|
1129
|
+
tagged.call("local_datetime", "epoch_seconds" => value.epoch_seconds, "nanoseconds" => value.nanoseconds,
|
|
1130
|
+
"value" => value.to_s)
|
|
1131
|
+
when Neo4j::Driver::Types::LocalTime
|
|
1132
|
+
tagged.call("local_time", "nanoseconds" => value.nanoseconds, "value" => value.to_s)
|
|
1133
|
+
when Neo4j::Driver::Types::OffsetTime
|
|
1134
|
+
tagged.call("time", "nanoseconds" => value.nanoseconds, "tz_offset_seconds" => value.tz_offset_seconds,
|
|
1135
|
+
"value" => value.to_s)
|
|
1136
|
+
when Neo4j::Driver::Types::Duration
|
|
1137
|
+
tagged.call("duration", "months" => value.months, "days" => value.days, "seconds" => value.seconds,
|
|
1138
|
+
"nanoseconds" => value.nanoseconds, "value" => value.to_s)
|
|
1139
|
+
when Neo4j::Driver::Types::Point
|
|
1140
|
+
tagged.call("point", { "srid" => value.srid, "x" => value.x, "y" => value.y, "z" => value.z }.compact)
|
|
1141
|
+
else
|
|
1142
|
+
value
|
|
1143
|
+
end
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
def decode_dump_properties(properties)
|
|
1147
|
+
return properties unless properties.is_a?(Hash)
|
|
1148
|
+
|
|
1149
|
+
properties.to_h { |key, value| [key, decode_dump_value(value)] }
|
|
1150
|
+
end
|
|
1151
|
+
|
|
1152
|
+
def decode_dump_value(value)
|
|
1153
|
+
return value.map { |item| decode_dump_value(item) } if value.is_a?(Array)
|
|
1154
|
+
return value unless value.is_a?(Hash) && value.key?(DUMP_TYPE_KEY)
|
|
1155
|
+
|
|
1156
|
+
case value[DUMP_TYPE_KEY]
|
|
1157
|
+
when "datetime" then Time.iso8601(value.fetch("value"))
|
|
1158
|
+
when "date" then Date.iso8601(value.fetch("value"))
|
|
1159
|
+
when "local_datetime"
|
|
1160
|
+
Neo4j::Driver::Types::LocalDateTime.new(value.fetch("epoch_seconds"), value.fetch("nanoseconds"))
|
|
1161
|
+
when "local_time" then Neo4j::Driver::Types::LocalTime.new(value.fetch("nanoseconds"))
|
|
1162
|
+
when "time" then Neo4j::Driver::Types::OffsetTime.new(value.fetch("nanoseconds"), value.fetch("tz_offset_seconds"))
|
|
1163
|
+
when "duration"
|
|
1164
|
+
Neo4j::Driver::Types::Duration.new(value.fetch("months"), value.fetch("days"), value.fetch("seconds"),
|
|
1165
|
+
value.fetch("nanoseconds"))
|
|
1166
|
+
when "point"
|
|
1167
|
+
Neo4j::Driver::Types::Point.new(srid: value.fetch("srid"), x: value.fetch("x"), y: value.fetch("y"),
|
|
1168
|
+
z: value["z"])
|
|
1169
|
+
else
|
|
1170
|
+
raise Error, "Unknown value type in dump: #{value[DUMP_TYPE_KEY].inspect}"
|
|
1171
|
+
end
|
|
1172
|
+
end
|
|
1173
|
+
|
|
1101
1174
|
def parse_dump(io, node_batches, relationship_batches, progress = nil)
|
|
1102
1175
|
node_count = 0
|
|
1103
1176
|
relationship_count = 0
|
|
@@ -1116,10 +1189,12 @@ module Neo4jBolt
|
|
|
1116
1189
|
value = JSON.parse(json)
|
|
1117
1190
|
case kind
|
|
1118
1191
|
when "n"
|
|
1192
|
+
value["properties"] = decode_dump_properties(value["properties"])
|
|
1119
1193
|
labels = value.fetch("labels")
|
|
1120
1194
|
node_batches[labels.sort] << value
|
|
1121
1195
|
node_count += 1
|
|
1122
1196
|
when "r"
|
|
1197
|
+
value["properties"] = decode_dump_properties(value["properties"])
|
|
1123
1198
|
relationship_batches[value.fetch("type")] << value
|
|
1124
1199
|
relationship_count += 1
|
|
1125
1200
|
else
|