lib/fdbtuple.rb



#encoding: BINARY

#
# fdbtuple.rb
#
# This source file is part of the FoundationDB open source project
#
# Copyright 2013-2018 Apple Inc. and the FoundationDB project authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

# FoundationDB Ruby API

# Documentation for this API can be found at
# https://apple.github.io/foundationdb/api-ruby.html

module FDB
  module Tuple
    @@size_limits = (0..8).map {|x| (1 << (x*8)) - 1}

    # Type codes 
    @@NULL_CODE       = 0x00
    @@BYTES_CODE      = 0x01
    @@STRING_CODE     = 0x02
    @@NESTED_CODE     = 0x05
    @@INT_ZERO_CODE   = 0x14
    @@POS_INT_END     = 0x1d
    @@NEG_INT_START   = 0x0b
    @@FLOAT_CODE      = 0x20
    @@DOUBLE_CODE     = 0x21
    @@FALSE_CODE      = 0x26
    @@TRUE_CODE       = 0x27
    @@UUID_CODE       = 0x30

    class UUID
      def initialize(data)
        if data.length != 16
          raise Error.new(2268) # invalid_uuid_size
        end
        @data=data.slice(0,16)
      end
      def data
        @data
      end
      def <=> (other)
        self.data <=> other.data
      end
      def to_s
        self.data.each_byte.map { |b| b.to_s(16) } .join
      end
    end

    class SingleFloat
      def initialize(value)
        if value.kind_of? Float
            @value=value
        elsif value.kind_of? Integer
            @value=value.to_f
        else
          raise ArgumentError, "Invalid value type for SingleFloat: " + value.class.name
        end
        @value=value
      end
      def value
        @value
      end
      def <=> (other)
        Tuple._compare_floats(self.value, other.value, false)
      end
      def to_s
        self.value.to_s
      end
    end

    def self.find_terminator(v, pos)
      while true
        pos = v.index("\x00", pos)
        if !pos
          return v.length
        elsif pos+1 == v.length || v[pos+1] != "\xff"
          return pos
        end
        pos += 2
      end
    end
    private_class_method :find_terminator

    def self.float_adjust(v, pos, length, encode)
      if (encode and v[pos].ord & 0x80 != 0x00) or (not encode and v[pos].ord & 0x80 == 0x00)
        v.slice(pos, length).chars.map { |b| (b.ord ^ 0xff).chr } .join
      else
        ret = v.slice(pos, length)
        ret[0] = (ret[0].ord ^ 0x80).chr
        ret
      end
    end
    private_class_method :float_adjust

    def self.decode(v, pos)
      code = v.getbyte(pos)
      if code == @@NULL_CODE
        [nil, pos+1]
      elsif code == @@BYTES_CODE
        epos = find_terminator(v, pos+1)
        [v.slice(pos+1, epos-pos-1).gsub("\x00\xFF", "\x00"), epos+1]
      elsif code == @@STRING_CODE
        epos = find_terminator(v, pos+1)
        [v.slice(pos+1, epos-pos-1).gsub("\x00\xFF", "\x00").force_encoding("UTF-8"), epos+1]
      elsif code >= @@INT_ZERO_CODE && code < @@POS_INT_END
        n = code - @@INT_ZERO_CODE
        [("\x00" * (8-n) + v.slice(pos+1, n)).unpack("Q>")[0], pos+n+1]
      elsif code > @@NEG_INT_START and code < @@INT_ZERO_CODE
        n = @@INT_ZERO_CODE - code
        [("\x00" * (8-n) + v.slice(pos+1, n)).unpack("Q>")[0]-@@size_limits[n], pos+n+1]
      elsif code == @@POS_INT_END
        length = v.getbyte(pos+1)
        val = 0
        length.times do |i|
          val = val << 8
          val += v.getbyte(pos+2+i)
        end
        [val, pos+length+2]
      elsif code == @@NEG_INT_START
        length = v.getbyte(pos+1) ^ 0xff
        val = 0
        length.times do |i|
          val = val << 8
          val += v.getbyte(pos+2+i)
        end
        [val - (1 << (length*8)) + 1, pos+length+2]
      elsif code == @@FALSE_CODE
        [false, pos+1]
      elsif code == @@TRUE_CODE
        [true, pos+1]
      elsif code == @@FLOAT_CODE
        [SingleFloat.new(float_adjust(v, pos+1, 4, false).unpack("g")[0]), pos+5]
      elsif code == @@DOUBLE_CODE
        [float_adjust(v, pos+1, 8, false).unpack("G")[0], pos+9]
      elsif code == @@UUID_CODE
        [UUID.new(v.slice(pos+1, 16)), pos+17]
      elsif code == @@NESTED_CODE
        epos = pos+1
        nested = []
        while epos < v.length
          if v.getbyte(epos) == @@NULL_CODE 
            if epos+1 < v.length and v.getbyte(epos+1) == 0xFF
              nested << nil
              epos += 2
            else
              break
            end
          else
            r, epos = decode(v, epos)
            nested << r
          end
        end
        [nested, epos+1]
      else
        raise "Unknown data type in DB: " + code.ord.to_s
      end
    end
    private_class_method :decode

    def self.bisect_left(list, item)
      count = 0
      list.each{|i|
        return count if i >= item
        count += 1
      }
      nil
    end
    private_class_method :bisect_left

    def self.encode(v, nested=false)
      if v.nil?
        if nested
          "\x00\xFF"
        else
          @@NULL_CODE.chr
        end
      elsif v.kind_of? String
        if v.encoding == Encoding::BINARY || v.encoding == Encoding::ASCII
          @@BYTES_CODE.chr + v.gsub("\x00", "\x00\xFF") + 0.chr
        elsif v.encoding == Encoding::UTF_8
          @@STRING_CODE.chr + v.dup.force_encoding("BINARY").gsub("\x00", "\x00\xFF") + 0.chr
        else
          raise ArgumentError, "unsupported encoding #{v.encoding.name}"
        end
      elsif v.kind_of? Integer
        raise RangeError, "Integer magnitude is too large (more than 255 bytes)" if v < -2**2040+1 || v > 2**2040-1
        if v == 0
          @@INT_ZERO_CODE.chr
        elsif v > 0
          if v > @@size_limits[-1]
            length = (v.bit_length + 7) / 8
            result = @@POS_INT_END.chr + length.chr
            length.times do |i|
              result << ((v >> (8 * (length-i-1))) & 0xff)
            end
            result
          else
            n = bisect_left( @@size_limits, v )
            (@@INT_ZERO_CODE+n).chr + [v].pack("Q>").slice(8-n, n)
          end
        else
          if -v > @@size_limits[-1]
            length = ((-v).bit_length + 7) / 8
            v += (1 << (length * 8)) - 1
            result = @@NEG_INT_START.chr + (length ^ 0xff).chr
            length.times do |i|
              result << ((v >> (8 * (length-i-1))) & 0xff)
            end
            result
          else
            n = bisect_left( @@size_limits, -v )
            (@@INT_ZERO_CODE-n).chr + [@@size_limits[n]+v].pack("Q>").slice(8-n, n)
          end
        end
      elsif v.kind_of? TrueClass
        @@TRUE_CODE.chr
      elsif v.kind_of? FalseClass
        @@FALSE_CODE.chr
      elsif v.kind_of? SingleFloat
        @@FLOAT_CODE.chr + float_adjust([v.value].pack("g"), 0, 4, true)
      elsif v.kind_of? Float
        @@DOUBLE_CODE.chr + float_adjust([v].pack("G"), 0, 8, true)
      elsif v.kind_of? UUID
        @@UUID_CODE.chr + v.data
      elsif v.kind_of? Array
        @@NESTED_CODE.chr + (v.map { |el| encode(el, true).force_encoding("BINARY") }).join + 0.chr
      else
        raise ArgumentError, "unsupported type #{v.class}"
      end
    end
    private_class_method :encode

    def self.pack(t)
      (t.each_with_index.map {|el, i|
         begin
           (encode el).force_encoding("BINARY")
         rescue
           raise $!, "#{$!} at index #{i}", $!.backtrace
         end
       }).join
    end

    def self.unpack(key)
      key = key.dup.force_encoding("BINARY")
      pos = 0
      res = []
      while pos < key.length
        r, pos = decode(key, pos)
        res << r
      end
      res
    end

    def self.range(tuple=[])
      p = pack(tuple)
      [p+"\x00", p+"\xFF"]
    end

    def self._code_for(v)
      if v.nil?
        @@NULL_CODE
      elsif v.kind_of? String
        if v.encoding == Encoding::BINARY || v.encoding == Encoding::ASCII
          @@BYTES_CODE
        elsif v.encoding == Encoding::UTF_8
          @@STRING_CODE
        else
          raise ArgumentError, "unsupported encoding #{v.encoding.name}"
        end
      elsif v.kind_of? Integer
         @@INT_ZERO_CODE
      elsif v.kind_of? TrueClass
        @@TRUE_CODE
      elsif v.kind_of? FalseClass
        @@FALSE_CODE
      elsif v.kind_of? SingleFloat
        @@FLOAT_CODE
      elsif v.kind_of? Float
        @@DOUBLE_CODE
      elsif v.kind_of? UUID
        @@UUID_CODE
      elsif v.kind_of? Array
        @@NESTED_CODE
      else
        raise ArgumentError, "unsupported type #{v.class}"
      end
    end

    def self._compare_floats(f1, f2, is_double)
      # This converts the floats to their byte representation and then
      # does the comparison. Why?
      #   1) NaN comparison - Ruby doesn't really do this
      #   2) -0.0 == 0.0 in Ruby but not in our representation
      # It would be better to just take the floats and compare them, but
      # this way handles the edge cases correctly.
      b1 = float_adjust([f1].pack(is_double ? ">G" : ">g"), 0, (is_double ? 8 : 4), true)
      b2 = float_adjust([f2].pack(is_double ? ">G" : ">g"), 0, (is_double ? 8 : 4), true)
      b1 <=> b2
    end

    def self._compare_elems(v1, v2)
      c1 = _code_for(v1)
      c2 = _code_for(v2)
      return c1 <=> c2 unless c1 == c2

      if c1 == @@NULL_CODE
        0
      elsif c1 == @@DOUBLE_CODE
        _compare_floats(v1, v2, true)
      elsif c1 == @@NESTED_CODE
        compare(v1, v2) # recurse
      else
        v1 <=> v2
      end
    end

    def self.compare(tuple1, tuple2)
      i = 0
      while i < tuple1.length && i < tuple2.length
        c = self._compare_elems(tuple1[i], tuple2[i])
        return c unless c == 0
        i += 1
      end
      tuple1.length <=> tuple2.length
    end
  end
end