tern 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/.gitignore +4 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.markdown +189 -0
  4. data/Rakefile +2 -0
  5. data/bin/tern +6 -0
  6. data/lib/tern.rb +207 -0
  7. data/lib/tern/app.rb +65 -0
  8. data/lib/tern/generators/change.sql +4 -0
  9. data/lib/tern/generators/new/alterations/.empty_directory +0 -0
  10. data/lib/tern/generators/new/alterations/001_create_people.sql.example +16 -0
  11. data/lib/tern/generators/new/config.yml.tt +15 -0
  12. data/lib/tern/generators/new/definitions/sequence.yml +18 -0
  13. data/lib/tern/generators/new/definitions/ultimate_answer.sql.example +7 -0
  14. data/spec/projects/alterations/alterations/001_create_people.sql +8 -0
  15. data/spec/projects/alterations/alterations/002_create_animals.sql +8 -0
  16. data/spec/projects/alterations/alterations/003_create_plants.sql +8 -0
  17. data/spec/projects/alterations/config.yml +15 -0
  18. data/spec/projects/alterations/definitions/sequence.yml +18 -0
  19. data/spec/projects/alterations/definitions/ultimate_answer.sql.example +7 -0
  20. data/spec/projects/definitions/alterations/001_create_people.sql.example +16 -0
  21. data/spec/projects/definitions/config.yml +15 -0
  22. data/spec/projects/definitions/definitions/sequence.yml +19 -0
  23. data/spec/projects/definitions/definitions/ultimate_answer.sql +7 -0
  24. data/spec/projects/dependencies_1/alterations/001_create_widgets.sql +9 -0
  25. data/spec/projects/dependencies_1/config.yml +15 -0
  26. data/spec/projects/dependencies_1/definitions/sequence.yml +19 -0
  27. data/spec/projects/dependencies_1/definitions/ultimate_answer.sql.example +7 -0
  28. data/spec/projects/dependencies_1/definitions/widgets_view.sql +5 -0
  29. data/spec/projects/dependencies_2/alterations/001_create_widgets.sql +9 -0
  30. data/spec/projects/dependencies_2/config.yml +15 -0
  31. data/spec/projects/dependencies_2/definitions/sequence.yml +18 -0
  32. data/spec/projects/dependencies_2/definitions/ultimate_answer.sql.example +7 -0
  33. data/spec/projects/duplicated_alteration/alterations/001_create_people.sql +8 -0
  34. data/spec/projects/duplicated_alteration/alterations/002_create_animals.sql +8 -0
  35. data/spec/projects/duplicated_alteration/alterations/002_create_plants.sql +8 -0
  36. data/spec/projects/duplicated_alteration/config.yml +15 -0
  37. data/spec/projects/duplicated_alteration/definitions/sequence.yml +18 -0
  38. data/spec/projects/duplicated_alteration/definitions/ultimate_answer.sql.example +7 -0
  39. data/spec/projects/irreversible_alterations/alterations/001_create_people.sql +8 -0
  40. data/spec/projects/irreversible_alterations/alterations/002_create_animals.sql +4 -0
  41. data/spec/projects/irreversible_alterations/alterations/003_create_plants.sql +8 -0
  42. data/spec/projects/irreversible_alterations/config.yml +15 -0
  43. data/spec/projects/irreversible_alterations/definitions/sequence.yml +18 -0
  44. data/spec/projects/irreversible_alterations/definitions/ultimate_answer.sql.example +7 -0
  45. data/spec/projects/missing_alteration/alterations/001_create_people.sql +8 -0
  46. data/spec/projects/missing_alteration/alterations/003_create_plants.sql +8 -0
  47. data/spec/projects/missing_alteration/config.yml +15 -0
  48. data/spec/projects/missing_alteration/definitions/sequence.yml +18 -0
  49. data/spec/projects/missing_alteration/definitions/ultimate_answer.sql.example +7 -0
  50. data/spec/projects/multiple_sequences/alterations/001_create_people.sql.example +16 -0
  51. data/spec/projects/multiple_sequences/config.yml +15 -0
  52. data/spec/projects/multiple_sequences/definitions/create_view_a.sql +5 -0
  53. data/spec/projects/multiple_sequences/definitions/create_view_b.sql +5 -0
  54. data/spec/projects/multiple_sequences/definitions/sequence.yml +21 -0
  55. data/spec/projects/multiple_sequences/definitions/ultimate_answer.sql.example +7 -0
  56. data/spec/projects/new/alterations/001_create_people.sql.example +16 -0
  57. data/spec/projects/new/config.yml +15 -0
  58. data/spec/projects/new/definitions/sequence.yml +18 -0
  59. data/spec/projects/new/definitions/ultimate_answer.sql.example +7 -0
  60. data/spec/projects/no_alterations/alterations/001_create_people.sql.example +16 -0
  61. data/spec/projects/no_alterations/config.yml +15 -0
  62. data/spec/projects/no_alterations/definitions/sequence.yml +18 -0
  63. data/spec/projects/no_alterations/definitions/ultimate_answer.sql.example +7 -0
  64. data/spec/tern_spec.rb +215 -0
  65. data/tern.gemspec +23 -0
  66. metadata +174 -0
