Class: JsonApiPgSql

Inherits:
Object
  • Object
show all
Defined in:
lib/active_model_serializers/adapter/json_api_pg.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(base_serializer, base_relation, instance_options, options) ⇒ JsonApiPgSql

Returns a new instance of JsonApiPgSql.



370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 370

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

Instance Attribute Details

#base_relationObject (readonly)

Returns the value of attribute base_relation.



359
360
361
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 359

def base_relation
  @base_relation
end

#base_serializerObject (readonly)

Returns the value of attribute base_serializer.



359
360
361
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 359

def base_serializer
  @base_serializer
end

Class Method Details

.json_column_typeObject



361
362
363
364
365
366
367
368
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 361

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

Instance Method Details

#_attribute_fields_for(resource) ⇒ Object



766
767
768
769
770
771
772
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 766

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) ⇒ Object



746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 746

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) ⇒ Object



778
779
780
781
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 778

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) ⇒ Object



762
763
764
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 762

def attribute_fields_for(resource)
  @attribute_fields_for[resource.full_name] ||= _attribute_fields_for(resource)
end

#base_resourceObject



691
692
693
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 691

def base_resource
  @json_things[:base]
end

#column_is_castable_to_jsonb?(column_class) ⇒ Boolean

Returns:

  • (Boolean)


484
485
486
487
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 484

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) ⇒ Boolean

Returns:

  • (Boolean)


489
490
491
492
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 489

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) ⇒ Boolean

Returns:

  • (Boolean)


475
476
477
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 475

def column_is_jsonb?(column_class)
  column_class.is_a? ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb
end

#column_is_jsonb_array?(column_class) ⇒ Boolean

Returns:

  • (Boolean)


479
480
481
482
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 479

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) ⇒ Object



742
743
744
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 742

def fields_for(resource)
  @fields_for[resource.full_name] ||= _fields_for(resource)
end

#get_json_thing(resource, field) ⇒ Object



411
412
413
414
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 411

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) ⇒ Object

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.



659
660
661
662
663
664
665
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 659

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) ⇒ Object

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



627
628
629
630
631
632
633
634
635
636
637
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 627

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) ⇒ Object



615
616
617
618
619
620
621
622
623
624
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 615

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_ctesObject



667
668
669
670
671
672
673
674
675
676
677
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 667

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) ⇒ Object

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



640
641
642
643
644
645
646
647
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 640

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_jbssesObject



679
680
681
682
683
684
685
686
687
688
689
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 679

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_selectsObject



603
604
605
606
607
608
609
610
611
612
613
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 603

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

#includesObject



649
650
651
652
653
654
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 649

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) ⇒ Object



537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 537

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 = nil
        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}"::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}"::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}"::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) ⇒ Object



420
421
422
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 420

def json_key(name)
  JsonThing.json_key(name)
end

#many?Boolean

Returns:

  • (Boolean)


416
417
418
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 416

def many?
  @many
end

#maybe_select_resource_relationships(resource) ⇒ Object



695
696
697
698
699
700
701
702
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 695

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) ⇒ Object



774
775
776
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 774

def reflection_fields_for(resource)
  @reflection_fields_for[resource.full_name] ||= _reflection_fields_for(resource)
end

#select_resource(resource) ⇒ Object



704
705
706
707
708
709
710
711
712
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 704

def select_resource(resource)
  fields = fields_for(resource)
  <<~EOQ
    jsonb_build_object('id', "#{resource.table_name}"."#{resource.primary_key}"::text,
                       'type', '#{resource.json_type}',
                       'attributes', #{select_resource_attributes(resource)}
                       #{maybe_select_resource_relationships(resource)})
  EOQ
end

#select_resource_attribute(resource, field) ⇒ Object

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.



437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 437

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) ⇒ Object

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



426
427
428
429
430
431
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 426

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) ⇒ Object



502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 502

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


494
495
496
497
498
499
500
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 494

def select_resource_relationship_links(resource, reflection)
  reflection.links.map {|link_name, link_parts|
    <<~EOQ
      '#{link_name}', CONCAT(#{link_parts.join(%Q{, "#{resource.parent.table_name}"."#{resource.parent.primary_key}", })})
    EOQ
  }.join(",\n")
end

#select_resource_relationships(resource) ⇒ Object



525
526
527
528
529
530
531
532
533
534
535
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 525

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) ⇒ Object

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



716
717
718
719
720
721
722
723
724
725
726
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 716

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) ⇒ Object

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



730
731
732
733
734
735
736
737
738
739
740
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 730

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_sqlObject



783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 783

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