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.



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
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# 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

Instance Attribute Details

#base_relationObject (readonly)

Returns the value of attribute base_relation.



373
374
375
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 373

def base_relation
  @base_relation
end

#base_serializerObject (readonly)

Returns the value of attribute base_serializer.



373
374
375
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 373

def base_serializer
  @base_serializer
end

Class Method Details

.json_column_typeObject



375
376
377
378
379
380
381
382
# 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

Instance Method Details

#_attribute_fields_for(resource) ⇒ Object



779
780
781
782
783
784
785
# 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) ⇒ Object



759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
# 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) ⇒ Object



791
792
793
794
# 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) ⇒ Object



775
776
777
# 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_resourceObject



704
705
706
# 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) ⇒ Boolean

Returns:

  • (Boolean)


498
499
500
501
# 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) ⇒ Boolean

Returns:

  • (Boolean)


503
504
505
506
# 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) ⇒ Boolean

Returns:

  • (Boolean)


489
490
491
# 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) ⇒ Boolean

Returns:

  • (Boolean)


493
494
495
496
# 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) ⇒ Object



755
756
757
# 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) ⇒ Object



425
426
427
428
# 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) ⇒ 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.



672
673
674
675
676
677
678
# 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) ⇒ Object

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



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

def include_cte(resource)
  parent = resource.parent
  "    SELECT  DISTINCT ON (\"\#{resource.table_name}\".\"\#{resource.primary_key}\")\n            \"\#{resource.table_name}\".*\n    FROM    \"\#{resource.table_name}\"\n    JOIN    \"\#{parent.cte_name}\"\n    ON      \#{include_cte_join_condition(resource)}\n    ORDER BY \"\#{resource.table_name}\".\"\#{resource.primary_key}\"\n  EOQ\nend\n"

#include_cte_join_condition(resource) ⇒ Object



628
629
630
631
632
633
634
635
636
637
# 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_ctesObject



680
681
682
683
684
685
686
687
688
689
690
# 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)
    "      \"\#{th.cte_name}\" AS (\n        \#{include_cte(th)}\n      ),\n    EOQ\n  }.join(\"\\n\")\nend\n"

#include_jbs(resource) ⇒ Object

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



653
654
655
656
657
658
659
660
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 653

def include_jbs(resource)
  "    SELECT  \"\#{resource.table_name}\".*,\n            \#{select_resource(resource)} AS j\n    FROM    \"\#{resource.cte_name}\" AS \"\#{resource.table_name}\"\n    \#{join_resource_relationships(resource)}\n  EOQ\nend\n"

#include_jbssesObject



692
693
694
695
696
697
698
699
700
701
702
# 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)
    "      \"\#{th.jbs_name}\" AS (\n        \#{include_jbs(th)}\n      ),\n    EOQ\n  }.join(\"\\n\")\nend\n"

#include_selectsObject



616
617
618
619
620
621
622
623
624
625
626
# 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

#includesObject



662
663
664
665
666
667
# 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) ⇒ Object



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
602
603
604
605
606
607
608
609
610
611
612
613
614
# 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
        "          LEFT OUTER JOIN LATERAL (\n            SELECT  coalesce(jsonb_agg(jsonb_build_object('id', rel.\"\#{child_resource.primary_key_attr}\"::text,\n                                                          'type', '\#{child_resource.json_type}') \#{ordering}), '[]') AS j\n            FROM    \"\#{child_resource.table_name}\" rel\n            WHERE   rel.\"\#{child_resource.foreign_key}\" = \"\#{resource.table_name}\".\"\#{resource.primary_key}\"\n          ) \"rel_\#{child_resource.cte_name}\" ON true\n        EOQ\n      elsif not refl.reflection_sql.nil?  # can't use .present? since that loads the Relation!\n        case refl.reflection_sql\n        when String\n          raise \"TODO\"\n        when ActiveRecord::Relation\n          rel = refl.reflection_sql\n          sql = rel.select(<<~EOQ).to_sql\n            coalesce(jsonb_agg(jsonb_build_object('id', \"\#{child_resource.table_name}\".\"\#{child_resource.primary_key_attr}\"::text,\n                                                  'type', '\#{child_resource.json_type}')), '[]') AS j\n          EOQ\n          <<~EOQ\n            LEFT OUTER JOIN LATERAL (\n              \#{sql}\n            ) \"rel_\#{child_resource.cte_name}\" ON true\n          EOQ\n        end\n      end\n    elsif refl.has_one?\n      <<~EOQ\n        LEFT OUTER JOIN LATERAL (\n          SELECT  jsonb_build_object('id', rel.\"\#{child_resource.primary_key_attr}\"::text,\n                                    'type', '\#{child_resource.json_type}') AS j\n          FROM    \"\#{child_resource.table_name}\" rel\n          WHERE   rel.\"\#{child_resource.foreign_key}\" = \"\#{resource.table_name}\".\"\#{resource.primary_key}\"\n        ) \"rel_\#{child_resource.cte_name}\" ON true\n      EOQ\n    else\n      nil\n    end\n  }.compact.join(\"\\n\")\nend\n"

#json_key(name) ⇒ Object



434
435
436
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 434

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

#many?Boolean

Returns:

  • (Boolean)


430
431
432
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 430

def many?
  @many
end

#maybe_select_resource_relationships(resource) ⇒ Object



708
709
710
711
712
713
714
715
# 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) ⇒ Object



