migration_bundler 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.gitignore +3 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +49 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +122 -0
  8. data/Guardfile +8 -0
  9. data/LICENSE +202 -0
  10. data/README.md +219 -0
  11. data/Rakefile +16 -0
  12. data/bin/mb +6 -0
  13. data/lib/migration_bundler/actions.rb +38 -0
  14. data/lib/migration_bundler/cli.rb +355 -0
  15. data/lib/migration_bundler/databases/abstract_database.rb +54 -0
  16. data/lib/migration_bundler/databases/cassandra_database.rb +88 -0
  17. data/lib/migration_bundler/databases/sqlite_database.rb +116 -0
  18. data/lib/migration_bundler/migrations.rb +52 -0
  19. data/lib/migration_bundler/project.rb +90 -0
  20. data/lib/migration_bundler/targets/base.rb +85 -0
  21. data/lib/migration_bundler/targets/cassandra/cassandra_target.rb +73 -0
  22. data/lib/migration_bundler/targets/cassandra/create_schema_migrations.cql.erb +16 -0
  23. data/lib/migration_bundler/targets/cassandra/migration.cql.erb +0 -0
  24. data/lib/migration_bundler/targets/cocoapods/cocoapods_target.rb +43 -0
  25. data/lib/migration_bundler/targets/cocoapods/podspec.erb +12 -0
  26. data/lib/migration_bundler/targets/maven/maven_target.rb +62 -0
  27. data/lib/migration_bundler/targets/maven/project/.gitignore +6 -0
  28. data/lib/migration_bundler/targets/maven/project/MonkeyButler.iml +19 -0
  29. data/lib/migration_bundler/targets/maven/project/build.gradle +54 -0
  30. data/lib/migration_bundler/targets/sqlite/create_migration_bundler_tables.sql.erb +15 -0
  31. data/lib/migration_bundler/targets/sqlite/migration.sql.erb +11 -0
  32. data/lib/migration_bundler/targets/sqlite/sqlite_target.rb +92 -0
  33. data/lib/migration_bundler/templates/Gemfile.erb +4 -0
  34. data/lib/migration_bundler/templates/gitignore.erb +1 -0
  35. data/lib/migration_bundler/util.rb +71 -0
  36. data/lib/migration_bundler/version.rb +3 -0
  37. data/lib/migration_bundler.rb +1 -0
  38. data/migration_bundler.gemspec +33 -0
  39. data/spec/cli_spec.rb +700 -0
  40. data/spec/databases/cassandra_database_spec.rb +260 -0
  41. data/spec/databases/sqlite_database_spec.rb +198 -0
  42. data/spec/migrations_spec.rb +4 -0
  43. data/spec/project_spec.rb +128 -0
  44. data/spec/sandbox/cassandra/.gitignore +2 -0
  45. data/spec/sandbox/cassandra/.migration_bundler.yml +9 -0
  46. data/spec/sandbox/cassandra/migrations/20140523123443021_create_sandbox.cql.sql +14 -0
  47. data/spec/sandbox/cassandra/sandbox.cql +0 -0
  48. data/spec/sandbox/sqlite/.gitignore +2 -0
  49. data/spec/sandbox/sqlite/.migration_bundler.yml +9 -0
  50. data/spec/sandbox/sqlite/migrations/20140523123443021_create_sandbox.sql +14 -0
  51. data/spec/sandbox/sqlite/sandbox.sql +0 -0
  52. data/spec/spec_helper.rb +103 -0
  53. data/spec/targets/cassandra_target_spec.rb +191 -0
  54. data/spec/targets/cocoapods_target_spec.rb +197 -0
  55. data/spec/targets/maven_target_spec.rb +156 -0
  56. data/spec/targets/sqlite_target_spec.rb +103 -0
  57. data/spec/util_spec.rb +13 -0
  58. metadata +260 -0
