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