prick 0.2.0 → 0.7.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 +4 -4
  2. data/.gitignore +6 -5
  3. data/Gemfile +4 -1
  4. data/TODO +10 -0
  5. data/doc/prick.txt +114 -0
  6. data/exe/prick +328 -402
  7. data/lib/ext/fileutils.rb +18 -0
  8. data/lib/ext/forward_method.rb +18 -0
  9. data/lib/ext/shortest_path.rb +44 -0
  10. data/lib/prick.rb +20 -10
  11. data/lib/prick/branch.rb +254 -0
  12. data/lib/prick/builder.rb +164 -0
  13. data/lib/prick/cache.rb +34 -0
  14. data/lib/prick/command.rb +19 -11
  15. data/lib/prick/constants.rb +122 -48
  16. data/lib/prick/database.rb +28 -20
  17. data/lib/prick/diff.rb +125 -0
  18. data/lib/prick/exceptions.rb +15 -3
  19. data/lib/prick/git.rb +77 -30
  20. data/lib/prick/head.rb +183 -0
  21. data/lib/prick/migration.rb +40 -200
  22. data/lib/prick/program.rb +493 -0
  23. data/lib/prick/project.rb +523 -351
  24. data/lib/prick/rdbms.rb +4 -13
  25. data/lib/prick/schema.rb +16 -90
  26. data/lib/prick/share.rb +64 -0
  27. data/lib/prick/state.rb +192 -0
  28. data/lib/prick/version.rb +62 -29
  29. data/libexec/strip-comments +33 -0
  30. data/make_releases +48 -345
  31. data/make_schema +10 -0
  32. data/prick.gemspec +14 -23
  33. data/share/diff/diff.after-tables.sql +4 -0
  34. data/share/diff/diff.before-tables.sql +4 -0
  35. data/share/diff/diff.tables.sql +8 -0
  36. data/share/migration/diff.tables.sql +8 -0
  37. data/share/migration/features.yml +6 -0
  38. data/share/migration/migrate.sql +3 -0
  39. data/share/migration/migrate.yml +8 -0
  40. data/share/migration/tables.sql +3 -0
  41. data/share/schema/build.yml +14 -0
  42. data/share/schema/schema.sql +5 -0
  43. data/share/schema/schema/build.yml +3 -0
  44. data/share/schema/schema/prick/build.yml +14 -0
  45. data/share/schema/schema/prick/data.sql +7 -0
  46. data/share/schema/schema/prick/schema.sql +5 -0
  47. data/share/{schemas/prick/schema.sql → schema/schema/prick/tables.sql} +2 -5
  48. data/{file → share/schema/schema/public/.keep} +0 -0
  49. data/share/schema/schema/public/build.yml +14 -0
  50. data/share/schema/schema/public/schema.sql +3 -0
  51. data/test_assorted +192 -0
  52. data/test_feature +112 -0
  53. data/test_refactor +34 -0
  54. data/test_single_dev +83 -0
  55. metadata +43 -68
  56. data/lib/prick/build.rb +0 -376
  57. data/lib/prick/migra.rb +0 -22
  58. data/share/schemas/prick/data.sql +0 -8
