class JsonApiPgSql

Attributes

base_relation[R]
base_serializer[R]

Public Class Methods

json_column_type() click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 375
def self.json_column_type
  # These classes may not exist, depending on the Rails version:
  @@json_column_type = if Rails::VERSION::STRING >= '5.2'
                         'ActiveRecord::Type::Json'
                       else
                         'ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json'
                       end.constantize
end
new(base_serializer, base_relation, instance_options, options) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 384
def initialize(base_serializer, base_relation, instance_options, options)
  @base_relation = base_relation
  @instance_options = instance_options
  @options = options

  # Make a JsonThing for everything,
  # cached as the full_name:

  # Watch out: User.where is a Relation, but plain User is not:
  ar_class = ActiveRecord::Relation === base_relation ? base_relation.klass : base_relation

  case base_serializer
  when ActiveModel::Serializer::CollectionSerializer
    ActiveModelSerializers::Adapter::JsonApiPg.warn_about_collection_serializer
    base_serializer = base_serializer.element_serializer
    @many = true
  when ActiveModelSerializersPg::CollectionSerializer
    base_serializer = base_serializer.element_serializer
    @many = true
  else
    base_serializer = base_serializer.class
    @many = false
  end
  base_serializer ||= ActiveModel::Serializer.serializer_for(ar_class.new, options)
  @base_serializer = base_serializer

  base_name = ar_class.name.underscore.pluralize
  base_thing = JsonThing.new(ar_class, base_name, base_serializer, options)
  @fields_for = {}
  @attribute_fields_for = {}
  @reflection_fields_for = {}
  @json_things = {
    base: base_thing, # `base` is a sym but every other key is a string
  }
  @json_things[base_name] = base_thing
  # We don't need to add anything else to @json_things yet
  # because we'll lazy-build it via get_json_thing.
  # That lets us go as deep in the relationships as we need
  # without loading anything extra.
end

Public Instance Methods

_attribute_fields_for(resource) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 779
def _attribute_fields_for(resource)
  attrs = Set.new(serializer_attributes(resource))
  # JSON:API always excludes the `id`
  # even if it's part of the serializer:
  attrs = attrs - [resource.primary_key.to_sym]
  fields_for(resource).select { |f| attrs.include? f }.to_a
end
_fields_for(resource) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 759
def _fields_for(resource)
  # Sometimes options[:fields] has plural keys and sometimes singular,
  # so try both:
  resource_key = resource.json_type.to_sym
  fields = @instance_options.dig :fields, resource_key
  if fields.nil?
    resource_key = resource.json_type.singularize.to_sym
    fields = @instance_options.dig :fields, resource_key
  end
  if fields.nil?
    # If the user didn't request specific fields, then give them all that appear in the serializer:
    fields = serializer_attributes(resource).to_a + serializer_reflections(resource).to_a
  end
  fields
end
_reflection_fields_for(resource) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 791
def _reflection_fields_for(resource)
  refls = Set.new(serializer_reflections(resource))
  fields_for(resource).select { |f| refls.include? f }.to_a
end
attribute_fields_for(resource) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 775
def attribute_fields_for(resource)
  @attribute_fields_for[resource.full_name] ||= _attribute_fields_for(resource)
end
base_resource() click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 704
def base_resource
  @json_things[:base]
end
column_is_castable_to_jsonb?(column_class) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 498
def column_is_castable_to_jsonb?(column_class)
  column_class.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Hstore) or
    column_class.is_a?(self.class.json_column_type)
end
column_is_castable_to_jsonb_array?(column_class) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 503
def column_is_castable_to_jsonb_array?(column_class)
  column_class.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array) and
    column_is_castable_to_jsonb?(column_class.subtype)
end
column_is_jsonb?(column_class) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 489
def column_is_jsonb?(column_class)
  column_class.is_a? ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb
end
column_is_jsonb_array?(column_class) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 493
def column_is_jsonb_array?(column_class)
  column_class.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array) and
    column_class.subtype.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb)
end
fields_for(resource) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 755
def fields_for(resource)
  @fields_for[resource.full_name] ||= _fields_for(resource)
end
get_json_thing(resource, field) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 425
def get_json_thing(resource, field)
  refl_name = "#{resource.full_name}.#{field}"
  @json_things[refl_name] ||= resource.from_reflection(field)
end
get_json_thing_from_base(field) click to toggle source

Takes a dotted field name (not including the base resource) like we might find in options, and builds up all the JsonThings needed to get to the end.

