shlima/click_house

View on GitHub
lib/click_house/response/result_set.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

module ClickHouse
  module Response
    class ResultSet
      extend Forwardable
      include Enumerable

      KEY_META_NAME = 'name'
      KEY_META_TYPE = 'type'

      def_delegators :to_a,
                     :inspect, :each, :fetch, :length, :count, :size,
                     :first, :last, :[], :to_h

      def_delegators :summary,
                     :statistics, :headers,
                     :totals, :rows_before_limit_at_least

      attr_reader :config, :meta, :data, :summary

      class << self
        # @param config [Config]
        # @return [ResultSet]
        def raw(config:, data:, summary:)
          new(config: config, data: data, to_a: data, meta: [], summary: summary)
        end
      end

      # @param config [Config]
      # @param meta [Array]
      # @param data [Array]
      # @param summary [Response::Summary]
      def initialize(config:, meta:, data:, summary:, to_a: nil)
        @config = config
        @meta = meta
        @data = data
        @summary = summary
        @to_a = to_a
      end

      # @return [Array, Hash]
      # @param data [Array, Hash]
      def serialize(data)
        case data
        when Hash
          serialize_one(data)
        when Array
          data.map(&method(:serialize_one))
        else
          raise ArgumentError, "expect Hash or Array, got: #{data.class}"
        end
      end

      # @return [Hash]
      # @param row [Hash]
      def serialize_one(row)
        row.each_with_object({}) do |(key, value), object|
          object[key] = serialize_column(key, value)
        end
      end

      # @param name [String] column name
      # @param value [Any]
      def serialize_column(name, value)
        stmt = types.fetch(name)
        serialize_type(stmt, value)
      rescue KeyError => e
        raise SerializeError, "field <#{name}> does not exists in table schema: #{types}", e.backtrace
      rescue StandardError => e
        raise SerializeError, "failed to serialize <#{name}> with #{stmt}, #{e.class}, #{e.message}", e.backtrace
      end

      def to_a
        @to_a ||= data.each do |row|
          row.each do |name, value|
            row[name] = cast_type(types.fetch(name), value)
          end
        end
      end

      # @return [Hash<String, Ast::Statement>]
      def types
        @types ||= meta.each_with_object({}) do |row, object|
          column = row.fetch(config.key(KEY_META_NAME))
          # make symbol keys, if config.symbolize_keys is true,
          # to be able to cast and serialize properly
          object[config.key(column)] = begin
            current = Ast::Parser.new(row.fetch(config.key(KEY_META_TYPE))).parse
            assign_type(current)
            current
          end
        end
      end

      private

      # @param stmt [Ast::Statement]
      def assign_type(stmt)
        stmt.caster = ClickHouse.types[stmt.name]

        if stmt.caster.is_a?(Type::UndefinedType)
          placeholders = stmt.arguments.map(&:placeholder)
          stmt.caster = ClickHouse.types["#{stmt.name}(#{placeholders.join(', ')})"]
        end

        stmt.arguments.each(&method(:assign_type))
      end

      # @param stmt [Ast::Statement]
      def cast_type(stmt, value)
        return cast_container(stmt, value) if stmt.caster.container?
        return cast_map(stmt, Hash(value)) if stmt.caster.map?
        return cast_tuple(stmt, Array(value)) if stmt.caster.tuple?

        stmt.caster.cast(value, *stmt.argument_values)
      end

      # @return [Hash]
      # @param stmt [Ast::Statement]
      # @param hash [Hash]
      def cast_map(stmt, hash)
        raise ArgumentError, "expect hash got #{hash.class}" unless hash.is_a?(Hash)

        key_type, value_type = stmt.arguments
        hash.each_with_object({}) do |(key, value), object|
          object[cast_type(key_type, key)] = cast_type(value_type, value)
        end
      end

      # @param stmt [Ast::Statement]
      def cast_container(stmt, value)
        stmt.caster.cast_each(value) do |item|
          cast_type(stmt.argument_first!, item)
        end
      end

      # @param stmt [Ast::Statement]
      def cast_tuple(stmt, value)
        value.map.with_index do |item, ix|
          cast_type(stmt.arguments.fetch(ix), item)
        end
      end

      # @param stmt [Ast::Statement]
      def serialize_type(stmt, value)
        return serialize_container(stmt, value) if stmt.caster.container?
        return serialize_map(stmt, value) if stmt.caster.map?
        return serialize_tuple(stmt, Array(value)) if stmt.caster.tuple?

        stmt.caster.serialize(value, *stmt.argument_values)
      end

      # @param stmt [Ast::Statement]
      def serialize_container(stmt, value)
        stmt.caster.serialize_each(value) do |item|
          serialize_type(stmt.argument_first!, item)
        end
      end

      # @return [Hash]
      # @param stmt [Ast::Statement]
      # @param hash [Hash]
      def serialize_map(stmt, hash)
        raise ArgumentError, "expect hash got #{hash.class}" unless hash.is_a?(Hash)

        key_type, value_type = stmt.arguments
        hash.each_with_object({}) do |(key, value), object|
          object[serialize_type(key_type, key)] = serialize_type(value_type, value)
        end
      end

      # @param stmt [Ast::Statement]
      def serialize_tuple(stmt, value)
        value.map.with_index do |item, ix|
          serialize_type(stmt.arguments.fetch(ix), item)
        end
      end
    end
  end
end