File: pg_array_associations.rb

package info (click to toggle)
ruby-sequel 5.63.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 10,408 kB
  • sloc: ruby: 113,747; makefile: 3
file content (580 lines) | stat: -rw-r--r-- 22,001 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
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
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
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
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
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
# frozen-string-literal: true

module Sequel
  extension :pg_array, :pg_array_ops

  module Plugins
    # This plugin allows you to create associations where the foreign keys
    # are stored in a PostgreSQL array column in one of the tables.  The
    # model with the table containing the array column has a
    # pg_array_to_many association to the associated model, and the
    # model with the table containing the primary key referenced by
    # elements in the array column has a many_to_pg_array association
    # to the associated model.
    #
    #   # Database schema:
    #   #   tags                albums
    #   #   :id (int4) <--\    :id
    #   #   :name          \-- :tag_ids (int4[])
    #   #                      :name
    #
    #   class Album
    #     plugin :pg_array_associations
    #     pg_array_to_many :tags
    #   end
    #   class Tag
    #     plugin :pg_array_associations
    #     many_to_pg_array :albums
    #   end
    #
    # These association types work similarly to Sequel's other association
    # types, so you can use them as you would any other association. Unlike
    # other associations, they do not support composite keys.
    #
    # One thing that is different is that the modification methods for
    # pg_array_to_many associations do not affect the database, since they
    # operate purely on the receiver.  For example:
    #
    #   album = Album[1]
    #   album.add_tag(Tag[2])
    #
    # does not save the album.  This allows you to call add_tag repeatedly
    # and the save after to combine all changes into a single query.  Note
    # that the many_to_pg_array association modification methods do save, so:
    #
    #   tag = Tag[2]
    #   tag.add_album(Album[1])
    #
    # will save the changes to the album.
    #
    # They support some additional options specific to this plugin:
    #
    # :array_type :: This overrides the type of the array.  By default, the type
    #                is determined by looking at the db_schema for the model, and if that fails,
    #                it defaults to :integer.
    # :raise_on_save_failure :: Do not raise exceptions for hook or validation failures when saving associated
    #                           objects in the add/remove methods (return nil instead).
    # :save_after_modify :: For pg_array_to_many associations, this makes the
    #                       the modification methods save the current object,
    #                       so they operate more similarly to the one_to_many
    #                       and many_to_many association modification methods.
    # :uniq :: Similar to many_to_many associations, this can be used to
    #          make sure the returned associated object array has uniq values.
    #
    # Note that until PostgreSQL gains the ability to enforce foreign key
    # constraints in array columns, this plugin is not recommended for
    # production use unless you plan on emulating referential integrity
    # constraints via triggers.
    #
    # This plugin should work on all supported PostgreSQL versions, except
    # the remove_all modification method for many_to_pg_array associations, which
    # requires the array_remove method added in PostgreSQL 9.3.
    #
    # This plugin requires that the underlying database have the pg_array
    # extension loaded.
    module PgArrayAssociations
      # The AssociationReflection subclass for many_to_pg_array associations.
      class ManyToPgArrayAssociationReflection < Sequel::Model::Associations::AssociationReflection
        Sequel.synchronize{Sequel::Model::Associations::ASSOCIATION_TYPES[:many_to_pg_array] = self}

        def array_type
          cached_fetch(:array_type) do
            if (sch = associated_class.db_schema) && (s = sch[self[:key]]) && (t = s[:db_type])
              t.sub(/\[\]\z/, '').freeze
            else
              :integer
            end
          end
        end

        # The array column in the associated model containing foreign keys to
        # the current model.
        def associated_object_keys
          [self[:key]]
        end

        # many_to_pg_array associations can have associated objects as long as they have
        # a primary key.
        def can_have_associated_objects?(obj)
          obj.get_column_value(self[:primary_key])
        end

        # Assume that the key in the associated table uses a version of the current
        # model's name suffixed with _ids.
        def default_key
          :"#{underscore(demodulize(self[:model].name))}_ids"
        end
        
        # Always use the ruby eager_graph limit strategy if association is limited.
        def eager_graph_limit_strategy(_)
          :ruby if self[:limit]
        end

        # Always use the ruby eager limit strategy
        def eager_limit_strategy
          cached_fetch(:_eager_limit_strategy) do
            :ruby if self[:limit]
          end
        end

        # Don't use a filter by associations limit strategy
        def filter_by_associations_limit_strategy
          nil
        end

        FINALIZE_SETTINGS = superclass::FINALIZE_SETTINGS.merge(
          :array_type=>:array_type
        ).freeze
        def finalize_settings
          FINALIZE_SETTINGS
        end

        # Handle silent failure of add/remove methods if raise_on_save_failure is false.
        def handle_silent_modification_failure?
          self[:raise_on_save_failure] == false
        end

        # The hash key to use for the eager loading predicate (left side of IN (1, 2, 3))
        def predicate_key
          cached_fetch(:predicate_key){qualify_assoc(self[:key_column])}
        end
    
        # The column in the current table that the keys in the array column in the
        # associated table reference.
        def primary_key
          self[:primary_key]
        end

        # Destroying the associated object automatically removes the association,
        # since the association is stored in the associated object.
        def remove_before_destroy?
          false
        end
    
        private
    
        # The predicate condition to use for the eager_loader.
        def eager_loading_predicate_condition(keys)
          Sequel.pg_array_op(predicate_key).overlaps(Sequel.pg_array(keys, array_type))
        end

        def filter_by_associations_add_conditions_dataset_filter(ds)
          key = qualify(associated_class.table_name, self[:key])
          ds.cross_join(Sequel.function(:unnest, key).as(:_smtopgaa_, [:_smtopgaa_key_])).exclude(key=>nil).select(:_smtopgaa_key_)
        end
        
        def filter_by_associations_conditions_key
          qualify(self[:model].table_name, primary_key)
        end

        # Only consider an association as a reciprocal if it has matching keys
        # and primary keys.
        def reciprocal_association?(assoc_reflect)
          super && self[:key] == assoc_reflect[:key] && primary_key == assoc_reflect.primary_key
        end

        def reciprocal_type
          :pg_array_to_many
        end

        def use_placeholder_loader?
          false
        end
      end

      # The AssociationReflection subclass for pg_array_to_many associations.
      class PgArrayToManyAssociationReflection < Sequel::Model::Associations::AssociationReflection
        Sequel.synchronize{Sequel::Model::Associations::ASSOCIATION_TYPES[:pg_array_to_many] = self}

        def array_type
          cached_fetch(:array_type) do
            if (sch = self[:model].db_schema) && (s = sch[self[:key]]) && (t = s[:db_type])
              t.sub(/\[\]\z/, '').freeze
            else
              :integer
            end
          end
        end

        # An array containing the primary key for the associated model.
        def associated_object_keys
          Array(primary_key)
        end

        # pg_array_to_many associations can only have associated objects if
        # the array field is not nil or empty.
        def can_have_associated_objects?(obj)
          v = obj.get_column_value(self[:key])
          v && !v.empty?
        end

        # pg_array_to_many associations do not need a primary key.
        def dataset_need_primary_key?
          false
        end

        # Use a default key name of *_ids, for similarity to other association types
        # that use *_id for single keys.
        def default_key
          :"#{singularize(self[:name])}_ids"
        end

        # Always use the ruby eager_graph limit strategy if association is limited.
        def eager_graph_limit_strategy(_)
          :ruby if self[:limit]
        end

        # Always use the ruby eager limit strategy
        def eager_limit_strategy
          cached_fetch(:_eager_limit_strategy) do
            :ruby if self[:limit]
          end
        end

        # Don't use a filter by associations limit strategy
        def filter_by_associations_limit_strategy
          nil
        end

        FINALIZE_SETTINGS = superclass::FINALIZE_SETTINGS.merge(
          :array_type=>:array_type,
          :primary_key=>:primary_key,
          :primary_key_method=>:primary_key_method
        ).freeze
        def finalize_settings
          FINALIZE_SETTINGS
        end

        # Handle silent failure of add/remove methods if raise_on_save_failure is false
        # and save_after_modify is true.
        def handle_silent_modification_failure?
          self[:raise_on_save_failure] == false && self[:save_after_modify]
        end

        # A qualified version of the associated primary key.
        def predicate_key
          cached_fetch(:predicate_key){qualify_assoc(primary_key)}
        end
    
        # The primary key of the associated model.
        def primary_key
          cached_fetch(:primary_key){associated_class.primary_key || raise(Error, "no primary key specified for #{associated_class.inspect}")}
        end

        # The method to call to get value of the primary key of the associated model.
        def primary_key_method
          cached_fetch(:primary_key_method){primary_key}
        end

        def filter_by_associations_conditions_expression(obj)
          ds = filter_by_associations_conditions_dataset.where(filter_by_associations_conditions_subquery_conditions(obj))
          Sequel.function(:coalesce, Sequel.pg_array(filter_by_associations_conditions_key).overlaps(ds), Sequel::SQL::Constants::FALSE)
        end

        private
    
        def filter_by_associations_add_conditions_dataset_filter(ds)
          pk = qualify(associated_class.table_name, primary_key)
          ds.select{array_agg(pk)}.exclude(pk=>nil)
        end
        
        def filter_by_associations_conditions_key
          qualify(self[:model].table_name, self[:key])
        end

        # Only consider an association as a reciprocal if it has matching keys
        # and primary keys.
        def reciprocal_association?(assoc_reflect)
          super && self[:key] == assoc_reflect[:key] && primary_key == assoc_reflect.primary_key
        end

        def reciprocal_type
          :many_to_pg_array
        end

        def use_placeholder_loader?
          false
        end
      end

      # Add the pg_array extension to the database
      def self.apply(model)
        model.db.extension(:pg_array)
      end

      module ClassMethods
        # Create a many_to_pg_array association, for the case where the associated
        # table contains the array with foreign keys pointing to the current table.
        # See associate for options.
        def many_to_pg_array(name, opts=OPTS, &block)
          associate(:many_to_pg_array, name, opts, &block)
        end

        # Create a pg_array_to_many association, for the case where the current
        # table contains the array with foreign keys pointing to the associated table.
        # See associate for options.
        def pg_array_to_many(name, opts=OPTS, &block)
          associate(:pg_array_to_many, name, opts, &block)
        end

        private

        # Setup the many_to_pg_array-specific datasets, eager loaders, and modification methods.
        def def_many_to_pg_array(opts)
          name = opts[:name]
          model = self
          pk = opts[:eager_loader_key] = opts[:primary_key] ||= model.primary_key
          raise(Error, "no primary key specified for #{inspect}") unless pk
          opts[:key] = opts.default_key unless opts.has_key?(:key)
          key = opts[:key]
          key_column = opts[:key_column] ||= opts[:key]
          if opts[:uniq]
            opts[:after_load] ||= []
            opts[:after_load].unshift(:array_uniq!)
          end
          opts[:dataset] ||= lambda do
            opts.associated_dataset.where(Sequel.pg_array_op(opts.predicate_key).contains(Sequel.pg_array([get_column_value(pk)], opts.array_type)))
          end
          opts[:eager_loader] ||= proc do |eo|
            id_map = eo[:id_map]
            eo = Hash[eo]
            eo[:loader] = false

            eager_load_results(opts, eo) do |assoc_record|
              if pks = assoc_record.get_column_value(key)
                pks.each do |pkv|
                  id_map[pkv].each do |object| 
                    object.associations[name].push(assoc_record)
                  end
                end
              end
            end
          end

          join_type = opts[:graph_join_type]
          select = opts[:graph_select]
          opts[:cartesian_product_number] ||= 1

          if opts.include?(:graph_only_conditions)
            conditions = opts[:graph_only_conditions]
            graph_block = opts[:graph_block]
          else
            conditions = opts[:graph_conditions]
            conditions = nil if conditions.empty?
            graph_block = proc do |j, lj, js|
              Sequel.pg_array_op(Sequel.deep_qualify(j, key_column)).contains([Sequel.deep_qualify(lj, opts.primary_key)])
            end

            if orig_graph_block = opts[:graph_block]
              pg_array_graph_block = graph_block
              graph_block = proc do |j, lj, js|
                Sequel.&(orig_graph_block.call(j,lj,js), pg_array_graph_block.call(j, lj, js))
              end
            end
          end

          opts[:eager_grapher] ||= proc do |eo|
            ds = eo[:self]
            ds = ds.graph(eager_graph_dataset(opts, eo), conditions, eo.merge(:select=>select, :join_type=>eo[:join_type]||join_type, :qualify=>:deep), &graph_block)
            ds
          end

          return if opts[:read_only]

          save_opts = {:validate=>opts[:validate]}
          save_opts[:raise_on_failure] = opts[:raise_on_save_failure] != false

          unless opts.has_key?(:adder)
            opts[:adder] = proc do |o|
              if array = o.get_column_value(key)
                array << get_column_value(pk)
              else
                o.set_column_value("#{key}=", Sequel.pg_array([get_column_value(pk)], opts.array_type))
              end
              o.save(save_opts)
            end
          end
    
          unless opts.has_key?(:remover)
            opts[:remover] = proc do |o|
              if (array = o.get_column_value(key)) && !array.empty?
                array.delete(get_column_value(pk))
                o.save(save_opts)
              end
            end
          end

          unless opts.has_key?(:clearer)
            opts[:clearer] = proc do
              pk_value = get_column_value(pk)
              db_type = opts.array_type
              opts.associated_dataset.where(Sequel.pg_array_op(key).contains(Sequel.pg_array([pk_value], db_type))).update(key=>Sequel.function(:array_remove, key, Sequel.cast(pk_value, db_type)))
            end
          end
        end

        # Setup the pg_array_to_many-specific datasets, eager loaders, and modification methods.
        def def_pg_array_to_many(opts)
          name = opts[:name]
          opts[:key] = opts.default_key unless opts.has_key?(:key)
          key = opts[:key]
          key_column = opts[:key_column] ||= key
          opts[:eager_loader_key] = nil
          if opts[:uniq]
            opts[:after_load] ||= []
            opts[:after_load].unshift(:array_uniq!)
          end
          opts[:dataset] ||= lambda do
            opts.associated_dataset.where(opts.predicate_key=>get_column_value(key).to_a)
          end
          opts[:eager_loader] ||= proc do |eo|
            rows = eo[:rows]
            id_map = {}
            pkm = opts.primary_key_method

            Sequel.synchronize_with(eo[:mutex]) do
              rows.each do |object|
                if associated_pks = object.get_column_value(key)
                  associated_pks.each do |apk|
                    (id_map[apk] ||= []) << object
                  end
                end
              end
            end

            eo = Hash[eo]
            eo[:id_map] = id_map
            eager_load_results(opts, eo) do |assoc_record|
              if objects = id_map[assoc_record.get_column_value(pkm)]
                objects.each do |object| 
                  object.associations[name].push(assoc_record)
                end
              end
            end
          end

          join_type = opts[:graph_join_type]
          select = opts[:graph_select]
          opts[:cartesian_product_number] ||= 1

          if opts.include?(:graph_only_conditions)
            conditions = opts[:graph_only_conditions]
            graph_block = opts[:graph_block]
          else
            conditions = opts[:graph_conditions]
            conditions = nil if conditions.empty?
            graph_block = proc do |j, lj, js|
              Sequel.pg_array_op(Sequel.deep_qualify(lj, key_column)).contains([Sequel.deep_qualify(j, opts.primary_key)])
            end

            if orig_graph_block = opts[:graph_block]
              pg_array_graph_block = graph_block
              graph_block = proc do |j, lj, js|
                Sequel.&(orig_graph_block.call(j,lj,js), pg_array_graph_block.call(j, lj, js))
              end
            end
          end

          opts[:eager_grapher] ||= proc do |eo|
            ds = eo[:self]
            ds = ds.graph(eager_graph_dataset(opts, eo), conditions, eo.merge(:select=>select, :join_type=>eo[:join_type]||join_type, :qualify=>:deep), &graph_block)
            ds
          end

          return if opts[:read_only]

          save_opts = {:validate=>opts[:validate]}
          save_opts[:raise_on_failure] = opts[:raise_on_save_failure] != false

          if opts[:save_after_modify]
            save_after_modify = proc do |obj|
              obj.save(save_opts)
            end
          end

          unless opts.has_key?(:adder)
            opts[:adder] = proc do |o|
              opk = o.get_column_value(opts.primary_key) 
              if array = get_column_value(key)
                modified!(key)
                array << opk
              else
                set_column_value("#{key}=", Sequel.pg_array([opk], opts.array_type))
              end
              save_after_modify.call(self) if save_after_modify
            end
          end
    
          unless opts.has_key?(:remover)
            opts[:remover] = proc do |o|
              if (array = get_column_value(key)) && !array.empty?
                modified!(key)
                array.delete(o.get_column_value(opts.primary_key))
                save_after_modify.call(self) if save_after_modify
              end
            end
          end

          unless opts.has_key?(:clearer)
            opts[:clearer] = proc do
              if (array = get_column_value(key)) && !array.empty?
                modified!(key)
                array.clear
                save_after_modify.call(self) if save_after_modify
              end
            end
          end
        end
      end

      module DatasetMethods
        private

        # Support filtering by many_to_pg_array associations using a subquery.
        def many_to_pg_array_association_filter_expression(op, ref, obj)
          pk = ref.qualify(model.table_name, ref.primary_key)
          key = ref[:key]
          # :nocov:
          expr = case obj
          # :nocov:
          when Sequel::Model
            if (assoc_pks = obj.get_column_value(key)) && !assoc_pks.empty?
              Sequel[pk=>assoc_pks.to_a]
            end
          when Array
            if (assoc_pks = obj.map{|o| o.get_column_value(key)}.flatten.compact.uniq) && !assoc_pks.empty?
              Sequel[pk=>assoc_pks]
            end
          when Sequel::Dataset
            obj.select(ref.qualify(obj.model.table_name, ref[:key_column]).as(:key)).from_self.where{{pk=>any(:key)}}.select(1).exists
          end
          expr = Sequel::SQL::Constants::FALSE unless expr
          expr = add_association_filter_conditions(ref, obj, expr)
          association_filter_handle_inversion(op, expr, [pk])
        end

        # Support filtering by pg_array_to_many associations using a subquery.
        def pg_array_to_many_association_filter_expression(op, ref, obj)
          key = ref.qualify(model.table_name, ref[:key_column])
          # :nocov:
          expr = case obj
          # :nocov:
          when Sequel::Model
            if pkv = obj.get_column_value(ref.primary_key_method)
              Sequel.pg_array_op(key).contains(Sequel.pg_array([pkv], ref.array_type))
            end
          when Array
            if (pkvs = obj.map{|o| o.get_column_value(ref.primary_key_method)}.compact) && !pkvs.empty?
              Sequel.pg_array(key).overlaps(Sequel.pg_array(pkvs, ref.array_type))
            end
          when Sequel::Dataset
            Sequel.function(:coalesce, Sequel.pg_array_op(key).overlaps(obj.select{array_agg(ref.qualify(obj.model.table_name, ref.primary_key))}), Sequel::SQL::Constants::FALSE)
          end
          expr = Sequel::SQL::Constants::FALSE unless expr
          expr = add_association_filter_conditions(ref, obj, expr)
          association_filter_handle_inversion(op, expr, [key])
        end
      end
    end
  end
end