@@ -0,0 +1,4 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Jack Christensen
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.
@@ -0,0 +1,189 @@
1
+ Tern - The SQL Fan's Migrator
2
+ ===============================
3
+
4
+ Tern is designed to simplify migrating database schemas with views, functions,
5
+ triggers, constraints and other objects where altering one may require dropping
6
+ and recreating others. For example, if view A selects from view B which selects
7
+ from table C, then altering C could require five steps: drop A, drop B, alter
8
+ C, recreate B, and recreate A. Tern can be told to alter C and it will
9
+ automatically perform the other four steps.
10
+
11
+ Installation
12
+ ============
13
+
14
+ gem install tern
15
+
16
+ Requirements
17
+ ============
18
+
19
+ Ruby 1.8.7+
20
+
21
+ Tern is built on top of [Sequel][1], so it can run on any database
22
+ [Sequel][1] supports. However, Tern relies on transactional DDL to keep the
23
+ database schema in a consistent state should something go wrong mid-migration.
24
+ [PostgreSQL][2] supports this. MySQL does not. Using Tern on a database
25
+ without transactional DDL is not recommended.
26
+
27
+ Creating a Tern Project
28
+ =======================
29
+
30
+ tern new my_project
31
+
32
+ Edit config.yml and set up your database environments. Database environment
33
+ parameters are passed directly to Sequel.connect so it uses the same
34
+ [options][3].
35
+
36
+ alterations:
37
+ table: tern_alterations
38
+ column: version
39
+ definitions:
40
+ table: tern_definitions
41
+ environments:
42
+ development:
43
+ adapter: postgres
44
+ database: my_project_development
45
+ test:
46
+ adapter: postgres
47
+ database: my_project_test
48
+ production:
49
+ adapter: postgres
50
+ database: my_project_production
51
+
52
+ Migration Types
53
+ ===============
54
+
55
+ Tern divides potential database changes into alterations and definitions. An
56
+ alteration is something that cannot be reversed without potentially losing data.
57
+ For example, table creation is an alteration because dropping a table could
58
+ result in data loss. Alterations correspond directly with the migration style
59
+ popularized by Ruby on Rails. View creation is a definition, because it can be
60
+ dropped without possibility of data loss.
61
+
62
+ Both types of migrations have an extremely simple file format. They are simply
63
+ the create and drop SQL statements divided by a magic comment.
64
+
65
+ ---- CREATE above / DROP below ----
66
+
67
+ Alterations
68
+ -----------
69
+
70
+ Alterations should be used to create, drop, and alter tables and any other
71
+ potentially irreversible migration.
72
+
73
+ tern generate alteration create_people
74
+
75
+ or abbreviate
76
+
77
+ tern g a create_people
78
+
79
+ This will create a numbered alteration in the alterations directory. Simply edit
80
+ the file and place the create code above the magic comment and the drop code
81
+ below it.
82
+
83
+ CREATE TABLE people(
84
+ id serial PRIMARY KEY,
85
+ name varchar NOT NULL
86
+ );
87
+
88
+ ---- CREATE above / DROP below ----
89
+
90
+ DROP TABLE people;
91
+
92
+ If this alteration is irreversible such as a drop table, simply delete the magic
93
+ comment.
94
+
95
+ DROP TABLE widgets;
96
+
97
+ Definitions
98
+ -----------
99
+
100
+ Definitions should be used to specify the desired views, functions, triggers,
101
+ constraints, and other database objects that are reversible without possibility
102
+ of data loss.
103
+
104
+ tern generate definition create_ultimate_answer
105
+
106
+ or abbreviate
107
+
108
+ tern g d create_ultimate_answer
109
+
110
+
111
+ This will create the file create_ultimate_answer.sql in the definitions
112
+ directory. Add the create and drop commands around the magic comment.
113
+
114
+ CREATE FUNCTION ultimate_answer() RETURNS integer AS $$
115
+ SELECT 42;
116
+ $$ LANGUAGE SQL;
117
+
118
+ ---- CREATE above / DROP below ----
119
+
120
+ DROP FUNCTION ultimate_answer();
121
+
122
+ Definitions need to be created in dropped in a particular order. This order is
123
+ defined in the file definitions/sequence.yml. Add this new definition to the
124
+ sequences file.
125
+
126
+ default:
127
+ - ultimate_answer.sql
128
+
129
+ Migrating
130
+ =========
131
+
132
+ tern migrate
133
+
134
+ To run alterations to a specific version:
135
+
136
+ tern migrate --alteration-version=42
137
+
138
+ To migrate a particular database environment:
139
+
140
+ tern migrate --environment=test
141
+
142
+ How it Works
143
+ ============
144
+
145
+ Tern in migrates in three steps.
146
+
147
+ 1. Drop all definitions in the reverse order they were created
148
+ 2. Run alterations
149
+ 3. Create all definitions
150
+
151
+ Tern stores the drop command for definitions in the database when it is first
152
+ created. This allows it to totally reverse all definitions even without the
153
+ original definition files. To make changes to the definitions just change the
154
+ files and rerun tern migrate. Tern will drop all definitions it has previously
155
+ created and create your new definitions.
156
+
157
+ Multiple Definition Sequences
158
+ =============================
159
+
160
+ Definitions such as a check constraint on a table with many rows may be too
161
+ time-consuming to drop and recreate for every migration. Tern allows you to
162
+ define multiple definition sequences in the sequence.yml file.
163
+
164
+ # default:
165
+ # - ultimate_answer.sql
166
+ # - my_first_view.sql
167
+ # - my_second_view.sql
168
+ # expensive:
169
+ # - super_slow_check_constraint.sql
170
+
171
+ Only the default sequence will normally run. To migrate the expensive
172
+ definition sequence use the --definition-sequences option. Note that default
173
+ will not run unless specified when using this option.
174
+
175
+ tern migrate --definition-sequences=expensive
176
+
177
+ Multiple sequences may be specified and they will be run in the order they are
178
+ listed.
179
+
180
+ tern migrate --definition-sequences=expensive default
181
+
182
+ License
183
+ =======
184
+
185
+ Copyright (c) 2011 Jack Christensen, released under the MIT license
186
+
187
+ [1]: http://sequel.rubyforge.org/
188
+ [2]: http://www.postgresql.org/
189
+ [3]: http://sequel.rubyforge.org/rdoc/files/doc/opening_databases_rdoc.html
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'tern'
4
+ require 'tern/app'
5
+
6
+ App.start
@@ -0,0 +1,207 @@
1
+ require 'sequel'
2
+ require 'yaml'
3
+
4
+ class Change
5
+ SPLIT_MARKER = '---- CREATE above / DROP below ----'
6
+
7
+ def self.parse(string)
8
+ create_sql, drop_sql = string.split('---- CREATE above / DROP below ----')
9
+ [create_sql, drop_sql]
10
+ end
11
+ end
12
+
13
+ class Definition < Change
14
+ class << self
15
+ attr_accessor :table_name
16
+
17
+ def table
18
+ DB[table_name]
19
+ end
20
+
21
+ def ensure_table_exists
22
+ DB.create_table? table_name do
23
+ primary_key :id
24
+ column :sequence, :text, :null => false
25
+ column :create_sql, :text, :null => false
26
+ column :drop_sql, :text, :null => false
27
+ end
28
+ end
29
+
30
+ def load_existing
31
+ table.order(:id).all.map do |row|
32
+ new row[:id], row[:sequence], row[:create_sql], row[:drop_sql]
33
+ end.group_by { |d| d.sequence }
34
+ end
35
+
36
+ def load_targets(sequence_yml_path)
37
+ definition_sequences = YAML.load(File.read(sequence_yml_path))
38
+ sequence_dir = File.dirname(sequence_yml_path)
39
+
40
+ definition_sequences.keys.each do |sequence|
41
+ definition_sequences[sequence] = definition_sequences[sequence].map do |f|
42
+ create_sql, drop_sql = parse File.read(File.join(sequence_dir, f))
43
+ Definition.new nil, sequence, create_sql, drop_sql
44
+ end
45
+ end
46
+
47
+ definition_sequences
48
+ end
49
+ end
50
+
51
+ attr_reader :id
52
+ attr_reader :sequence
53
+ attr_reader :create_sql
54
+ attr_reader :drop_sql
55
+
56
+ def initialize(id, sequence, create_sql, drop_sql)
57
+ @id = id
58
+ @sequence = sequence
59
+ @create_sql = create_sql
60
+ @drop_sql = drop_sql
61
+ end
62
+
63
+ def create
64
+ DB.run create_sql
65
+ table.insert :sequence => sequence, :create_sql => create_sql, :drop_sql => drop_sql
66
+ end
67
+
68
+ def drop
69
+ DB.run drop_sql
70
+ table.filter(:id => id).delete
71
+ end
72
+
73
+ def table
74
+ self.class.table
75
+ end
76
+ end
77
+
78
+ class Alteration < Change
79
+ class IrreversibleAlteration < StandardError
80
+ end
81
+
82
+ class MissingAlteration < StandardError
83
+ end
84
+
85
+ class DuplicateAlteration < StandardError
86
+ end
87
+
88
+ class << self
89
+ attr_accessor :table_name
90
+ attr_accessor :version_column
91
+
92
+ def table
93
+ DB[table_name]
94
+ end
95
+
96
+ def ensure_table_exists
97
+ vc = version_column # because create_table? block is run with different binding and can't access version_column
98
+ DB.create_table? table_name do
99
+ column vc, :integer, :null => false
100
+ end
101
+ table.insert version_column => 0
102
+ end
103
+
104
+ def version
105
+ table.get(version_column)
106
+ end
107
+
108
+ def version=(new_version)
109
+ table.update version_column => new_version
110
+ end
111
+
112
+ def load(alterations_path)
113
+ alterations = Dir.glob("#{alterations_path}/[0-9]*.sql").map do |path|
114
+ raise "This can't happen" unless File.basename(path) =~ /^(\d+)/
115
+ version = $1.to_i
116
+ create_sql, drop_sql = parse File.read(path)
117
+ new version, create_sql, drop_sql
118
+ end.sort_by(&:version)
119
+
120
+ alterations.each_with_index do |a, i|
121
+ expected = i+1
122
+ raise DuplicateAlteration, "Alteration #{a.version.to_s.rjust(3, "0")} is duplicated" if a.version < expected
123
+ raise MissingAlteration, "Alteration #{expected.to_s.rjust(3, "0")} is missing" if a.version > expected
124
+ end
125
+
126
+ alterations
127
+ end
128
+ end
129
+
130
+ attr_reader :version
131
+ attr_reader :create_sql
132
+ attr_reader :drop_sql
133
+
134
+ def initialize(version, create_sql, drop_sql)
135
+ @version = version
136
+ @create_sql = create_sql
137
+ @drop_sql = drop_sql
138
+ end
139
+
140
+ def create
141
+ DB.run create_sql
142
+ Alteration.version = version
143
+ end
144
+
145
+ def drop
146
+ raise IrreversibleAlteration, "Alteration #{version.to_s.rjust(3, "0")} is irreversible" unless drop_sql
147
+ DB.run drop_sql
148
+ Alteration.version = version - 1
149
+ end
150
+ end
151
+
152
+ class Tern
153
+ def initialize(alterations_table, alterations_column, definitions_table)
154
+ Alteration.table_name = alterations_table.to_sym
155
+ Alteration.version_column = alterations_column.to_sym
156
+ Alteration.ensure_table_exists
157
+ @alterations = Alteration.load 'alterations'
158
+
159
+ Definition.table_name = definitions_table.to_sym
160
+ Definition.ensure_table_exists
161
+ @existing_definitions = Definition.load_existing
162
+ @target_definitions = Definition.load_targets 'definitions/sequence.yml'
163
+ end
164
+
165
+ def migrate(options={})
166
+ sequences = options[:sequences] || ['default']
167
+ DB.transaction do
168
+ drop_existing_definitions(sequences)
169
+ run_alterations(options[:version])
170
+ create_target_definitions(sequences)
171
+ end
172
+ end
173
+
174
+ private
175
+ def run_alterations(version=nil)
176
+ return if @alterations.empty?
177
+ version ||= @alterations.size
178
+
179
+ if Alteration.version < version
180
+ @alterations[Alteration.version..version].each(&:create)
181
+ elsif
182
+ @alterations[version..(Alteration.version-1)].reverse.each(&:drop)
183
+ end
184
+ end
185
+
186
+ def drop_existing_definitions(sequences)
187
+ sequences.each do |s|
188
+ sequence = @existing_definitions[s]
189
+ if sequence
190
+ sequence.reverse.each do |definition|
191
+ definition.drop
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ def create_target_definitions(sequences)
198
+ sequences.each do |s|
199
+ sequence = @target_definitions[s]
200
+ if sequence
201
+ sequence.each do |definition|
202
+ definition.create
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,65 @@
1
+ require 'thor'
2
+ require 'thor/group'
3
+
4
+ class App < Thor
5
+ include Thor::Actions
6
+
7
+ source_root(File.join(File.dirname(__FILE__), "generators"))
8
+
9
+ attr_accessor :app_name
10
+
11
+ desc "new PATH", "Create a new Tern project"
12
+ def new(path)
13
+ self.app_name = File.basename path
14
+ self.destination_root = path
15
+ directory "new", "."
16
+ end
17
+
18
+ map "m" => "migrate"
19
+ desc "migrate", "Drop definitions, run alterations, then recreate definitions"
20
+ method_option :environment, :type => :string, :desc => "Database environment to load", :default => "development", :aliases => "-e"
21
+ method_option :alteration_version, :type => :numeric, :desc => "Target alteration version", :aliases => "-a"
22
+ method_option :definition_sequences, :type => :array, :desc => "Definition sequences to drop and create", :default => ["default"], :aliases => "-d"
23
+ def migrate
24
+ require 'yaml'
25
+
26
+ unless File.exist?('config.yml')
27
+ say 'This directory does not appear to be a Tern project. config.yml not found.'
28
+ return
29
+ end
30
+ config = YAML.load(File.read('config.yml'))
31
+ ::Kernel.const_set("DB", Sequel.connect(config['environments'][options["environment"]])) # using const_set to avoid dynamic constant assignment error
32
+
33
+ begin
34
+ tern = Tern.new(config['alterations']['table'], config['alterations']['column'], config['definitions']['table'])
35
+ tern.migrate(:version => options["alteration_version"], :sequences => options["definition_sequences"])
36
+ rescue Alteration::IrreversibleAlteration, Alteration::MissingAlteration, Alteration::DuplicateAlteration
37
+ say $!, :red
38
+ end
39
+ end
40
+
41
+ map "g" => "generate"
42
+ desc "generate TYPE NAME", "Generate files"
43
+ def generate(type, name)
44
+ unless File.exist?('config.yml')
45
+ say 'This directory does not appear to be a Tern project. config.yml not found.', :red
46
+ return
47
+ end
48
+
49
+ case type
50
+ when 'a', 'alteration'
51
+ current_version = Dir.entries('alterations').map do |f|
52
+ f =~ /^(\d+)_.*.sql$/ ? $1.to_i : 0
53
+ end.max
54
+ zero_padded_next_version = (current_version+1).to_s.rjust(3, "0")
55
+ file_name = "#{zero_padded_next_version}_#{name}.sql"
56
+ copy_file "change.sql", "alterations/#{file_name}"
57
+ when 'd', 'definition'
58
+ file_name = "#{name}.sql"
59
+ copy_file "change.sql", "definitions/#{file_name}"
60
+ say "Remember to add #{file_name} in your sequence.yml file."
61
+ else
62
+ say "#{type} is not a valid TYPE", :red
63
+ end
64
+ end
65
+ end