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



769
770
771
772
773
774
775
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 769

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



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

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



781
782
783
784
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 781

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



765
766
767
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 765

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

#base_resourceObject



694
695
696
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 694

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



745
746
747
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 745

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.



662
663
664
665
666
667
668
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 662

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.



630
631
632
633
634
635
636
637
638
639
640
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 630

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



618
619
620
621
622
623
624
625
626
627
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 618

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



670
671
672
673
674
675
676
677
678
679
680
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 670

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.



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

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



682
683
684
685
686
687
688
689
690
691
692
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 682

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



606
607
608
609
610
611
612
613
614
615
616
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 606

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



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

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
602
603
604
# 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
        ActiveSupport::Deprecation.silence do
          # TODO: Calling `orders` prints deprecation warnings, so find another way:
          ordering = p.send(refl.name).orders
          ordering = child_resource.ar_class.default_scoped.orders if ordering.empty?
        end
        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}\"::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}\"::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}\"::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



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



698
699
700
701
702
703
704
705
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 698

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



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

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

#select_resource(resource) ⇒ Object



707
708
709
710
711
712
713
714
715
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 707

def select_resource(resource)
  fields = fields_for(resource)
  "    jsonb_build_object('id', \"\#{resource.table_name}\".\"\#{resource.primary_key}\"::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.



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



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)
  "    jsonb_build_object(\#{fields.map{|f| \"'\#{json_key(f)}', \#{select_resource_attribute(resource, f)}\"}.join(', ')})\n  EOQ\nend\n"

#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}"}
    "      '\#{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"


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|
    "      '\#{link_name}', CONCAT(\#{link_parts.join(%Q{, \"\#{resource.parent.table_name}\".\"\#{resource.parent.primary_key}\", })})\n    EOQ\n  }.join(\",\\n\")\nend\n"

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



719
720
721
722
723
724
725
726
727
728
729
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 719

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.



733
734
735
736
737
738
739
740
741
742
743
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 733

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



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
818
819
820
# File 'lib/active_model_serializers/adapter/json_api_pg.rb', line 786

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