787
788
789
# 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) ⇒ Object



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

def select_resource(resource)
  fields = fields_for(resource)
  "    jsonb_build_object('id', \"\#{resource.table_name}\".\"\#{resource.primary_key_attr}\"::text,\n                       'type', '\#{resource.json_type}',\n                       'attributes', \#{select_resource_attributes(resource)}\n                       \#{maybe_select_resource_relationships(resource)})\n  EOQ\nend\n"

#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.



451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
# 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
    "      CASE \#{typ.as_json['mapping'].map{|str, int| %Q{WHEN \"\#{resource.table_name}\".\"\#{field}\" = \#{int} THEN '\#{str}'}}.join(\"\\n     \")} END\n    EOQ\n  elsif resource.has_sql_method?(field)\n    resource.sql_method(field)\n  else\n    field = resource.unaliased(field)\n    # Standard AMS dasherizes json/jsonb/hstore columns,\n    # so we have to do the same:\n    if ActiveModelSerializers.config.key_transform == :dash\n      cl = resource.ar_class.attribute_types[field.to_s]\n      if column_is_jsonb? cl\n        %Q{jsonb_dasherize(\"\#{resource.table_name}\".\"\#{field}\")}\n      elsif column_is_jsonb_array? cl\n        # TODO: Could be faster:\n        # If we made the jsonb_dasherize function smarter so it could handle jsonb[],\n        # we wouldn't have to build a json object from the array then cast to jsonb[].\n        %Q{jsonb_dasherize(array_to_json(\"\#{resource.table_name}\".\"\#{field}\")::jsonb)}\n      elsif column_is_castable_to_jsonb? cl\n        # Fortunately we can cast hstore to jsonb,\n        # which gives us a solution that works whether or not the hstore extension is installed.\n        # Defining an hstore_dasherize function would work only if the extension were present.\n        %Q{jsonb_dasherize(\"\#{resource.table_name}\".\"\#{field}\"::jsonb)}\n      elsif column_is_castable_to_jsonb_array? cl\n        # TODO: Could be faster (see above):\n        %Q{jsonb_dasherize(array_to_json(\"\#{resource.table_name}\".\"\#{field}\"::jsonb[])::jsonb)}\n      else\n        %Q{\"\#{resource.table_name}\".\"\#{field}\"}\n      end\n    else\n      %Q{\"\#{resource.table_name}\".\"\#{field}\"}\n    end\n  end\nend\n"

#select_resource_attributes(resource) ⇒ Object

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



440
441
442
443
444
445
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 440

def select_resource_attributes(resource)
  fields = attribute_fields_for(resource)
  "    jsonb_build_object(\#{fields.map{|f| \"'\#{json_key(f)}', \#{select_resource_attribute(resource, f)}\"}.join(', ')})\n  EOQ\nend\n"

#select_resource_relationship(resource) ⇒ Object



516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
# 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}"}
    "      '\#{resource.json_key}',\n      jsonb_build_object('data',\n                         CASE WHEN \#{fk} IS NULL THEN NULL\n                              ELSE jsonb_build_object('id', \#{fk}::text,\n                                                      'type', '\#{resource.json_type}') END)\n    EOQ\n  elsif resource.has_many? or resource.has_one?\n    refl = resource.reflection\n    <<~EOQ\n      '\#{resource.json_key}',\n       jsonb_build_object(\#{refl.include_data ? %Q{'data', \"rel_\#{resource.cte_name}\".j} : ''}\n                          \#{refl.include_data && refl.links.any? ? ',' : ''}\n                          \#{refl.links.any? ? %Q{'links',  jsonb_build_object(\#{select_resource_relationship_links(resource, refl)})} : ''})\n    EOQ\n  else\n    raise \"Unknown kind of field reflection for \#{resource.full_name}\"\n  end\nend\n"


508
509
510
511
512
513
514
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 508

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

#select_resource_relationships(resource) ⇒ Object



539
540
541
542
543
544
545
546
547
548
549
# 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?
    "      jsonb_build_object(\#{children.map{|ch| select_resource_relationship(ch)}.join(', ')})\n    EOQ\n  else\n    nil\n  end\nend\n"

#serializer_attributes(resource) ⇒ Object

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



729
730
731
732
733
734
735
736
737
738
739
# 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) ⇒ Object

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



743
744
745
746
747
748
749
750
751
752
753
# 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_sqlObject



796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
# 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 "  WITH\n  t AS (\n    \#{base_relation.select(%Q{\"\#{base_resource.table_name}\".*}).to_sql}\n  ),\n  t2 AS (\n    \#{many? ? \"SELECT  COALESCE(jsonb_agg(\#{select_resource(base_resource)}), '[]') AS j\"\n            : \"SELECT                     \#{select_resource(base_resource)}         AS j\"}\n    FROM    t AS \"\#{base_resource.table_name}\"\n    \#{join_resource_relationships(base_resource)}\n  ),\n  \#{include_ctes}\n  \#{include_jbsses}\n  all_jbsses AS (\n    SELECT  '{}'::jsonb AS j\n    WHERE   1=0\n    \#{include_selects.join(\"\\n\")}\n  ),\n  inc AS (\n    SELECT  COALESCE(jsonb_agg(j), '[]') AS j\n    FROM    all_jbsses\n  )\nSELECT jsonb_build_object('data', t2.j\n                             \#{maybe_included})\n  FROM    t2\n  CROSS JOIN  inc\n"
end