Class: HairTrigger::Builder

Inherits:
Object
  • Object
show all
Defined in:
lib/hair_trigger/builder.rb

Defined Under Namespace

Classes: DeclarationError, GenerationError

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name = nil, options = {}) ⇒ Builder

Returns a new instance of Builder.



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/hair_trigger/builder.rb', line 12

def initialize(name = nil, options = {})
  @adapter = options[:adapter]
  @compatibility = options.delete(:compatibility) || self.class.compatibility
  @options = {}
  @chained_calls = []
  @errors = []
  @warnings = []
  set_name(name) if name
  {:timing => :after, :for_each => :row}.update(options).each do |key, value|
    if respond_to?("set_#{key}")
      send("set_#{key}", *Array[value])
    else
      @options[key] = value
    end
  end
end

Class Attribute Details

.base_compatibilityObject



568
569
570
# File 'lib/hair_trigger/builder.rb', line 568

def base_compatibility
  @base_compatibility ||= 0
end

.show_warningsObject



563
564
565
566
# File 'lib/hair_trigger/builder.rb', line 563

def show_warnings
  @show_warnings = true if @show_warnings.nil?
  @show_warnings
end

.tab_spacingObject



559
560
561
# File 'lib/hair_trigger/builder.rb', line 559

def tab_spacing
  @tab_spacing ||= 4
end

Instance Attribute Details

#optionsObject

Returns the value of attribute options.



8
9
10
# File 'lib/hair_trigger/builder.rb', line 8

def options
  @options
end

#prepared_actionsObject (readonly)

after delayed interpolation



10
11
12
# File 'lib/hair_trigger/builder.rb', line 10

def prepared_actions
  @prepared_actions
end

#prepared_whereObject (readonly)

after delayed interpolation



10
11
12
# File 'lib/hair_trigger/builder.rb', line 10

def prepared_where
  @prepared_where
end

#triggersObject (readonly)

nil unless this is a trigger group



9
10
11
# File 'lib/hair_trigger/builder.rb', line 9

def triggers
  @triggers
end

Class Method Details

.chainable_methods(*methods) ⇒ Object



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
# File 'lib/hair_trigger/builder.rb', line 148

