bzync-nextsql 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +37 -0
- data/lib/nextsql/client.rb +595 -0
- data/lib/nextsql/cluster.rb +190 -0
- data/lib/nextsql/errors.rb +18 -0
- data/lib/nextsql/protocol.rb +1126 -0
- data/lib/nextsql/version.rb +5 -0
- data/lib/nextsql.rb +68 -0
- metadata +53 -0
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
require "json"
|
|
6
|
+
require "bigdecimal"
|
|
7
|
+
|
|
8
|
+
require_relative "errors"
|
|
9
|
+
|
|
10
|
+
module NextSQL
|
|
11
|
+
# Wire encoding/decoding for NSQL v1 — mirrors drivers/php/src/Protocol.php
|
|
12
|
+
# and drivers/go/nextsql.go byte-for-byte. Keep the three in sync: this is
|
|
13
|
+
# a faithful reimplementation, not an independent design.
|
|
14
|
+
module Protocol
|
|
15
|
+
VERSION = 1
|
|
16
|
+
MAX_PACKET = 1 << 20
|
|
17
|
+
MAX_SQL = 1 << 20
|
|
18
|
+
MAX_NAME = 256
|
|
19
|
+
MAX_PARAMS = 256
|
|
20
|
+
MAX_ENUM_LABELS = 4096
|
|
21
|
+
MAX_ENUM_LABEL_BYTES = 255
|
|
22
|
+
|
|
23
|
+
TYPE_HELLO = 1
|
|
24
|
+
TYPE_HELLO_OK = 2
|
|
25
|
+
TYPE_AUTH = 3
|
|
26
|
+
TYPE_AUTH_OK = 4
|
|
27
|
+
TYPE_QUERY = 5
|
|
28
|
+
TYPE_PREPARE = 6
|
|
29
|
+
TYPE_PREPARE_OK = 7
|
|
30
|
+
TYPE_EXECUTE = 8
|
|
31
|
+
TYPE_CLOSE_STMT = 9
|
|
32
|
+
TYPE_CLOSE_OK = 10
|
|
33
|
+
TYPE_FLOW_ACK = 11
|
|
34
|
+
TYPE_CANCEL = 12
|
|
35
|
+
TYPE_TERMINATE = 13
|
|
36
|
+
TYPE_ROW_DESC = 14
|
|
37
|
+
TYPE_DATA_BATCH = 15
|
|
38
|
+
TYPE_COMMAND_COMPLETE = 16
|
|
39
|
+
TYPE_ERROR = 17
|
|
40
|
+
TYPE_READY = 18
|
|
41
|
+
TYPE_UNLOCK = 19
|
|
42
|
+
TYPE_UNLOCK_OK = 20
|
|
43
|
+
TYPE_IDEMPOTENT_QUERY = 21
|
|
44
|
+
TYPE_SET_READ_CONSISTENCY = 22
|
|
45
|
+
TYPE_NODE_STATUS = 23
|
|
46
|
+
TYPE_NODE_STATUS_RESP = 24
|
|
47
|
+
|
|
48
|
+
# Read-consistency modes. Values match the wire byte ordering.
|
|
49
|
+
READ_STRONG = 0
|
|
50
|
+
READ_BOUNDED = 1
|
|
51
|
+
READ_STALE = 2
|
|
52
|
+
|
|
53
|
+
AUTH_PASSWORD = 1
|
|
54
|
+
AUTH_PASSWORD_KEY = 2
|
|
55
|
+
FLAG_CANCEL = 1
|
|
56
|
+
FLAG_NULL = 0x01
|
|
57
|
+
|
|
58
|
+
KIND_UUID = 1
|
|
59
|
+
KIND_STRING = 2
|
|
60
|
+
KIND_TEXT = 3
|
|
61
|
+
KIND_DECIMAL = 4
|
|
62
|
+
KIND_TIMESTAMPTZ = 5
|
|
63
|
+
KIND_JSON = 6
|
|
64
|
+
KIND_VECTOR = 7
|
|
65
|
+
KIND_BOOL = 8
|
|
66
|
+
KIND_NULL = 9
|
|
67
|
+
KIND_POINT = 10
|
|
68
|
+
KIND_BOX = 11
|
|
69
|
+
KIND_LINE = 12
|
|
70
|
+
KIND_POLYGON = 13
|
|
71
|
+
KIND_BLOB = 14
|
|
72
|
+
KIND_INT8 = 15
|
|
73
|
+
KIND_INT16 = 16
|
|
74
|
+
KIND_INT32 = 17
|
|
75
|
+
KIND_INT64 = 18
|
|
76
|
+
KIND_UINT8 = 19
|
|
77
|
+
KIND_UINT16 = 20
|
|
78
|
+
KIND_UINT32 = 21
|
|
79
|
+
KIND_UINT64 = 22
|
|
80
|
+
KIND_DATE = 23
|
|
81
|
+
KIND_TIME = 24
|
|
82
|
+
KIND_CHAR = 25
|
|
83
|
+
KIND_VARCHAR = 26
|
|
84
|
+
KIND_TIMESTAMP = 27
|
|
85
|
+
KIND_FLOAT32 = 28
|
|
86
|
+
KIND_FLOAT64 = 29
|
|
87
|
+
KIND_ENUM = 30
|
|
88
|
+
KIND_INTERVAL = 31
|
|
89
|
+
KIND_STRUCT = 32
|
|
90
|
+
KIND_ARRAY = 33
|
|
91
|
+
KIND_MAP = 34
|
|
92
|
+
KIND_GEOMETRY = 35
|
|
93
|
+
KIND_GEOGRAPHY = 36
|
|
94
|
+
MAX_NEST_DEPTH = 8
|
|
95
|
+
MAX_STRUCT_FIELDS = 128
|
|
96
|
+
MAX_COLLECTION_LEN = 1 << 20
|
|
97
|
+
|
|
98
|
+
Column = Struct.new(:name, :kind, :labels, :coll_type)
|
|
99
|
+
NodeStatus = Struct.new(:role, :has_leader, :healthy, :applied_lsn, :last_contact_ms, :apply_backlog)
|
|
100
|
+
|
|
101
|
+
# Dense (values), reference (ref=true, dim only), or sparse
|
|
102
|
+
# (indices + values) VECTOR/BITVECTOR/SPARSEVECTOR payload.
|
|
103
|
+
Vector = Struct.new(:dim, :values, :indices, :ref) do
|
|
104
|
+
def initialize(dim:, values: [], indices: nil, ref: false)
|
|
105
|
+
super(dim, values, indices, ref)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
Point = Struct.new(:lon, :lat)
|
|
110
|
+
Box = Struct.new(:west, :south, :east, :north)
|
|
111
|
+
Line = Struct.new(:coords)
|
|
112
|
+
Polygon = Struct.new(:rings)
|
|
113
|
+
|
|
114
|
+
# Int8/16/32/64 (D2, Datatype expansion track): explicit fixed-width int
|
|
115
|
+
# wrappers. A bare Integer still defaults to KIND_DECIMAL (see
|
|
116
|
+
# encode_param) and coerces server-side into any numeric column — these
|
|
117
|
+
# are only needed to pin an exact wire width (Ruby Integer is arbitrary
|
|
118
|
+
# precision, so there is no natural bare-value mapping to one width).
|
|
119
|
+
Int8 = Struct.new(:value)
|
|
120
|
+
Int16 = Struct.new(:value)
|
|
121
|
+
Int32 = Struct.new(:value)
|
|
122
|
+
Int64 = Struct.new(:value)
|
|
123
|
+
|
|
124
|
+
# Uint8/16/32/64 (D3, Datatype expansion track): explicit fixed-width
|
|
125
|
+
# unsigned int wrappers, mirroring Int8/16/32/64 above.
|
|
126
|
+
Uint8 = Struct.new(:value)
|
|
127
|
+
Uint16 = Struct.new(:value)
|
|
128
|
+
Uint32 = Struct.new(:value)
|
|
129
|
+
Uint64 = Struct.new(:value)
|
|
130
|
+
|
|
131
|
+
# EnumValue (D11, Datatype expansion track): an explicit ENUM parameter
|
|
132
|
+
# wrapper (value:, labels:). Ordinary INSERT/UPDATE params can just pass
|
|
133
|
+
# a plain String — the server coerces STRING -> ENUM against the
|
|
134
|
+
# destination column, same as a SQL string literal. This wrapper exists
|
|
135
|
+
# for explicit round-tripping and mirrors Int8/Uint8's precedent.
|
|
136
|
+
EnumValue = Struct.new(:value, :labels) do
|
|
137
|
+
def initialize(value:, labels:)
|
|
138
|
+
super(value, labels)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# NaiveTimestamp/TimeOfDay/Float32/Float64 (D7/D5/D8, Datatype expansion
|
|
143
|
+
# track): explicit wrappers for types with no unambiguous native Ruby
|
|
144
|
+
# mapping. A bare Time/DateTime already means TimestampTZ (see below), so
|
|
145
|
+
# NaiveTimestamp is required to select the no-timezone Kind instead.
|
|
146
|
+
# Ruby has no time-only stdlib class, so TimeOfDay carries nanoseconds
|
|
147
|
+
# since midnight directly. Date needs no wrapper: it is unambiguous (not
|
|
148
|
+
# a Time/DateTime superclass — DateTime is Date's *subclass*, checked
|
|
149
|
+
# first below) and has no prior meaning to conflict with.
|
|
150
|
+
NaiveTimestamp = Struct.new(:value) do
|
|
151
|
+
def initialize(value:)
|
|
152
|
+
super(value)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
TimeOfDay = Struct.new(:nanos_since_midnight) do
|
|
156
|
+
def initialize(nanos_since_midnight:)
|
|
157
|
+
super(nanos_since_midnight)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
Float32 = Struct.new(:value)
|
|
161
|
+
Float64 = Struct.new(:value)
|
|
162
|
+
|
|
163
|
+
# Interval (D6, Datatype expansion track): months (Integer, calendar) +
|
|
164
|
+
# days (Integer, calendar) + nanos (Integer, time-of-day component) —
|
|
165
|
+
# Postgres-style 3-field storage. A plain String still works as an
|
|
166
|
+
# INTERVAL param for INSERT/UPDATE column assignment (server-side
|
|
167
|
+
# Coerce) but not inside an arithmetic expression like `dur + $1`,
|
|
168
|
+
# which requires the actual wire Kind.
|
|
169
|
+
# StructValue / MapValue (Collections track, docs/design-collections.md):
|
|
170
|
+
# explicit STRUCT / MAP parameter wrappers. StructValue.fields is an
|
|
171
|
+
# ordered Array of [name, value]; MapValue.entries is a Hash or an Array
|
|
172
|
+
# of [key, value].
|
|
173
|
+
StructValue = Struct.new(:fields, keyword_init: true)
|
|
174
|
+
MapValue = Struct.new(:entries, keyword_init: true)
|
|
175
|
+
|
|
176
|
+
# Geometry (Spatial track, docs/design-spatial.md): a decoded GEOMETRY /
|
|
177
|
+
# GEOGRAPHY value. +type+ is the OGC subtype name, +coordinates+ nested
|
|
178
|
+
# per type (Point: [x, y]); a GeometryCollection uses +geometries+
|
|
179
|
+
# instead. Also doubles as an explicit param wrapper: Geometry.new(type:
|
|
180
|
+
# nil, srid: 4326, wkt: "POINT(1 2)").
|
|
181
|
+
Geometry = Struct.new(:type, :srid, :coordinates, :geometries, :wkt, keyword_init: true) do
|
|
182
|
+
def to_wkt
|
|
183
|
+
return wkt if wkt
|
|
184
|
+
|
|
185
|
+
pt = ->(xy) { "#{xy[0]} #{xy[1]}" }
|
|
186
|
+
ring = ->(r) { "(#{r.map(&pt).join(', ')})" }
|
|
187
|
+
case type
|
|
188
|
+
when 'Point' then "POINT(#{pt.call(coordinates)})"
|
|
189
|
+
when 'LineString' then "LINESTRING(#{coordinates.map(&pt).join(', ')})"
|
|
190
|
+
when 'Polygon' then "POLYGON(#{coordinates.map(&ring).join(', ')})"
|
|
191
|
+
when 'MultiPoint' then "MULTIPOINT(#{coordinates.map { |c| "(#{pt.call(c)})" }.join(', ')})"
|
|
192
|
+
when 'MultiLineString' then "MULTILINESTRING(#{coordinates.map(&ring).join(', ')})"
|
|
193
|
+
when 'MultiPolygon'
|
|
194
|
+
"MULTIPOLYGON(#{coordinates.map { |poly| "(#{poly.map(&ring).join(', ')})" }.join(', ')})"
|
|
195
|
+
when 'GeometryCollection'
|
|
196
|
+
"GEOMETRYCOLLECTION(#{geometries.map(&:to_wkt).join(', ')})"
|
|
197
|
+
else
|
|
198
|
+
raise Error.new('invalid_argument', 'unsupported geometry type')
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
Interval = Struct.new(:months, :days, :nanos) do
|
|
204
|
+
def initialize(months:, days:, nanos:)
|
|
205
|
+
super(months, days, nanos)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
class ProtocolError < Error
|
|
210
|
+
def initialize(message)
|
|
211
|
+
super("protocol", message)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
module_function
|
|
216
|
+
|
|
217
|
+
def need!(b, off, n, what)
|
|
218
|
+
raise ProtocolError, "truncated #{what}" if off + n > b.bytesize
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def u16(b, off)
|
|
222
|
+
need!(b, off, 2, "u16")
|
|
223
|
+
b.byteslice(off, 2).unpack1("v")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def u32(b, off)
|
|
227
|
+
need!(b, off, 4, "u32")
|
|
228
|
+
b.byteslice(off, 4).unpack1("V")
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def u64(b, off)
|
|
232
|
+
need!(b, off, 8, "u64")
|
|
233
|
+
b.byteslice(off, 8).unpack1("Q<")
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def i64(b, off)
|
|
237
|
+
need!(b, off, 8, "i64")
|
|
238
|
+
b.byteslice(off, 8).unpack1("q<")
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def u16le(n) = [n].pack("v")
|
|
242
|
+
def u32le(n) = [n].pack("V")
|
|
243
|
+
def u64le(n) = [n & 0xFFFFFFFFFFFFFFFF].pack("Q<")
|
|
244
|
+
|
|
245
|
+
def u16str(s, max = MAX_NAME)
|
|
246
|
+
raw = s.to_s.b
|
|
247
|
+
raise ProtocolError, "string exceeds limit" if raw.bytesize > max || raw.bytesize > 0xFFFF
|
|
248
|
+
|
|
249
|
+
u16le(raw.bytesize) + raw
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def u32bytes(raw, max)
|
|
253
|
+
raw = raw.b
|
|
254
|
+
raise ProtocolError, "bytes exceed limit" if raw.bytesize > max
|
|
255
|
+
|
|
256
|
+
u32le(raw.bytesize) + raw
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def read_u16_string(b, off, max)
|
|
260
|
+
need!(b, off, 2, "string length")
|
|
261
|
+
n = u16(b, off)
|
|
262
|
+
raise ProtocolError, "truncated string" if n > max
|
|
263
|
+
|
|
264
|
+
need!(b, off + 2, n, "string")
|
|
265
|
+
[b.byteslice(off + 2, n).force_encoding("UTF-8"), off + 2 + n]
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def read_u32_bytes(b, off, max)
|
|
269
|
+
need!(b, off, 4, "bytes length")
|
|
270
|
+
n = u32(b, off)
|
|
271
|
+
raise ProtocolError, "truncated bytes" if n > max
|
|
272
|
+
|
|
273
|
+
need!(b, off + 4, n, "bytes")
|
|
274
|
+
[b.byteslice(off + 4, n), off + 4 + n]
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# append_enum_labels/read_enum_labels carry an ENUM Type's declared
|
|
278
|
+
# label list on the wire, matching internal/protocol/value.go's
|
|
279
|
+
# appendEnumLabels/readEnumLabels exactly (docs/design-datatypes.md
|
|
280
|
+
# D11): ENUM is the first D-track type whose Type needs variable-length
|
|
281
|
+
# metadata beyond the fixed 5/6-byte Precision/Scale/VecElem shape every
|
|
282
|
+
# other type fits into.
|
|
283
|
+
def append_enum_labels(labels)
|
|
284
|
+
out = +u16le(labels.size)
|
|
285
|
+
labels.each { |l| out << u16str(l, MAX_ENUM_LABEL_BYTES) }
|
|
286
|
+
out
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def read_enum_labels(b, off)
|
|
290
|
+
need!(b, off, 2, "enum label count")
|
|
291
|
+
n = u16(b, off)
|
|
292
|
+
raise ProtocolError, "enum label count exceeds limit" if n > MAX_ENUM_LABELS
|
|
293
|
+
|
|
294
|
+
off += 2
|
|
295
|
+
labels = []
|
|
296
|
+
n.times do
|
|
297
|
+
label, next_off = read_u16_string(b, off, MAX_ENUM_LABEL_BYTES)
|
|
298
|
+
labels << label
|
|
299
|
+
off = next_off
|
|
300
|
+
end
|
|
301
|
+
[labels, off]
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def encode_hello(version, flags, secret, database, user, realm = "")
|
|
305
|
+
sec = (secret || "").b
|
|
306
|
+
sec = sec[0, 8].ljust(8, "\x00")
|
|
307
|
+
out = u16le(version) + u16le(flags) + sec + u16str(database) + u16str(user)
|
|
308
|
+
# Realm is an optional trailing field (M2-2): emitted only when
|
|
309
|
+
# selected, so a Hello with no realm is byte-identical to the
|
|
310
|
+
# pre-realm wire shape.
|
|
311
|
+
out << u16str(realm) unless realm.nil? || realm.empty?
|
|
312
|
+
out
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def decode_hello_ok(b)
|
|
316
|
+
raise ProtocolError, "bad hello-ok length" unless b.bytesize == 11
|
|
317
|
+
|
|
318
|
+
[u16(b, 0), b.getbyte(2), b.byteslice(3, 8)]
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def encode_query(sql, params)
|
|
322
|
+
raise ProtocolError, "too many parameters" if params.size > MAX_PARAMS
|
|
323
|
+
|
|
324
|
+
out = +u32bytes(sql.to_s.b, MAX_SQL)
|
|
325
|
+
out << u16le(params.size)
|
|
326
|
+
params.each { |v| out << u16str("") << encode_param(v) }
|
|
327
|
+
out
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def encode_execute(stmt_id, params)
|
|
331
|
+
raise ProtocolError, "too many parameters" if params.size > MAX_PARAMS
|
|
332
|
+
|
|
333
|
+
out = +u32le(stmt_id)
|
|
334
|
+
out << u16le(params.size)
|
|
335
|
+
params.each { |v| out << u16str("") << encode_param(v) }
|
|
336
|
+
out
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def encode_idempotent_query(key, sql, params)
|
|
340
|
+
raise ProtocolError, "too many parameters" if params.size > MAX_PARAMS
|
|
341
|
+
|
|
342
|
+
out = +u16str(key)
|
|
343
|
+
out << u32bytes(sql.to_s.b, MAX_SQL)
|
|
344
|
+
out << u16le(params.size)
|
|
345
|
+
params.each { |v| out << u16str("") << encode_param(v) }
|
|
346
|
+
out
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def reserved5 = "\x00\x00\x00\x00\x00"
|
|
350
|
+
|
|
351
|
+
def encode_int(kind, value, min, max, pack_fmt)
|
|
352
|
+
raise Error.new("invalid_argument", "integer out of range") if value < min || value > max
|
|
353
|
+
|
|
354
|
+
(kind.chr + "\x00" + reserved5).b + [value].pack(pack_fmt)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def encode_uint(kind, value, max, pack_fmt)
|
|
358
|
+
raise Error.new("invalid_argument", "integer out of range") if value < 0 || value > max
|
|
359
|
+
|
|
360
|
+
(kind.chr + "\x00" + reserved5).b + [value].pack(pack_fmt)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def encode_enum(label, labels)
|
|
364
|
+
ord = labels.index(label)
|
|
365
|
+
raise Error.new("invalid_argument", "value is not a member of the ENUM label set") if ord.nil?
|
|
366
|
+
|
|
367
|
+
(KIND_ENUM.chr + "\x00" + reserved5).b + append_enum_labels(labels) + u16le(ord)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def encode_param(v)
|
|
371
|
+
case v
|
|
372
|
+
when nil
|
|
373
|
+
(KIND_STRING.chr + FLAG_NULL.chr + reserved5).b
|
|
374
|
+
when true, false
|
|
375
|
+
(KIND_BOOL.chr + "\x00" + reserved5 + (v ? "\x01" : "\x00")).b
|
|
376
|
+
when Integer
|
|
377
|
+
(KIND_DECIMAL.chr + "\x00" + reserved5).b + encode_decimal(v.to_s)
|
|
378
|
+
when BigDecimal
|
|
379
|
+
(KIND_DECIMAL.chr + "\x00" + reserved5).b + encode_decimal(v.to_s("F"))
|
|
380
|
+
when Float
|
|
381
|
+
raise Error.new("invalid_argument", "parameter is not finite") unless v.finite?
|
|
382
|
+
|
|
383
|
+
(KIND_DECIMAL.chr + "\x00" + reserved5).b + encode_decimal(format("%.17g", v))
|
|
384
|
+
when String
|
|
385
|
+
# A binary-encoded (ASCII-8BIT, e.g. via String#b) String is a BLOB;
|
|
386
|
+
# any other encoding (the UTF-8 default included) stays STRING.
|
|
387
|
+
kind = v.encoding == Encoding::ASCII_8BIT ? KIND_BLOB : KIND_STRING
|
|
388
|
+
(kind.chr + "\x00" + reserved5).b + u32bytes(v.b, MAX_PACKET)
|
|
389
|
+
when Time, DateTime
|
|
390
|
+
t = v.is_a?(DateTime) ? v.to_time : v
|
|
391
|
+
ns = (t.to_r * 1_000_000_000).to_i
|
|
392
|
+
(KIND_TIMESTAMPTZ.chr + "\x00" + reserved5).b + [ns].pack("q<")
|
|
393
|
+
when Date
|
|
394
|
+
# Checked after Time/DateTime above — DateTime is Date's own
|
|
395
|
+
# subclass in Ruby's stdlib, so only a bare Date reaches here.
|
|
396
|
+
day_count = (v - Date.new(1970, 1, 1)).to_i
|
|
397
|
+
(KIND_DATE.chr + "\x00" + reserved5).b + [day_count].pack("l<")
|
|
398
|
+
when NaiveTimestamp
|
|
399
|
+
# Treats v.value's own wall-clock fields as literal, ignoring
|
|
400
|
+
# whatever offset it carries — constructs a UTC Time from the same
|
|
401
|
+
# Y/M/D/H/M/S/usec fields rather than converting through the
|
|
402
|
+
# absolute instant, matching "the civil value read literally with
|
|
403
|
+
# no offset applied" (docs/design-datatypes.md D7). Converting
|
|
404
|
+
# through the absolute instant (t.to_r directly, as TimestampTZ
|
|
405
|
+
# does above) would be wrong here: a local-zoned Time's wall-clock
|
|
406
|
+
# reading is not its UTC epoch value.
|
|
407
|
+
t = v.value
|
|
408
|
+
t = t.to_time if t.is_a?(DateTime)
|
|
409
|
+
usec = (t.subsec * 1_000_000).to_i
|
|
410
|
+
utc_civil = Time.utc(t.year, t.month, t.day, t.hour, t.min, t.sec, usec)
|
|
411
|
+
ns = (utc_civil.to_r * 1_000_000_000).to_i
|
|
412
|
+
(KIND_TIMESTAMP.chr + "\x00" + reserved5).b + [ns].pack("q<")
|
|
413
|
+
when TimeOfDay
|
|
414
|
+
(KIND_TIME.chr + "\x00" + reserved5).b + [v.nanos_since_midnight].pack("Q<")
|
|
415
|
+
when Float32
|
|
416
|
+
# NaN/+-Infinity are valid FLOAT32/FLOAT64 values (unlike the bare
|
|
417
|
+
# Float -> Decimal path above, which requires finite) — the server
|
|
418
|
+
# canonicalizes -0.0 -> +0.0 and every NaN payload to one value
|
|
419
|
+
# (docs/design-datatypes.md D8).
|
|
420
|
+
(KIND_FLOAT32.chr + "\x00" + reserved5).b + [v.value].pack("e")
|
|
421
|
+
when Float64
|
|
422
|
+
(KIND_FLOAT64.chr + "\x00" + reserved5).b + [v.value].pack("E")
|
|
423
|
+
when Interval
|
|
424
|
+
(KIND_INTERVAL.chr + "\x00" + reserved5).b + [v.months, v.days].pack("l<l<") + [v.nanos].pack("q<")
|
|
425
|
+
when Int8
|
|
426
|
+
encode_int(KIND_INT8, v.value, -0x80, 0x7f, "c")
|
|
427
|
+
when Int16
|
|
428
|
+
encode_int(KIND_INT16, v.value, -0x8000, 0x7fff, "s<")
|
|
429
|
+
when Int32
|
|
430
|
+
encode_int(KIND_INT32, v.value, -0x80000000, 0x7fffffff, "l<")
|
|
431
|
+
when Int64
|
|
432
|
+
encode_int(KIND_INT64, v.value, -0x8000000000000000, 0x7fffffffffffffff, "q<")
|
|
433
|
+
when Uint8
|
|
434
|
+
encode_uint(KIND_UINT8, v.value, 0xff, "C")
|
|
435
|
+
when Uint16
|
|
436
|
+
encode_uint(KIND_UINT16, v.value, 0xffff, "S<")
|
|
437
|
+
when Uint32
|
|
438
|
+
encode_uint(KIND_UINT32, v.value, 0xffffffff, "L<")
|
|
439
|
+
when Uint64
|
|
440
|
+
encode_uint(KIND_UINT64, v.value, 0xffffffffffffffff, "Q<")
|
|
441
|
+
when EnumValue
|
|
442
|
+
encode_enum(v.value, v.labels)
|
|
443
|
+
when Point
|
|
444
|
+
(KIND_POINT.chr + "\x00" + reserved5).b + [v.lon, v.lat].pack("E2")
|
|
445
|
+
when Box
|
|
446
|
+
(KIND_BOX.chr + "\x00" + reserved5).b + [v.west, v.south, v.east, v.north].pack("E4")
|
|
447
|
+
when Vector
|
|
448
|
+
encode_vector(v)
|
|
449
|
+
when StructValue, MapValue
|
|
450
|
+
encode_collection_param(v)
|
|
451
|
+
when Geometry
|
|
452
|
+
wkt = v.wkt || v.to_wkt
|
|
453
|
+
wkt = "SRID=#{v.srid};#{wkt}" if v.srid && wkt !~ /\ASRID=/i
|
|
454
|
+
(KIND_STRING.chr + "\x00" + reserved5).b + u32bytes(wkt.b, MAX_PACKET)
|
|
455
|
+
when Array
|
|
456
|
+
if !v.empty? && v.all? { |x| x.is_a?(Numeric) }
|
|
457
|
+
encode_vector(Vector.new(dim: v.size, values: v.map(&:to_f)))
|
|
458
|
+
else
|
|
459
|
+
# A non-numeric (or empty) Array is an ARRAY collection param; the
|
|
460
|
+
# server re-coerces element types against the destination column.
|
|
461
|
+
encode_collection_param(v)
|
|
462
|
+
end
|
|
463
|
+
when Hash
|
|
464
|
+
json = JSON.generate(v)
|
|
465
|
+
(KIND_STRING.chr + "\x00" + reserved5).b + u32bytes(json.b, MAX_PACKET)
|
|
466
|
+
else
|
|
467
|
+
raise Error.new("invalid_argument", "unsupported parameter type: #{v.class}")
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# --- Collections (STRUCT / ARRAY / MAP), docs/design-collections.md -------
|
|
472
|
+
|
|
473
|
+
def read_type_full(b, off, depth)
|
|
474
|
+
need!(b, off, 6, "type")
|
|
475
|
+
t = { kind: b.getbyte(off), precision: u16(b, off + 1), scale: u16(b, off + 3), elem: b.getbyte(off + 5) }
|
|
476
|
+
nxt = read_nested_descriptor(b, off + 6, t, depth)
|
|
477
|
+
[t, nxt]
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def read_nested_descriptor(b, off, t, depth)
|
|
481
|
+
raise ProtocolError, "collection type nesting too deep" if depth > MAX_NEST_DEPTH + 1
|
|
482
|
+
|
|
483
|
+
case t[:kind]
|
|
484
|
+
when KIND_ENUM
|
|
485
|
+
labels, off = read_enum_labels(b, off)
|
|
486
|
+
t[:labels] = labels
|
|
487
|
+
off
|
|
488
|
+
when KIND_ARRAY
|
|
489
|
+
et, off = read_type_full(b, off, depth + 1)
|
|
490
|
+
t[:elem_type] = et
|
|
491
|
+
off
|
|
492
|
+
when KIND_MAP
|
|
493
|
+
kt, off = read_type_full(b, off, depth + 1)
|
|
494
|
+
vt, off = read_type_full(b, off, depth + 1)
|
|
495
|
+
t[:key_type] = kt
|
|
496
|
+
t[:elem_type] = vt
|
|
497
|
+
off
|
|
498
|
+
when KIND_STRUCT
|
|
499
|
+
need!(b, off, 2, "struct field count")
|
|
500
|
+
n = u16(b, off)
|
|
501
|
+
raise ProtocolError, "struct field count out of range" if n.zero? || n > MAX_STRUCT_FIELDS
|
|
502
|
+
|
|
503
|
+
off += 2
|
|
504
|
+
fields = []
|
|
505
|
+
n.times do
|
|
506
|
+
name, off = read_u16_string(b, off, 255)
|
|
507
|
+
ft, off = read_type_full(b, off, depth + 1)
|
|
508
|
+
fields << [name, ft]
|
|
509
|
+
end
|
|
510
|
+
t[:fields] = fields
|
|
511
|
+
off
|
|
512
|
+
else
|
|
513
|
+
off
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def decode_payload(b, off, t)
|
|
518
|
+
kind = t[:kind]
|
|
519
|
+
return decode_collection_payload(b, off, t) if [KIND_STRUCT, KIND_ARRAY, KIND_MAP].include?(kind)
|
|
520
|
+
|
|
521
|
+
header = (kind.chr + "\x00" + reserved5).b
|
|
522
|
+
header << append_enum_labels(t[:labels] || []) if kind == KIND_ENUM
|
|
523
|
+
synthetic = header + b.byteslice(off, b.bytesize - off)
|
|
524
|
+
value, nxt, = decode_value(synthetic, 0)
|
|
525
|
+
[value, off + (nxt - header.bytesize)]
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def decode_collection_payload(b, off, t)
|
|
529
|
+
need!(b, off, 4, "collection")
|
|
530
|
+
body_len = u32(b, off)
|
|
531
|
+
body_end = off + 4 + body_len
|
|
532
|
+
need!(b, off + 4, body_len, "collection body")
|
|
533
|
+
p = off + 4
|
|
534
|
+
n = u32(b, p)
|
|
535
|
+
p += 4
|
|
536
|
+
raise ProtocolError, "collection member count out of range" if n > (2 * MAX_COLLECTION_LEN) + 2 || n > body_len
|
|
537
|
+
|
|
538
|
+
nb = (n + 7) / 8
|
|
539
|
+
nulls = b.byteslice(p, nb)
|
|
540
|
+
p += nb
|
|
541
|
+
kind = t[:kind]
|
|
542
|
+
members = []
|
|
543
|
+
n.times do |i|
|
|
544
|
+
if (nulls.getbyte(i / 8) & (1 << (i % 8))) != 0
|
|
545
|
+
members << nil
|
|
546
|
+
next
|
|
547
|
+
end
|
|
548
|
+
mt = if kind == KIND_STRUCT
|
|
549
|
+
t[:fields][i][1]
|
|
550
|
+
elsif kind == KIND_ARRAY
|
|
551
|
+
t[:elem_type]
|
|
552
|
+
else
|
|
553
|
+
i.even? ? t[:key_type] : t[:elem_type]
|
|
554
|
+
end
|
|
555
|
+
value, p = decode_payload(b, p, mt)
|
|
556
|
+
members << value
|
|
557
|
+
end
|
|
558
|
+
case kind
|
|
559
|
+
when KIND_STRUCT
|
|
560
|
+
out = {}
|
|
561
|
+
t[:fields].each_with_index { |(name, _), i| out[name] = members[i] }
|
|
562
|
+
[out, body_end]
|
|
563
|
+
when KIND_ARRAY
|
|
564
|
+
[members, body_end]
|
|
565
|
+
else
|
|
566
|
+
out = {}
|
|
567
|
+
(0...(members.size - 1)).step(2) { |i| out[members[i]] = members[i + 1] }
|
|
568
|
+
[out, body_end]
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def encode_type_full(t)
|
|
573
|
+
out = +(t[:kind].chr + reserved5).b
|
|
574
|
+
case t[:kind]
|
|
575
|
+
when KIND_ENUM
|
|
576
|
+
out << append_enum_labels(t[:labels] || [])
|
|
577
|
+
when KIND_ARRAY
|
|
578
|
+
out << encode_type_full(t[:elem_type])
|
|
579
|
+
when KIND_MAP
|
|
580
|
+
out << encode_type_full(t[:key_type]) << encode_type_full(t[:elem_type])
|
|
581
|
+
when KIND_STRUCT
|
|
582
|
+
out << u16le(t[:fields].size)
|
|
583
|
+
t[:fields].each { |name, ft| out << u16str(name, 255) << encode_type_full(ft) }
|
|
584
|
+
end
|
|
585
|
+
out
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def infer_value(v)
|
|
589
|
+
case v
|
|
590
|
+
when nil
|
|
591
|
+
return [{ kind: KIND_STRING }, nil]
|
|
592
|
+
when StructValue
|
|
593
|
+
fields = []
|
|
594
|
+
payloads = []
|
|
595
|
+
v.fields.each do |name, fv|
|
|
596
|
+
ft, pl = infer_value(fv)
|
|
597
|
+
fields << [name.to_s, ft]
|
|
598
|
+
payloads << pl
|
|
599
|
+
end
|
|
600
|
+
return [{ kind: KIND_STRUCT, fields: fields }, collection_payload(payloads)]
|
|
601
|
+
when MapValue
|
|
602
|
+
items = v.entries.is_a?(Hash) ? v.entries.to_a : v.entries
|
|
603
|
+
types = []
|
|
604
|
+
payloads = []
|
|
605
|
+
items.each do |k, val|
|
|
606
|
+
kt, kp = infer_value(k)
|
|
607
|
+
vt, vp = infer_value(val)
|
|
608
|
+
types << kt << vt
|
|
609
|
+
payloads << kp << vp
|
|
610
|
+
end
|
|
611
|
+
ki = (0...payloads.size).step(2).find { |i| !payloads[i].nil? }
|
|
612
|
+
vi = (1...payloads.size).step(2).find { |i| !payloads[i].nil? }
|
|
613
|
+
key_type = ki ? types[ki] : { kind: KIND_STRING }
|
|
614
|
+
val_type = vi ? types[vi] : { kind: KIND_STRING }
|
|
615
|
+
return [{ kind: KIND_MAP, key_type: key_type, elem_type: val_type }, collection_payload(payloads)]
|
|
616
|
+
when Array
|
|
617
|
+
types = []
|
|
618
|
+
payloads = []
|
|
619
|
+
v.each do |x|
|
|
620
|
+
xt, xp = infer_value(x)
|
|
621
|
+
types << xt
|
|
622
|
+
payloads << xp
|
|
623
|
+
end
|
|
624
|
+
ei = payloads.index { |pl| !pl.nil? }
|
|
625
|
+
elem_type = ei ? types[ei] : { kind: KIND_STRING }
|
|
626
|
+
return [{ kind: KIND_ARRAY, elem_type: elem_type }, collection_payload(payloads)]
|
|
627
|
+
end
|
|
628
|
+
enc = encode_param(v)
|
|
629
|
+
kind = enc.getbyte(0)
|
|
630
|
+
hdr = 7
|
|
631
|
+
if kind == KIND_ENUM
|
|
632
|
+
lc = u16(enc, 7)
|
|
633
|
+
hdr = 9
|
|
634
|
+
lc.times { hdr += 2 + u16(enc, hdr) }
|
|
635
|
+
end
|
|
636
|
+
[{ kind: kind }, enc.byteslice(hdr, enc.bytesize - hdr)]
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def collection_payload(payloads)
|
|
640
|
+
n = payloads.size
|
|
641
|
+
nb = (n + 7) / 8
|
|
642
|
+
nulls = Array.new(nb, 0)
|
|
643
|
+
chunks = +"".b
|
|
644
|
+
payloads.each_with_index do |pl, i|
|
|
645
|
+
if pl.nil?
|
|
646
|
+
nulls[i / 8] |= 1 << (i % 8)
|
|
647
|
+
else
|
|
648
|
+
chunks << pl
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
body = u32le(n) + nulls.pack("C*") + chunks
|
|
652
|
+
u32le(body.bytesize) + body
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def encode_collection_param(v)
|
|
656
|
+
t, payload = infer_value(v)
|
|
657
|
+
full = encode_type_full(t)
|
|
658
|
+
type_body = full.byteslice(1, full.bytesize - 1)
|
|
659
|
+
(t[:kind].chr + "\x00").b + type_body + (payload || "".b)
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
# --- Spatial: EWKB decode (Spatial track, docs/design-spatial.md) ------
|
|
663
|
+
|
|
664
|
+
EWKB_TYPES = {
|
|
665
|
+
1 => "Point", 2 => "LineString", 3 => "Polygon",
|
|
666
|
+
4 => "MultiPoint", 5 => "MultiLineString", 6 => "MultiPolygon",
|
|
667
|
+
7 => "GeometryCollection"
|
|
668
|
+
}.freeze
|
|
669
|
+
EWKB_SRID_FLAG = 0x20000000
|
|
670
|
+
|
|
671
|
+
def decode_ewkb(b, off, depth)
|
|
672
|
+
raise ProtocolError, "geometry nesting too deep" if depth > 8
|
|
673
|
+
|
|
674
|
+
need!(b, off, 5, "geometry header")
|
|
675
|
+
raise ProtocolError, "only little-endian EWKB is supported" unless b.getbyte(off) == 1
|
|
676
|
+
|
|
677
|
+
tword = u32(b, off + 1)
|
|
678
|
+
gtype = tword & ~EWKB_SRID_FLAG
|
|
679
|
+
p = off + 5
|
|
680
|
+
srid = 0
|
|
681
|
+
if (tword & EWKB_SRID_FLAG) != 0
|
|
682
|
+
srid = u32(b, p)
|
|
683
|
+
p += 4
|
|
684
|
+
end
|
|
685
|
+
name = EWKB_TYPES[gtype]
|
|
686
|
+
raise ProtocolError, "unknown geometry type" unless name
|
|
687
|
+
|
|
688
|
+
f64 = lambda {
|
|
689
|
+
v = b.byteslice(p, 8).unpack1("E")
|
|
690
|
+
p += 8
|
|
691
|
+
v
|
|
692
|
+
}
|
|
693
|
+
u32f = lambda {
|
|
694
|
+
v = u32(b, p)
|
|
695
|
+
p += 4
|
|
696
|
+
v
|
|
697
|
+
}
|
|
698
|
+
pts = ->(n) { Array.new(n) { [f64.call, f64.call] } }
|
|
699
|
+
|
|
700
|
+
case gtype
|
|
701
|
+
when 1
|
|
702
|
+
[Geometry.new(type: name, srid: srid, coordinates: [f64.call, f64.call]), p]
|
|
703
|
+
when 2
|
|
704
|
+
[Geometry.new(type: name, srid: srid, coordinates: pts.call(u32f.call)), p]
|
|
705
|
+
when 3
|
|
706
|
+
nr = u32f.call
|
|
707
|
+
rings = Array.new(nr) { pts.call(u32f.call) }
|
|
708
|
+
[Geometry.new(type: name, srid: srid, coordinates: rings), p]
|
|
709
|
+
else
|
|
710
|
+
np = u32f.call
|
|
711
|
+
parts = []
|
|
712
|
+
np.times do
|
|
713
|
+
sub, p = decode_ewkb(b, p, depth + 1)
|
|
714
|
+
parts << sub
|
|
715
|
+
end
|
|
716
|
+
if gtype == 7
|
|
717
|
+
[Geometry.new(type: name, srid: srid, geometries: parts), p]
|
|
718
|
+
else
|
|
719
|
+
[Geometry.new(type: name, srid: srid, coordinates: parts.map(&:coordinates)), p]
|
|
720
|
+
end
|
|
721
|
+
end
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
def decode_value(b, off)
|
|
725
|
+
need!(b, off, 7, "value header")
|
|
726
|
+
kind = b.getbyte(off)
|
|
727
|
+
flags = b.getbyte(off + 1)
|
|
728
|
+
off += 7
|
|
729
|
+
enum_labels = nil
|
|
730
|
+
coll_type = nil
|
|
731
|
+
if kind == KIND_ENUM
|
|
732
|
+
enum_labels, off = read_enum_labels(b, off)
|
|
733
|
+
elsif [KIND_STRUCT, KIND_ARRAY, KIND_MAP].include?(kind)
|
|
734
|
+
coll_type = { kind: kind }
|
|
735
|
+
off = read_nested_descriptor(b, off, coll_type, 0)
|
|
736
|
+
end
|
|
737
|
+
return [nil, off, kind] if flags & FLAG_NULL != 0
|
|
738
|
+
|
|
739
|
+
if [KIND_STRUCT, KIND_ARRAY, KIND_MAP].include?(kind)
|
|
740
|
+
value, nxt = decode_collection_payload(b, off, coll_type)
|
|
741
|
+
return [value, nxt, kind]
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
case kind
|
|
745
|
+
when KIND_ENUM
|
|
746
|
+
need!(b, off, 2, "enum")
|
|
747
|
+
ord = u16(b, off)
|
|
748
|
+
raise ProtocolError, "ENUM ordinal out of range" if ord >= enum_labels.size
|
|
749
|
+
|
|
750
|
+
[enum_labels[ord], off + 2, kind]
|
|
751
|
+
when KIND_UUID
|
|
752
|
+
need!(b, off, 16, "uuid")
|
|
753
|
+
[format_uuid(b.byteslice(off, 16)), off + 16, kind]
|
|
754
|
+
when KIND_STRING, KIND_TEXT, KIND_CHAR, KIND_VARCHAR
|
|
755
|
+
raw, next_off = read_u32_bytes(b, off, MAX_PACKET)
|
|
756
|
+
[raw.dup.force_encoding("UTF-8"), next_off, kind]
|
|
757
|
+
when KIND_BLOB
|
|
758
|
+
raw, next_off = read_u32_bytes(b, off, MAX_PACKET)
|
|
759
|
+
[raw.dup.force_encoding("ASCII-8BIT"), next_off, kind]
|
|
760
|
+
when KIND_JSON
|
|
761
|
+
raw, next_off = read_u32_bytes(b, off, MAX_PACKET)
|
|
762
|
+
[decode_nsjb(raw), next_off, kind]
|
|
763
|
+
when KIND_DECIMAL
|
|
764
|
+
raw, next_off = read_u32_bytes(b, off, MAX_PACKET)
|
|
765
|
+
[decode_decimal(raw), next_off, kind]
|
|
766
|
+
when KIND_TIMESTAMPTZ
|
|
767
|
+
ns = i64(b, off)
|
|
768
|
+
sec, nsec = ns.divmod(1_000_000_000)
|
|
769
|
+
[Time.at(sec, nsec, :nanosecond, in: "UTC"), off + 8, kind]
|
|
770
|
+
when KIND_TIMESTAMP
|
|
771
|
+
# Naive/no-timezone: same wire shape as TimestampTZ. Ruby has no
|
|
772
|
+
# distinct naive-time type, so this returns a UTC-tagged Time whose
|
|
773
|
+
# fields are the intended civil value — same convention as
|
|
774
|
+
# TimestampTZ's own decode above, just carrying no real zone
|
|
775
|
+
# information (docs/design-datatypes.md D7).
|
|
776
|
+
ns = i64(b, off)
|
|
777
|
+
sec, nsec = ns.divmod(1_000_000_000)
|
|
778
|
+
[Time.at(sec, nsec, :nanosecond, in: "UTC"), off + 8, kind]
|
|
779
|
+
when KIND_DATE
|
|
780
|
+
need!(b, off, 4, "date")
|
|
781
|
+
day_count = b.byteslice(off, 4).unpack1("l<")
|
|
782
|
+
[Date.new(1970, 1, 1) + day_count, off + 4, kind]
|
|
783
|
+
when KIND_TIME
|
|
784
|
+
need!(b, off, 8, "time")
|
|
785
|
+
[u64(b, off), off + 8, kind]
|
|
786
|
+
when KIND_FLOAT32
|
|
787
|
+
need!(b, off, 4, "float32")
|
|
788
|
+
[b.byteslice(off, 4).unpack1("e"), off + 4, kind]
|
|
789
|
+
when KIND_FLOAT64
|
|
790
|
+
need!(b, off, 8, "float64")
|
|
791
|
+
[b.byteslice(off, 8).unpack1("E"), off + 8, kind]
|
|
792
|
+
when KIND_INTERVAL
|
|
793
|
+
need!(b, off, 16, "interval")
|
|
794
|
+
months, days = b.byteslice(off, 8).unpack("l<l<")
|
|
795
|
+
nanos = b.byteslice(off + 8, 8).unpack1("q<")
|
|
796
|
+
[Interval.new(months: months, days: days, nanos: nanos), off + 16, kind]
|
|
797
|
+
when KIND_BOOL
|
|
798
|
+
need!(b, off, 1, "bool")
|
|
799
|
+
[b.getbyte(off) != 0, off + 1, kind]
|
|
800
|
+
when KIND_INT8
|
|
801
|
+
need!(b, off, 1, "int8")
|
|
802
|
+
[b.byteslice(off, 1).unpack1("c"), off + 1, kind]
|
|
803
|
+
when KIND_INT16
|
|
804
|
+
need!(b, off, 2, "int16")
|
|
805
|
+
[b.byteslice(off, 2).unpack1("s<"), off + 2, kind]
|
|
806
|
+
when KIND_INT32
|
|
807
|
+
need!(b, off, 4, "int32")
|
|
808
|
+
[b.byteslice(off, 4).unpack1("l<"), off + 4, kind]
|
|
809
|
+
when KIND_INT64
|
|
810
|
+
need!(b, off, 8, "int64")
|
|
811
|
+
[b.byteslice(off, 8).unpack1("q<"), off + 8, kind]
|
|
812
|
+
when KIND_UINT8
|
|
813
|
+
need!(b, off, 1, "uint8")
|
|
814
|
+
[b.getbyte(off), off + 1, kind]
|
|
815
|
+
when KIND_UINT16
|
|
816
|
+
need!(b, off, 2, "uint16")
|
|
817
|
+
[b.byteslice(off, 2).unpack1("S<"), off + 2, kind]
|
|
818
|
+
when KIND_UINT32
|
|
819
|
+
need!(b, off, 4, "uint32")
|
|
820
|
+
[b.byteslice(off, 4).unpack1("L<"), off + 4, kind]
|
|
821
|
+
when KIND_UINT64
|
|
822
|
+
need!(b, off, 8, "uint64")
|
|
823
|
+
[b.byteslice(off, 8).unpack1("Q<"), off + 8, kind]
|
|
824
|
+
when KIND_VECTOR
|
|
825
|
+
value, next_off = decode_vector(b, off)
|
|
826
|
+
[value, next_off, kind]
|
|
827
|
+
when KIND_POINT
|
|
828
|
+
need!(b, off, 16, "point")
|
|
829
|
+
lon, lat = b.byteslice(off, 16).unpack("E2")
|
|
830
|
+
[Point.new(lon, lat), off + 16, kind]
|
|
831
|
+
when KIND_BOX
|
|
832
|
+
need!(b, off, 32, "box")
|
|
833
|
+
w, s, e, n = b.byteslice(off, 32).unpack("E4")
|
|
834
|
+
[Box.new(w, s, e, n), off + 32, kind]
|
|
835
|
+
when KIND_LINE
|
|
836
|
+
n = u16(b, off)
|
|
837
|
+
p = off + 2
|
|
838
|
+
coords = []
|
|
839
|
+
(n * 2).times do
|
|
840
|
+
need!(b, p, 8, "line coord")
|
|
841
|
+
coords << b.byteslice(p, 8).unpack1("E")
|
|
842
|
+
p += 8
|
|
843
|
+
end
|
|
844
|
+
[Line.new(coords), p, kind]
|
|
845
|
+
when KIND_GEOMETRY, KIND_GEOGRAPHY
|
|
846
|
+
len = u32(b, off)
|
|
847
|
+
g, = decode_ewkb(b, off + 4, 0)
|
|
848
|
+
[g, off + 4 + len, kind]
|
|
849
|
+
when KIND_POLYGON
|
|
850
|
+
nr = u16(b, off)
|
|
851
|
+
p = off + 2
|
|
852
|
+
rings = []
|
|
853
|
+
nr.times do
|
|
854
|
+
npts = u16(b, p)
|
|
855
|
+
p += 2
|
|
856
|
+
ring = []
|
|
857
|
+
(npts * 2).times do
|
|
858
|
+
need!(b, p, 8, "polygon coord")
|
|
859
|
+
ring << b.byteslice(p, 8).unpack1("E")
|
|
860
|
+
p += 8
|
|
861
|
+
end
|
|
862
|
+
rings << ring
|
|
863
|
+
end
|
|
864
|
+
[Polygon.new(rings), p, kind]
|
|
865
|
+
else
|
|
866
|
+
raise ProtocolError, "unsupported type"
|
|
867
|
+
end
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
def decode_vector(b, off)
|
|
871
|
+
dim = u16(b, off)
|
|
872
|
+
flag = b.getbyte(off + 2)
|
|
873
|
+
if flag & 1 != 0
|
|
874
|
+
return [Vector.new(dim: dim, ref: true), off + 3]
|
|
875
|
+
end
|
|
876
|
+
if flag & 2 != 0
|
|
877
|
+
need!(b, off + 3, 4, "sparse nnz")
|
|
878
|
+
nnz = u32(b, off + 3)
|
|
879
|
+
indices = []
|
|
880
|
+
values = []
|
|
881
|
+
p = off + 7
|
|
882
|
+
nnz.times do
|
|
883
|
+
need!(b, p, 8, "sparse entry")
|
|
884
|
+
indices << u32(b, p)
|
|
885
|
+
values << b.byteslice(p + 4, 4).unpack1("e")
|
|
886
|
+
p += 8
|
|
887
|
+
end
|
|
888
|
+
return [Vector.new(dim: dim, values: values, indices: indices), p]
|
|
889
|
+
end
|
|
890
|
+
p = off + 3
|
|
891
|
+
values = []
|
|
892
|
+
dim.times do
|
|
893
|
+
need!(b, p, 4, "vector component")
|
|
894
|
+
values << b.byteslice(p, 4).unpack1("e")
|
|
895
|
+
p += 4
|
|
896
|
+
end
|
|
897
|
+
[Vector.new(dim: dim, values: values), p]
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
def decode_row_desc(b)
|
|
901
|
+
n = u16(b, 0)
|
|
902
|
+
off = 2
|
|
903
|
+
cols = []
|
|
904
|
+
n.times do
|
|
905
|
+
name, off2 = read_u16_string(b, off, MAX_NAME)
|
|
906
|
+
off = off2
|
|
907
|
+
need!(b, off, 6, "column type")
|
|
908
|
+
kind = b.getbyte(off)
|
|
909
|
+
off += 6
|
|
910
|
+
labels = nil
|
|
911
|
+
coll_type = nil
|
|
912
|
+
if kind == KIND_ENUM
|
|
913
|
+
labels, off = read_enum_labels(b, off)
|
|
914
|
+
elsif [KIND_STRUCT, KIND_ARRAY, KIND_MAP].include?(kind)
|
|
915
|
+
coll_type = { kind: kind }
|
|
916
|
+
off = read_nested_descriptor(b, off, coll_type, 0)
|
|
917
|
+
end
|
|
918
|
+
cols << Column.new(name, kind, labels, coll_type)
|
|
919
|
+
end
|
|
920
|
+
cols
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
def decode_data_batch(b)
|
|
924
|
+
nrows = u32(b, 0)
|
|
925
|
+
off = 4
|
|
926
|
+
rows = []
|
|
927
|
+
nrows.times do
|
|
928
|
+
ncols = u16(b, off)
|
|
929
|
+
off += 2
|
|
930
|
+
row = []
|
|
931
|
+
ncols.times do
|
|
932
|
+
value, next_off, = decode_value(b, off)
|
|
933
|
+
row << value
|
|
934
|
+
off = next_off
|
|
935
|
+
end
|
|
936
|
+
rows << row
|
|
937
|
+
end
|
|
938
|
+
rows
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
def decode_command_complete(b)
|
|
942
|
+
raise ProtocolError, "bad command-complete length" unless b.bytesize == 8
|
|
943
|
+
|
|
944
|
+
u64(b, 0)
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
def decode_error(b)
|
|
948
|
+
code, off = read_u16_string(b, 0, MAX_NAME)
|
|
949
|
+
msg, = read_u16_string(b, off, MAX_NAME)
|
|
950
|
+
Error.new(code, msg)
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
def encode_set_read_consistency(mode, max_staleness_ms)
|
|
954
|
+
raise Error.new("invalid_argument", "unknown read consistency mode") unless [READ_STRONG, READ_BOUNDED,
|
|
955
|
+
READ_STALE].include?(mode)
|
|
956
|
+
|
|
957
|
+
ms = max_staleness_ms.positive? ? max_staleness_ms : 0
|
|
958
|
+
mode.chr + u64le(ms)
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
def decode_node_status(b)
|
|
962
|
+
role, off = read_u16_string(b, 0, MAX_NAME)
|
|
963
|
+
raise ProtocolError, "bad node-status length" unless b.bytesize - off == 25
|
|
964
|
+
|
|
965
|
+
flags = b.getbyte(off)
|
|
966
|
+
off += 1
|
|
967
|
+
applied_lsn = u64(b, off)
|
|
968
|
+
raw_contact = u64(b, off + 8)
|
|
969
|
+
last_contact_ms = raw_contact == 0xFFFFFFFFFFFFFFFF ? -1 : raw_contact
|
|
970
|
+
apply_backlog = u64(b, off + 16)
|
|
971
|
+
NodeStatus.new(role, flags & 1 != 0, flags & 2 != 0, applied_lsn, last_contact_ms, apply_backlog)
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
DECIMAL_RE = /\A\d+(\.\d+)?\z/.freeze
|
|
975
|
+
|
|
976
|
+
def encode_decimal(s)
|
|
977
|
+
s = s.strip
|
|
978
|
+
neg = false
|
|
979
|
+
s = s[1..] if s.start_with?("+")
|
|
980
|
+
if s.start_with?("-")
|
|
981
|
+
neg = true
|
|
982
|
+
s = s[1..]
|
|
983
|
+
end
|
|
984
|
+
raise Error.new("invalid_argument", "invalid decimal") unless DECIMAL_RE.match?(s)
|
|
985
|
+
|
|
986
|
+
int_part, frac_part = s.split(".", 2)
|
|
987
|
+
frac_part ||= ""
|
|
988
|
+
scale = frac_part.length
|
|
989
|
+
digits = (int_part + frac_part).sub(/\A0+(?=\d)/, "")
|
|
990
|
+
coef = dec_to_bytes(digits)
|
|
991
|
+
body = (neg ? "\x01" : "\x00").b + "\x00".b + u16le(scale) + coef
|
|
992
|
+
u32bytes(body, MAX_PACKET)
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
def decode_decimal(body)
|
|
996
|
+
raise Error.new("invalid_format", "truncated decimal") if body.bytesize < 4
|
|
997
|
+
|
|
998
|
+
neg = body.getbyte(0) & 1 != 0
|
|
999
|
+
scale = u16(body, 2)
|
|
1000
|
+
digits = bytes_to_dec(body.byteslice(4, body.bytesize - 4))
|
|
1001
|
+
s = if scale.positive?
|
|
1002
|
+
digits = digits.rjust(scale + 1, "0")
|
|
1003
|
+
"#{digits[0..-scale - 1]}.#{digits[-scale..]}"
|
|
1004
|
+
else
|
|
1005
|
+
digits
|
|
1006
|
+
end
|
|
1007
|
+
s = "-#{s}" if neg && !(s == "0" || s.match?(/\A0\.0+\z/))
|
|
1008
|
+
BigDecimal(s)
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
def dec_to_bytes(digits)
|
|
1012
|
+
n = digits.to_i
|
|
1013
|
+
return "".b if n.zero?
|
|
1014
|
+
|
|
1015
|
+
nbytes = (n.bit_length + 7) / 8
|
|
1016
|
+
out = +"".b
|
|
1017
|
+
nbytes.times { |i| out.prepend(((n >> (8 * i)) & 0xFF).chr) }
|
|
1018
|
+
out
|
|
1019
|
+
end
|
|
1020
|
+
|
|
1021
|
+
def bytes_to_dec(raw)
|
|
1022
|
+
n = 0
|
|
1023
|
+
raw.each_byte { |byte| n = (n * 256) + byte }
|
|
1024
|
+
n.to_s
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
def format_uuid(raw)
|
|
1028
|
+
hex = raw.unpack1("H*")
|
|
1029
|
+
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
def encode_vector(v)
|
|
1033
|
+
dim = v.dim && v.dim.positive? ? v.dim : v.values.size
|
|
1034
|
+
header = (KIND_VECTOR.chr + "\x00" + reserved5).b
|
|
1035
|
+
if v.indices
|
|
1036
|
+
payload = +(u16le(dim) + "\x02")
|
|
1037
|
+
payload << u32le(v.indices.size)
|
|
1038
|
+
v.indices.each_with_index do |idx, i|
|
|
1039
|
+
payload << u32le(idx)
|
|
1040
|
+
payload << [v.values[i]].pack("e")
|
|
1041
|
+
end
|
|
1042
|
+
return header + payload
|
|
1043
|
+
end
|
|
1044
|
+
payload = +(u16le(dim) + "\x00")
|
|
1045
|
+
v.values.each { |val| payload << [val].pack("e") }
|
|
1046
|
+
header + payload
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
# --- NSJB binary JSON (see internal/protocol/messages.go EncodeJSON) ---
|
|
1050
|
+
|
|
1051
|
+
def decode_nsjb(doc)
|
|
1052
|
+
raise Error.new("invalid_format", "not binary JSON") if doc.bytesize < 5 || doc.byteslice(0,
|
|
1053
|
+
4) != "NSJB" || doc.getbyte(4) != 1
|
|
1054
|
+
|
|
1055
|
+
value, next_off = read_nsjb(doc, 5)
|
|
1056
|
+
raise Error.new("invalid_format", "trailing JSON bytes") unless next_off == doc.bytesize
|
|
1057
|
+
|
|
1058
|
+
value
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
def read_nsjb(b, off)
|
|
1062
|
+
raise Error.new("invalid_format", "truncated JSON") if off >= b.bytesize
|
|
1063
|
+
|
|
1064
|
+
tag = b.getbyte(off)
|
|
1065
|
+
case tag
|
|
1066
|
+
when 0x00 then [nil, off + 1]
|
|
1067
|
+
when 0x01 then [false, off + 1]
|
|
1068
|
+
when 0x02 then [true, off + 1]
|
|
1069
|
+
when 0x03 then [i64(b, off + 1), off + 9]
|
|
1070
|
+
when 0x04 then read_nsjb_str(b, off, false)
|
|
1071
|
+
when 0x05 then read_nsjb_str(b, off, true)
|
|
1072
|
+
when 0x06 then read_nsjb_array(b, off)
|
|
1073
|
+
when 0x07 then read_nsjb_object(b, off)
|
|
1074
|
+
else raise Error.new("invalid_format", "unknown JSON tag")
|
|
1075
|
+
end
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
def read_nsjb_str(b, off, number)
|
|
1079
|
+
n = u32(b, off + 1)
|
|
1080
|
+
end_off = off + 5 + n
|
|
1081
|
+
s = b.byteslice(off + 5, n).force_encoding("UTF-8")
|
|
1082
|
+
if number
|
|
1083
|
+
if /\A-?\d+\z/.match?(s)
|
|
1084
|
+
return [s.to_i, end_off]
|
|
1085
|
+
elsif /\A-?\d+\.\d+\z/.match?(s)
|
|
1086
|
+
return [s.to_f, end_off]
|
|
1087
|
+
end
|
|
1088
|
+
end
|
|
1089
|
+
[s, end_off]
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
def read_nsjb_array(b, off)
|
|
1093
|
+
size = u32(b, off + 1)
|
|
1094
|
+
body = off + 5
|
|
1095
|
+
end_off = body + size
|
|
1096
|
+
count = u32(b, body)
|
|
1097
|
+
cur = body + 4
|
|
1098
|
+
out = []
|
|
1099
|
+
count.times do
|
|
1100
|
+
v, cur2 = read_nsjb(b, cur)
|
|
1101
|
+
cur = cur2
|
|
1102
|
+
out << v
|
|
1103
|
+
end
|
|
1104
|
+
[out, end_off]
|
|
1105
|
+
end
|
|
1106
|
+
|
|
1107
|
+
def read_nsjb_object(b, off)
|
|
1108
|
+
size = u32(b, off + 1)
|
|
1109
|
+
body = off + 5
|
|
1110
|
+
end_off = body + size
|
|
1111
|
+
count = u16(b, body)
|
|
1112
|
+
cur = body + 2
|
|
1113
|
+
out = {}
|
|
1114
|
+
count.times do
|
|
1115
|
+
klen = u16(b, cur)
|
|
1116
|
+
cur += 2
|
|
1117
|
+
key = b.byteslice(cur, klen).force_encoding("UTF-8")
|
|
1118
|
+
cur += klen
|
|
1119
|
+
v, cur2 = read_nsjb(b, cur)
|
|
1120
|
+
cur = cur2
|
|
1121
|
+
out[key] = v
|
|
1122
|
+
end
|
|
1123
|
+
[out, end_off]
|
|
1124
|
+
end
|
|
1125
|
+
end
|
|
1126
|
+
end
|