neo4j-ruby-driver 6.2.1 → 6.3.0

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: 701bd350949810304707654ca39de518eda02faeda8b6d1c9575d14a3f5ac703
4
- data.tar.gz: 7920f8aa3f11ded084dda941bf7de0882e7858c7b99d69168b816e2ab6b65e44
3
+ metadata.gz: 57826a02d1e095bb0ceda73c602953499aed1cf4f06b09543606f65873b41a94
4
+ data.tar.gz: 69b75755af9e3a9491d2ae50a6149529f2052131436b35dfa8fab3662224f980
5
5
  SHA512:
6
- metadata.gz: 9afb8c944e1661dcc0f5d6f4573d6e797bc218f8f823ee5304c1e7d4ae01bd85fe6804e055f1dd58b41732cbd852a2bb8fd4128f5127c8a3644c73c705903af3
7
- data.tar.gz: bfd5dd64dd2cd2709409daeb8f3df67b641119c24683f4e6e2022db3fb3de335bc83c8f3ad04c6cefbcdb701121bc06db6198549a39237346f26fc9939fe2fd5
6
+ metadata.gz: 647cafdddacd1edd462d9fe131ec7b7fcc28489b4ee6da28c3c6a563fe7fb89f19935bc838b693046c6f77ffae1e4d9d270e6bd84a3471d3c39453cf6f953743
7
+ data.tar.gz: 8c63e3b41d666d2a03944bc2fef8f314e8b8f05aa9b974952990fd8580bfb88430db7f19c5052dd51e00bb1c14296bde4774f27f1100fc5fa4a4587d82b8c631
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Neo4j Ruby Driver
2
2
 