def self.chainable_methods(*methods)
  methods.each do |method|
    class_eval <<-METHOD, __FILE__, __LINE__ + 1
      alias #{method}_orig #{method}
      def #{method}(*args, &block)
        @chained_calls << :#{method}
        if @triggers || @trigger_group
          @errors << ["mysql doesn't support #{method} within a trigger group", *HairTrigger::MYSQL_ADAPTERS] unless [:name, :where, :all, :of].include?(:#{method})
        end
        set_#{method}(*args, &(block_given? ? block : nil))
      end
      def set_#{method}(*args, &block)
        if @triggers # i.e. each time we say t.something within a trigger group block
          @chained_calls.pop # the subtrigger will get this, we don't need it
          @chained_calls = @chained_calls.uniq
          @triggers << trigger = clone
          trigger.#{method}(*args, &(block_given? ? block : nil))
        else
          #{method}_orig(*args, &block)
          maybe_execute(&block) if block_given?
          self
        end
      end
    METHOD
  end
end

.compatibilityObject



572
573
574
575
576
577
578
579
580
581
582
# File 'lib/hair_trigger/builder.rb', line 572

def compatibility
  @compatibility ||= begin
    if HairTrigger::VERSION <= "0.1.3"
      0 # initial releases
    else
      1 # postgres RETURN bugfix
    # TODO: add more as we implement things that change the generated
    # triggers (e.g. chained call merging)
    end
  end
end

Instance Method Details

#<=>(other) ⇒ Object



277
278
279
280
281
# File 'lib/hair_trigger/builder.rb', line 277

def <=>(other)
  ret = prepared_name <=> other.prepared_name
  return ret unless ret == 0
  hash <=> other.hash
end

#==(other) ⇒ Object



283
284
285
# File 'lib/hair_trigger/builder.rb', line 283

def ==(other)
  components == other.components
end

#after(*events) ⇒ Object



67
68
69
70
# File 'lib/hair_trigger/builder.rb', line 67

def after(*events)
  set_timing(:after)
  set_events(*events)
end

#allObject

noop, just a way you can pass a block within a trigger group



102
103
# File 'lib/hair_trigger/builder.rb', line 102

def all
end

#all_namesObject



138
139
140
# File 'lib/hair_trigger/builder.rb', line 138

def all_names
  [prepared_name] + (@triggers ? @triggers.map(&:prepared_name) : [])
end

#all_triggers(include_self = true) ⇒ Object



142
143
144
145
146
# File 'lib/hair_trigger/builder.rb', line 142

def all_triggers(include_self = true)
  triggers = []
  triggers << self if include_self
  (@triggers || []).map(&:all_triggers).inject(triggers, &:concat)
end

#before(*events) ⇒ Object



62
63
64
65
# File 'lib/hair_trigger/builder.rb', line 62

def before(*events)
  set_timing(:before)
  set_events(*events)
end

#change_clause(column) ⇒ Object



201
202
203
# File 'lib/hair_trigger/builder.rb', line 201

def change_clause(column)
  "NEW.#{column} <> OLD.#{column} OR (NEW.#{column} IS NULL) <> (OLD.#{column} IS NULL)"
end

#componentsObject



296
297
298
# File 'lib/hair_trigger/builder.rb', line 296

def components
  [@options, @prepared_actions, @explicit_where, @triggers, @compatibility]
end

#create_grouped_trigger?Boolean

Returns:

  • (Boolean)


176
177
178
# File 'lib/hair_trigger/builder.rb', line 176

def create_grouped_trigger?
  HairTrigger::MYSQL_ADAPTERS.include?(adapter_name)
end

#declare(declarations) ⇒ Object



97
98
99
# File 'lib/hair_trigger/builder.rb', line 97

def declare(declarations)
  options[:declarations] = declarations
end

#drop_triggersObject



42
43
44
# File 'lib/hair_trigger/builder.rb', line 42

def drop_triggers
  all_names.map{ |name| self.class.new(name, {:table => options[:table], :drop => true}) }
end

#eql?(other) ⇒ Boolean

Returns:

  • (Boolean)


287
288
289
# File 'lib/hair_trigger/builder.rb', line 287

def eql?(other)
  other.is_a?(HairTrigger::Builder) && self == other
end

#errorsObject



300
301
302
# File 'lib/hair_trigger/builder.rb', line 300

def errors
  (@triggers || []).map(&:errors).inject(@errors, &:+)
end

#events(*events) ⇒ Object

Raises:



121
122
123
124
125
126
127
128
# File 'lib/hair_trigger/builder.rb', line 121

def events(*events)
  events << :insert if events.delete(:create)
  events << :delete if events.delete(:destroy)
  raise DeclarationError, "invalid events" unless events & [:insert, :update, :delete, :truncate] == events
  @errors << ["sqlite and mysql triggers may not be shared by multiple actions", *HairTrigger::MYSQL_ADAPTERS, *HairTrigger::SQLITE_ADAPTERS] if events.size > 1
  @errors << ["sqlite and mysql do not support truncate triggers", *HairTrigger::MYSQL_ADAPTERS, *HairTrigger::SQLITE_ADAPTERS] if events.include?(:truncate)
  options[:events] = events.map{ |e| e.to_s.upcase }
end

#for_each(for_each) ⇒ Object

Raises:



56
57
58
59
60
# File 'lib/hair_trigger/builder.rb', line 56

def for_each(for_each)
  @errors << ["sqlite and mysql don't support FOR EACH STATEMENT triggers", *HairTrigger::SQLITE_ADAPTERS, *HairTrigger::MYSQL_ADAPTERS] if for_each == :statement
  raise DeclarationError, "invalid for_each" unless [:row, :statement].include?(for_each)
  options[:for_each] = for_each.to_s.upcase
end

#generate(validate = true) ⇒ Object

Raises:



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
# File 'lib/hair_trigger/builder.rb', line 222

def generate(validate = true)
  validate!(@trigger_group ? :both : :down) if validate

  return @triggers.map{ |t| t.generate(false) }.flatten if @triggers && !create_grouped_trigger?
  prepare!
  raise GenerationError, "need to specify the table" unless options[:table]
  if options[:drop]
    generate_drop_trigger
  else
    raise GenerationError, "no actions specified" if @triggers && create_grouped_trigger? ? @triggers.any?{ |t| t.raw_actions.nil? } : raw_actions.nil?
    raise GenerationError, "need to specify the event(s) (:insert, :update, :delete)" if !options[:events] || options[:events].empty?
    raise GenerationError, "need to specify the timing (:before/:after)" unless options[:timing]

    [generate_drop_trigger] +
    [case adapter_name
      when *HairTrigger::SQLITE_ADAPTERS
        generate_trigger_sqlite
      when *HairTrigger::MYSQL_ADAPTERS
        generate_trigger_mysql
      when *HairTrigger::POSTGRESQL_ADAPTERS
        generate_trigger_postgresql
      else
        raise GenerationError, "don't know how to build #{adapter_name} triggers yet"
    end].flatten
  end
end

#hashObject



291
292
293
294
# File 'lib/hair_trigger/builder.rb', line 291

def hash
  prepare!
  components.hash
end

#initialize_copy(other) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/hair_trigger/builder.rb', line 29

def initialize_copy(other)
  @trigger_group = other
  @triggers = nil
  @chained_calls = []
  @errors = []
  @warnings = []
  @options = @options.dup
  @options.delete(:name) # this will be inferred (or set further down the line)
  @options.each do |key, value|
    @options[key] = value.dup rescue value
  end
end

#name(name) ⇒ Object



46
47
48
49
# File 'lib/hair_trigger/builder.rb', line 46

def name(name)
  @errors << ["trigger name cannot exceed 63 for postgres", *HairTrigger::POSTGRESQL_ADAPTERS] if name.to_s.size > 63
  options[:name] = name.to_s
end

#new_as(table) ⇒ Object

Raises:



91
92
93
94
95
# File 'lib/hair_trigger/builder.rb', line 91

def new_as(table)
  raise DeclarationError, "`new_as' requested, but no table_name specified" unless table.present?
  options[:referencing] ||= {}
  options[:referencing][:new] = table
end

#nowrap(flag = true) ⇒ Object



76
77
78
# File 'lib/hair_trigger/builder.rb', line 76

def nowrap(flag = true)
  options[:nowrap] = flag
end

#of(*columns) ⇒ Object

Raises:



80
81
82
83
# File 'lib/hair_trigger/builder.rb', line 80

def of(*columns)
  raise DeclarationError, "`of' requested, but no columns specified" unless columns.present?
  options[:of] = columns
end

#old_as(table) ⇒ Object

Raises:



85
86
87
88
89
# File 'lib/hair_trigger/builder.rb', line 85

def old_as(table)
  raise DeclarationError, "`old_as' requested, but no table_name specified" unless table.present?
  options[:referencing] ||= {}
  options[:referencing][:old] = table
end

#on(table) ⇒ Object

Raises:



51
52
53
54
# File 'lib/hair_trigger/builder.rb', line 51

def on(table)
  raise DeclarationError, "table has already been specified" if options[:table]
  options[:table] = table.to_s
end

#prepare!Object



180
181
182
183
184
185
186
187
188
189
# File 'lib/hair_trigger/builder.rb', line 180

def prepare!
  @triggers.each(&:prepare!) if @triggers
  prepare_where!
  if @actions
    @prepared_actions = @actions.is_a?(Hash) ?
      @actions.inject({}){ |hash, (key, value)| hash[key] = interpolate(value).rstrip; hash } :
      interpolate(@actions).rstrip
  end
  all_names # ensure (component) trigger names are all cached
end

#prepare_where!Object



191
192
193
194
195
196
197
198
199
# File 'lib/hair_trigger/builder.rb', line 191

def prepare_where!
  parts = []
  parts << @explicit_where = options[:where] = interpolate(options[:where]) if options[:where]
  parts << options[:of].map{ |col| change_clause(col) }.join(" OR ") if options[:of] && !supports_of?
  if parts.present?
    parts.map!{ |part| "(" + part + ")" } if parts.size > 1
    @prepared_where = parts.join(" AND ")
  end
end

#prepared_nameObject



134
135
136
# File 'lib/hair_trigger/builder.rb', line 134

def prepared_name
  @prepared_name ||= options[:name] ||= infer_name
end

#raw_actionsObject



130
131
132
# File 'lib/hair_trigger/builder.rb', line 130

def raw_actions
  @raw_actions ||= prepared_actions.is_a?(Hash) ? prepared_actions[adapter_name] || prepared_actions[:default] : prepared_actions
end

#security(user) ⇒ Object



105
106
107
108
109
110
111
112
113
114
# File 'lib/hair_trigger/builder.rb', line 105

def security(user)
  unless [:invoker, :definer].include?(user) || user.to_s =~ /\A'[^']+'@'[^']+'\z/ || user.to_s.downcase =~ /\Acurrent_user(\(\))?\z/
    raise DeclarationError, "trigger security should be :invoker, :definer, CURRENT_USER, or a valid user (e.g. 'user'@'host')"
  end
  # sqlite default is n/a, mysql default is :definer, postgres default is :invoker
  @errors << ["sqlite doesn't support trigger security", *HairTrigger::SQLITE_ADAPTERS]
  @errors << ["postgresql doesn't support arbitrary users for trigger security", *HairTrigger::POSTGRESQL_ADAPTERS] unless [:definer, :invoker].include?(user)
  @errors << ["mysql doesn't support invoker trigger security", *HairTrigger::MYSQL_ADAPTERS] if user == :invoker
  options[:security] = user
end

#timing(timing) ⇒ Object

Raises:



116
117
118
119
# File 'lib/hair_trigger/builder.rb', line 116

def timing(timing)
  raise DeclarationError, "invalid timing" unless [:before, :after].include?(timing)
  options[:timing] = timing.to_s.upcase
end

#to_ruby(indent = '', always_generated = true) ⇒ Object



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
# File 'lib/hair_trigger/builder.rb', line 249

def to_ruby(indent = '', always_generated = true)
  prepare!
  if options[:drop]
    str = "#{indent}drop_trigger(#{prepared_name.inspect}, #{options[:table].inspect}"
    str << ", :generated => true" if always_generated || options[:generated]
    str << ")"
  else
    if @trigger_group
      str = "t." + chained_calls_to_ruby + " do\n"
      str << actions_to_ruby("#{indent}  ") + "\n"
      str << "#{indent}end"
    else
      str = "#{indent}create_trigger(#{prepared_name.inspect}"
      str << ", :generated => true" if always_generated || options[:generated]
      str << ", :compatibility => #{@compatibility}"
      str << ").\n#{indent}    " + chained_calls_to_ruby(".\n#{indent}    ")
      if @triggers
        str << " do |t|\n"
        str << "#{indent}  " + @triggers.map{ |t| t.to_ruby("#{indent}  ") }.join("\n\n#{indent}  ") + "\n"
      else
        str << " do\n"
        str << actions_to_ruby("#{indent}  ") + "\n"
      end
      str << "#{indent}end"
    end
  end
end

#validate!(direction = :down) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/hair_trigger/builder.rb', line 205

def validate!(direction = :down)
  @errors.each do |(error, *adapters)|
    raise GenerationError, error if adapters.include?(adapter_name)
    $stderr.puts "WARNING: " + error if self.class.show_warnings
  end
  @warnings.each do |(error, *adapters)|
    $stderr.puts "WARNING: " + error if adapters.include?(adapter_name) && self.class.show_warnings
  end

  if direction != :up
    @triggers.each{ |t| t.validate!(:down) } if @triggers
  end
  if direction != :down
    @trigger_group.validate!(:up) if @trigger_group
  end
end

#warningsObject



304
305
306
# File 'lib/hair_trigger/builder.rb', line 304

def warnings
  (@triggers || []).map(&:warnings).inject(@warnings, &:+)
end

#where(where) ⇒ Object



72
73
74
# File 'lib/hair_trigger/builder.rb', line 72

def where(where)
  options[:where] = where
end