migration_bundler 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.gitignore +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +49 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +122 -0
- data/Guardfile +8 -0
- data/LICENSE +202 -0
- data/README.md +219 -0
- data/Rakefile +16 -0
- data/bin/mb +6 -0
- data/lib/migration_bundler/actions.rb +38 -0
- data/lib/migration_bundler/cli.rb +355 -0
- data/lib/migration_bundler/databases/abstract_database.rb +54 -0
- data/lib/migration_bundler/databases/cassandra_database.rb +88 -0
- data/lib/migration_bundler/databases/sqlite_database.rb +116 -0
- data/lib/migration_bundler/migrations.rb +52 -0
- data/lib/migration_bundler/project.rb +90 -0
- data/lib/migration_bundler/targets/base.rb +85 -0
- data/lib/migration_bundler/targets/cassandra/cassandra_target.rb +73 -0
- data/lib/migration_bundler/targets/cassandra/create_schema_migrations.cql.erb +16 -0
- data/lib/migration_bundler/targets/cassandra/migration.cql.erb +0 -0
- data/lib/migration_bundler/targets/cocoapods/cocoapods_target.rb +43 -0
- data/lib/migration_bundler/targets/cocoapods/podspec.erb +12 -0
- data/lib/migration_bundler/targets/maven/maven_target.rb +62 -0
- data/lib/migration_bundler/targets/maven/project/.gitignore +6 -0
- data/lib/migration_bundler/targets/maven/project/MonkeyButler.iml +19 -0
- data/lib/migration_bundler/targets/maven/project/build.gradle +54 -0
- data/lib/migration_bundler/targets/sqlite/create_migration_bundler_tables.sql.erb +15 -0
- data/lib/migration_bundler/targets/sqlite/migration.sql.erb +11 -0
- data/lib/migration_bundler/targets/sqlite/sqlite_target.rb +92 -0
- data/lib/migration_bundler/templates/Gemfile.erb +4 -0
- data/lib/migration_bundler/templates/gitignore.erb +1 -0
- data/lib/migration_bundler/util.rb +71 -0
- data/lib/migration_bundler/version.rb +3 -0
- data/lib/migration_bundler.rb +1 -0
- data/migration_bundler.gemspec +33 -0
- data/spec/cli_spec.rb +700 -0
- data/spec/databases/cassandra_database_spec.rb +260 -0
- data/spec/databases/sqlite_database_spec.rb +198 -0
- data/spec/migrations_spec.rb +4 -0
- data/spec/project_spec.rb +128 -0
- data/spec/sandbox/cassandra/.gitignore +2 -0
- data/spec/sandbox/cassandra/.migration_bundler.yml +9 -0
- data/spec/sandbox/cassandra/migrations/20140523123443021_create_sandbox.cql.sql +14 -0
- data/spec/sandbox/cassandra/sandbox.cql +0 -0
- data/spec/sandbox/sqlite/.gitignore +2 -0
- data/spec/sandbox/sqlite/.migration_bundler.yml +9 -0
- data/spec/sandbox/sqlite/migrations/20140523123443021_create_sandbox.sql +14 -0
- data/spec/sandbox/sqlite/sandbox.sql +0 -0
- data/spec/spec_helper.rb +103 -0
- data/spec/targets/cassandra_target_spec.rb +191 -0
- data/spec/targets/cocoapods_target_spec.rb +197 -0
- data/spec/targets/maven_target_spec.rb +156 -0
- data/spec/targets/sqlite_target_spec.rb +103 -0
- data/spec/util_spec.rb +13 -0
- 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
|