@@ -0,0 +1,38 @@
1
+ module MigrationBundler
2
+ module Actions
3
+ # Run a command in git.
4
+ #
5
+ # git :init
6
+ # git add: "this.file that.rb"
7
+ # git add: "onefile.rb", rm: "badfile.cxx"
8
+ def git(commands={})
9
+ if commands.is_a?(Symbol)
10
+ run "git #{commands}"
11
+ else
12
+ commands.each do |cmd, options|
13
+ run "git #{cmd} #{options}"
14
+ end
15
+ end
16
+ end
17
+
18
+ def git_add(*paths)
19
+ inside(destination_root) do
20
+ git add: paths.flatten.join(' ')
21
+ end
22
+ end
23
+
24
+ def truncate_database
25
+ say_status :truncate, database.to_s, :yellow
26
+ database.drop
27
+ end
28
+
29
+ def bundle
30
+ inside(destination_root) { run "bundle" }
31
+ end
32
+
33
+ def unique_tag_for_version(version)
34
+ return version if options['pretend']
35
+ MigrationBundler::Util.unique_tag_for_version(version)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,355 @@
1
+ require 'thor'
2
+ require "open3"
3
+ require 'migration_bundler/project'
4
+ require 'migration_bundler/actions'
5
+ require 'migration_bundler/databases/abstract_database'
6
+ require 'migration_bundler/migrations'
7
+ require 'migration_bundler/util'
8
+ require 'migration_bundler/targets/base'
9
+
10
+ module MigrationBundler
11
+ class CLI < Thor
12
+ include Thor::Actions
13
+ include MigrationBundler::Actions
14
+
15
+ add_runtime_options!
16
+
17
+ # Configures root path for resources (e.g. templates)
18
+ def self.source_root
19
+ File.dirname(__FILE__)
20
+ end
21
+
22
+ attr_reader :project
23
+
24
+ desc 'init [PATH]', 'Initializes a new repository into PATH'
25
+ method_option :name, type: :string, aliases: '-n', desc: "Specify project name"
26
+ method_option :database, type: :string, aliases: '-d', desc: "Specify database path or URL."
27
+ method_option :targets, type: :array, aliases: '-g', default: [], desc: "Specify default code targets."
28
+ method_option :bundler, type: :boolean, aliases: '-b', default: false, desc: "Use Bundler to import MigrationBundler into project."
29
+ method_option :config, type: :hash, aliases: '-c', default: {}, desc: "Specify config variables."
30
+ def init(path)
31
+ if File.exists?(path)
32
+ raise Error, "Cannot create repository: regular file exists at path '#{path}'" unless File.directory?(path)
33
+ raise Error, "Cannot create repository into non-empty path '#{path}'" if File.directory?(path) && Dir.entries(path) != %w{. ..}
34
+ end
35
+ self.destination_root = File.expand_path(path)
36
+ empty_directory('.')
37
+ inside(destination_root) { git init: '-q' }
38
+
39
+ # hydrate the project
40
+ project_name = options['name'] || File.basename(path)
41
+ sanitized_options = options.reject { |k,v| %w{bundler pretend database}.include?(k) }
42
+ sanitized_options[:name] = project_name
43
+ sanitized_options[:database_url] = options[:database] || "sqlite:#{project_name}.sqlite"
44
+ @project = MigrationBundler::Project.set(sanitized_options)
45
+
46
+ # generate_gitignore
47
+ template('templates/gitignore.erb', ".gitignore")
48
+ git_add '.gitignore'
49
+
50
+ # generate_config
51
+ create_file '.migration_bundler.yml', YAML.dump(sanitized_options)
52
+ git_add '.migration_bundler.yml'
53
+
54
+ # generate_gemfile
55
+ if options['bundler']
56
+ template('templates/Gemfile.erb', "Gemfile")
57
+ end
58
+
59
+ # init_targets
60
+ project = MigrationBundler::Project.set(sanitized_options)
61
+ target_options = options.merge('name' => project_name)
62
+ MigrationBundler::Util.target_classes_named(options[:targets]) do |target_class|
63
+ say "Initializing target '#{target_class.name}'..."
64
+ invoke(target_class, :init, [], target_options)
65
+ end
66
+ project.config['db.dump_tables'] = %w{schema_migrations}
67
+ project.save!(destination_root) unless options['pretend']
68
+ git_add '.migration_bundler.yml'
69
+
70
+ # Run after targets in case they modify the Gemfile
71
+ # run_bundler
72
+ if options['bundler']
73
+ git_add "Gemfile"
74
+ bundle
75
+ git_add "Gemfile.lock"
76
+ end
77
+
78
+ # touch_database
79
+ create_file(project.schema_path)
80
+ git_add project.schema_path
81
+
82
+ # init_database_adapter
83
+ empty_directory('migrations')
84
+ inside do
85
+ invoke(project.database_target_class, :init, [], target_options)
86
+ end
87
+ end
88
+
89
+ desc "dump", "Dump project schema from a database"
90
+ def dump
91
+ @project = MigrationBundler::Project.load
92
+ invoke(project.database_target_class, :dump, [], options)
93
+ end
94
+
95
+ desc "load", "Load project schema into a database"
96
+ def load
97
+ @project = MigrationBundler::Project.load
98
+ invoke(project.database_target_class, :load, [], options)
99
+ end
100
+
101
+ desc "drop", "Drop the schema currently loaded into a database"
102
+ def drop
103
+ @project = MigrationBundler::Project.load
104
+ invoke(project.database_target_class, :drop, [], options)
105
+ end
106
+
107
+ desc "new NAME", "Create a new migration"
108
+ def new(name)
109
+ @project = MigrationBundler::Project.load
110
+ empty_directory('migrations')
111
+ invoke(project.database_target_class, :new, [name], options)
112
+ end
113
+
114
+ desc "status", "Display current schema version and any pending migrations"
115
+ method_option :database, type: :string, aliases: '-d', desc: "Set target DATABASE"
116
+ def status
117
+ project = MigrationBundler::Project.load
118
+ migrations = MigrationBundler::Migrations.new(project.migrations_path, database)
119
+
120
+ if database.migrations_table?
121
+ say "Current version: #{migrations.current_version}"
122
+ pending_count = migrations.pending.size
123
+ version = (pending_count == 1) ? "version" : "versions"
124
+ say "The database at '#{database}' is #{pending_count} #{version} behind #{migrations.latest_version}" unless migrations.up_to_date?
125
+ else
126
+ say "New database"
127
+ say "The database at '#{database}' does not have a 'schema_migrations' table."
128
+ end
129
+
130
+ if migrations.up_to_date?
131
+ say "Database is up to date."
132
+ return
133
+ end
134
+
135
+ say
136
+ say "Migrations to be applied"
137
+ with_padding do
138
+ say %q{(use "mb migrate" to apply)}
139
+ say
140
+ with_padding do
141
+ migrations.pending do |version, path|
142
+ say "pending migration: #{path}", :green
143
+ end
144
+ end
145
+ say
146
+ end
147
+ end
148
+
149
+ desc "migrate [VERSION]", "Apply pending migrations to a database"
150
+ method_option :database, type: :string, aliases: '-d', desc: "Set target DATABASE"
151
+ method_option :dump, type: :boolean, aliases: '-D', desc: "Dump schema after migrate"
152
+ def migrate(version = nil)
153
+ project = MigrationBundler::Project.load
154
+
155
+ if migrations.up_to_date?
156
+ say "Database is up to date."
157
+ return
158
+ end
159
+
160
+ target_version = version || migrations.latest_version
161
+ if database.migrations_table?
162
+ say "Migrating from #{database.current_version} to #{target_version}"
163
+ else
164
+ say "Migrating new database to #{target_version}"
165
+ end
166
+ say
167
+
168
+ with_padding do
169
+ say "Migrating database..."
170
+ say
171
+ with_padding do
172
+ migrations.pending do |version, path|
173
+ say "applying migration: #{path}", :green
174
+ begin
175
+ database.execute_migration(File.read(path))
176
+ database.insert_version(version)
177
+ rescue project.database_class.exception_class => exception
178
+ fail Error, "Failed loading migration: #{exception}"
179
+ end
180
+ end
181
+ end
182
+ say
183
+ end
184
+
185
+ say "Migration to version #{target_version} complete."
186
+
187
+ if options['dump']
188
+ say
189
+ invoke :dump, [], options
190
+ end
191
+ end
192
+
193
+ desc "validate", "Validate that schema loads and all migrations are linearly applicable"
194
+ def validate
195
+ project = MigrationBundler::Project.load
196
+
197
+ say "Validating project configuration..."
198
+ say_status :git, "configuration", (project.git_url.empty? ? :red : :green)
199
+ if project.git_url.empty?
200
+ fail Error, "Invalid configuration: git does not have a remote named 'origin'."
201
+ end
202
+ say
203
+
204
+ invoke(project.database_target_class, :validate, [], options)
205
+
206
+ say "Validating schema loads..."
207
+ truncate_database
208
+ load
209
+ say
210
+
211
+ say "Validating migrations apply..."
212
+ truncate_database
213
+ migrate
214
+ say
215
+
216
+ say "Validating targets..."
217
+ target_names = options['targets'] || project.targets
218
+ MigrationBundler::Util.target_classes_named(target_names) do |target_class|
219
+ with_padding do
220
+ say_status :validate, target_class.name
221
+ invoke_with_padding(target_class, :validate, [], options)
222
+ end
223
+ end
224
+ say
225
+
226
+ say "Validation successful."
227
+ end
228
+
229
+ desc "generate", "Generate platform specific migration implementations"
230
+ method_option :targets, type: :array, aliases: '-t', desc: "Generate only the specified targets."
231
+ def generate
232
+ project = MigrationBundler::Project.load
233
+ invoke(project.database_target_class, :generate, [], options)
234
+ target_names = options['targets'] || project.targets
235
+ MigrationBundler::Util.target_classes_named(target_names) do |target_class|
236
+ say "Invoking target '#{target_class.name}'..."
237
+ invoke(target_class, :generate, [], options)
238
+ end
239
+ end
240
+
241
+ desc "package", "Package a release by validating, generating, and tagging a version."
242
+ method_option :diff, type: :boolean, desc: "Show Git diff after packaging"
243
+ method_option :commit, type: :boolean, desc: "Commit package artifacts after build."
244
+ def package
245
+ validate
246
+ say
247
+ generate
248
+ say
249
+
250
+ git_add '.'
251
+ git :status unless options['quiet']
252
+
253
+ show_diff = options['diff'] != false && (options['diff'] || ask("Review package diff?", limited_to: %w{y n}) == 'y')
254
+ git diff: '--cached' if show_diff
255
+
256
+ commit = options['commit'] != false && (options['commit'] || ask("Commit package artifacts?", limited_to: %w{y n}) == 'y')
257
+ if commit
258
+ tag = unique_tag_for_version(migrations.latest_version)
259
+ git commit: "#{options['quiet'] && '-q '}-m 'Packaging release #{tag}' ."
260
+ git tag: "#{tag}"
261
+ else
262
+ say "Package artifacts were built but not committed. Re-run `mb package` when ready to complete build."
263
+ end
264
+ end
265
+
266
+ desc "push", "Push a release to Git, CocoaPods, Maven, etc."
267
+ def push
268
+ # Verify that the tag exists
269
+ git tag: "-l #{migrations.latest_version}"
270
+ unless $?.exitstatus.zero?
271
+ fail Error, "Could not find tag #{migrations.latest_version}. Did you forget to run `mb package`?"
272
+ end
273
+ push_options = []
274
+ push_options << '--force' if options['force']
275
+ branch_name = project.git_current_branch
276
+ run "git config branch.`git symbolic-ref --short HEAD`.merge", verbose: false
277
+ unless $?.exitstatus.zero?
278
+ say_status :git, "no merge branch detected: setting upstream during push", :yellow
279
+ push_options << "--set-upstream origin #{branch_name}"
280
+ end
281
+ push_options << "origin #{branch_name}"
282
+ push_options << "--tags"
283
+
284
+ git push: push_options.join(' ')
285
+ unless $?.exitstatus.zero?
286
+ fail Error, "git push failed."
287
+ end
288
+
289
+ # Give the targets a chance to push
290
+ target_names = options['targets'] || project.targets
291
+ MigrationBundler::Util.target_classes_named(target_names) do |target_class|
292
+ say "Invoking target '#{target_class.name}'..."
293
+ invoke(target_class, :push, [], options)
294
+ end
295
+ end
296
+
297
+ desc "config", "Get and set configuration options."
298
+ def config(key = nil, value = nil)
299
+ if key && value
300
+ project.config[key] = value
301
+ project.save!(Dir.pwd)
302
+ elsif key
303
+ value = project.config[key]
304
+ if value
305
+ say "#{key}=#{value}"
306
+ else
307
+ say "No value for key '#{key}'"
308
+ end
309
+ else
310
+ project.config.each { |key, value| say "#{key}=#{value}" }
311
+ end
312
+ end
313
+
314
+ # Hook into the command execution for dynamic task configuration
315
+ def self.start(given_args = ARGV, config = {})
316
+ if File.exists?(Dir.pwd + '/.migration_bundler.yml')
317
+ project = MigrationBundler::Project.load
318
+ project.database_target_class.register_with_cli(self)
319
+ end
320
+ super
321
+ end
322
+
323
+ private
324
+ def unique_tag_for_version(version)
325
+ return version if options['pretend']
326
+
327
+ revision = nil
328
+ tag = nil
329
+ begin
330
+ tag = [version, revision].compact.join('.')
331
+ existing_tag = run "git tag -l #{tag}", capture: true
332
+ break if existing_tag == ""
333
+ revision = revision.to_i + 1
334
+ end while true
335
+ tag
336
+ end
337
+
338
+ private
339
+ def bundle
340
+ inside(destination_root) { run "bundle" }
341
+ end
342
+
343
+ def project
344
+ @project ||= MigrationBundler::Project.load
345
+ end
346
+
347
+ def database
348
+ @database ||= project.database_class.new((options[:database] && URI(options[:database])) || project.database_url)
349
+ end
350
+
351
+ def migrations
352
+ @migrations ||= MigrationBundler::Migrations.new(project.migrations_path, database)
353
+ end
354
+ end
355
+ end
@@ -0,0 +1,54 @@
1
+ module MigrationBundler
2
+ module Databases
3
+ class AbstractDatabase
4
+ class << self
5
+ def migration_ext
6
+ raise NotImplementedError, "Required method not implemented."
7
+ end
8
+ end
9
+
10
+ def migrations_table?
11
+ raise NotImplementedError, "Required method not implemented."
12
+ end
13
+
14
+ def origin_version
15
+ raise NotImplementedError, "Required method not implemented."
16
+ end
17
+
18
+ def current_version
19
+ raise NotImplementedError, "Required method not implemented."
20
+ end
21
+
22
+ def all_versions
23
+ raise NotImplementedError, "Required method not implemented."
24
+ end
25
+
26
+ def insert_version(version)
27
+ raise NotImplementedError, "Required method not implemented."
28
+ end
29
+
30
+ def execute_migration(content)
31
+ raise NotImplementedError, "Required method not implemented."
32
+ end
33
+
34
+ def drop
35
+ raise NotImplementedError, "Required method not implemented."
36
+ end
37
+
38
+ # Dumps the specified table into SQL
39
+ def dump_rows(table_name)
40
+ raise NotImplementedError, "Required method not implemented."
41
+ end
42
+
43
+ attr_reader :url
44
+
45
+ def initialize(url)
46
+ @url = url
47
+ end
48
+
49
+ def to_s
50
+ url.to_s
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,88 @@
1
+ require 'uri'
2
+ require 'cql'
3
+
4
+ module MigrationBundler
5
+ module Databases
6
+ class CassandraDatabase < AbstractDatabase
7
+ attr_reader :client, :keyspace
8
+
9
+ class << self
10
+ def migration_ext
11
+ ".cql"
12
+ end
13
+
14
+ def exception_class
15
+ Cql::CqlError
16
+ end
17
+ end
18
+
19
+ def initialize(url)
20
+ super(url)
21
+ options = { host: url.host, port: (url.port || 9042) }
22
+ @client = Cql::Client.connect(options)
23
+ @keyspace = url.path[1..-1] # Drop leading slash
24
+ end
25
+
26
+ def migrations_table?
27
+ client.use('system')
28
+ rows = client.execute "SELECT columnfamily_name FROM schema_columnfamilies WHERE keyspace_name='#{keyspace}' AND columnfamily_name='schema_migrations'"
29
+ !rows.empty?
30
+ end
31
+
32
+ def origin_version
33
+ client.use(keyspace)
34
+ rows = client.execute("SELECT version FROM schema_migrations WHERE partition_key = 0 ORDER BY version ASC LIMIT 1")
35
+ rows.empty? ? nil : rows.each.first['version']
36
+ end
37
+
38
+ def current_version
39
+ client.use(keyspace)
40
+ rows = client.execute("SELECT version FROM schema_migrations WHERE partition_key = 0 ORDER BY version DESC LIMIT 1")
41
+ rows.empty? ? nil : rows.each.first['version']
42
+ end
43
+
44
+ def all_versions
45
+ client.use(keyspace)
46
+ rows = client.execute("SELECT version FROM schema_migrations WHERE partition_key = 0 ORDER BY version ASC")
47
+ rows.each.map { |row| row['version'] }
48
+ end
49
+
50
+ def insert_version(version)
51
+ client.use(keyspace)
52
+ client.execute "INSERT INTO schema_migrations (partition_key, version) VALUES (0, ?)", version
53
+ end
54
+
55
+ def execute_migration(cql)
56
+ cql.split(';').each { |statement| client.execute(statement) unless statement.strip.empty? }
57
+ end
58
+
59
+ def drop(keyspaces = [keyspace])
60
+ keyspaces.each { |keyspace| client.execute "DROP KEYSPACE IF EXISTS #{keyspace}" }
61
+ end
62
+
63
+ def create_migrations_table
64
+ client.execute "CREATE KEYSPACE IF NOT EXISTS #{keyspace} WITH replication = {'class' : 'SimpleStrategy', 'replication_factor' : 1};"
65
+ client.execute "CREATE TABLE IF NOT EXISTS #{keyspace}.schema_migrations (partition_key INT, version VARINT, PRIMARY KEY (partition_key, version));"
66
+ end
67
+
68
+ def dump_rows(table_name)
69
+ client.use(keyspace)
70
+ rows = client.execute "SELECT * FROM #{table_name}"
71
+ columns = Array.new.tap do |columns|
72
+ rows.metadata.each do |column_metadata|
73
+ columns << column_metadata.column_name
74
+ end
75
+ end
76
+ Array.new.tap do |statements|
77
+ rows.each do |row|
78
+ values = columns.map do |column|
79
+ value = row[column]
80
+ value.is_a?(String) ? "\"#{value}\"" : value
81
+ end
82
+ statements << "INSERT INTO #{table_name} (#{columns.join(', ')}) VALUES (#{values.join(', ')});"
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,116 @@
1
+ require 'sqlite3'
2
+ require 'uri'
3
+
4
+ module MigrationBundler
5
+ module Databases
6
+ class SqliteDatabase < AbstractDatabase
7
+ attr_reader :db, :path
8
+
9
+ class << self
10
+ def create_schema_migrations_sql
11
+ MigrationBundler::Util.strip_leading_whitespace <<-SQL
12
+ CREATE TABLE schema_migrations(
13
+ version INTEGER UNIQUE NOT NULL
14
+ );
15
+ SQL
16
+ end
17
+
18
+ def create(url)
19
+ new(url) do |database|
20
+ database.db.execute(create_schema_migrations_sql)
21
+ end
22
+ end
23
+
24
+ def migration_ext
25
+ ".sql"
26
+ end
27
+
28
+ def exception_class
29
+ SQLite3::Exception
30
+ end
31
+ end
32
+
33
+ def initialize(url)
34
+ super(url)
35
+ raise ArgumentError, "Must initialize with a URI" unless url.kind_of?(URI)
36
+ raise ArgumentError, "Must initialize with a sqlite URI" unless url.scheme.nil? || url.scheme == 'sqlite'
37
+ path = url.path || url.opaque
38
+ raise ArgumentError, "Must initialize with a sqlite URI that has a path component" unless path
39
+ @path = path
40
+ @db = SQLite3::Database.new(path)
41
+ yield self if block_given?
42
+ end
43
+
44
+ def migrations_table?
45
+ has_table?('schema_migrations')
46
+ end
47
+
48
+ def origin_version
49
+ db.get_first_value('SELECT MIN(version) FROM schema_migrations')
50
+ end
51
+
52
+ def current_version
53
+ db.get_first_value('SELECT MAX(version) FROM schema_migrations')
54
+ end
55
+
56
+ def all_versions
57
+ db.execute('SELECT version FROM schema_migrations ORDER BY version ASC').map { |row| row[0] }
58
+ end
59
+
60
+ def insert_version(version)
61
+ db.execute("INSERT INTO schema_migrations(version) VALUES (?)", version)
62
+ end
63
+
64
+ def execute_migration(sql)
65
+ db.transaction do |db|
66
+ db.execute_batch(sql)
67
+ end
68
+ end
69
+
70
+ def drop
71
+ File.truncate(path, 0) if File.size?(path)
72
+ end
73
+
74
+ def to_s
75
+ path
76
+ end
77
+
78
+ def dump_rows(table_name)
79
+ statement = db.prepare("SELECT * FROM #{table_name}")
80
+ result_set = statement.execute
81
+ Array.new.tap do |statements|
82
+ result_set.each do |row|
83
+ values = row.map { |v| v.is_a?(String) ? SQLite3::Database.quote(v) : v }
84
+ statements << "INSERT INTO #{table_name} (#{result_set.columns.join(', ')}) VALUES (#{values.join(', ')});"
85
+ end
86
+ end
87
+ end
88
+
89
+ ####
90
+ # Outside of abstract interface...
91
+
92
+ def dump_to_schema(type, schema_path)
93
+ sql = MigrationBundler::Util.strip_leading_whitespace <<-SQL
94
+ SELECT name, sql
95
+ FROM sqlite_master
96
+ WHERE sql NOT NULL AND type = '#{type}'
97
+ ORDER BY name ASC
98
+ SQL
99
+ File.open(schema_path, 'a') do |f|
100
+ db.execute(sql) do |row|
101
+ name, sql = row
102
+ next if name =~ /^sqlite/
103
+ f << "#{sql};\n\n"
104
+ yield name if block_given?
105
+ end
106
+ f.puts
107
+ end
108
+ end
109
+
110
+ private
111
+ def has_table?(table)
112
+ db.get_first_value("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table)
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,52 @@
1
+ module MigrationBundler
2
+ class Migrations
3
+ attr_reader :path, :database
4
+
5
+ def initialize(path, database)
6
+ @path = path
7
+ @database = database
8
+ migration_paths = Dir.glob(File.join(path, "*#{database.class.migration_ext}"))
9
+ @paths_by_version = MigrationBundler::Util.migrations_by_version(migration_paths)
10
+ end
11
+
12
+ def current_version
13
+ database.migrations_table? ? database.current_version : nil
14
+ end
15
+
16
+ def all_versions
17
+ @paths_by_version.keys
18
+ end
19
+
20
+ def latest_version
21
+ all_versions.max
22
+ end
23
+
24
+ def applied_versions
25
+ database.migrations_table? ? database.all_versions : []
26
+ end
27
+
28
+ def pending_versions
29
+ (all_versions - applied_versions).sort
30
+ end
31
+
32
+ def pending(&block)
33
+ pending_versions.inject({}) { |hash, v| hash[v] = self[v]; hash }.tap do |hash|
34
+ hash.each(&block) if block_given?
35
+ end
36
+ end
37
+
38
+ def all(&block)
39
+ all_versions.inject({}) { |hash, v| hash[v] = self[v]; hash }.tap do |hash|
40
+ hash.each(&block) if block_given?
41
+ end
42
+ end
43
+
44
+ def up_to_date?
45
+ pending_versions.empty?
46
+ end
47
+
48
+ def [](version)
49
+ @paths_by_version[version]
50
+ end
51
+ end
52
+ end