@@ -0,0 +1,493 @@
1
+
2
+ require "prick.rb"
3
+
4
+ module Prick
5
+ # Implements the command line commands
6
+ class Program
7
+ # Lazy-constructed because Project can only be initialized when the
8
+ # directory structure is present
9
+ def project() @project ||= Project.load end
10
+
11
+ attr_accessor :quiet
12
+ attr_accessor :verbose
13
+
14
+ def initialize(quiet: false, verbose: false)
15
+ @quiet = quiet
16
+ @verbose = verbose
17
+ end
18
+
19
+ # Check if the git repository is clean. Raise an error if not
20
+ def check_clean()
21
+ Git.clean? or raise Error, "Repository is dirty - please commit your changes first"
22
+ end
23
+
24
+ # Create project directory structure
25
+ def init(name, user, directory)
26
+ !Project.exist?(directory) or raise Error, "Directory #{directory} is already initialized"
27
+ Project.create(name, user, directory)
28
+ if name != directory
29
+ mesg "Initialized project #{name} in #{directory}"
30
+ else
31
+ mesg "Initialized project #{name}"
32
+ end
33
+ end
34
+
35
+ def info
36
+ if project.tag?
37
+ puts "At v#{project.version} tag"
38
+ else
39
+ puts "On branch #{project.head.name}, #{project.version}"
40
+ end
41
+ puts " Git is " + (Git.clean? ? "clean" : "dirty")
42
+ bv = project.head.version
43
+ dv = project.database.version
44
+ sv = project.head.schema.version
45
+ puts " Database version: #{dv}" + (dv != bv ? " (mismatch)" : "")
46
+ puts " Schema version : #{sv}" + (sv != bv ? " (mismatch)" : "")
47
+ end
48
+
49
+ def list_releases(migrations: false, cancelled: false)
50
+ raise NotYet
51
+ end
52
+
53
+ def list_migrations
54
+ raise NotYet
55
+ end
56
+
57
+ def list_upgrades(from = nil, to = nil)
58
+ raise NotYet
59
+ end
60
+
61
+ def list_cache
62
+ project.cache.list.each { |l| puts l }
63
+ end
64
+
65
+ def build(database, version, nocache)
66
+ into_mesg = database && "into #{database}"
67
+ version_mesg = version ? "v#{version}" : "current schema"
68
+ version &&= Version.new(version)
69
+ version.nil? || Git.tag?(version) or raise Error, "Can't find tag v#{version}"
70
+ database = database ? Database.new(database, project.user) : project.database(version)
71
+ project.build(database, version: version)
72
+ project.save(database) if version && !nocache
73
+ mesg "Built", version_mesg, into_mesg
74
+ end
75
+
76
+ def make(database, version, nocache)
77
+ version &&= Version.new(version)
78
+ version.nil? || Git.tag?(version) or raise Error, "Can't find tag v#{version}"
79
+ if !nocache && version && project.cache.exist?(version)
80
+ load(database, version)
81
+ else
82
+ build(database, version, nocache)
83
+ end
84
+ end
85
+
86
+ def make_clean(all)
87
+ project.cache.clean.each { |file| mesg "Removed cache file #{File.basename(file)}" }
88
+ drop(nil, all)
89
+ end
90
+
91
+ def load(database, file_or_version)
92
+ check_owned(database) if database
93
+ into_mesg = database && "into #{database}"
94
+ if version = Version.try(file_or_version)
95
+ database = database ? Database.new(database, project.user) : project.database(version)
96
+ project.load(database, version: version)
97
+ mesg "Loaded v#{version}", into_mesg, "from cache"
98
+ else file = file_or_version
99
+ project.load(database, file: file)
100
+ mesg "Loaded #{File.basename(file)}", into_mesg
101
+ end
102
+ end
103
+
104
+ def save(version, file)
105
+ database = project.database(version)
106
+ database.exist? or raise "Can't find database '#{database}'"
107
+ subj_mesg = file ? file : "cache"
108
+ project.save(database, file: file)
109
+ mesg "Saved #{database} to #{subj_mesg}"
110
+ end
111
+
112
+ def drop(database, all)
113
+ database.nil? || !all or raise Error, "Can't use --all when database is given"
114
+ if database
115
+ check_owned(database)
116
+ dbs = [database]
117
+ else
118
+ dbs = Rdbms.list_databases(Prick.database_re(project.name))
119
+ dbs << project.name if all && project.database.exist?
120
+ end
121
+ dbs += Rdbms.list_databases(Prick.tmp_databases_re(project.name)) # FIXME: Only used in dev
122
+ dbs.each { |db|
123
+ Rdbms.drop_database(db)
124
+ mesg "Dropped database #{db}"
125
+ }
126
+ end
127
+
128
+ # `select` is a tri-state variable that can be :tables, :no_tables, or :all
129
+ # (or any other value)
130
+ def diff(from, to, mark, select)
131
+ diff = project.diff(from && Version.new(from), to && Version.new(to))
132
+ if !diff.same?
133
+ if select != :tables && !diff.before_table_changes.empty?
134
+ puts "-- BEFORE TABLE CHANGES" if mark
135
+ puts diff.before_table_changes
136
+ end
137
+ if select != :no_tables && !diff.table_changes.empty?
138
+ puts "-- TABLE CHANGES" if mark
139
+ puts diff.table_changes
140
+ end
141
+ if select != :tables && !diff.after_table_changes.empty?
142
+ puts "-- AFTER TABLE CHANGES" if mark
143
+ puts diff.after_table_changes
144
+ end
145
+ end
146
+ end
147
+
148
+ def migrate
149
+ raise NotYet
150
+ end
151
+
152
+ def prepare_release(fork)
153
+ project.prepare_release(fork)
154
+ end
155
+
156
+ def prepare_feature(name)
157
+ raise NotYet
158
+ end
159
+
160
+ def prepare_migration(from = nil)
161
+ raise NotYet
162
+ end
163
+
164
+ def prepare_schema(name)
165
+ raise NotYet
166
+ end
167
+
168
+ def prepare_diff(version = nil)
169
+ # Helpful check to ensure the user has built the current version
170
+ # Diff.same?(to_db, database) or
171
+ # raise Error, "Schema and project database are not synchronized"
172
+
173
+ project.prepare_diff(version || project.version)
174
+ if FileUtils.compare_file(TABLES_DIFF_PATH, TABLES_DIFF_SHARE_PATH)
175
+ mesg "Created diff. No table changes found, no manual migration updates needed"
176
+ else
177
+ mesg "Created diff. Please inspect #{TABLES_DIFF_PATH} and incorporate the"
178
+ mesg "changes in #{MIGRATION_DIR}/migrate.sql"
179
+ end
180
+ end
181
+
182
+ def include(name)
183
+ raise NotYet
184
+ end
185
+
186
+ def check
187
+ version ||=
188
+ if project.prerelease? || project.migration?
189
+ project.branch.base_version
190
+ else
191
+ project.branch.version
192
+ end
193
+ project.check_migration(version)
194
+ end
195
+
196
+ def create_release(version = nil)
197
+ if project.prerelease_branch?
198
+ raise NotYet
199
+ elsif project.release_branch?
200
+ project.create_release(Version.new(version))
201
+ else
202
+ raise Error, "You need to be on a release or pre-release branch to create a new release"
203
+ end
204
+ mesg "Created release v#{project.head.version}"
205
+ end
206
+
207
+ def create_prerelease(version = nil)
208
+ raise NotYet
209
+ end
210
+
211
+ def create_feature(name)
212
+ raise NotYet
213
+ end
214
+
215
+ def cancel(version)
216
+ raise NotYet
217
+ end
218
+
219
+ def generate_schema
220
+ project.generate_schema
221
+ end
222
+
223
+ def generate_migration
224
+ project.generate_migration
225
+ end
226
+
227
+ def upgrade
228
+ raise NotYet
229
+ end
230
+
231
+ # TODO: Create a Backup class and a Project#backup_store object
232
+ def backup(file = nil)
233
+ file = file || File.join(SPOOL_DIR, "#{project.name}-#{Time.now.utc.strftime("%Y%m%d-%H%M%S")}.sql.gz")
234
+ project.save(file: file)
235
+ mesg "Backed-up database to #{file}"
236
+ end
237
+
238
+ def restore(file_arg = nil)
239
+ file = file_arg || Dir.glob(File.join(SPOOL_DIR, "#{name}-*.sql.gz")).sort.last
240
+ File.exist?(file) or raise Error, "Can't find #{file_arg || "any backup file"}"
241
+ project.load(file: file)
242
+ mesg "Restored database from #{file}"
243
+ end
244
+
245
+ protected
246
+ def check_owned(database)
247
+ database =~ ALL_DATABASES_RE or raise Error, "Not a prick database: #{database}"
248
+ project_name = $1
249
+ project_name == project.name or raise Error, "Database not owned by this prick project: #{database}"
250
+ end
251
+
252
+ def mesg(*args) puts args.compact.grep(/\S/).join(' ') if !quiet end
253
+ def verb(*args) puts args.compact.grep(/\S/).join(' ') if verbose end
254
+ end
255
+ end
256
+
257
+ __END__
258
+
259
+
260
+ module Prick
261
+ class Program
262
+ def project() @project ||= Project.load end
263
+
264
+ attr_accessor :quiet
265
+ attr_accessor :verbose
266
+
267
+ def initialize(quiet: false, verbose: false)
268
+ @quiet = quiet
269
+ @verbose = verbose
270
+ end
271
+
272
+ def mesg(*args) puts args.compact.grep(/\S/).join(' ') if !quiet end
273
+ def verb(*args) puts args.compact.grep(/\S/).join(' ') if verbose end
274
+ def check_clean() Git.clean? or raise Error, "Repository is dirty - please commit your changes first" end
275
+
276
+ def initialize_directory(project_name, database_user, directory)
277
+ !Project.initialized?(directory) or raise Error, "Directory #{directory} is already initialized"
278
+ Project.initialize_directory(project_name, database_user, directory)
279
+ if project_name != File.basename(directory)
280
+ mesg "Initialized project #{project_name} in #{directory}"
281
+ else
282
+ mesg "Initialized project #{project_name}"
283
+ end
284
+ end
285
+
286
+ def info
287
+ if project.tag?
288
+ puts "At v#{project.version} tag"
289
+ else
290
+ puts "On branch #{project.branch.name}"
291
+ end
292
+ puts " Git is " + (Git.clean? ? "clean" : "dirty")
293
+ bv = project.branch.version
294
+ dv = project.database.version
295
+ sv = project.branch.schema.version
296
+ puts " Database version: #{dv}" + (dv != bv ? " (mismatch)" : "")
297
+ puts " Schema version : #{sv}" + (sv != bv ? " (mismatch)" : "")
298
+ end
299
+
300
+ # TODO: Move to project to take advantage of cache
301
+ def build(database, version, no_cache)
302
+ version = version && Version.new(version)
303
+ into_mesg = database && "into #{database}"
304
+ database = database ? Database.new(database, project.user) : project.database(version)
305
+ if version
306
+ Git.tag?(version) or raise Error, "Can't find tag v#{version}"
307
+ cache_file = project.cache_file(version)
308
+ if !no_cache && File.exist?(cache_file)
309
+ project.load(cache_file, database: database)
310
+ mesg "Loaded v#{version}", into_mesg, "from cache"
311
+ else
312
+ project.build(database: database, version: version)
313
+ project.save(cache_file, database: database)
314
+ mesg "Built v#{version}", into_mesg
315
+ end
316
+ else
317
+ project.build(database: database)
318
+ mesg "Built current schema", into_mesg
319
+ end
320
+ end
321
+
322
+ def load(database, file_or_version)
323
+ version = Version.try(file_or_version)
324
+ into_mesg = database && "into #{database}"
325
+ database = database ? Database.new(database, project.user) : project.database(version)
326
+ if version
327
+ file = project.cache_file(version)
328
+ File.exist?(file) or raise Error, "Can't find #{file} - forgot to build?"
329
+ project.load(file, database: database)
330
+ mesg "Loaded v#{version}", into_mesg
331
+ else
332
+ file = file_or_version
333
+ project.load(file, database: database)
334
+ mesg "Loaded #{file}", into_mesg
335
+ end
336
+ end
337
+
338
+ def save(database, file)
339
+ file ||= "#{ENV['USER']}-#{name}-#{branch}.sql.gz"
340
+ subject_mesg = database ? "database #{database}" : "current database"
341
+ database = database ? Database.new(database, project.user) : project.database(version)
342
+ project.save(file, database: database)
343
+ mesg "Saved", subject_mesg, "to #{file}"
344
+ end
345
+
346
+ def make(subject)
347
+ project.database.exist? or raise Error, "Project database is not present"
348
+ project.make(project.database, subject)
349
+ end
350
+
351
+ def list_releases(migrations: false, cancelled: false)
352
+ puts (project.list_releases(all: cancelled) + (migrations ? project.list_migrations : [])).sort.map(&:name)
353
+ end
354
+
355
+ def list_migrations
356
+ puts project.list_migrations.sort.map(&:name)
357
+ end
358
+
359
+ def list_upgrades(from = nil, to = nil)
360
+ from = from ? Version.new(from) : project.database.version
361
+ to = to ? Version.new(to) : project.branch.version
362
+ branches = project.list_upgrades(from, to)
363
+ puts branches.map(&:name)
364
+ end
365
+
366
+ def prepare_schema(name)
367
+ project.prepare_schema(name)
368
+ mesg project.message
369
+ end
370
+
371
+ def prepare_diff(version = nil)
372
+ version ||=
373
+ if project.prerelease? || project.migration? || project.feature?
374
+ project.branch.base_version
375
+ else
376
+ project.branch.version
377
+ end
378
+ project.prepare_diff(version)
379
+ mesg "Remember to update the associated SQL migration files"
380
+ end
381
+
382
+ def prepare_release
383
+ check_clean
384
+ project.version.release? or raise Error, "You need to be on a release branch to prepare a release"
385
+ project.prepare_release
386
+ mesg project.message
387
+ end
388
+
389
+ def check
390
+ version ||=
391
+ if project.prerelease? || project.migration?
392
+ project.branch.base_version
393
+ else
394
+ project.branch.version
395
+ end
396
+ project.check_migration(version)
397
+ end
398
+
399
+ # `arg` can be a version numer of a relative increase (eg. 'minor')
400
+ def create_release(arg = nil)
401
+ check_clean
402
+ if project.release?
403
+ arg or raise Error, "Need a version argument"
404
+ version = compute_version(project.version, arg)
405
+ project.create_release(Version.new(version))
406
+ mesg project.message
407
+ elsif project.prerelease?
408
+ arg.nil? or raise Error, "Illegal number of arguments"
409
+ project.create_release_from_prerelease
410
+ mesg project.message
411
+ else
412
+ raise Error, "You need to be on a release or pre-release branch to create a new release"
413
+ end
414
+ end
415
+
416
+ def cancel_release(arg)
417
+ project.cancel_release(Version.new(arg))
418
+ end
419
+
420
+ def create_prerelease(arg)
421
+ check_clean
422
+ if project.release?
423
+ version = %w(major minor patch).include?(arg) ? project.version.increment(arg.to_sym) : Version.new(arg)
424
+ project.prepare_release(commit: false)
425
+ prerelease = project.create_prerelease(version)
426
+ mesg "Created pre-release #{prerelease.version}"
427
+ elsif project.prerelease?
428
+ arg.nil? or raise Error, "Illegal number of arguments"
429
+ prerelease = project.increment_prerelease
430
+ mesg "Created pre-release #{prerelease.prerelease_version}"
431
+ else
432
+ raise Error, "You need to be on a release branch to create a pre-release"
433
+ end
434
+ end
435
+
436
+ def prepare_migration(arg)
437
+ check_clean
438
+ version = Version.new(arg)
439
+ project.release? or raise "You need to be on a release or migration branch to prepare a migration"
440
+ project.prepare_migration(version)
441
+ mesg project.message
442
+ end
443
+
444
+ def create_feature(name)
445
+ check_clean
446
+ project.release? or raise "You ned to be on a release branch to create a feature"
447
+ project.create_feature(name)
448
+ mesg "Created feature '#{name}'"
449
+ end
450
+
451
+ def include_feature(name_or_version)
452
+ check_clean
453
+ project.prerelease? or raise Error, "You need to be on a pre-release branch to include a feature"
454
+ version = Version.try(name_or_version) ||
455
+ Version.new(project.branch.base_version, feature: name_or_version)
456
+ Git.branch?(version.to_s) or raise Error, "Can't find feature #{version}"
457
+ project.include_feature(version)
458
+ mesg "Included feature '#{name_or_version}'"
459
+ mesg "Please resolve eventual conflicts and then commit"
460
+ end
461
+
462
+ def upgrade
463
+ # TODO: Shutdown connections
464
+ project.database.version != project.version or raise Error, "Database already up to date"
465
+ project.backup
466
+ begin
467
+ project.upgrade
468
+ rescue RuntimeError
469
+ project.restore
470
+ raise Fail, "Failed upgrading database, rolled back to last version"
471
+ end
472
+ end
473
+
474
+ def backup(file = nil) project.backup(file) end
475
+
476
+ def restore(file = nil)
477
+ file.nil? || File.exist?(file) or raise Error, "Can't find #{file}"
478
+ project.restore(file)
479
+ end
480
+
481
+ private
482
+ def compute_version(version, arg)
483
+ if arg.nil?
484
+ nil
485
+ elsif %w(major minor patch).include?(arg)
486
+ version.increment(arg.to_sym)
487
+ else
488
+ Prick::Version.new(arg)
489
+ end
490
+ end
491
+ end
492
+ end
493
+