Class: HairTrigger::Builder
- Inherits:
-
Object
- Object
- HairTrigger::Builder
- Defined in:
- lib/hair_trigger/builder.rb
Defined Under Namespace
Classes: DeclarationError, GenerationError
Class Attribute Summary collapse
Instance Attribute Summary collapse
-
#options ⇒ Object
Returns the value of attribute options.
-
#prepared_actions ⇒ Object
readonly
after delayed interpolation.
-
#prepared_where ⇒ Object
readonly
after delayed interpolation.
-
#triggers ⇒ Object
readonly
nil unless this is a trigger group.
Class Method Summary collapse
Instance Method Summary collapse
- #<=>(other) ⇒ Object
- #==(other) ⇒ Object
- #after(*events) ⇒ Object
-
#all ⇒ Object
noop, just a way you can pass a block within a trigger group.
- #all_names ⇒ Object
- #all_triggers(include_self = true) ⇒ Object
- #before(*events) ⇒ Object
- #change_clause(column) ⇒ Object
- #components ⇒ Object
- #create_grouped_trigger? ⇒ Boolean
- #declare(declarations) ⇒ Object
- #drop_triggers ⇒ Object
- #eql?(other) ⇒ Boolean
- #errors ⇒ Object
- #events(*events) ⇒ Object
- #for_each(for_each) ⇒ Object
- #generate(validate = true) ⇒ Object
- #hash ⇒ Object
-
#initialize(name = nil, options = {}) ⇒ Builder
constructor
A new instance of Builder.
- #initialize_copy(other) ⇒ Object
- #name(name) ⇒ Object
- #new_as(table) ⇒ Object
- #nowrap(flag = true) ⇒ Object
- #of(*columns) ⇒ Object
- #old_as(table) ⇒ Object
- #on(table) ⇒ Object
- #prepare! ⇒ Object
- #prepare_where! ⇒ Object
- #prepared_name ⇒ Object
- #raw_actions ⇒ Object
- #security(user) ⇒ Object
- #timing(timing) ⇒ Object
- #to_ruby(indent = '', always_generated = true) ⇒ Object
- #validate!(direction = :down) ⇒ Object
- #warnings ⇒ Object
- #where(where) ⇒ Object
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, = {}) @adapter = [:adapter] @compatibility = .delete(:compatibility) || self.class.compatibility @options = {} @chained_calls = [] @errors = [] @warnings = [] set_name(name) if name {:timing => :after, :for_each => :row}.update().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_compatibility ⇒ Object
568 569 570 |
# File 'lib/hair_trigger/builder.rb', line 568 def base_compatibility @base_compatibility ||= 0 end |
.show_warnings ⇒ Object
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_spacing ⇒ Object
559 560 561 |
# File 'lib/hair_trigger/builder.rb', line 559 def tab_spacing @tab_spacing ||= 4 end |
Instance Attribute Details
#options ⇒ Object
Returns the value of attribute options.
8 9 10 |
# File 'lib/hair_trigger/builder.rb', line 8 def @options end |
#prepared_actions ⇒ Object (readonly)
after delayed interpolation
10 11 12 |
# File 'lib/hair_trigger/builder.rb', line 10 def prepared_actions @prepared_actions end |
#prepared_where ⇒ Object (readonly)
after delayed interpolation
10 11 12 |
# File 'lib/hair_trigger/builder.rb', line 10 def prepared_where @prepared_where end |
#triggers ⇒ Object (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 |
.compatibility ⇒ Object
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 |
#all ⇒ Object
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_names ⇒ Object
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 |
#components ⇒ Object
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
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) [:declarations] = declarations end |
#drop_triggers ⇒ Object
42 43 44 |
# File 'lib/hair_trigger/builder.rb', line 42 def drop_triggers all_names.map{ |name| self.class.new(name, {:table => [:table], :drop => true}) } end |
#eql?(other) ⇒ Boolean
287 288 289 |
# File 'lib/hair_trigger/builder.rb', line 287 def eql?(other) other.is_a?(HairTrigger::Builder) && self == other end |
#errors ⇒ Object
300 301 302 |
# File 'lib/hair_trigger/builder.rb', line 300 def errors (@triggers || []).map(&:errors).inject(@errors, &:+) end |
#events(*events) ⇒ Object
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) [:events] = events.map{ |e| e.to_s.upcase } end |
#for_each(for_each) ⇒ Object
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) [:for_each] = for_each.to_s.upcase end |
#generate(validate = true) ⇒ Object
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 [:table] if [: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 ![:events] || [:events].empty? raise GenerationError, "need to specify the timing (:before/:after)" unless [: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 |
#hash ⇒ Object
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 [:name] = name.to_s end |
#new_as(table) ⇒ Object
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? [:referencing] ||= {} [:referencing][:new] = table end |
#nowrap(flag = true) ⇒ Object
76 77 78 |
# File 'lib/hair_trigger/builder.rb', line 76 def nowrap(flag = true) [:nowrap] = flag end |
#of(*columns) ⇒ Object
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? [:of] = columns end |
#old_as(table) ⇒ Object
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? [:referencing] ||= {} [:referencing][:old] = table end |
#on(table) ⇒ Object
51 52 53 54 |
# File 'lib/hair_trigger/builder.rb', line 51 def on(table) raise DeclarationError, "table has already been specified" if [:table] [: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 = [:where] = interpolate([:where]) if [:where] parts << [:of].map{ |col| change_clause(col) }.join(" OR ") if [:of] && !supports_of? if parts.present? parts.map!{ |part| "(" + part + ")" } if parts.size > 1 @prepared_where = parts.join(" AND ") end end |
#prepared_name ⇒ Object
134 135 136 |
# File 'lib/hair_trigger/builder.rb', line 134 def prepared_name @prepared_name ||= [:name] ||= infer_name end |
#raw_actions ⇒ Object
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 [:security] = user end |
#timing(timing) ⇒ Object
116 117 118 119 |
# File 'lib/hair_trigger/builder.rb', line 116 def timing(timing) raise DeclarationError, "invalid timing" unless [:before, :after].include?(timing) [: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 [:drop] str = "#{indent}drop_trigger(#{prepared_name.inspect}, #{[:table].inspect}" str << ", :generated => true" if always_generated || [: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 || [: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 |
#warnings ⇒ Object
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) [:where] = where end |