# File lib/active_model_serializers/adapter/json_api_pg.rb, line 672
def get_json_thing_from_base(field)
  r = base_resource
  field.split('.').each do |f|
    r = get_json_thing(r, f)
  end
  r
end
include_cte(resource) click to toggle source

See note in _jbs_name method for why we split each thing into two CTEs.

# File lib/active_model_serializers/adapter/json_api_pg.rb, line 640
  def include_cte(resource)
    parent = resource.parent
    <<~EOQ
      SELECT  DISTINCT ON ("#{resource.table_name}"."#{resource.primary_key}")
              "#{resource.table_name}".*
      FROM    "#{resource.table_name}"
      JOIN    "#{parent.cte_name}"
      ON      #{include_cte_join_condition(resource)}
      ORDER BY "#{resource.table_name}"."#{resource.primary_key}"
    EOQ
  end
include_cte_join_condition(resource) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 628
def include_cte_join_condition(resource)
  parent = resource.parent
  if resource.belongs_to?
    %Q{"#{parent.cte_name}"."#{resource.foreign_key}" = "#{resource.table_name}"."#{resource.primary_key}"}
  elsif resource.has_many? or resource.has_one?
    %Q{"#{parent.cte_name}"."#{parent.primary_key}" = "#{resource.table_name}"."#{resource.foreign_key}"}
  else
    raise "not supported relationship: #{resource.full_name}"
  end
end
include_ctes() click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 680
  def include_ctes
    includes.map { |inc|
      # Be careful: inc might have dots:
      th = get_json_thing_from_base(inc)
      <<~EOQ
        "#{th.cte_name}" AS (
          #{include_cte(th)}
        ),
      EOQ
    }.join("\n")
  end
include_jbs(resource) click to toggle source

See note in _jbs_name method for why we split each thing into two CTEs.

# File lib/active_model_serializers/adapter/json_api_pg.rb, line 653
  def include_jbs(resource)
    <<~EOQ
      SELECT  "#{resource.table_name}".*,
              #{select_resource(resource)} AS j
      FROM    "#{resource.cte_name}" AS "#{resource.table_name}"
      #{join_resource_relationships(resource)}
    EOQ
  end
include_jbsses() click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 692
  def include_jbsses
    includes.map { |inc|
      # Be careful: inc might have dots:
      th = get_json_thing_from_base(inc)
      <<~EOQ
        "#{th.jbs_name}" AS (
          #{include_jbs(th)}
        ),
      EOQ
    }.join("\n")
  end
include_selects() click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 616
def include_selects
  @include_selects ||= includes.map {|inc|
    th = get_json_thing_from_base(inc)
    # TODO: UNION ALL would be faster than UNION,
    # but then we still need to de-dupe when we have two paths to the same table,
    # e.g. buyer and seller for User.
    # But we could group those and union just them, or even better do a DISTINCT ON (id).
    # Since we don't get the id here that could be another CTE.
    %Q{UNION SELECT j FROM "#{th.jbs_name}"}
  }
end
includes() click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 662
def includes
  @includes ||= (@instance_options[:include] || []).sort_by do |inc|
    # Sort these by length so we never have bad foreign references in the CTEs:
    inc.size
  end
