hairtrigger 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +4 -0
- data/README.rdoc +28 -9
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/lib/hair_trigger.rb +150 -14
- data/lib/hair_trigger/adapter.rb +52 -0
- data/lib/hair_trigger/builder.rb +11 -9
- data/lib/hair_trigger/migration.rb +3 -2
- data/lib/hair_trigger/schema.rb +17 -0
- data/lib/hair_trigger/schema_dumper.rb +87 -0
- data/lib/tasks/hair_trigger.rake +14 -68
- data/spec/builder_spec.rb +15 -1
- data/spec/migrations/20110331212003_initial_tables.rb +17 -0
- data/spec/migrations/20110331212631_user_trigger.rb +18 -0
- data/spec/models/group.rb +3 -0
- data/spec/models/user.rb +6 -0
- data/spec/schema_dumper_spec.rb +99 -0
- metadata +67 -9
- data/spec/spec_helper.rb +0 -12
data/Gemfile
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
source "http://rubygems.org"
|
2
|
+
|
2
3
|
gem "activerecord", ">=2.3.0", "<3.0"
|
3
4
|
group :development do
|
4
5
|
gem "rspec", "~> 2.3.0"
|
5
6
|
gem "bundler", "~> 1.0.0"
|
6
7
|
gem "jeweler", "~> 1.5.2"
|
7
8
|
gem "rcov", ">= 0"
|
9
|
+
gem 'mysql', '>= 2.8.1'
|
10
|
+
gem 'pg', '>= 0.10.1'
|
11
|
+
gem 'sqlite3-ruby', '>= 1.3.2'
|
8
12
|
end
|
data/README.rdoc
CHANGED
@@ -4,7 +4,7 @@ HairTrigger lets you create and manage database triggers in a concise,
|
|
4
4
|
db-agnostic, Rails-y way. You declare triggers right in your models in Ruby,
|
5
5
|
and a simple rake task does all the dirty work for you.
|
6
6
|
|
7
|
-
==
|
7
|
+
== Installation
|
8
8
|
|
9
9
|
=== Step 1.
|
10
10
|
|
@@ -55,7 +55,8 @@ contain the ":generated => true" option, indicating that they were created
|
|
55
55
|
from the model definition. This is important, as the rake task will also
|
56
56
|
generate appropriate drop/create statements for any model triggers that get
|
57
57
|
removed or updated. It does this by diffing the current model trigger
|
58
|
-
declarations and any auto-generated triggers
|
58
|
+
declarations and any auto-generated triggers in schema.rb (and subsequent
|
59
|
+
migrations).
|
59
60
|
|
60
61
|
=== Manual Migrations
|
61
62
|
|
@@ -81,7 +82,7 @@ Optional, inferred from other calls.
|
|
81
82
|
Ignored in models, required in migrations.
|
82
83
|
|
83
84
|
==== for_each(item)
|
84
|
-
Defaults to :row, PostgreSQL
|
85
|
+
Defaults to :row, PostgreSQL allows :statement.
|
85
86
|
|
86
87
|
==== before(*events)
|
87
88
|
Shorthand for timing(:before).events(*events).
|
@@ -128,10 +129,28 @@ For MySQL, this will just create a single trigger with conditional logic
|
|
128
129
|
distinct triggers. This same notation is also used within trigger migrations.
|
129
130
|
MySQL does not currently support nested trigger groups.
|
130
131
|
|
132
|
+
== rake db:schema:dump
|
133
|
+
|
134
|
+
HairTrigger hooks into rake db:schema:dump (and rake tasks that call it) to
|
135
|
+
make it trigger-aware. A newly generated schema.rb will contain:
|
136
|
+
|
137
|
+
* create_trigger statements for any database triggers that exactly match a
|
138
|
+
create_trigger statement in an applied migration or in the previous
|
139
|
+
schema.rb file. this includes both generated and manual create_trigger
|
140
|
+
calls.
|
141
|
+
* adapter-specific execute('CREATE TRIGGER..') statements for any unmatched
|
142
|
+
database triggers.
|
143
|
+
|
144
|
+
As long as you don't delete old migrations and schema.rb prior to running
|
145
|
+
rake db:schema:dump, the result should be what you expect (and portable).
|
146
|
+
If you have deleted all trigger migrations, you can regenerate a new
|
147
|
+
baseline for model triggers via rake db:generate_trigger_migration.
|
148
|
+
|
131
149
|
== Testing
|
132
150
|
|
133
151
|
To stay on top of things, it's strongly recommended that you add a test or
|
134
|
-
spec to ensure your migrations match your models. This is as simple
|
152
|
+
spec to ensure your migrations/schema.rb match your models. This is as simple
|
153
|
+
as:
|
135
154
|
|
136
155
|
assert HairTrigger::migrations_current?
|
137
156
|
|
@@ -166,10 +185,8 @@ e.g.
|
|
166
185
|
HairTrigger does not validate your SQL, so be sure to test it in all databases
|
167
186
|
you want to support.
|
168
187
|
|
169
|
-
== Gotchas
|
188
|
+
== Gotchas
|
170
189
|
|
171
|
-
* HairTrigger does not check config.active_record.timestamped_migrations, it
|
172
|
-
always assumes it is true. As a workaround, you can rename the migration.
|
173
190
|
* As is the case with ActiveRecord::Base.update_all or any direct SQL you do,
|
174
191
|
be careful to reload updated objects from the database. For example, the
|
175
192
|
following code will display the wrong count since we aren't reloading the
|
@@ -188,11 +205,13 @@ you want to support.
|
|
188
205
|
|
189
206
|
* Rails 2.3.x
|
190
207
|
* Postgres 8.0+
|
191
|
-
* MySQL 5.0+
|
192
|
-
* SQLite 3.
|
208
|
+
* MySQL 5.0.10+
|
209
|
+
* SQLite 3.3.8+
|
193
210
|
|
194
211
|
== Version History
|
195
212
|
|
213
|
+
* 0.1.4 Compatibility tracking, fixed Postgres return bug, ensure last action
|
214
|
+
has a semicolon
|
196
215
|
* 0.1.3 Better error handling, Postgres 8.x support, updated docs
|
197
216
|
* 0.1.2 Fixed Builder#security, updated docs
|
198
217
|
* 0.1.1 Fixed bug in HairTrigger.migrations_current?, fixed up Gemfile
|
data/Rakefile
CHANGED
@@ -19,7 +19,7 @@ Jeweler::Tasks.new do |gem|
|
|
19
19
|
gem.description = %Q{allows you to declare database triggers in ruby in your models, and then generate appropriate migrations as they change}
|
20
20
|
gem.email = "jenseng@gmail.com"
|
21
21
|
gem.authors = ["Jon Jensen"]
|
22
|
-
gem.
|
22
|
+
gem.add_dependency "activerecord", ">=2.3.0", "<3.0"
|
23
23
|
gem.add_development_dependency "rspec", "~> 2.3.0"
|
24
24
|
end
|
25
25
|
Jeweler::RubygemsDotOrgTasks.new
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.5
|
data/lib/hair_trigger.rb
CHANGED
@@ -1,13 +1,22 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'hair_trigger/base'
|
3
|
+
require 'hair_trigger/builder'
|
4
|
+
require 'hair_trigger/migration'
|
5
|
+
require 'hair_trigger/adapter'
|
6
|
+
require 'hair_trigger/schema_dumper'
|
7
|
+
require 'hair_trigger/schema'
|
8
|
+
|
1
9
|
module HairTrigger
|
2
10
|
def self.current_triggers
|
3
11
|
# see what the models say there should be
|
4
12
|
canonical_triggers = []
|
5
|
-
Dir['
|
13
|
+
Dir[model_path + '/*rb'].each do |model|
|
6
14
|
class_name = model.sub(/\A.*\/(.*?)\.rb\z/, '\1').camelize
|
7
15
|
begin
|
16
|
+
require model
|
8
17
|
klass = Kernel.const_get(class_name)
|
9
|
-
rescue
|
10
|
-
raise "unable to load #{class_name} and its trigger(s)"
|
18
|
+
rescue LoadError
|
19
|
+
raise "unable to load #{class_name} and its trigger(s)" if File.read(model) =~ /^\s*trigger[\.\(]/
|
11
20
|
next
|
12
21
|
end
|
13
22
|
canonical_triggers += klass.triggers if klass < ActiveRecord::Base && klass.triggers
|
@@ -15,36 +24,163 @@ module HairTrigger
|
|
15
24
|
canonical_triggers.each(&:prepare!) # interpolates any vars so we match the migrations
|
16
25
|
end
|
17
26
|
|
18
|
-
def self.current_migrations
|
27
|
+
def self.current_migrations(options = {})
|
28
|
+
if options[:in_rake_task]
|
29
|
+
options[:include_manual_triggers] = true
|
30
|
+
options[:schema_rb_first] = true
|
31
|
+
options[:skip_pending_migrations] = true
|
32
|
+
end
|
33
|
+
|
34
|
+
prev_verbose = ActiveRecord::Migration.verbose
|
19
35
|
ActiveRecord::Migration.verbose = false
|
20
36
|
ActiveRecord::Migration.extract_trigger_builders = true
|
37
|
+
ActiveRecord::Migration.extract_all_triggers = options[:include_manual_triggers] || false
|
21
38
|
|
22
|
-
#
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
39
|
+
# if we're in a db:schema:dump task (explict or kicked off by db:migrate),
|
40
|
+
# we evaluate the previous schema.rb (if it exists), and then all applied
|
41
|
+
# migrations in order (even ones older than schema.rb). this ensures we
|
42
|
+
# handle db:migrate:down scenarios correctly
|
43
|
+
#
|
44
|
+
# if we're not in such a rake task (i.e. we just want to know what
|
45
|
+
# triggers are defined, whether or not they are applied in the db), we
|
46
|
+
# evaluate all migrations along with schema.rb, ordered by version
|
47
|
+
migrator = ActiveRecord::Migrator.new(:up, migration_path)
|
48
|
+
migrated = migrator.migrated rescue []
|
49
|
+
migrations = migrator.migrations.select{ |migration|
|
50
|
+
(options[:skip_pending_migrations] ? migrated.include?(migration.version) : true)
|
51
|
+
}.each{ |migration|
|
28
52
|
migration.migrate(:up)
|
53
|
+
}
|
54
|
+
|
55
|
+
if options.has_key?(:previous_schema)
|
56
|
+
eval(options[:previous_schema]) if options[:previous_schema]
|
57
|
+
elsif File.exist?(schema_rb_path)
|
58
|
+
load(schema_rb_path)
|
59
|
+
end
|
60
|
+
if ActiveRecord::Schema.info && ActiveRecord::Schema.trigger_builders
|
61
|
+
migrations.unshift OpenStruct.new({:version => ActiveRecord::Schema.info[:version], :trigger_builders => ActiveRecord::Schema.trigger_builders})
|
62
|
+
end
|
63
|
+
migrations = migrations.sort_by(&:version) unless options[:schema_rb_first]
|
64
|
+
|
65
|
+
all_builders = []
|
66
|
+
migrations.each do |migration|
|
67
|
+
next unless migration.trigger_builders
|
29
68
|
migration.trigger_builders.each do |new_trigger|
|
30
69
|
# if there is already a trigger with this name, delete it since we are
|
31
70
|
# either dropping it or replacing it
|
32
|
-
|
33
|
-
|
71
|
+
new_trigger.prepare!
|
72
|
+
all_builders.delete_if{ |(n, t)| t.prepared_name == new_trigger.prepared_name }
|
73
|
+
all_builders << [migration.name, new_trigger] unless new_trigger.options[:drop]
|
34
74
|
end
|
35
75
|
end
|
36
|
-
|
76
|
+
|
77
|
+
all_builders
|
78
|
+
|
37
79
|
ensure
|
38
|
-
ActiveRecord::Migration.verbose =
|
80
|
+
ActiveRecord::Migration.verbose = prev_verbose
|
39
81
|
ActiveRecord::Migration.extract_trigger_builders = false
|
82
|
+
ActiveRecord::Migration.extract_all_triggers = false
|
40
83
|
end
|
41
84
|
|
42
85
|
def self.migrations_current?
|
43
86
|
current_migrations.map(&:last).sort.eql? current_triggers.sort
|
44
87
|
end
|
88
|
+
|
89
|
+
def self.generate_migration(silent = false)
|
90
|
+
begin
|
91
|
+
canonical_triggers = current_triggers
|
92
|
+
rescue
|
93
|
+
$stderr.puts $!
|
94
|
+
exit 1
|
95
|
+
end
|
96
|
+
|
97
|
+
migrations = current_migrations
|
98
|
+
migration_names = migrations.map(&:first)
|
99
|
+
existing_triggers = migrations.map(&:last)
|
100
|
+
|
101
|
+
up_drop_triggers = []
|
102
|
+
up_create_triggers = []
|
103
|
+
down_drop_triggers = []
|
104
|
+
down_create_triggers = []
|
105
|
+
|
106
|
+
existing_triggers.each do |existing|
|
107
|
+
unless canonical_triggers.any?{ |t| t.prepared_name == existing.prepared_name }
|
108
|
+
up_drop_triggers += existing.drop_triggers
|
109
|
+
down_create_triggers << existing
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
(canonical_triggers - existing_triggers).each do |new_trigger|
|
114
|
+
up_create_triggers << new_trigger
|
115
|
+
down_drop_triggers += new_trigger.drop_triggers
|
116
|
+
if existing = existing_triggers.detect{ |t| t.prepared_name == new_trigger.prepared_name }
|
117
|
+
# it's not sufficient to rely on the new trigger to replace the old
|
118
|
+
# one, since we could be dealing with trigger groups and the name
|
119
|
+
# alone isn't sufficient to know which component triggers to remove
|
120
|
+
up_drop_triggers += existing.drop_triggers
|
121
|
+
down_create_triggers << existing
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
unless up_drop_triggers.empty? && up_create_triggers.empty?
|
126
|
+
migration_base_name = if up_create_triggers.size > 0
|
127
|
+
("create trigger#{up_create_triggers.size > 1 ? 's' : ''} " +
|
128
|
+
up_create_triggers.map{ |t| [t.options[:table], t.options[:events].join(" ")].join(" ") }.join(" and ")
|
129
|
+
).downcase.gsub(/[^a-z0-9_]/, '_').gsub(/_+/, '_').camelize
|
130
|
+
else
|
131
|
+
("drop trigger#{up_drop_triggers.size > 1 ? 's' : ''} " +
|
132
|
+
up_drop_triggers.map{ |t| t.options[:table] }.join(" and ")
|
133
|
+
).downcase.gsub(/[^a-z0-9_]/, '_').gsub(/_+/, '_').camelize
|
134
|
+
end
|
135
|
+
|
136
|
+
name_version = nil
|
137
|
+
while migration_names.include?("#{migration_base_name}#{name_version}")
|
138
|
+
name_version = name_version.to_i + 1
|
139
|
+
end
|
140
|
+
migration_name = "#{migration_base_name}#{name_version}"
|
141
|
+
migration_version = ActiveRecord::Base.timestamped_migrations ?
|
142
|
+
Time.now.getutc.strftime("%Y%m%d%H%M%S") :
|
143
|
+
Dir.glob(migration_path + '/*rb').map{ |f| f.gsub(/.*\/(\d+)_.*/, '\1').to_i}.inject(0){ |curr, i| i > curr ? i : curr }
|
144
|
+
file_name = migration_path + '/' + migration_version + "_" + migration_name.underscore + ".rb"
|
145
|
+
File.open(file_name, "w"){ |f| f.write <<-MIGRATION }
|
146
|
+
# This migration was auto-generated via `rake db:generate_trigger_migration'.
|
147
|
+
# While you can edit this file, any changes you make to the definitions here
|
148
|
+
# will be undone by the next auto-generated trigger migration.
|
149
|
+
|
150
|
+
class #{migration_name} < ActiveRecord::Migration
|
151
|
+
def self.up
|
152
|
+
#{(up_drop_triggers + up_create_triggers).map{ |t| t.to_ruby(' ') }.join("\n\n").lstrip}
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.down
|
156
|
+
#{(down_drop_triggers + down_create_triggers).map{ |t| t.to_ruby(' ') }.join("\n\n").lstrip}
|
157
|
+
end
|
158
|
+
end
|
159
|
+
MIGRATION
|
160
|
+
file_name
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class << self
|
165
|
+
attr_writer :model_path, :schema_rb_path, :migration_path
|
166
|
+
|
167
|
+
def model_path
|
168
|
+
@model_path ||= 'app/models'
|
169
|
+
end
|
170
|
+
|
171
|
+
def schema_rb_path
|
172
|
+
@schema_rb_path ||= 'db/schema.rb'
|
173
|
+
end
|
174
|
+
|
175
|
+
def migration_path
|
176
|
+
@migration_path ||= 'db/migrate'
|
177
|
+
end
|
178
|
+
end
|
45
179
|
end
|
46
180
|
|
47
181
|
ActiveRecord::Base.send :extend, HairTrigger::Base
|
48
182
|
ActiveRecord::Migration.send :extend, HairTrigger::Migration
|
49
183
|
ActiveRecord::MigrationProxy.send :delegate, :trigger_builders, :to=>:migration
|
50
184
|
ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval { include HairTrigger::Adapter }
|
185
|
+
ActiveRecord::SchemaDumper.class_eval { include HairTrigger::SchemaDumper }
|
186
|
+
ActiveRecord::Schema.send :extend, HairTrigger::Schema
|
data/lib/hair_trigger/adapter.rb
CHANGED
@@ -11,5 +11,57 @@ module HairTrigger
|
|
11
11
|
def drop_trigger(name, table, options = {})
|
12
12
|
::HairTrigger::Builder.new(name, options.merge(:execute => true, :drop => true, :table => table)){}
|
13
13
|
end
|
14
|
+
|
15
|
+
def triggers(options = {})
|
16
|
+
triggers = {}
|
17
|
+
name_clause = options[:only] ? "IN ('" + options[:only].join("', '") + "')" : nil
|
18
|
+
case self.adapter_name.downcase.to_sym
|
19
|
+
when :sqlite
|
20
|
+
select_rows("SELECT name, sql FROM sqlite_master WHERE type = 'trigger' #{name_clause ? " AND name " + name_clause : ""}").each do |(name, definition)|
|
21
|
+
triggers[name] = definition + ";\n"
|
22
|
+
end
|
23
|
+
when :mysql
|
24
|
+
select_rows("SHOW TRIGGERS").each do |(name, event, table, actions, timing, created, sql_mode, definer)|
|
25
|
+
next if options[:only] && !options[:only].include?(name)
|
26
|
+
triggers[name] = <<-SQL
|
27
|
+
CREATE #{definer != "#{@config[:username]}@#{@config[:host]}" ? "DEFINER = #{definer} " : ""}TRIGGER #{name} #{timing} #{event} ON #{table}
|
28
|
+
FOR EACH ROW
|
29
|
+
#{actions}
|
30
|
+
SQL
|
31
|
+
end
|
32
|
+
when :postgresql
|
33
|
+
function_conditions = "(SELECT typname FROM pg_type WHERE oid = prorettype) = 'trigger'"
|
34
|
+
function_conditions << <<-SQL unless options[:simple_check]
|
35
|
+
AND oid IN (
|
36
|
+
SELECT tgfoid
|
37
|
+
FROM pg_trigger
|
38
|
+
WHERE NOT tgisinternal AND tgconstrrelid = 0 AND tgrelid IN (
|
39
|
+
SELECT oid FROM pg_class WHERE relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')
|
40
|
+
)
|
41
|
+
)
|
42
|
+
SQL
|
43
|
+
function_conditions =
|
44
|
+
sql = <<-SQL
|
45
|
+
SELECT tgname::varchar, pg_get_triggerdef(oid, true)
|
46
|
+
FROM pg_trigger
|
47
|
+
WHERE NOT tgisinternal AND tgconstrrelid = 0 AND tgrelid IN (
|
48
|
+
SELECT oid FROM pg_class WHERE relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')
|
49
|
+
)
|
50
|
+
|
51
|
+
#{name_clause ? " AND tgname::varchar " + name_clause : ""}
|
52
|
+
UNION
|
53
|
+
SELECT proname || '()', pg_get_functiondef(oid)
|
54
|
+
FROM pg_proc
|
55
|
+
WHERE #{function_conditions}
|
56
|
+
#{name_clause ? " AND (proname || '()')::varchar " + name_clause : ""}
|
57
|
+
SQL
|
58
|
+
select_rows(sql).each do |(name, definition)|
|
59
|
+
triggers[name] = definition
|
60
|
+
end
|
61
|
+
else
|
62
|
+
raise "don't know how to retrieve #{adapter_name} triggers yet"
|
63
|
+
end
|
64
|
+
triggers
|
65
|
+
end
|
14
66
|
end
|
15
67
|
end
|
data/lib/hair_trigger/builder.rb
CHANGED
@@ -50,7 +50,7 @@ module HairTrigger
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def for_each(for_each)
|
53
|
-
@errors << ["sqlite
|
53
|
+
@errors << ["sqlite and mysql don't support FOR EACH STATEMENT triggers", :sqlite, :mysql] if for_each == :statement
|
54
54
|
raise DeclarationError, "invalid for_each" unless [:row, :statement].include?(for_each)
|
55
55
|
options[:for_each] = for_each.to_s.upcase
|
56
56
|
end
|
@@ -170,8 +170,8 @@ module HairTrigger
|
|
170
170
|
raise GenerationError, "need to specify the event(s) (:insert, :update, :delete)" if !options[:events] || options[:events].empty?
|
171
171
|
raise GenerationError, "need to specify the timing (:before/:after)" unless options[:timing]
|
172
172
|
|
173
|
-
|
174
|
-
|
173
|
+
[generate_drop_trigger] +
|
174
|
+
[case adapter_name
|
175
175
|
when :sqlite
|
176
176
|
generate_trigger_sqlite
|
177
177
|
when :mysql
|
@@ -180,23 +180,23 @@ module HairTrigger
|
|
180
180
|
generate_trigger_postgresql
|
181
181
|
else
|
182
182
|
raise GenerationError, "don't know how to build #{adapter_name} triggers yet"
|
183
|
-
end
|
184
|
-
ret
|
183
|
+
end].flatten
|
185
184
|
end
|
186
185
|
end
|
187
186
|
|
188
|
-
def to_ruby(indent = '')
|
187
|
+
def to_ruby(indent = '', always_generated = true)
|
189
188
|
prepare!
|
190
189
|
if options[:drop]
|
191
|
-
"#{indent}drop_trigger(#{prepared_name.inspect}, #{options[:table].inspect}
|
190
|
+
"#{indent}drop_trigger(#{prepared_name.inspect}, #{options[:table].inspect})"
|
192
191
|
else
|
193
192
|
if @trigger_group
|
194
193
|
str = "t." + chained_calls_to_ruby + " do\n"
|
195
194
|
str << actions_to_ruby("#{indent} ") + "\n"
|
196
195
|
str << "#{indent}end"
|
197
196
|
else
|
198
|
-
str = "#{indent}create_trigger(#{prepared_name.inspect}
|
199
|
-
"#{
|
197
|
+
str = "#{indent}create_trigger(#{prepared_name.inspect}"
|
198
|
+
str << ", :generated => true, :compatibility => #{@compatibility}" if always_generated || options[:generated]
|
199
|
+
str << ").\n#{indent} " + chained_calls_to_ruby(".\n#{indent} ")
|
200
200
|
if @triggers
|
201
201
|
str << " do |t|\n"
|
202
202
|
str << "#{indent} " + @triggers.map{ |t| t.to_ruby("#{indent} ") }.join("\n\n#{indent} ") + "\n"
|
@@ -337,6 +337,8 @@ BEGIN
|
|
337
337
|
sql << <<-SQL
|
338
338
|
END;
|
339
339
|
$$ LANGUAGE plpgsql#{security ? " SECURITY #{security.to_s.upcase}" : ""};
|
340
|
+
SQL
|
341
|
+
[sql, <<-SQL]
|
340
342
|
CREATE TRIGGER #{prepared_name} #{options[:timing]} #{options[:events].join(" OR ")} ON #{options[:table]}
|
341
343
|
FOR EACH #{options[:for_each]}#{prepared_where && db_version >= 90000 ? " WHEN (" + prepared_where + ')': ''} EXECUTE PROCEDURE #{prepared_name}();
|
342
344
|
SQL
|
@@ -4,13 +4,13 @@ module HairTrigger
|
|
4
4
|
|
5
5
|
def method_missing_with_trigger_building(method, *arguments, &block)
|
6
6
|
if extract_trigger_builders
|
7
|
-
if method.to_sym == :create_trigger && arguments[1].delete(:generated)
|
7
|
+
if method.to_sym == :create_trigger && (extract_all_triggers || arguments[1].delete(:generated))
|
8
8
|
arguments.unshift(nil) if arguments.first.is_a?(Hash)
|
9
9
|
arguments[1][:compatibility] ||= 0
|
10
10
|
trigger = ::HairTrigger::Builder.new(*arguments)
|
11
11
|
(@trigger_builders ||= []) << trigger
|
12
12
|
trigger
|
13
|
-
elsif method.to_sym == :drop_trigger && arguments[2] && arguments[2].delete(:generated)
|
13
|
+
elsif method.to_sym == :drop_trigger && (extract_all_triggers || arguments[2] && arguments[2].delete(:generated))
|
14
14
|
trigger = ::HairTrigger::Builder.new(arguments[0], {:table => arguments[1], :drop => true})
|
15
15
|
(@trigger_builders ||= []) << trigger
|
16
16
|
trigger
|
@@ -28,6 +28,7 @@ module HairTrigger
|
|
28
28
|
class << self
|
29
29
|
alias_method_chain :method_missing, :trigger_building
|
30
30
|
cattr_accessor :extract_trigger_builders
|
31
|
+
cattr_accessor :extract_all_triggers
|
31
32
|
end
|
32
33
|
end
|
33
34
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module HairTrigger
|
2
|
+
module Schema
|
3
|
+
attr_reader :info
|
4
|
+
def define_with_no_save(info={}, &block)
|
5
|
+
instance_eval(&block)
|
6
|
+
@info = info
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.extended(base)
|
10
|
+
base.instance_eval do
|
11
|
+
class << self
|
12
|
+
alias_method_chain :define, :no_save
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module HairTrigger
|
2
|
+
module SchemaDumper
|
3
|
+
def trailer_with_triggers(stream)
|
4
|
+
triggers(stream)
|
5
|
+
trailer_without_triggers(stream)
|
6
|
+
end
|
7
|
+
|
8
|
+
def triggers(stream)
|
9
|
+
@connection = ActiveRecord::Base.connection
|
10
|
+
@adapter_name = @connection.adapter_name.downcase.to_sym
|
11
|
+
|
12
|
+
db_triggers = @connection.triggers; nil
|
13
|
+
db_trigger_warnings = {}
|
14
|
+
|
15
|
+
migration_triggers = HairTrigger.current_migrations(:in_rake_task => true, :previous_schema => self.class.previous_schema).map do |(name, builder)|
|
16
|
+
definitions = []
|
17
|
+
builder.generate.each do |statement|
|
18
|
+
if statement =~ /\ACREATE(.*TRIGGER| FUNCTION) ([^ \n]+)/
|
19
|
+
definitions << [$2, statement, $1 == ' FUNCTION' ? :function : :trigger]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
{:builder => builder, :definitions => definitions}
|
23
|
+
end
|
24
|
+
|
25
|
+
migration_triggers.each do |migration|
|
26
|
+
next unless migration[:definitions].all? do |(name, definition, type)|
|
27
|
+
db_triggers[name] && (db_trigger_warnings[name] = true) && db_triggers[name] == normalize_trigger(name, definition, type)
|
28
|
+
end
|
29
|
+
migration[:definitions].each do |(name, trigger, type)|
|
30
|
+
db_triggers.delete(name)
|
31
|
+
db_trigger_warnings.delete(name)
|
32
|
+
end
|
33
|
+
|
34
|
+
stream.print migration[:builder].to_ruby(' ', false) + "\n\n"
|
35
|
+
end
|
36
|
+
|
37
|
+
db_triggers.to_a.sort_by{ |t| (t.first + 'a').sub(/\(/, '_') }.each do |(name, definition)|
|
38
|
+
if db_trigger_warnings[name]
|
39
|
+
stream.puts " # WARNING: generating adapter-specific definition for #{name} due to a mismatch."
|
40
|
+
stream.puts " # either there's a bug in hairtrigger or you've messed up your migrations and/or db :-/"
|
41
|
+
else
|
42
|
+
stream.puts " # no candidate create_trigger statement could be found, creating an adapter-specific one"
|
43
|
+
end
|
44
|
+
if definition =~ /\n/
|
45
|
+
stream.print " execute(<<-TRIGGERSQL)\n#{definition.rstrip}\n TRIGGERSQL\n\n"
|
46
|
+
else
|
47
|
+
stream.print " execute(#{definition.inspect})\n\n"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def normalize_trigger(name, definition, type)
|
53
|
+
@connection = ActiveRecord::Base.connection
|
54
|
+
@adapter_name = @connection.adapter_name.downcase.to_sym
|
55
|
+
|
56
|
+
return definition unless @adapter_name == :postgresql
|
57
|
+
begin
|
58
|
+
# because postgres does not preserve the original CREATE TRIGGER/
|
59
|
+
# FUNCTION statements, its decompiled reconstruction will not match
|
60
|
+
# ours. we work around it by creating our generated trigger/function,
|
61
|
+
# asking postgres for its definition, and then rolling back.
|
62
|
+
begin
|
63
|
+
@connection.transaction do
|
64
|
+
chars = ('a'..'z').to_a + ('0'..'9').to_a + ['_']
|
65
|
+
test_name = '_hair_trigger_test_' + (0..43).map{ chars[(rand * chars.size).to_i] }.join
|
66
|
+
test_name << (type == :function ? '()' : '')
|
67
|
+
@connection.execute(definition.sub(name, test_name))
|
68
|
+
definition = @connection.triggers(:only => [test_name], :simple_check => true).values.first
|
69
|
+
definition.sub!(test_name, name)
|
70
|
+
raise
|
71
|
+
end
|
72
|
+
rescue
|
73
|
+
end
|
74
|
+
end
|
75
|
+
definition
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.included(base)
|
79
|
+
base.class_eval do
|
80
|
+
alias_method_chain :trailer, :triggers
|
81
|
+
class << self
|
82
|
+
attr_accessor :previous_schema
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/tasks/hair_trigger.rake
CHANGED
@@ -1,77 +1,23 @@
|
|
1
1
|
namespace :db do
|
2
2
|
desc "Creates a database migration for any newly created/modified/deleted triggers in the models"
|
3
3
|
task :generate_trigger_migration => :environment do
|
4
|
-
|
5
|
-
begin
|
6
|
-
canonical_triggers = HairTrigger::current_triggers
|
7
|
-
rescue
|
8
|
-
$stderr.puts $!
|
9
|
-
exit 1
|
10
|
-
end
|
11
|
-
|
12
|
-
migrations = HairTrigger::current_migrations
|
13
|
-
migration_names = migrations.map(&:first)
|
14
|
-
existing_triggers = migrations.map(&:last)
|
15
|
-
|
16
|
-
up_drop_triggers = []
|
17
|
-
up_create_triggers = []
|
18
|
-
down_drop_triggers = []
|
19
|
-
down_create_triggers = []
|
20
|
-
|
21
|
-
existing_triggers.each do |existing|
|
22
|
-
unless canonical_triggers.any?{ |t| t.prepared_name == existing.prepared_name }
|
23
|
-
up_drop_triggers += existing.drop_triggers
|
24
|
-
down_create_triggers << existing
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
(canonical_triggers - existing_triggers).each do |new_trigger|
|
29
|
-
up_create_triggers << new_trigger
|
30
|
-
down_drop_triggers += new_trigger.drop_triggers
|
31
|
-
if existing = existing_triggers.detect{ |t| t.prepared_name == new_trigger.prepared_name }
|
32
|
-
# it's not sufficient to rely on the new trigger to replace the old
|
33
|
-
# one, since we could be dealing with trigger groups and the name
|
34
|
-
# alone isn't sufficient to know which component triggers to remove
|
35
|
-
up_drop_triggers += existing.drop_triggers
|
36
|
-
down_create_triggers << existing
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
unless up_drop_triggers.empty? && up_create_triggers.empty?
|
41
|
-
migration_base_name = if up_create_triggers.size > 0
|
42
|
-
("create trigger#{up_create_triggers.size > 1 ? 's' : ''} " +
|
43
|
-
up_create_triggers.map{ |t| [t.options[:table], t.options[:events].join(" ")].join(" ") }.join(" and ")
|
44
|
-
).downcase.gsub(/[^a-z0-9_]/, '_').gsub(/_+/, '_').camelize
|
45
|
-
else
|
46
|
-
("drop trigger#{up_drop_triggers.size > 1 ? 's' : ''} " +
|
47
|
-
up_drop_triggers.map{ |t| t.options[:table] }.join(" and ")
|
48
|
-
).downcase.gsub(/[^a-z0-9_]/, '_').gsub(/_+/, '_').camelize
|
49
|
-
end
|
50
|
-
|
51
|
-
name_version = nil
|
52
|
-
while migration_names.include?("#{migration_base_name}#{name_version}")
|
53
|
-
name_version = name_version.to_i + 1
|
54
|
-
end
|
55
|
-
migration_name = "#{migration_base_name}#{name_version}"
|
56
|
-
file_name = 'db/migrate/' + Time.now.getutc.strftime("%Y%m%d%H%M%S") + "_" + migration_name.underscore + ".rb"
|
57
|
-
File.open(file_name, "w"){ |f| f.write <<-MIGRATION }
|
58
|
-
# This migration was auto-generated via `rake db:generate_trigger_migration'.
|
59
|
-
# While you can edit this file, any changes you make to the definitions here
|
60
|
-
# will be undone by the next auto-generated trigger migration.
|
61
|
-
|
62
|
-
class #{migration_name} < ActiveRecord::Migration
|
63
|
-
def self.up
|
64
|
-
#{(up_drop_triggers + up_create_triggers).map{ |t| t.to_ruby(' ') }.join("\n\n").lstrip}
|
65
|
-
end
|
66
|
-
|
67
|
-
def self.down
|
68
|
-
#{(down_drop_triggers + down_create_triggers).map{ |t| t.to_ruby(' ') }.join("\n\n").lstrip}
|
69
|
-
end
|
70
|
-
end
|
71
|
-
MIGRATION
|
4
|
+
if file_name = HairTrigger.generate_migration
|
72
5
|
puts "Generated #{file_name}"
|
73
6
|
else
|
74
7
|
puts "Nothing to do"
|
75
8
|
end
|
76
9
|
end
|
10
|
+
|
11
|
+
namespace :schema do
|
12
|
+
desc "Create a db/schema.rb file that can be portably used against any DB supported by AR"
|
13
|
+
task :dump => :environment do
|
14
|
+
require 'active_record/schema_dumper'
|
15
|
+
filename = ENV['SCHEMA'] || "#{RAILS_ROOT}/db/schema.rb"
|
16
|
+
ActiveRecord::SchemaDumper.previous_schema = File.exist?(filename) ? File.read(filename) : nil
|
17
|
+
File.open(filename, "w") do |file|
|
18
|
+
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
|
19
|
+
end
|
20
|
+
Rake::Task["db:schema:dump"].reenable
|
21
|
+
end
|
22
|
+
end
|
77
23
|
end
|
data/spec/builder_spec.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'rspec'
|
2
|
-
require
|
2
|
+
require 'hair_trigger/builder'
|
3
3
|
|
4
4
|
HairTrigger::Builder.show_warnings = false
|
5
5
|
|
@@ -25,6 +25,14 @@ describe "builder" do
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
+
context "generation" do
|
29
|
+
it "should tack on a semicolon if none is provided" do
|
30
|
+
@adapter = MockAdapter.new("mysql")
|
31
|
+
builder.on(:foos).after(:update){ "FOO " }.generate.
|
32
|
+
grep(/FOO;/).size.should eql(1)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
28
36
|
context "comparison" do
|
29
37
|
it "should view identical triggers as identical" do
|
30
38
|
@adapter = MockAdapter.new("mysql")
|
@@ -78,6 +86,12 @@ describe "builder" do
|
|
78
86
|
}.should raise_error
|
79
87
|
end
|
80
88
|
|
89
|
+
it "should reject for_each :statement" do
|
90
|
+
lambda {
|
91
|
+
builder.on(:foos).after(:update).for_each(:statement){ "FOO" }.generate
|
92
|
+
}.should raise_error
|
93
|
+
end
|
94
|
+
|
81
95
|
it "should reject multiple events" do
|
82
96
|
lambda {
|
83
97
|
builder.on(:foos).after(:update, :delete){ "FOO" }.generate
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class InitialTables < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table "users" do |t|
|
4
|
+
t.integer "group_id"
|
5
|
+
t.string "name"
|
6
|
+
end
|
7
|
+
|
8
|
+
create_table "groups" do |t|
|
9
|
+
t.integer "bob_count", :default => 0
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.down
|
14
|
+
drop_table "users"
|
15
|
+
drop_table "groups"
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# This migration was auto-generated via `rake db:generate_trigger_migration'.
|
2
|
+
# While you can edit this file, any changes you make to the definitions here
|
3
|
+
# will be undone by the next auto-generated trigger migration.
|
4
|
+
|
5
|
+
class UserTrigger < ActiveRecord::Migration
|
6
|
+
def self.up
|
7
|
+
create_trigger("users_after_insert_row_when_new_name_bob__tr", :generated => true, :compatibility => 1).
|
8
|
+
on("users").
|
9
|
+
after(:insert).
|
10
|
+
where("NEW.name = 'bob'") do
|
11
|
+
"UPDATE groups SET bob_count = bob_count + 1"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.down
|
16
|
+
drop_trigger("users_after_insert_row_when_new_name_bob__tr", "users")
|
17
|
+
end
|
18
|
+
end
|
data/spec/models/user.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
3
|
+
require 'active_record/connection_adapters/mysql_adapter'
|
4
|
+
require 'active_record/connection_adapters/sqlite3_adapter'
|
5
|
+
require 'rspec'
|
6
|
+
require 'hair_trigger'
|
7
|
+
|
8
|
+
# for this spec to work, you need to have postgres and mysql installed (in
|
9
|
+
# addition to the gems), and you should make sure that you have set up
|
10
|
+
# hairtrigger_schema_test dbs in mysql and postgres, along with a hairtrigger
|
11
|
+
# user (no password) having appropriate privileges (user needs to be able to
|
12
|
+
# drop/recreate this db)
|
13
|
+
|
14
|
+
def reset_tmp
|
15
|
+
HairTrigger.model_path = 'tmp/models'
|
16
|
+
HairTrigger.migration_path = 'tmp/migrations'
|
17
|
+
FileUtils.rm_rf('tmp') if File.directory?('tmp')
|
18
|
+
FileUtils.mkdir_p(HairTrigger.model_path)
|
19
|
+
FileUtils.mkdir_p(HairTrigger.migration_path)
|
20
|
+
FileUtils.cp_r('spec/models', 'tmp')
|
21
|
+
FileUtils.cp_r('spec/migrations', 'tmp')
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize_db(adapter)
|
25
|
+
reset_tmp
|
26
|
+
config = {:database => 'hairtrigger_schema_test', :username => 'hairtrigger', :adapter => adapter.to_s, :host => 'localhost'}
|
27
|
+
case adapter
|
28
|
+
when :mysql
|
29
|
+
ret = `echo "drop database if exists hairtrigger_schema_test; create database hairtrigger_schema_test;" | mysql hairtrigger_schema_test -u hairtrigger`
|
30
|
+
raise "error creating database: #{ret}" unless $?.exitstatus == 0
|
31
|
+
when :sqlite3
|
32
|
+
config[:database] = 'tmp/hairtrigger_schema_test.sqlite3'
|
33
|
+
when :postgresql
|
34
|
+
`dropdb -U hairtrigger hairtrigger_schema_test &>/dev/null`
|
35
|
+
ret = `createdb -U hairtrigger hairtrigger_schema_test 2>&1`
|
36
|
+
raise "error creating database: #{ret}" unless $?.exitstatus == 0
|
37
|
+
config[:min_messages] = :error
|
38
|
+
end
|
39
|
+
ActiveRecord::Base.establish_connection(config)
|
40
|
+
ActiveRecord::Base.logger = Logger.new('/dev/null')
|
41
|
+
ActiveRecord::Migrator.migrate(HairTrigger.migration_path)
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "schema" do
|
45
|
+
[:mysql, :postgresql, :sqlite3].each do |adapter|
|
46
|
+
it "should correctly dump #{adapter}" do
|
47
|
+
ActiveRecord::Migration.verbose = false
|
48
|
+
initialize_db(adapter)
|
49
|
+
ActiveRecord::Base.connection.triggers.values.grep(/bob_count \+ 1/).size.should eql(1)
|
50
|
+
|
51
|
+
# schema dump w/o previous schema.rb
|
52
|
+
ActiveRecord::SchemaDumper.previous_schema = nil
|
53
|
+
io = StringIO.new
|
54
|
+
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, io)
|
55
|
+
io.rewind
|
56
|
+
schema_rb = io.read
|
57
|
+
schema_rb.should match(/create_trigger\("users_after_insert_row_when_new_name_bob__tr", :generated => true, :compatibility => 1\)/)
|
58
|
+
|
59
|
+
# schema dump w/ schema.rb
|
60
|
+
ActiveRecord::SchemaDumper.previous_schema = schema_rb
|
61
|
+
io = StringIO.new
|
62
|
+
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, io)
|
63
|
+
io.rewind
|
64
|
+
schema_rb2 = io.read
|
65
|
+
schema_rb2.should eql(schema_rb)
|
66
|
+
|
67
|
+
# edit our model trigger, generate and apply a new migration
|
68
|
+
user_model = File.read(HairTrigger.model_path + '/user.rb')
|
69
|
+
File.open(HairTrigger.model_path + '/user.rb', 'w') { |f|
|
70
|
+
f.write user_model.sub('UPDATE groups SET bob_count = bob_count + 1', 'UPDATE groups SET bob_count = bob_count + 2')
|
71
|
+
}
|
72
|
+
migration = HairTrigger.generate_migration
|
73
|
+
ActiveRecord::Migrator.migrate(HairTrigger.migration_path)
|
74
|
+
ActiveRecord::Base.connection.triggers.values.grep(/bob_count \+ 1/).size.should eql(0)
|
75
|
+
ActiveRecord::Base.connection.triggers.values.grep(/bob_count \+ 2/).size.should eql(1)
|
76
|
+
|
77
|
+
# schema dump, should have the updated trigger
|
78
|
+
ActiveRecord::SchemaDumper.previous_schema = schema_rb2
|
79
|
+
io = StringIO.new
|
80
|
+
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, io)
|
81
|
+
io.rewind
|
82
|
+
schema_rb3 = io.read
|
83
|
+
schema_rb3.should_not eql(schema_rb2)
|
84
|
+
schema_rb3.should match(/create_trigger\("users_after_insert_row_when_new_name_bob__tr", :generated => true, :compatibility => 1\)/)
|
85
|
+
schema_rb3.should match(/UPDATE groups SET bob_count = bob_count \+ 2/)
|
86
|
+
|
87
|
+
# undo migration, schema dump should be back to previous version
|
88
|
+
ActiveRecord::Migrator.rollback(HairTrigger.migration_path)
|
89
|
+
ActiveRecord::SchemaDumper.previous_schema = schema_rb3
|
90
|
+
io = StringIO.new
|
91
|
+
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, io)
|
92
|
+
io.rewind
|
93
|
+
schema_rb4 = io.read
|
94
|
+
schema_rb4.should_not eql(schema_rb3)
|
95
|
+
schema_rb4.should eql(schema_rb2)
|
96
|
+
ActiveRecord::Base.connection.triggers.values.grep(/bob_count \+ 1/).size.should eql(1)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hairtrigger
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 17
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
9
|
+
- 5
|
10
|
+
version: 0.1.5
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Jon Jensen
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-03-
|
18
|
+
date: 2011-03-31 00:00:00 -06:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -105,6 +105,54 @@ dependencies:
|
|
105
105
|
type: :development
|
106
106
|
- !ruby/object:Gem::Dependency
|
107
107
|
requirement: &id006 !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
hash: 45
|
113
|
+
segments:
|
114
|
+
- 2
|
115
|
+
- 8
|
116
|
+
- 1
|
117
|
+
version: 2.8.1
|
118
|
+
version_requirements: *id006
|
119
|
+
name: mysql
|
120
|
+
prerelease: false
|
121
|
+
type: :development
|
122
|
+
- !ruby/object:Gem::Dependency
|
123
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
124
|
+
none: false
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
hash: 53
|
129
|
+
segments:
|
130
|
+
- 0
|
131
|
+
- 10
|
132
|
+
- 1
|
133
|
+
version: 0.10.1
|
134
|
+
version_requirements: *id007
|
135
|
+
name: pg
|
136
|
+
prerelease: false
|
137
|
+
type: :development
|
138
|
+
- !ruby/object:Gem::Dependency
|
139
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
140
|
+
none: false
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
hash: 31
|
145
|
+
segments:
|
146
|
+
- 1
|
147
|
+
- 3
|
148
|
+
- 2
|
149
|
+
version: 1.3.2
|
150
|
+
version_requirements: *id008
|
151
|
+
name: sqlite3-ruby
|
152
|
+
prerelease: false
|
153
|
+
type: :development
|
154
|
+
- !ruby/object:Gem::Dependency
|
155
|
+
requirement: &id009 !ruby/object:Gem::Requirement
|
108
156
|
none: false
|
109
157
|
requirements:
|
110
158
|
- - <
|
@@ -122,12 +170,12 @@ dependencies:
|
|
122
170
|
- 3
|
123
171
|
- 0
|
124
172
|
version: 2.3.0
|
125
|
-
version_requirements: *
|
173
|
+
version_requirements: *id009
|
126
174
|
name: activerecord
|
127
175
|
prerelease: false
|
128
176
|
type: :runtime
|
129
177
|
- !ruby/object:Gem::Dependency
|
130
|
-
requirement: &
|
178
|
+
requirement: &id010 !ruby/object:Gem::Requirement
|
131
179
|
none: false
|
132
180
|
requirements:
|
133
181
|
- - ~>
|
@@ -138,7 +186,7 @@ dependencies:
|
|
138
186
|
- 3
|
139
187
|
- 0
|
140
188
|
version: 2.3.0
|
141
|
-
version_requirements: *
|
189
|
+
version_requirements: *id010
|
142
190
|
name: rspec
|
143
191
|
prerelease: false
|
144
192
|
type: :development
|
@@ -165,10 +213,16 @@ files:
|
|
165
213
|
- lib/hair_trigger/base.rb
|
166
214
|
- lib/hair_trigger/builder.rb
|
167
215
|
- lib/hair_trigger/migration.rb
|
216
|
+
- lib/hair_trigger/schema.rb
|
217
|
+
- lib/hair_trigger/schema_dumper.rb
|
168
218
|
- lib/tasks/hair_trigger.rake
|
169
219
|
- rails/init.rb
|
170
220
|
- spec/builder_spec.rb
|
171
|
-
- spec/
|
221
|
+
- spec/migrations/20110331212003_initial_tables.rb
|
222
|
+
- spec/migrations/20110331212631_user_trigger.rb
|
223
|
+
- spec/models/group.rb
|
224
|
+
- spec/models/user.rb
|
225
|
+
- spec/schema_dumper_spec.rb
|
172
226
|
has_rdoc: true
|
173
227
|
homepage: http://github.com/jenseng/hair_trigger
|
174
228
|
licenses:
|
@@ -205,4 +259,8 @@ specification_version: 3
|
|
205
259
|
summary: easy database triggers for active record
|
206
260
|
test_files:
|
207
261
|
- spec/builder_spec.rb
|
208
|
-
- spec/
|
262
|
+
- spec/migrations/20110331212003_initial_tables.rb
|
263
|
+
- spec/migrations/20110331212631_user_trigger.rb
|
264
|
+
- spec/models/group.rb
|
265
|
+
- spec/models/user.rb
|
266
|
+
- spec/schema_dumper_spec.rb
|
data/spec/spec_helper.rb
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
-
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
-
require 'rspec'
|
4
|
-
require 'hairtrigger'
|
5
|
-
|
6
|
-
# Requires supporting files with custom matchers and macros, etc,
|
7
|
-
# in ./support/ and its subdirectories.
|
8
|
-
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
9
|
-
|
10
|
-
RSpec.configure do |config|
|
11
|
-
|
12
|
-
end
|