Module: Tina4Ultipa::Codec
- Defined in:
- lib/tina4_ultipa/codec.rb
Defined Under Namespace
Classes: Reader
Constant Summary collapse
- NUMERIC =
PropertyType (as the enum symbol google-protobuf hands back) -> pack/unpack directive for the fixed-width little-endian numeric types.
{ PROPERTY_TYPE_INT32: "l<", PROPERTY_TYPE_UINT32: "L<", PROPERTY_TYPE_INT64: "q<", PROPERTY_TYPE_UINT64: "Q<", PROPERTY_TYPE_FLOAT: "e", PROPERTY_TYPE_DOUBLE: "E", }.freeze
- STRINGY =
%i[PROPERTY_TYPE_STRING PROPERTY_TYPE_TEXT].freeze
- NULLISH =
%i[PROPERTY_TYPE_NULL PROPERTY_TYPE_UNSET].freeze
- LISTY =
%i[PROPERTY_TYPE_LIST PROPERTY_TYPE_SET].freeze
- MAPPY =
%i[PROPERTY_TYPE_MAP PROPERTY_TYPE_RECORD].freeze
- LOCAL_DT =
%i[PROPERTY_TYPE_DATETIME PROPERTY_TYPE_LOCAL_DATETIME].freeze
- INT_TO_SYM =
A google-protobuf enum field usually reads back as a Symbol, but falls back to the raw Integer for values it does not know. Normalise to the symbol.
{ 0 => :PROPERTY_TYPE_UNSET, 1 => :PROPERTY_TYPE_INT32, 2 => :PROPERTY_TYPE_UINT32, 3 => :PROPERTY_TYPE_INT64, 4 => :PROPERTY_TYPE_UINT64, 5 => :PROPERTY_TYPE_FLOAT, 6 => :PROPERTY_TYPE_DOUBLE, 7 => :PROPERTY_TYPE_STRING, 8 => :PROPERTY_TYPE_DATETIME, 9 => :PROPERTY_TYPE_TIMESTAMP, 10 => :PROPERTY_TYPE_TEXT, 11 => :PROPERTY_TYPE_BLOB, 12 => :PROPERTY_TYPE_POINT, 13 => :PROPERTY_TYPE_DECIMAL, 14 => :PROPERTY_TYPE_LIST, 15 => :PROPERTY_TYPE_SET, 16 => :PROPERTY_TYPE_MAP, 17 => :PROPERTY_TYPE_NULL, 18 => :PROPERTY_TYPE_BOOL, 19 => :PROPERTY_TYPE_LOCAL_DATETIME, 20 => :PROPERTY_TYPE_ZONED_DATETIME, 21 => :PROPERTY_TYPE_DATE, 22 => :PROPERTY_TYPE_ZONED_TIME, 23 => :PROPERTY_TYPE_LOCAL_TIME, 24 => :PROPERTY_TYPE_YEAR_TO_MONTH, 25 => :PROPERTY_TYPE_DAY_TO_SECOND, 26 => :PROPERTY_TYPE_RECORD, 27 => :PROPERTY_TYPE_POINT3D, 28 => :PROPERTY_TYPE_VECTOR, 29 => :PROPERTY_TYPE_TABLE, 30 => :PROPERTY_TYPE_PATH, 31 => :PROPERTY_TYPE_ERROR, 32 => :PROPERTY_TYPE_NODE, 33 => :PROPERTY_TYPE_EDGE }.freeze
Class Method Summary collapse
-
.date(data) ⇒ Object
── temporal / spatial helpers ──────────────────────────────────────.
- .datetime_frac(base, nanos) ⇒ Object
- .day_to_second(data) ⇒ Object
-
.decode(tv) ⇒ Object
gqldb.TypedValue -> a Ruby value (nil for null; Hash for node/edge/...).
- .edge(data) ⇒ Object
-
.encode(value) ⇒ Object
A Ruby value -> gqldb.TypedValue, for query parameters.
- .error(data) ⇒ Object
-
.list(data) ⇒ Object
── composites ──────────────────────────────────────────────────────.
- .local_datetime(data) ⇒ Object
- .local_time(data) ⇒ Object
- .node(data) ⇒ Object
- .offset(minutes) ⇒ Object
- .path(data) ⇒ Object
- .point(data) ⇒ Object
- .point3d(data) ⇒ Object
-
.scalar(ptype, data) ⇒ Object
Decode a raw property/value payload of a given PropertyType.
- .table(data) ⇒ Object
- .time_str(hour, minute, sec, nanos) ⇒ Object
- .timestamp(data) ⇒ Object
-
.trailer(reader) ⇒ Object
Optional 8-byte little-endian uint64 internal-id trailer (gqldb 6.1.147+).
-
.unix(data) ⇒ Object
Legacy 8-byte unix time (seconds/ms/us) -> 'YYYY-MM-DD HH:MM:SS' (UTC).
- .vector(data) ⇒ Object
- .year_to_month(data) ⇒ Object
- .zoned_datetime(data) ⇒ Object
- .zoned_time(data) ⇒ Object
Class Method Details
.date(data) ⇒ Object
── temporal / spatial helpers ──────────────────────────────────────
59 60 61 62 63 64 |
# File 'lib/tina4_ultipa/codec.rb', line 59 def date(data) return nil if data.nil? || data.bytesize < 4 y = data.byteslice(0, 2).unpack1("s<") # signed year (BCE round-trips) format("%04d-%02d-%02d", y, data.getbyte(2), data.getbyte(3)) end |
.datetime_frac(base, nanos) ⇒ Object
94 95 96 97 98 |
# File 'lib/tina4_ultipa/codec.rb', line 94 def datetime_frac(base, nanos) return base if nanos.nil? || nanos.zero? base + format(".%09d", nanos).sub(/0+\z/, "").sub(/\.\z/, "") end |
.day_to_second(data) ⇒ Object
184 185 186 187 188 |
# File 'lib/tina4_ultipa/codec.rb', line 184 def day_to_second(data) return { "seconds" => 0, "nanoseconds" => 0 } if data.nil? || data.bytesize < 12 { "seconds" => data.unpack1("q<"), "nanoseconds" => data.unpack1("@8 L<") } end |
.decode(tv) ⇒ Object
gqldb.TypedValue -> a Ruby value (nil for null; Hash for node/edge/...).
362 363 364 365 366 |
# File 'lib/tina4_ultipa/codec.rb', line 362 def decode(tv) return nil if tv.is_null scalar(tv.type, tv.data) end |
.edge(data) ⇒ Object
289 290 291 292 293 294 295 296 297 298 |
# File 'lib/tina4_ultipa/codec.rb', line 289 def edge(data) r = Reader.new(data) eid = r.str etype = r.str src = r.str dst = r.str props = r.props { "_kind" => "edge", "id" => eid, "type" => etype, "from" => src, "to" => dst, "properties" => props, "uuid" => trailer(r) } end |
.encode(value) ⇒ Object
A Ruby value -> gqldb.TypedValue, for query parameters.
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 |
# File 'lib/tina4_ultipa/codec.rb', line 369 def encode(value) case value when nil Gqldb::TypedValue.new(type: :PROPERTY_TYPE_NULL, is_null: true) when true, false Gqldb::TypedValue.new(type: :PROPERTY_TYPE_BOOL, data: (value ? "\x01" : "\x00").b) when Integer Gqldb::TypedValue.new(type: :PROPERTY_TYPE_INT64, data: [value].pack("q<")) when Float Gqldb::TypedValue.new(type: :PROPERTY_TYPE_DOUBLE, data: [value].pack("E")) when String # A binary (ASCII-8BIT) String is the Ruby analogue of Python `bytes`: # send it as a BLOB so raw bytes round-trip intact. UTF-8/other -> STRING. if value.encoding == Encoding::ASCII_8BIT Gqldb::TypedValue.new(type: :PROPERTY_TYPE_BLOB, data: value) else Gqldb::TypedValue.new(type: :PROPERTY_TYPE_STRING, data: value.b) end else # Anything else -> UTF-8 bytes. Gqldb::TypedValue.new(type: :PROPERTY_TYPE_STRING, data: value.to_s.b) end end |
.error(data) ⇒ Object
190 191 192 193 194 195 196 |
# File 'lib/tina4_ultipa/codec.rb', line 190 def error(data) s = (data || "".b).dup.force_encoding("UTF-8") obj = JSON.parse(s) { "_kind" => "error", "code" => (obj["code"] || 0), "message" => (obj["message"] || "") } rescue StandardError { "_kind" => "error", "code" => 0, "message" => s } end |
.list(data) ⇒ Object
── composites ──────────────────────────────────────────────────────
263 264 265 266 |
# File 'lib/tina4_ultipa/codec.rb', line 263 def list(data) r = Reader.new(data) Array.new(r.u16) { r.tve } end |
.local_datetime(data) ⇒ Object
100 101 102 103 104 105 106 107 108 109 |
# File 'lib/tina4_ultipa/codec.rb', line 100 def local_datetime(data) if !data.nil? && data.bytesize >= 11 y = data.unpack1("s<") nanos = data.unpack1("@7 L<") base = format("%04d-%02d-%02d %02d:%02d:%02d", y, data.getbyte(2), data.getbyte(3), data.getbyte(4), data.getbyte(5), data.getbyte(6)) return datetime_frac(base, nanos) end unix(data) # legacy 8-byte unix fallback end |
.local_time(data) ⇒ Object
73 74 75 76 77 78 |
# File 'lib/tina4_ultipa/codec.rb', line 73 def local_time(data) return nil if data.nil? || data.bytesize < 8 nanos = data.unpack1("@4 L<") time_str(data.getbyte(0), data.getbyte(1), data.getbyte(2), nanos) end |
.node(data) ⇒ Object
280 281 282 283 284 285 286 287 |
# File 'lib/tina4_ultipa/codec.rb', line 280 def node(data) r = Reader.new(data) nid = r.str labels = Array.new(r.u16) { r.str } props = r.props { "_kind" => "node", "id" => nid, "labels" => labels, "properties" => props, "uuid" => trailer(r) } end |
.offset(minutes) ⇒ Object
88 89 90 91 92 |
# File 'lib/tina4_ultipa/codec.rb', line 88 def offset(minutes) sign = minutes >= 0 ? "+" : "-" m = minutes.abs format("%s%02d:%02d", sign, m / 60, m % 60) end |
.path(data) ⇒ Object
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/tina4_ultipa/codec.rb', line 300 def path(data) r = Reader.new(data) nodes = Array.new(r.u16) do n = r.u32 v = node(r.d.byteslice(r.p, n)) r.p += n v end edges = Array.new(r.u16) do n = r.u32 v = edge(r.d.byteslice(r.p, n)) r.p += n v end { "_kind" => "path", "nodes" => nodes, "edges" => edges } end |
.point(data) ⇒ Object
150 151 152 153 154 155 156 157 |
# File 'lib/tina4_ultipa/codec.rb', line 150 def point(data) return nil if data.nil? || data.bytesize < 16 x = data.unpack1("E") y = data.unpack1("@8 E") srid = data.bytesize >= 20 ? data.unpack1("@16 L<") : 7203 { "_kind" => "point", "x" => x, "y" => y, "srid" => srid } end |
.point3d(data) ⇒ Object
159 160 161 162 163 164 165 166 167 |
# File 'lib/tina4_ultipa/codec.rb', line 159 def point3d(data) return nil if data.nil? || data.bytesize < 24 x = data.unpack1("E") y = data.unpack1("@8 E") z = data.unpack1("@16 E") srid = data.bytesize >= 28 ? data.unpack1("@24 L<") : 9757 { "_kind" => "point3d", "x" => x, "y" => y, "z" => z, "srid" => srid } end |
.scalar(ptype, data) ⇒ Object
Decode a raw property/value payload of a given PropertyType. Used both for the top-level TypedValue and for each value inside a composite.
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 |
# File 'lib/tina4_ultipa/codec.rb', line 321 def scalar(ptype, data) t = ptype t = INT_TO_SYM[t] || t if t.is_a?(Integer) return nil if NULLISH.include?(t) fmt = NUMERIC[t] unless fmt.nil? return nil if data.nil? || data.empty? return data.unpack1(fmt) end return data.dup.force_encoding("UTF-8") if STRINGY.include?(t) return !(data.nil? || data.empty? || data.getbyte(0).zero?) if t == :PROPERTY_TYPE_BOOL # BLOB: raw bytes as a binary string. return (data || "".b).dup.force_encoding("BINARY") if t == :PROPERTY_TYPE_BLOB # DECIMAL is a precision-preserving UTF-8 string: return it verbatim, never to_f. return data.dup.force_encoding("UTF-8") if t == :PROPERTY_TYPE_DECIMAL return date(data) if t == :PROPERTY_TYPE_DATE return local_datetime(data) if LOCAL_DT.include?(t) return zoned_datetime(data) if t == :PROPERTY_TYPE_ZONED_DATETIME return (data) if t == :PROPERTY_TYPE_TIMESTAMP return local_time(data) if t == :PROPERTY_TYPE_LOCAL_TIME return zoned_time(data) if t == :PROPERTY_TYPE_ZONED_TIME return year_to_month(data) if t == :PROPERTY_TYPE_YEAR_TO_MONTH return day_to_second(data) if t == :PROPERTY_TYPE_DAY_TO_SECOND return point(data) if t == :PROPERTY_TYPE_POINT return point3d(data) if t == :PROPERTY_TYPE_POINT3D return vector(data) if t == :PROPERTY_TYPE_VECTOR return list(data) if LISTY.include?(t) return Reader.new(data).props if MAPPY.include?(t) return table(data) if t == :PROPERTY_TYPE_TABLE return node(data) if t == :PROPERTY_TYPE_NODE return edge(data) if t == :PROPERTY_TYPE_EDGE return path(data) if t == :PROPERTY_TYPE_PATH return error(data) if t == :PROPERTY_TYPE_ERROR # Unmapped/future types: hand back the raw bytes rather than lose or guess. data end |
.table(data) ⇒ Object
268 269 270 271 272 273 |
# File 'lib/tina4_ultipa/codec.rb', line 268 def table(data) r = Reader.new(data) cols = Array.new(r.u16) { r.str } rows = Array.new(r.u16) { Array.new(r.u16) { r.tve } } { "_kind" => "table", "columns" => cols, "rows" => rows } end |
.time_str(hour, minute, sec, nanos) ⇒ Object
66 67 68 69 70 71 |
# File 'lib/tina4_ultipa/codec.rb', line 66 def time_str(hour, minute, sec, nanos) base = format("%02d:%02d:%02d", hour, minute, sec) return base if nanos.nil? || nanos.zero? base + format(".%09d", nanos).sub(/0+\z/, "").sub(/\.\z/, "") end |
.timestamp(data) ⇒ Object
123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/tina4_ultipa/codec.rb', line 123 def (data) if !data.nil? && data.bytesize >= 12 secs = data.unpack1("q<") nanos = data.unpack1("@8 L<") begin base = Time.at(secs).utc.strftime("%Y-%m-%d %H:%M:%S") return datetime_frac(base, nanos) rescue StandardError, RangeError return secs end end unix(data) end |
.trailer(reader) ⇒ Object
Optional 8-byte little-endian uint64 internal-id trailer (gqldb 6.1.147+).
276 277 278 |
# File 'lib/tina4_ultipa/codec.rb', line 276 def trailer(reader) reader.p + 8 <= reader.d.bytesize ? reader.u64.to_s : "" end |
.unix(data) ⇒ Object
Legacy 8-byte unix time (seconds/ms/us) -> 'YYYY-MM-DD HH:MM:SS' (UTC).
138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/tina4_ultipa/codec.rb', line 138 def unix(data) return nil if data.nil? || data.bytesize < 8 v = data.byteslice(0, 8).unpack1("Q<") secs = v > 1e14 ? v / 1e6 : (v > 1e11 ? v / 1e3 : v) begin Time.at(secs).utc.strftime("%Y-%m-%d %H:%M:%S") rescue StandardError, RangeError v end end |
.vector(data) ⇒ Object
169 170 171 172 173 174 175 176 |
# File 'lib/tina4_ultipa/codec.rb', line 169 def vector(data) return [] if data.nil? || data.bytesize < 4 dim = data.unpack1("L<") return [] if data.bytesize < 4 + dim * 4 Array.new(dim) { |i| data.unpack1("@#{4 + i * 4} e") } end |
.year_to_month(data) ⇒ Object
178 179 180 181 182 |
# File 'lib/tina4_ultipa/codec.rb', line 178 def year_to_month(data) return { "months" => 0 } if data.nil? || data.bytesize < 4 { "months" => data.unpack1("l<") } end |
.zoned_datetime(data) ⇒ Object
111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/tina4_ultipa/codec.rb', line 111 def zoned_datetime(data) if !data.nil? && data.bytesize >= 13 y = data.unpack1("s<") nanos = data.unpack1("@7 L<") off = data.unpack1("@11 s<") base = format("%04d-%02d-%02d %02d:%02d:%02d", y, data.getbyte(2), data.getbyte(3), data.getbyte(4), data.getbyte(5), data.getbyte(6)) return datetime_frac(base, nanos) + offset(off) end unix(data) end |
.zoned_time(data) ⇒ Object
80 81 82 83 84 85 86 |
# File 'lib/tina4_ultipa/codec.rb', line 80 def zoned_time(data) return nil if data.nil? || data.bytesize < 10 nanos = data.unpack1("@4 L<") off = data.unpack1("@8 s<") time_str(data.getbyte(0), data.getbyte(1), data.getbyte(2), nanos) + offset(off) end |