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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f9f38fc0e16014ae13f2f31d2ba77f119185249821da7d4f7858fa0ef3f9010f
4
- data.tar.gz: 5ab718f1dd9829aa1f118a6614f7d29d184d63eb20b4ce7d3e36fc92fea5f204
3
+ metadata.gz: 865849878d27a8936cb83b5d6567b2ef5d4256f422dc716cc0d496fcdfa31657
4
+ data.tar.gz: e3d4d1b1a57214d0ef7f99c75cd6a76c9a0c2fbb187fb02bb815b821f5dbc4b2
5
5
  SHA512:
6
- metadata.gz: df61ebc16c46698ccab04c94e9a8abd6120ef07d899b7e52e8e9a80d13bcc4b2f8d17bddff330f6d10ff794faabd705ebfba3d3531fdad311cb8ea5de02dbc8c
7
- data.tar.gz: 6522b70e44f279f182c289ae3397dafd11d5dff4e72a3437247a5f25bd66d4185e6696b2aa2c96bce976f518d63a30c98c48ea86b2823d28383c3989df24b7eb
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.0` 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.
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.0"
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.0`.
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.
@@ -1,3 +1,3 @@
1
1
  module Neo4jBolt
2
- VERSION = "0.5.0"
2
+ VERSION = "0.5.1"
3
3
  end
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: neo4j_bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Specht