3
+ [![codecov](https://codecov.io/gh/neo4jrb/neo4j-ruby-driver/branch/main/graph/badge.svg)](https://codecov.io/gh/neo4jrb/neo4j-ruby-driver)
4
+
3
5
  A driver for [Neo4j](https://neo4j.com) that speaks the
4
6
  [Bolt protocol](https://neo4j.com/docs/bolt/current/), with **two
5
7
  implementations behind one public Ruby API**:
@@ -126,6 +128,13 @@ age = record[1] # or by index
126
128
  records = result.to_a
127
129
  ```
128
130
 
131
+ Node labels and relationship types come back as **symbols** off the entity —
132
+ `node.labels # => [:Person]`, `rel.type # => :KNOWS` — as do record columns and
133
+ map/property keys. Cypher functions that *return* strings (`labels()`, `keys()`,
134
+ `type()`) return **strings**, because their result is an ordinary value on the
135
+ wire, indistinguishable from data you returned yourself. Read the accessor for
136
+ symbols, or convert the function result with `.map(&:to_sym)`.
137
+
129
138
  ## Architecture
130
139
 
131
140
  The development tree is split so shared code lives in one place and each
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Neo4j
4
4
  module Driver
5
+ # Builds Bolt auth-token hashes (basic, bearer, kerberos, none, custom),
6
+ # mirroring Java's org.neo4j.driver.AuthTokens factory for the MRI flavour.
5
7
  module AuthTokens
6
8
  class << self
7
9
  # Signatures mirror Java's org.neo4j.driver.AuthTokens. The JRuby
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Neo4j
4
4
  module Driver
5
+ # Class-level helper: wraps the named factory methods so that, when given
6
+ # a block, the returned resource is yielded and guaranteed closed
7
+ # afterwards (Java try-with-resources semantics).
5
8
  module AutoCloseable
6
9
  def auto_closeable(*methods)
7
10
  prepend with_block_definer(methods)
@@ -710,9 +710,9 @@ module Neo4j
710
710
  def advance
711
711
  case (chunk = @socket.read_nonblock(READ_CHUNK, exception: false))
712
712
  when :wait_readable
713
- @socket.wait_readable(current_read_timeout) or fail_broken(read_timeout_error)
713
+ wait_ready(:wait_readable) or fail_broken(read_timeout_error)
714
714
  when :wait_writable # SSL renegotiation mid-read
715
- @socket.wait_writable(current_read_timeout) or fail_broken(read_timeout_error)
715
+ wait_ready(:wait_writable) or fail_broken(read_timeout_error)
716
716
  when nil
717
717
  raise EOFError, 'end of file reached'
718
718
  else
@@ -857,11 +857,27 @@ module Neo4j
857
857
  callback&.call(self)
858
858
  end
859
859
 
860
+ # Block until the socket is ready to read (or write, during a mid-read
861
+ # SSL renegotiation), bounded by the current read timeout. A nil timeout
862
+ # waits indefinitely (steady state with no server hint). When the
863
+ # acquisition deadline is set and already past, don't wait at all —
864
+ # report the timeout immediately rather than polling the socket with a
865
+ # zero or negative interval. Returns falsey on timeout so the caller
866
+ # raises read_timeout_error.
867
+ def wait_ready(readiness)
868
+ timeout = current_read_timeout
869
+ return false if timeout && !timeout.positive?
870
+
871
+ @socket.public_send(readiness, timeout)
872
+ end
873
+
860
874
  # The timeout the next read may take. During acquisition (handshake,
861
- # ROUTE) it's the remaining budget of the total deadline; in steady
862
- # state it's the server's recv-timeout hint (nil = block indefinitely).
875
+ # ROUTE) it's the time left on the total deadline — non-positive once the
876
+ # deadline has passed, which #wait_ready turns into an immediate timeout.
877
+ # In steady state it's the server's recv-timeout hint (nil = block
878
+ # indefinitely).
863
879
  def current_read_timeout
864
- return [@read_deadline - current_monotonic, 0.001].max if @read_deadline
880
+ return @read_deadline - current_monotonic if @read_deadline
865
881
 
866
882
  @recv_timeout
867
883
  end
@@ -963,6 +979,7 @@ module Neo4j
963
979
  else
964
980
  @address = format_address(host, port)
965
981
  tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
982
+ enable_keepalive(tcp_socket)
966
983
 
967
984
  if timeout
968
985
  timeval = [timeout, 0].pack('l_2')
@@ -973,6 +990,27 @@ module Neo4j
973
990
  @socket = wrap_with_tls(tcp_socket, bare_host, port)
974
991
  end
975
992
 
993
+ # Enable TCP keepalive so a dead peer — a crashed host or a silent network
994
+ # partition that never delivers FIN/RST — is detected and the blocked read
995
+ # fails, instead of the socket sitting "ESTABLISHED" until the OS default
996
+ # idle (~2h on Linux) or, with nothing in flight, forever. Probe after ~60s
997
+ # idle, every 10s, 3 times (~90s to detection). The per-interval options
998
+ # are platform-specific (TCP_KEEPIDLE on Linux, TCP_KEEPALIVE on macOS/BSD)
999
+ # and best-effort — skip any the platform lacks or rejects; SO_KEEPALIVE
1000
+ # alone (OS-default timing) still applies. Does not affect a slow-but-alive
1001
+ # server: its TCP stack keeps ACKing probes, so keepalive never fires on it.
1002
+ def enable_keepalive(socket)
1003
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
1004
+ idle_opt = if defined?(Socket::TCP_KEEPIDLE) then Socket::TCP_KEEPIDLE
1005
+ elsif defined?(Socket::TCP_KEEPALIVE) then Socket::TCP_KEEPALIVE
1006
+ end
1007
+ socket.setsockopt(Socket::IPPROTO_TCP, idle_opt, 60) if idle_opt
1008
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPINTVL, 10) if defined?(Socket::TCP_KEEPINTVL)
1009
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPCNT, 3) if defined?(Socket::TCP_KEEPCNT)
1010
+ rescue StandardError
1011
+ nil # keepalive stays on with OS-default timing; interval tuning is an optimization
1012
+ end
1013
+
976
1014
  # When the URI uses a +s/+ssc scheme (or :encryption is set
977
1015
  # explicitly), wrap the TCP socket in an OpenSSL::SSL::SSLSocket
978
1016
  # before returning. Errors during the TLS handshake — peer
@@ -158,12 +158,12 @@ module Neo4j
158
158
  count = (range_int >> 16) & 0xFF
159
159
  return false unless version.major == major
160
160
 
161
- version.minor <= top_minor && version.minor >= top_minor - count
161
+ version.minor.between?(top_minor - count, top_minor)
162
162
  end
163
163
 
164
164
  def read_int32
165
165
  bytes = read_fully(4)
166
- bytes ? bytes.unpack1('L>') : nil
166
+ bytes&.unpack1('L>')
167
167
  end
168
168
 
169
169
  # Read exactly n bytes, bounded by @deadline. Returns the bytes, or nil
@@ -208,7 +208,7 @@ module Neo4j
208
208
  raise IOError, 'Unexpected end of stream while reading varint' if byte.nil?
209
209
 
210
210
  value |= (byte & 0x7F) << shift
211
- return value if byte & 0x80 == 0
211
+ return value if byte.nobits?(0x80)
212
212
 
213
213
  shift += 7
214
214
  end
@@ -220,7 +220,7 @@ module Neo4j
220
220
  indices.each_slice(2) do |rel_idx, node_idx|
221
221
  next_node = nodes[node_idx]
222
222
 
223
- if rel_idx < 0
223
+ if rel_idx.negative?
224
224
  unbound_rel = unbound_rels[rel_idx.abs - 1]
225
225
  bound_rel = unbound_rel.bind(next_node.id, current_node.id,
226
226
  next_node.element_id, current_node.element_id)
@@ -3,6 +3,12 @@
3
3
  module Neo4j
4
4
  module Driver
5
5
  module Exceptions
6
+ # Base class for the driver's Neo4j-error exceptions — those carrying a
7
+ # server error code plus the GQL status fields (status, description,
8
+ # classification, cause, diagnostic record) and a list of suppressed
9
+ # exceptions. (A few driver exceptions — NoSuchRecordException,
10
+ # IllegalStateException, UntrustedServerException — subclass RuntimeError
11
+ # directly and sit outside this hierarchy.)
6
12
  class Neo4jException < RuntimeError
7
13
  attr_reader :code, :suppressed, :gql_status, :status_description,
8
14
  :classification, :raw_classification, :diagnostic_record, :gql_cause
@@ -3,6 +3,8 @@
3
3
  module Neo4j
4
4
  module Driver
5
5
  module Exceptions
6
+ # Raised when a single record is requested but the result is empty or
7
+ # holds more than one, or when iteration/peek runs past the last record.
6
8
  class NoSuchRecordException < RuntimeError
7
9
  EMPTY = 'Cannot retrieve a single record, because this result is empty.'
8
10
  TOO_MANY = 'Expected a result with a single record, but this result ' \
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Neo4j
2
4
  module Driver
3
5
  module Exceptions
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Neo4j
2
4
  module Driver
3
5
  module Exceptions
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Neo4j
2
4
  module Driver
3
5
  module Exceptions
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Neo4j
2
4
  module Driver
3
5
  module Exceptions
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Neo4j
2
4
  module Driver
3
5
  module Exceptions
@@ -3,6 +3,8 @@
3
3
  module Neo4j
4
4
  module Driver
5
5
  module Internal
6
+ # No-op stand-in for an ActiveSupport-style deprecator; swallows the
7
+ # `behavior=` configuration the driver would otherwise forward.
6
8
  module Deprecator
7
9
  class << self
8
10
  def deprecator
@@ -3,6 +3,10 @@
3
3
  module Neo4j
4
4
  module Driver
5
5
  module Internal
6
+ # Converts a timeout from seconds (or ActiveSupport::Duration) to integer
7
+ # milliseconds, rejecting negatives. Used for the Bolt tx_timeout (MRI
8
+ # session) and, on JRuby, for the connection / routing-table and other
9
+ # config timeouts handed to the Java driver.
6
10
  module DurationNormalizer
7
11
  # Convert timeout from seconds (or ActiveSupport::Duration) to
8
12
  # milliseconds for the Bolt protocol. A negative timeout is
@@ -3,6 +3,8 @@
3
3
  module Neo4j
4
4
  module Driver
5
5
  module Internal
6
+ # Client-side argument checks (hash parameters, non-nil values, non-empty
7
+ # query text, credentials) mirroring the Java driver's own validation.
6
8
  module Validator
7
9
  def self.require_hash_parameters!(parameters)
8
10
  require_hash!(parameters) do
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Neo4j
4
+ module Driver
5
+ module Internal
6
+ # The IANA zone identifier for a TimeWithZone-like value's zone, for the
7
+ # Bolt DateTimeZoneId structure. Matches the Java driver, which sends
8
+ # ZonedDateTime#getZone#getId verbatim: a zone whose own name is already a
9
+ # valid IANA id is kept as-is — so "UTC" stays "UTC" rather than TZInfo's
10
+ # canonical "Etc/UTC" — while an ActiveSupport::TimeZone built from a Rails
11
+ # display name ("Mountain Time (US & Canada)"), which is not a valid id,
12
+ # falls back to its tzinfo identifier ("America/Denver"). Returns nil when
13
+ # the zone exposes no usable identifier (e.g. an offset-only zone), so the
14
+ # caller can fall back to the offset form.
15
+ module ZoneIdentifier
16
+ module_function
17
+
18
+ def call(zone)
19
+ name = zone.name if zone.respond_to?(:name)
20
+ return name if name && valid?(name)
21
+
22
+ zone.tzinfo.identifier if zone.respond_to?(:tzinfo)
23
+ end
24
+
25
+ def valid?(name)
26
+ TZInfo::Timezone.get(name)
27
+ true
28
+ rescue TZInfo::InvalidTimezoneIdentifier
29
+ false
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -114,16 +114,16 @@ module Neo4j
114
114
  end
115
115
 
116
116
  def pack_integer(value)
117
- if value >= -16 && value <= 127
117
+ if value.between?(-16, 127)
118
118
  # TINY_INT: -16 to +127
119
119
  @buffer << [value].pack('c')
120
- elsif value >= -128 && value <= 127
120
+ elsif value.between?(-128, 127)
121
121
  @buffer << [INT_8, value].pack('Cc')
122
- elsif value >= -32_768 && value <= 32_767
122
+ elsif value.between?(-32_768, 32_767)
123
123
  @buffer << [INT_16, value].pack('Cs>')
124
- elsif value >= -2_147_483_648 && value <= 2_147_483_647
124
+ elsif value.between?(-2_147_483_648, 2_147_483_647)
125
125
  @buffer << [INT_32, value].pack('Cl>')
126
- elsif value >= -9_223_372_036_854_775_808 && value <= 9_223_372_036_854_775_807
126
+ elsif value.between?(-9_223_372_036_854_775_808, 9_223_372_036_854_775_807)
127
127
  @buffer << [INT_64, value].pack('Cq>')
128
128
  else
129
129
  raise ArgumentError, "Integer value out of range: #{value}"
@@ -270,18 +270,16 @@ module Neo4j
270
270
  end
271
271
  end
272
272
 
273
- # Return the IANA zone name for a TimeWithZone-like value when it has
274
- # one, otherwise nil (so the caller falls back to offset-only 0x46).
275
- # Offset-shaped names like "+07:00" or "-05:00" don't round-trip
276
- # through Neo4j's zoned DateTime and are treated as nil.
273
+ # The IANA zone identifier for a TimeWithZone-like value's zone, or nil
274
+ # when it has none (so the caller falls back to the offset-only 0x46
275
+ # form). Internal::ZoneIdentifier keeps a zone name that is already a
276
+ # valid IANA id (so "UTC" stays "UTC") and maps a Rails display name
277
+ # ("Mountain Time (US & Canada)") to its tzinfo id ("America/Denver"),
278
+ # returning nil when neither yields an identifier.
277
279
  def named_zone_for(value)
278
280
  return nil unless value.respond_to?(:time_zone)
279
281
 
280
- tz = value.time_zone
281
- name = tz.respond_to?(:name) ? tz.name : nil
282
- return nil if name.nil? || name.empty? || name.match?(/\A[+-]?\d/)
283
-
284
- name
282
+ Neo4j::Driver::Internal::ZoneIdentifier.call(value.time_zone)
285
283
  end
286
284
 
287
285
  def pack_local_datetime(value)
@@ -51,6 +51,11 @@ module Neo4j
51
51
  # server's authorization cache is per-server — a reader's expiry must
52
52
  # not force the writer pool to re-auth.
53
53
  @auth_epochs = Hash.new(0)
54
+ # Leaf lock for @auth_epochs: never held while taking another lock.
55
+ # Must not be @refresh_lock: open_connection reads the epoch from
56
+ # inside the pool's create block (under TimedStack's mutex), while
57
+ # release and refresh take that pool mutex under @refresh_lock.
58
+ @auth_epoch_lock = Mutex.new
54
59
  # Monitor (reentrant) because ensure_routing_table_is_fresh holds
55
60
  # the lock while it goes through pool_for, which also locks.
56
61
  @refresh_lock = Monitor.new
@@ -238,16 +243,16 @@ module Neo4j
238
243
  # owns the token; the manager notification is skipped for a
239
244
  # per-session identity.
240
245
  if error.is_a?(Exceptions::AuthorizationExpiredException)
241
- @refresh_lock.synchronize { @auth_epochs[address] += 1 }
246
+ @auth_epoch_lock.synchronize { @auth_epochs[address] += 1 }
242
247
  end
243
248
  return false if session_scoped
244
249
 
245
250
  @auth_manager.handle_security_exception(token, error)
246
251
  end
247
252
 
248
- # Current auth generation for a server, read under the lock that guards
249
- # the @auth_epochs Hash (mutated in on_security_exception).
250
- def auth_epoch_for(address) = @refresh_lock.synchronize { @auth_epochs[address] }
253
+ # Current auth generation for a server, read under the leaf lock that
254
+ # guards the @auth_epochs Hash (mutated in on_security_exception).
255
+ def auth_epoch_for(address) = @auth_epoch_lock.synchronize { @auth_epochs[address] }
251
256
 
252
257
  def release(connection)
253
258
  # Direct provider tolerates nil; mirror that. Internal callers
@@ -423,7 +428,7 @@ module Neo4j
423
428
  def ensure_routing_table_is_fresh(database, access_mode, bookmarks: nil, imp_user: nil, auth: nil)
424
429
  @refresh_lock.synchronize do
425
430
  table = @routing_tables[database]
426
- return table if table && table.fresh?(readonly: access_mode == :read)
431
+ return table if table&.fresh?(readonly: access_mode == :read)
427
432
 
428
433
  update_routing_table(database, bookmarks: bookmarks, imp_user: imp_user, auth: auth)
429
434
  end
@@ -44,7 +44,7 @@ module Neo4j
44
44
  end
45
45
 
46
46
  def server
47
- @server_info ||=
47
+ @server ||=
48
48
  if @metadata[:server]
49
49
  ServerInfo.new(@metadata[:server])
50
50
  elsif @connection
@@ -59,7 +59,7 @@ module Neo4j
59
59
  end
60
60
 
61
61
  def database
62
- @database_info ||= DatabaseInfo.new(@metadata[:db])
62
+ @database ||= DatabaseInfo.new(@metadata[:db])
63
63
  end
64
64
 
65
65
  # Time until results are available (milliseconds). 't_first' on the wire.
@@ -60,7 +60,7 @@ module Neo4j
60
60
  # Normalize nanoseconds overflow/underflow into seconds:
61
61
  # -1ns becomes -1s + 999999999ns; 1.5e9 ns becomes 1s + 5e8 ns.
62
62
  def normalize!
63
- if @nanoseconds < 0
63
+ if @nanoseconds.negative?
64
64
  sec_adjust = (@nanoseconds.abs / NANOS_PER_SECOND.to_f).ceil
65
65
  @seconds -= sec_adjust
66
66
  @nanoseconds += sec_adjust * NANOS_PER_SECOND
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Neo4j
4
4
  module Driver
5
- VERSION = '6.2.1'
5
+ VERSION = '6.3.0'
6
6
  end
7
7
  end
data/lib/neo4j/driver.rb CHANGED
@@ -8,6 +8,8 @@ require 'stringio'
8
8
  require 'tzinfo'
9
9
 
10
10
  module Neo4j
11
+ # Top-level namespace for the MRI flavour; boots the pure-Ruby Bolt
12
+ # implementation through the shared Zeitwerk loader.
11
13
  module Driver
12
14
  Loader.load(:mri)
13
15
  AuthTokenManager = Internal::InternalAuthTokenManager
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This file is here so that there is a file with the same name as the gem that
2
4
  # can be required by Bundler.require. Applications should normally
3
5
  # require 'neo4j/driver'.
@@ -8,6 +8,8 @@ require 'zeitwerk'
8
8
 
9
9
  module Neo4j
10
10
  module Driver
11
+ # Boots the driver for a given flavour (:mri or :jruby): configures the
12
+ # Zeitwerk loader over the shared and impl roots, then eager-loads them.
11
13
  class Loader
12
14
  class << self
13
15
  # `__dir__` resolves to:
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This file is for backwards compatibility only.
2
4
  # Applications should require 'neo4j/driver'.
3
5
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: neo4j-ruby-driver
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.2.1
4
+ version: 6.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Neo4j Driver Team
@@ -149,6 +149,7 @@ files:
149
149
  - lib/neo4j/driver/internal/metrics.rb
150
150
  - lib/neo4j/driver/internal/security/static_auth_token_manager.rb
151
151
  - lib/neo4j/driver/internal/validator.rb
152
+ - lib/neo4j/driver/internal/zone_identifier.rb
152
153
  - lib/neo4j/driver/net.rb
153
154
  - lib/neo4j/driver/packstream/markers.rb
154
155
  - lib/neo4j/driver/packstream/packer.rb