File: instance_filters.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 (135 lines) | stat: -rw-r--r-- 4,579 bytes parent folder | download | duplicates (4)
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
# frozen-string-literal: true

module Sequel
  module Plugins
    # This plugin allows you to add filters on a per object basis that
    # restrict updating or deleting the object.  It's designed for cases
    # where you would normally have to drop down to the dataset level
    # to get the necessary control, because you only want to delete or
    # update the rows in certain cases based on the current status of
    # the row in the database.  The main purpose of this plugin is to
    # avoid race conditions by relying on the atomic properties of database
    # transactions.
    # 
    #   class Item < Sequel::Model
    #     plugin :instance_filters
    #   end
    #
    #   # These are two separate objects that represent the same
    #   # database row. 
    #   i1 = Item.first(id: 1, delete_allowed: false)
    #   i2 = Item.first(id: 1, delete_allowed: false)
    #
    #   # Add an instance filter to the object. This filter is in effect
    #   # until the object is successfully updated or deleted.
    #   i1.instance_filter(delete_allowed: true)
    #
    #   # Attempting to delete the object where the filter doesn't
    #   # match any rows raises an error.
    #   i1.delete # raises Sequel::NoExistingObject
    #
    #   # The other object that represents the same row has no
    #   # instance filters, and can be updated normally.
    #   i2.update(delete_allowed: true)
    #
    #   # Even though the filter is now still in effect, since the
    #   # database row has been updated to allow deleting,
    #   # delete now works.
    #   i1.delete
    #
    # This plugin sets the require_modification flag on the model,
    # so if the model's dataset doesn't provide an accurate number
    # of matched rows, this could result in invalid exceptions being raised.
    module InstanceFilters
      # Exception class raised when updating or deleting an object does
      # not affect exactly one row.
      Error = Sequel::NoExistingObject
      
      # Set the require_modification flag to true for the model.
      def self.configure(model)
        model.require_modification = true
      end

      module InstanceMethods
        # Clear the instance filters after successfully destroying the object.
        def after_destroy
          super
          clear_instance_filters
        end
        
        # Clear the instance filters after successfully updating the object.
        def after_update
          super
          clear_instance_filters
        end

        # Freeze the instance filters when freezing the object
        def freeze
          instance_filters.freeze
          super
        end
      
        # Add an instance filter to the array of instance filters
        # Both the arguments given and the block are passed to the
        # dataset's filter method.
        def instance_filter(*args, &block)
          instance_filters << [args, block]
        end
      
        private
        
        # If there are any instance filters, make sure not to use the
        # instance delete optimization.
        def _delete_without_checking
          if @instance_filters && !@instance_filters.empty?
            _delete_dataset.delete 
          else
            super
          end
        end

        # Duplicate internal structures when duplicating model instance.
        def initialize_copy(other)
          super
          @instance_filters = other.send(:instance_filters).dup
          self
        end
      
        # Lazily initialize the instance filter array.
        def instance_filters
          @instance_filters ||= []
        end
        
        # Apply the instance filters to the given dataset
        def apply_instance_filters(ds)
          instance_filters.inject(ds){|ds1, i| ds1.where(*i[0], &i[1])}
        end
        
        # Clear the instance filters.
        def clear_instance_filters
          instance_filters.clear
        end
        
        # Apply the instance filters to the dataset returned by super.
        def _delete_dataset
          apply_instance_filters(super)
        end
        
        # Apply the instance filters to the dataset returned by super.
        def _update_dataset
          apply_instance_filters(super)
        end

        # Only use prepared statements for update and delete queries
        # if there are no instance filters.
        def use_prepared_statements_for?(type)
          if type == :update && !instance_filters.empty?
            false
          else
            super if defined?(super)
          end
        end
      end
    end
  end
end