prodder 1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,390 @@
1
+ module Prodder
2
+ # The list of default rake tasks which prodder will be removing or replacing.
3
+ # @see databases.rake (currently at lib/active_record/railties/databases.rake)
4
+ def self.obsoleted_rake_tasks
5
+ [/^db:_dump$/,
6
+ /^db:migrate:reset$/,
7
+ /^db:drop$/,
8
+ /^db:create$/,
9
+ /^db:drop:all$/,
10
+ /^db:create:all$/,
11
+ /^db:migrate$/,
12
+ /^db:migrate:up$/,
13
+ /^db:migrate:down$/,
14
+ /^db:rollback$/,
15
+ /^db:forward$/,
16
+ /^db:version$/,
17
+ /^db:fixtures:.*$/,
18
+ /^db:abort_if_pending_migrations$/,
19
+ /^db:purge$/,
20
+ /^db:purge:all$/,
21
+ /^db:charset$/,
22
+ /^db:collation$/,
23
+ /^db:reset$/,
24
+ /^db:schema:.*$/,
25
+ /^db:seed$/,
26
+ /^db:setup$/,
27
+ /^db:structure:.*$/,
28
+ /^db:test:.*$/,
29
+ /^test:prepare$/
30
+ ]
31
+ end
32
+ end
33
+
34
+ tasks = Rake.application.instance_variable_get :@tasks
35
+ tasks.keys.select { |name|
36
+ Prodder.obsoleted_rake_tasks.any? { |obsoleted| obsoleted.match(name) }
37
+ }.each { |name| tasks.delete name }
38
+
39
+ namespace :db do
40
+ desc "Drop, recreate, reseed, remigrate the database"
41
+ task :reset => ['db:drop', 'db:setup']
42
+
43
+ desc "Create the database, load db/structure.sql, db/seeds.sql, db/quality_checks.sql"
44
+ task :setup => ['db:create', 'db:structure:load', 'db:seed', 'db:quality_check', 'db:permission', 'db:settings']
45
+
46
+ dependencies = [:load_config]
47
+ if Rake::Task.task_defined?('rails_env')
48
+ dependencies << :rails_env
49
+ end
50
+
51
+ namespace :migrate do
52
+ task :up => [:environment].concat(dependencies) do
53
+ version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
54
+ raise 'VERSION is required' unless version
55
+ as("migration_user", in: ENV['RAILS_ENV'] || Rails.env) do
56
+ ActiveRecord::Base.establish_connection((ENV['RAILS_ENV'] || Rails.env).intern)
57
+ ActiveRecord::Migrator.run(:up, ActiveRecord::Migrator.migrations_paths, version)
58
+ end
59
+ end
60
+
61
+ task :down => [:environment].concat(dependencies) do
62
+ version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
63
+ raise 'VERSION is required - To go down one migration, run db:rollback' unless version
64
+ as("migration_user", in: ENV['RAILS_ENV'] || Rails.env) do
65
+ ActiveRecord::Base.establish_connection((ENV['RAILS_ENV'] || Rails.env).intern)
66
+ ActiveRecord::Migrator.run(:down, ActiveRecord::Migrator.migrations_paths, version)
67
+ end
68
+ end
69
+ end
70
+
71
+ namespace :purge do
72
+ task :all => dependencies do
73
+ as("superuser") do
74
+ ActiveRecord::Tasks::DatabaseTasks.purge_all
75
+ end
76
+ end
77
+ end
78
+
79
+ desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases."
80
+ task :purge => dependencies do
81
+ as("superuser", in: ENV['RAILS_ENV'] || [Rails.env, "test"]) do
82
+ ActiveRecord::Tasks::DatabaseTasks.purge_current
83
+ end
84
+ end
85
+
86
+ desc "Retrieves the charset for the current environment's database"
87
+ task :charset => [:environment].concat(dependencies) do
88
+ as("migration_user", in: ENV['RAILS_ENV'] || Rails.env) do
89
+ puts ActiveRecord::Tasks::DatabaseTasks.charset_current
90
+ end
91
+ end
92
+
93
+ desc "Retrieves the collation for the current environment's database"
94
+ task :collation => [:environment].concat(dependencies) do
95
+ as("migration_user", in: ENV['RAILS_ENV'] || Rails.env) do
96
+ begin
97
+ puts ActiveRecord::Tasks::DatabaseTasks.collation_current
98
+ rescue NoMethodError
99
+ $stderr.puts 'Sorry, your database adapter is not supported yet. Feel free to submit a patch.'
100
+ end
101
+ end
102
+ end
103
+
104
+ desc 'Rolls the schema back to the previous version (specify steps w/ STEP=n).'
105
+ task :rollback => [:environment].concat(dependencies) do
106
+ step = ENV['STEP'] ? ENV['STEP'].to_i : 1
107
+ as("migration_user", in: ENV['RAILS_ENV'] || Rails.env) do
108
+ ActiveRecord::Base.establish_connection((ENV['RAILS_ENV'] || Rails.env).intern)
109
+ ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step)
110
+ end
111
+ end
112
+
113
+ desc 'Pushes the schema to the next version (specify steps w/ STEP=n).'
114
+ task :forward => [:environment].concat(dependencies) do
115
+ step = ENV['STEP'] ? ENV['STEP'].to_i : 1
116
+ as("migration_user", in: ENV['RAILS_ENV'] || Rails.env) do
117
+ ActiveRecord::Base.establish_connection((ENV['RAILS_ENV'] || Rails.env).intern)
118
+ ActiveRecord::Migrator.forward(ActiveRecord::Migrator.migrations_paths, step)
119
+ end
120
+ end
121
+
122
+ desc 'Retrieves the current schema version number'
123
+ task :version => [:environment].concat(dependencies) do
124
+ as("migration_user", in: ENV['RAILS_ENV'] || Rails.env) do
125
+ ActiveRecord::Base.establish_connection((ENV['RAILS_ENV'] || Rails.env).intern)
126
+ puts "Current version: #{ActiveRecord::Migrator.current_version}"
127
+ end
128
+ end
129
+
130
+ task :abort_if_pending_migrations => [:environment].concat(dependencies) do
131
+ as("migration_user", in: ENV['RAILS_ENV'] || Rails.env) do
132
+ ActiveRecord::Base.establish_connection((ENV['RAILS_ENV'] || Rails.env).intern)
133
+ pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations
134
+
135
+ if pending_migrations.any?
136
+ puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}"
137
+ pending_migrations.each do |pending_migration|
138
+ puts ' %4d %s' % [pending_migration.version, pending_migration.name]
139
+ end
140
+ abort %{Run `rake db:migrate` to update your database then try again.}
141
+ end
142
+ end
143
+ end
144
+
145
+ namespace :drop do
146
+ task :all => dependencies do
147
+ as("superuser") do
148
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
149
+ end
150
+ end
151
+ end
152
+
153
+ desc "Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV, it defaults to dropping the development and test databases."
154
+ task :drop => dependencies do
155
+ as("superuser", in: ENV['RAILS_ENV'] || [Rails.env, "test"]) do
156
+ ActiveRecord::Tasks::DatabaseTasks.drop_current
157
+ end
158
+ end
159
+
160
+ desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV, it defaults to creating the development and test databases.'
161
+ task :create => dependencies do
162
+ environments = nil
163
+ if ENV['RAILS_ENV']
164
+ environments = Array(ENV['RAILS_ENV'])
165
+ else
166
+ environments = [Rails.env, "test"]
167
+ end
168
+ as("superuser", in: environments) do
169
+ ActiveRecord::Tasks::DatabaseTasks.create_current
170
+ ActiveRecord::Base.configurations.each do |env, config|
171
+ if environments.include?(env) && config["migration_user"] && config['database']
172
+ `psql --no-psqlrc --command "ALTER DATABASE #{config['database']} OWNER TO #{config['migration_user']}" #{Shellwords.escape(config['database'])}`
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ namespace :create do
179
+ task :all => dependencies do
180
+ as("superuser") do
181
+ ActiveRecord::Tasks::DatabaseTasks.create_all
182
+ ActiveRecord::Base.configurations.each do |env, config|
183
+ if config["migration_user"] && config['database']
184
+ `psql --no-psqlrc --command "ALTER DATABASE #{config['database']} OWNER TO #{config['migration_user']}" #{Shellwords.escape(config['database'])}`
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
192
+ task :migrate => [:environment].concat(dependencies) do
193
+ as("migration_user", in: ENV['RAILS_ENV'] || Rails.env) do
194
+ ActiveRecord::Base.establish_connection((ENV['RAILS_ENV'] || Rails.env).intern)
195
+ ActiveRecord::Tasks::DatabaseTasks.migrate
196
+ end
197
+ end
198
+
199
+ namespace :structure do
200
+ desc "Load db/structure.sql into the current environment's database"
201
+ task :load => dependencies do
202
+ config = ActiveRecord::Base.configurations[ENV['RAILS_ENV'] || Rails.env]
203
+ config["username"] = config["superuser"] if config["superuser"] && File.exist?('db/permissions.sql')
204
+ set_psql_env config
205
+ puts "Loading db/structure.sql into database '#{config['database']}'"
206
+ `psql --no-psqlrc -f db/structure.sql #{Shellwords.escape(config['database'])}`
207
+ raise 'Error loading db/structure.sql' if $?.exitstatus != 0
208
+ end
209
+ end
210
+
211
+ desc "Load initial seeds from db/seeds.sql"
212
+ task :seed => dependencies do
213
+ if File.exist?('db/seeds.sql')
214
+ config = ActiveRecord::Base.configurations[ENV['RAILS_ENV'] || Rails.env]
215
+ config["username"] = config["superuser"] if config["superuser"] && File.exist?('db/permissions.sql')
216
+ set_psql_env config
217
+ puts "Loading db/seeds.sql into database '#{config['database']}'"
218
+ `psql --no-psqlrc -f db/seeds.sql #{Shellwords.escape(config['database'])}`
219
+ raise 'Error loading db/seeds.sql' if $?.exitstatus != 0
220
+ else
221
+ puts 'db/seeds.sql not found: no seeds to load.'
222
+ end
223
+ end
224
+
225
+ desc "Load quality_checks (indexes, triggers, foreign keys) from db/quality_checks.sql"
226
+ task :quality_check => dependencies do
227
+ if File.exist?('db/quality_checks.sql')
228
+ config = ActiveRecord::Base.configurations[ENV['RAILS_ENV'] || Rails.env]
229
+ config["username"] = config["superuser"] if config["superuser"] && File.exist?('db/permissions.sql')
230
+ set_psql_env config
231
+ puts "Loading db/quality_checks.sql into database '#{config['database']}'"
232
+ `psql --no-psqlrc -f db/quality_checks.sql #{Shellwords.escape(config['database'])}`
233
+ raise 'Error loading db/quality_checks.sql' if $?.exitstatus != 0
234
+ else
235
+ puts 'db/quality_checks.sql not found: no quality_checks to load.'
236
+ end
237
+ end
238
+
239
+ desc "Load permissions (DB object level access control, group role memberships) from db/permissions.sql"
240
+ task :permission => dependencies do
241
+ if File.exist?('db/permissions.sql')
242
+ config = ActiveRecord::Base.configurations[ENV['RAILS_ENV'] || Rails.env]
243
+ config["username"] = config["superuser"] if config["superuser"]
244
+ set_psql_env config
245
+ puts "Loading db/permissions.sql into database '#{config['database']}'"
246
+ disconnect
247
+ ActiveRecord::Base.establish_connection((ENV['RAILS_ENV'] || Rails.env).intern)
248
+ is_super = ActiveRecord::Base.connection.execute(<<-SQL).first['is_super']
249
+ select 1 as is_super from pg_roles where rolname = '#{config['username']}' and rolsuper
250
+ SQL
251
+ unless is_super
252
+ puts "Restoring permissions as config/database.yml non-superuser: #{config['username']}, expect errors, or rerun after granting superuser"
253
+ end
254
+ `psql --no-psqlrc -f db/permissions.sql #{Shellwords.escape(config['database'])}`
255
+
256
+ raise 'Error loading db/permissions.sql' if $?.exitstatus != 0
257
+ else
258
+ puts 'db/permissions.sql not found: no permissions to load.'
259
+ end
260
+ end
261
+
262
+ desc "Load database settings"
263
+ task :settings => dependencies do
264
+ config = ActiveRecord::Base.configurations[ENV['RAILS_ENV'] || Rails.env]
265
+ config["username"] = config["superuser"] if config["superuser"] && File.exist?('db/permissions.sql')
266
+ set_psql_env config
267
+ puts "Loading db/settings.sql into database '#{config['database']}'"
268
+ disconnect
269
+ ActiveRecord::Base.establish_connection((ENV['RAILS_ENV'] || Rails.env).intern)
270
+ is_super = ActiveRecord::Base.connection.execute(<<-SQL).first['is_super']
271
+ select 1 as is_super from pg_roles where rolname = '#{config['username']}' and rolsuper
272
+ SQL
273
+ unless is_super
274
+ puts "Restoring settings as config/database.yml non-superuser: #{config['username']}, expect errors, or rerun after granting superuser"
275
+ end
276
+ `psql --no-psqlrc -f db/settings.sql #{Shellwords.escape(config['database'])}`
277
+
278
+ raise 'Error loading db/settings.sql' if $?.exitstatus != 0
279
+ end
280
+
281
+ # Empty this, we don't want db:migrate writing structure.sql any more.
282
+ task :_dump do
283
+ end
284
+
285
+ # Ugh. cucumber.rake, installed by the cucumber generator, always uses a task dependency
286
+ # on db:test:prepare. rspec.rake, contained within rspec-rails, uses either db:test:prepare
287
+ # or db:test:clone_structure, depending on what schema_format you declare.
288
+ #
289
+ # Gut and redefine both.
290
+ namespace :test do
291
+ task :prepare do
292
+ begin
293
+ orig_env_var, orig_rails_var = ENV['RAILS_ENV'], Rails.env
294
+ Rails.env = ENV['RAILS_ENV'] = 'test'
295
+ Rake::Task['db:reset'].invoke
296
+ Rake::Task['db:migrate'].invoke
297
+ ensure
298
+ ENV['RAILS_ENV'], Rails.env = orig_env_var, orig_rails_var
299
+ end
300
+ end
301
+
302
+ # What rspec calls as a prereq to :spec
303
+ task :clone_structure => :prepare
304
+ end
305
+
306
+ # Exposed as a global method in Rails 3.x, but moved to a private method in Rails 4.
307
+ # We should instead be registering our own `seed_loader`, which would obviate a lot
308
+ # of this hackery to support Rails 4.
309
+ if !defined?(set_psql_env)
310
+ def set_psql_env(config)
311
+ ENV['PGHOST'] = config['host'] if config['host']
312
+ ENV['PGPORT'] = config['port'].to_s if config['port']
313
+ ENV['PGPASSWORD'] = config['password'].to_s if config['password']
314
+ ENV['PGUSER'] = config['username'].to_s if config['username']
315
+ end
316
+ end
317
+
318
+ #adding to the Rails hackery
319
+ if !defined?(ActiveRecord::Tasks::DatabaseTasks.migrate)
320
+ module ActiveRecord::Tasks::DatabaseTasks
321
+ def migrate
322
+ verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
323
+ version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
324
+ scope = ENV['SCOPE']
325
+ verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, verbose
326
+ ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, version) do |migration|
327
+ scope.blank? || scope == migration.scope
328
+ end
329
+ ensure
330
+ ActiveRecord::Migration.verbose = verbose_was
331
+ end
332
+ end
333
+ end
334
+
335
+ if !defined?(ActiveRecord::Tasks::DatabaseTasks.purge_all)
336
+ module ActiveRecord::Tasks::DatabaseTasks
337
+ def purge_all
338
+ each_local_configuration { |configuration|
339
+ purge configuration
340
+ }
341
+ end
342
+ end
343
+ end
344
+
345
+ if !defined?(ActiveRecord::Tasks::DatabaseTasks.purge_current)
346
+ module ActiveRecord::Tasks::DatabaseTasks
347
+ def purge_current(environment = env)
348
+ each_current_configuration(environment) { |configuration|
349
+ purge configuration
350
+ }
351
+ ActiveRecord::Base.establish_connection(environment.to_sym)
352
+ end
353
+ end
354
+ end
355
+
356
+ def as(user, opts = {}, &block)
357
+ if File.exist?('db/permissions.sql')
358
+ config, config_was = ActiveRecord::Base.configurations.deep_dup, ActiveRecord::Base.configurations.deep_dup
359
+ in_env = Array(opts[:in]) || config.keys
360
+ if config.all? { |env, config_hash| in_env.include?(env) ? config_hash[user] : true }
361
+ disconnect
362
+ config.each { |env, config_hash| config_hash["username"] = config_hash[user] if in_env.include?(env) }
363
+ ActiveRecord::Base.configurations = config
364
+ end
365
+ else
366
+ puts "No permissions file (db/permissions.sql) found, running everything in context of user"
367
+ end
368
+ yield
369
+ ensure
370
+ ActiveRecord::Base.configurations = config_was if config_was
371
+ in_env.each { |env| ActiveRecord::Base.establish_connection(env.intern) } if in_env
372
+ end
373
+
374
+ def disconnect
375
+ if ActiveRecord::Base.connection_pool && ActiveRecord::Base.connection_pool.connections.size > 0
376
+ ActiveRecord::Base.connection_pool.disconnect!
377
+ end
378
+ rescue ActiveRecord::ConnectionNotEstablished
379
+ end
380
+
381
+ end
382
+
383
+ namespace :test do
384
+ task :prepare => [ 'db:test:prepare' ]
385
+ end
386
+
387
+ # Yes, I really want migrations to run against the test DB.
388
+ Rake::Task['db:migrate'].actions.unshift(proc {
389
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ENV['RAILS_ENV'] || Rails.env])
390
+ })
@@ -0,0 +1,150 @@
1
+ require 'deject'
2
+ require 'fileutils'
3
+ require 'prodder/pg'
4
+ require 'prodder/git'
5
+
6
+ module Prodder
7
+ class Project
8
+ SeedConfigFileMissing = Class.new(StandardError) do
9
+ attr_reader :filename
10
+ def initialize(filename); @filename = filename; end
11
+ end
12
+
13
+ Deject self
14
+ dependency(:pg) { |project| Prodder::PG.new(project.db_credentials) }
15
+ dependency(:git) { |project| Prodder::Git.new(project.local_git_path, project.git_origin) }
16
+
17
+ attr_reader :name, :workspace
18
+
19
+ def initialize(name, workspace, definition)
20
+ @name = name
21
+ @workspace = workspace
22
+ @defn = definition
23
+ end
24
+
25
+ def init
26
+ git.clone_or_remote_update
27
+ end
28
+
29
+ def dump
30
+ FileUtils.mkdir_p File.dirname(structure_file_name)
31
+ pg.dump_structure db_credentials['name'], structure_file_name,
32
+ exclude_tables: excluded_tables, exclude_schemas: excluded_schemas
33
+
34
+ FileUtils.mkdir_p File.dirname(settings_file_name)
35
+ pg.dump_settings db_credentials['name'], settings_file_name
36
+
37
+ FileUtils.mkdir_p File.dirname(seed_file_name)
38
+ pg.dump_tables db_credentials['name'], seed_tables, seed_file_name
39
+
40
+ # must split the structure file to allow data to be loaded after tables
41
+ # being created but before triggers and foreign keys are created. this
42
+ # facilitates validation during loading, yet avoids extra overhead and
43
+ # false errors
44
+ if separate_quality_checks?
45
+ contents = File.readlines(structure_file_name)
46
+ rgx = /^\-\- .* Type: INDEX; |^\-\- .* Type: TRIGGER; |^\-\- .* Type: FK CONSTRAINT; /
47
+ structure, *quality = contents.slice_before(rgx).to_a
48
+ quality_checks = structure.grep(/SET search_path/).last + quality.join
49
+
50
+ File.open(quality_check_file_name, 'w') { |f| f.write(quality_checks) }
51
+ File.open(structure_file_name, 'w') { |f| f.write(structure.join) }
52
+ end
53
+
54
+ if dump_permissions?
55
+ FileUtils.mkdir_p File.dirname(permissions_file_name)
56
+ pg.dump_permissions db_credentials['name'], permissions_file_name, included_users: included_users,
57
+ exclude_tables: excluded_tables, exclude_schemas: excluded_schemas
58
+
59
+ end
60
+ end
61
+
62
+ def commit
63
+ return unless git.dirty?
64
+ git.add structure_file_name
65
+ git.add seed_file_name
66
+ git.add quality_check_file_name if separate_quality_checks?
67
+ git.add permissions_file_name if dump_permissions?
68
+ git.add settings_file_name
69
+ git.commit "Auto-commit by prodder", @defn['git']['author']
70
+ end
71
+
72
+ def push
73
+ if git.fast_forward?
74
+ git.push
75
+ else
76
+ raise Prodder::Git::NotFastForward.new(git_origin)
77
+ end
78
+ end
79
+
80
+ def nothing_to_push?
81
+ git.remote_update
82
+ git.no_new_commits?
83
+ end
84
+
85
+ def db_credentials
86
+ @defn['db']
87
+ end
88
+
89
+ def permissions
90
+ @defn['permissions']
91
+ end
92
+
93
+ def local_git_path
94
+ workspace
95
+ end
96
+
97
+ def git_origin
98
+ @defn['git']['origin']
99
+ end
100
+
101
+ def structure_file_name
102
+ File.join workspace, @defn['structure_file']
103
+ end
104
+
105
+ def settings_file_name
106
+ File.join workspace, 'db/settings.sql'
107
+ end
108
+
109
+ def seed_file_name
110
+ File.join workspace, @defn['seed_file']
111
+ end
112
+
113
+ def quality_check_file_name
114
+ File.join workspace, @defn['quality_check_file']
115
+ end
116
+
117
+ def permissions_file_name
118
+ File.join workspace, permissions['file']
119
+ end
120
+
121
+ def separate_quality_checks?
122
+ @defn.key? 'quality_check_file'
123
+ end
124
+
125
+ def dump_permissions?
126
+ @defn.key?('permissions') && permissions.key?('file')
127
+ end
128
+
129
+ def excluded_schemas
130
+ db_credentials['exclude_schemas'] || []
131
+ end
132
+
133
+ def excluded_tables
134
+ db_credentials['exclude_tables'] || []
135
+ end
136
+
137
+ def included_users
138
+ permissions['included_users'] || []
139
+ end
140
+
141
+ def seed_tables
142
+ value = db_credentials['tables']
143
+ return value unless value.is_a?(String)
144
+
145
+ path = File.join(workspace, value)
146
+ raise SeedConfigFileMissing.new(File.join(name, value)) unless File.exist?(path)
147
+ YAML.load IO.read(path)
148
+ end
149
+ end
150
+ end