heroku-schemas 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +3 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +94 -0
  5. data/Rakefile +2 -0
  6. data/features/drop.feature +9 -0
  7. data/features/migration.feature +18 -0
  8. data/features/show.feature +9 -0
  9. data/features/step_definitions/common_steps.rb +8 -0
  10. data/features/step_definitions/drop_steps.rb +12 -0
  11. data/features/step_definitions/migration_steps.rb +57 -0
  12. data/features/step_definitions/show_steps.rb +12 -0
  13. data/features/support/config.example.yml +2 -0
  14. data/features/support/env.rb +42 -0
  15. data/features/support/lib/configuration.rb +23 -0
  16. data/features/support/lib/heroku-schemas/test/app.rb +109 -0
  17. data/features/support/lib/output_buffer.rb +26 -0
  18. data/heroku-schemas.gemspec +29 -0
  19. data/init.rb +1 -0
  20. data/lib/heroku-schemas.rb +19 -0
  21. data/lib/heroku-schemas/current_database.rb +9 -0
  22. data/lib/heroku-schemas/database.rb +63 -0
  23. data/lib/heroku-schemas/drop.rb +22 -0
  24. data/lib/heroku-schemas/migration.rb +109 -0
  25. data/lib/heroku-schemas/pgbackups.rb +18 -0
  26. data/lib/heroku-schemas/schema_command.rb +12 -0
  27. data/lib/heroku-schemas/schema_reference.rb +88 -0
  28. data/lib/heroku-schemas/schema_utilities.rb +45 -0
  29. data/lib/heroku-schemas/show.rb +15 -0
  30. data/lib/heroku-schemas/target_database.rb +9 -0
  31. data/lib/heroku-schemas/version.rb +3 -0
  32. data/lib/heroku/command/schemas.rb +28 -0
  33. data/spec/dummy-app/.gitignore +17 -0
  34. data/spec/dummy-app/Gemfile +37 -0
  35. data/spec/dummy-app/README.rdoc +261 -0
  36. data/spec/dummy-app/Rakefile +7 -0
  37. data/spec/dummy-app/app/assets/javascripts/application.js +15 -0
  38. data/spec/dummy-app/app/assets/stylesheets/application.css +13 -0
  39. data/spec/dummy-app/app/controllers/application_controller.rb +3 -0
  40. data/spec/dummy-app/app/helpers/application_helper.rb +2 -0
  41. data/spec/dummy-app/app/mailers/.gitkeep +0 -0
  42. data/spec/dummy-app/app/models/.gitkeep +0 -0
  43. data/spec/dummy-app/app/models/dummy_record.rb +3 -0
  44. data/spec/dummy-app/app/views/layouts/application.html.erb +14 -0
  45. data/spec/dummy-app/config.ru +4 -0
  46. data/spec/dummy-app/config/application.rb +65 -0
  47. data/spec/dummy-app/config/boot.rb +6 -0
  48. data/spec/dummy-app/config/environment.rb +5 -0
  49. data/spec/dummy-app/config/environments/development.rb +37 -0
  50. data/spec/dummy-app/config/environments/production.rb +67 -0
  51. data/spec/dummy-app/config/environments/test.rb +37 -0
  52. data/spec/dummy-app/config/initializers/backtrace_silencers.rb +7 -0
  53. data/spec/dummy-app/config/initializers/inflections.rb +15 -0
  54. data/spec/dummy-app/config/initializers/mime_types.rb +5 -0
  55. data/spec/dummy-app/config/initializers/secret_token.rb +7 -0
  56. data/spec/dummy-app/config/initializers/session_store.rb +8 -0
  57. data/spec/dummy-app/config/initializers/wrap_parameters.rb +14 -0
  58. data/spec/dummy-app/config/locales/en.yml +5 -0
  59. data/spec/dummy-app/config/routes.rb +58 -0
  60. data/spec/dummy-app/db/migrate/20131106080427_create_dummy_records.rb +9 -0
  61. data/spec/dummy-app/db/schema.rb +22 -0
  62. data/spec/dummy-app/db/seeds.rb +7 -0
  63. data/spec/dummy-app/doc/README_FOR_APP +2 -0
  64. data/spec/dummy-app/lib/assets/.gitkeep +0 -0
  65. data/spec/dummy-app/lib/tasks/.gitkeep +0 -0
  66. data/spec/dummy-app/log/.gitkeep +0 -0
  67. data/spec/dummy-app/script/rails +6 -0
  68. data/spec/dummy-app/test/fixtures/.gitkeep +0 -0
  69. data/spec/dummy-app/test/fixtures/dummy_records.yml +7 -0
  70. data/spec/dummy-app/test/functional/.gitkeep +0 -0
  71. data/spec/dummy-app/test/integration/.gitkeep +0 -0
  72. data/spec/dummy-app/test/performance/browsing_test.rb +12 -0
  73. data/spec/dummy-app/test/test_helper.rb +13 -0
  74. data/spec/dummy-app/test/unit/.gitkeep +0 -0
  75. data/spec/dummy-app/test/unit/dummy_record_test.rb +7 -0
  76. data/spec/dummy-app/vendor/assets/javascripts/.gitkeep +0 -0
  77. data/spec/dummy-app/vendor/assets/stylesheets/.gitkeep +0 -0
  78. data/spec/dummy-app/vendor/plugins/.gitkeep +0 -0
  79. data/spec/heroku-schemas/schema_utilities_spec.rb +46 -0
  80. data/spec/spec_helper.rb +8 -0
  81. metadata +313 -0
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ .DS_Store
2
+ *.dump
3
+ Gemfile.lock
4
+ features/support/config.yml
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 Tom Benner
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ Heroku Schemas
2
+ ==============
3
+ Run many apps on a single database
4
+
5
+ Description
6
+ -----------
7
+
8
+ Heroku Schemas lets you run multiple Heroku apps on top of a single Heroku Postgres database. A Postgres database can have multiple "schemas" (basically Postgres's word for database "namespaces"), and Heroku Schemas simply makes each app use its own schema within a single, shared database.
9
+
10
+ For example, if you have five apps with small levels of traffic, instead of using five databases, you can now just use one database with five schemas to serve all of them.
11
+
12
+ Installation
13
+ ------------
14
+
15
+ Install the plugin:
16
+
17
+ ```sh
18
+ heroku plugins:install git://github.com/tombenner/heroku-schemas.git
19
+ ```
20
+
21
+ Usage
22
+ -----
23
+
24
+ To make an app use a schema named `my_schema` in the database of an app called `my-other-app`:
25
+
26
+ ```sh
27
+ cd path/to/my-app
28
+ heroku schemas:use my-other-app:my_schema
29
+ ```
30
+
31
+ This copies the app's database into the new schema and makes the app use it. You can then remove the original database from your plan.
32
+
33
+ Heroku Schemas also lets you see what database/schema the current app is using (`show`) and drop schemas (`drop`).
34
+
35
+ Commands
36
+ --------
37
+
38
+ ### Use
39
+
40
+ Make the app in the current directory use a new database/schema. If the app has an existing database, it is copied to the target database/schema.
41
+
42
+ The following command makes my-app use the schema `my_schema` in the default database of my-other-app:
43
+
44
+ ```sh
45
+ heroku schemas:use my-other-app:my_schema
46
+ ```
47
+
48
+ If my-other-app has more than one database, you can specify which database the schema should be in:
49
+ ```sh
50
+ heroku schemas:use my-other-app:HEROKU_POSTGRESQL_BLUE_URL:my_schema
51
+ ```
52
+
53
+ ("`BLUE`"" in `HEROKU_POSTGRESQL_BLUE_URL` should be replaced with the color name in the database's name.)
54
+
55
+ ### Show
56
+
57
+ Show which database/schema is currently being used by the app.
58
+
59
+ ```sh
60
+ heroku schemas:show
61
+ => my-other-app:HEROKU_POSTGRESQL_BLUE_URL:my_schema
62
+ ```
63
+
64
+ ### Drop
65
+
66
+ Drop (delete) the schema that is currently being used by the app. This is irreversible, so please be sure that you're dropping the intended schema.
67
+
68
+ ```sh
69
+ heroku schemas:drop
70
+ => Dropped schema my-other-app:HEROKU_POSTGRESQL_BLUE_URL:my_schema
71
+ ```
72
+
73
+ Tests
74
+ -----
75
+
76
+ The feature tests create and manipulate two Heroku apps; to run them, you'll need to:
77
+
78
+ ```sh
79
+ cp features/support/config.example.yml features/support/config.yml
80
+ ```
81
+
82
+ And then edit config.yml to include your Heroku API key and a prefix for the app names (choose something unique to avoid naming conflicts with other people who are running these tests).
83
+
84
+ Notes
85
+ -----
86
+
87
+ A shared database may not be wise for significant, production apps, but it may be worthwhile if you have multiple small apps or apps that are in development.
88
+
89
+ Heroku Schemas is for educational purposes. The author assumes no liability for anything that happens to your data or your Heroku account while using Heroku Schemas.
90
+
91
+ License
92
+ -------
93
+
94
+ Heroku Schemas is released under the MIT License. Please see the MIT-LICENSE file for details.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,9 @@
1
+ Feature: Drop
2
+ In order to drop a schema
3
+ A developer
4
+ Should be able to run a command
5
+
6
+ Scenario: App has been initialized
7
+ Given an app named 'current-app'
8
+ And I drop the schema named 'public'
9
+ Then no schemas should exist
@@ -0,0 +1,18 @@
1
+ Feature: Migration
2
+ In order to move an app's database
3
+ A developer
4
+ Should be able to run a migration
5
+
6
+ Scenario: App has not been migrated
7
+ Given two apps named 'current-app' and 'target-app'
8
+ And I create a backup with the target schema
9
+ And I create the target schema
10
+ And I update the database URL
11
+ And I import the backup into the target schema
12
+ Then the first app should be using the database of the second app
13
+
14
+ Scenario: App has been migrated
15
+ Given two apps named 'current-app' and 'target-app'
16
+ And I add the current app's schema to the target app's database
17
+ And I run the migration
18
+ Then an error containing "already contains data" is raised
@@ -0,0 +1,9 @@
1
+ Feature: Show
2
+ In order to see what schema an app is using
3
+ A developer
4
+ Should be able to run a command
5
+
6
+ Scenario: App has been initialized
7
+ Given an app named 'current-app'
8
+ And I run the show command
9
+ Then the output should match 'current-app:[A-Z_]+:public'
@@ -0,0 +1,8 @@
1
+ Given /^an app named '([\w_-]+)'$/ do |app|
2
+ @configuration = Configuration
3
+ @heroku = Heroku::API.new(:api_key => @configuration['heroku_api_key'])
4
+
5
+ prefix = @configuration['heroku_app_prefix']
6
+ app_name = "#{prefix}#{app}"
7
+ @app = HerokuSchemas::Test::App.new(app_name, HerokuSchemas::CurrentDatabase)
8
+ end
@@ -0,0 +1,12 @@
1
+ And /^I drop the schema named '(.+)'$/ do |schema|
2
+ @schema = schema
3
+ @drop = HerokuSchemas::Drop.new(
4
+ context_app: @app.name,
5
+ string_reference: "#{@app.name}:#{@schema}"
6
+ )
7
+ @drop.perform
8
+ end
9
+
10
+ Then /^no schemas should exist$/ do
11
+ @drop.database.existing_schemas.should be_empty
12
+ end
@@ -0,0 +1,57 @@
1
+ Given /^two apps named '([\w_-]+)' and '([\w_-]+)'$/ do |current_app, target_app|
2
+ @configuration = Configuration
3
+ @heroku = Heroku::API.new(:api_key => @configuration['heroku_api_key'])
4
+
5
+ prefix = @configuration['heroku_app_prefix']
6
+ current_app_name = "#{prefix}#{current_app}"
7
+ target_app_name = "#{prefix}#{target_app}"
8
+
9
+ @current_app = HerokuSchemas::Test::App.new(current_app_name, HerokuSchemas::CurrentDatabase)
10
+ @target_app = HerokuSchemas::Test::App.new(target_app_name, HerokuSchemas::TargetDatabase)
11
+
12
+ @current_schema = 'public'
13
+ @target_schema = HerokuSchemas::SchemaUtilities.app_to_schema(current_app_name)
14
+ @migration = HerokuSchemas::Migration.new(
15
+ context_app: current_app_name,
16
+ string_reference: "#{target_app_name}:#{@target_schema}"
17
+ )
18
+ end
19
+
20
+ And /^I add the current app's schema to the target app's database$/ do
21
+ @target_app.add_data_to_schema(@target_schema)
22
+ end
23
+
24
+ And /^I create a backup with the target schema$/ do
25
+ @migration.create_backup_with_target_schema
26
+ @migration.backup_url.should =~ %r|^https://.+\.dump.+$|
27
+ end
28
+
29
+ And /^I update the database URL$/ do
30
+ @migration.update_database_url
31
+ end
32
+
33
+ And /^I create the target schema$/ do
34
+ @migration.create_target_schema_in_target_database
35
+ @migration.target_database.existing_schemas.should =~ ['public', @target_schema]
36
+ end
37
+
38
+ And /^I import the backup into the target schema$/ do
39
+ @migration.import_backup_into_target_database
40
+ end
41
+
42
+ And /^I run the migration$/ do
43
+ begin
44
+ @migration.perform
45
+ rescue Exception => @error
46
+ end
47
+ end
48
+
49
+ Then /an error containing "(.+)" is raised/ do |message_excerpt|
50
+ @error.should_not be_nil
51
+ @error.message.should include(message_excerpt)
52
+ end
53
+
54
+ Then /^the first app should be using the database of the second app$/ do
55
+ @migration.target_database.schema_tables(@target_schema).should =~ ['dummy_records']
56
+ @migration.target_database.select_values("SELECT dummy_records.name FROM #{@target_schema}.dummy_records").should == [@current_app.name]
57
+ end
@@ -0,0 +1,12 @@
1
+ And /^I run the show command$/ do
2
+ @show = HerokuSchemas::Show.new(
3
+ context_app: @app.name
4
+ )
5
+ @buffer = OutputBuffer.new.activate
6
+ @show.perform
7
+ @buffer.stop
8
+ end
9
+
10
+ Then /^the output should match '(.+)'$/ do |regex|
11
+ @buffer.to_s.should match(/#{regex}/)
12
+ end
@@ -0,0 +1,2 @@
1
+ heroku_api_key: MY_API_KEY
2
+ heroku_app_prefix: my-prefix-
@@ -0,0 +1,42 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../../lib')
2
+ require 'rubygems'
3
+ require 'bundler/setup'
4
+ require 'rspec/expectations'
5
+ require 'aruba/cucumber'
6
+
7
+ require 'heroku-schemas'
8
+ support_path = File.expand_path('../', __FILE__)
9
+ require support_path + '/lib/configuration'
10
+ require support_path + '/lib/heroku-schemas/test/app'
11
+
12
+ Before do
13
+ @aruba_timeout_seconds = 120 # A long time needed some times
14
+ unset_bundler_env_vars
15
+ end
16
+
17
+ if(ENV['ARUBA_REPORT_DIR'])
18
+ # Override reporting behaviour so we don't document all files, only the ones
19
+ # that have been created after @aruba_report_start (a Time object). This is
20
+ # given a value after the Rails app is generated (see cucumber_rails_steps.rb)
21
+ module Aruba
22
+ module Reporting
23
+ def children(dir)
24
+ children = Dir["#{dir}/*"].sort
25
+
26
+ # include
27
+ children = children.select do |child|
28
+ File.directory?(child) ||
29
+ (@aruba_report_start && File.stat(child).mtime > @aruba_report_start)
30
+ end
31
+
32
+ # exclude
33
+ children = children.reject do |child|
34
+ child =~ /Gemfile/ ||
35
+ child =~ /\.log$/
36
+ end
37
+
38
+ children
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,23 @@
1
+ require 'yaml'
2
+
3
+ class Configuration
4
+ class << self
5
+ def load
6
+ @@config = nil
7
+ io = File.open(File.dirname(__FILE__) + '/../config.yml')
8
+ YAML::load_documents(io) { |doc| @@config = doc }
9
+ raise 'Could not locate a configuration named "config.yml"' unless @@config
10
+ end
11
+
12
+ def [] key
13
+ @@config[key]
14
+ end
15
+
16
+ def []= key, value
17
+ @@config[key] = value
18
+ end
19
+ end
20
+ end
21
+
22
+ Configuration.load
23
+ @configuration = Configuration
@@ -0,0 +1,109 @@
1
+ # Allows for manipulations of a Heroku app and its database in a testing environment
2
+ module HerokuSchemas
3
+ module Test
4
+ class App
5
+ attr_reader :database, :name
6
+
7
+ def initialize(name, database)
8
+ @name = name
9
+ @database = database
10
+ @heroku = Heroku::API.new(:api_key => Configuration['heroku_api_key'])
11
+ @dummy_app_path = File.expand_path('../../../../../spec/dummy-app', __FILE__)
12
+ initialize_app
13
+ end
14
+
15
+ def initialize_app
16
+ if app_exists?
17
+ puts "Resetting app #{name}..."
18
+ reset_app
19
+ else
20
+ puts "Creating app #{name}..."
21
+ create_app
22
+ end
23
+ end
24
+
25
+ def create_app
26
+ begin
27
+ @heroku.post_app('name' => name)
28
+ rescue Heroku::API::Errors::RequestFailed
29
+ end
30
+
31
+ begin
32
+ @heroku.post_addon(name, 'pgbackups:plus')
33
+ rescue Heroku::API::Errors::RequestFailed
34
+ end
35
+
36
+ set_git_remote
37
+ Dir.chdir @dummy_app_path do
38
+ system "git push heroku master"
39
+ end
40
+ add_data_to_schema
41
+ end
42
+
43
+ def set_git_remote
44
+ Dir.chdir @dummy_app_path do
45
+ system "git remote set-url heroku git@heroku.com:#{name}.git"
46
+ end
47
+ end
48
+
49
+ def add_data_to_schema(schema=nil)
50
+ if schema
51
+ original_schema_search_path = database.connection.schema_search_path
52
+ database.connection.schema_search_path = schema
53
+ database.execute("CREATE SCHEMA #{schema}")
54
+ end
55
+ app_database = database
56
+ ActiveRecord::Schema.define do
57
+ @connection = app_database.connection
58
+ create_table "dummy_records", :force => true do |t|
59
+ t.string "name"
60
+ t.datetime "created_at", :null => false
61
+ t.datetime "updated_at", :null => false
62
+ end
63
+ end
64
+ database.execute("INSERT INTO dummy_records (name, created_at, updated_at) VALUES ('#{name}', NOW(), NOW())")
65
+ if schema
66
+ database.connection.schema_search_path = original_schema_search_path
67
+ end
68
+ end
69
+
70
+ def app_exists?
71
+ begin
72
+ app = @heroku.get_app(name)
73
+ rescue Heroku::API::Errors::NotFound
74
+ return false
75
+ end
76
+ app
77
+ end
78
+
79
+ def reset_app
80
+ database_url = reset_app_database_url
81
+ database.connect_to_url(database_url)
82
+ database.existing_schemas.each do |schema|
83
+ database.execute("DROP SCHEMA #{schema} CASCADE")
84
+ end
85
+ database.execute('CREATE SCHEMA public')
86
+ add_data_to_schema
87
+ end
88
+
89
+ # Reset DATABASE_URL to the value in the first HEROKU_POSTGRESQL_$COLOR_URL-style config variable
90
+ def reset_app_database_url
91
+ @heroku.get_config_vars(name).body.each do |key, value|
92
+ if key != 'DATABASE_URL' && key.end_with?('_URL') && value.start_with?('postgres://')
93
+ @heroku.put_config_vars(name, 'DATABASE_URL' => value)
94
+ return value
95
+ end
96
+ end
97
+ end
98
+
99
+ def delete_app
100
+ begin
101
+ app = @heroku.delete_app(name)
102
+ rescue Heroku::API::Errors::NotFound
103
+ return false
104
+ end
105
+ app
106
+ end
107
+ end
108
+ end
109
+ end