prick 0.2.0 → 0.7.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 +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
+