migration_bundler 1.3.0

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.
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