end
join_resource_relationships(resource) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 551
  def join_resource_relationships(resource)
    fields = reflection_fields_for(resource)
    fields.map{|f|
      child_resource = get_json_thing(resource, f)
      refl = child_resource.reflection
      if refl.has_many?
        if refl.ar_reflection.present?
          # Preserve ordering options, either from the AR association itself
          # or from the class's default scope.
          # TODO: preserve the whole custom relation, not just ordering
          p = refl.ar_class.new
          ordering = p.send(refl.name).arel.orders
          ordering = child_resource.ar_class.default_scoped.arel.orders if ordering.empty?
          ordering = ordering.map{|o|
            case o
            # TODO: The gsub is pretty awful....
            when Arel::Nodes::Ordering
              o.to_sql.gsub("\"#{child_resource.table_name}\"", "rel")
            when String
              o
            else
              raise "Unknown type of ordering: #{o.inspect}"
            end
          }.join(', ').presence
          ordering = "ORDER BY #{ordering}" if ordering
          <<~EOQ
            LEFT OUTER JOIN LATERAL (
              SELECT  coalesce(jsonb_agg(jsonb_build_object('id', rel."#{child_resource.primary_key_attr}"::text,
                                                            'type', '#{child_resource.json_type}') #{ordering}), '[]') AS j
              FROM    "#{child_resource.table_name}" rel
              WHERE   rel."#{child_resource.foreign_key}" = "#{resource.table_name}"."#{resource.primary_key}"
            ) "rel_#{child_resource.cte_name}" ON true
          EOQ
        elsif not refl.reflection_sql.nil?  # can't use .present? since that loads the Relation!
          case refl.reflection_sql
          when String
            raise "TODO"
          when ActiveRecord::Relation
            rel = refl.reflection_sql
            sql = rel.select(<<~EOQ).to_sql
              coalesce(jsonb_agg(jsonb_build_object('id', "#{child_resource.table_name}"."#{child_resource.primary_key_attr}"::text,
                                                    'type', '#{child_resource.json_type}')), '[]') AS j
            EOQ
            <<~EOQ
              LEFT OUTER JOIN LATERAL (
                #{sql}
              ) "rel_#{child_resource.cte_name}" ON true
            EOQ
          end
        end
      elsif refl.has_one?
        <<~EOQ
          LEFT OUTER JOIN LATERAL (
            SELECT  jsonb_build_object('id', rel."#{child_resource.primary_key_attr}"::text,
                                      'type', '#{child_resource.json_type}') AS j
            FROM    "#{child_resource.table_name}" rel
            WHERE   rel."#{child_resource.foreign_key}" = "#{resource.table_name}"."#{resource.primary_key}"
          ) "rel_#{child_resource.cte_name}" ON true
        EOQ
      else
        nil
      end
    }.compact.join("\n")
  end
json_key(name) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 434
def json_key(name)
  JsonThing.json_key(name)
end
many?() click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 430
def many?
  @many
end
maybe_select_resource_relationships(resource) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 708
def maybe_select_resource_relationships(resource)
  rels_sql = select_resource_relationships(resource)
  if rels_sql.nil?
    ''
  else
    %Q{, 'relationships', #{rels_sql}}
  end
end
reflection_fields_for(resource) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 787
def reflection_fields_for(resource)
  @reflection_fields_for[resource.full_name] ||= _reflection_fields_for(resource)
end
select_resource(resource) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 717
  def select_resource(resource)
    fields = fields_for(resource)
    <<~EOQ
      jsonb_build_object('id', "#{resource.table_name}"."#{resource.primary_key_attr}"::text,
                         'type', '#{resource.json_type}',
                         'attributes', #{select_resource_attributes(resource)}
                         #{maybe_select_resource_relationships(resource)})
    EOQ
  end
select_resource_attribute(resource, field) click to toggle source

Returns SQL for one JSON value for the resource's 'attributes' object. If a field is an enum then we convert it from an int to a string. If a field has a #{field}__sql method on the ActiveRecord class, we use that instead.

# File lib/active_model_serializers/adapter/json_api_pg.rb, line 451
  def select_resource_attribute(resource, field)
    typ = resource.ar_class.attribute_types[field.to_s]
    if typ.is_a? ActiveRecord::Enum::EnumType
      <<~EOQ
        CASE #{typ.as_json['mapping'].map{|str, int| %Q{WHEN "#{resource.table_name}"."#{field}" = #{int} THEN '#{str}'}}.join("\n     ")} END
      EOQ
    elsif resource.has_sql_method?(field)
      resource.sql_method(field)
    else
      field = resource.unaliased(field)
      # Standard AMS dasherizes json/jsonb/hstore columns,
      # so we have to do the same:
      if ActiveModelSerializers.config.key_transform == :dash
        cl = resource.ar_class.attribute_types[field.to_s]
        if column_is_jsonb? cl
          %Q{jsonb_dasherize("#{resource.table_name}"."#{field}")}
        elsif column_is_jsonb_array? cl
          # TODO: Could be faster:
          # If we made the jsonb_dasherize function smarter so it could handle jsonb[],
          # we wouldn't have to build a json object from the array then cast to jsonb[].
          %Q{jsonb_dasherize(array_to_json("#{resource.table_name}"."#{field}")::jsonb)}
        elsif column_is_castable_to_jsonb? cl
          # Fortunately we can cast hstore to jsonb,
          # which gives us a solution that works whether or not the hstore extension is installed.
          # Defining an hstore_dasherize function would work only if the extension were present.
          %Q{jsonb_dasherize("#{resource.table_name}"."#{field}"::jsonb)}
        elsif column_is_castable_to_jsonb_array? cl
          # TODO: Could be faster (see above):
          %Q{jsonb_dasherize(array_to_json("#{resource.table_name}"."#{field}"::jsonb[])::jsonb)}
        else
          %Q{"#{resource.table_name}"."#{field}"}
        end
      else
        %Q{"#{resource.table_name}"."#{field}"}
      end
    end
  end
select_resource_attributes(resource) click to toggle source

Given a JsonThing and the fields you want, outputs the json column for a SQL SELECT clause.

# File lib/active_model_serializers/adapter/json_api_pg.rb, line 440
  def select_resource_attributes(resource)
    fields = attribute_fields_for(resource)
    <<~EOQ
      jsonb_build_object(#{fields.map{|f| "'#{json_key(f)}', #{select_resource_attribute(resource, f)}"}.join(', ')})
    EOQ
  end
select_resource_relationship(resource) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 516
  def select_resource_relationship(resource)
    if resource.belongs_to?
      fk = %Q{"#{resource.parent.table_name}"."#{resource.foreign_key}"}
      <<~EOQ
        '#{resource.json_key}',
        jsonb_build_object('data',
                           CASE WHEN #{fk} IS NULL THEN NULL
                                ELSE jsonb_build_object('id', #{fk}::text,
                                                        'type', '#{resource.json_type}') END)
      EOQ
    elsif resource.has_many? or resource.has_one?
      refl = resource.reflection
      <<~EOQ
        '#{resource.json_key}',
         jsonb_build_object(#{refl.include_data ? %Q{'data', "rel_#{resource.cte_name}".j} : ''}
                            #{refl.include_data && refl.links.any? ? ',' : ''}
                            #{refl.links.any? ? %Q{'links',  jsonb_build_object(#{select_resource_relationship_links(resource, refl)})} : ''})
      EOQ
    else
      raise "Unknown kind of field reflection for #{resource.full_name}"
    end
  end
select_resource_relationships(resource) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 539
  def select_resource_relationships(resource)
    fields = reflection_fields_for(resource)
    children = fields.map{|f| get_json_thing(resource, f)}
    if children.any?
      <<~EOQ
        jsonb_build_object(#{children.map{|ch| select_resource_relationship(ch)}.join(', ')})
      EOQ
    else
      nil
    end
  end
serializer_attributes(resource) click to toggle source

Returns all the attributes listed in the serializer, after checking `include_foo?` methods.

# File lib/active_model_serializers/adapter/json_api_pg.rb, line 729
def serializer_attributes(resource)
  ms = Set.new(resource.serializer.instance_methods)
  resource.serializer._attributes.select{|f|
    if ms.include? "include_#{f}?".to_sym
      ser = resource.serializer.new(nil, @options)
      ser.send("include_#{f}?".to_sym)
    else
      true
    end
  }
end
serializer_reflections(resource) click to toggle source

Returns all the relationships listed in the serializer, after checking `include_foo?` methods.

# File lib/active_model_serializers/adapter/json_api_pg.rb, line 743
def serializer_reflections(resource)
  ms = Set.new(resource.serializer.instance_methods)
  resource.serializer._reflections.keys.select{|f|
    if ms.include? "include_#{f}?".to_sym
      ser = resource.serializer.new(nil, @options)
      ser.send("include_#{f}?".to_sym)
    else
      true
    end
  }
end
to_sql() click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 796
  def to_sql
    table_name = base_resource.table_name
    maybe_included = if include_selects.any?
                       %Q{, 'included', inc.j}
                     else
                       ''
                     end
    return <<~EOQ
      WITH
      t AS (
        #{base_relation.select(%Q{"#{base_resource.table_name}".*}).to_sql}
      ),
      t2 AS (
        #{many? ? "SELECT  COALESCE(jsonb_agg(#{select_resource(base_resource)}), '[]') AS j"
                : "SELECT                     #{select_resource(base_resource)}         AS j"}
        FROM    t AS "#{base_resource.table_name}"
        #{join_resource_relationships(base_resource)}
      ),
      #{include_ctes}
      #{include_jbsses}
      all_jbsses AS (
        SELECT  '{}'::jsonb AS j
        WHERE   1=0
        #{include_selects.join("\n")}
      ),
      inc AS (
        SELECT  COALESCE(jsonb_agg(j), '[]') AS j
        FROM    all_jbsses
      )
                        SELECT       jsonb_build_object('data', t2.j
                                 #{maybe_included})
      FROM    t2
      CROSS JOIN  inc
                EOQ
  end